Slp_Wk_and_Tx
は、定期起床後、何か実行(センサーデータの取得など)を行って、その結果を無線パケットとして送信するようなアプリケーションを想定した、テンプレートソースコードです。setup()
, loop()
の形式では、どうしても loop()
中が判読しづらい条件分岐が発生しがちです。本actでは、loop()
中をSM_SIMPLE
ステートマシンを用いて _switch_
構文による単純な状態遷移を用いることで、コードの見通しを良くしています。
このアクトには以下が含まれます。
- 代表的な間欠動作(スリープ→起床→計測→無線送信→スリープ)の制御構造
- 送信パケットの生成と送信手続き、完了待ち
アクトの機能
- 起動後、初期化処理を経て、一旦スリープする
setup()
初期化するbegin()
スリープ実行する
- スリープ起床後、状態変数を初期化し、以下の順に動作を行う
wakeup()
スリープからの起床、各初期化を行うloop()
状態INIT
->WORK_JOB
に遷移: 何らかの処理を行う(このactでは 1ms ごとのTickCount
ごとにカウンタを更新し乱数で決めたカウント後にTX
状態に進む)loop()
状態TX
送信要求を行うloop()
状態WAIT_TX
送信完了待ちを行うloop()
状態EXIT_NORMAL
スリープする (1. に戻る)
loop()
状態EXIT_FATAL
エラーが発生した場合は、モジュールリセットする
アクトの解説
宣言部
インクルード
#include <TWELITE>
#include <NWK_SIMPLE>
#include <SM_SIMPLE>
#include "Common.h"
パケット送信を行うため <NWK_SIMPLE>
をインクルードしています。また、アプリケーションIDなど基本的な定義は "Common.h"
に記述しています。
状態定義
loop()
内の順次処理を記述うするため、このサンプルではステートマシン(状態遷移)の考え方を用います。ごく単純な状態遷移の処理をまとめた<SM_SIMPLE>
を用います。
Common.h
に以下の状態に対応する列挙体 STATE
が定義されています。
enum class STATE {
INIT = 0, // INIT STATE
WORK_JOB, // do some job (e.g sensor capture)
TX, // reuest transmit
WAIT_TX, // wait its completion
EXIT_NORMAL, // normal exiting.
EXIT_FATAL // has a fatal error (will do system reset)
};
状態を示す列挙体STATE
を用いてSM_SIMPLE
ステートマシン(状態遷移)を宣言します。
SM_SIMPLE<STATE> step;
ここで宣言されたstep
は、状態の管理、タイムアウト、処理待ちを行うための機能が含まれています。
センサーデータ
このサンプルではセンサーデーターの処理は行いませんが、ダミーデータを用意しておきます。
struct {
uint16_t dummy_work_ct_now;
uint16_t dummy_work_ct_max; // counter for dummy work job.
} sensor;
setup()
void setup() {
/*** SETUP section */
step.setup(); // init state machine
// the twelite main class
the_twelite
<< TWENET::appid(APP_ID) // set application ID (identify network group)
<< TWENET::channel(CHANNEL) // set channel (pysical channel)
<< TWENET::rx_when_idle(false); // open receive circuit (if not set, it can't listen packts from others)
// Register Network
auto&& nwk = the_twelite.network.use<NWK_SIMPLE>();
nwk << NWK_SIMPLE::logical_id(DEVICE_ID); // set Logical ID.
/*** BEGIN section */
the_twelite.begin(); // start twelite!
/*** INIT message */
Serial << "--- Sleep an Tx Act ---" << crlf;
}
変数やクラスオブジェクトの初期化を行います。
step
ステートマシンの初期化the_twelite
クラスオブジェクトの初期化- ネットワーク
<NWK_SIMPLE>
の登録と初期化(DEVICE_ID
の登録)を行います。
つづいてクラスオブジェクトやハードウェアなどの開始処理を行います。
the_twelite.begin(); // start twelite!
the_twelite
を開始するための手続きです。act0..4
では出てきませんでしたがthe_twelite
の設定や各種ビヘイビアの登録を行った場合は、必ず呼び出すようにしてください。
begin()
void begin() {
Serial << "..begin (run once at boot)" << crlf;
SleepNow();
}
setup()
の直後に一度だけ呼び出されます。SleepNow()
関数を呼び出して初回のスリープ手続きを行います。
wakeup()
void wakeup() {
memset(&sensor, 0, sizeof(sensor));
Serial << crlf << int(millis()) << ":wake up!" << crlf;
}
起床直後に呼び出されます。ここではセンサーデータ領域の初期化と、起床時のメッセージを出力しています。
loop()
void loop() {
do {
switch(step.state()) {
case STATE::INIT:
sensor.dummy_work_ct_now = 0;
sensor.dummy_work_ct_max = random(10,1000);
step.next(STATE::WORK_JOB);
break;
...
}
} while (step.b_more_loop());
}
上記のコードは、実際のコードを簡略化したものです。
この制御構造はSM_SIMPLE
ステートマシンを利用しています。do..while()
構文のループになっています。ループの中はswitch case
節となっていて、.state()
で得られた状態により処理を分岐しています。状態の遷移は.next()
を呼び出しステートマシン内の内部変数を新しい状態値に書き換えます。
step.b_more_loop()
は、.next()
により状態遷移があった場合 true
に設定されます。これは状態遷移が発生したときloop()
を脱出せずに次の状態のコード(case
節)を実行する目的です。
以下に各状態の解説を行います。
STATE::INIT
sensor.dummy_work_ct_now = 0;
sensor.dummy_work_ct_max = random(10,1000);
step.next(STATE::WORK_JOB);
ダミーーのセンサー値を初期化します。一つは加算カウンタ、一つはカウンター停止値でランダムに決定しています。
STATE::WORK_JOB
if (TickTimer.available()) {
Serial << '.';
sensor.dummy_work_ct_now++;
if (sensor.dummy_work_ct_now >= sensor.dummy_work_ct_max) {
Serial << crlf;
step.next(STATE::TX);
}
}
WORK_JOB
状態では1msごとのタイマー単位で処理します。TickタイマーごとにTickTimer.available()
になります。Tickタイマーごとにカウンタを加算しdummy_work_ct_max
になったら、次の状態STATE::TX
に遷移します。
STATE::TX
if (Transmit()) {
Serial << int(millis()) << ":tx request success!" << crlf;
step.set_timeout(100);
step.clear_flag();
step.next(STATE::WAIT_TX);
} else {
// normall it should not be here.
Serial << int(millis()) << "!FATAL: tx request failed." << crlf;
step.next(STATE::EXIT_FATAL);
}
Transmit()
関数を呼び出しパケット送信要求を行います。送信要求が成功した場合はSTATE::WAIT_TXEVENT
に遷移して送信完了を待つことになります。ここでは完了待ちとしてSM_SIMPLE
ステートマシンのタイムアウトとフラッグ機能を用います(待ちループ中での変数値の変化により判定する単純なものです)。
単一の送信要求が失敗することは通常想定しませんが、失敗時はSTATE::EXIT_FATAL
として例外処理する状態に遷移します。
Transmit()
関数はMWX_APIRET
オブジェクトを返しますが、このオブジェクトはbool
型の成功の可否と、最大31ビットの値を保持しています。bool
型として評価できますから、if
文の判定は送信要求が成功したら true
、失敗したらfalse
を返します。STATE::WAIT_TX
if (step.is_flag_ready()) {
Serial << int(millis()) << ":tx completed!" << crlf;
step.next(STATE::EXIT_NORMAL);
} else if (step.is_timeout()) {
Serial << int(millis()) << "!FATAL: tx timeout." << crlf;
step.next(STATE::EXIT_FATAL);
}
送信完了待ちは後述のon_tx_comp()
によりステートマシン機能のフラッグをセットすることで判定しています。タイムアウトは.is_timeout()
を呼び出すことで.set_timeout()
を行ったときからの経過時間により判定します。
送信が成功しても失敗しても通常は完了通知がありますが、タイムアウトを設け例外処理のための状態STATE::EXIT_FATAL
に遷移します。
STATE::EXIT_NORMAL
SleepNow();
SleepNow()
を呼び出して、スリープ処理に入ります。
STATE::EXIT_FATAL
Serial << crlf << "!FATAL: RESET THE SYSTEM.";
delay(1000); // wait a while.
the_twelite.reset_system();
重大なエラーとして、システムリセットを行います。
SleepNow()
void SleepNow() {
uint16_t u16dur = SLEEP_DUR;
u16dur = random(SLEEP_DUR - SLEEP_DUR_TERMOR, SLEEP_DUR + SLEEP_DUR_TERMOR);
Serial << int(millis()) << ":sleeping for " << int(u16dur) << "ms" << crlf;
Serial.flush();
step.on_sleep(); // reset status of statemachine to INIT state.
the_twelite.sleep(u16dur, false);
}
周期スリープを行います。スリープ時間はrandom()
関数を用いて、一定の時間ブレを作っています。これは複数のデバイスの送信周期が同期した場合、著しく失敗率が上がる場合があるためです。
スリープ前にはSM_SIMPLE
ステートマシンの状態を.on_sleep()
を呼び出してセットしておきます。
Transmit()
MWX_APIRET vTransmit() {
Serial << int(millis()) << ":vTransmit()" << crlf;
if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
// set tx packet behavior
pkt << tx_addr(0x00) // 0..0xFF (LID 0:parent, FE:child w/ no id, FF:LID broad cast), 0x8XXXXXXX (long address)
<< tx_retry(0x1) // set retry (0x3 send four times in total)
<< tx_packet_delay(0,0,2); // send packet w/ delay (send first packet with randomized delay from 0 to 0ms, repeat every 2ms)
// prepare packet payload
pack_bytes(pkt.get_payload() // set payload data objects.
, make_pair(FOURCC, 4) // string should be paired with length explicitly.
, uint32_t(millis()) // put timestamp here.
, uint16_t(sensor.dummy_work_ct_now) // put dummy sensor information.
);
// do transmit
//return nwksmpl.transmit(pkt);
return pkt.transmit();
}
return MWX_APIRET(false, 0);
}
ID=0x00
の親機宛に無線パケットの送信要求を行います。格納されるデータはActサンプルで共通に使われている4文字識別子(FOURCC
)に加え、システム時間[ms]とダミーセンサー値(sensor.dummy_work_ct_now
)を格納します。
まず最初に送信パケットを格納するオブジェクトを取得します。このオブジェクトを操作し、送信データや条件を設定します。
if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
MWX ライブラリでは、if
文中でオブジェクトを取得し、そのオブジェクトのbool
判定でtrue
の場合に処理を行う記述を採用しています。
ここではthe_twelite.network.use<NWK_SIMPLE>()
によりボードオブジェクトを取得し、ボードオブジェクトの.prepare_tx_packet()
によりパケットオブジェクトを取得しています。パケットオブジェクトの取得失敗は通常想定しませんが、失敗時は送信キューが一杯で送信要求が受け付けられない場合です。このサンプルは単一の送信のみですから、エラーは想定外の重大な問題に限られます。
pkt << tx_addr(0x00) // 宛先
<< tx_retry(0x1) // 再送回数
<< tx_packet_delay(0,0,2); // 送信遅延
得られたpkt
オブジェクトに対して、送信条件(宛先や再送など)を<<
演算子を用いて設定します。
tx_addr
はパケットの宛先を指定します。tx_retry
は再送回数、tx_packet_delay
は送信遅延の指定です。
pack_bytes(pkt.get_payload() // set payload data objects.
, make_pair(FOURCC, 4) // string should be paired with length explicitly.
, uint32_t(millis()) // put timestamp here.
, uint16_t(sensor.dummy_work_ct_now) // put dummy sensor information.
);
パケットのペイロード(データ部分)はpkt.get_payload()
により得られるsmblbuf<uint8_t>
派生の配列です。この配列に対して直接値を設定しても構いませんが、ここではpack_bytes()
を用いた値の設定を行います。
NWK_SIMPLE
パケット構造と最大長を参照ください。この関数は可変数引数により指定できます。一番最初のパラメータは.get_payload()
より得られた配列オブジェクトです。
make_pair(FOURCC,4)
:make_pair
はC++標準ライブラリのもので、std::pair
オブジェクトを生成します。文字列型に対して先頭から4バイト分を書き出すという意味になります。(文字列型の配列は終端を含める、含めないといった話題が混乱を生むため、明示的に書き出すバイト数を指定するために、このような指定をします)uint32_t
型のデータを指定するとビッグエンディアン並びで4バイト分のデータを書き込みます。uint16_t
型のデータについても同様です。
uint8_t
型のポインタを用いてデータの書き込みを行うことも出来ます。
auto&& pay = pkt.get_payload(); // get buffer object.
// the following code will write data directly to internal buffer of `pay' object.
uint8_t *p = pay.begin(); // get the pointer of buffer head.
S_OCTET(p, FOURCC[0]); // store byte at pointer `p' and increment the pointer.
S_OCTET(p, FOURCC[1]);
S_OCTET(p, FOURCC[2]);
S_OCTET(p, FOURCC[3]);
S_DWORD(p, millis()); // store uint32_t data.
S_WORD(p, sensor.dummy_work_ct_now); // store uint16_t data.
pay.redim(p - pay.begin());
.get_payload()
から得られた配列オブジェクトは、何も格納されていないサイズ0の配列ですが、この配列にデータを書き込むことでサイズが拡張され(実際は内部の固定長のバッファに対してデータを書き込み、内部管理のデータサイズを更新します)、最終的なサイズがペイロードのデータサイズです。
ここでは.begin()
を用いてuint8_t*
のポインタを得て、このポインタを用いてデータを書き込み、最後に書き込んだサイズを.redim()
で設定します。
S_OCTET()
, S_WORD()
, S_DWORD()
といった関数を書き込みに用いていますが、例えばS_OCTET(p, 'H')
は *p = 'H'; p++;
と同じ処理を行うポインタを用いたデータ書き込みです。
最後の.redim()
は配列のサイズをバッファの初期化をせずに変更する手続きです。.resize()
を呼び出すとすべて0クリアされます。
最後に.transmit()
を呼び出して、送信要求を行います。戻り値はMWX_APIRET
型です。要求後、実際の送信が行われますが、送信パラメータや送信サイズにもよりますが、完了まで数ms~数十ms程度はかかります。完了時にはon_tx_comp()
が呼び出されます。
return pkt.transmit();
MWX_APIRET
はuint32_t
型をラップしたクラスで、MSBを失敗成功のフラグとし、以下31ビットをデータとして用いています。pkt.transmit()
の戻り型になっており、送信要求の成功と失敗(bool
型へのキャスト)ならびに送信IDをデータ部(.get_value()
)に格納しています。on_tx_comp()
void on_tx_comp(mwx::packet_ev_tx& ev, bool_t &b_handled) {
step.set_flag(ev.bStatus);
}
送信完了時に呼び出されるシステムイベントです。ここでは.set_flag()
により完了としています。