小白的DIY Arduino+ESP8266 WIFI 空气质量检测仪(1)- 串口通讯

127 阅读6分钟

介绍

基于离线版的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

相关仓库 arduino-air-monitor arduino-air-monitor-transfer