こんにちは、ENGかぴです。
ESP32-WROOM-32EはWiFiを使用してUDP通信を行うことができます。DHT20モジュールから取得した温湿度データをクライアントの要求に対して電文で応答するUDPサーバーを実装して動作確認を行いました。
本記事はUDP通信を電文を生成してクライアント(ロガーなどの外部機器)に応答する例をまとめています。UDPライブラリを使ってUDP通信を行う方法は下記記事にまとめています。
クライアントは下記記事のVB.NETで作成したアプリを使用します。
VB.NET(VB2022)のUDP通信を非同期処理で行う方法
ESP32-WROOM-32E開発ボード(秋月電子)を使用しArduino IDEで開発を行います。また、温湿度センサーはDHT20モジュール(Grove)を使用しています。アクセスポイントにはWZR-HP-G300NH(バッファロー:生産中止)を使用しています。
ESP32-WROOM-32E(以下ではESP32とする)を使って動作確認したことをまとめています。
ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方
UDP通信の全体構成

ESP32のWiFiを使用してデータを送信すると標準ではTCP/IPの通信になります。UDP通信を使用する場合はUDPライブラリを追加してサーバーまたはクライアントの機能を追加する必要があります。本記事ではUDPサーバーの機能を使用します。
①のESP32はUDPサーバーとして動作し、②クライアントからのUDPパケットを待機します。②のクライアントで使用しているパソコンはWiFiを搭載していないためLANケーブルをアクセスポイントに接続して通信します。
クライアントから「CHARTAPP」の文字列を送信するとESP32は温湿度データを含めた電文で応答するようにします。
ESP32とクライアントの通信状態をWiresharkでパケットをキャプチャーして動作確認を行います。
UDPライブラリを実装する
#include "AsyncUDP.h"
unsigned int localPort = 10002; // local port to listen on
AsyncUDP Udp;
void setup() {
if(Udp.listen(localPort )) {
Udp.onPacket(UdpRcv);//受信イベントが発生した時に遷移する関数を登録
}
}
UDP通信を行う方法は下記記事でまとめています。ここではポイントを絞って説明します。
UDPライブラリを使用するためAsyncUDP.hをインクルードします。AsyncUDPクラスの変数としてUdpをインスタンス化(Udpオブジェクト)します。
Udpオブジェクトのlisten()関数でUDP通信を行うポートを指定します。ポートを指定しない場合はIPが自局のものであればすべて受け付けます。
UdpオブジェクトのonPacket()関数はパケットを受信した時に遷移させる関数を登録します。登録する関数はAsyncUDPPacketクラスの引数を持つ関数です。例では受信パケットの確認を行う自作の関数UdpRcv()を指定しています。
PR:スキマ時間を有効に!スマホで学べる人気のオンライン資格講座【スタディング】まずは気になる講座を無料で体験しよう!
受信パケットの確認
/* 受信イベントが発生すると遷移する関数 */
void UdpRcv(AsyncUDPPacket packet){
String str="";
uint8_t i;
uint8_t *chr;
chr = packet.data();
for(i=0; i< sizeof(logdata.header); i++){
str += (char)*chr;
logdata.header[i] = *chr;
chr++;
}
if(str == "CHARTAPP" ){
UdpSend(packet);//電文で応答
}
}
引数のpacketに受信パケットの情報が格納されているのでデータを確認します。パケットのデータが「CHARTAPP」の文字列であるかを確認し、一致すると電文で応答するようにします。
uint8_t型のポインタにpaket.data()を指定する(6行目)ことでパケットのデータを格納しているアドレスを取得します。
電文のヘッダ(以下の電文を生成する参照)は8Byte(バイト)としているので8回for文で繰り返して文字列を生成します。9行目ではポインタの値をchar型にキャストして文字データとして認識させて文字列に追加します。11行目でポインタを更新しています。
生成した文字列と「CHARTAPP」の文字列を比較して一致した場合は、自作のUdpSend()関数で電文を生成してクライアントに応答します。
PR:わからないを放置せず、あなたにあった最低限のスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!
電文を生成する

電文はヘッダー、湿度データ、温度データで構成します。ヘッダーはクライアントから受信するパケットと同じものをセットすることでクライアントで応答電文であることの判定に使用できます。
humi = (int16_t)(temp_humi[0] * 100 );//湿度データ:humidata[]
temp = (int16_t)(temp_humi[1] * 100 );//湿度データ:tempdata[]
湿度データ、温度データはDHT20モジュールから取得した浮動小数点を100倍したデータを整数データにキャストして2バイトのデータとして送信します。例)温度が25.12℃の場合は2512を送信します。
humi_l = humi;
temp_l = (int16_t)(((float)temp + 4000)*10000/12000);
トレンドグラフ用のデータはChartでグラフ表示する際のプロットに使用することを想定し0~10000の値の範囲で湿度と温度をスケーリングした値です。
DHT20は湿度は0~100%の範囲なので100倍したデータがそのままスケーリング値になります。温度は-40~+80℃が範囲なので-40℃が0、+80℃が10000になるようにスケーリングします。
/* UDPパケットの電文チェックと返信 */
void UdpSend(AsyncUDPPacket packet){
logdata.humidata[0] = (humi >> 8 );
logdata.humidata[1] = humi & 0xFF;
packet.write(&logdata.header[0],sizeof(logdata));
}
電文をpacketオブジェクトのwrite()メソッドでクライアントに送信します。第1引数に送信するデータのアドレスを指定します。第2引数は送信するデータのサイズを指定します。
PR:次の一手があなたの未来を決める! アビリティクラウドーフリーランスを目指すあなたに向けたエンジニアのマッチングサービス。大手のエンドクライアントメインの企業が多く、業務内容も規模が大きいのが特徴
DHT20モジュールからデータを取得
Arduino IDEのライブラリマネージャでGrove_Temperature_And_Humidity_Sensor by Seeed Studioをインストールします。使用例は以下の通りです
#include <DHT.h>
#include <Grove_Temperature_And_Humidity_Sensor.h>
#include <Wire.h>
DHT dht(DHT20);
float temp_humi[2];
void setup() {
Serial.begin(115200);
Wire.begin();
dht.begin();
}
void loop() {
if (!dht.readTempAndHumidity(temp_humi)) {
Serial.print("Humidity: ");
Serial.print(temp_humi[0]);
Serial.print(" %\t");
Serial.print("Temperature: ");
Serial.print(temp_humi[1]);
Serial.println(" *C");
}
else{
Serial.println("Failed to get temprature and humidity value.");
}
delay(2000);
}
Grove_Temperature_And_Humidity_Sensor.h、DHT.h、Wire.hをインクルードします。DHT クラスに引数のDHT20を指定し初期化してdhtをインスタンス化(dhtオブジェクト)します。
Wire(I2C)通信を使用するためWire.begin()でWireを初期化してからdhtオブジェクトのbegin()メソッドでDHT20を初期化します。
dhtオブジェクトのreadTempAndHumidity()メソッドで温湿度データを取得します。測定データは浮動小数点に換算されているため浮動小数点の配列を指定しています。
動作確認

ESP32は電源を入れるとアクセスポイントに接続します。接続に成功するとUDPパケットの受信を待機し、DHT20モジュールの測定を開始します。
クライアントはVB.NETで作成したVB.NET(VB2022)のUDP通信を非同期処理で行う方法のアプリを使用します。

アプリを起動し接続元からUDP通信するイーサーネットのインターフェースを選択します。ESP32のIPアドレス(192.168.11.10)とポート番号(10002)を指定し、「接続」をクリックするとUDP通信を開始します。
「送信」ボタン横のテキストボックスに「CHARTAPP」の文字列を指定し、「送信」をクリックするとESP32に文字列を送信します。下のテキストボックスはESP32から応答で受信した文字列を表示しています。
また「CHARTAPP」以外の文字列を送信するとESP32でヘッダー不一致と判定し応答しないことが確認できます。
WiresharkでESP32とアプリ間のパケットを確認します。

No.36のパケットを確認します。Source「192.168.11.10」はESP32でDestination「192.168.11.100」はアプリです。右下のパケットの詳細すると「43 48 41 ・・・・45 13 e9」の16バイトが電文のデータです。
湿度のデータは16進数で「1ccb」なので10進数に換算すると7371になります。100倍値をセットしているので湿度は73.71%になります。同様に温度のデータは16進数で「0845」なので21.17℃になります。
クライアントアプリとWiresharkで電文のUDPパケットが送信できていることが確認できました。
ソースコード全体
ソースコードは記事作成時点において動作確認できていますが、使用しているライブラリの更新により動作が保証できなくなる可能性があります。また、コメントに誤記が含まれていることもありますが、参考にお使いいただければと思います。
#include <DHT.h>
#include <Grove_Temperature_And_Humidity_Sensor.h>
#include <WiFi.h>
#include "AsyncUDP.h"
#include <Wire.h>
#define TIME_UP 0
#define TIME_OFF -1
#define BASE_CNT 10
#define TIM_MEASURE 1000
struct LOGDATA{
uint8_t header[8];
uint8_t humidata[2];
uint8_t humilog[2];
uint8_t tempdata[2];
uint8_t templog[2];
};
const char *ssid = "xxxxxxxxxxxxx"; //SSID
const char *pass = "yyyyyyyyyyyyy"; //password
const IPAddress ip(192,168,11,10); //IPアドレス
const IPAddress subnet(255,255,255,0); //サブネットマスク
/* 変数宣言 */
AsyncUDP udp;
int16_t waitcnt;
uint32_t timer= millis();
int16_t timMeasure;
DHT dht(DHT20);
float temp_humi[2];
int16_t temp;
int16_t temp_l;
int16_t humi;
int16_t humi_l;
LOGDATA logdata;
/* プロトタイプ宣言 */
void mainApp(void);
void mainTimer(void);
void UdpRcv(AsyncUDPPacket packet);
void setup() {
Serial.begin(115200);
Wire.begin();
dht.begin();
WiFi.begin(ssid,pass);
WiFi.config(ip,ip,subnet);
while( WiFi.status() != WL_CONNECTED ){
delay(1000);
Serial.print(".");
if( ++waitcnt >= 60 ){
esp_restart();
}
Serial.print(waitcnt);
}
Serial.println("");
Serial.print("Connected to ");
Serial.println(ssid);
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
if(udp.listen(10002)) {
Serial.print("UDP Listening on IP: ");
Serial.println(WiFi.localIP());
udp.onPacket(UdpRcv);
}
}
/* 受信イベントが発生すると遷移する関数 */
void UdpRcv(AsyncUDPPacket packet){
String str="";
uint8_t i;
uint8_t *chr;
Serial.print("UDP Packet Type: ");
Serial.println(packet.isBroadcast()?"Broadcast":packet.isMulticast()?"Multicast":"Unicast");
Serial.print("From: ");
Serial.print(packet.remoteIP());
Serial.print(":");
Serial.print(packet.remotePort());
Serial.print(", To: ");
Serial.print(packet.localIP());
Serial.print(":");
Serial.print(packet.localPort());
Serial.print(" Length: ");
Serial.println(packet.length());
chr = packet.data();
for(i=0; i< sizeof(logdata.header); i++){
str += (char)*chr;
logdata.header[i] = *chr;
chr++;
}
if(str == "CHARTAPP" ){
UdpSend(packet);//電文で応答
}
}
void loop() {
mainApp();
mainTimer();
}
/* メイン処理関数 */
void mainApp(void){
if( timMeasure == TIME_UP ){
timMeasure = TIM_MEASURE;
if (!dht.readTempAndHumidity(temp_humi)) {
Serial.print("Humidity: ");
Serial.print(temp_humi[0]);
Serial.print(" %\t");
Serial.print("Temperature: ");
Serial.print(temp_humi[1]);
Serial.println(" *C");
humi = (int16_t)(temp_humi[0] * 100 );
temp = (int16_t)(temp_humi[1] * 100 );
humi_l = humi;
temp_l = (int16_t)(((float)temp + 4000)*10000/12000);
}
else{
Serial.println("Failed to get temprature and humidity value.");
}
}
}
/* タイマ管理 */
void mainTimer(void){
if( millis() - timer >= BASE_CNT ){
timer = millis();
if( timMeasure > TIME_UP ){
--timMeasure;
}
}
}
/* UDPパケットの電文チェックと返信 */
void UdpSend(AsyncUDPPacket packet){
logdata.humidata[0] = (humi >> 8 );
logdata.humidata[1] = humi & 0xFF;
logdata.humilog[0] = (humi_l >> 8 );
logdata.humilog[1] = humi_l & 0xFF;
logdata.tempdata[0] = (temp >> 8 );
logdata.tempdata[1] = temp & 0xFF;
logdata.templog[0] = (temp_l >> 8 );
logdata.templog[1] = temp_l & 0xFF;
packet.write(&logdata.header[0],sizeof(logdata));
}
関連リンク
Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。
Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方
Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方
ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方
最後まで、読んでいただきありがとうございました。