隐约雷鸣,阴霾天空,但盼风雨来,能留你在此。 隐约雷鸣,阴霾天空,即使天无雨,我亦留此地。 ——《万叶集》
序
这是一个从刚入学就挖下的坑,宿舍老旧的门锁与会捉迷藏的钥匙总把我锁在外面,室友有时也没有那么可靠。于是准备把门锁做非破坏性的改造,支持上手机控制。后来又考虑到需要在断网的时候也能够正常访问,又加入了NFC刷卡开门。由于东西仅限自娱自乐且宿舍楼道有完备的安防监控,本项目没有准备任何访问控制。以下是实现的过程。
本项目源码已打包至https://github.com/MoeMion/Magic_Door/.
材料准备
-
ESP8266开发板
整个系统的控制中枢,支持Wi-Fi连接并且附带丰富的引脚,常用于物联网开发。在这里我选择了CH340处理器的版本,并在Arduino IDE上进行编程开发。(关于Arduino IDE与CH340驱动的问题可以通过搜索引擎快速解决)
价格:13 元
附ESP8266引脚定义图示.注意,D0(GPIO16) 仅能用于GPIO读写,不支持PWM/I2C/OW信号传输。
-
MF RC522 模块
负责NFC通信。由于需求简单,这里选择的版本不支持加密卡读写。
价格:12 元
-
MG995舵机
用来拉动机械门闩。
价格:15元
注意:MG995电机有多个版本,请选购180°带限位版本,360°版本仅能通过PWM信号控制旋转时间,不能控制具体角度!由于舵机不正确使用易烧坏,请小心使用!
引脚解释:
棕 : GND
红 : VCC (5V或3.3V均可,实测5V转动速度更快)
黄: PWM信号
-
MH-FMD 无源蜂鸣器
用于输出声音提醒。
价格:2元
-
杜邦线若干、热熔胶枪
连接各部分传感器。主要用到了公对母与母对母两种。
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添加至桌面 - 门锁已开启时给出提示
- 美观的界面
演示
参考代码
这里仅给出部分代码,与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并相应错误提示音
接线
参考代码
#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大佬对本项目的指导与帮助!!!