使用 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提供了对其收集的任何数据的访问权限,以及额外的处理、警报或其他操作。

参考文献