【物联网服务NodeJs-5天学习】第三天实战篇② ——基于物联网的WiFi自动打卡考勤系统
创始人
2024-06-03 11:53:23
0

【NodeJs-5天学习】第三天实战篇② ——基于物联网的WiFi自动打卡考勤系统

    • 1. 前言
    • 2.实现思路
      • 2.1 NodeJs服务器代码
        • 2.1.1 对接Express服务器
        • 2.1.2 对接Mac地址处理
        • 2.1.3 对接飞书群处理
      • 2.2 ESP8266代码
      • 2.3 测试效果
        • 2.3.1 串口打印日志
        • 2.3.2 NodeJs服务器打印的数据
        • 2.3.3 txt文件存储的内容
        • 2.3.4 飞书群信息展示
    • 4.总结

面向读者群体

  • ❤️ 电子物联网专业同学,想针对硬件功能构造简单的服务器,不需要学习专业的服务器开发知识 ❤️
  • ❤️ 业余爱好物联网开发者,有简单技术基础,想针对硬件功能构造简单的服务器❤️
  • ❤️ 本篇创建记录 2023-03-12 ❤️
  • ❤️ 本篇更新记录 2023-03-12 ❤️

技术要求

  • 有HTML、CSS、JavaScript基础更好,当然也没事,就直接运行实例代码学习

专栏介绍

  • 通过简短5天时间的渐进式学习NodeJs,可以了解到基本的服务开发概念,同时可以学习到npm、内置核心API(FS文件系统操作、HTTP服务器、Express框架等等),最终能够完成基本的物联网web开发,而且能够部署到公网访问。

🙏 此博客均由博主单独编写,不存在任何商业团队运营,如发现错误,请留言轰炸哦!及时修正!感谢支持!🎉 欢迎关注 🔎点赞 👍收藏 ⭐️留言📝

1. 前言

在学习ESP8266 WiFi探针时,我们了解通过Probe Request帧可以获取到无线设备(手机、手提电脑等)的MAC地址。

MAC地址可以简单理解为无线网卡地址。每一块无线网卡出厂时都会由厂家分配全球唯一的MAC地址,用来表示它的唯一性

现代社会上,基本上人手一部智能手机,自带wifi功能。只要我们打开了WiFi功能,我们就可以通过自动捕获手机发出的 802.11 帧 来获取到对应的手机MAC地址。

当我们在后台服务器上预先配置好 MAC地址与用户信息的关联关系(比如用户名字、用户工号、学生编号等),并且把捕获到的MAC地址上传到后台服务器进行对比,我们就可以完成自动考勤或者无线点名功能。这整个过程都是无感知、全自动。

这里的服务器就可以用到我们NodeJs的Express服务器。

当然,如果你想骗过考勤系统或者点名系统,那么建议你学习我的混杂模式篇,自己伪装一个proberequest管理帧,实现不在场证据(请不要告诉你的老师)。

2.实现思路

实现三部曲

  • esp8266开启混杂Sniffer模式
  • 捕获管理帧和数据帧,解析出MAC地址
  • 上传到NodeJs服务器,后台服务器会对mac地址和用户信息进行比对,然后发送到班级飞书群里面。

具体帧的含义请参考

  • ESP8266开发之旅 进阶篇⑨ 深入了解 802.11 无线协议(非常重要)

这里涉及到的知识点:

  • 【NodeJs-5天学习】第二天篇① ——fs文件系统
    使用FS模块来获取用户信息配置文件以及存储打卡记录
  • 【NodeJs-5天学习】第二天篇③ ——Express Web框架 和 中间件
    构造服务器用来处理客户端数据上报。

2.1 NodeJs服务器代码

在这里插入图片描述

2.1.1 对接Express服务器

  • index.js
// 1、导入所需插件模块
const express = require("express")
const {getIPAdress} = require('./utils/utils.js')
const bodyParser = require('body-parser')
const {router} = require('./router/router.js')// 2、创建web服务器
let app = express()
const port = 8266 // 端口号                 
const myHost = getIPAdress();// 3、注册中间件,app.use 函数用于注册全局中间件 (局部中间件?)/**** Express(npm ls 包名 参考版本号) 内置了几个常用的中间件:* - express.static 快速托管静态资源的中间件,比如 HTML文件、图片、CSS等* - express.json 解析JSON格式的请求体数据 (post请求:application/json)* - express.urlencoded 解析 URL-encoded 格式的请求体数据(表单 application/x-www-form-urlencoded)* 
*/// 3.1 预处理中间件app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));app.use(function(req, res, next){// url地址栏出现中文则浏览器会进行iso8859-1编码,解决方案使用decode解码console.log('解码之后' + decodeURI(req.url));console.log('URL:' + req.url);console.log(req.body);next()
})// 3.2 路由中间件
app.use(router)// 3.3 错误级别中间件(专门用于捕获整个项目发生的异常错误,防止项目奔溃),必须注册在所有路由之后
app.use((err, req, res, next) => {console.log('出现异常:' + err.message)res.send('Error: 服务器异常,请耐心等待!')
})// 4、启动web服务器
app.listen(port,() => {console.log("express 服务器启动成功 http://"+ myHost +":" + port);
});

2.1.2 对接Mac地址处理

// 1、导入所需插件模块
const express = require("express")
const fs = require('fs')
const time = require('../utils/time.js')
const alarmFeishu = require('../alarm/alarm_feishu.js')// 2、创建路由对象
const router = express.Router();// 用户配置信息
var fileName = './config/三年一班.json';
var config = JSON.parse(fs.readFileSync(fileName));
var configMap = new Map()
config.forEach(element => {var key = element.macvar value = element.nameconfigMap.set(key, value)
})console.log(configMap)// 3、挂载具体的路由
// 配置add URL请求处理
// 参数1:客户端请求的URL地址
// 参数2:请求对应的处理函数
//        req:请求对象(包含与请求相关属性方法)
//        res:响应对象(包含与响应相关属性方法)router.post('/api/add/check', (req, res) => {var body = req.bodyvar datas = body.datasvar name = ''var date = time.getCurrentDate()var fileName = './storage/fs/' + date + '_打卡记录.txt';if (datas){datas.forEach(element => {var value = element.valuename = configMap.get(value)if (name) {var exist = fs.existsSync(fileName)var content = time.getCurrentDateTime() + ' ' + name + '\n'alarmFeishu.sendText(`${name} 打卡了!`)if (exist) {fs.appendFile(fileName, content ,function (err, fd){if (err) {return console.error(err);}console.log("文件追加成功!");});} else {fs.writeFile(fileName, content, {flag: 'a'}, function(err){if(err){return console.log(err);}else {console.log("写入成功");}});}console.log(`${name} 打卡了!`)// throw new Error('模拟项目抛出错误!')} else {console.log(`${value} 无法在配置表中找到!`)}});}res.send("OK")
})// 4、向外导出路由对象
module.exports = {router
}

首先会把用户配置信息映射为一个map对象

[{"name": "霍师傅","mac": "B0:E1:7E:70:25:CD"
}, {"name": "华师傅","mac": "78:DA:07:04:5D:18"
}, {"name": "陈师傅","mac": "30:FC:68:19:52:A4"
}, {"name": "叶师傅","mac": "F4:EE:14:0E:4C:14"
}, {"name": "张师傅","mac": "94:B9:7E:1A:42:F9"
}, {"name": "滑师傅","mac": "1C:60:DE:AE:D9:06"
}]

有数据过来的时候需要匹配map,然后把数据写入文件里面。

var fileName = './storage/fs/' + date + '_打卡记录.txt';

打卡记录以天隔开。

2.1.3 对接飞书群处理

// 1、导入所需插件模块
const request = require('request')/******** 飞书自定义机器人相关配置信息 ***********/
// 官方文档:https://open.feishu.cn/document/ukTMukTMukTM/ucTM5YjL3ETO24yNxkjN// token,从机器人链接得到,替换为自己的
const tokenHouge = `052e7e00-7455-437d-838f-xxxxxx`const sendMsgUrl = `https://open.feishu.cn/open-apis/bot/v2/hook/${tokenHouge}`
/******** 飞书自定义机器人相关配置信息 ***********//*** 真正发送消息* "{\"msg_type\":\"text\",\"content\":{\"text\":\"所有人 %s\"}}"
*/
function sendMessage(content){const requestData = {msg_type: "text",content: {text: `${content}`}}request({url: sendMsgUrl,method: "POST",json: true,headers: {"content-type": "application/json",},body: requestData}, function(error, response, body) {console.log(body)if (!error && response.statusCode == 200) {}}); 
}/**** 发送具体消息 */
function sendText(content) {sendMessage(content)
}// 4、向外导出路由对象
module.exports = {sendText,
}

这里会构造出信息发送到飞书群。

alarmFeishu.sendText(`${name} 打卡了!`)

2.2 ESP8266代码

/***  功能:ESP8266 自动考勤系统*  作者:单片机菜鸟*  时间:2022-08-06*  描述:*      1.OneNet平台端:创建Http协议的产品,创建设备*      2.开启混杂模式,收集MAC地址*      2.把获取的MAC地址上传到OneNet平台*      *  硬件材料:*   1、ESP8266-12 NodeMcu板子**/// 导入必要的库
#include         // 引入WiFi核心库
#include 
#include   // 引入HttpClient库
#include              // 引入定时库
#include 
#include "H_project.h"          // 上传服务相关
#include "H_80211Frame.h"       // 混杂模式相关定义库void setup() {// put your setup code here, to run once:initSystem();
}void loop() {//每1s切换一次信道 也就是每个信道的工作时间是1sif (millis() - hop_time >= 1000) {hop_time = millis();hop_channel++;if (hop_channel > 13) {isUploadMac = true;hop_channel = 1;}Serial.println(hop_channel);enable_promisc(hop_channel);}// 捕获完一轮之后上传一次 也就是 1-13信道if (isUploadMac){isUploadMac = false;if (unique_num > 0){upload_mac_to_server();} else {Serial.println("------------- No Match ------------------");}Serial.println("------------- END ------------------");Serial.println("------------- START ----------------");}
}/*** 初始化系统*/
void initSystem(void){Serial.begin (115200);Serial.println("\r\n\r\nStart ESP8266 自动考勤");Serial.print("Firmware Version:");Serial.println(VER);Serial.print("SDK Version:");Serial.println(ESP.getSdkVersion());wifi_station_set_auto_connect(0);//关闭自动连接ESP.wdtEnable(5000);pinMode(LED_BUILTIN, OUTPUT);hop_channel = 1;enable_promisc(hop_channel);Serial.println("------------- START ----------------");
}/*** 连接到AP热点*/
void connectToAP(void){int cnt = 0;WiFi.begin(ssid, password);while (WiFi.status() != WL_CONNECTED) {delay(500);cnt++;Serial.print(".");if(cnt>=40){cnt = 0;//重启系统delayRestart(1);}}
}/*
*  WiFiTick
*  检查是否需要初始化WiFi
*  检查WiFi是否连接上
*  控制指示灯
*/
void wifiTick(){static bool ledTurnon = false;if ( WiFi.status() != WL_CONNECTED ) {if (millis() - lastWiFiCheckTick > 1000) {lastWiFiCheckTick = millis();ledState = !ledState; digitalWrite(LED_BUILTIN, ledState);ledTurnon = false;}}else{if (ledTurnon == false) {ledTurnon = true;digitalWrite(LED_BUILTIN, 0);}}
}/*判断,解析抓取到的数据包*/
void do_process(uint8_t *buf)
{ieee80211_mgmt_frame *mgmt = (ieee80211_mgmt_frame *)buf;uint8_t type = mgmt->ctl.type;uint8_t sub_type = mgmt->ctl.subtype;uint8_t sta_addr[6];unsigned long now = millis();int do_flag = 0;if (type == 0){// 管理帧/*** AssociationRequest = 0, // 关联请求* AssociationResponse,    // 连接响应* ReassociationRequest,   // 重连接请求* ReassociationResponse,  // 重连接联响应* ProbeRequest,           // 探测请求* ProbeResponse,          // 探测响应* Beacon,                 // 信标,被动扫描时AP 发出,notify* ATIM,                   // 通知传输指示消息* Disassociation,         // 解除连接,notify* Authentication,         // 身份验证* Deauthentication,       // 解除认证,notify* Reserved                // 保留,未使用*/if (sub_type == ProbeRequest){// 获取MAC地址memcpy(sta_addr, mgmt->addr2, 6);if (is_normal_mac(sta_addr)){add_mac(now,sta_addr);}} } else {//此情况下,addr1肯定为AP,sta为addr2,手机发出if (mgmt->ctl.from_ds == 0 && mgmt->ctl.to_ds == 1){memcpy(sta_addr, mgmt->addr2, 6);do_flag = 1;}//此情况下,addr2肯定为AP,如果addr3等于addr2,为路由发出,if (mgmt->ctl.from_ds == 1 && mgmt->ctl.to_ds == 0){memcpy(sta_addr, mgmt->addr1, 6);do_flag = 1;}if (mgmt->ctl.from_ds == 0 && mgmt->ctl.to_ds == 0){memcpy(sta_addr, mgmt->addr2, 6);do_flag = 1;}if (do_flag == 0){return ;}if (is_normal_mac(sta_addr)){add_mac(now,sta_addr);}} 
}/*** 函数说明:解析抓取到的数据包* 参数:*   1. buf 收到的数据包*   2. len buf的长度*/ 
static void promisc_cb(uint8_t * buf, uint16_t len){if (len == 12 || len < 10){return;}struct RxPacket * pkt = (struct RxPacket*) buf;do_process((uint8_t *)&pkt->buf);
}/*** 函数说明:启用特定频道的混杂模式* 参数:*   1. channel 设置频道*/
static void enable_promisc(int channel){WiFi.disconnect();WiFi.mode(WIFI_STA);wifi_set_channel(channel);  // 初始化为通道wifi_promiscuous_enable(0); // 先关闭混杂模式// 注册混杂模式下的接收数据的回调函数,每收到一包数据,都会进入注册的回调函数里面。wifi_set_promiscuous_rx_cb(promisc_cb);wifi_promiscuous_enable(1); // 开启混杂模式
}/*** 函数说明:关闭混杂模式* 参数:*   1. channel 设置频道*/
static void disable_promisc(int channel){wifi_promiscuous_enable(0);
}/**格式化打印mac*/
static void print_mac(const uint8_t * mac){char text[32];sprintf(text, "%02X:%02X:%02X:%02X:%02X:%02X",mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);Serial.println(text);
}/**判断是否是普通mac*/
static bool is_normal_mac(const uint8_t * mac)
{char text[32];char c;sprintf(text, "%02X:%02X:%02X:%02X:%02X:%02X",mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);c = text[1];if (c == '0' || c == '4' || c == '8' || c == 'C') {return true;}return  false;
}/** 保存mac地址到已经上传列表*/
static void add_upload_mac(const uint8_t *mac){for (int i = 0; i < unique_upload_num; i++) {if (memcmp(mac, upload_mac[i].mac, 6) == 0) {// 已经上传过return;}}if (unique_upload_num < UPLOAD_MAC){memcpy(&upload_mac[unique_upload_num].mac, mac, 6);unique_upload_num++;} else {Serial.println("unique_upload_num OVER MAX==========");}
}/**检测扫描到的mac是否已存在,*不存在,并且还有空间,添加保存 返回true*没空间返回false*已存在更新时间戳,返回false*/
static bool add_mac(unsigned long now, const uint8_t *mac){int i;for (i = 0; i < unique_upload_num; i++) {if (memcmp(mac, upload_mac[i].mac, 6) == 0) {// 已经上传过return false;}}// 判断是否已经存在过for (i = 0; i < unique_num; i++) {if (memcmp(mac, unique_mac[i].mac, 6) == 0) {// 更新时间unique_mac[i].last_seen = now;return false;}}// 还有足够空间就添加进去if (unique_num < MAX_MAC) {Serial.print("New Mac: ");print_mac(mac);memcpy(&unique_mac[unique_num].mac, mac, 6);unique_mac[unique_num].last_seen = now;unique_num++;return true;} else {Serial.println("unique_num OVER MAX==========");// could not fit itreturn false;}
}/** 功能:mac生命周期检测,从列表清除长时间未检测到的mac* 参数:*   1. now 当前时间*   2. expire 过期间隔*/
static void expire_mac(unsigned long now, unsigned long expire) {char text[32];int i;for (i = 0; i < unique_num; i++) {if ((now - unique_mac[i].last_seen) > expire) {// 过期之后 用数组的最后一个内容覆盖过期位置if (--unique_num > i) {sprintf(text, "%10d: ", now);Serial.print(text);print_mac(unique_mac[i].mac);sprintf(text, " expired: %d\n", unique_num);Serial.print(text);memcpy(&unique_mac[i], &unique_mac[unique_num], sizeof(mac_t));}}}
}/** 上传Mac地址*/
static bool upload_mac_to_server(){uint8_t *mac;disable_promisc(hop_channel);DynamicJsonDocument doc(2048);//在 doc 对象中加入data数组JsonArray datas = doc.createNestedArray("datas");for(int i = 0;i < unique_num ; i ++){char text[32];JsonObject value = datas.createNestedObject();mac = unique_mac[i].mac;sprintf(text, "%02X:%02X:%02X:%02X:%02X:%02X",mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);value["value"] = text;}String data;serializeJson(doc, data);serializeJsonPretty(doc, Serial); connectToAP();retry = 0;Serial.println("Upload Start");while(!postToDeviceDataPoint(data)){retry ++;ESP.wdtFeed();if(retry == 20){retry = 0;delayRestart(1);}}Serial.println("Upload Success!"); for (int i = 0; i < unique_num; i++){add_upload_mac(unique_mac[i].mac);      memset(&unique_mac[i], 0x00, sizeof(mac_t));}unique_num = 0;enable_promisc(hop_channel);return true;
}

2.3 测试效果

2.3.1 串口打印日志

在这里插入图片描述

2.3.2 NodeJs服务器打印的数据

在这里插入图片描述

2.3.3 txt文件存储的内容

在这里插入图片描述

2.3.4 飞书群信息展示

在这里插入图片描述

至此,一个简单的教室WiFi自动打卡考勤系统就可以了。

4.总结

篇②结合ESP8266来开发简单物联网应用——自动考勤系统,麻雀虽小五脏俱全,初学者需要理解文件系统、服务请求等等对应的知识点并加以实际应用。

相关内容

热门资讯

汽车系统识别到安卓,智能驾驶新... 你知道吗?最近汽车界可是掀起了一股新潮流呢!那就是汽车系统识别到安卓,这可不是简单的兼容,而是深度融...
工程业务系统安卓版,助力项目管... 你知道吗?最近我在手机上发现了一个超级实用的工程业务系统安卓版,简直让我爱不释手!这款应用不仅功能强...
安卓手机不能升级系统,探寻无法... 最近是不是有很多小伙伴发现自己的安卓手机不能升级系统了?这可真是让人头疼啊!今天,就让我来给你详细解...
安卓系统大型枪战游戏,打造你的... 你有没有想过,在手机上也能体验一场惊心动魄的枪战呢?没错,就是那种让你心跳加速、肾上腺素飙升的大型枪...
安卓系统桌面返回按键,桌面返回... 你有没有发现,每次拿起手机,那个小小的安卓系统桌面返回按键就像一个忠诚的小助手,默默陪伴着我们穿梭在...
鸿蒙系统是换壳安卓,揭秘“换壳... 你知道吗?最近科技圈里有个大瓜,那就是鸿蒙系统竟然是换壳安卓!是不是觉得有点不可思议?别急,让我带你...
华为手机安卓系统流畅,引领科技... 你有没有发现,最近用华为手机的小伙伴们都在悄悄地炫耀他们的手机运行得超级流畅呢?这可不是随便说说哦,...
安卓系统短信无通知,无通知新体... 你是不是也遇到了这个问题?手机里的安卓系统短信竟然没有通知,这可真是让人头疼啊!别急,今天就来给你详...
安卓系统彩蛋怎么打开,开启隐藏... 你有没有发现,安卓系统里隐藏着许多有趣的彩蛋呢?这些彩蛋就像是一扇扇神秘的大门,等待着我们去探索。今...
安卓系统关闭压缩内存,安卓系统... 你知道吗?最近在安卓系统里,有一个小秘密引起了大家的热议。那就是关闭压缩内存的功能。听起来有点复杂,...
安卓系统上位机编写,基于安卓系... 你有没有想过,手机里的安卓系统其实是个大宝藏呢?它不仅能让你的生活变得丰富多彩,还能让你成为编程小达...
华为平板安卓7.1系统,探索性... 你有没有发现,最近华为平板的新款机型简直让人眼前一亮?没错,我要跟你聊聊的就是这款搭载了安卓7.1系...
鸿蒙系统安卓怎么升级,轻松实现... 你有没有发现,最近你的华为手机好像有点不一样了?没错,那就是鸿蒙系统升级的魅力!今天,就让我来带你一...
安卓引导系统的软件,架构、功能... 你有没有发现,每次打开安卓手机,那熟悉的引导系统就像一位热情的导游,带你一步步走进这个五彩斑斓的数字...
谷歌做的安卓系统,安卓系统的创... 亲爱的读者,你是否曾好奇过,那个几乎无处不在的安卓系统,究竟是由谁打造的呢?没错,就是那个改变世界的...
安卓系统总提示更新系统,体验流... 手机又闹腾了!安卓系统总提示更新系统,你是不是也和我一样,每次看到这个提示就有点头疼呢?别急,今天就...
aos是安卓系统么?,安卓系统... 你有没有想过,手机里的那个神秘的aos系统,它是不是安卓家族的一员呢?今天,就让我带你一探究竟,揭开...
诺基亚8刷安卓系统,解锁无限可... 你手中的诺基亚8是不是已经有点儿落伍了呢?别急,今天就来给你支个招,让你的老伙计焕发新生,变身安卓小...
安卓系统能不能,可以。 你有没有想过,安卓系统到底能不能?这个问题,就像是在问一个老朋友,他是不是真的懂你。安卓系统,这个陪...
安卓系统恢复误删视频,轻松找回... 手机里的视频突然不见了,是不是你也遇到了这样的尴尬情况?别急,今天就来教你如何用安卓系统恢复误删的视...