1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-03-26

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

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

フェーダーユニット

ESP32におけるUDP送受信

宣言

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

受信準備

module_udp.cpp
  udp.stop();
  udp.begin(port);

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

受信があったか確認

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

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

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を挿入しています。

送信

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

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形式です。

{
	"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": 読み出し値(04095)
}

topic="led"の場合

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

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

image.png

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

image.png

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

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

image.png

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

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

image.png

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

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

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

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

image.png

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

次は、changeノードです。

image.png

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

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

の形式を、

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

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

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

image.png

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

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

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

image.png

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

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

image.png

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

湿度表示も同じです。

image.png

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

image.png

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

image.png

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

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

image.png

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

image.png

送信する先のポート番号と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);

以上

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?