COVID-19によりテレワーク中心の生活になった方が多くいらっしゃるかと思います。
こういう私も、ここ1年で出社したのは10回程度と、ほぼテレワークで仕事をしています。
さて皆さんのテレワーク環境は、仕事をするにあたり快適な環境を維持しているでしょうか?
特に二酸化炭素(CO2)の濃度が上昇すると集中力が低下し眠気を催すことが解っています。
(会議脱出ボタンの話題に絡むかもしれませんね)
国内の法律において、労働安全衛生法では労働環境は5,000ppm以下に保つ必要があると定められていますし、建築物における衛生的環境の確保に関する法律(ビル管理法)において3,000m2を超えるオフィスビル等においては 1,000ppm以下に保つ必要があると定められています。
(なお、大気中の二酸化炭素濃度は概ね400ppm程度です)
そこで業務環境を快適に保つため、CO2濃度を測定し一定値を超過した場合には通知するようにしたいと思います。
デバイス
今回は以下の組み合わせを準備しました。
- M5Stack (Basic or Gray)
- Grove – SCD30搭載 CO2・温湿度センサ(Arduino用)
なお、先に発売になったTVOC/eCO2 ガスセンサユニット(SGP30)も試しましたが、こちらは「CO2濃度はH2濃度に基づいて計算される」との記載の通り、異常値を示すことが多かったため、SCD30を採用した次第です。
アーキテクチャ
図に表すほどではありませんが、ひとまずこんな感じにしました。
Node-RED経由でローカル環境のZabbixに値を投入し可視化します。また、当方のZabbixはアラート発生時にBacklogに課題を自動起票するように設定されているので、アラート発生時は課題が追加されメールが届くようになります。
自宅内にNode-REDでMQTTブローカーを立ち上げて、M5Stackからのパブリッシュを受け付け、フォーマットを書き換えてZabbixに値を投入する流れです。

手順
デバイス側コード
M5Stack側のコードを記載します。
5秒おきにCO2濃度・温度・湿度の測定を測定し、30秒おきに平均値をMQTTブローカー宛にパブリッシュするよう設計しました。Node-REDのIPやWiFi関連パラメータは読み替えて下さい。
#include "M5Stack.h"
#include "SCD30.h"
#include "PubSubClient.h"
#include "WiFi.h"
#define SEND_INTERVAL (30000)
#define SENSOR_INTERVAL (5000)
#define MQTT_SERVER_HOST "Node-REDのホスト名orIP"
#define MQTT_SERVER_PORT (1883)
#define MQTT_OUTTOPICBASE "co2sensor"
const char *wifissid = "WiFi-SSID";
const char *wifipass = "WiFi-Password";
const char *thingname = "M5Stack_Gray";
#if defined(ARDUINO_ARCH_AVR)
#pragma message("Defined architecture for ARDUINO_ARCH_AVR.")
#define SERIAL Serial
#elif defined(ARDUINO_ARCH_SAM)
#pragma message("Defined architecture for ARDUINO_ARCH_SAM.")
#define SERIAL SerialUSB
#elif defined(ARDUINO_ARCH_SAMD)
#pragma message("Defined architecture for ARDUINO_ARCH_SAMD.")
#define SERIAL SerialUSB
#elif defined(ARDUINO_ARCH_STM32F4)
#pragma message("Defined architecture for ARDUINO_ARCH_STM32F4.")
#define SERIAL SerialUSB
#else
#pragma message("Not found any architecture.")
#define SERIAL Serial
#endif
WiFiClient NetClient;
PubSubClient MqttClient;
void setup() {
M5.begin();
M5.Power.begin();
// Initialize Serial Port
Serial.begin(9600);
// Initialize Display
M5.Lcd.clear(BLACK);
M5.Lcd.setTextColor(WHITE);
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(40, 10);
M5.Lcd.print("Startup...");
// Connect to WiFi
connect_wifi();
// Connect to MQTT Broker
connect_mqtt();
// Set LCD TextSize
M5.Lcd.setTextSize(3);
}
void connect_wifi() {
Serial.print("Connecting to ");
Serial.println(wifissid);
WiFi.begin(wifissid, wifipass);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi Connected.");
}
void connect_mqtt() {
Serial.print("Connecting to MQTT Broker : ");
Serial.print(MQTT_SERVER_HOST);
Serial.print(":");
Serial.println(MQTT_SERVER_PORT);
MqttClient.setServer(MQTT_SERVER_HOST, MQTT_SERVER_PORT);
MqttClient.setClient(NetClient);
if (!MqttClient.connect(thingname)) {
Serial.println(MqttClient.state());
return;
}
Serial.println("MQTT Server Connected.");
}
void loop() {
unsigned long now = millis();
int n = 0, i;
char payload[64];
char content_length_hdr[32];
char topicname[32];
float measure[3] = {0};
double sum[3] = {0}, ave[3];
while (millis() < now + SEND_INTERVAL)
{
// delay to next measurement
delay(SENSOR_INTERVAL);
// Measurement CO2
if (scd30.isAvailable()) {
scd30.getCarbonDioxideConcentration(measure);
for (i=0; i<=3; i++)
sum[i] = sum[i] + (double)measure[i];
n++;
}
// Output to Serial
SERIAL.print("CO2(ppm) : ");
SERIAL.println(measure[0]);
SERIAL.print("Temp(C) : ");
SERIAL.println(measure[1]);
SERIAL.print("Humi(%) : ");
SERIAL.println(measure[2]);
// Output to Display
M5.Lcd.clear(BLACK);
M5.Lcd.setCursor(40, 10);
M5.Lcd.print("CO2");
M5.Lcd.setCursor(40, 50);
M5.Lcd.print(measure[0]);
M5.Lcd.println(" ppm");
M5.Lcd.setCursor(40, 90);
M5.Lcd.print("Temp");
M5.Lcd.setCursor(40, 130);
M5.Lcd.print(measure[1]);
M5.Lcd.println(" C");
M5.Lcd.setCursor(40, 170);
M5.Lcd.print("Humi");
M5.Lcd.setCursor(40, 210);
M5.Lcd.print(measure[2]);
M5.Lcd.print(" %");
}
// Calculate&Print Average
for (i=0; i<=3; i++)
ave[i] = sum[i] / n;
// Create Payload & Header
sprintf(payload, "{\"co2ppm\": %5.2f,\"temp\": %3.2f,\"humi\": %3.2f}", ave[0], ave[1], ave[2]);
sprintf(content_length_hdr, "Content-Length: %d", strlen(payload));
// Reconnect WiFi if disconnected
if (WiFi.status() != WL_CONNECTED) {
connect_wifi();
}
// Reconnect MQTT Server if disconnected
if (!MqttClient.connected()) {
connect_mqtt();
}
// Publish
sprintf(topicname, "%s/%s",MQTT_OUTTOPICBASE,thingname );
SERIAL.print("topicname : ");
SERIAL.println(topicname);
SERIAL.print("payload : ");
SERIAL.println(payload);
MqttClient.publish(topicname, payload);
M5.Lcd.println(" Sent.");
}
Node-RED
当方では自宅Linuxサーバ上でLinux Container(LXC) – Almalinux9 にて起動しました。
構築手順は下記の通り
(インストール可能なNode.jsバージョンを確認) # yum module list nodejs Last metadata expiration check: 0:00:38 ago on Sat 19 Apr 2025 03:53:24 PM JST. AlmaLinux 9 - AppStream Name Stream Profiles Summary nodejs 18 common [d], development, minimal, s2i Javascript runtime nodejs 20 common [d], development, minimal, s2i Javascript runtime nodejs 22 common [d], development, minimal, s2i Javascript runtime (Node.jsのインストール) # yum module install nodejs:22 (Node-REDのインストール) # bash <(curl -sL https://raw.githubusercontent.com/node-red/linux-installers/master/rpm/update-nodejs-and-nodered) (自動起動の有効化) # systemctl enable nodered
起動後、Noderedパレット管理から以下のノードを追加します。
- node-red-contrib-aedes (Aedes MQTT broker)
- node-red-contrib-zabbix-sender (Zabbix Sender)
フローは単純ですが以下のようにしました。
MQTTブローカーにて co2sensor/# トピックでサブスクライブしています。
JSONオブジェクトで出力するようにします。
Zabbix Senderに変換するロジックは関数で定義してしまいましたが、他の機能で代替できるかもしれません。
var dev_name = msg.topic.split("/")[1];
var data = [
[dev_name,"co2.ppm",msg.payload.co2ppm],
[dev_name,"temp",msg.payload.temp],
[dev_name,"humi",msg.payload.humi]
];
msg.payload = data;
return msg;
Zabbix側設定
M5Stack側コードで指定したデバイス名(*thingname)の値でホストを作成します。
また、Node-REDのコードに合わせ、以下のアイテムを作成します。
- co2.ppm : CO2濃度
- temp : 温度
- humi : 湿度
CO2濃度(ppm)はKppmに自動変換して欲しくないので、単位を「!ppm」と記載します。
また、閾値によって通知するようトリガーの設定を行います。
結果
監視状況
以上により簡便にCO2濃度の測定ができるようになりました。
現在はモニターベゼルの空きスペースに設置して常時監視してます。
今は過ごしやすいシーズンで窓開放のため濃度は低めですが、今後冷房シーズンになると上昇しそうです。
監視して適度に換気を心がけたいと思います。
今後
監視通知がメールというのも能が無いですよね。
また、データを活かしてクラウドへのアップロードして可視化、分析に繋げたいですね。
今後もゆるりと開発して行ければと思います。









コメント