物联网服务NodeJs-5天学习第三天实战篇② ——基于物联网的WiFi自动打卡考勤系统
Posted 单片机菜鸟哥
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了物联网服务NodeJs-5天学习第三天实战篇② ——基于物联网的WiFi自动打卡考勤系统相关的知识,希望对你有一定的参考价值。
【NodeJs-5天学习】第三天实战篇② ——基于物联网的WiFi自动打卡考勤系统
面向读者群体
- ❤️ 电子物联网专业同学,想针对硬件功能构造简单的服务器,不需要学习专业的服务器开发知识 ❤️
- ❤️ 业余爱好物联网开发者,有简单技术基础,想针对硬件功能构造简单的服务器❤️
- ❤️ 本篇创建记录 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地址和用户信息进行比对,然后发送到班级飞书群
里面。
具体帧的含义请参考
这里涉及到的知识点:
- 【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.mac
var value = element.name
configMap.set(key, value)
)
console.log(configMap)
// 3、挂载具体的路由
// 配置add URL请求处理
// 参数1:客户端请求的URL地址
// 参数2:请求对应的处理函数
// req:请求对象(包含与请求相关属性方法)
// res:响应对象(包含与响应相关属性方法)
router.post('/api/add/check', (req, res) =>
var body = req.body
var datas = body.datas
var name = ''
var date = time.getCurrentDate()
var fileName = './storage/fs/' + date + '_打卡记录.txt';
if (datas)
datas.forEach(element =>
var value = element.value
name = 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\\":\\"<at user_id=\\\\\\"all\\\\\\">所有人</at> %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 <ESP8266WiFi.h> // 引入WiFi核心库
#include <ArduinoJson.h>
#include <ESP8266HTTPClient.h> // 引入HttpClient库
#include <stdlib.h> // 引入定时库
#include <Ticker.h>
#include "H_project.h" // 上传服务相关
#include "H_80211Frame.h" // 混杂模式相关定义库
void setup()
// put your setup code here, to run once:
initSystem();
void loop()
//每1s切换一次信道 也就是每个信道的工作时间是1s
if (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]以上是关于物联网服务NodeJs-5天学习第三天实战篇② ——基于物联网的WiFi自动打卡考勤系统的主要内容,如果未能解决你的问题,请参考以下文章
物联网服务NodeJs-5天学习第三天实战篇④ ——QQ机器人,实现自动回复重要提醒
物联网服务NodeJs-5天学习第三天实战篇① ——10行代码给她造个熬夜提醒睡觉机器人
物联网服务NodeJs-5天学习第四天存储篇② ——NodeJs连接操作mysql 8.0
物联网服务NodeJs-5天学习第二天篇② —— 网络编程(TCPHTTPWeb应用服务)