1.双方向通信の考え方
前回の仕組みで標記のような心配があるとしても、計測用端末の台数が少なければ、十分長い計測間隔(前回のケースでは loraTerminal.inoのスリープ時間)を設定することで問題の発生は抑えられるでしょう。しかし、端末台数が多くなり計測間隔が短くなると、この方法では対応が難しくなります。
問題の原因は、各計測用端末に最初に一度だけ計測開始指示を送った後は、計測タイミングを端末任せにしていることにあります。端末の性能などに起因するタイミングのズレが累積すると、混信が起きるわけです。そこで、毎回センター側から計測を指示して、端末はそれに合わせて計測と送信を行う双方向通信を採用すれば、この問題を一気に解決することができます。
具体的な方法として、図に示す 2つの通信形態が考えられます。なお図の t0は、計測間隔ごとに到達する計測開始時刻で、kは計測間隔を表します。
①方式A:順次送受信方式
時刻 t0から計測間隔 k秒ごとに、各端末へ計測指示を順番に送信して計測データの返信を待ちます。図の流れをそのまま実現しようとすると、k秒後に受信できなかったケースにどう対応するかといった対策が必要になります。もし、送信と受信を独立したタスクとして処理することができれば、送信は前回の loraCentral.inoの kickOffTerminals()関数の仕組みを利用することができます。受信は別のタスクとして常時受信状態にしておけば、計測データの受信の成否に関係なく、届いたデータを単純に受け入れることが可能になります。
これを実現するために、後述する ESP32のマルチタスク機能を利用します。
②方式B:ブロードキャスト方式
時刻 t0に、全端末に向けて計測指示をいっせいにブロードキャストして、後は次の t0になるまで受信をするだけです。計測指示を受けた端末は、前回の loraTerminal.inoでディープスリープのタイマー設定と同様に、自身の端末番号から計測待ち時間を計算して、その時間だけ待ってから計測と送信を行います。センター側と計測用端末側とも簡単なスケッチで、混信を避けながら通信することができます。

今回はこの両方の方式を試します。どちらの方式も、端末側はセンターからの計測指示を受信しなければならないので、前回のようにディープスリープに移行することはできません。これをどう解決するかは、次回にゆずることにします。
2.ESP32のマルチタスク処理
ESP32は 2つの CPUを持った、いわゆるデュアルコアの MPUです。これらを有効に活用するためには、タスクの管理や同期制御のためのリアルタイムOSが不可欠です。ESP32の開発環境である ESP-IDF(Espressif Systems社から提供されている ESP-IoT Development Framework)は 、組み込みシステム用のオープンソースである FreeRTOS(Real-Time Operating System)をベースに構築されています。
このため、ESP32のマルチタスクなど高度な機能を簡単に利用することができます。余談ながら、Arduinoのスケッチ
void setup() {
}
void loop() {
}
も、FreeRTOSのマルチタスクを利用した仕組みになっています。
(1) マルチタスクの作成
マルチタスクの作成はとても簡単です。この好例が、ライブラリ esp32_e220900t22s_jp_lib_v1.6のサンプルスケッチ lora_multi_task.inoにあるので引用します。
// マルチタスク
xTaskCreateUniversal(LoRaRecvTask, "LoRaRecvTask", 8192, NULL, 1, NULL, APP_CPU_NUM);
xTaskCreateUniversal(LoRaSendTask, "LoRaSendTask", 8192, NULL, 1, NULL, APP_CPU_NUM);
引数の先頭で指定している LoRaRecvTaskと LoRaSendTaskが、それぞれマルチタスクとして実行させる関数の名前です。このように受信タスクと送信タスクをマルチタスクにすることで、それぞれが必要になった時点で独立して動作させることができるようになります。
ESP32のマルチタスク作成には次の 3種類があります。
・xTaskCreate() : シングルコア向けのタスク作成
・xTaskCreatePinnedToCore() : コアを指定したタスク作成
・xTaskCreateUniversal() : 最新版で登場したタスク作成
FreeRTOSの関数は、先頭が 'x'で始まるのが特徴です。最新版のライブラリでは、これらの関係が \cores\esp32\esp32-hal-misc.cの中で次のように記述されています。したがって、xTaskCreateUniversal()を使えば他の 2つの機能は必要ないことがわかります。
$ mosquitto -v BaseType_t xTaskCreateUniversal(TaskFunction_t pxTaskCode, const char *const pcName, const uint32_t usStackDepth, void *const pvParameters, UBaseType_t uxPriority, TaskHandle_t *const pxCreatedTask, const BaseType_t xCoreID) { #ifndef CONFIG_FREERTOS_UNICORE if (xCoreID >= 0 && xCoreID < 2) { return xTaskCreatePinnedToCore(pxTaskCode, pcName, usStackDepth, pvParameters, uxPriority, pxCreatedTask, xCoreID); } else { #endif return xTaskCreate(pxTaskCode, pcName, usStackDepth, pvParameters, uxPriority, pxCreatedTask); #ifndef CONFIG_FREERTOS_UNICORE } #endif } |
(2) xTaskCreateUniversal()の引数
次に、7つの引数で指定するパラメータの内容を見ておきましょう。
xTaskCreateUniversal(TaskFunction_t pxTaskCode, const char *const pcName,
const uint32_t usStackDepth, void *const pvParameters, UBaseType_t uxPriority,
TaskHandle_t *const pxCreatedTask, const BaseType_t xCoreID);
1) TaskFunction_t pxTaskCode
実行するタスクの関数を指定します。
2) const char *const pcName
実行するタスクの名前を指定します。特に意味はありません。
3) const uint32_t usStackDepth
スタックメモリ量を指定します。内部では 8192を使用しているので、これを基準として指定します、
4) void *const pvParameters
作成するタスクに渡す起動パラメータです。ない場合は NULLを指定します。
5) UBaseType_t uxPriority
タスクの優先度です。(configMAX_PRIORITIES - 1) 、つまり 0~24が指定できます。24が最優先度です。
・FreeRTOSConfig.h内の定義
#define configMAX_PRIORITIES ( 25 )
6) TaskHandle_t *const pxCreatedTask
タスクの終了など、作成したタスクを参照するハンドルです。必要がなければ NULLを指定します。
7) const BaseType_t xCoreID
実行するコアを指定します。2つのコアは soc.h内で次のように定義されています。
#define PRO_CPU_NUM (0)
#define APP_CPU_NUM (1)
loop()関数は APP_CPU_NUMで実行されているので、通常は APP_CPU_NUMでよいと思われます。
引数は多いですが、多くは既定値を指定するだけで簡単に作成することができます。しかし、実際に動かしてみるとエラーが発生したり思うように動作しなかったりすることもあります。これは次に述べるタスクの構造も関係するので、それを理解しておく必要があります。
ここでもう一度、先に掲げたサンプルスケッチを見直しておきましょう。
xTaskCreateUniversal(LoRaRecvTask, "LoRaRecvTask", 8192, NULL, 1, NULL, APP_CPU_NUM);
xTaskCreateUniversal(LoRaSendTask, "LoRaSendTask", 8192, NULL, 1, NULL, APP_CPU_NUM);
それぞれ LoRa受信用のタスク LoRaRecvTaskと送信用のタスク LoRaSendTaskを作成しています。その他は両方共に、スタックメモリ量はメインループのデフォールトの 8192を割り当て、優先順位は低く、コアは APP_CPU_NUM、起動パラメータとタスクハンドルは無しとしています。これによって 2つのタスクは低優先度で、メインの処理を邪魔することなく動作することになります。
(3)対象タスクの構造
先に例示した LoRaRecvTaskと LoRaSendTaskは、どちらもシンプルで同じような構成のコードになります。以下の例では処理内容は略していますが気にしないでください。構造上の要点は次の3点です。
・タスク関数は引数に voidのポインターを指定すること。
・関数内では無限ループを形成し、終了させてはならない。
・何も処理しない場合は最少でも delay(1)を指定すること。
void LoRaRecvTask(void *pvParameters) {
while (1) {
if (lora.RecieveFrame(&data) == 0) {
SerialMon.printf("recv data: %s\n", data.recv_data);
SerialMon.flush();
}
delay(1);
}
}
delay(1)を忘れると、システムの状態を監視しているウォッチドッグタイマー(WDT)が動作して再起動が発生します。
3.順次送受信方式の実験
(1) センター側のタスク:loraCentral2A.ino
順次送受信方式でのセンター側タスクは、計測時刻になるたびに、複数の計測用端末宛に時間差をおいて計測指示を送信します。計測時刻になったかどうかは反復処理関数 loop()内で判定し、計測時刻になるとグローバル変数 bool doSendSignalを trueにセットします。
送信タスク LoRaSendTask()内では doSendSignalを監視していて、trueになると各計測端末へ、端末識別番号ごとに時間差をおきながら計測指示メッセージ "Go ahead!"を送信します。
受信タスクは計測端末からのデータの到着を監視して、到着すると受信データをシリアルモニターに表示します。この時 0.1秒だけ内蔵LEDを点灯します。
ここで、マルチタスクに指定された送信タスクと受信タスクは並行して動作しています。LoRaモジュールが受信を感知すると LoRaRecvTask()内の一連の処理が実行されます。また loop()関数で doSendSignalが trueにセットされると、LoRaSendTask()内の計測シグナル送信処理が実行されます。送受信のタイミングにとらわれずに、それぞれを独立したタスクとして記述できるので簡潔で見通しの良いコードになります。
コードの解説
①ヘッダーファイルとデータ等の定義
・16行目: 今回は2台の端末でテストをします。
・17行目: 1.5秒間隔で各計測端末への開始シグナルを送信します。
・28行目: 計測間隔は10秒です。
・29行目: 前回の計測時刻をここに退避します。
・30行目: 送信要求の判定フラグです。
・31行目: 計測端末宛の計測指示メッセージです。
②初期化処理 setup部
・49~50行: それぞれ受信タスク LoRaRecvTask()と送信タスク LoRaSendTask()のマルチタスク設定です。
③反復処理 loop部
・62行目: 現在の millis()の秒数(mSec)を取得します。
・63行目: 前回からの経過時間を秒数で求めます。
・64行目: 経過時間が計測間隔以上であれば doSendSignalをセットして、前回の計測時刻を更新します。
④受信タスク LoRaRecvTask部
・81行: 計測端末からの受信を監視して、受信すればデータの表示などをします
⑤送信タスク LoRaSendTask部
・101行目: 送信要求があると以下を行います。
・102行目: まず、送信要求フラグをリセットします。
・104行目: すべての計測端末に対して以下を行います。
・105行目: コンフィギュレーションの送信先アドレスに計測端末番号をセットします。
・106行目: 計測端末宛に計測指示メッセージを送信します。
・111行目: 計測間隔の秒数だけ次の処理を待たせます。
#include "esp32_e220900t22s_jp_lib.h"
#define LED_BUILTIN 2 // 内蔵LED
#define SEND_DATA_SIZE 29 // 32-3
#define NUMBER_OF_TERMINALS 2 // 端末台数
#define KICK_OFF_INTERVAL 1.5 // キックオフ間隔(秒)
#define SEND_DATA_SIZE 29 // 32-3
CLoRa lora; // LoRaクラスのインスタンス
struct LoRaConfigItem_t config; // LoRaコンフィギュレーション
struct RecvFrameE220900T22SJP_t data; // 受信用構造体
// Function prototype
void LoRaRecvTask(void *pvParameters);
void LoRaSendTask(void *pvParameters);
int MeasureInterval = 10; // 計測間隔(秒)
unsigned long prev_value = 0; // 前回の計測時刻
bool doSendSignal = false;
char msg[SEND_DATA_SIZE] = "Go ahead!";
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
SerialMon.begin(9600);
delay(1000);
// 既定のLoRaコンフィギュレーションを取得して
lora.SetDefaultConfigValue(config);
delay(10);
// E220-900T22S(JP)を初期設定する
if (lora.InitLoRaModule(config)) {
SerialMon.printf("LoRa init error\n");
return;
}
// マルチタスクを設定して
xTaskCreateUniversal(LoRaRecvTask, "LoRaRecvTask", 8192, NULL, 1, NULL, APP_CPU_NUM);
xTaskCreateUniversal(LoRaSendTask, "LoRaSendTask", 8192, NULL, 1, NULL, APP_CPU_NUM);
// ノーマルモードへ移行する
SerialMon.println("switch to normal mode.");
lora.SwitchToNormalMode();
delay(10);
SerialMon.println("Start sending message!");
}
void loop() {
int differ;
unsigned long now = millis();
differ = (now - prev_value) / 1000;
if (differ >= MeasureInterval && doSendSignal == false) {
doSendSignal = true;
prev_value = now;
Serial.printf("\n> Request to send message .... %d\n", now / 1000);
}
delay(1000);
}
/*
受信タスク
*/
void LoRaRecvTask(void *pvParameters) {
struct tm *tm;
time_t t;
char str[256];
while (1) {
if (lora.ReceiveFrame(&data) == 0) { // 受信データが届くと表示処理を行う
t = time(NULL); tm = localtime(&t);
sprintf(str, "< Get responce: %02d:%02d:%02d", tm->tm_hour, tm->tm_min, tm->tm_sec);
SerialMon.println(str);
data.recv_data[data.recv_data_len] = '\0';
sprintf(str, "Receive data: %s", data.recv_data);
SerialMon.println(str);
digitalWrite(LED_BUILTIN, HIGH);
delay(100);
digitalWrite(LED_BUILTIN, LOW);
}
delay(1);
}
}
/*
送信タスク
*/
void LoRaSendTask(void *pvParameters) {
while (1) {
if (doSendSignal) { // 送信要求があると送信処理を行う
doSendSignal = false;
for (int i=1; i<=NUMBER_OF_TERMINALS; i++) {
config.target_address = i;
int state = lora.SendFrame(config, (uint8_t *)msg, sizeof(msg));
if (state == 0)
SerialMon.printf("Terminal-%d kick off succeeded!\n", i);
else
SerialMon.printf("Terminal-%d kick off failed!\n", i);
delay(KICK_OFF_INTERVAL * 1000);
}
}
delay(1);
}
}
(2) 計測用端末側のタスク:loraTerminal2A.ino
計測用端末のコードはきわめてシンプルで、動作後はひたすらセンターからの計測指示を待ちます。指示が届くと温度と湿度を計測して計測回数と共に編集してセンターへ送信します。この時 0.1秒だけ内蔵LEDを点灯します。
コードの解説
①ヘッダーファイルとデータ等の定義
・15~16行目: スケッチ書き込み前に、各端末ごとに重複しない識別番号(1,2,3,・・・・)を設定します。
②初期化処理 setup部
・40行目: 自身のアドレスに計測端末番号をセットします。
これで、ブロードキャストされた情報と、このアドレス向けに送信された情報だけを受信できます。
③反復処理 loop部
・55行目: センターからの計測指示を受信すると温度と湿度を計測し、端末番号と計測回数を編集して送信します。
この時、内蔵LEDを 0.1秒だけ点灯します。
#include "esp32_e220900t22s_jp_lib.h"
#include <DHT.h>
#define TERMINAL_NUMBER 1
//#define TERMINAL_NUMBER 2
#define LED_BUILTIN 2 // 内蔵LED
#define SEND_DATA_SIZE 61 // 64-3
#define DHTTYPE DHT11 // DHTセンサーの型式
/* DHTセンサー*/
const int DHTPin = 14; // DHTセンサーの接続ピン
DHT dht(DHTPin, DHTTYPE); // DHTクラスの実体
CLoRa lora; // LoRaクラスのインスタンス
struct LoRaConfigItem_t config; // LoRaコンフィギュレーション
struct RecvFrameE220900T22SJP_t data; // 受信用構造体
int replyCount = 0; // 返答(計測)回数
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
SerialMon.begin(9600);
dht.begin();
delay(5000);
// 既定のLoRaコンフィギュレーションを取得して
lora.SetDefaultConfigValue(config);
// E220-900T22S(JP)を初期設定する
config.own_address = TERMINAL_NUMBER;
if (lora.InitLoRaModule(config)) {
SerialMon.printf("LoRa init error\n");
return;
}
// ノーマルモードへ移行する
SerialMon.printf("switch to normal mode\n");
lora.SwitchToNormalMode();
SerialMon.printf("Start terminal No.%d\n", TERMINAL_NUMBER);
}
void loop() {
char msg[SEND_DATA_SIZE] = { 0 };
// 計測指示を受信すると
if (lora.ReceiveFrame(&data) == 0) {
// 温度と湿度を計測して編集し
float t = dht.readTemperature();
float h = dht.readHumidity();
sprintf(msg, "%02d:%04d=%4.1f,%4.1f", TERMINAL_NUMBER, ++replyCount, t, h);
SerialMon.printf("送信データ: %s\n", msg);
// 計測データを送信する
if (lora.SendFrame(config, (uint8_t *)msg, sizeof(msg)) == 0) {
SerialMon.println("send succeeded.\n");
} else {
SerialMon.println("send failed.\n");
}
digitalWrite(LED_BUILTIN, HIGH);
delay(100);
digitalWrite(LED_BUILTIN, LOW);
}
delay(1);
}
(3) 送受信の実験
まず、loraTerminal.inoを書き込んだ 2台の計測用端末の電源を入れます。続いてセンター側の ESP32をパソコンに USB接続してArduino IDEを開き、loraCentral2A.inoを書き込みます。
シリアルモニターに "Start sending message!"が表示されて少し待つと、計測端末1号機の LEDが光り、これに呼応してセンター側の LEDが光り、わずかに待って同様に計測端末2号機の LEDとセンター側のそれが光ります。
シリアルモニターには、計測時刻になると「Request to send message .... 」に続いてセンター側の起動からの時間が表示されます。続いて計測端末宛のメッセージが送信され、データを受信すると、「Receive data: 」に続いて1号機からの受信データ、さらに2号機からのデータが表示されます。
【参考までに】 センター側 ESP32の計測時刻判定について
センター側での計測時刻の到来については、前記 loraCentral2A.inoの loop()のように millis関数と計測間隔を使って判定しています。millis()は ESP32を起動してからの経過時間をミリ秒単位で返却する関数ですが、返却値は unsigend longなので 4294967296になると(約49日経過すると)オーバーフローします。この影響を避けるために、現在の時刻(millis関数の値)から前回の計測時刻の差を求めて計測間隔と比較しています。
(現在のmillisの値 - 前回計測時のmillisの値)≧ 計測間隔?
このようにすることで、次のように一定間隔での計測指示が可能になります。ただし表示内容は、 Now:現在の時刻、Prev:前回の計測時刻、Differ:経過時間、trigger:計測指示の要否(1=必要、0=不要)です。trigger以外の数値の単位は秒です。
・63行目: 前回からの経過時間を秒数で求めます。
・64行目: 経過時間が計測間隔以上であれば doSendSignalをセットして、前回の計測時刻を更新します。
・102行目: まず、送信要求フラグをリセットします。
・104行目: すべての計測端末に対して以下を行います。
・105行目: コンフィギュレーションの送信先アドレスに計測端末番号をセットします。
・106行目: 計測端末宛に計測指示メッセージを送信します。
・111行目: 計測間隔の秒数だけ次の処理を待たせます。
#include "esp32_e220900t22s_jp_lib.h" #define LED_BUILTIN 2 // 内蔵LED #define SEND_DATA_SIZE 29 // 32-3 #define NUMBER_OF_TERMINALS 2 // 端末台数 #define KICK_OFF_INTERVAL 1.5 // キックオフ間隔(秒) #define SEND_DATA_SIZE 29 // 32-3 CLoRa lora; // LoRaクラスのインスタンス struct LoRaConfigItem_t config; // LoRaコンフィギュレーション struct RecvFrameE220900T22SJP_t data; // 受信用構造体 // Function prototype void LoRaRecvTask(void *pvParameters); void LoRaSendTask(void *pvParameters); int MeasureInterval = 10; // 計測間隔(秒) unsigned long prev_value = 0; // 前回の計測時刻 bool doSendSignal = false; char msg[SEND_DATA_SIZE] = "Go ahead!"; void setup() { pinMode(LED_BUILTIN, OUTPUT); SerialMon.begin(9600); delay(1000); // 既定のLoRaコンフィギュレーションを取得して lora.SetDefaultConfigValue(config); delay(10); // E220-900T22S(JP)を初期設定する if (lora.InitLoRaModule(config)) { SerialMon.printf("LoRa init error\n"); return; } // マルチタスクを設定して xTaskCreateUniversal(LoRaRecvTask, "LoRaRecvTask", 8192, NULL, 1, NULL, APP_CPU_NUM); xTaskCreateUniversal(LoRaSendTask, "LoRaSendTask", 8192, NULL, 1, NULL, APP_CPU_NUM); // ノーマルモードへ移行する SerialMon.println("switch to normal mode."); lora.SwitchToNormalMode(); delay(10); SerialMon.println("Start sending message!"); } void loop() { int differ; unsigned long now = millis(); differ = (now - prev_value) / 1000; if (differ >= MeasureInterval && doSendSignal == false) { doSendSignal = true; prev_value = now; Serial.printf("\n> Request to send message .... %d\n", now / 1000); } delay(1000); } /* 受信タスク */ void LoRaRecvTask(void *pvParameters) { struct tm *tm; time_t t; char str[256]; while (1) { if (lora.ReceiveFrame(&data) == 0) { // 受信データが届くと表示処理を行う t = time(NULL); tm = localtime(&t); sprintf(str, "< Get responce: %02d:%02d:%02d", tm->tm_hour, tm->tm_min, tm->tm_sec); SerialMon.println(str); data.recv_data[data.recv_data_len] = '\0'; sprintf(str, "Receive data: %s", data.recv_data); SerialMon.println(str); digitalWrite(LED_BUILTIN, HIGH); delay(100); digitalWrite(LED_BUILTIN, LOW); } delay(1); } } /* 送信タスク */ void LoRaSendTask(void *pvParameters) { while (1) { if (doSendSignal) { // 送信要求があると送信処理を行う doSendSignal = false; for (int i=1; i<=NUMBER_OF_TERMINALS; i++) { config.target_address = i; int state = lora.SendFrame(config, (uint8_t *)msg, sizeof(msg)); if (state == 0) SerialMon.printf("Terminal-%d kick off succeeded!\n", i); else SerialMon.printf("Terminal-%d kick off failed!\n", i); delay(KICK_OFF_INTERVAL * 1000); } } delay(1); } }
(2) 計測用端末側のタスク:loraTerminal2A.ino
計測用端末のコードはきわめてシンプルで、動作後はひたすらセンターからの計測指示を待ちます。指示が届くと温度と湿度を計測して計測回数と共に編集してセンターへ送信します。この時 0.1秒だけ内蔵LEDを点灯します。
コードの解説
①ヘッダーファイルとデータ等の定義
・15~16行目: スケッチ書き込み前に、各端末ごとに重複しない識別番号(1,2,3,・・・・)を設定します。
②初期化処理 setup部
・40行目: 自身のアドレスに計測端末番号をセットします。
これで、ブロードキャストされた情報と、このアドレス向けに送信された情報だけを受信できます。
③反復処理 loop部
・55行目: センターからの計測指示を受信すると温度と湿度を計測し、端末番号と計測回数を編集して送信します。
この時、内蔵LEDを 0.1秒だけ点灯します。
#include "esp32_e220900t22s_jp_lib.h"
#include <DHT.h>
#define TERMINAL_NUMBER 1
//#define TERMINAL_NUMBER 2
#define LED_BUILTIN 2 // 内蔵LED
#define SEND_DATA_SIZE 61 // 64-3
#define DHTTYPE DHT11 // DHTセンサーの型式
/* DHTセンサー*/
const int DHTPin = 14; // DHTセンサーの接続ピン
DHT dht(DHTPin, DHTTYPE); // DHTクラスの実体
CLoRa lora; // LoRaクラスのインスタンス
struct LoRaConfigItem_t config; // LoRaコンフィギュレーション
struct RecvFrameE220900T22SJP_t data; // 受信用構造体
int replyCount = 0; // 返答(計測)回数
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
SerialMon.begin(9600);
dht.begin();
delay(5000);
// 既定のLoRaコンフィギュレーションを取得して
lora.SetDefaultConfigValue(config);
// E220-900T22S(JP)を初期設定する
config.own_address = TERMINAL_NUMBER;
if (lora.InitLoRaModule(config)) {
SerialMon.printf("LoRa init error\n");
return;
}
// ノーマルモードへ移行する
SerialMon.printf("switch to normal mode\n");
lora.SwitchToNormalMode();
SerialMon.printf("Start terminal No.%d\n", TERMINAL_NUMBER);
}
void loop() {
char msg[SEND_DATA_SIZE] = { 0 };
// 計測指示を受信すると
if (lora.ReceiveFrame(&data) == 0) {
// 温度と湿度を計測して編集し
float t = dht.readTemperature();
float h = dht.readHumidity();
sprintf(msg, "%02d:%04d=%4.1f,%4.1f", TERMINAL_NUMBER, ++replyCount, t, h);
SerialMon.printf("送信データ: %s\n", msg);
// 計測データを送信する
if (lora.SendFrame(config, (uint8_t *)msg, sizeof(msg)) == 0) {
SerialMon.println("send succeeded.\n");
} else {
SerialMon.println("send failed.\n");
}
digitalWrite(LED_BUILTIN, HIGH);
delay(100);
digitalWrite(LED_BUILTIN, LOW);
}
delay(1);
}
(3) 送受信の実験
まず、loraTerminal.inoを書き込んだ 2台の計測用端末の電源を入れます。続いてセンター側の ESP32をパソコンに USB接続してArduino IDEを開き、loraCentral2A.inoを書き込みます。
シリアルモニターに "Start sending message!"が表示されて少し待つと、計測端末1号機の LEDが光り、これに呼応してセンター側の LEDが光り、わずかに待って同様に計測端末2号機の LEDとセンター側のそれが光ります。
シリアルモニターには、計測時刻になると「Request to send message .... 」に続いてセンター側の起動からの時間が表示されます。続いて計測端末宛のメッセージが送信され、データを受信すると、「Receive data: 」に続いて1号機からの受信データ、さらに2号機からのデータが表示されます。
【参考までに】 センター側 ESP32の計測時刻判定について
センター側での計測時刻の到来については、前記 loraCentral2A.inoの loop()のように millis関数と計測間隔を使って判定しています。millis()は ESP32を起動してからの経過時間をミリ秒単位で返却する関数ですが、返却値は unsigend longなので 4294967296になると(約49日経過すると)オーバーフローします。この影響を避けるために、現在の時刻(millis関数の値)から前回の計測時刻の差を求めて計測間隔と比較しています。
(現在のmillisの値 - 前回計測時のmillisの値)≧ 計測間隔?
このようにすることで、次のように一定間隔での計測指示が可能になります。ただし表示内容は、 Now:現在の時刻、Prev:前回の計測時刻、Differ:経過時間、trigger:計測指示の要否(1=必要、0=不要)です。trigger以外の数値の単位は秒です。
これで、ブロードキャストされた情報と、このアドレス向けに送信された情報だけを受信できます。
この時、内蔵LEDを 0.1秒だけ点灯します。
#include "esp32_e220900t22s_jp_lib.h" #include <DHT.h> #define TERMINAL_NUMBER 1 //#define TERMINAL_NUMBER 2 #define LED_BUILTIN 2 // 内蔵LED #define SEND_DATA_SIZE 61 // 64-3 #define DHTTYPE DHT11 // DHTセンサーの型式 /* DHTセンサー*/ const int DHTPin = 14; // DHTセンサーの接続ピン DHT dht(DHTPin, DHTTYPE); // DHTクラスの実体 CLoRa lora; // LoRaクラスのインスタンス struct LoRaConfigItem_t config; // LoRaコンフィギュレーション struct RecvFrameE220900T22SJP_t data; // 受信用構造体 int replyCount = 0; // 返答(計測)回数 void setup() { pinMode(LED_BUILTIN, OUTPUT); SerialMon.begin(9600); dht.begin(); delay(5000); // 既定のLoRaコンフィギュレーションを取得して lora.SetDefaultConfigValue(config); // E220-900T22S(JP)を初期設定する config.own_address = TERMINAL_NUMBER; if (lora.InitLoRaModule(config)) { SerialMon.printf("LoRa init error\n"); return; } // ノーマルモードへ移行する SerialMon.printf("switch to normal mode\n"); lora.SwitchToNormalMode(); SerialMon.printf("Start terminal No.%d\n", TERMINAL_NUMBER); } void loop() { char msg[SEND_DATA_SIZE] = { 0 }; // 計測指示を受信すると if (lora.ReceiveFrame(&data) == 0) { // 温度と湿度を計測して編集し float t = dht.readTemperature(); float h = dht.readHumidity(); sprintf(msg, "%02d:%04d=%4.1f,%4.1f", TERMINAL_NUMBER, ++replyCount, t, h); SerialMon.printf("送信データ: %s\n", msg); // 計測データを送信する if (lora.SendFrame(config, (uint8_t *)msg, sizeof(msg)) == 0) { SerialMon.println("send succeeded.\n"); } else { SerialMon.println("send failed.\n"); } digitalWrite(LED_BUILTIN, HIGH); delay(100); digitalWrite(LED_BUILTIN, LOW); } delay(1); }
シリアルモニターに "Start sending message!"が表示されて少し待つと、計測端末1号機の LEDが光り、これに呼応してセンター側の LEDが光り、わずかに待って同様に計測端末2号機の LEDとセンター側のそれが光ります。
シリアルモニターには、計測時刻になると「Request to send message .... 」に続いてセンター側の起動からの時間が表示されます。続いて計測端末宛のメッセージが送信され、データを受信すると、「Receive data: 」に続いて1号機からの受信データ、さらに2号機からのデータが表示されます。

(現在のmillisの値 - 前回計測時のmillisの値)≧ 計測間隔?
このようにすることで、次のように一定間隔での計測指示が可能になります。ただし表示内容は、 Now:現在の時刻、Prev:前回の計測時刻、Differ:経過時間、trigger:計測指示の要否(1=必要、0=不要)です。trigger以外の数値の単位は秒です。
Now: 4294967289, Prev: 4294967280, Differ: 9, trigger:0 Now: 4294967290, Prev: 4294967290, Differ: 10, trigger:1 Now: 4294967291, Prev: 4294967290, Differ: 1, trigger:0 Now: 4294967292, Prev: 4294967290, Differ: 2, trigger:0 Now: 4294967293, Prev: 4294967290, Differ: 3, trigger:0 Now: 4294967294, Prev: 4294967290, Differ: 4, trigger:0 Now: 4294967295, Prev: 4294967290, Differ: 5, trigger:0 Now: 0, Prev: 4294967290, Differ: 6, trigger:0 Now: 1, Prev: 4294967290, Differ: 7, trigger:0 Now: 2, Prev: 4294967290, Differ: 8, trigger:0 Now: 3, Prev: 4294967290, Differ: 9, trigger:0 Now: 4, Prev: 4, Differ: 10, trigger:1 Now: 5, Prev: 4, Differ: 1, trigger:0 Now: 6, Prev: 4, Differ: 2, trigger:0 Now: 7, Prev: 4, Differ: 3, trigger:0 Now: 8, Prev: 4, Differ: 4, trigger:0 Now: 9, Prev: 4, Differ: 5, trigger:0 Now: 10, Prev: 4, Differ: 6, trigger:0 Now: 11, Prev: 4, Differ: 7, trigger:0 Now: 12, Prev: 4, Differ: 8, trigger:0 Now: 13, Prev: 4, Differ: 9, trigger:0 Now: 14, Prev: 14, Differ: 10, trigger:1 |
4.ブロードキャスト方式の実験
(1) センター側のタスク:loraCentral2B.ino
ブロードキャスト方式でのセンター側タスクは、計測時刻になるとすべての計測端末へいっせいに計測指示をブロードキャストします。計測端末は時間差を空けて計測データを送信してくるので、受信すると表示します。送信時には1秒間、受信時には0.2秒だけ内蔵LEDを点灯します。
送信と受信はそれぞれマルチタスクとして動作させます。
コードの解説
順次送受信方式のスケッチと類似部分が多いので、特に異なっている部分だけを解説します。
①初期化処理 setup部
・40行目: 全端末宛にブロードキャストするため、コンフィギュレーションの送信宛先に '0xffff'を設定します。
// E220-900T22S(JP)を初期設定する
config.target_address = 0xffff;
if (lora.InitLoRaModule(config)) {
SerialMon.printf("LoRa init error\n");
return;
}
②反復処理 loop部
③受信タスク LoRaRecvTask部
どちらも順次送受信方式のセンター側タスク(loraCentral2A.ino)と同じです。
④送信タスク LoRaSendTask部
送信要求フラグがセットされていると、まずそれをリセットして計測メッセージを送信します。
これで全端末へいっせいに送信されます。
/*
送信タスク
*/
void LoRaSendTask(void *pvParameters) {
char msg[SEND_DATA_SIZE] = { 0 };
while (1) {
if (doSendSignal) { // 送信要求があると送信処理を行う
doSendSignal = false;
sprintf(msg, "Go ahead! (%d)", ++sendCtr);
if (lora.SendFrame(config, (uint8_t *)msg, sizeof(msg)) == 0) {
SerialMon.println("send succeeded.");
} else {
SerialMon.println("send failed.");
}
digitalWrite(LED_BUILTIN, HIGH);
delay(1000);
digitalWrite(LED_BUILTIN, LOW);
}
delay(1);
}
}
(2) 計測用端末側のタスク:loraTerminal2B.ino
コードの解説
これもほとんど順次送受信方式(loraTerminal2A.ino)と同じなので、異なる部分だけを解説します。
①ヘッダーファイルとデータ等の定義
・16~17行: スケッチ書き込み前に、各端末ごとに重複しない識別番号(1,2,3,・・・・)を設定します。
#define TERMINAL_NUMBER 1
//#define TERMINAL_NUMBER 2
②初期化処理 setup部
・50行目: 端末識別番号を基準にして計測開始までの待ち時間(単位:秒)を計算します。
// 返信待ち時間を計算して
waitTime = (TERMINAL_NUMBER - 1) * TIME_TO_REPLY * 1000;
// ノーマルモードへ移行する
③反復処理 loop部
・61行目: 計測指示を受信すると以下を行います。
・64行目: 計測を行う前に「返信待ち時間」だけ処理を待たせます。
・74行目: 計測データを送信します。
char msg[SEND_DATA_SIZE] = { 0 };
// 計測指示を受信すると
if (lora.ReceiveFrame(&data) == 0) {
// 返信待ち時間まで待って
Serial.printf("Wait time = %d\n", waitTime);
delay(waitTime);
Serial.println("--------------");
// 温度と湿度を計測して編集し
float t = dht.readTemperature();
float h = dht.readHumidity();
sprintf(msg, "%02d:%04d=%4.1f,%4.1f", TERMINAL_NUMBER, ++replyCount, t, h);
SerialMon.printf("送信データ: %s\n", msg);
// 計測データを送信する
if (lora.SendFrame(config, (uint8_t *)msg, sizeof(msg)) == 0) {
SerialMon.println("send succeeded.\n");
} else {
(3) 送受信の実験
まず、loraTerminal.inoを書き込んだ 2台の計測用端末の電源を入れます。続いてセンター側の ESP32をパソコンに USB接続してArduino IDEを開き、loraCentral2B.inoを書き込みます。
シリアルモニターに "Start broadcasting!"が表示されて、少し待つと "> Request to send message .... "に続いて送信時刻が表示されます。すぐに、端末番号 1の LEDが光り、わずかに遅れて 2号機の LEDが光ります。それぞれの LED点灯に呼応してセンター側の LEDが光り、シリアルモニターに受信データが表示されます。
③受信タスク LoRaRecvTask部
これで全端末へいっせいに送信されます。
/* 送信タスク */ void LoRaSendTask(void *pvParameters) { char msg[SEND_DATA_SIZE] = { 0 }; while (1) { if (doSendSignal) { // 送信要求があると送信処理を行う doSendSignal = false; sprintf(msg, "Go ahead! (%d)", ++sendCtr); if (lora.SendFrame(config, (uint8_t *)msg, sizeof(msg)) == 0) { SerialMon.println("send succeeded."); } else { SerialMon.println("send failed."); } digitalWrite(LED_BUILTIN, HIGH); delay(1000); digitalWrite(LED_BUILTIN, LOW); } delay(1); } }
これもほとんど順次送受信方式(loraTerminal2A.ino)と同じなので、異なる部分だけを解説します。
①ヘッダーファイルとデータ等の定義
#define TERMINAL_NUMBER 1 //#define TERMINAL_NUMBER 2
②初期化処理 setup部
・50行目: 端末識別番号を基準にして計測開始までの待ち時間(単位:秒)を計算します。
// 返信待ち時間を計算して
waitTime = (TERMINAL_NUMBER - 1) * TIME_TO_REPLY * 1000;
// ノーマルモードへ移行する
③反復処理 loop部
・61行目: 計測指示を受信すると以下を行います。
・64行目: 計測を行う前に「返信待ち時間」だけ処理を待たせます。
・74行目: 計測データを送信します。
char msg[SEND_DATA_SIZE] = { 0 };
// 計測指示を受信すると
if (lora.ReceiveFrame(&data) == 0) {
// 返信待ち時間まで待って
Serial.printf("Wait time = %d\n", waitTime);
delay(waitTime);
Serial.println("--------------");
// 温度と湿度を計測して編集し
float t = dht.readTemperature();
float h = dht.readHumidity();
sprintf(msg, "%02d:%04d=%4.1f,%4.1f", TERMINAL_NUMBER, ++replyCount, t, h);
SerialMon.printf("送信データ: %s\n", msg);
// 計測データを送信する
if (lora.SendFrame(config, (uint8_t *)msg, sizeof(msg)) == 0) {
SerialMon.println("send succeeded.\n");
} else {
(3) 送受信の実験
まず、loraTerminal.inoを書き込んだ 2台の計測用端末の電源を入れます。続いてセンター側の ESP32をパソコンに USB接続してArduino IDEを開き、loraCentral2B.inoを書き込みます。
シリアルモニターに "Start broadcasting!"が表示されて、少し待つと "> Request to send message .... "に続いて送信時刻が表示されます。すぐに、端末番号 1の LEDが光り、わずかに遅れて 2号機の LEDが光ります。それぞれの LED点灯に呼応してセンター側の LEDが光り、シリアルモニターに受信データが表示されます。
・64行目: 計測を行う前に「返信待ち時間」だけ処理を待たせます。
・74行目: 計測データを送信します。
char msg[SEND_DATA_SIZE] = { 0 }; // 計測指示を受信すると if (lora.ReceiveFrame(&data) == 0) { // 返信待ち時間まで待って Serial.printf("Wait time = %d\n", waitTime); delay(waitTime); Serial.println("--------------"); // 温度と湿度を計測して編集し float t = dht.readTemperature(); float h = dht.readHumidity(); sprintf(msg, "%02d:%04d=%4.1f,%4.1f", TERMINAL_NUMBER, ++replyCount, t, h); SerialMon.printf("送信データ: %s\n", msg); // 計測データを送信する if (lora.SendFrame(config, (uint8_t *)msg, sizeof(msg)) == 0) { SerialMon.println("send succeeded.\n"); } else {
(3) 送受信の実験
まず、loraTerminal.inoを書き込んだ 2台の計測用端末の電源を入れます。続いてセンター側の ESP32をパソコンに USB接続してArduino IDEを開き、loraCentral2B.inoを書き込みます。
シリアルモニターに "Start broadcasting!"が表示されて、少し待つと "> Request to send message .... "に続いて送信時刻が表示されます。すぐに、端末番号 1の LEDが光り、わずかに遅れて 2号機の LEDが光ります。それぞれの LED点灯に呼応してセンター側の LEDが光り、シリアルモニターに受信データが表示されます。

※実験の評価
順次送受信方式とブロードキャスト方式のどちらも、同じように複数の計測端末から計測データを受信できていることがわかります。しかし一点だけ大きな違いがあります。順次送信方式のそれぞれの計測端末は、計測指示を受けて直ちに計測して結果を送信します。これに対してブロードキャスト方式では、計測端末側が端末番号に応じて返信待ちをすることになります。今後のディープスリープの利用を考えると、端末識別番号の大きいものほど待ち時間が長くなり、消費電力が大きくなってしまいます。このため、省エネ対応の計測端末を考える上では、各端末の待ち時間にバラツキがない「順次送受信方式」の方が好ましいと言えます。
次回は、ディープスリープと WOR(Wake On Radio)通信を使って省電力LoRa端末を開発します。これはLoRa計測端末の最終的な完成形になる予定です。ただ、現時点ではディープスリープの制御が思うようにできず、実験に手間取っています。できるだけ早期に完成させるべく頑張ります。
お楽しみに!