介绍
基于离线版的Arduino 小白的 DIY 空气质量检测仪(1~5),这里将扩展实现一个 WIFI 联网版的 DIY 空气质量检测仪。
- Arduino Nano 开发板作为传感器数据采集端,仓库在这里: arduino-air-monitor
- ESP8266 NodeMcu 开发板作为数据中转端,仓库在这里:arduino-air-monitor-transfer
- Linux Alpine + NodeJS 服务端作为数据接收、存储、分析端
接线图
主要就是在原来的接线基础上,增加了 ESP8266 开发板用作数据中转。
当然,这里有点“大材小用”了,其实可以使用其它更小ESP系列开发板,在这里,买了哪个就用哪个了,还带一个显示屏,方便一些。
串口通讯 - 单向
本章主要是解决 Arduino Nano 和 ESP8266 NodeMcu 两个开发板的通讯,基于 SoftwareSerial RX TX,本示例只需要实现 Arduino Nano 的 TX 到 ESP8266 NodeMcu 的 RX 即可。
本示例 Arduino Nano 使用它的 D11 作为 TX 引脚,而 ESP8266 NodeMcu 使用它的 D1 作为 RX 引脚,接一根线作为单向通讯线即可。
因此,下面将
在 arduino-air-monitor 中新增 tx.h 实现数据发送逻辑;
在 arduino-air-monitor-transfer 中新增 rx.h 实现数据接收逻辑。
数据帧定义
// 起始符 x 2 [2字节] 0XFF、0XFF
// display [1字节] 显示屏是否显示
// hcho [4字节]
// tvoc [4字节]
// co2 [4字节]
// temp [4字节]
// hum [4字节]
// uv [4字节]
// 检验位 [4字节] display ~ uv 的总和
这里传感器的数据都以 float 类型传递,均需要占用 4 个字节。
总得的下来,数据帧总长为 31 个字节。
数据发送
// arduino-air-monitor/tx.h
#include <SoftwareSerial.h>
// 实际可不接此引脚
#define _Pin_RX 10
// 实际只用此引脚
#define _Pin_TX 11
// 起始符
#define _Frame_Start 0XFF
// 数据帧长度:
#define _Frame_Len 31
namespace Transfer {
SoftwareSerial _serial(_Pin_RX, _Pin_TX);
uint8_t floatSize = sizeof(float); // 4
uint8_t int32Size = sizeof(int32_t); // 4
unsigned char floatBytes[4];
unsigned char int32Bytes[4];
struct _TX {
void init() {
_serial.begin(9600);
}
// display: 是否显示
void send(uint8_t display, float hcho, float tvoc, float co2, float temp, float hum, float uv) {
// 数据帧
byte buffer[_Frame_Len] = {};
// 下标
uint8_t idx = 0;
// 起始符
buffer[0] = _Frame_Start;
buffer[1] = _Frame_Start;
idx += 2;
// 是否显示
buffer[2] = (unsigned char)display;
idx += 1;
// 4位/指标
memcpy(floatBytes, &hcho, floatSize);
for (uint8_t i = 0; i < floatSize; i++) {
buffer[idx + i] = (unsigned char)floatBytes[i];
}
idx += floatSize;
memcpy(floatBytes, &tvoc, floatSize);
for (uint8_t i = 0; i < floatSize; i++) {
buffer[idx + i] = (unsigned char)floatBytes[i];
}
idx += floatSize;
memcpy(floatBytes, &co2, floatSize);
for (uint8_t i = 0; i < floatSize; i++) {
buffer[idx + i] = (unsigned char)floatBytes[i];
}
idx += floatSize;
memcpy(floatBytes, &temp, floatSize);
for (uint8_t i = 0; i < floatSize; i++) {
buffer[idx + i] = (unsigned char)floatBytes[i];
}
idx += floatSize;
memcpy(floatBytes, &hum, floatSize);
for (uint8_t i = 0; i < floatSize; i++) {
buffer[idx + i] = (unsigned char)floatBytes[i];
}
idx += floatSize;
memcpy(floatBytes, &uv, floatSize);
for (uint8_t i = 0; i < floatSize; i++) {
buffer[idx + i] = (unsigned char)floatBytes[i];
}
idx += floatSize;
// 检验位计算(指标位之和)
int32_t sum = 0;
for (int i = 2; i < _Frame_Len - int32Size; i++) {
sum += buffer[i];
}
memcpy(int32Bytes, &sum, int32Size);
for (uint8_t i = 0; i < int32Size; i++) {
buffer[idx + i] = (unsigned char)int32Bytes[i];
}
// 发送
for (uint8_t i = 0; i < _Frame_Len; i++) {
_serial.write(buffer[i]);
}
}
} TX;
}
整体逻辑本身并不复杂,send 把传入的起始符、所有数据变量、校验位,通过 memcpy 一个个塞到 buffer 之中。最后,用 _serial.write 按字节一个个发送出去。
需要注意点是,校验位要使用 int32_t 而不是 int,实测在 Arduino Nano 中测试 sizeof(int) 为 2 个字节,而在 ESP8266 NodeMcu 却是 4 个字节,为了稳定准确需使用 int32_t(4字节)。
数据接收
// arduino-air-monitor-transfer/rx.h
#include <SoftwareSerial.h>
// 实际只用此引脚
#define _Pin_RX D1
// 实际可不接此引脚
#define _Pin_TX D2
// 起始符
#define _Frame_Start 0XFF
// 数据帧长度:
#define _Frame_Len 31
namespace Transfer {
SoftwareSerial _serial(_Pin_RX, _Pin_TX);
uint8_t floatSize = sizeof(float);
uint8_t int32Size = sizeof(int);
unsigned char floatBytes[4];
unsigned char int32Bytes[2];
struct _RX {
void init() {
_serial.begin(9600);
}
bool check(byte* buffer) {
// 检验位计算
int32_t sum = 0;
for (int i = 2; i < _Frame_Len - int32Size; i++) {
sum += buffer[i];
}
// 检验位记录
for (uint8_t i = 0; i < int32Size; i++) {
int32Bytes[i] = buffer[_Frame_Len - int32Size + i];
}
int32_t sumCheck = 0;
memcpy(&sumCheck, int32Bytes, int32Size);
// 对比、排除空数据
return sum == sumCheck && sum > 0;
}
// 基于 JSON 结构:{"display":1,"hcho":1,"tvoc":2,"co2":3,"temp":4.5,"hum":6.7,"uv":8}
String parse(byte* buffer) {
if (check(buffer)) {
// 下标
uint8_t idx = 2;
uint8_t display = buffer[idx];
idx += 1;
float hcho;
for (uint8_t i = 0; i < floatSize; i++) {
floatBytes[i] = buffer[idx + i];
}
memcpy(&hcho, floatBytes, floatSize);
idx += floatSize;
float tvoc;
for (uint8_t i = 0; i < floatSize; i++) {
floatBytes[i] = buffer[idx + i];
}
memcpy(&tvoc, floatBytes, floatSize);
idx += floatSize;
float co2;
for (uint8_t i = 0; i < floatSize; i++) {
floatBytes[i] = buffer[idx + i];
}
memcpy(&co2, floatBytes, floatSize);
idx += floatSize;
float temp;
for (uint8_t i = 0; i < floatSize; i++) {
floatBytes[i] = buffer[idx + i];
}
memcpy(&temp, floatBytes, floatSize);
idx += floatSize;
float hum;
for (uint8_t i = 0; i < floatSize; i++) {
floatBytes[i] = buffer[idx + i];
}
memcpy(&hum, floatBytes, floatSize);
idx += floatSize;
float uv;
for (uint8_t i = 0; i < floatSize; i++) {
floatBytes[i] = buffer[idx + i];
}
memcpy(&uv, floatBytes, floatSize);
idx += floatSize;
return "{\"display\":" + (String)display + ",\"hcho\":" + (String)hcho + ",\"tvoc\":" + (String)tvoc + ",\"co2\":" + (String)co2 + ",\"temp\":" + (String)temp + ",\"hum\":" + (String)hum + ",\"uv\":" + (String)uv + "}";
}
return "";
}
String read() {
// 帧数据
byte buffer[_Frame_Len] = {};
// 上一位
byte last_byte = 0x00;
// 下标
uint8_t idx = 0;
// 开始组装帧数据
bool buffer_start = false;
// 组装帧数据
while (idx < _Frame_Len) {
// 串口可用
if (_serial.available() > 0) {
// 按位读数
int int_val = _serial.read();
byte byte_val = (byte)int_val;
// 识别起始(连续两个起始符)
if (byte_val == _Frame_Start && last_byte == _Frame_Start) {
// 标记开始
buffer_start = true;
buffer[0] = _Frame_Start;
buffer[1] = _Frame_Start;
idx = 2;
} else {
if (buffer_start) {
buffer[idx] = byte_val;
idx++;
} else {
last_byte = byte_val;
}
}
}
}
return parse(buffer);
}
} RX;
}
read
接收数据逻辑,简单的说,就是使用 _serial.read() 持续不断接收字节数据,一旦读取到连续 2 个起始符,就按数据帧长度开始组装 buffer 数据帧。
check
buffer 数据帧组装结束后,进行数据帧校验。把接收到的 display ~ uv 数据计算总和,最后与校验位进行对比,计算的和与校验位一致,即为有效数据,否则放弃。
parse
根据数据帧的定义,从数据帧中使用 memcpy 把字节数据恢复成 float 类型数据。最后,组装成 JSON 文本,将用于通过 WIFI + http 发送到 Linux Alpine + NodeJS 服务端,存储到硬盘目录中。
服务器接收成功,将显示在 ESP8266 NodeMcu 的显示屏中:
未完待续。。。
多多支持其它文章 juejin.cn/user/155656…
又或者请我喝杯奶茶😍 vue3-zoom-drag