ESP32とNode-REDはUDPで簡単に連携できるので、M5StickCに、ENV 2 Hatとフェーダーユニットをつないで、Node-REDのダッシュボードに表示してみます。ついでに、M5StickCについているLEDの点灯も操作できるようにします。
ENV 2 Hat
※今回は温湿度計を使います。
フェーダーユニット
ESP32におけるUDP送受信
宣言
#include <WiFiUdp.h>
static WiFiUDP udp;
受信準備
udp.stop();
udp.begin(port);
portには、受信を待ち受けたいUDPのポート番号を指定します。
受信があったか確認
int packetSize = udp.parsePacket();
if( packetSize <= 0 )
return JS_NULL;
上記のpacketSizeが0より大きい場合は、受信データがあることを示しています。
あとは、以下のような形で、受信データを取り出します。
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を挿入しています。
送信
文字列を送信する場合の例です。
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形式です。
{
"topic": "temp" or "humidity" or "fader" or "led",
"value": topicごとの値
}
topic="temp"の場合
{
"topic": "temp",
"value": 温度(浮動小数)
}
topic="humidity"の場合
{
"topic": "humidity",
"value": 湿度(浮動小数)
}
topic="fader"の場合
{
"topic": "fader",
"value": 読み出し値(0~4095)
}
topic="led"の場合
{
"topic": "led",
"value": 点灯状態(boolean)
}
Node-REDのフロー全体は以下のような形になります。
これをNode-REDのダッシュボードで見ると以下のように見えます。
左上から順にみてみます。
まずは、ネットワークカテゴリのudp inノードです。
Node-REDにおいて、ポート番号3335番で、UDPパケットを待ち受けています。
次は、パーサカテゴリのjsonノードです。
単に、テキストで受信されたUDPパケットをJSONにパースして次のノードに渡しています。パースすることで以下のような形で以降のノードに流れていきます。
{
"topic": "",
"payload": {
"topic": "temp" or "humidity" or "fader" or "led",
"value": topicごとの値
}
}
payloadの中の値がJSON化された文字列、すなわち、UDPで受信したデータです。
ちょっと横道にそれて、共通カテゴリのdebugノードがあります。
これは、以降のノードで処理する元となった入力ノードをデバッグ出力して確認できるようにしたものです。デバッグ目的であるため、このノードは必須ではありません。
次は、changeノードです。
以降の処理では、入力されたtopicに基づいて処理分岐します。また、分岐した後はpayloadの値をNode-REDのダッシュボードに出力します。
したがって、
{
"topic": "",
"payload": {
"topic": "temp" or "humidity" or "fader" or "led",
"value": topicごとの値
}
}
の形式を、
{
"topic": UDPから受信したtopicの値,
"payload": UDPから受信したvalueの値
}
の形式に変換しているわけです。
次は、機能ノードにあるswitchノードです。
topicの内容に応じて、次のノードに振り分けています。
4つの分岐("temp" or "humidity" or "fader" or "led")があります。
次からはダッシュボードへの表示のためのノードです。
まずは、gaugeノードです。topic="fader"の場合のノードです。
TypeにはGaugeを選択しています。スピードメータのような見え方になります。
Rangeは、payloadの値がどの範囲で来るかを指定しておきます。要は最小値と最大値です。
フェーダーユニットはアナログ値の出力であるため、0から4095になります。
次が、ダッシュボードカテゴリのtextノードです。温度表示です。
payloadの浮動小数が入ってきますので、それに℃を付けて表示しています。ただ、そのまま表示すると、浮動小数点が長くなってみにくいため、浮動小数点第2位までの表示にしています。
湿度表示も同じです。
最後が、LEDの状態表示で、ダッシュボードカテゴリのswitchノードです。
以下のノードもあります。
これは、ダッシュボード上にボタンを表示して、そのボタンの押下によって、M5StickCにあるLEDの点灯・消灯をトグルさせるためのものです。
左側から、ダッシュボードカテゴリのbuttonノードです。
次が、udp outノードです。その名の通り、UDPパケットを送信するためのノードです。
送信する先のポート番号とIPv4アドレスを指定します。
送信される内容は、ひとつ前のbuttonノードの出力文ですが、今回は特にみていません。
M5StackC側の実装
UDPに先ほど示したJSON文字列を指定して送信するだけなので、あまり難しくはないと思います。
今回は、以下の技術書で紹介している、ESP32内部で動作するJavascript環境で動作させる場合のJavascriptコード例を示します。
M5StackとJavascriptではじめるIoTデバイス制御
10秒ごとに温湿度を計測するためのループ(setInterval)と、常時、ボタン押下状態の更新、フェーダの値の取得、M5StickCのボタン押下状態の確認、UDPパケット受信状態の確認を実施するためのループ(setInterval)の2つのループがあります。
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を使って制御するのですが、扱いやすいようにライブラリを用意しました。ライブラリとしてアップロードしておいてください。
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()は非同期関数であることに注意してください。
wire.begin(0, 26);
var sht30 = new SHT30(wire);
await sht30.get();
console.log(sht30.cTemp, sht30.humidity);
以上