요약
이 프로젝트는 Seeed Wio Terminal을 오존(O3)과 미세먼지(PM2.5 및 PM10) 데이터를 보여주는 원격 실외 공기질 디스플레이로 설정합니다. Machinechat의 JEDI One IoT 데이터 플랫폼으로 MQTT를 사용하여 라이브 센서 데이터를 구독함으로써 Wio Terminal은 WiFi로 AQI 데이터를 수신합니다. Wio Terminal 애플리케이션의 구현에 아두이노를 사용하며 JEDI One은 라즈베리 파이 4에 설치해서 구동합니다.
하드웨어
- RASPBERRY PI 4B/4GB
라즈베리 파이 4 모델 B 4GB SDRAM - Seeed Wio Terminal
Wio Terminal은 독립형의 아두이노/마이크로파이썬(MicroPython) 호환 SAMD51 기반 마이크로컨트롤러 평가 플랫폼으로 와이파이/블루투스 기능은 리얼텍 RTL8720DN으로 구동됩니다. 2.4인치 LCD 스크린, 온보드 IMU(LIS3DHTR), 마이크, 버저, 마이크로SD 카드 슬롯, 조도 센서 및 적외선 이미터(IR 940nm)가 장착되어 있습니다.
소프트웨어
- JEDI One
JEDI One은 바로 사용할 수 있는 IoT 데이터 관리 소프트웨어 솔루션입니다. 기능에는 센서, 장치 및 기계로부터 데이터 수집, 직관적인 실시간 및 과거 데이터와 시스템 뷰 대시보드 구축, 데이터를 모니터링하여 자동으로 데이터 조건에 응답하는 규칙 생성, 이메일과 SMS로 경고 알림 수신 등이 있습니다. - 아두이노
아두이노는 사용하기 쉬운 하드웨어와 소프트웨어에 기반한 오픈 소스 전자 장치 플랫폼입니다.
배경
미국 EPA(Environmental Protection Agency, 환경보호청)의 모니터 요원이 감시하는 주요 대기 오염원은 오존과 미세먼지입니다. https://www.airnow.gov에서 발췌한 아래 지도는 미국 전역의 공기질에 대한 정보를 제공하는 대화형 GUI입니다.
지표(ground-level) 오존은 특히 어린이, 노인 그리고 천식과 같은 폐 질환이 있는 모든 연령대의 사람들에게 다양한 건강 문제를 유발할 수 있습니다. 미세먼지(PM2.5 및 PM10)는 미세한 고체 또는 액체 입자를 포함하고 있으며, 이는 흡입될 수 있어서 심각한 건강 문제를 야기할 수 있습니다.
구현
프로젝트에서 Wio Terminal은 원격 실외 공기질 디스플레이의 구현에 사용되며 아두이노는 코드의 구현에 사용됩니다. JEDI One에서 실행되는 MQTT 브로커에 와이파이로 연결되도록 Wio Terminal을 설정합니다. 연결이 되면, Wio Terminal은 JEDI One에서 수집하는 오존 및 PM 공기질 센서 데이터를 구독합니다. LCD 디스플레이는 아래와 같이 공기질 정보를 제공하는 데 사용됩니다.
디스플레이에는 AQI 값과 색깔로 구분되는 AQI 수준, 가장 최근 수신된 데이터의 타임스탬프, 그리고 두 AQI 값의 최근 50개 센서 판독 값을 보여주는 롤링 차트가 표시됩니다. JEDI One은 또한 센서 데이터를 센서 대시보드의 위젯에 표시하도록 설정됩니다.
참고: 이 프로젝트는 Machinechat의 JEDI One IoT 데이터 플랫폼에 오존과 PM 데이터를 제공하는 두 개의 다른 Machinechat 센서 프로젝트를 기반으로 합니다. 첫 번째 프로젝트는 르네사스 ZMOD4510 실외 공기질 센서를 사용하는 Machinechat이며, 두 번째 프로젝트는 파나소닉 SN-GCJA5 공기질 (PM) 센서를 사용하는 Machinechat입니다.
아두이노 애플리케이션에 맞게 Wio Terminal 플랫폼 설정
1 - Wio Terminal에 아두이노를 설정합니다. 링크 참조: Getting started with Wio Terminal
참고: 리얼텍 RTL8720DN 펌웨어가 Update the Wireless Core Firmware에 따라 업데이트되어 있는지 확인합니다.
2 - 애플리케이션에 필요한 라이브러리를 설치합니다. 아두이노의 Library Manager를 통해 아래 라이브러리를 추가합니다:
- Adafruit Zero DMA
- Seeed_Arduino_Linechart
- ArduinoJson
- PubSubClient
와이파이에 필요한 라이브러리 - Seeed_Arduino_rpcWiFi
- Seeed_Arduino_rpcUnified
- Seeed_Arduino_mbedtls
- Seeed_Arduino_FS
- Seeed_Arduino_SFUD
참고: "Free_Fonts.h"와 “Arduino_secret.h” 파일을 프로젝트 디렉토리에 추가합니다. Free_Fonts.h는 ~/Arduino/library/Seeed_LCD_master/예제/320 x 240/All_Free_Fonts_Demo에 있습니다.
3 - 코드 검토 (파일명: WioTerminalMQTTDisplay2topicOAQrev2.ino)
LCD 디스플레이 설정, 와이파이 네트워크 연결, MQTT 브로커 연결 및 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);
}
메인 루프는 MQTT 연결을 확인하고, MQTT 메시지를 처리하며 수신된 센서 데이터의 최신 라인 차트를 LCD 디스플레이에 전송합니다.
void loop() {
// check if connected to mqtt
if (!client.connected()) {
reconnect();
}
if (msg == 1) { // check if new callback message
msg = 0; // reset message flag
Serial.print("decoded timestamp = ");
Serial.println(stamp);
Serial.print("decoded data1 = ");
Serial.println(data1);
Serial.print("decoded data2 = ");
Serial.println(data2);
// update line chart code
// chart consists of two lines tracking values data1 and data2
Serial.print("data[].size = ");
Serial.println(data[0].size()); //debug - print data[] size for line chart
if (data[0].size() == max_size) {
for (uint8_t i = 0; i<2; i++){
data[i].pop(); //this is used to remove the first read variable when max_size reached
}
}
data[0].push(1.0 * data1); //read variables and store in line chart data array
data[1].push(1.0 * data2);
// Serial.println(data[0]); //debug
spr.fillSprite(TFT_WHITE); //clear line chart area
//Settings for the line graph title
auto header = text( 0, 100)
.value("test")
.align(center)
.valign(vcenter)
.width(tft.width())
.thickness(3);
header.height(header.font_height() );
// header.draw(); //Header is not used so disabled
auto content = line_chart(20, header.height()-10); //(x,y) where the line graph begins
content
.height(tft.height() - 140) //header.height() * 1.5) //actual height of the line chart
.width(tft.width() - 20) //actual width of the line chart
.based_on(0.0) //Starting point of y-axis, must be a float
.show_circle(false) //drawing a cirle at each point, default is on so set to false.
.value({data[0], data[1]}) //passing through the data to line graph
.color(TFT_PURPLE, TFT_BLUE) //Setting the color for each line
.draw();
spr.pushSprite(0, 130); //display line chart
}
Serial.println("debug - in main loop");
delay(2000);
client.loop();
}
4 - 아두이노 코드의 직렬 모니터 출력 예시
디버깅 및 코드 수정 시 도움이 될 수 있도록 예제 아두이노 코드에 다양한 프린트문이 삽입되어 있습니다. 아래는 출력 예시입니다.
5 - WioTerminalMQTTDisplay2topicOAQrev2.ino 애플리케이션의 최신 소스 코드는 아래 링크의 깃허브에 있습니다:
JEDI One 설정
1 - Machinechat의 JEDI One이 라즈베리 파이에 아직 설치되지 않은 경우 아래를 참조하십시오:
- JEDI One의 라즈베리 파이 버전인 DK-JEDIONE-RP 구매
- 라즈베리 파이에 설치, Raspberry Pi - Installing JEDI One as a Service를 참조하십시오
2 - JEDI One을 위에서 언급한 두 프로젝트에 설명된 대로 설정합니다. 첫 번째 프로젝트는 르네사스 ZMOD4510 실외 공기질 센서를 사용하는 Machinechat이며, 두 번째 프로젝트는 파나소닉 SN-GCJA5 공기질 (PM) 센서를 사용하는 Machinechat입니다.
JEDI One 공기질 데이터의 스크린샷 예시.
결론
Seeed의 Wio Terminal은 실외 공기질 모니터링을 위한 저비용 풀 컬러 그래픽의 원격 MQTT 데이터 디스플레이를 쉽게 구현할 수 있도록 해줍니다. 내장 와이파이와 관련 아두이노 라이브러리를 통해 라즈베리 파이에서 구동되는 JEDI One MQTT 브로커에 직접 연결할 수 있습니다. Machinechat의 JEDI One IoT 데이터 관리는 수집 중인 모든 데이터에 대한 MQTT 접근 뿐만 아니라 추가적인 처리, 경고 및 기타 작업도 가능합니다.
참고 자료
- Seeed - Wio Terminal 제품 페이지
- Seeed - Wio Terminal 위키
- Seeed - Wio Terminal LCD 라인 차트
- Seeed - Wio Terminal LCD 폰트
- Machinechat의 JEDI One IoT 플랫폼 시작하기
- 아두이노 - How to use ArduinoJson with PubSubClient?
- Machinechat 제품 안내 - Built In MQTT Broker - Data Collector
- Machinechat 제품 안내 - MQTT Broker - Subscribing to a topic
- HIVEMQ - MQTT Essentials
- EPA - 지표 오존 오염 웹사이트
- EPA - 미세먼지 (PM) 오염 웹사이트
영문 원본: Set up a Wio Terminal as a Remote Outdoor Air Quality Monitor Display using Machinechat and MQTT