opnizでつくる「CO2センサ（MH-Z19C）データのGoogle Spreadsheetsロギングシステム」

![image](https://user-images.githubusercontent.com/22117028/145237900-e3b310b2-dbff-4a48-be62-bf1ab709a4a1.png)

# はじめに

こんにちは。田中みそです。
**「[opniz](https://github.com/miso-develop/opniz)」**という[obniz](https://obniz.com/ja/how_obniz_works)っぽいことができるオープンソースIoTフレームワークを作って公開しています。

![image](https://user-images.githubusercontent.com/22117028/145335374-e809e1ec-b614-44a5-9d21-b45bd01f5b72.png)

最近WebSocketに対応したv0.2を公開し、家でopnizにて実際に運用しているおうちハックデバイスのv0.2化対応を行いました。
そのうちのひとつをopnizの作例として紹介したいと思います。



# CO2センサ（MH-Z19C）データのGoogle Spreadsheetsロギング

このおうちハックデバイスは、CO2センサ「[MH-Z19C](https://akizukidenshi.com/catalog/g/gM-16142/)」にて取得したCO2濃度データを10分おきにGoogle Spreadsheetsへ書き込む、というデバイスです。
寝室に設置して就寝中のCO2濃度を計測しています。

今の時期ですと寝室のドアを締め、一晩中エアコンをつけて家族4人で寝ていますが、そうするとCO2濃度が危険域に入ってしまうことが分かりました。
このような環境では安眠は得られません。
対策としてドアを少し開けて換気するようにし、あわせてCO2濃度をロギングし効果計測を行っています。



# 構成図

![image](https://user-images.githubusercontent.com/22117028/145238865-203a8c7a-02f0-4a8e-8a61-a9f7c3d9df81.png)

構成はとてもシンプルです。
opniz SDKを実装したプログラムにて、マイコンデバイスと接続したMH-Z19CのCO2濃度の値を取得し、Google SpreadSheetsへHTTP POSTで書き込んでいます。
マイコンデバイスにはopniz Arduino Libraryを書き込んだM5ATOMを使用しています（M5ATOM Lite、M5ATOM Matrixどちらでも大丈夫です）。



# opnizのしくみ

詳細な実装説明へ入る前にopnizのしくみ・構成について簡単に説明します。
opnizはマイコンデバイスへ書き込むための「[opniz Arduino Library](https://github.com/miso-develop/opniz-arduino-m5atom)」、Node.jsでマイコンデバイスを遠隔制御するための「[opniz Node.js SDK](https://github.com/miso-develop/opniz-sdk-nodejs)」、そしてマイコンデバイスとプログラム間のメッセージを中継する「[opniz Server](https://github.com/miso-develop/opniz-server)」があります。

![image](https://user-images.githubusercontent.com/22117028/145159761-63f34665-5c36-4d0d-b053-1b0cddf4c76d.png)

今回の例もそうですが、ひとつのマイコンデバイスを制御するだけであれば「opniz Server」は不要です。
「opniz Arduino Library」を書き込んだマイコンデバイスと「opniz Node.js SDK」を実装したプログラムとで直接通信を行えます。
複数のマイコンデバイスを複数のプログラムから制御したいケースでは「opniz Server」を導入すると管理が楽になります。



# 実装

それでは実装の説明に入っていきます。
以下の4点についてopnizの実装を中心とした説明とソースコードを掲載します。

* [MH-Z19CとM5ATOMの接続](#mh-z19c%E3%81%A8m5atom%E3%81%AE%E6%8E%A5%E7%B6%9A)
* [opniz Arduino Libraryの実装](#opniz-arduino-library%E3%81%AE%E5%AE%9F%E8%A3%85)
* [opniz Node.js SDKの実装](#opniz-nodejs-sdk%E3%81%AE%E5%AE%9F%E8%A3%85)
* [Google Apps Scriptの実装](#google-apps-script%E3%81%AE%E5%AE%9F%E8%A3%85)



## MH-Z19CとM5ATOMの接続

MH-Z19Cの各ピンを以下のようにM5ATOMとつなげます。
M5ATOMの4つ穴側のピン列がぴったり使えます。

|MH-Z19C|M5ATOM|
|---|---|
|Vin|5V|
|GND|GND|
|Tx|21|
|Rx|25|

私はユニバーサル基板で自作したハットを使っています。
材料費数十円で作れますが割と手間です。

![image](https://user-images.githubusercontent.com/22117028/145190311-494bc67c-0f87-48e1-9003-ac17dfb5bb61.png)

![image](https://user-images.githubusercontent.com/22117028/145190321-5dbaf0d8-332b-4fdd-b29f-b0bde680f002.png)



## opniz Arduino Libraryの実装

opniz Arduino Libraryは基本的には[example](https://github.com/miso-develop/opniz-arduino-m5atom/blob/main/examples/Basic/Basic.ino)のコードをWi-Fi情報等のみ書き換えてスケッチ書き込みすればそのまま使えます（opnizはあくまでNode.jsでマイコンデバイスをコントロールするのが主目的なので）。
外部のArduinoライブラリをopniz Node.js SDKから使いたいときはRPC定義を拡張して使うこともできます。

今回はMH-Z19Cを制御する「[MHZ19_uart](https://github.com/nara256/mhz19_uart)」というArduinoライブラリを使用しています。
以下のソースコードの大部分はexampleと同様のコードですが、MHZ19_uartを用いたopniz拡張のコードも含まれています。

<details><summary><b>╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╮<br>╏&nbsp;&nbsp;&nbsp;&nbsp;ソースコードを表示（折りたたみ）&nbsp;&nbsp;&nbsp;╏<br>╰╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╯</b></summary><div>

```cpp:main.ino
#include <OpnizM5Atom.h>
#include <lib/WiFiConnector.h>
#include <MHZ19_uart.h>



const char* ssid = "<SSID>";         // WiFiのSSIDに書き換え
const char* password = "<PASSWORD>"; // WiFiのパスワードに書き換え
WiFiConnector wifiConnector(ssid, password);

const char* address = "192.168.0.1"; // Node.js SDKを実行する端末のIPアドレスに書き換え
const uint16_t port = 3000;
Opniz::M5Atom* opniz = new Opniz::M5Atom(address, port);

MHZ19_uart mhz19;
#define TX_PIN 25
#define RX_PIN 21



// MHZ19_uart関数用ハンドラ定義
// CO2濃度取得関数
class getCO2PPMHandler : public BaseHandler {
public:
    String name() override { return "getCO2PPM"; };
    String procedure(JsonArray params) override {
        return (String)mhz19.getCO2PPM();
    }
};
// CO2センサ内温度取得関数
class getTemperatureHandler : public BaseHandler {
public:
    String name() override { return "getTemperature"; };
    String procedure(JsonArray params) override {
        return (String)mhz19.getTemperature();
    }
};



void setup() {
    initM5(); // M5ATOM初期化
    
    wifiConnector.connect(); // WiFi接続
    
    // MHZ19_uart関数用ハンドラを登録
    opniz->addHandler({
        new getCO2PPMHandler,
        new getTemperatureHandler,
    });
    opniz->connect(); // Node.js SDKへ接続
    
    // mhz19ライブラリ初期化
    mhz19.begin(RX_PIN, TX_PIN);
    mhz19.setAutoCalibration(false);
}

void loop() {
    opniz->loop();         // opnizメインループ
    wifiConnector.watch(); // WiFi接続監視
}
```

</div></details>

### opniz Arduino Libraryの書き込み方法

opniz Arduino LibraryはまだArduino IDEのライブラリ等で公開していません。
そのためライブラリのインストールはGitHubからZIPをダウンロードし、Arduinoライブラリへ登録する必要があります。
画面キャプチャを交えたインストール手順を[コチラ](https://github.com/miso-develop/opniz-arduino-m5atom#%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E6%96%B9%E6%B3%95)に掲載してますので参照いただければと思います。

また上記の手順にもありますが「ArduinoJson」「WebSockets」「M5Atom」「FastLED」も依存ライブラリとして必要となり、加えて今回は「MHZ19_uart」も必要となります。
これらのライブラリもArduino IDEへインストールしてください。

### opniz実装のポイント

opniz拡張としてやっていることは以下の2点です。

* MHZ19_uartのインクルード、初期化
* MHZ19_uartの関数をopniz Node.js SDKから呼び出せるようハンドラ定義し追加する

ポイントは`class getCO2PPMHandler : public BaseHandler {`という記述から始まるコードブロック部です。
`BaseHandler`クラスを継承し、opniz Node.js SDKからコールされるRPC名を`name`メソッドで定義し、コールされた際の動きを`procedure`メソッドで定義します。
今回は単純にMHZ19_uartにて定義された関数を呼び出しているだけです。

```cpp:main.ino（opniz拡張部の抜粋）
// MHZ19_uart関数用ハンドラ定義
// CO2濃度取得関数
class getCO2PPMHandler : public BaseHandler {
public:
    String name() override { return "getCO2PPM"; };
    String procedure(JsonArray params) override {
        return (String)mhz19.getCO2PPM();
    }
};
// CO2センサ内温度取得関数
class getTemperatureHandler : public BaseHandler {
public:
    String name() override { return "getTemperature"; };
    String procedure(JsonArray params) override {
        return (String)mhz19.getTemperature();
    }
};
```



## opniz Node.js SDKの実装

opniz Node.js SDKにてメインロジックを実装します。
`main`関数のforループがメイン処理のループとなっています。

このコードでは以下の処理を行っています。

* 5秒おきにCO2濃度を取得し、値によってM5ATOMのLEDの点灯色を変更
* 10分おきにCO2濃度をGoogle Spreadsheetsに書き込む

<details><summary><b>╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╮<br>╏&nbsp;&nbsp;&nbsp;&nbsp;ソースコードを表示（折りたたみ）&nbsp;&nbsp;&nbsp;╏<br>╰╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╯</b></summary><div>

```sh:npmパッケージのインストール
npm install opniz@0.2.2 node-fetch dayjs
```

```ts:index.ts
import { Opniz } from "opniz"
import fetch from "node-fetch"
import dayjs from "dayjs"



const port = 3000

const spreadsheetUrl = "https://script.google.com/macros/s/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/exec" // Google SpreadsheetのApps Script公開アプリURLを指定

const intervalTime = 5000
const writeMinute = 10



// CO2センサ対応opniz拡張クラス定義
class MHZ19C extends Opniz.M5Atom {
	// CO2濃度取得
	public async getCO2PPM(): Promise<number> {
		return Number(await this.exec("getCO2PPM"))
	}
	// CO2センサ内温度取得
	public async getTemperature(): Promise<number> {
		return Number(await this.exec("getTemperature"))
	}
}
const opniz = new MHZ19C({ port })



// CO2濃度によりLED表示変更
const getCO2Color = (co2ppm) => {
	// MEMO: RGB形式ではなく、GRB形式で指定
	let color = "#000000"
	if (co2ppm >= 1000) color = "#00ff00" // 赤
	if (co2ppm < 1000) color = "#88ff00" // 橙
	if (co2ppm < 800) color = "#ff8800" // 黄緑
	if (co2ppm < 600) color = "#ff0000" // 緑
	if (co2ppm === -1) color = "#0000ff" // 青
	return color
}



// タイマー
const makeTimer = () => {
	let preMinute = -1
	return (func, time) => {
		const nowMinute = Number(dayjs().format("m"))
		if (nowMinute % time === 0 && nowMinute !== preMinute) {
			preMinute = nowMinute
			func()
		}
	}
}
const timer = makeTimer()



// Google Spreadsheetに書き込み
const writeSpreadsheet = async (...data) => {
	if (!spreadsheetUrl) throw Error("Url invalid!")
	await fetch(spreadsheetUrl, {
		method: "POST",
		headers: { "Content-Type": "application/json" },
		body: JSON.stringify([dayjs().format("YYYY/MM/DD HH:mm:ss"), ...data]),
	})
}



// メインロジック
const main = async () => {
	while (!(await opniz.connect())) { console.log("connect...")}
	console.log("[connected]")
	
	try {
		for (;;) {
			console.log("[loop]")
			
			const co2ppm = await opniz.getCO2PPM() // CO2濃度取得
			console.log({ co2ppm })
			await opniz.dis.drawpix(0, getCO2Color(co2ppm)) // LED表示色変更
			timer(() => writeSpreadsheet(co2ppm), writeMinute) // 10分おきにGoogle Spreadsheetに書き込み
			
			await opniz.sleep(intervalTime)
		}
	} catch(e) {
		console.log("[error]", e.message)
		await main()
	}
}
main()
```

</div></details>

### opniz実装のポイント

opniz実装としてのポイントは`class MHZ19C extends Opniz.M5Atom {`から始まるコードブロック部です。
ここで`Opniz.M5Atom`クラスを継承しopniz Arduino Library側で定義したMHZ19_uartの関数を呼び出すハンドラをコールするメソッドを定義しています。
`this.exec()`にてパラメータに指定している文字列が前述のopniz Arduino Libraryのソースコードで定義したハンドラの`name`メソッドの値に相当します。

```ts:index.ts（opniz拡張部の抜粋）
// CO2センサ対応opniz拡張クラス定義
class MHZ19C extends Opniz.M5Atom {
	// CO2濃度取得
	public async getCO2PPM(): Promise<number> {
		return Number(await this.exec("getCO2PPM"))
	}
	// CO2センサ内温度取得
	public async getTemperature(): Promise<number> {
		return Number(await this.exec("getTemperature"))
	}
}
const opniz = new MHZ19C({ port })
```



## Google Apps Scriptの実装

GASのコードも掲載しておきます。
わりとよくあるコードだと思いますがHTTP POSTで受け取ったJSONデータをもとに、シートの最下行にレコードを追加していくコードです。

このApps Scriptを公開アプリとしてデプロイし、発行されたURLを前述のopniz Node.js SDKのソースコードに記述します。

```js
function doPost(e) {
	const data = JSON.parse(e.postData.getDataAsString())
	
	const sheet = SpreadsheetApp.getActive().getActiveSheet()
	sheet.appendRow(data)
	
	const output = ContentService.createTextOutput()
	output.setMimeType(ContentService.MimeType.JSON)
	output.setContent(JSON.stringify({ message: "success!" }))
	return output
}
```



# おわりに

opnizの作例としてCO2センサを制御する例を紹介させていただきました。
今回のCO2ロギングシステムは家で常時稼働しているRaspberry Piにデプロイして実行しています。

しかしこのopnizプログラムをHerokuといったクラウド環境にデプロイして実行することも可能です。
さらにExpress等を用いてセンサデータを取得するWeb API化する、なんてことも可能です。

![image](https://user-images.githubusercontent.com/22117028/145351977-036d40af-2e57-4e9d-a1d3-17097de7f090.png)

opnizはこういった無限の可能性をローコストで実現できますので、ぜひとも一度お試しいただけますと幸いです。