使用 Machinechat 和 MQTT 设置一个 Wio Terminal 作为远程室外空气质量监测显示器

描述

该项目设置了一个Seeed Wio Terminal,作为远程室外空气质量显示器,显示臭氧(O3)和颗粒物(PM2.5和PM10)数据。Wio Terminal通过Machinechat的JEDI One物联网数据平台订阅MQTT实时传感器数据,通过WiFi接收AQI数据。Wio Terminal应用使用Arduino实现,JEDI One安装并运行在Raspberry Pi 4上。

硬件

  • RASPBERRY PI 4B/4GB
    RASPBERRY PI 4 Model B with 4GB SDRAM
  • Seeed Wio Terminal
    Wio Terminal是一个独立的Arduino/MicroPython兼容samd51基于微控制器评估平台与WiFi/蓝牙由Realtek RTL8720DN供电。它配备了2.4英寸LCD屏幕,板载IMU(LIS3DHTR),麦克风,蜂鸣器,microSD卡插槽,光传感器和红外发射器(IR 940nm)。

软件

  • JEDI One
    JEDI One是一款即用型物联网数据管理软件解决方案。功能包括:收集来自传感器、设备和机器的数据;构建直观的实时和历史数据以及系统视图仪表板;创建规则,自动监控和响应数据情况;通过电子邮件和短信接收警报通知。
  • Arduino
    是一个基于易于使用的硬件和软件的开源电子平台。

背景

美国环境保护署(EPA)监测的主要空气污染物是臭氧和颗粒物。下图是来自https://www.airnow.gov/的交互式GUI,提供了美国各地的空气质量信息。

image

地面臭氧会引发各种健康问题,尤其是儿童、老年人以及患有哮喘等肺部疾病的各个年龄段的人。颗粒物(PM2.5和PM10)含有微小的固体或液体液滴,可被吸入并引起严重的健康问题。

实现

本项目使用Wio Terminal 实现远程室外空气质量显示,使用Arduino实现代码。Wio Terminal 设置为通过WiFi连接到运行在JEDI One上的MQTT BROKER。一旦连接,它就会订阅JEDI One收集的臭氧和PM空气质量传感器数据。LCD显示屏用于提供空气质量信息,如下图所示。

显示器显示AQI值,颜色编码的AQI水平,颜色编码的时间戳,最近收到的数据和滚动的两个值图表显示最近50个传感器读数。JEDI One也被配置为在传感器仪表板上的小部件中显示传感器数据。

为Arduino应用搭建Wio Terminal 平台

1 - 在Wio Terminal 上设置Arduino。请参阅链接开始使用Wio Terminal
注意:确保Realtek RTL8720DN固件按照这里更新

2 - 安装应用程序所需的库。通过Arduino的库管理器添加这些库:

注:添加“Free_Fonts.h”和“arduino_secrets.h”文件到项目目录 (Free_Fonts.h is located in ~/Arduino/libraries/Seeed_LCD_master/examples/320 x 240/All_Free_Fonts_Demo)

3 - 代码演练(文件名:WioTerminalMQTTDisplay2topicOAQrev2.ino)

设置LCD显示屏,连接WiFi网络,连接MQTT BROKER,解析JSON MQTT消息

/*
 Wio Terminal MQTT data display example 
 filename: WioTerminalMQTTDisplay2topicOAQ.ino
 project based on WioTerminalMQTTDisplay.ino which is a sketch that demonstrates using the Wio Terminal to mqtt subscribe to air quality sensor data on the 
 machinechat JEDI One IoT data platform. It uses WiFi to connect to the JEDI One MQTT broker.
 Ozone air quality sensor data - Renesas
 Particle air quality sensor date - Panasonic
 11/10/2021 - this project takes sketch WioTerminalMQTTDisplay.ino and modifies to subscribe to 2 topics and changes topics to outdoor air quality sensors for 
 ozone and particulate matter to display AQI
 11/16/2021 - clean up code and change to version to "_rev1", also add 3rd subtopic for testing
 3/30/2022 - rev2 change back to subscribe to just two topics (comment out subtopic1
*/

#include <rpcWiFi.h>
#include <PubSubClient.h>
#include <TFT_eSPI.h>
#include "Free_Fonts.h"
#include"seeed_line_chart.h" //include the library

TFT_eSPI tft;

// for LCD line chart
#define max_size 50 //maximum size of data
doubles data[2]; //Initilising a doubles type to store data
TFT_eSprite spr = TFT_eSprite(&tft);  // Sprite 


char value[7] = "      "; //initial values
char value2[7] = "      ";
char stamp[40] = "2021-09-01T13:04:34"; 

//#include <ESP8266WiFi.h>
//#include <PubSubClient.h>
#include <ArduinoJson.h>
#include "arduino_secrets.h" 

// Update these with values suitable for your network.
char ssid[] = SECRET_SSID;     // your network SSID (name)
char pass[] = SECRET_PASS;    // your network password (use for WPA, or use as key for WEP)

// MQTT server info for JEDI One
const char* mqttServer = "192.168.1.7";
const int mqttPort = 1883;

StaticJsonDocument<256> doc;

int data1 = 0;  //data1 of MQTT json message
int data2 = 0;  //data2 of MQTT json message
int data3 = 0;  //data3 of MQTT json message
int data4 = 0;  //data3 of MQTT json message
int msg = 0;
const char* timestamp = "dummy data";  //the is the MQTT message timestamp (this works)
String recv_payload;
const char* subtopic1 = "datacache/T960981B2D";                //pump house humidity, temp sensor
const char* subtopic2 = "datacache/MKR1010_Z4510oaq2S31PMOD";  //ZMOD4510 air quality/ozone sensor
const char* subtopic3 = "datacache/MKR1010WiFiSensor_SNGCJA5"; //SNGCJA5x air quality particl sensor

// wio terminal wifi 
WiFiClient wclient;
PubSubClient client(wclient); // Setup MQTT client

void setup_wifi() {

  delay(10);
  // We start by connecting to a WiFi network
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, pass);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  randomSeed(micros());
  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
}

// mqtt message callback
void callback(char* topic, byte* payload, unsigned int length) {

  //print out message topic and payload  
  Serial.print("New message from Subscribe topic: ");
  Serial.println(topic);
  Serial.print("Subscribe JSON payload: ");
  for (int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);     // print mqtt payload
  }
  Serial.println();

  msg = 1;  //set message flag = 1 when new subscribe message received

  
  //check subscribe topic for message received and decode    
  
  //********************************//
  //if message from topic subtopic1
  //********************************//
  if (strcmp(topic, subtopic1)== 0) {  
    // "clear" old data by rewriting old info in white
    tft.setTextColor(TFT_WHITE);   //"clear" old data by rewriting old info in white
    tft.setFreeFont(FF19);
    tft.drawString(value,200,78); 
    //tft.drawString(value2,60,78);  //value2 is same as data2
    tft.setFreeFont(FF18);
    tft.drawString(stamp,60,110);             

    Serial.print("decode payload from topic ");
    Serial.println(topic);
    deserializeJson(doc, (const byte*)payload, length);   //parse MQTT message
    data1 = doc["data1"];    // data1 is humidity
    //data2 = data4;           // data4 is O3 FastAQI
    //data2 = doc["data2"];    // data2 is temp
    //data3 = doc["data3"];    // data3 is empty
    timestamp = doc["timestamp"];    //mqtt message timestamp
    //  stamp = timestamp;
    strcpy (stamp,timestamp);
    stamp[19] = 0;   // terminate string after seconds
    Serial.println(stamp);     //debug print

    tft.setTextColor(TFT_MAGENTA);
    tft.setFreeFont(FF19);     
    itoa(data1,value,10);  //convert data integer value to character
    tft.drawString(value,200,78);//prints data at (x,y)
    //itoa(data2,value2,10);  
    //tft.setTextColor(TFT_BLUE);
    //tft.drawString(value2,60,78);  
    tft.setTextColor(TFT_BLACK);
    tft.setFreeFont(FF18);
    tft.drawString(stamp,60,110); //print timestamp
  }
  //********************************//
  //if message from topic subtopic2
  //********************************//
  if (strcmp(topic, subtopic2)== 0) {   
    // "clear" old data by rewriting old info in white
    tft.setTextColor(TFT_WHITE);   //"clear" old data by rewriting old info in white
    tft.setFreeFont(FF19);
    tft.drawString(value2,50,78);  //"clear" old value2 (is same as data2)  
    tft.setFreeFont(FF18);
    tft.drawString(stamp,60,110);   //"clear" old timestamp      
    
    Serial.print("decode payload from topic ");
    Serial.println(topic);
    deserializeJson(doc, (const byte*)payload, length);   //parse MQTT message
//    data1 = doc["data1"];    
    data2 = doc["FastAQI"];    // 
    itoa(data2,value2,10); 
    tft.setFreeFont(FF19); 
    tft.setTextColor(TFT_BLUE);
    tft.drawString(value2,50,78);  //print new value2 on LCD
    Serial.print("data2 is O3 FastAQI = ");
    Serial.println(data2);
    
    timestamp = doc["timestamp"];    //mqtt message timestamp
    //  stamp = timestamp;
    strcpy (stamp,timestamp);
    stamp[19] = 0;   // terminate string after seconds     
    tft.setTextColor(TFT_BLUE);
    tft.setFreeFont(FF18);
    tft.drawString(stamp,60,110); //print new timestamp on LCD   
    
    //print out color of "AQI" font based on value of AQI
    // <50=green,51to100=yellow,101to150=orange,>150=red
    if (data2 < 51) {
      tft.setTextColor(TFT_GREEN);
    }
    else if (data2 < 101){
      tft.setTextColor(TFT_YELLOW);
    }
    else if (data2 < 151){
      tft.setTextColor(TFT_ORANGE);
    }
    else {
      tft.setTextColor(TFT_RED);  
    }
    tft.setFreeFont(FF18);
    tft.drawString("AQI",100,78);
     
  }
    
  //********************************//
  //if message from topic subtopic3
  //********************************//
  if (strcmp(topic, subtopic3)== 0) {   
    // "clear" old data by rewriting old info in white
    tft.setTextColor(TFT_WHITE);   //"clear" old data by rewriting old info in white
    tft.setFreeFont(FF19);
    tft.drawString(value,200,78);  //"clear" old value   
    tft.setFreeFont(FF18);
    tft.drawString(stamp,60,110);   //"clear" old timestamp   

    Serial.print("decode payload from topic ");
    Serial.println(topic);
    deserializeJson(doc, (const byte*)payload, length);   //parse MQTT message
    data3 = doc["aqiPM2_5"];
    data4 = doc["aqiPM10"];
    Serial.print("data3 = aqiPM2_5 = ");
    Serial.println(data3);
    Serial.print("data4 = aqiPM10 = ");
    Serial.println(data4);
    // determine which PM2.5 or PM10 is larger to use when displaying PM AQI
    if (data3 >= data4) {
      data1 = data3;
    }
    else {
      data1 = data4;
    }
    Serial.print("Particle AQI = ");
    Serial.println(data1);
    tft.setTextColor(TFT_MAGENTA);
    tft.setFreeFont(FF19);     
    itoa(data1,value,10);  //convert data integer to character "value"
    tft.drawString(value,200,78);//prints "value" at (x,y)

    timestamp = doc["timestamp"];    //mqtt message timestamp
    strcpy (stamp,timestamp);
    stamp[19] = 0;   // terminate string after seconds     
    tft.setTextColor(TFT_MAGENTA);
    tft.setFreeFont(FF18);
    tft.drawString(stamp,60,110); //print new timestamp on LCD 

    //print out color of "AQI" font based on value of AQI
    // <50=green,51to100=yellow,101to150=orange,>150=red
    if (data1 < 51) {
      tft.setTextColor(TFT_GREEN);
    }
    else if (data1 < 101){
      tft.setTextColor(TFT_YELLOW);
    }
    else if (data1 < 151){
      tft.setTextColor(TFT_ORANGE);
    }
    else {
      tft.setTextColor(TFT_RED);  
    }
    tft.setFreeFont(FF18);
    tft.drawString("AQI",250,78);
  }
}

//connect to mqtt broker on JEDI One and subscribe to topics
void reconnect() {
  // Loop until we're reconnected
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Create a unique client ID using the Wio Terminal MAC address
    String MACadd = WiFi.macAddress();
    MACadd = "WioTerminal" + MACadd;  
    String clientID = MACadd;

    // Attempt to connect
    if (client.connect(clientID.c_str())) {
      Serial.println("connected");
      // set up MQTT topic subscription     note: topic needs to be "datacache/" + Device on JEDI One 
      Serial.println("subscribing to topics:");
      //Serial.println(subtopic1);        //comment out subtopic1 for this revision of code
      //client.subscribe(subtopic1);        
      Serial.println(subtopic2);
      client.subscribe(subtopic2); 
      Serial.println(subtopic3);
      client.subscribe(subtopic3); 
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}

void setup() {
  // setup lcd display
  tft.begin();
  tft.setRotation(3);
  tft.fillScreen(TFT_WHITE); //Black background
  spr.createSprite(320,110); //try 320x110
  tft.setFreeFont(FF19); //select font
  tft.setTextColor(TFT_BLACK  );
  tft.drawString("Outdoor Air ",5,5);
  tft.drawString("Quality",195,5);
  tft.setTextColor(TFT_BLACK);
  tft.setFreeFont(FF18);
  tft.drawString("  Ozone AQI:    Particle AQI:",10,50);
  tft.drawString(stamp,60,110);
  tft.setFreeFont(FF18);
  tft.setTextColor(TFT_BLACK);
  tft.drawString("AQI",250,78);
  tft.drawString("AQI",100,78);  
  tft.fillRect(0, 40, 320, 2, TFT_BLACK);
  
  Serial.begin(115200);
  setup_wifi();
  client.setServer(mqttServer, 1883);  //set mqtt server
  client.setCallback(callback);
}

4 - Arduino代码的串口监视器输出示例

在示例Arduino代码中插入了各种Print语句,以便在调试和代码修改期间提供帮助。示例输出如下所示。

5 - Latest source code for the WioTerminalMQTTDisplay2topicOAQrev2.ino应用程序在github上的链接如下:

eewiki/machinechat/blob/master/Wio Terminal/WioTerminalMQTTDisplay2topicOAQrev2.ino

设置JEDI One

1 - 如果machinechat JEDI One尚未安装在Raspberry Pi上,请参见以下内容:

2 - 按照参考的两个项目中描述的设置JEDI One。

示例JEDI One空气质量数据截图。

结论

Seeed的Wio Terminal 可以轻松实现户外空气质量的低成本全彩图形远程MQTT数据显示。它的内置WiFi和相关的Arduino库可以直接连接到Raspberry Pi上运行的JEDI One MQTT BROKER。Machinechat的JEDI One物联网数据管理为MQTT提供了对其收集的任何数据的访问权限,以及额外的处理、警报或其他操作。

参考文献

2 个赞

Wio Terminal(边缘端数据采集+显示) + Machinechat(云端数据处理+管理)的分层架构极具性价比,尤其适合中小型环境监测项目。其优势在于:

低延迟显示:终端直接解析MQTT消息并刷新本地屏幕,避免云端到设备的显示延迟

数据韧性:Machinechat内置存储可在网络中断时缓存数据,恢复后自动同步

扩展灵活:通过MQTT主题轻松添加新传感器节点(如温湿度、CO₂)

1 个赞

结合Seeed Wio Terminal和树莓派4B,实现了基于物联网AIoT平台和MQTT通信协议的环境监测系统,有利于检测环境健康指标,增强生活便利性和舒适度,是很好的AIoT和环境计算实际案例。

非常感谢您的分享,这个基于 Wio Terminal + Machinechat JEDI One + MQTT 的远端室外空气质量监测系统,架构清晰、实现完整,值得学习与推广。通过 Raspberry Pi 4上的 JEDI One MQTT broker,结合 Wio Terminal 的 Wi‑Fi 与 Arduino JSON/PubSubClient 代码,实现 O₃、PM2.5、PM10 数据的实时订阅与 LCD 显示,实测效果包括图表滚动、时间戳更新与 AQI 颜色编码功能。这套方案真正体现了“采集—传输—可视化—管理”端到端的 IoT 流程。

为了让项目更具实用性和扩展性,建议考虑以下方向:

订阅主题配置化:将子主题(subtopic)从代码中抽出为配置文件或通过 OTA 参数更新,能提升可维护性和灵活性。
数据刷新节制策略:当前的 loop() 逻辑中每次新消息都会刷新图表,若传感频率高,可能造成界面闪烁。建议增加节流或阈值判断,比如每隔固定秒刷新一次或只有数据差异超过一定值时才更新。
UI 可视化增强:除文字和折线图外,考虑加入 AQI 等级图标、背景渐变、颜色块等,提升用户视觉感知;并且可以在屏幕底部显示历史趋势或最大/最小值。
远程调试与告警机制:可利用 JEDI One 的规则引擎配置阈值报警,通过 MQTT/wifi 驱动终端蜂鸣器、LED 或在云端发送 Email、短信互联,实现异常提醒功能。
硬件与能源适配:若部署于户外长期运行,建议配合太阳能+锂电池供电方案,同时优化 Wi‑Fi 断线重连与低功耗睡眠策略,以保证续航和稳定性。

整体而言,你的实现不仅易于复用,也为后续多点布局提供了基础,完全可以延伸成校园、城市乃至园区级空气监测终端网络。期待你及社区其他成员继续优化,比如加入历史回溯功能、地图定位展示、AI 预测趋势等,进一步提升实用价值。