前言
根据项目结课报告改编而成,可能更适合作为一份文档而不是一篇记录类型的博客,没有留存接线图和运行图片,感到抱歉。
使用的板子是YwRobot的ESP8266板子,使用Arduino IDE开发,用到了舵机、按钮、人体感应传感器、射频模块等器件。另外使用了第三方代码改编实现了阿里云物联网平台的远程控制监控功能,使用aliyun iot studio进行开发。代码最后由于时间原因没有拆成多个文件,不过没超过500行问题也不算大XD。
使用这套设备的小伙伴可以尝试使用后面我发的源码中的引脚定义进行接线,可以避免不少问题(例如上传出现点和横线的组合然后报错),其中LED灯使用了板载LED BUILTIN_LED,在Arduino IDE中也要将其设置为0号;有一个按钮接在了唯一一个模拟IO A0上,可以看后面的代码。
对于RC522模块,可以参考这篇文章。接线就按照里面图的GPIOxxx,对应xxx接口来接就好了。
nodeMCU(ESP8266)和RC522的接线图_51CTO博客_nodemcu和esp8266是什么关系
需要完整源码的,可以在评论区留言。因为还涉及到与阿里云通信的库文件和代码,就不在博客中贴出了。
项目要求:
① 自动控制时,人体感应模块检测到有人、或者声音传感器检测到有声音,则打开门口的灯。手动控制时,不管传感器是否检测到人或声音,可以直接开关灯。
② 刷RFID卡开关门,信息正确则打开房门,信息不正确不开门。
(1)可以通过网页进行远程监控。
(2)远程端分别设置:
①本地控制/远程控制切换开关;
②手动控制/自动控制切换开关;
③远程手动控制时使用的设备总启动开关和总停止开关;设备端设置:本地手动控制时使用的设备总启动开关和总停止开关。
设备端和远程端均需要直观显示当前设备的运行状况。本地控制时,远程端仅能进行监视、不能控制。
本地控制分为手动控制和自动控制两种模式,当系统处于本地自动控制模式时,设备根据各传感器测量值和系统时间自动工作;
当系统处于本地手动控制模式时,使用设备的手动输入元件和系统时间控制设备的动作。远程控制时,仅远程手动控制起作用、无远程自动控制模式。
远程手动控制时,可以直观监视设备的运行状况,也可以通过网页遥控本地设备的动作。
设计方案:
本项目是一个物联网带照明RFID门禁系统,使用Arduino及相关开发板、元器件开发实现。
本项目主要由以下几个模块构成:
模式切换及运行控制模块、照明模块、开锁模块、物联网模块
- 模式切换及运行控制模块
该系统设计了自动运行模式和手动运行模式,可以通过在本地端和远程端切换两种运行模式。
- 照明模块
一个人体检测传感器和一个LED灯,经多次研究考虑后决定使用开发版内置LED。
- 开锁模块
由RFID读取部分和舵机部分组成,分别对应门禁刷卡器和门。连接这两个部分的是开门验证程序,并额外增加了密码开门程序。
- 物联网模块
用于实现该系统的远程控制,依托于阿里云物联网平台实现。
模块及对应的元器件(对应的软件见后面编程实现部分)
模块名称 | 对应的物理元件 |
模式切换及运行控制模块 | 开发板、按钮x1 |
照明模块 | 板内置LED、按钮x1、人体感应传感器 |
开锁模块 | 舵机、RFID读卡器 |
物联网模块 | 开发板(Wi-Fi芯片) |
功能一览:
完成了所有目标要求,并额外实现了远程密码开锁功能,设计了精美的物联网控制UI界面。所有的功能均可正常使用并即时在物联网平台更新,远程操控功能正常与手动控制无冲突。
- 能读取指定的RFID卡并通过验证操纵舵机开门
- 在自动模式下人体感应传感器检测到物体运动将会打开灯,在手动模式下可以通过按钮/远程来开关灯(提醒传感器有一段时间时延)
- 可以通过按钮/远程控制自动和手动模式,在远程控制任何功能将会强制切换到手动模式,直到用户在本地通过按钮切换成自动模式
- 在物联网平台可以看到实时的数据:控制状态(自动还是手动),是否有人,灯的开关,卡的合法性(门的开启和关闭),操控:控制状态,开关灯,门的开启(密码)
- 额外增加了在远程端设置密码并使用密码来开门的功能
编程思路及关键代码说明:
编程实现(软件部分)是这套项目的核心也是重点,在项目的开发过程中大部分时间都用于此部分。在开发中使用了函数式编程的思想,在源码以及接下来关键功能的代码说明中可以看到功能的实现是函数化的。
Arduino开发语言在很大程度上是与C/C++兼容的,除了一些Arduino的内置函数,以及在验证门卡和使用密码开门时使用了Arduino封装的String()类型和在使用RFID-RC522射频模块读取卡片时使用了类和对象一些面向对象C++编程方法[https://github.com/miguelbalboa/rfid]外,本项目的代码可以被学过基础C语言的读者所读懂。
可以在Arduino官方文档[https://www.arduino.cc/reference/en/]找到相对应的Arduino语言特性。
本项目的代码本地变量名和函数名使用小驼峰命名法。
下面将按照模块进行说明,并指出一些容易出错的地方及其解决方案。
模式切换及运行控制模块
使用一个状态变量MODE来标识处于自动运行模式还是手动运行模式,在切换时翻转这个状态变量(二进制),并在需要分模式运行的功能运行前读取状态变量来选择提供的功能。
模式切换的代码是简单的,重要的是在这里引入了一段按钮防抖代码,用于消除机械开关的抖动。如果没有这段代码,按钮按下后你不能确定你会得到哪一个状态(结果完全是随机的,由抖动次数决定)。在下面的灯开关按钮中也需要用到。
在这里讲解一下防抖的思路:
在物理层面上避免机械开关的抖动是复杂的,毕竟没有专门设计消抖电路。发现问题后决定通过软件层面消除抖动。通过变量currentBtnX记录当前按钮的状态,并与按钮在上一时刻的状态lastBtnX进行对比,如果按钮的状态发生了变化,怎么确定是由于抖动引起的还是用户的实际操作?我们记录上一次由用户操作的按钮按下时间lastDebounceTimeX,通过millis()获取当前时间计算出两次操作之间的时间差,如果这个时间差小于一个较小的时间,可以确定是由于抖动产生的,我们只需处理时间差大于设定时间的操作,并更新按钮按下时间。
事实上Arduino也有第三方库来实现防抖,但基于种种考虑本项目并没有采用。
1. void modeSwitch()
2. {
3. currentBtn1 = digitalRead(modeSwitchBtn);
4. if (lastBtn1 != currentBtn1) //模式切换按钮被按下,切换模式
5. {
6. if (millis() - lastDebounceTime1 > debounceDelay)
7. {
8. if (currentBtn1)
9. {
10. //Serial.println("ModeSwitchButton Pressed");
11. MODE ^= 1;
12. }
13. lastDebounceTime1 = millis();
14. }
15. }
16. }
照明模块
使用一个变量Detected来标识人体感应传感器是否感应到人体,一个变量ledStatus来标记灯的开关,以进一步写入高低电平来控制灯的亮灭。
1. void ledControl()
2. {
3. if (MODE) //手动控制
4. {
5. //Serial.printf("AnalogRead LEDBTN %d\n",analogRead(ledBtn));
6. currentBtn2 = (analogRead(ledBtn) == 1024) ? 1 : 0;
7. if (currentBtn2 != lastBtn2)
8. {
9. if (millis() - lastDebounceTime2 > debounceDelay)
10. {
11. if (currentBtn2)
12. {
13. Serial.println("LED Control Button pressed");
14. ledStatus ^= 1;
15. remoteLED = 0;
16. }
17. lastDebounceTime2 = millis();
18. }
19. }
20. if (!remoteLED)
21. digitalWrite(LED, !ledStatus);
22. }
23. else
24. digitalWrite(LED, !Detected); //自动模式直接根据传感器返回的信号控制灯
25. }
在这段代码中灯开关按钮读取代码由于该按钮接的是模拟引脚,需要将模拟数值(0-1024)转换成0和1来使用,经过实验发现按钮按下时返回1024,故将1024映射成数字量1。
在这里说明一下remoteLED变量,这是由于远程控制的数据发送到本地时是在获取数据的函数void mqtt_callback(char *topic, byte *payload, unsigned int length)中直接控制的,这是为了更少的延时,但也造成了一些设计上的瑕疵,或许有更好的方法来解决,但你只需要知道这是一个bug patch。当然这没有造成任何功能上的影响,在按钮按下时该变量复位,只是降低了一点代码的可读性和设计美学。
另外由于控制的是板内置LED,高电平灭,低电平亮,这点需要注意。
void mqtt_callback(char *topic, byte *payload, unsigned int length)中控制灯的部分
1. if (params_LightSwitch == 0)
2. {
3. Serial.println("led off");
4. digitalWrite(LED, 1);
5. remoteLED = 1;
6. }
7. else if (params_LightSwitch == 1)
8. {
9. Serial.println("led on");
10. digitalWrite(LED, 0);
11. remoteLED = 1;
12. }
开锁模块
两种开锁方式,远程密码开锁和手动密码开锁。合法的密码legalPwd是在远程端首次操控前需要设置并保存到本地的,合法的卡UIDlegalID是在程序编写时根据读取结果设定的。通过变量cardvalidity来监控门的开启与否,而doorOpen变量是为了解决门的延迟3秒函数执行后才运行aliyunUpload()上报数据而增加的一个强制上传标识变量。在执行完开门操作后要讲密码或卡UID清空。
1. void accessControl()
2. {
3. readCard();
4. if (readID == legalID || legalPwd == inputPwd)//两种方式开门
5. {
6. Serial.println("OK,Open door!\n");
7. cardvalidity = 1;
8. doorOpen = 1;//解决因为开门状态延迟导致的物联网上传延迟
9. aliyunUpload();//解决延迟在这里忽略间隔强制上传
10. doorOpen = 0;
11. doorControl(1); //开门
12. delay(3000);//开门状态保持3秒
13. doorControl(0);//关门
14. //读取的卡ID字符串和密码清零
15. readID = "";
16. inputPwd = "";
17. }
18. else
19. {
20. cardvalidity = 0;
21. //Serial.println("Card Not Match!\n");
22. }
23. }
1. void readCard()
2. {
3. // 寻找新卡
4. if ( ! mfrc522.PICC_IsNewCardPresent()) {
5. //Serial.println("没有找到卡");
6. return;
7. }
8.
9. // 选择一张卡
10. if ( ! mfrc522.PICC_ReadCardSerial()) {
11. Serial.println("没有卡可选");
12. return;
13. }
14.
15.
16. // 显示卡片的详细信息
17. Serial.print(F("卡片 UID:"));
18. dump_byte_array(mfrc522.uid.uidByte, mfrc522.uid.size);//将卡的UID转换成字符串
19. Serial.println();
20. Serial.print(F("卡片类型: "));
21. MFRC522::PICC_Type piccType = mfrc522.PICC_GetType(mfrc522.uid.sak);
22. Serial.println(mfrc522.PICC_GetTypeName(piccType));
23.
24. // 检查兼容性
25. if ( piccType != MFRC522::PICC_TYPE_MIFARE_MINI
26. && piccType != MFRC522::PICC_TYPE_MIFARE_1K
27. && piccType != MFRC522::PICC_TYPE_MIFARE_4K) {
28. Serial.println(F("仅仅适合Mifare Classic卡的读写"));
29. return;
30. }
31.
32. MFRC522::StatusCode status;
33. if (status != MFRC522::STATUS_OK) {
34. Serial.print(F("身份验证失败?或者是卡链接失败"));
35. Serial.println(mfrc522.GetStatusCodeName(status));
36. return;
37. }
38. //停止 PICC
39. mfrc522.PICC_HaltA();
40. //停止加密PCD
41. mfrc522.PCD_StopCrypto1();
42. return;
43. }
将卡的UID转换成Arduino封装的String类型储存
1. void dump_byte_array(byte *buffer, byte bufferSize)
2. {
3. readID = "";
4. for (byte i = 0; i < bufferSize; i++)
5. {
6. //将uid转成字符串
7. readID += buffer[i];
8. Serial.print(buffer[i] < 0x10 ? " 0" : " ");
9. Serial.print(buffer[i], HEX);
10. }
11. Serial.println('\n');
12. Serial.print(readID);
13. }
在这段舵机控制代码中读取了舵机当前的角度,避免错误的发生。
1. void doorControl(int op)//0关门,1开门
2. {
3. int pos = 0;
4. int cur = door.read();
5. if (op) //开门
6. {
7. if (cur >= 90)
8. {
9. Serial.println("Door has already been opened! ");
10. return ;
11. }
12. for (pos = cur; pos <= 90; pos++)
13. {
14. door.write(pos);
15. delay(10);
16. }
17. }
18. else
19. {
20. if (cur <= 0)
21. {
22. Serial.println("Door has already been closed! ");
23. return ;
24. }
25. for (pos = cur; pos >= 0; pos--)
26. {
27. door.write(pos);
28. delay(10);
29. }
30. }
31. }
另外请注意为了保证第一次开门舵机能顺利转动,需要在setup()函数中先将舵机复位
door.write(0);//舵机启动时复位
物联网模块
物联网模块是由一个基于MQTT协议的阿里云库aliyun_mqtt.h和aliyun_mqtt.cpp的基础上改写的,在主ino文件中物联网上传数据的入口是aliyunUpload()函数。为了让数据上报更加及时,将上传间隔改成1秒,并在开门时通过doorOpen变量忽略上传时间间隔上报一次数据。在该函数中将要上报的数值赋值给了专门定义的上传变量,它们的命名为upload_xxx。
1. void aliyunUpload()//在loop中上传数据
2. {
3. if (millis() - lastMs >= 1000 || doorOpen) //每1秒读取一次本地数据或强制上传
4. {
5. lastMs = millis();
6. mqtt_check_connect();//连接阿里云IOT平台
7. //将进行格式转换并串口显示
8. upload_mode = MODE;
9. upload_cardvalidity = cardvalidity;
10. upload_detect = Detected;
11. mqtt_interval_post();//ESP8266向阿里云IOT平台上报本地数据
12. }
13. mqttClient.loop();
14. }
这是处理远程控制下传数据的代码,实现的是远程操控的功能。这段代码是在此前实验原有代码基础上增加修改而成的。有的远程操控为了保证不延迟不是通过控制变量来实现的,而是直接改变,如LED。阿里云lot发送的是JSON格式的报文,需要使用ArduinoJson库来进行解析。
在阿里云平台发送数据时选择的数据类型有限,例如说为了下传设定的密码,阿里云平台的字符串显示的是Text类型,实际上可以在Arduino端使用String来解析。额外增加的密码开锁功能在这里也引起了一点麻烦,例如需要处理输入密码为空的情况和未设定密码就使用密码开锁的情况,然而这并不是这个项目的重点,读者可以参考代码自行理解。
在开发过程中曾遇到一个严重的问题,在使用lot Studio开发出来的Web操控端的UI按键操控设备功能时,会发生数据覆盖的问题——然而在使用设备调试功能手动输入值发送时并不会出现这个问题。(一点击开关灯的按钮,模式就自动切换成自动模式了)经调试发现,使用Web操控端控制单一按钮,只发送该页面组件绑定的事件的JSON数据包,其它值均为缺省项;而在设备调试功能中是发送所有可读写的值。
查阅ArduinoJSON官方文档后发现是由于解析未在JSON报文中的数据时,将会用默认值来代替,这个默认值(数值的默认值是0)会与状态的含义和接下来要进行的操作发生冲突。
继续查阅,发现可通过更改默认值的方法来解决,将数值数据的默认值设置为-1,就不会发生冲突了。
1. //处理ESP8266向阿里云IOT平台订阅的其他客户端的主题
2. void mqtt_callback(char *topic, byte *payload, unsigned int length)
3. {
4. //如果云端虚拟设备发布了本地设备已订阅的主题的消息,串口显示有消息进来,并显示消息的主题(topic)和消息体(payload)
5. //串口显示Message arrived [topic],其中topic是消息的主题,具体内容是阿里云平台上任意可以订阅的主题,我们关心的是ALINK_TOPIC_PROP_SET
6. Serial.print("Message arrived [");
7. Serial.print(topic);
8. Serial.println("] ");
9. //payload字符串长度不确定,将其放入StaticJsonBuffer后有剩余的部分用\0字符填充,并标识字符串结束
10. payload[length] = '\0';
11. Serial.println((char *)payload);//串口显示消息体(payload)的内容
12.
13. //在topic中检索ALINK_TOPIC_PROP_SET第一次出现的位置,如果topic中第一次出现了ALINK_TOPIC_PROP_SET,
14. //返回ALINK_TOPIC_PROP_SET的地址;如果没有检索到ALINK_TOPIC_PROP_SET,返回NULL
15. if (strstr(topic, ALINK_TOPIC_PROP_SET)) {
16. //Json对象 对象树的内存工具 静态buffer
17. //100是静态buffer的大小。如果这个Json对象更加复杂,那么就要根据需要去增加这个数值
18. StaticJsonBuffer<200> jsonBuffer;
19.
20. //创建最外层的json对象:root对象,顶节点
21. //解析JSON对象字符串,将JSON格式的payload消息拆分开
22. JsonObject &root = jsonBuffer.parseObject(payload);
23. // https://arduinojson.org/v5/assistant/ json数据解析网站
24. int params_LightSwitch = root["params"]["LightSwitch"] | -1; //完成解析后,可以直接读取params中的各个变量参数值
25. int params_ModeSwitch = root["params"]["upload_mode"] | -1;//默认赋值问题非常重要
26. String params_legalPwd = root["params"]["legal_pwd"] ;
27. String params_inputPwd = root["params"]["input_pwd"] ;
28. if (params_legalPwd.length() > 0)
29. legalPwd = params_legalPwd;
30. else if (!params_inputPwd.length())
31. Serial.println("设置密码为空请重新输入");
32. if (params_inputPwd.length() > 0)
33. {
34. inputPwd = params_inputPwd;
35. Serial.printf("%s %s\n", inputPwd, legalPwd);
36. }
37. else if (!params_legalPwd.length())
38. Serial.println("输入密码为空请重新输入");
39. //如果读到了所关心的变量,可以执行进一步的操作,这里是用LightSwitch变量开灯或关灯
40. if (params_ModeSwitch == 1) //先切换模式再控制灯,防止出现同时更改灯和模式状态出现自动模式不能控制灯的情况
41. {
42. Serial.println("Switch to Manual Mode");
43. MODE = 1;
44. }
45. else if (params_ModeSwitch == 0)
46. {
47. Serial.println("Switch to Auto Mode");
48. MODE = 0;
49. }
50. if (!MODE)
51. {
52. MODE = 1;
53. Serial.println("Switch to Manual Mode due to control remotely");
54. }
55. if (params_LightSwitch == 0)
56. {
57. Serial.println("led off");
58. digitalWrite(LED, 1);
59. remoteLED = 1;
60. }
61. else if (params_LightSwitch == 1)
62. {
63. Serial.println("led on");
64. digitalWrite(LED, 0);
65. remoteLED = 1;
66. }
67.
68. if (!root.success())//如果解析没成功,串口输出解析失败(parseObject() failed)
69. { Serial.println("parseObject() failed");
70. return;
71. }
72. }
73. }
远程控制页截图(设备未在线)
点击设置密码进入密码设置页
阿里云功能定义