M5StickC+CO2 Hat
ここまではTWELITE PAL, ARIAでセンシングしたデータをRaspberry Piに送信し、そこからAWS IoTにpublishして温度・湿度のデータをグラフ化してきました。
次はM5StickC+CO2 HatでセンシングしたCO2濃度をグラフ化します。
CO2濃度が高いと息苦しさや頭痛など色々な症状が出てくるそうなので、これでリビングを快適な環境に保てているか確認していきたいと思います。
因みに、M5StackシリーズでAWS IoTと通信する場合には、M5Stack Core2 for AWS
というAWS IoTのセキュアキーが予め入っているKitも販売されています。
今回はこちらを使わずに、家に余っていたM5StickCを使いたいため、Raspiの時と同様に証明書と鍵をM5StickC側に入れてCA運用する形をとります。
M5Stack Core2 for AWS
についてはこちらを参照ください。
更に本製品用に追加で搭載されたMicrochip社製ATECC608A Trust&GO暗号認証チップはAWSへの接続を簡素化し、さらに暗号計算の高速化に利用できるセキュアキーが予め提供されています。
こちらにもセキュアキー周りの差分について記載されていました。
CO2 Hat
CO2 HatはCO2濃度を測定するセンサーをM5StickCに接続するパーツになります。
詳しくはこちらのリンク先を参照ください。
搭載するCO2センサーモジュールはMH-Z19BもしくはMH-Z19Cになります。
データシートも秋月のリンクから辿れます。
AWS IoTのThingの作成
以前と基本的に同じ流れですが、IoTのThingをCO2センサー用に追加します。
詳細は以前のChapterに記載されているのでここは概要だけ記載していきます。
今回はco2_sensor
というThing名にします。
証明書も新規で作成します。必要なファイル(デバイス証明書、パブリックキー、プライベートキー)は作成時に保存します。
ポリシーは前回作成したものが流用できるので、同じものを証明書にアタッチします。
M5StickC側のPublisherのファームウェア作成
今回はRaspiを経由せずに、M5StickCから直接AWS IoTにMQTTのtopicをpublishしてみます。
Arduino環境で開発します。
ArduinoライブラリのPubSubClient
とArduinoJson
を追加します。
PubSubClient
はMQTT Clientとしてpublishするため、
ArduinoJson
はMQTTでJSONでデータをpublishするのに使用します。
証明書やconfig情報
src/cert.h
というファイルに証明書、キー情報を記載します。BEGIN, ENDの間にダウンロードした情報を追加してください。
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管理から外すようにしてください。
またWiFi接続情報などをsrc/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についてはこちらが参考になります。
起動時にUARTの設定をします。
void MHZ19B::setup(HardwareSerial* hs, int8_t tx, int8_t rx)
{
this->serial = hs;
this->serial->begin(9600, SERIAL_8N1, tx, rx);
void setupCO2Sensor()
{
mhz19b.setup(&Serial1, 26, 0);
}
void setup()
{
setupCO2Sensor();
センサーのデータシートを読むと、CO2濃度を読み込むには読み込むコマンドを送信してその後受信した中に値が入っています。
これを送信して、応答データを受信するread()関数を作成します。
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側を実装しました。
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を取得します。
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の初期設定はそちらを参照ください。
実際に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を分岐するようにしました。コード全体を貼っておきます。
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
を追加します。
Charts.tsx
側もquery経由でデータ取得するgetSeries
を変更したgetCO2Series
を追加し、requestCO2sensors()
を呼ぶuseEffectを追加、あとはCO2用のChartを追加します。
主な変更箇所だけコード貼り付けします。
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]);
<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}
/>
コード全体はこちらに公開しています。
これでローカルでyarn start
してグラフ描画されることを確認します。(グラフはデータがたまったあとに描画したものです。)
問題なければ、
$ amplify push
でデプロイしてWebApp側に反映して終了です。
M5Stack系を使った異なるセンサーを追加してみましたが、一度仕組みが出来てしまえばそこまでグラフ追加までの変更点は多くはありませんでしたし、他のコードを流用するだけで済みました。
最初Lambdaもセンサーごとに別に分けてもいいかと思いましたが、結構module importするなど手間だったので1つのlambdaで分岐するようにしています。このあたりどうするのがベターなのかは正直まだよくわかっていないです。