Project

使用ESP8266+RC522+MG995低成本改造宿舍门禁支持NFC刷卡与远程控制

by Mion, 2021-09-08


这是一个从刚入学就挖下的坑,宿舍老旧的门锁与会捉迷藏的钥匙总把我锁在外面,室友有时也没有那么可靠。于是准备把门锁做非破坏性的改造,支持上手机控制。后来又考虑到需要在断网的时候也能够正常访问,又加入了NFC刷卡开门。由于东西仅限自娱自乐且宿舍楼道有完备的安防监控,本项目没有准备任何访问控制。以下是实现的过程。

本项目源码已打包至https://github.com/MoeMion/Magic_Door/.

材料准备

  • ESP8266开发板

    整个系统的控制中枢,支持Wi-Fi连接并且附带丰富的引脚,常用于物联网开发。在这里我选择了CH340处理器的版本,并在Arduino IDE上进行编程开发。(关于Arduino IDE与CH340驱动的问题可以通过搜索引擎快速解决)

    价格:13 元


ESP8266


ESP8266_GPIO

附ESP8266引脚定义图示.注意,D0(GPIO16) 仅能用于GPIO读写,不支持PWM/I2C/OW信号传输。

  • MF RC522 模块

    负责NFC通信。由于需求简单,这里选择的版本不支持加密卡读写。

    价格:12 元


RC522

  • MG995舵机

    用来拉动机械门闩。

    价格:15元

    注意:MG995电机有多个版本,请选购180°带限位版本,360°版本仅能通过PWM信号控制旋转时间,不能控制具体角度!由于舵机不正确使用易烧坏,请小心使用!


MG995

引脚解释:

棕 : GND

红 : VCC (5V或3.3V均可,实测5V转动速度更快)

黄: PWM信号

  • MH-FMD 无源蜂鸣器

    用于输出声音提醒。

    价格:2元


MH-FMD.jpg

  • 杜邦线若干、热熔胶枪

    连接各部分传感器。主要用到了公对母与母对母两种。

MG995舵机控制

装置的核心功能,需要将舵机物理连接到门闩。

实现目标

  • 控制舵机旋转一定角度拉动机械结构开门,并在一定时间后复位。
  • 开机时自动复位。
  • 防止在开门时间内重复动作。
  • 防止delay()函数带来的进程阻塞。

接线

ESP8266 MG995
VU VCC
GND(临近VU) GND
D3(GPIO0) PWM

参考代码

为实现防止进程阻塞,这里使用CxgJSTime库,GitHub Repo:https://github.com/chengxg/cxg-arduino-lib/tree/master/CxgJSTime

以下的代码仅为精简过后的该部分功能实现代码,仅供参考。

#include <Arduino.h>
#include <Servo.h>
#include <cxg_JSTime.h>

Servo servo; //初始化舵机对象
JSTime jsTime; //初始化jstime对象
int servoStatus = 0; //标记舵机状态,0为放下1为拉起

void setup() {
  servo.attach(0, 500, 2500); //第一个坑,参数:PWM对应的GPIO接口,最小PWM信号,最大PWM信号,不同舵机参数不一样,默认参数不适用于本舵机。搜索引擎上大部分文档不会给出后两个参数!
  servo.write(1); //参数为旋转的角度,通电后将电机复位,为0时偶尔会造成舵机卡死
}

void opendoor() {
  if(servoStatus){
      return;
  }
  servoStatus = 1; //标记舵机升起
  servo.write(90);//舵机旋转90度,拉起门栓。实测90度够用,若长度不够可以改成180度
  buzzersuccess(); //控制蜂鸣器播放成功提示音
  int timeId = jsTime.setTimeout([]() {
    servo.write(1); //复位舵机
    servoStatus = 0; //标记舵机放下
  }, 10000); //延迟10秒,舵机复位,放下门栓
}
void loop() {
  jsTime.refresh(); //刷新jsTime对象
}

Web控制

通过无线网络接入来控制舵机开关。

实现目标

  • 使用ESP8266WiFi库接入无线局域网。
  • 使用ESP8266WebServer库实现WebServer以相应请求。
  • 实现开门、心跳检测、重启、关闭面板LED灯等操作。

网络接入

ESP8266模块仅支持2.4G的Wi-Fi连接。此外在连接到无线路由器后需要绑定IP地址。若需要外网访问的话还得进行内网穿透。在这里我使用部署在宿舍的树莓派进行转发。

参考代码

#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>

ESP8266WebServer web(80); //初始化web对象

void setup(){
  //连接网络

  WiFi.begin("SSID", "PASSWORD");
  while (WiFi.status() != WL_CONNECTED)
  {
    Serial.println("Scan wifi...");
    delay(1000);
  }
  Serial.println('\n');
  Serial.print("Connected to ");
  Serial.println(WiFi.SSID()); // WiFi连接成功后通过串口监视器输出连接的WiFI名称
  Serial.print("IP address:\t");
  Serial.println(WiFi.localIP()); // WiFi连接成功后通过串口监视器输出IP地址

  //初始化WEB服务
  web.on("/", handleindex); //默认页面
  web.on("/check", handleHeartbeat); //心跳检测
  web.on("/open", handleOpenDoor); //开门
  web.on("/restart", handleRestart); //重启
  web.on("/ledoff", handleLedoff); //关闭面板LED灯
  web.onNotFound(handleNotFound); //处理404请求
  web.begin();
  Serial.println("Webserver begain.");
}

void loop() {
  web.handleClient();
}

void handleOpenDoor() {
  if(servoStatus == 0){
  web.send(200, "text/plain", "1"); //舵机成功升起并返回1
  opendoor();
  }else{
  web.send(200, "text/plain", "2"); //若舵机已升起则返回2
  }
}

void handleHeartbeat() {
  web.send(200, "text/plain", "1");
}

void handleindex() {
  web.send(200, "text/plain", "This is a unsmart door locker by Mion!");
}

void handleNotFound() {
  web.send(404, "text/plain", "Oops!Page is missing!");
}

void handleRestart() {
  web.send(200, "text/plain", "1");
  ESP.restart();
}

void handleLedoff() {
  web.send(200, "text/plain", "1");
  led_off(); //关闭ESP8266面板上的LED灯,此部分代码在之后给出
}

Web客户端

上文实现了Web控制及响应,但总不能每次都通过访问URL这样不友好的方式开门,于是通过Web与PWA实现了客户端。PWA实现方式详见:为网站增加PWA支持实现原生应用体验

实现目标

  • 点击按钮即可开门
  • 实时刷新门禁在线状态
  • 使用PWA将Web添加至桌面
  • 门锁已开启时给出提示
  • 美观的界面

演示


WebClient

参考代码

这里仅给出部分代码,与PWA相关文件请参考上述文章进行配置。页面使用Argon Design Sytem进行构建。

index.html

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <title>MagicDoor | Powered By Mion</title>
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700">
  <link rel="stylesheet" href="assets/vendor/nucleo/css/nucleo.css" type="text/css">
  <link rel="stylesheet" href="assets/vendor/@fortawesome/fontawesome-free/css/all.min.css" type="text/css">
  <link rel="stylesheet" href="assets/css/argon.css?v=1.1.0" type="text/css">
  <script src="index.js" defer></script>
  <link rel="manifest" href="manifest.webmanifest">
  <link rel="shortcut icon" href="favicon.ico">
  <link
    rel="shortcut icon"
    href="icon.png"
    type="image/x-icon"
  />
  <link rel="apple-touch-icon" href="icon.png" />
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-title" content="芝麻开门">
</head>

<body class="bg-default">
  <!-- Main content -->
  <div class="main-content">
    <!-- Header -->
    <div class="header bg-gradient-primary py-7 py-lg-8 pt-lg-9">
      <div class="container">
        <div class="header-body text-center mb-7">
          <div class="row justify-content-center">
            <div class="col-xl-5 col-lg-6 col-md-8 px-5">
              <h1 class="text-white">少年,快使用魔力!</h1>
              <p class="text-lead text-white" id="hitokoto_text">正在给先生作诗...</p>
            </div>
          </div>
        </div>
      </div>
      <div class="separator separator-bottom separator-skew zindex-100">
        <svg x="0" y="0" viewBox="0 0 2560 100" preserveAspectRatio="none" version="1.1"
          xmlns="http://www.w3.org/2000/svg">
          <polygon class="fill-default" points="2560 0 2560 100 0 100"></polygon>
        </svg>
      </div>
    </div>
    <!-- Page content -->
    <div class="container mt--8 pb-5">
      <div class="row justify-content-center">
        <div class="col-lg-5 col-md-7">
          <div class="card bg-secondary border-0 mb-0">
            <div class="card-header bg-transparent pb-5">
              <div class="text-muted text-center mt-2 mb-3">门禁状态</div>
              <div class="btn-wrapper text-center">
                <span class="badge badge-info" id="doorstatus" onclick="getstatus();">查询中</span>
              </div>
            </div>
            <div class="card-body px-lg-5 py-lg-5">
              <div class="text-center">
                <button type="button" class="btn btn-primary my-4" onclick="opendoor();">开门</button>
                <button type="button" class="btn btn-danger my-4 add-button ">安装APP</button>
              </div>
              <div class="alert alert-info" role="alert" id="openstatus" style="display:none">
                查询中...
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
  <!-- Argon Scripts -->
  <!-- Core -->
  <script src="assets/vendor/jquery/dist/jquery.min.js"></script>
  <script src="assets/vendor/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
  <script src="assets/vendor/js-cookie/js.cookie.js"></script>
  <script src="assets/vendor/jquery.scrollbar/jquery.scrollbar.min.js"></script>
  <script src="assets/vendor/jquery-scroll-lock/dist/jquery-scrollLock.min.js"></script>
  <!-- Argon JS -->
  <script src="assets/js/argon.js?v=1.1.0"></script>
  <script>
    fetch('https://v1.hitokoto.cn')
      .then(response => response.json())
      .then(data => {
        const hitokoto = document.getElementById('hitokoto_text')
        hitokoto.innerText = data.hitokoto
      })
      .catch(console.error)
  </script>
  <script>
  function getstatus(){
    $.ajax( {
        type : 'get',
        dataType : 'text',
        url : 'controller.php?action=check',        
        success : function(data) {
          if(data == 100){
            $("#doorstatus").attr("class","badge badge-success")
            $("#doorstatus").text("正常")
          }else{
            $("#doorstatus").attr("class","badge badge-danger")
            $("#doorstatus").text("异常")
          }
    }
    });
  }
  function freshstatus(){
    getstatus();
    setInterval('freshstatus();', 11000);
  }
 freshstatus();
    function opendoor(){
         $("#openstatus").attr("class","alert alert-info");
        $("#openstatus").text("提交请求中,请稍后...");
        $("#openstatus").toggle(500);
      $.ajax( {
        type : 'get',
        dataType : 'text',
        url : 'controller.php?action=open',        
        success : function(data) {
          if(data == 100){
            $("#openstatus").attr("class","alert alert-success");
            $("#openstatus").text("门已打开,请在10秒之内进入!");
            setTimeout('$("#openstatus").toggle(1000);', 5000);
          }else if (data == 200){
            $("#openstatus").attr("class","alert alert-warning");
            $("#openstatus").text("门已打开,请尽快通行!");
            setTimeout('$("#openstatus").toggle(1000);', 5000);
          }else{
            $("#openstatus").attr("class","alert alert-danger");
            $("#openstatus").text("出现异常,无法开门!");
            setTimeout('$("#openstatus").toggle(1000);', 5000);
          }
    }
    });
    }
  </script>
</body>
</html>

controller.php

<?php
    $ip = "ESP8266地址";
    $action = $_REQUEST["action"];
    switch ($action){
        case "open" : opendoor($ip);break;
        case "check" : check($ip);break;
        case "restart" : restart($ip);break;
        default : exit("参数非法或不存在!");
    }

    function opendoor($ip){
    $ch = curl_init(); //file_get_contents()在这里效率莫名的低...
    curl_setopt($ch, CURLOPT_URL, "http://$ip/open");
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT , 5);
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);
    $result = curl_exec($ch);
    curl_close($ch);
    if($result == "1"){
        exit("100");
    }elseif ($result == "2") {
        exit("200");
    }else{
        echo $result;
        exit("0");
        }
    }

    function check($ip){
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, "http://$ip/check");
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT , 5);
        curl_setopt($ch, CURLOPT_TIMEOUT, 10);
        $result = curl_exec($ch);
        curl_close($ch);
        if($result == "1"){
            exit("100");
        }else{
            echo $result;
            exit("0");
        }
    }
    function restart($ip){
    $ch = curl_init(); //file_get_contents()在这里效率莫名的低...
    curl_setopt($ch, CURLOPT_URL, "http://$ip/restart");
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT , 5);
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);
    $result = curl_exec($ch);
    curl_close($ch);
    if($result == "1"){
        exit("100");
    }else{
        echo $result;
        exit("0");
        }
    }
?>

NFC控制

本部分用于在离线场景下进行开门,实际由于手机和手表支持NFC模拟门卡解锁速度比Web更快。

实现目标

  • 读取NFC卡片ID信息
  • 根据ID信息执行相应动作
  • 正确则开门;错误则在串口输出ID并相应错误提示音

接线


RC522

参考代码

#include <SPI.h>
#include <MFRC522.h>
#include <Arduino.h>

const int RST_PIN = 5;
const int SS_PIN = 4;
MFRC522 rfid(SS_PIN, RST_PIN); //初始化rfid实例
MFRC522::MIFARE_Key key;
byte keynuid[4] = {0, 0, 0, 0}; //正确的NFC卡片八进制ID,请根据串口输出修改

void setup(){
  //初始化NFC
  SPI.begin();
  rfid.PCD_Init();
  for (byte i = 0; i < 6; i++) {
    key.keyByte[i] = 0xFF;
  }
  Serial.println("NFC module initialized.");
}

void loop(){
    if ( ! rfid.PICC_IsNewCardPresent())
    return;

  if ( ! rfid.PICC_ReadCardSerial())
    return;

  if (rfid.uid.uidByte[0] == keynuid[0] ||
      rfid.uid.uidByte[1] == keynuid[1] ||
      rfid.uid.uidByte[2] == keynuid[2] ||
      rfid.uid.uidByte[3] == keynuid[3] ) {
    Serial.println("NFC card right.");
    opendoor();
  }else {
    buzzerfailed();
    Serial.println("NFC card wrong.Card OTC NID:");
    printnuid(rfid.uid.uidByte, rfid.uid.size); //输出错误卡片的八进制ID
  }

  rfid.PICC_HaltA();
  rfid.PCD_StopCrypto1();
}

void printnuid(byte *buffer, byte bufferSize) {
  for (byte i = 0; i < bufferSize; i++) {
    Serial.print(buffer[i] < 0x10 ? " 0" : " ");
    Serial.print(buffer[i], OCT);
  }
}

蜂鸣器与LED控制

接线

ESP8266 蜂鸣器
3V VCC
GND GND
D4(GPIO02) I/O

参考代码

void buzzersuccess() {
  tone(2, 523,200);
  delay(200);
  noTone(2);
  tone(2, 587);
  delay(200);
  noTone(2);
  tone(2, 659);
  delay(200);
  noTone(2);
}

void buzzerfailed() {
  tone(2, 261);
  delay(200);
  noTone(2);
  delay(100);
  tone(2, 261);
  delay(200);
  noTone(2);
}

void led_off(){
  digitalWrite(LED_BUILTIN, HIGH);
}

附:Frequencies for equal-tempered scale

总结

将以上代码有机组合之后即可完成整件作品。很容易发现本项目没有任何访问控制与安全控制,请谨慎采用。当然,由于经验不足以上代码还有大量的缺点,若您有能力请自行修改!

本文部分配图来源于网络,若有侵权请联系删除。

感谢

感谢@Kvar_ispw17大佬对本项目的指导与帮助!!!

ESP8266 RC522 SG90 门禁 NFC WEB控制

作者: Mion

2022 © Mion'Blog & Theme By xingr