【SALZmini2022作製日誌】ステートマシン図で次の一手が見えました。
はじめに
前回記事
の続きをやっていきます。
前回記事の締めくくり
まずはSALZmini2022とSMTesterについて注目します。
そもそも、SMTesterはSALZmini2022からWateringUnitを制御するためのコードをコメントアウトして作りました。
SMTesterが正常動作して、SALZmini2022が動かないということは、この2つのソースの違いのどこかに問題があると推測されます。
単純に考えると、degitalRead()、degitalWrite()の有無で挙動が変わるように感じますが、そんなことがあるのでしょうか?
ここで、あれこれいじるのは多分得策でないと感じ、まず、最初に確認したのは、SALZmini2022とSMTesterを入れ替えるということです。
ところが実はこのテストは実施していません
なんということでしょうか。不思議なことに動作が安定し始めました。
SMTesterが安定稼働しているのはもちろんのこと、SALZmini2022もBLEの電波が飛び続けています。
不安定なのは、SMGatewayだけになっているようです。
SALZmini2022について、何か変えたところと言えば。。。
筐体を変えました。
少し大きなサイズでぐすぐすなのですが、ケーブルやコネクタは
ストレスがなくなりました。
接触不良の問題をもう少し突き詰めたいとは思っているのですが、うまくいっているのであればそれでよし。
次に考えておきたい問題があります。
ステートマシン(状態遷移)を作る
SALZmini2022のソースを見ていて、一番気に入らない所はloop()関数です。
寄せ集めのつくりで眺めていても、何がしたいのか頭に入ってきません。
void
loop(void) {
salzData.currentWaterLevel = GetCurrentWaterLevel();
if (salzData.currentWaterLevel < salzData.wateringLevel || command == cmdWaterNow) {
Serial.println("水やり");
command = cmdNOP;
digitalWrite(PUMP_PIN, true);
Serial.println("PUMP ON");
salzData.wCount = (++salzData.wCount < UINT16_MAX) ? salzData.wCount : 0;
delay(SECONDS(salzData.pumpTime));
digitalWrite(PUMP_PIN, false);
Serial.println("PUMP OFF");
salzData.status = stWatering;
}
else if (salzData.currentWaterLevel < salzData.yellowLevel) {
Serial.println("赤色待機");
salzData.status = stWaitRed;
}
else if (salzData.currentWaterLevel < salzData.blueLevel) {
Serial.println("黄色待機");
salzData.status = stWaitYellow;
}
else {
Serial.println("水色待機");
salzData.status = stWaitBlue;
}
SendData();
delay(SECONDS(salzData.waitTime));
// notify changed value
if (deviceConnected) {
Serial.println("deviceConnected");
pCharacteristic->setValue((uint8_t*)&salzData, sizeof(SALZData));
Serial.println("pCharacteristic->setValue();");
pCharacteristic->notify();
Serial.println("pCharacteristic->notify();");
delay(3); // bluetooth stack will go into congestion, if too many packets are sent, in 6 hours test i was able to go as low as 3ms
}
// disconnecting
if (!deviceConnected && oldDeviceConnected) {
Serial.println("!deviceConnected && oldDeviceConnected");
oldDeviceConnected = deviceConnected;
}
// connecting
if (deviceConnected && !oldDeviceConnected) {
Serial.println("deviceConnected && !oldDeviceConnected");
oldDeviceConnected = deviceConnected;
}
}
このようになっています。
void
loop(void) {
水分量%を取得する。
if (水分量%が水やりレベルより低い か 今すぐ水やりコマンドが投入されている) {
ポンプをオンにする。
ポンプ作動秒まつ。
ポンプをオフにする。
statusを「stWatering」にする。
}
else if (水分量%が黄色レベル設定より小さければ) {
statusを「stWaitRed」にする。
}
else if (水分量%が青色レベル設定より小さければ) {
statusを「stWaitYellow」にする。
}
else {
statusを「stWaitBlue」にする。
}
現在のデータを送信する。
待ち秒まつ
if (デバイスが接続されている) {
salzDataをnotify()する。
delay(3);
}
if (デバイスが接続されていない && 前回デバイスが接続されている) {
oldDeviceConnected = deviceConnected;
}
if (デバイスが接続されている && 前回デバイスが接続されていない) {
oldDeviceConnected = deviceConnected;
}
}
ざっくり翻訳してみるとこんな感じ。
最後のあたりはまとめられそうな感じ。
どんな動作をさせたいか?
整理してみます。
ずいぶん昔、仕事でコンパイラを作った時、状態遷移図を書き、ステートマシンを作って実装を行いました。
あれから20年以上経っています。今どきのソフトウェア設計はさぞかし進化しているのではないか?そんな興味が湧いてきました。
「ステートマシン図」で検索すると、いくつか情報が出てきます。
「UML」という言葉に出会いました。
私は今まで受注システムや下請け業務に携わっていないので、このような規格化が行われていることを全く知りませんでした。
どこまでできるかわかりませんが一度チャレンジしてみたいと思います。
概要を勉強しました。
「ステートマシン図」が今作ってみたいものです。
似て非なるものがたくさんあり、抽象的な概念で括られたものが多く、難しいと感じました。
INPUTと現在のSTATEから次回のSTATE、ACTIONを求めます。
INPUT:
青色状態、
黄色状態、
赤色状態、
水やり、
WL変更、
YL変更、
BL変更、
PT変更、
WT変更、
データ送信、
接続要求、
切断要求、
エラー発生
STATE:
エラー、
青待機、
黄待機、
赤待機、
水やり開始、
WL変更、
YL変更、
BL変更、
PT変更、
WT変更、
水やり中、
接続、
切断、
データ送信
ACTION:
PUMP_ON、
PUMP_OFF、
データ送信、
WL変更、
YL変更、
BL変更、
PT変更、
WT変更、
接続、
切断、
エラー処理、
何もしない
というオートマトン(自動機械)を作ることになります。
typedef enum {
IN_BLU, // 青色状態
IN_YLW, // 黄色状態
IN_RED, // 赤色状態
IN_WTN, // 水やり
IN_WTE, // 水やり終了
IN_CNT, // 接続要求
IN_DCN, // 切断要求
IN_WLS, // WL変更
IN_YLS, // YL変更
IN_BLS, // BL変更
IN_PTS, // PT変更
IN_WTS, // WT変更
IN_DTS, // データ送信
IN_ERR, // エラー発生
NINPUTS
} INPUT;
typeder enum {
ST_BLU, // 青待機
ST_YLW, // 黄待機
ST_RED, // 赤待機
ST_WTI, // 水やり中
ST_ERR, // エラー
NSTATUS
} STATUS;
typeder enum {
AC_PON, // PUMP_ON
AC_POF, // PUMP_OFF
AC_DTS, // データ送信
AC_WLS, // WL変更
AC_YLS, // YL変更
AC_BLS, // BL変更
AC_PTS, // PT変更
AC_WTS, // WT変更
AC_CNT, // 接続
AC_DCN, // 切断
AC_ERR, // エラー処理
AC_NOP, // 何もしない
NACTIONS
} ACTION;
とここまで考えて、ふと我に返りました。
INPUTについて考えるとき、水分量%は値を取りに行っていますが、コマンド指令についてはコールバック関数で呼ばれていますので、裏で勝手に値が書き換わっているような状況です。
ということは、このような一元管理的な、ステートマシン図を想定すること自体が無意味です。
というより、今の状況を正しく図式化できないと理解が進まないということもわかってきました。
と思いながら、図式化しようとよくソースを眺めてみると。。。
複数のサンプルを寄せ集めていたので複雑になっていましたが、ソースを眺めていると何となく読めてきました。
気に入らない所もだいぶわかってきました。
void
loop(void) {
水分量%を取得する。
if (水分量%が水やりレベルより低い か 今すぐ水やりコマンドが投入されている) {
ポンプをオンにする。
ポンプ作動秒まつ。
ポンプをオフにする。
statusを「stWatering」にする。
}
else if (水分量%が黄色レベル設定より小さければ) {
statusを「stWaitRed」にする。
}
else if (水分量%が青色レベル設定より小さければ) {
statusを「stWaitYellow」にする。
}
else {
statusを「stWaitBlue」にする。
}
現在のデータを送信する。
待ち秒まつ
if (デバイスが接続されている) {
salzDataをnotify()する。
delay(3);
}
if (デバイスが接続されていない && 前回デバイスが接続されている) {
oldDeviceConnected = deviceConnected;
}
if (デバイスが接続されている && 前回デバイスが接続されていない) {
oldDeviceConnected = deviceConnected;
}
}
loop()関数の中でdelayが入っています。
これが、条件によって変わっている点に問題を感じます。
loop()関数の中では、水やりのタイマーや、データ送信の間隔調整など、複数のタイミングを調整しています。
例えば、データ送信を20分間隔で行いたいと思っていても、途中で水やりが複数回実行されるとその時間はどんどん伸びて行ってしまいます。
void
loop(void) {
水分量%を取得する。
if (水分量%が水やりレベルより低い か 今すぐ水やりコマンドが投入されている) {
ポンプをオンにする。
ポンプ作動秒まつ。
ポンプをオフにする。
statusを「stWatering」にする。
}
else if (水分量%が黄色レベル設定より小さければ) {
statusを「stWaitRed」にする。
}
else if (水分量%が青色レベル設定より小さければ) {
statusを「stWaitYellow」にする。
}
else {
statusを「stWaitBlue」にする。
}
現在のデータを送信する。
待ち秒まつ
if (デバイスが接続されている) {
salzDataをnotify()する。
delay(3);
}
}
これを解消するためには、delay()関数を使わず、実行中のプログラムがスタートしてからの時間をカウントするmillis()関数を利用して動作させる仕掛けを取りこむように考えます。
ただそのとき、漠然と考えていたのが、カウンタの回り込みの問題です。
unsigned long
DiffTime(unsigned long t1, unsigned long t2)
{
if (t1 - t2 < 0)
return t1 + (ULONG_MAX - t2);
else
return t1 - t2;
}
// 待ち秒経っていたら、データ送信する。
if (DiffTime(millis(), waitTimeMillis) > SECONDS(salzData.waitTime)) {
Serial.print("W\n");
// 水分量%を取得する。
salzData.currentWaterLevel = GetCurrentWaterLevel();
SendData();
waitTimeMillis = millis();
}
ということで、その後、loop()関数は下記のように書き改めました。
void
loop(void) {
// 待ち秒経っていたら、データ送信する。
if (TimeDiff(millis(), waitTimeMillis) > SECONDS(salzData.waitTime)) {
Serial.print("W\n");
// 水分量%を取得する。
salzData.currentWaterLevel = GetCurrentWaterLevel();
SendData();
waitTimeMillis = millis();
}
if (salzData.currentWaterLevel < salzData.wateringLevel || bForceWater) {
Serial.println("\n水やり");
bForceWater = false;
digitalWrite(PUMP_PIN, true);
Serial.println("PUMP ON");
salzData.wCount = (++salzData.wCount < ULONG_MAX) ? salzData.wCount : 0;
delay(SECONDS(salzData.pumpTime));
digitalWrite(PUMP_PIN, false);
Serial.println("PUMP OFF");
}
else if (salzData.currentWaterLevel < salzData.yellowLevel) {
Serial.print("R");
salzData.status = stWaitRed;
}
else if (salzData.currentWaterLevel < salzData.blueLevel) {
Serial.print("Y");
salzData.status = stWaitYellow;
}
else {
Serial.print("B");
salzData.status = stWaitBlue;
}
// notify changed value
if (deviceConnected && TimeDiff(millis(), notifyTimeMillis) > SECONDS(2)) {
Serial.println("\ndeviceConnected");
pCharacteristic->setValue((uint8_t*)&salzData, sizeof(SALZData));
Serial.println("pCharacteristic->setValue();");
pCharacteristic->notify();
Serial.println("pCharacteristic->notify();");
notifyTimeMillis = millis();
}
}
結局、ポンプのON/OFFのためのdelayはなくせませんでした。
さてここから本腰を入れて取り組みます。
本気でこの問題に取り組み、解決してみたいと思います。
とはいっても、ステート図どこから始めてよいかわかりません。
Figmaを使い、付箋メモの要領で気になる項目を色分けしながらピックアップしました。
そこから、項目を精査し、状態遷移表を整理しました。
本来であれば、ここでステート図(状態遷移図)を書き、そこから、状態遷移表へと移っていくようなのですが、私のイメージにしっくりくる書き方が見つからなかったため、状態遷移表からスタートしました。
行は前回の状態、列はイベントで表を作ります。
各項目は、アクションと次の状態を記入します。
何もしない、前回と変わらない項目は「-」と記入しています。
お?何とか形になったようです。
この表を眺めることで、頭の中が整理され、装置の動きが分かるようになってきます。
矛盾点や考え落ちを確認した所で、次のステップへ進みます。
typedef enum {
IN_WLGT, // 水分量%を調べる時間になった
IN_WTNW, // 今すぐ水やりコマンド
IN_POFF, // ポンプOFFタイマーの時間になった
IN_CNCT, // 接続中になった
IN_DCNT, // 接続が解除された
IN_ADDS, // Advartiseデータ送信の時間になった
IN_NTDS, // notifyデータ送信の時間になった
IN_NOOP, // 何もない
NINPUTS
};
typedef enum {
AC_WLGT, // 水分量%を調べる。タイマーリセット
AC_PPON, // ポンプON。 ポンプOFFタイマーセット
AC_NPTR, // 何もしない。 ポンプOFFタイマーリセット
AC_POFF, // ポンプOFF
AC_AVDS, // Advartiseデータ送信。ADタイマーリセット
AC_NTDS, // notifyデータ送信。notifyタイマーリセット
AC_NOOP, // 何もしない。
NACTIONS
};
typedef enum {
ST_WAIT, // 待機
ST_WTNG, // 水やり中
ST_CTWT, // 接続中で待機
ST_CTWG, // 接続中で水やり中
NSTATUS
};
static int action[NSTATUS][NINPUTS] = {
{ AC_WLGT, AC_PPON, AC_NOOP, AC_NOOP, AC_NOOP, AC_AVDS, AC_NTDS, AC_NOOP },
{ AC_WLGT, AC_NPTR, AC_POFF, AC_NOOP, AC_NOOP, AC_AVDS, AC_NTDS, AC_NOOP },
{ AC_WLGT, AC_PPON, AC_NOOP, AC_NOOP, AC_NOOP, AC_AVDS, AC_NTDS, AC_NOOP },
{ AC_WLGT, AC_NPTR, AC_POFF, AC_NOOP, AC_NOOP, AC_AVDS, AC_NTDS, AC_NOOP }
};
static int next_status[NSTATUS][NINPUTS] = {
{ ST_WAIT, ST_WTNG, ST_WAIT, ST_CTWT, ST_WAIT, ST_WAIT, ST_WAIT, ST_WAIT },
{ ST_WTNG, ST_WTNG, ST_WAIT, ST_CTWG, ST_WTNG, ST_WTNG, ST_WTNG, ST_WTNG },
{ ST_CTWT, ST_CTWG, ST_CTWT, ST_CTWT, ST_WAIT, ST_CTWT, ST_CTWT, ST_CTWT },
{ ST_CTWG, ST_CTWG, ST_CTWT, ST_CTWG, ST_WTNG, ST_CTWG, ST_CTWG, ST_CTWG }
};
あとは、loop()関数を書き換えるのみです。
void
loop(void) {
static int status = ST_WAIT;
int input = GetInput(); // 入力を決定します。
switch (action[status][input]) { // status と input から次の action を決定します。
case AC_XXXX: // 各アクションに対する処理
break;
:
}
status = next_status[status][input]; // status と input から次の staus を決定します。
}
ステートマシンの良い所は、一度この仕組みを組み上げれば、あとは状態遷移表とアクションの処理を修正するだけでよい所です。私は先読みが苦手なので、複雑な条件処理が必要になると、すぐにステートマシンに走ってしまいます。
ただ久しぶりに作ったので、項目の洗い出しに時間が掛かりました。
実際にテストしてみたところ、だいたい思った通りの動きをしているようです。
プログラムを置き換えてしばらく使ってみることにします。
次の課題はグラフの扱いです。
SMGatewayはあくまでもデバッグ用のツールとしてとらえています。
最終的には、きちんとエラーチェックを行いクオリティを高めていきたいと思いますが、しばらくは塩漬けでもよいかなと考えています。
今回のSALZmini2022のゴールとしては、SALZmini2022とBLEレシーバーのBOWLで、必要な情報のやり取りができるようにしたいと思っています。
そこで、次なる課題となるのが過去のデータを蓄積しておき、リクエストがあれば、一括送信する仕組みを盛り込みたいと思います。
イメージは既にあります。
作りたいのはリアルタイムグラフではないのですが、こんな感じです。
実現するためには、
SALZmini2022で時系列データを保持する方法
リクエストに応じて時系列データを送信する方法
BOWLからリクエストをし、時系列データを受信、グラフ化する方法
が必要と思われます。
サンプルを探しました。
ネット上では探すことができず、迷っていた所、ArduinoIDEのサンプルでBLE_UARTを見つけました。
あれこれ悩みながら、なんとなく形を作ることができました。
SALZmini2022で時系列データを保持する方法
まず、データを保持する方法ですが、円環バッファでFIFOしながら、あふれたものから捨てていくものにします。
これにより、現時点で装置に蓄積されているデータを取り出すことができます。ここで1つこだわりポイントがあって、なんとか時間を記録しようとしています。
というのは、M5StampC3などマイコンには、時計は内蔵されていません。どうしても時計が使いたいのであれば、Wi-fiでネットに接続し、時間を取得する方法が一般的です。
今回作製しているSALZmini2022はWi-fiを用いないため、時間のチェックはできません。
ただ、装置の電源が入ってからの時間をカウントするmillis()関数があります。
これを記録しておけば、クライアント側で情報をつなぎ合わせて、時間に復元できるのではないかと思ったのです。
できるかどうかわかりませんが、この方向で進めていきたいと思います。
これらの変更を取り込んだ結果、ステートマシン図は下記のようになりました。
今回はここまでか。。。
最近、作製日誌の書き方がわかってきたような気がします。作製しながら文章を書いていますが、課題に当たり行き詰ったらそこまでの内容をまとめ記事にするとよいようです。記事の内容としてはすっきりしない結末となりますが、作者としては課題をあぶりだすことができ、またいったん考えることをやめてクールダウンできるので、スタートダッシュがよいようです。
今回はステートマシン図を作って、処理をスマートに書き換えたことが大きな進歩でした。
リクエストに応じて時系列データを送信する方法
BOWLからリクエストをし、時系列データを受信、グラフ化する方法
が次回の課題となります。
#自動潅水装置 #盆栽 #園芸 #電子工作 #BLE #ステートマシン図 #ArduinoIDE #M5StampC3 #SALZmini2022 #夏の自由研究
この記事が気に入ったらサポートをしてみませんか?