はじめに
水位を測りたくてM5atomS3-liteを試しましたが無線LANでは数十メートルしか電波が届きません。そこでLorawanのGatewayを用意してエンドノードにA660-900T22-EV61を使ってみました。
ハードウェア
制御マイコン M5atomS3-lite(スイッチサイエンス)
Lorawanモジュール A660-900T22-EV61(スイッチサイエンス)
超音波距離センサPWM A0221AM(Amazon)
事前準備
The things network(TTN)のアカウントを作ってGatewayとエンドノードを登録してください。
設定したDEVEUI、APPEUI、APPKEYをメモしておいてエンドノードに設定します。
ソースコード
ソースコードの共通化のため、エンドノードごとの個別設定は本体ボタンを押しながら電源ONして手動で入力する。
現時点では距離センサとLorawanの機能確認のみ。機械的な取り付け位置が決まってないので、距離のオフセットを引くのは後で実装する。
// Hardware M5AtomS3 lite , A0221AM (Ultrasonic sensor Pin assign modified), LoraWan module A660-900T22-EV61
// M5AtomS3 lite G1 --> A0221AM Pin 1 Yellow(modified)
// M5AtomS3 lite G2 --> A0221AM Pin 2 White(modified)
// M5AtomS3 lite 5V --> A0221AM Pin 3 Red(modified)
// M5AtomS3 lite GND --> A0221AM Pin 4 Black(modified)
#include <M5Unified.h>
#include <FastLED.h>
#include "HardwareSerial.h"
#include <CayenneLPP.h>
HardwareSerial mySerial(1); // UART1 を使用
// AtomS3 LiteのGPIOの定義
#define TRIGGER_PIN 1 // 超音波距離センサー トリガー信号=G1
#define PWM_OUTPUT_PIN 2 // 超音波距離センサー 距離パルス=G2
#define SERIAL_RX_PIN 38 // AtomS3 -> A660-900T22 ATコマンド送信=G38
#define SERIAL_TX_PIN 39 // A660-900T22 -> AtomS3 レスポンス受信=G39
// FastLEDライブラリでフルカラーLEDを使用するための設定
#define NUM_LEDS 1
#define LED_DATA_PIN 35
CRGB leds[NUM_LEDS];
// 計測(送信)間隔
#define SLEEP_MINUTES 20 // 計測間隔(分単位)= deep sleepする時間 Fair Access Policy
// 水位を測定 modified by Google Gemini
float getWaterLevel() {
long duration;
float distance_cm;
// 1. トリガ信号の送出 (Falling Edge)
digitalWrite(TRIGGER_PIN, HIGH);
delayMicroseconds(10); // 安定待ち
digitalWrite(TRIGGER_PIN, LOW); // 立ち下がり
delayMicroseconds(100); // 100us程度維持
digitalWrite(TRIGGER_PIN, HIGH); // HIGHに戻す
// 2. パルス幅の測定
// タイムアウトを 100ms (100,000us) に設定(未検出時の35msをカバー)
duration = pulseIn(PWM_OUTPUT_PIN, HIGH, 100000);
if (duration == 0) {
return -30.0; // 測定失敗時は-30cm
}
// 3. 仕様書の計算式に基づき算出 (S = T / 57.5)
// 結果は cm 単位
distance_cm = (float)duration / 57.5;
return distance_cm;
}
// CayenneLPPのバッファを16進数のStringに変換する関数 Copilot programed
String getLPPString(CayenneLPP& lpp) {
String hexStr = "";
uint8_t* buffer = lpp.getBuffer();
uint8_t size = lpp.getSize();
for (uint8_t i = 0; i < size; i++) {
if (buffer[i] < 0x10) hexStr += "0"; // 1桁の場合は0を埋める
hexStr += String(buffer[i], HEX);
}
hexStr.toUpperCase(); // 大文字に揃える(任意)
return hexStr;
}
// ATコマンド送信関数 Google Gemini programed
String sendATCommand(String command) {
String response = "";
// 送信前に受信バッファに残っているゴミを掃除する
while(mySerial.available()) mySerial.read();
Serial.print(">> "); Serial.println(command);
mySerial.println(command);
// AT+DJOINが含まれる場合は応答待ちを30秒、それ以外は5秒に設定
unsigned long timeoutLimit = 5000;
if (command.indexOf("AT+DJOIN") >= 0) {
timeoutLimit = 30000;
}
// 応答待ち
unsigned long start = millis();
while (millis() - start < timeoutLimit) {
while (mySerial.available()) {
response += (char)mySerial.read();
// 連続したデータの取りこぼしを防ぐためのわずかな待ち時間
delay(2);
}
// 判定条件:OK/ERRORに加え、ステータス応答があればループを抜ける
if (response.indexOf("OK") >= 0 ||
response.indexOf("ERROR") >= 0 ||
response.indexOf("OK+SEND:") >= 0 || // 送信成功時のレスポンス
response.indexOf("+DULSTAT:") >= 0 ||
response.indexOf("+DJOIN:") >= 0) {
break;
}
}
Serial.print("<< "); Serial.println(response);
return response;
}
// ------------------------------------------------------------
// Setup 関数 Setup function.
// ------------------------------------------------------------
void setup(){
// M5AtomS3 lite setup
auto cfg = M5.config(); // 設定用の構造体を代入。
cfg.serial_baudrate = 9600; // USB-C通信ボーレート9600bps
cfg.output_power = true;
M5.begin(cfg); // 本体初期設定
// ハードウェアシリアルを UART1 に割り当て、通信速度 9600bps で初期化
mySerial.begin(9600, SERIAL_8N1, SERIAL_RX_PIN, SERIAL_TX_PIN);
// ピンの初期設定
pinMode(TRIGGER_PIN, OUTPUT);
pinMode(PWM_OUTPUT_PIN, INPUT);
// LED string setup
FastLED.addLeds<WS2811, LED_DATA_PIN, GRB>(leds, NUM_LEDS); //フルカラーLED初期設定
FastLED.setBrightness(10); //フルカラーLED明るさMAX20までが限界
Serial.println("AtomS3 Lite wakeup!"); // シリアルモニタ出力
// Push A button at boot time to A660-900T22 manual configuration
bool doManualConfig = false;
Serial.println("Push A button to enter A660-900T22 manual configuration");
for(int i=0 ; i<60 ; i++) {
M5.update();
if (M5.BtnA.isPressed()) {
doManualConfig = true;
break;
}
delay(10);
}
// 本体のボタンを押してATコマンドでA660-900T22の初期設定をする
// PC -> AtomS3 lite -> A660-900T22 シリアル通信をパススルー
// AT+CSAVEで設定を保存して電源を切る。
// 本体のボタンを押さずに電源をONすると通常動作。
if (doManualConfig) {
leds[0] = CRGB::Yellow; //フルカラーLED黄色設定
FastLED.show(); //フルカラーLED表示
Serial.println("Usage : A660-900T22初期設定");
Serial.println("Usage : ATコマンドでLoraWanの通信パラメータを設定してください");
Serial.println("Usage : ATコマンドは大文字小文字を区別します。小文字だとエラーになります。");
Serial.println("Usage : 最後にAT+CSAVEでパラメータを保存して電源をOFFのこと");
Serial.println("Usage : AT+CDEVEUI=<value> →A600-T920に付属の紙に書かれたDevEUI 16桁");
Serial.println("Usage : AT+CAPPEUI=<value>");
Serial.println("Usage : AT+CAPPKEY=<value>");
Serial.println("Usage : AT+RREGION=2 →バンドプランをAS923-1-JPに設定");
Serial.println("Usage : AT+CSAVE");
return; //loop()に移動してコマンド待ち
}
// 測定開始
leds[0] = CRGB::Green; //フルカラーLED緑色設定
FastLED.show(); //フルカラーLED表示
// 水位を読む Read ultrasonic distance renger
float WaterLevel=getWaterLevel(); // 水位読み込み単位cm
Serial.printf("水位=%2.1f cm\n",WaterLevel); // シリアルモニターに水位を出力、改行
delay(200);
// 水温を読む
// 温度センサは取り付けていないのでここはパスする
//送信バッファにCayenneLPPのPayloadを積む
CayenneLPP lpp(51);
lpp.reset();
lpp.addAnalogInput(1, WaterLevel); // Cayenne LPP CH1 水位データを追加 単位cm
lpp.addTemperature(2, 0); // Cayenne LPP CH2 温度データを追加0℃ この行はダミーデータで将来の拡張用
// Payloadをテキストとして取得、送信
uint8_t payloadlength = lpp.getSize(); //payloadサイズ
String payloadText = getLPPString(lpp); //payloadの16進数テキスト
Serial.print("Payload (Hex Text): ");
Serial.println(payloadText); // 例: "016700FF"
// The Things Networks(TTN)接続開始
leds[0] = CRGB::Red; //フルカラーLED赤色設定
FastLED.show(); //フルカラーLED表示
// A660-T920のスリープ解除にダミーでATを送信
mySerial.println("AT");
delay(200);
// sendATCommand("AT");
// 1. Join状態の確認 modified by Google Gemini
String status = sendATCommand("AT+DULSTAT?");
// 接続状況のチェック (+DULSTAT:01-08なら接続済み、00なら未接続)
// statusの中に ":00" が含まれているか、または正常な応答がない場合を「未接続」と判定
if (status.indexOf(":00") >= 0 || status == "") {
Serial.println("Not Joined. Initializing and Joining...");
// Joinリクエスト送信
sendATCommand("AT+DJOIN=1,0,8,2");
// Join完了を待つ(OTAAは時間がかかるため長めに待機)
Serial.println("Waiting for Join process...");
delay(10000);
} else {
// 01〜08のいずれかであればこちらを通る
Serial.print("Already Joined. Current Status: ");
Serial.println(status);
Serial.println("Proceeding to send data.");
}
// 2. データ送信
// 確認型送信(1), 再送3回, 8バイト
String sendRes = sendATCommand("AT+DTRX=1,3,8,"+payloadText);
if (sendRes.indexOf("OK+SEND") >= 0) {
Serial.println("Data sent successfully.");
} else {
Serial.println("Send failed or timeout.");
}
// 3. Deep Sleep設定
// LEDを消す
FastLED.clear(); // 全LEDのデータを黒(消灯)にする
FastLED.show(); //フルカラーLED表示
// Deep sleep GO!
Serial.printf("Entering Deep Sleep for %d minutes...\n", SLEEP_MINUTES);
delay(400);
esp_sleep_enable_timer_wakeup(SLEEP_MINUTES*60*1000*1000);
esp_deep_sleep_start();
}
void loop(){
// UART1 で受信して USB-C(UART0) に表示
if (mySerial.available()) {
String received = mySerial.readStringUntil('\n');
Serial.println("A660-900T22 : " + received);
}
// USB-C からの入力を UART1 へ送信
if (Serial.available()) {
String outgoing = Serial.readStringUntil('\n');
mySerial.println(outgoing);
Serial.println("COMMAND : " + outgoing);
}
}
ここで困った
プログラミングの能力不足
無料のGoogle GeminiとMicrosoft Copilotに自作ソースコードと仕様書を渡して添削してもらいました。生成AIが無かったら動かせなかったという自信があります(^^)。別に検討したE220-900T22S(JP)は安価ですがエンドノードやらサーバー側も自力で構築できる上級者向けorz。初心者は業界標準(TTN)を使いましょう。
仕様書の誤記でGatewayに繋がらない
Ver1.0 AT+RREGION=3 AS923-1-JP
Ver1.1 AT+RREGION=2 AS923-1-JP
無線部の仕様が違ったらそら繋がらないわ...。仕様書に更新リストを付けて欲しい。
仕様書通りに無線出力が設定できない
AT+CTXP=13でエラー。送信出力が日本で許可された最大出力の13dBmに設定できない。テスト用でGatewayからは至近距離というこで今回の使い方は許容しました。ファームのバージョンが古いせい?最新ファームは公開されているが書き換えツールを用意するのが大変なのでパス。
超音波距離センサの仕様がわからない
Amazonの販売ページは仕様書へのリンクなし、判るのはPWM仕様ということだけ。届いた現物を見て、4本の配線があるから、どれかが電源、GND、Trigger、Echoだよねで推定。よく動いたもんだ。コネクタのピンを挿し替えてGroveポートの配列に合わせてあります。
A660-900T22-EV61のAT+DULSTAT?コマンドの返り値の解釈
+DULSTAT:00がJoinしてないが正解でした。Geminiに仕様書をアップロードして質問。仕様を把握した上で、自作ソースの修正を指示。吐き出されたコードを読んで、適正なら本番ソースに挿入。なるほど生成AIはこうやって使うのか。
これからすること
ケースに入れて屋外で耐久試験をします。超音波距離センサの防水仕様IP67は水深1mで30分の防水性能。経験上、屋根は必要です。
M5atomS3-liteだけだとニッケル水素電池4本で2ヵ月動きました。Lorawanモジュールの消費電力が増えたらどれくらい動くか確認します。
まだ実戦投入まで先は長い。