UDPでESP32と連携してNode-REDのダッシュボードに表示

ESP32とNode-REDはUDPで簡単に連携できるので、M5StickCに、ENV 2 Hatとフェーダーユニットをつないで、Node-REDのダッシュボードに表示してみます。ついでに、M5StickCについているLEDの点灯も操作できるようにします。

ENV 2 Hat
※今回は温湿度計を使います。

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

フェーダーユニット

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

# ESP32におけるUDP送受信

宣言

```c:module_udp.cpp
#include <WiFiUdp.h>
static WiFiUDP udp;
```

受信準備

```c:module_udp.cpp
  udp.stop();
  udp.begin(port);
```

portには、受信を待ち受けたいUDPのポート番号を指定します。

受信があったか確認

```c:module_udp.cpp
  int packetSize = udp.parsePacket();
  if( packetSize <= 0 )
    return JS_NULL;
```

上記のpacketSizeが0より大きい場合は、受信データがあることを示しています。
あとは、以下のような形で、受信データを取り出します。

```c:module_udp.cpp
  char *p_buffer = (char*)malloc(packetSize + 1);
  if( p_buffer == NULL )
    return JS_NULL;
  
  int len = udp.read(p_buffer, packetSize);
  if( len <= 0 ){
    free(p_buffer);
    return JS_EXCEPTION;
  }
  p_buffer[len] = '\0';
```

NULL終端の文字列であることを想定して、最後にNULLを挿入しています。

# 送信

文字列を送信する場合の例です。

```c:module_udp.cpp
  char temp[16];
  sprintf(temp, "%d.%d.%d.%d", (host >> 24) & 0xff, (host >> 16) & 0xff, (host >> 8) & 0xff, host & 0xff);
  udp.beginPacket(temp, port);
  udp.write((const uint8_t*)text, strlen(text));
  udp.endPacket();
```

hostに、32ビット整数で、IPv4のIPアドレスを指定した場合です。portは送信したい相手側のUDPのポート番号です。

# Node-REDで待ち受けるUDPデータ形式

どのようなデータ形式であっても、Node-RED側で成形すればよいのですが、今回は以下のようなデータ形式を想定します。
以下のようなJSON形式です。

```json
{
	"topic": "temp" or "humidity" or "fader" or "led",
	"value": topicごとの値
}
```

topic="temp"の場合

```json
{
	"topic": "temp",
	"value": 温度(浮動小数)
}
```

topic="humidity"の場合

```json
{
	"topic": "humidity",
	"value": 湿度(浮動小数)
}
```

topic="fader"の場合

```json
{
	"topic": "fader",
	"value": 読み出し値(0～4095)
}
```

topic="led"の場合

```json
{
	"topic": "led",
	"value": 点灯状態(boolean)
}
```

Node-REDのフロー全体は以下のような形になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/c58f69c3-59b4-19d7-87f0-4d1d1beb81f2.png)

これをNode-REDのダッシュボードで見ると以下のように見えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/dd8d8902-740c-bf8d-ec43-83ab6f51b599.png)

左上から順にみてみます。

まずは、ネットワークカテゴリのudp inノードです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/b2fd1737-c07c-3e95-4eae-859026934e97.png)

Node-REDにおいて、ポート番号3335番で、UDPパケットを待ち受けています。

次は、パーサカテゴリのjsonノードです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/3a7dfe4e-90a5-11bf-2d20-2110559a36f7.png)

単に、テキストで受信されたUDPパケットをJSONにパースして次のノードに渡しています。パースすることで以下のような形で以降のノードに流れていきます。

```json
{
	"topic": "",
	"payload": {
		"topic": "temp" or "humidity" or "fader" or "led",
		"value": topicごとの値
	}
}
```

payloadの中の値がJSON化された文字列、すなわち、UDPで受信したデータです。

ちょっと横道にそれて、共通カテゴリのdebugノードがあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/06a24733-cc49-3aff-1756-8e83e891b05a.png)

これは、以降のノードで処理する元となった入力ノードをデバッグ出力して確認できるようにしたものです。デバッグ目的であるため、このノードは必須ではありません。

次は、changeノードです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/f746485f-3986-a6c6-a741-87ce76406fae.png)

以降の処理では、入力されたtopicに基づいて処理分岐します。また、分岐した後はpayloadの値をNode-REDのダッシュボードに出力します。
したがって、

```json
{
	"topic": "",
	"payload": {
		"topic": "temp" or "humidity" or "fader" or "led",
		"value": topicごとの値
	}
}
```

の形式を、

```json
{
	"topic": UDPから受信したtopicの値,
	"payload": UDPから受信したvalueの値
}
```

の形式に変換しているわけです。

次は、機能ノードにあるswitchノードです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/651664be-f4d4-4ea1-28be-ac400fc64846.png)

topicの内容に応じて、次のノードに振り分けています。
4つの分岐("temp" or "humidity" or "fader" or "led")があります。

次からはダッシュボードへの表示のためのノードです。

まずは、gaugeノードです。topic="fader"の場合のノードです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/9e95f8b0-2a3d-4f60-1019-1d70cc29c7cc.png)

TypeにはGaugeを選択しています。スピードメータのような見え方になります。
Rangeは、payloadの値がどの範囲で来るかを指定しておきます。要は最小値と最大値です。
フェーダーユニットはアナログ値の出力であるため、0から4095になります。

次が、ダッシュボードカテゴリのtextノードです。温度表示です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/e0295175-7b20-1909-6e3f-fee03205dbc1.png)

payloadの浮動小数が入ってきますので、それに℃を付けて表示しています。ただ、そのまま表示すると、浮動小数点が長くなってみにくいため、浮動小数点第2位までの表示にしています。

湿度表示も同じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/7f1707b7-a6a0-0a5c-3992-81980852ce75.png)

最後が、LEDの状態表示で、ダッシュボードカテゴリのswitchノードです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/3e0d0bc7-ba91-a797-c53f-75436a9e0b63.png)

以下のノードもあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/5693d249-c5c2-9bb8-53a1-344d902c5a5d.png)

これは、ダッシュボード上にボタンを表示して、そのボタンの押下によって、M5StickCにあるLEDの点灯・消灯をトグルさせるためのものです。

左側から、ダッシュボードカテゴリのbuttonノードです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/6fb967df-378c-3d0a-a0de-a288246d764a.png)

次が、udp outノードです。その名の通り、UDPパケットを送信するためのノードです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/4f98aede-1656-56fc-d78a-68110c75d188.png)

送信する先のポート番号とIPv4アドレスを指定します。
送信される内容は、ひとつ前のbuttonノードの出力文ですが、今回は特にみていません。

# M5StackC側の実装

UDPに先ほど示したJSON文字列を指定して送信するだけなので、あまり難しくはないと思います。
今回は、以下の技術書で紹介している、ESP32内部で動作するJavascript環境で動作させる場合のJavascriptコード例を示します。

M5StackとJavascriptではじめるIoTデバイス制御

https://poruruba.booth.pm/items/3735175

10秒ごとに温湿度を計測するためのループ(setInterval)と、常時、ボタン押下状態の更新、フェーダの値の取得、M5StickCのボタン押下状態の確認、UDPパケット受信状態の確認を実施するためのループ(setInterval)の2つのループがあります。

```js
import * as udp from "Udp";
import * as gpio from "Gpio";
import * as input from "Input";
import * as wire from "Wire";
import SHT30 from "SHT30.js";

const MARGIN = 100; /* フェーダの値変化の遊び */
const FADER_PIN = 33; /* フェーダのPIN番号 */
const LED_PIN = 10; /* LEDのPIN番号 */
const UDP_PORT = 3335; /* UDPの送信・受信ポート番号 */
const UDP_HOST = 0xC0A80110; /* UDPの送信先IPアドレス */

/* フェーダーのポジション */
var position = 0;

/* LED点灯状態 */
var led = true;

/* SHT30のセットアップ */
wire.begin(0, 26);
var sht30 = new SHT30(wire);

/* LEDのセットアップ */
gpio.pinMode(LED_PIN, gpio.OUTPUT);
gpio.digitalWrite(LED_PIN, led ? gpio.LOW : gpio.HIGH);

/* フェーダのセットアップ */
gpio.pinMode(FADER_PIN, gpio.INPUT);

/* UDP受信待ち開始 */
udp.recvBegin(UDP_PORT);

/* 10秒ごとに、温湿度を計測 */
setInterval(async () =>{
	/* 温湿度計測 */
	await sht30.get();
	console.log(sht30.cTemp, sht30.humidity);

	/* 温度のUDP送信 */
	var message = {
		topic:"temp",
		value: sht30.cTemp
	};
	udp.sendText(0xC0A80110, UDP_PORT, JSON.stringify(message));

	/* 湿度のUDP送信 */
	var message = {
		topic:"humidity",
		value: sht30.humidity
	};
	udp.sendText(UDP_HOST, UDP_PORT, JSON.stringify(message));
}, 10000);

/* 常時、ボタン押下状態の更新、フェーダの値の取得、M5StickCのボタン押下状態の確認、UDPパケット受信状態の確認を実施 */
setInterval(() =>{
	/* ボタン押下状態の更新 */
	esp32.update();

	/* フェーダの値の取得 */
	var val = gpio.analogRead(FADER_PIN );
	if( Math.abs(position - val) >= MARGIN ){
		position = val;
		/* フェーダ値のUDP送信 */
		var message = {
			topic:"fader",
			value: position
		};
		udp.sendText(UDP_HOST, UDP_PORT, JSON.stringify(message));
	}

	/* M5StickCのボタン押下状態の確認 */
	if( input.wasPressed(input.BUTTON_A) ){
		console.log("pressed");

		/* LEDの点灯状態の更新 */
		led = !led;
		gpio.digitalWrite(LED_PIN, led ? gpio.LOW : gpio.HIGH);

		/* LEDの点灯状態のUDP送信 */
		var message = {
			topic:"led",
			value: led
		};
		udp.sendText(UDP_HOST, UDP_PORT, JSON.stringify(message));
	}

	/* UDPパケット受信状態の確認 */
	var text = udp.checkRecvText();
	if( text ){
		console.log("received");

		/* LEDの点灯状態の更新 */
		led = !led;
		gpio.digitalWrite(LED_PIN, led ? gpio.LOW : gpio.HIGH);

		/* LEDの点灯状態のUDP送信 */
		var message = {
			topic:"led",
			value: led
		};
		udp.sendText(UDP_HOST, UDP_PORT, JSON.stringify(message));
	}
}, 1);
```

ENV 2 Hatについている温湿度計は、SHT30というものです。I2Cを使って制御するのですが、扱いやすいようにライブラリを用意しました。ライブラリとしてアップロードしておいてください。

```js
export default
class SHT30 {
  constructor(wire, address = 0x44) {
    this.wire = wire;
    this._address = address;
    this.cTemp = 0;
    this.fTemp = 0;
    this.humidity = 0;
  }

  async get() {
    // Start I2C Transmission
    this.wire.beginTransmission(this._address);
    // Send measurement command
    this.wire.write(0x2C);
    this.wire.write(0x06);
    // Stop I2C transmission
    if (this.wire.endTransmission() != 0)
      return 1;

	await this.sleep(500);

    // Request 6 bytes of data
    this.wire.requestFrom(this._address, 6);

    // Read 6 bytes of data
    // cTemp msb, cTemp lsb, cTemp crc, humidity msb, humidity lsb, humidity crc
    var data = this.wire.read(6);

    await this.sleep(50);

    if (this.wire.available() != 0)
      return 2;

    // Convert the data
    this.cTemp = ((((data[0] * 256.0) + data[1]) * 175) / 65535.0) - 45;
    this.fTemp = (this.cTemp * 1.8) + 32;
    this.humidity = ((((data[3] * 256.0) + data[4]) * 100) / 65535.0);

    return 0;
  }
  
  async sleep(msec){
  	return new Promise(resolve => setTimeout(resolve, msec));
  }
}

//module.exports = SHT30;
```

こんな感じで使ってます。sht30.get()は非同期関数であることに注意してください。

```js
wire.begin(0, 26);
var sht30 = new SHT30(wire);
await sht30.get();
console.log(sht30.cTemp, sht30.humidity);
```

以上