Chapter 13

M5StickC+CO2 Hatを用いてCO2濃度グラフを追加

KatsuShun89
KatsuShun89
2022.05.29に更新

M5StickC+CO2 Hat

ここまではTWELITE PAL, ARIAでセンシングしたデータをRaspberry Piに送信し、そこからAWS IoTにpublishして温度・湿度のデータをグラフ化してきました。
次はM5StickC+CO2 HatでセンシングしたCO2濃度をグラフ化します。

CO2濃度が高いと息苦しさや頭痛など色々な症状が出てくるそうなので、これでリビングを快適な環境に保てているか確認していきたいと思います。

https://www.njkk.co.jp/blog/?itemid=81&dispmid=764#:~:text=空気中の二酸化炭素,不足と判断されます。

因みに、M5StackシリーズでAWS IoTと通信する場合には、M5Stack Core2 for AWSというAWS IoTのセキュアキーが予め入っているKitも販売されています。
今回はこちらを使わずに、家に余っていたM5StickCを使いたいため、Raspiの時と同様に証明書と鍵をM5StickC側に入れてCA運用する形をとります。

M5Stack Core2 for AWSについてはこちらを参照ください。

https://www.switch-science.com/catalog/6784/

更に本製品用に追加で搭載されたMicrochip社製ATECC608A Trust&GO暗号認証チップはAWSへの接続を簡素化し、さらに暗号計算の高速化に利用できるセキュアキーが予め提供されています。

こちらにもセキュアキー周りの差分について記載されていました。

https://qiita.com/ma2shita/items/dda457d178486ac0b94f

CO2 Hat

CO2 HatはCO2濃度を測定するセンサーをM5StickCに接続するパーツになります。
詳しくはこちらのリンク先を参照ください。

https://kitto-yakudatsu.com/archives/7766

https://kitto-yakudatsu.com/archives/7286

搭載するCO2センサーモジュールはMH-Z19BもしくはMH-Z19Cになります。
データシートも秋月のリンクから辿れます。

https://akizukidenshi.com/catalog/g/gM-16142/

AWS IoTのThingの作成

以前と基本的に同じ流れですが、IoTのThingをCO2センサー用に追加します。
詳細は以前のChapterに記載されているのでここは概要だけ記載していきます。

今回はco2_sensorというThing名にします。

証明書も新規で作成します。必要なファイル(デバイス証明書、パブリックキー、プライベートキー)は作成時に保存します。

ポリシーは前回作成したものが流用できるので、同じものを証明書にアタッチします。

M5StickC側のPublisherのファームウェア作成

今回はRaspiを経由せずに、M5StickCから直接AWS IoTにMQTTのtopicをpublishしてみます。

Arduino環境で開発します。

ArduinoライブラリのPubSubClientArduinoJsonを追加します。

PubSubClientはMQTT Clientとしてpublishするため、
ArduinoJsonはMQTTでJSONでデータをpublishするのに使用します。

証明書やconfig情報

src/cert.hというファイルに証明書、キー情報を記載します。BEGIN, ENDの間にダウンロードした情報を追加してください。

cert.h
const char *root_ca = R"(-----BEGIN CERTIFICATE-----
-----END CERTIFICATE-----
)";

const char *certificate = R"(-----BEGIN CERTIFICATE-----
-----END CERTIFICATE-----
)";

const char *private_key = R"(-----BEGIN RSA PRIVATE KEY-----
-----END RSA PRIVATE KEY-----
)";

追加した状態でgithubなどにアップロードしないようにcert.hはgithub管理から外すようにしてください。

https://qiita.com/usamik26/items/56d0d3ba7a1300625f92

またWiFi接続情報などをsrc/config.hに記載します。

config.h
//WiFi
const char* SSID = "xxxxxxxxxx";
const char* PASS = "xxxxxxxx";

//AWS IOT
const char* AWS_IOT_ENDPOINT = "xxxxxxxxx";
const uint32_t AWS_IOT_PORT = 8883;
const char* THING_NAME = "xxx_sensor";
const char* TOPIC = "sensor/device_name/sensor_update";

SSID,PASSは自宅のWiFi APのものを記載します。

THING_NAMEはAWS IoT側に合わせてco2_sensorにします。
TOPICのdevice_nameもco2stickc01としました。

TOPICはAWS IoT側で以前の環境センサーと同じルールを使うため、sensor/+/sensor_updateでフィルタできるようにdevice_nameだけ区別できるようにしています。

CO2センサーの濃度データ取得

MH-Z19BセンサーのCO2濃度をUART通信で受信します。

CO2 Hatの接続ピンはバージョンで異なる可能性があるため、購入したときのもの情報を参照ください。

私が購入したものはTX:26, RX:0となっていました。

MH-Z19B通信用にMHZ19B Classを作成します。
M5StickC(ESP32)のSerial通信にはHardwareSerial Serial1を使用します。

M5StackシリーズのUARTについてはこちらが参考になります。
https://lang-ship.com/blog/work/m5stickc-uartserial/

起動時にUARTの設定をします。

mhz19b.cpp
void MHZ19B::setup(HardwareSerial* hs, int8_t tx, int8_t rx)
{
  this->serial = hs;
  this->serial->begin(9600, SERIAL_8N1, tx, rx);
m5stickc_aws_iot_co2_pub.ino
void setupCO2Sensor()
{
  mhz19b.setup(&Serial1, 26, 0);
}

void setup()
{
  setupCO2Sensor();

センサーのデータシートを読むと、CO2濃度を読み込むには読み込むコマンドを送信してその後受信した中に値が入っています。

https://akizukidenshi.com/download/ds/winsen/MH-Z19C_20210518.pdf

これを送信して、応答データを受信するread()関数を作成します。

mhz19b.cpp
uint16_t MHZ19B::read()
{
  const size_t SIZE = 9;
  uint8_t read_cmd[SIZE] = {0xFF, 0x01, 0x86, 0, 0, 0, 0, 0, 0x79};
  for (int i = 0; i < SIZE; i++){
    this->serial->write(read_cmd[i]);
  }

  uint8_t read_data[SIZE] = {0};
  delay(100);
  uint32_t data_len = this->serial->available();
  if(data_len != 0){
    for(int i = 0; i < data_len; i++){
        read_data[i] = (uint8_t)this->serial->read();
    }
  }
  uint16_t concentration = 0;

  //check read data
  if (read_data[0] == 0xff && read_data[1] == 0x86 && checksum(read_data, SIZE)){
    concentration = (read_data[2] << 8) + read_data[3];
  }
  return concentration;
}

this->serial->write(read_cmd[i]);で読み込み用のコマンドをセンサーに送信して、その後の
read_data[i] = (uint8_t)this->serial->read();でデータを受信、最後に
concentration = (read_data[2] << 8) + read_data[3];
でCO2濃度を取得しています。

AWS IoTへのMQTT TOPICのpublish

CO2濃度が取得できるようになったので、これを10分間隔でMQTTでpublishします。
こちらのAmazonのブログを参考にClient側を実装しました。

https://aws.amazon.com/jp/builders-flash/202105/smart-pet-communication/?awsf.filter-name=*all

m5stickc_aws_iot_co2_pub.ino

void loop()
{
  M5.update();  // ボタン状態更新
  printMenu();

  char json_string[1000];
  uint16_t co2_concentration = readCO2();

  static uint32_t last_pub_time = 0;
  uint32_t current_time = getTime();
  if((current_time - last_pub_time) >= 60 * 10){ //10min
    connectAWSIOT();
    json_document["device_name"] = "co2stickc01";
    json_document["type"] = "CO2sensor";
    json_document["co2_concentration"] = co2_concentration;
    json_document["timestamp"] = current_time;

    serializeJson(json_document, json_string);
    Serial.println(json_string);
    bool ret = mqtt_client.publish(TOPIC, json_string);
    Serial.printf("pub ret %d\n", ret);
    last_pub_time = current_time;
  }
  delay(ALARM_WAIT_SEC);
}

json_document[]でjsonの各パラメータをセットし、
mqtt_client.publish(TOPIC, json_string);の箇所でjsonデータをMQTTでpublishしています。

typeには"CO2sensor"を入れ、これで後ほどAWS Lambdaで使用するGraphQLのmutationを区別するようにします。

また、環境センサーのときと同様にtimestampはUnix Time(Epoch Tiime)をセットするようにしています。これはNTPでLocalTimeを取得します。

m5stickc_aws_iot_co2_pub.ino

void setup()
{
  const uint32_t JST = 3600 * 9;
  configTime(JST, 0, "ntp.nict.jp", "ntp.jst.mfeed.ad.jp");
}

uint32_t getTime() {
  time_t now;
  struct tm time_info;

  if (!getLocalTime(&time_info)){
    return 0;
  }
  time(&now);
  return now;
}

コードは公開しているので、WiFiの接続やMQTT Clientの初期設定はそちらを参照ください。

https://github.com/Katsushun89/m5stickc_aws_iot_co2_pub/blob/0.0.1/m5stickc_aws_iot_co2_pub.ino

実際にM5StickCにファームウェアを書き込みして動作確認します。
まずはLcdにCO2の値が表示されるか確認します。適宜Serial.printf()などでデバッグします。

次にAWS IoTのテストでサブスクライブ出来ているか確認します。
以前と同様にトピックをサブスクライブするのフィルタの欄にsensor/+/sensor_updateと入力して結果が表示されればAWS IoT側でサブスクライブ出来ている点までは確認出来ます。

GraphQLのschemaの追加

次にIoTのルールで呼び出しされるLambda関数で、GraphQLのmutationを実行するために、Amplifyのschema.graphqlにmodelを追加します。

type CO2sensor
  @model
  @auth(rules: [
  { allow: private, provider: iam, operations: [create, read, update] }
  { allow: private, provider: userPools, operations: [read] }
  { allow: owner }
  ]) {
  id: ID!
  type: String! @index(name: "byCO2Timestamp", queryField: "byCO2Timestamp", sortKeyFields: ["timestamp"])
  deviceid: String!
  timestamp: AWSTimestamp!
  concentration: Int!
}

基本的に以前の環境センサーEnvsensorとほぼ同等で、アップロードするデータがconcentration: Int!のみになっています。

時系列でフィルタするための@indexは以前のbyTimestampと被らないようにするために、byCO2Timestampとしました。

これで

$ amplify api update
$ amplify push

して更新した内容を反映させます。

AppSync 側でスキーマ追加されたことを確認します。
queryでみるとこのようになっており、CO2sensor用のqueryが追加されていることが確認出来ました。

Lambdaのmutationの追加

AWS IoTでサブスクライブした際にLambda関数を呼び出すので、それにCO2sensorのmutationを追加します。
これもcloud9でjsファイルを編集します。

exports.handler = async (event, context)でjson処理するときにtypeで呼び出すmutationを分岐するようにしました。コード全体を貼っておきます。

index.js
require('isomorphic-fetch');
const aws = require('aws-sdk');
const AWSAppSyncClient = require('aws-appsync').default;
const gql = require('graphql-tag');
const AppSyncConfig = require('./aws_exports').default;

const mutationEnvsensor = gql(`mutation createEnvsensor($input: CreateEnvsensorInput!) {
    createEnvsensor(input: $input) {
    deviceid
    humidity
    illuminance
    power
    temperature
    timestamp
    type
  }
}`);

const mutationCO2sensor = gql(`mutation createCO2sensor($input: CreateCO2sensorInput!) {
    createCO2sensor(input: $input) {
    deviceid
    concentration
    timestamp
    type
  }
}`);

exports.handler = async (event, context) => {
    console.log(`REQUEST++++${JSON.stringify(event)}`);
    const client = new AWSAppSyncClient({
        url: AppSyncConfig.aws_appsync_graphqlEndpoint,
        region: "ap-northeast-1",
        auth: {
            type: 'AWS_IAM',
            credentials: ()=> aws.config.credentials
        },
    
        disableOffline: true
    });
    
    if(event.data.type == "Envsensor"){
        await client.mutate({
            mutation: mutationEnvsensor,
            variables: {input: {
                deviceid: event.data.device_name,
                humidity: event.data.humidity,
                illuminance: event.data.illuminance,
                power: event.data.power,
                temperature: event.data.temperature,
                timestamp: event.data.timestamp,
                type: event.data.type
            }},
            fetchPolicy: 'no-cache'
        });
    }
    else if(event.data.type == "CO2sensor"){
        await client.mutate({
            mutation: mutationCO2sensor,
            variables: {input: {
                deviceid: event.data.device_name,
                concentration: event.data.co2_concentration,
                timestamp: event.data.timestamp,
                type: event.data.type
            }},
            fetchPolicy: 'no-cache'
        });
    }
}

Reactでのグラフ追加

ReactのWebApp側でGraphQLのqueryを実行してデータ取得して、グラフ描画までを追加します。
これも前のコードを参考に追加していきます。
useEnvsensors.tsxのEnvをCO2に書き換えるかたちでuseCO2sensors.tsxを追加します。

https://github.com/Katsushun89/home_sensor_monitor/blob/0.0.1/src/useCO2sensors.tsx

Charts.tsx側もquery経由でデータ取得するgetSeriesを変更したgetCO2Seriesを追加し、requestCO2sensors()を呼ぶuseEffectを追加、あとはCO2用のChartを追加します。
主な変更箇所だけコード貼り付けします。

Charts.tsx
const getCO2Series = (
    co2sensors: readonly CO2sensor[],
    field: 'concentration'
) =>
    Object.entries(co2sensorDeviceIdToRoom).map(([deviceId, room]) => ({
        name: room,
        data: co2sensors
            .filter((m) => m.deviceid === deviceId)
            .map((m) => [m.timestamp * 1000, m[field]]), //convert javascript unixtime (*1000)
    }));

const Charts = () => {
    const { envsensors, requestEnvsensors } = useEnvsensors();
    const { co2sensors, requestCO2sensors } = useCO2sensors();

    useEffect(() => {
        console.log("requestEnvsensors");
        const now = new Date();
        const priviousDay = subDays(now, initialDaysRange);

        requestEnvsensors(Math.floor(priviousDay.getTime() / 1000),
                            Math.floor(now.getTime() / 1000));
    }, [requestEnvsensors]);

    useEffect(() => {
        console.log("requestCO2sensors");
        const now = new Date();
        const priviousDay = subDays(now, initialDaysRange);

        requestCO2sensors(Math.floor(priviousDay.getTime() / 1000),
                            Math.floor(now.getTime() / 1000));
    }, [requestCO2sensors]);
 
Charts.tsx
           <Chart
                className="Chart"
                options={{
                    ...commonOptions,
                    chart: {
                        ...commonOptions.chart,
                        id: 'co2-concentration-chart',
                    },
                    yaxis: {
                        ...commonOptions.yaxis,
                        title: {
                            text: 'CO2濃度[ppm]',
                        },
                    },
                    tooltip: {
                        ...commonOptions.tooltip,
                        y: {
                            formatter: (value) => `${value.toFixed(1)}ppm`,
                        },
                    },
                }}
                series={ getCO2Series(co2sensors, 'concentration') as ApexAxisChartSeries}
                hegit={chartHeight}
            />

コード全体はこちらに公開しています。
https://github.com/Katsushun89/home_sensor_monitor/blob/0.0.1/src/Charts.tsx

これでローカルでyarn startしてグラフ描画されることを確認します。(グラフはデータがたまったあとに描画したものです。)

問題なければ、

$ amplify push

でデプロイしてWebApp側に反映して終了です。

M5Stack系を使った異なるセンサーを追加してみましたが、一度仕組みが出来てしまえばそこまでグラフ追加までの変更点は多くはありませんでしたし、他のコードを流用するだけで済みました。
最初Lambdaもセンサーごとに別に分けてもいいかと思いましたが、結構module importするなど手間だったので1つのlambdaで分岐するようにしています。このあたりどうするのがベターなのかは正直まだよくわかっていないです。