MWSDK/Act_Samples
ディレクトリには、actのサンプルプログラムを格納しています。セクションの複数ページをまとめています。 印刷またはPDF形式で保存...
act サンプル
- 1: act サンプル
- 1.1: act0..4
- 1.2: Scratch
- 1.3: Slp_Wk_and_Tx
- 1.4: Parent_MONOSTICK
- 1.5: PingPong
- 1.6: BRD_APPTWELITE
- 1.7: BRD_I2C_TEMPHUMID
- 1.8: BRD_ARIA
- 1.9: PAL_AMB
- 1.10: PAL_AMB-usenap
- 1.11: PAL_AMB-behavior
- 1.12: PAL_MAG
- 1.13: PAL_MOT-single
- 1.14: PAL_MOT-fifo
- 1.15: PulseCounter
- 1.16: WirelessUART
- 1.17: ユニバーサル レシーバー
- 1.18: Unit_???
1 - act サンプル
MWSDK/Act_Samples
ディレクトリには、actのサンプルプログラムを格納しています。サンプルの紹介
以下には、目的別のアクトを紹介します。
無線通信を行わなず、マイコン機能のみの短いアクト
act0..4
無線機能などを使わないごくシンプルな例です。actの基本構造が理解できます。
I2C センサーを用いたアクトの記述例
I2Cセンサーを接続し、スリープによる簡潔動作を行いながら無線パケットを送信する、無線センサーの実装例です。
BRD_I2C_TEMPHUMID
TWELITE で無線センサーを実装するための代表的な要素(シンプル中継ネット <NWK_SIMPLE>
の利用・インタラクティブモード <STG_STD>
、I2Cセンサーの取り扱い Wire
、スリープによる間欠動作など)が含まれます。
無線通信を行う基本的なアクト
無線パケットを送信、または送受信するサンプルですが、各々少し違った視点で実装されています。
Scratch
UARTから1バイトコマンドを受けて、送信などを行うシンプルなコードです。
Slp_Wk_and_Tx
ステートマシンを用い、スリープを用いた間欠動作で、スリープ復帰→無線送信→スリープを繰り返します。
PingPong
一方から他方にパケットを送信し、受信した他方がパケットを送り返すサンプルです。送信と受信についての基本的な手続きが含まれます。
WirelessUART
UART入力をserparser
を用いてアスキー形式を解釈してから、これを送信します。
親機側のアクト
独自の受信側親機アプリケーションを実装するときに参照してください。
Parent-MONOSTICK
専ら受信のみを行い、シリアルポートへ受信結果を出力します。このサンプルの無線パケットで、親機向け(0x00
)や子機ブロードキャスト(0xFE
)とアドレス設定しているものは受信できます。またインタラクティブモード<STG_STD>
をactに追加するための手続きが含まれます。
Rcv_Univsl
ユニバーサルパケットレシーバ (TWENETレイヤーツリーネットワーク, App_Twelite
, act
, … など) のサンプルコードです。また、コンテナやアルゴリズムにEASTLライブラリを使用しています。
インタラクティブモードを追加するためのアクト
インタラクティブモードを使用するアクトの解説には大まかな流れを記しています(ここでは上述の BRD_I2C_TEMPHUMID
を引用します)。どのサンプルの解説も大きな差はありません。
BRD_I2C_TEMPHUMID
I2Cセンサーデバイスの読み書きコマンドを実行し I2C センサーから得られた計測値を無線送信します。またインタラクティブモード<STG_STD>
をactに追加するための手続きが含まれます。
Settings
インタラクティブモード<STG_STD>
のより高度なカスタマイズを行います。詳細はコードを参照ください。
センサーなどのデバイスを動作させるためのアクト
内蔵ペリフェラルや外部センサーデバイスからセンサー情報を得るサンプルです。
BRD_APPTWELITE
ディジタル入力、アナログ入力、ディジタル出力、アナログ出力を用いた双方向通信を行っています。またインタラクティブモード<STG_STD>
をactに追加するための手続きが含まれます。
BRD_I2C_TEMPHUMID
I2Cセンサーデバイスの読み書きコマンドを実行し I2C センサーから得られた計測値を無線送信します。またインタラクティブモード<STG_STD>
をactに追加するための手続きが含まれます。
PulseCounter
パルスカウンター機能を用い、スリープ中も含め入力ポートで検出したパルス数を計数し、これを無線送信します。
PAL_AMB_behavior
ビヘイビアを用いた例です。PAL_AMBでは温湿度センサーはライブラリ内部のコードが呼ばれますが、このサンプルでは温湿度センサーのアクセスのための独自の手続きも含まれます。
TWELITE PAL を使用するためのアクト
TWELITE PAL には標準的なPALアプリが書き込まれていますが、PALアプリを用いずアクトによる記述を行うことができます。MWXライブラリには、PALで使用するセンサーを動作させるための標準的な手続きが用意されています。
各種PAL基板用のサンプルです。PAL基板上のセンサー値を取得し、送信し、スリープします。
PAL_AMB
PAL_MOT-single
PAL_MAG
以下は応用例で、上記のアクトより少し複雑な記述になっています。
PAL_AMB_usenap
は、数十msかかるセンサーの動作時間にTWELITEマイコンを短くスリープさせ、より省電力を目指すサンプルです。PAL_AMB_behavior
は、ビヘイビアを用いた例です。PAL_AMBでは温湿度センサーはライブラリ内部のコードが呼ばれますが、このサンプルでは温湿度センサーのアクセスのための独自の手続きも含まれます。PAL_MOT_fifo
は、加速度センサーのFIFOおよびFIFOの割り込みを用いて、サンプルを中断することなく、連続的に取得し無線送信するためのサンプルです。
TWELITE CUE を使用するためのアクト
PAL_MOT
アクトが利用できます。軽微な修整が必要となる場合があります。
PAL_MOT-single
PAL_MOT_fifo
は、加速度センサーのFIFOおよびFIFOの割り込みを用いて、サンプルを中断することなく、連続的に取得し無線送信するためのサンプルです。
TWELITE ARIA を使用するためのアクト
BRD_ARIA
は、TWELITE ARIA を動作させるためのアクトです。BRD_I2C_TEMPHUMID
は、I2C センサー利用のためのテンプレートですが、実装例として TWELITE ARIA で利用する SHT40 センサー用のコードが含まれます。PAL_AMB
用のアクトを修整することで利用できます。
単体機能を紹介したアクト
Unit-*
は機能やAPIの紹介を目的としています。
最新版の入手
最新版のコードや MWSDK バージョン間の修正履歴を確認する目的で Github にソース一式を置いています。以下のリンク先を参照してください。
共通の記述
アクトのサンプル中で以下の項目は共通の設定項目になり、以下で解説します。
const uint32_t APP_ID = 0x1234abcd;
const uint8_t CHANNEL = 13;
const char APP_FOURCHAR[] = "BAT1";
サンプルアクト共通として以下の設定をしています。
- アプリケーションID 0x1234abcd
- チャネル 13
アプリケーションIDとチャネルはともに他のネットワークと混在しないようにする仕組みです。
アプリケーションIDが異なる者同士は、チャネルが同じであっても混信することはありません。ただし、別のアプリケーションIDのシステムが頻繁に無線送信しているような場合はその無線送信が妨害となりますので影響が認められます。
チャネルは通信に使う周波数を決めます。TWELITE無線モジュールでは原則として16個のチャネルが利用でき、通常のシステムでは実施しないような極めて例外的な場合を除いて、他のチャネルとは通信できません。
サンプルアクト共通の仕様として、パケットのペイロード(データ部)の先頭には4バイトの文字列(APP_FOURCHAR[]
)を格納しています。種別の識別性には1バイトで十分ですが、解説のための記述です。こういったシステム特有の識別子やデータ構造を含めることも混信対策の一つであるといえます。
1.1 - act0..4
act0 から始まるアクト(act)は、actを始める - Opening actで紹介されたものを収録しています。LEDやボタンの動作のみの単純なものですが、最初にお試しいただくことをお勧めします。
act0
処理の記述がないテンプレート
act1
Lチカ(LEDの点滅)
act2
タイマーを用いたLチカ
act3
2つのタイマーを用いたLチカ
act4
ボタン(スイッチ)を用いたLED点灯
1.2 - Scratch
このアクトには以下が含まれます。
- 無線パケットの送信 (’t’ キー)
- スリープ (’s’キー)
- シリアルポートからの入力 -
Serial
- ディジタル(ボタン)入力 -
Buttons
setup()
void setup() {
/*** SETUP section */
tx_busy = false;
// 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(); // 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(0xFE); // set Logical ID. (0xFE means a child device with no ID)
/*** BEGIN section */
Buttons.begin(pack_bits(PIN_BTN), 5, 10); // check every 10ms, a change is reported by 5 consequent values.
the_twelite.begin(); // start twelite!
/*** INIT message */
Serial << "--- Scratch act ---" << mwx::crlf;
}
the_twelite
を設定してアプリケーションID APP_ID
, 無線チャネル CHANNEL
、受信有を設定します。
またnwk
を生成し、子機アドレス0xFE
を指定しています。このアドレスは子機でアドレスを指定していない名無しの子機という意味です。
設定できるアドレスは0x00
: 親機,0x01
~0xEF
: 子機, 0xFE
:子機アドレス未指定の範囲です。
送信先として指定するアドレスは0x00
は親機宛、0x01
~0xEF
は指定の親機アドレス、0xFE
は任意の子機アドレス、0xFF
は親機を含む任意のアドレスです。
またButtons
オブジェクトを初期化します。連続参照によるチャタリング抑制アルゴリズムです。10msごとに5回連続同じ値になれば対象のポート(PIN_BTN
のみ)のHIGH
またはLOW
を確定します。pack_bits(N1, N2, ..)
は1UL<<N1 | 1UL << N2 | ...
を行いビットマップを生成します。
the_twelite.begin(); // start twelite!
the_twelite
を開始するための手続きです。act0..4
では出てきませんでしたがthe_twelite
の設定や各種ビヘイビアの登録を行った場合は、必ず呼び出すようにしてください。
begin()
void begin() {
Serial << "..begin (run once at boot)" << mwx::crlf;
}
始動時setup()
の後に1回だけ呼び出されます。メッセージの表示のみ。
loop()
ボタン(スイッチ)の入力検出
if (Buttons.available()) {
uint32_t bm, cm;
Buttons.read(bm, cm);
if (cm & 0x80000000) {
// the first capture.
}
Serial << int(millis()) << ":BTN" << format("%b") << mwx::crlf;
}
Buttons
による連続参照により状態を確定します。ボタン状態が変化したらシリアルに出力します。
シリアルからの入力
while(Serial.available()) {
int c = Serial.read();
Serial << '[' << char(c) << ']';
switch(c) {
case 'p': ... // millis() を表示
case 't': ... // 無線パケットを送信 (vTransmit)
if (!tx_busy) {
tx_busy = Transmit();
if (tx_busy) {
Serial << int(millis()) << ":tx request success! ("
<< int(tx_busy.get_value()) << ')' << mwx::crlf;
} else {
Serial << int(millis()) << ":tx request failed" << mwx::crlf;;
}
}
case 's': ... // スリープする
Serial << int(millis()) << ":sleeping for " << 5000 << "ms" << mwx::crlf << mwx::flush;
the_twelite.sleep(5000);
break;
}
}
Serial.available()
がtrue
の場合は、シリアルポートからの入力が保存されています。シリアルから1文字読み込んで、入力文字に応じた処理をします。
’t’を入力して無線送信
’t’を入力したときは送信を行います。このサンプルではtx_busy
フラグを用い連続的に入力は行わないようにしています。
送信要求は一定数までキューに保存されるため、キューの範囲(3パケット)で要求を積むことは可能です。
以下はif(!tx_busy)
の判定をしないようにして ’tttt’と連続的に入力した場合の処理例です。4つ目の要求でキューが一杯になって要求は失敗しています。
Transmit()
の.prepare_tx_packet()
で得られたpktオブジェクトが false
になります。
送信タイミングはランダム化されるため、送信完了は送信要求順にはなりません。
--- Scratch act ---
..begin (run once at boot)
[t]11591:Transmit()
11592:tx request success! (1)
[t]11593:Transmit()
11593:tx request success! (2)
[t]11594:Transmit()
11595:tx request success! (3)
[t]11595:Transmit()
TX QUEUE is FULL
11596:tx request failed
11654:tx completed!(id=2, stat=1)
11719:tx completed!(id=3, stat=1)
11745:tx completed!(id=1, stat=1)
’s’を入力してスリープ
the_twelite.sleep(5000);
5000ms=5秒のスリープを実施します。復帰後はwakeup()
が実行されます。
wakeup()
void wakeup() {
Serial << int(millis()) << ":wake up!" << mwx::crlf;
}
スリープ起床時に最初に呼び出されます。メッセージの表示のみ。
Transmit()
MWX_APIRET Transmit() {
Serial << int(millis()) << ":Transmit()" << mwx::crlf;
if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
// set tx packet behavior
pkt << tx_addr(0xFF) // 同報通信=ブロードキャスト
<< tx_retry(0x1) // 再送1回
<< tx_packet_delay(100,200,20); // 送信時遅延100-200msの間に送信、再送間隔20ms
// 送信データの指定(アプリケーションごとに決める)
pack_bytes(pkt.get_payload()
, make_pair("SCRT", 4) // 4文字識別子
, uint32_t(millis()) // タイムスタンプ
);
// 送信要求を行う
return pkt.transmit();
} else {
// .prepare_tx_packet() 時点で失敗している(送信キューが一杯)
Serial << "TX QUEUE is FULL" << mwx::crlf;
return MWX_APIRET(false, 0);
}
}
送信要求を行う最小限の手続きです。
この関数を抜けた時点では、まだ要求は実行されていません。しばらく待つ必要があります。この例では100-200msの送信開始の遅延を設定しているため、送信が開始されるのは早くて100ms後です。
on_tx_comp()
void on_tx_comp(mwx::packet_ev_tx& ev, bool_t &b_handled) {
Serial << int(millis()) << ":tx completed!"
<< format("(id=%d, stat=%d)", ev.u8CbId, ev.bStatus) << mwx::crlf;
tx_busy = false; // clear tx busy flag.
}
送信完了時に呼び出されます。ev
には送信IDと完了ステータスが含まれます。
on_rx_packet()
void on_rx_packet(packet_rx& rx, bool_t &handled) {
Serial << format("rx from %08x/%d",
rx.get_addr_src_long(), rx.get_addr_src_lid()) << mwx::crlf;
}
パケットを受信したら、送信元のアドレス情報を表示します。
1.3 - Slp_Wk_and_Tx
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()
により完了としています。
1.4 - Parent_MONOSTICK
このアクトには以下が含まれます。
- 無線パケットの受信
- 受信したパケットのデータ解釈
- インタラクティブモードの設定 -
<STG_STD>
- バイト列のアスキー形式への変換 -
serparser
アクトの機能
- サンプルアクトの子機からのパケットを受信して、シリアルポートへ出力する。
アクトの使い方
必要なTWELITEと配線
役割 | 例 |
---|---|
親機 | MONOSTICK BLUE/RED |
子機 | サンプルアクトの子機に設定したTWELITEシリーズ(例: Slp_Wk_and_Tx , PAL_AMB , PAL_MAG , PAL_MOT??? など) |
最初は以下のデフォルトの設定にて確認してください。
- アプリケーションID:
0x1234abcd
- チャネル:
13
アクトの解説
宣言部
インクルード
// use twelite mwx c++ template library
#include <TWELITE>
#include <MONOSTICK>
#include <NWK_SIMPLE>
#include <STG_STD>
MONOSTICK用のボードビヘイビア<MONOSTICK>
をインクルードしています。このボードサポートには、LEDの制御、ウォッチドッグ対応が含まれます。
<NWK_SIMPLE>
簡易中継ネットの定義を読み込みます<STG_STD>
インタラクティブモードの定義を読み込みます。
その他
// application ID
const uint32_t DEFAULT_APP_ID = 0x1234abcd;
// channel
const uint8_t DEFAULT_CHANNEL = 13;
// option bits
uint32_t OPT_BITS = 0;
/*** function prototype */
bool analyze_payload(packet_rx& rx);
デフォルト値や関数プロトタイプなどの宣言をしています。
setup()
auto&& brd = the_twelite.board.use<MONOSTICK>();
auto&& set = the_twelite.settings.use<STG_STD>();
auto&& nwk = the_twelite.network.use<NWK_SIMPLE>();
setup()
では、まず<MONOSTICK>
ボードビヘイビア、<STG_STD>
インタラクティブモード ビヘイビア、<NWK_SIMPLE>
ビヘイビアをuse<>
を用い読み込みます。この手続きは必ずsetup()
内で行います。
set << SETTINGS::appname("PARENT"); // 設定画面中のタイトル
set << SETTINGS::appid_default(DEFAULT_APP_ID); // アプリケーションIDデフォルト
set << SETTINGS::ch_default(DEFAULT_CHANNEL); // チャネルデフォルト
set << SETTINGS::lid_default(0x00); // LIDデフォルト
set.hide_items(E_STGSTD_SETID::OPT_DWORD2, E_STGSTD_SETID::OPT_DWORD3, E_STGSTD_SETID::OPT_DWORD4, E_STGSTD_SETID::ENC_KEY_STRING, E_STGSTD_SETID::ENC_MODE);
set.reload(); // 設定を不揮発性メモリから読み出す
OPT_BITS = set.u32opt1(); // 読み込み例(オプションビット)
続いてインタラクティブモードの設定と設定値の読み出しを行います。<STG_STD>
インタラクティブモードでは、標準的な項目が用意されていますが、作成するアクトごとにいくつかのカスタマイズを行えるようになっています。
appname
→ 設定画面中のタイトル行にでるアクト名称appid_default
→ デフォルトのアプリケーションIDch_default
→ デフォルトのチャネルlid_default
→ デバイスID(LID)のデフォルト値.hide_items()
→ 項目の非表示設定
設定値を読み出す前には必ず.reload()
を呼び出します。設定値は.u32opt1()
のように設定値ごとに読み出し用のメソッドが用意されています。
the_twelite
<< set // インタラクティブモードの設定を反映
<< TWENET::rx_when_idle() // 受信するように指定
;
// Register Network
nwk << set; // インタラクティブモードの設定を反映
nwk << NWK_SIMPLE::logical_id(0x00) // LIDだけは再設定
;
いくつかの設定値は<STG_STD>
オブジェクトを用いて直接反映することが出来ます。また、DIPスイッチの設定などにより特定の値を書き換えたいような場合などは、反映されたあとに別個に値を書き換えることも出来ます。上記の例ではthe_twelite
オブジェクトにアプリケーションID、チャネル、無線出力などを設定し、nwk
オブジェクトに対してLIDと再送回数の設定をしてから、再度LIDを0
に設定し直しています。
brd.set_led_red(LED_TIMER::ON_RX, 200); // RED (on receiving)
brd.set_led_yellow(LED_TIMER::BLINK, 500); // YELLOW (blinking)
<MONOSTICK>
ボードビヘイビアではLED点灯制御のための手続きを利用できます。
1行目では赤色のLEDを無線パケットを受信したら200ms点灯する設定をしています。最初のパラメータはLED_TIMER::ON_RX
が無線パケット受信時を意味します。2番目は点灯時間をmsで指定します。
2行目はLEDの点滅指定です。1番目のパラメータはLED_TIMER::BLINK
が点滅の指定で、2番目のパラメータは点滅のON/OFF切り替え時間です。500msごとにLEDが点灯、消灯(つまり1秒周期の点滅)を繰り返します。
the_twelite.begin(); // start twelite!
the_twelite
を開始するための手続きです。act0..4
では出てきませんでしたがthe_twelite
の設定や各種ビヘイビアの登録を行った場合は、必ず呼び出すようにしてください。
loop()
このサンプルではloop()
中の処理はありません。
void loop() {
}
on_rx_packet()
パケットを受信したときに呼び出されるコールバック関数です。この例では受信したパケットデータに対していくつかの出力を行っています。
void on_rx_packet(packet_rx& rx, bool_t &handled) {
Serial << ".. coming packet (" << int(millis()&0xffff) << ')' << mwx::crlf;
...
// packet analyze
analyze_payload(rx);
}
analyze_payload
関数の末尾で呼び出されるanalyze_payload()
は、いくつかのサンプルアクトのパケットを解釈するコードが含まれています。サンプルアクト中のパケット生成部分と対応させてコードを参照してください。
bool b_handled = false;
uint8_t fourchars[4]{};
auto&& np = expand_bytes(
rx.get_payload().begin(), rx.get_payload().end()
, fourchars
);
if (np == nullptr) return;
// display fourchars at first
Serial
<< fourchars
<< format("(ID=%d/LQ=%d)", rx.get_addr_src_lid(), rx.get_lqi())
<< "-> ";
この関数では最初に4文字識別データをfourchars[5]
配列に読み込みます。
読み込みはexpand_bytes()
関数を用います。この関数の第1・第2パラメータはC++の標準ライブラリの作法に倣い、受信パケットのペイロード部の先頭ポインタ.begin()
と末尾ポインタの次.end()
を与えます。続くパラメータは可変引数として、読み込むデータ変数を与えます。戻り値はエラー時はnullptr
、それ以外は次の解釈ポインタとなります。末尾まで解釈した場合は.end()
が戻ります。ここでのパラメータはuint8_t fourchars[4]
です。
この記述で対応するのは配列長さNが指定されるuint8_t[N]
型のみで、uint8*
型、char*
型、char[]
型などを用いる場合は、make_pair(char*,int)
を用いた指定が必要になります。
char fourchars[5]{}; // 終端文字\0も含め5バイト確保する
auto&& np = expand_bytes(
rx.get_payload().begin(), rx.get_payload().end()
, make_pair((char *)fourchars, 4)
);
つづいて4バイトヘッダに対応した処理を行います。ここではサンプルアクトSlp_Wk_and_Tx
のパケットを解釈し内容を表示します。
// Slp_Wk_and_Tx
if (!b_handled && !strncmp(fourchars, "TXSP", 4)) {
b_handled = true;
uint32_t tick_ms;
uint16_t u16work_ct;
np = expand_bytes(np, rx.get_payload().end()
, tick_ms
, u16work_ct
);
if (np != nullptr) {
Serial << format("Tick=%d WkCt=%d", tick_ms, u16work_ct);
} else {
Serial << ".. error ..";
}
}
他の解釈部の判定をスキップするようにb_handled
をtrue
に設定します。
"TXSP"
のパケットではuint32_t
型のシステムタイマーカウントと、uint16_t
型のダミーカウンタの値が格納されています。各々変数を宣言してexpand_bytes()
関数を用い読み込みます。上述と違うのは、読み出しの最初のポインタとして第一パラメータがnp
となっている点です。tick_ms
とu16work_ct
をパラメータとして与え、ビッグエンディアン形式のバイト列としてペイロードに格納された値を読み出します。
読み出しに成功すれば内容を出力して終了です。
独自のアスキー書式を定義して出力する
ユーザが定義した並び順でアスキー形式により構成します。
smplbuf_u8<128> buf;
mwx::pack_bytes(buf
, uint8_t(rx.get_addr_src_lid()) // 送信元の論理ID
, uint8_t(0xCC) // 0xCC
, uint8_t(rx.get_psRxDataApp()->u8Seq) // パケットのシーケンス番号
, uint32_t(rx.get_addr_src_long()) // 送信元のシリアル番号
, uint32_t(rx.get_addr_dst()) // 宛先アドレス
, uint8_t(rx.get_lqi()) // LQI:受信品質
, uint16_t(rx.get_length()) // 以降のバイト数
, rx.get_payload() // データペイロード
);
serparser_attach pout;
pout.begin(PARSER::ASCII, buf.begin(), buf.size(), buf.size());
Serial << "FMT PACKET -> ";
pout >> Serial;
Serial << mwx::flush;
1行目はアスキー書式に変換する前のデータ列を格納するバッファをローカルオブジェクトとして宣言しています。
2行目はpack_bytes()
を用いてデータ列を先ほどのbuf
に格納します。データ構造はソースコードのコメントを参照ください。pack_bytes()
のパラメータにはsmplbuf_u8 (smplbuf<uint8_t, ???>)
形式のコンテナを指定することもできます。
パケットのシーケンス番号は、<NWK_SIMPLE>
で自動設定され、送信パケット順に割り振られます。この値はパケットの重複検出に用いられます。
LQI (Link Quality Indicator)は受信時の電波強度に相当する値で、値が大きければ大きいほどより強い電界強度で受信できていることになります。ただしこの値と物理量との厳格な関連は定義されていませんし、環境のノイズと相対的なものでLQIがより大きな値であってもノイズも多ければ通信の成功率も低下することになります。
13,14,17行目は、シリアルパーサーの宣言と設定、出力です。
NWK_SIMPLEのヘッダを含めてダンプ出力する
最初の出力(if(0)
により実行されないようになっています)は<NWK_SIMPLE>
の制御データを含めたデータをすべて表示します。制御データは11バイトあります。通常は制御情報を直接参照することはありませんが、あくまでも参考です。
serparser_attach pout;
pout.begin(PARSER::ASCII, rx.get_psRxDataApp()->auData,
rx.get_psRxDataApp()->u8Len, rx.get_psRxDataApp()->u8Len);
Serial << "RAW PACKET -> ";
pout >> Serial;
Serial << mwx::flush;
// 参考:制御部のパケット構造
// uint8_t : 0x01 固定
// uint8_t : 送信元のLID
// uint32_t : 送信元のロングアドレス(シリアル番号)
// uint32_t : 宛先アドレス
// uint8_t : 中継回数
1行目は出力用のシリアルパーサをローカルオブジェクトとして宣言しています。内部にバッファを持たず、外部のバッファを流用し、パーサーの出力機能を用いて、バッファ内のバイト列をアスキー形式出力します。
2行目はシリアルパーサーのバッファを設定します。すでにあるデータ配列、つまり受信パケットのペイロード部を指定します。serparser_attach pout
は、既にあるバッファを用いたシリアルパーサーの宣言です。pout.begin()
の1番目のパラメータは、パーサーの対応書式をPARSER::ASCII
つまりアスキー形式として指定しています。2番目はバッファの先頭アドレス。3番目はバッファ中の有効なデータ長、4番目はバッファの最大長を指定します。出力用で書式解釈に使わないため4番目のパラメータは3番目と同じ値を入れています。
6行目でシリアルポートへ>>
演算子を用いて出力しています。
7行目のSerial << mwx::flush
は、ここで出力が終わっていないデータの出力が終わるまで待ち処理を行う指定です。(Serial.flush()
も同じ処理です)
1.5 - PingPong
このアクトには以下が含まれます。
- 無線パケットの受信からの速やかな応答送信
- 相手のアドレスを直接指定した送信
- シリアルポートからの入力 -
Serial
- ディジタル(ボタン)入力 -
Buttons
- アナログ入力 -
Analogue
アクトの使い方
必要なTWELITE
いずれかを2台用意します。
- MONOSTICK BLUE / RED
- TWELITE R シリーズ でUART接続した TWELITE DIP など
アクトの解説
宣言部
インクルード
// use twelite mwx c++ template library
#include <TWELITE>
#include <NWK_SIMPLE>
全てのアクトで<TWELITE>
をインクルードします。ここでは、シンプルネットワーク <NWK_SIMPLE>
をインクルードしておきます。
その他
// application ID
const uint32_t APP_ID = 0x1234abcd;
// channel
const uint8_t CHANNEL = 13;
// DIO pins
const uint8_t PIN_BTN = 12;
/*** function prototype */
void vTransmit(const char* msg, uint32_t addr);
/*** application defs */
// packet message
const int MSG_LEN = 4;
const char MSG_PING[] = "PING";
const char MSG_PONG[] = "PONG";
- サンプルアクト共通宣言
- 長めの処理を関数化しているため、そのプロトタイプ宣言(送信と受信)
- アプリケーション中のデータ保持するための変数
setup()
void setup() {
/*** SETUP section */
Buttons.setup(5); // init button manager with 5 history table.
Analogue.setup(true, 50); // setup analogue read (check every 50ms)
// 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(); // open receive circuit (if not set, it can't listen packts from others)
// Register Network
auto&& nwksmpl = the_twelite.network.use<NWK_SIMPLE>();
nwksmpl << NWK_SIMPLE::logical_id(0xFE) // set Logical ID. (0xFE means a child device with no ID)
<< NWK_SIMPLE::repeat_max(3); // can repeat a packet up to three times. (being kind of a router)
/*** BEGIN section */
Buttons.begin(pack_bits(PIN_BTN), 5, 10); // check every 10ms, a change is reported by 5 consequent values.
Analogue.begin(pack_bits(PIN_ANALOGUE::A1, PIN_ANALOGUE::VCC)); // _start continuous adc capture.
the_twelite.begin(); // start twelite!
/*** INIT message */
Serial << "--- PingPong sample (press 't' to transmit) ---" << mwx::crlf;
}
大まかな流れは、各部の初期設定、各部の開始となっています。
the_twelite
このオブジェクトはTWENETを操作するための中核クラスオブジェクトです。
// 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(); // open receive circuit (if not set, it can't listen packts from others)
the_twelite
に設定を反映するには <<
を用います。
TWENET::appid(APP_ID)
アプリケーションIDの指定TWENET::channel(CHANNEL)
チャネルの指定TWENET::rx_when_idle()
受信回路をオープンにする指定
<<
, >>
演算子はC言語におけるビットシフト演算子ですが、ここではその意味合いとは異なる挿入演算子(stream insertion operator)として使用しています。MWXライブラリでは、C++標準ライブラリの入出力利用にならって上記のような設定やシリアルポートの入出力等で利用します。
ただし、以下の記述は MWX ライブラリでは利用できません。
#include <iostream>
std::cout << "hello world" << std::endl;
次にネットワークを登録します。
auto&& nwksmpl = the_twelite.network.use<NWK_SIMPLE>();
nwksmpl << NWK_SIMPLE::logical_id(0xFE);
<< NWK_SIMPLE::repeat_max(3);
1行目は、ボードの登録と同じ書き方で <>
には <NWK_SIMPLE>
を指定します。
2行目は、<NWK_SIMPLE>
の設定で、0xFE
(ID未設定の子機)という指定を行います。
3行目は、中継回数の最大値を指定しています。この解説では中継には触れませんが、複数台で動作させたときにパケットの中継が行われます。
the_twelite.begin(); // start twelite!
setup()
関数の末尾で the_twelite.begin()
を実行しています。
Analogue
ADC(アナログディジタルコンバータ)を取り扱うクラスオブジェクトです。
Analogue.setup(true);
初期化Analogue.setup()
で行います。パラメータのtrue
はADC回路の安定までその場で待つ指定です。
Analogue.begin(pack_bits(PIN_ANALOGUE::A1, PIN_ANALOGUE::VCC), 50);
ADCを開始するにはAnalogue.begin()
を呼びます。パラメータはADC対象のピンに対応するビットマップです。
ビットマップを指定するのにpack_bits()
関数を用います。可変数引数の関数で、各引数には1を設定するビット位置を指定します。例えばpack_bits(1,3,5)
なら2進数で 101010
の値が戻ります。この関数はconstexpr
指定があるため、パラメータが定数のみであれば定数に展開されます。
パラメータにはPIN_ANALOGUE::A1
(ADC0)とPIN_ANALOGUE::VCC
(モジュール電源電圧)が指定されています。
2番目のパラメータには50
が指定されています。ADCの動作はデフォルトではTickTimerで開始されていて、初回を除き ADC の開始は、割り込みハンドラ内で行います。
Buttons
DIO (ディジタル入力) の値の変化を検出します。Buttons
では、メカ式のボタンのチャタリング(摺動)の影響を軽減するため、一定回数同じ値が検出されてから、値の変化とします。
Buttons.setup(5);
初期化は Buttons.setup()
で行います。パラメータの 5
は、値の確定に必要な検出回数ですが、設定可能な最大値を指定します。内部的にはこの数値をもとに内部メモリの確保を行っています。
Buttons.begin(pack_bits(PIN_BTN),
5, // history count
10); // tick delta
開始は Buttons.begin()
で行います。1番目のパラメータは検出対象のDIOです。BRD_APPTWELITE::
に定義されるPIN_BTN
(12) を指定しています。2番めのパラメータは状態を確定するのに必要な検出回数です。3番めのパラメータは検出間隔です。10
を指定しているので10msごとに5回連続で同じ値が検出できた時点で、HIGH
, LOW
の状態が確定します。
Buttons
でのDIO状態の検出はイベントハンドラで行います。イベントハンドラは、割り込み発生後にアプリケーションループで呼ばれるため割り込みハンドラに比べ遅延が発生します。Serial
Serial
オブジェクトは、初期化や開始手続きなく利用できます。
Serial << "--- PingPong sample (press 't' to transmit) ---" << mwx::crlf;
シリアルポートへの文字列出力を行います。mwx::crlf
は改行文字です。
loop()
ループ関数は TWENET ライブラリのメインループからコールバック関数として呼び出されます。ここでは、利用するオブジェクトが available になるのを待って、その処理を行うのが基本的な記述です。ここではアクトで使用されているいくつかのオブジェクトの利用について解説します。
TWENET ライブラリのメインループは、事前にFIFOキューに格納された受信パケットや割り込み情報などをイベントとして処理し、そののちloop()
が呼び出されます。loop()
を抜けた後は CPU が DOZE モードに入り、低消費電流で新たな割り込みが発生するまでは待機します。
したがってCPUが常に稼働していることを前提としたコードはうまく動作しません。
void loop() {
// read from serial
while(Serial.available()) {
int c = Serial.read();
Serial << mwx::crlf << char(c) << ':';
switch(c) {
case 't':
vTransmit(MSG_PING, 0xFF);
break;
default:
break;
}
}
// Button press
if (Buttons.available()) {
uint32_t btn_state, change_mask;
Buttons.read(btn_state, change_mask);
// Serial << fmt("<BTN %b:%b>", btn_state, change_mask);
if (!(change_mask & 0x80000000) && (btn_state && (1UL << PIN_BTN))) {
// PIN_BTN pressed
vTransmit(MSG_PING, 0xFF);
}
}
}
Serial
while(Serial.available()) {
int c = Serial.read();
Serial << mwx::crlf << char(c) << ':';
switch(c) {
case 't':
vTransmit(MSG_PING, 0xFF);
break;
default:
break;
}
}
Serial.available()
がtrue
の間はシリアルポートからの入力があります。内部のFIFOキューに格納されるためある程度の余裕はありますが、速やかに読み出すようにします。データの読み出しはSerial.read()
を呼びます。
ここでは't'
キーの入力に対応してvTransmit()
関数を呼び出しPINGパケットを送信します。
Buttons
DIO(ディジタルIO)の入力変化を検出したタイミングで available になり、Buttons.read()
により読み出します。
if (Buttons.available()) {
uint32_t btn_state, change_mask;
Buttons.read(btn_state, change_mask);
1番目のパラメータは、現在のDIOのHIGH/LOWのビットマップで、bit0から順番にDIO0,1,2,.. と並びます。例えば DIO12 であれば btn_state & (1UL << 12)
を評価すれば HIGH / LOW が判定できます。ビットが1になっているものがHIGHになります。
初回確定以外の場合かつPIN_BTN
のボタンが離されたタイミングでvTransmit()
を呼び出しています。押したタイミングにするには(!(btn_state && (1UL << PIN_BTN)))
のように条件を論理反転します。
transmit()
無線パケットの送信要求をTWENETに行う関数です。本関数が終了した時点では、まだ無線パケットの処理は行われません。実際に送信が完了するのは、送信パラメータ次第ですが、数ms後以降になります。ここでは代表的な送信要求方法について解説します。
void vTransmit(const char* msg, uint32_t addr) {
Serial << "vTransmit()" << mwx::crlf;
if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
// set tx packet behavior
pkt << tx_addr(addr) // 0..0xFF (LID 0:parent, FE:child w/ no id, FF:LID broad cast), 0x8XXXXXXX (long address)
<< tx_retry(0x3) // set retry (0x3 send four times in total)
<< tx_packet_delay(100,200,20); // send packet w/ delay (send first packet with randomized delay from 100 to 200ms, repeat every 20ms)
// prepare packet payload
pack_bytes(pkt.get_payload() // set payload data objects.
, make_pair(msg, MSG_LEN) // string should be paired with length explicitly.
, uint16_t(analogRead(PIN_ANALOGUE::A1)) // possible numerical values types are uint8_t, uint16_t, uint32_t. (do not put other types)
, uint16_t(analogRead_mv(PIN_ANALOGUE::VCC)) // A1 and VCC values (note: alalog read is valid after the first (Analogue.available() == true).)
, uint32_t(millis()) // put timestamp here.
);
// do transmit
pkt.transmit();
}
}
ネットワークオブジェクトとパケットオブジェクトの取得
if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
ネットワークオブジェクトをthe_twelite.network.use<NWK_SIMPLE>()
で取得します。そのオブジェクトを用いて.prepare_tx_packet()
によりpkt
オブジェクトを取得します。
ここではif
文の条件判定式の中で宣言しています。宣言したpkt
オブジェクトはif
節の終わりまで有効です。pkt
オブジェクトはbool
型の応答をし、ここではTWENETの送信要求キューに空きがあって送信要求を受け付ける場合にtrue
、空きがない場合にfalse
となります。
パケットの送信設定
pkt << tx_addr(addr) // 0..0xFF (LID 0:parent, FE:child w/ no id, FF:LID broad cast), 0x8XXXXXXX (long address)
<< tx_retry(0x3) // set retry (0x3 send four times in total)
<< tx_packet_delay(100,200,20); // send packet w/ delay (send first packet with randomized delay from 100 to 200ms, repeat every 20ms)
パケットの設定はthe_twelite
の初期化設定のように<<
演算子を用いて行います。
tx_addr()
パラメータに送信先アドレスを指定します。0x00
なら自分が子機で親機宛に、0xFE
なら自分が親機で任意の子機宛のブロードキャストという意味です。tx_retry()
パラメータに再送回数を指定します。例の3は再送回数が3回、つまり合計4回パケットを送ります。無線パケット1回のみの送信では条件が良くても数%程度の失敗はあります。tx_packet_delay()
送信遅延を設定します。一つ目のパラメータは、送信開始までの最低待ち時間、2番目が最長の待ち時間です。この場合は送信要求を発行後におよそ100msから200msの間で送信を開始します。3番目が再送間隔です。最初のパケットが送信されてから20ms置きに再送を行うという意味です。
パケットのデータペイロード
ペイロードは積載物という意味ですが、無線パケットでは「送りたいデータ本体」という意味でよく使われます。無線パケットのデータにはデータ本体以外にもアドレス情報などいくつかの補助情報が含まれます。
送受信を正しく行うために、データペイロードのデータ並び順を意識するようにしてください。ここでは以下のようなデータ順とします。このデータ順に合わせてデータペイロードを構築します。
# 先頭バイトのインデックス: データ型 : バイト数 : 内容
00: uint8_t[4] : 4 : 4文字識別子
08: uint16_t : 2 : AI1のADC値 (0..1023)
06: uint16_t : 2 : Vccの電圧値 (2000..3600)
10: uint32_t : 4 : millis()システム時間
データペイロードには90バイト格納できます(実際にはあと数バイト格納できます)。
IEEE802.15.4の無線パケットの1バイトは貴重です。できるだけ節約して使用することを推奨します。1パケットで送信できるデータ量に限りがあります。パケットを分割する場合は分割パケットの送信失敗などを考慮する必要がありコストは大きくつきます。また1バイト余分に送信するのに、およそ16μ秒×送信時の電流に相当するエネルギーが消費され、特に電池駆動のアプリケーションには大きく影響します。
上記のデータペイロードのデータ構造を実際に構築してみます。データペイロードは pkt.get_payload()
により simplbuf<uint8_t>
型のコンテナとして参照できます。このコンテナに上記の仕様に基づいてデータを構築します。
上記のように記述できますがMWXライブラリでは、データペイロード構築のための補助関数pack_bytes()
を用意しています。
// prepare packet payload
pack_bytes(pkt.get_payload() // set payload data objects.
, make_pair(msg, MSG_LEN) // string should be paired with length explicitly.
, uint16_t(analogRead(PIN_ANALOGUE::A1)) // possible numerical values types are uint8_t, uint16_t, uint32_t. (do not put other types)
, uint16_t(analogRead_mv(PIN_ANALOGUE::VCC)) // A1 and VCC values (note: alalog read is valid after the first (Analogue.available() == true).)
, uint32_t(millis()) // put timestamp here.
);
pack_bytes
の最初のパラメータはコンテナを指定します。この場合はpkt.get_payload()
です。
そのあとのパラメータは可変数引数でpack_bytes
で対応する型の値を必要な数だけ指定します。pack_bytes
は内部で.push_back()
メソッドを呼び出して末尾に指定した値を追記していきます。
3行目のmake_pair()
は標準ライブラリの関数でstd::pair
を生成します。文字列型の混乱(具体的にはペイロードの格納時にヌル文字を含めるか含めないか)を避けるための指定です。make_pair()
の1番目のパラメータに文字列型(char*
やuint8_t*
型、uint8_t[]
など)を指定します。2番目のパラメータはペイロードへの格納バイト数です。
4,5,6行目は、数値型の値 (uint8_t
, uint16_t
, uint32_t
)を格納します。符号付などの数値型、char
型など同じ数値型であっても左記の3つの型にキャストして投入します。
analogRead()
とanalogRead_mv()
は、ADCの結果を取得するものです。前者はADC値(0..1023
)、後者は電圧(mV, 0..2470
)となります。モジュールの電源電圧は内部的に分圧抵抗の値を読んでいるためその変換を行うadalogRead_mv()
を利用しています。
これでパケットの準備は終わりです。最後に、送信要求を行います。
pkt.transmit();
パケットを送信するにはpkt
オブジェクトのpkt.transmit()
メソッドを用います。
on_rx_packet()
受信パケットがある場合の処理です。
void on_rx_packet(packet_rx& rx, bool_t &handled) {
uint8_t msg[MSG_LEN];
uint16_t adcval, volt;
uint32_t timestamp;
// expand packet payload (shall match with sent packet data structure, see pack_bytes())
expand_bytes(rx.get_payload().begin(), rx.get_payload().end()
, msg // 4bytes of msg
// also can be -> std::make_pair(&msg[0], MSG_LEN)
, adcval // 2bytes, A1 value [0..1023]
, volt // 2bytes, Module VCC[mV]
, timestamp // 4bytes of timestamp
);
// if PING packet, respond pong!
if (!strncmp((const char*)msg, "PING", MSG_LEN)) {
// transmit a PONG packet with specifying the address.
vTransmit(MSG_PONG, rx.get_psRxDataApp()->u32SrcAddr);
}
// display the packet
Serial << format("<RX ad=%x/lq=%d/ln=%d/sq=%d:" // note: up to 4 args!
, rx.get_psRxDataApp()->u32SrcAddr
, rx.get_lqi()
, rx.get_length()
, rx.get_psRxDataApp()->u8Seq
)
<< format(" %s AD=%d V=%d TS=%dms>" // note: up to 4 args!
, msg
, adcval
, volt
, timestamp
)
<< mwx::crlf
<< mwx::flush;
}
まず受信パケットのデータはパラメータrx
として渡されます。rx
から無線パケットのアドレス情報やデータペイロードにアクセスします。
while (the_twelite.receiver.available()) {
auto&& rx = the_twelite.receiver.read();
次の行では、受信パケットデータには、送信元のアドレス(32bitのロングアドレスと8bitの論理アドレス)などの情報を参照しています。
Serial << format("..receive(%08x/%d) : ",
rx.get_addr_src_long(), rx.get_addr_src_lid());
<NWK_SIMPLE>
では、8bitの論理IDと32bitのロングアドレスの2種類が常にやり取りされます。送り先を指定する場合はロングアドレスか論理アドレスのいずれかを指定します。受信時には両方のアドレスが含まれます。MWXライブラリにはtransmit()
のときに使ったpack_bytes()
の対になる関数expand_bytes()
が用意されています。
uint8_t msg[MSG_LEN];
uint16_t adcval, volt;
uint32_t timestamp;
// expand packet payload (shall match with sent packet data structure, see pack_bytes())
expand_bytes(rx.get_payload().begin(), rx.get_payload().end()
, msg // 4bytes of msg
// also can be -> std::make_pair(&msg[0], MSG_LEN)
, adcval // 2bytes, A1 value [0..1023]
, volt // 2bytes, Module VCC[mV]
, timestamp // 4bytes of timestamp
);
1行目から3行目までは、データを格納する変数を指定しています。
6行目でexpand_bytes()
によりパケットのペイロードのデータを変数に格納します。1番目のパラメータでコンテナの先頭イテレータ(uint8_t*
ポインタ)を指定します。.begin()
メソッドにより取得できます。2番目のパラメータはコンテナの末尾の次を指すイテレータで.end()
メソッドで取得できます。2番目はコンテナの末尾を超えた読み出しを行わないようにするためです。
3番目以降のパラメータに変数を列挙します。列挙した順番にペイロードの読み出しとデータ格納が行われます。
このアクトでは、パケット長が間違っていた場合などのエラーチェックを省いています。チェックを厳格にしたい場合は、expand_bytes()
の戻り値により判定してください。
expand_bytes()
の戻り値は uint8_t*
ですが、末尾を超えたアクセスの場合はnullptr
(ヌルポインタ)を戻します。
msg
に読み出した4バイト文字列の識別子が"PING"
の場合はPONGメッセージを送信する処理です。
if (!strncmp((const char*)msg, "PING", MSG_LEN)) {
vTransmit(MSG_PONG, rx.get_psRxDataApp()->u32SrcAddr);
}
続いて到着したパケット情報を表示します。
Serial << format("<RX ad=%x/lq=%d/ln=%d/sq=%d:" // note: up to 4 args!
, rx.get_psRxDataApp()->u32SrcAddr
, rx.get_lqi()
, rx.get_length()
, rx.get_psRxDataApp()->u8Seq
)
<< format(" %s AD=%d V=%d TS=%dms>" // note: up to 4 args!
, msg
, adcval
, volt
, timestamp
)
<< mwx::crlf
<< mwx::flush;
数値のフォーマット出力が必要になるのでformat()
を用いています。>>
演算子向けにprintf()
と同じ構文を利用できるようにしたヘルパークラスですが、引数の数は最大8つまで(32bitパラメータの場合)に制限されています。(制限を超えるとコンパイルエラーが出ます。なおSerial.printfmt()
には引数の数の制限がありません。)
mwx::crlf
は改行文字(CRLF)を、mwx::flush
は出力完了待ちを指定します(mwx::flush
はSerial.flush()
と記述しても構いません)。
1.6 - BRD_APPTWELITE
<BRD_APPTWELITE>
を用いたサンプルです。このアクトには以下が含まれます。
- 無線パケットの送受信
- インタラクティブモードによる設定 -
<STG_STD>
- ディジタル(ボタン)入力 -
Buttons
- アナログ入力 -
Analogue
アクトの機能
- M1を読み取り、親機か子機かを決めます。
- DI1-DI4 の値を読み取ります。
Buttons
クラスにより、チャタリングの影響を小さくするため、連続で同じ値になったときにはじめて変化が通知されます。変化があったときには通信を行います。 - AI1-AI4 の値を読み取ります。
- DIの変化または1秒おきに、DI1-4, AI1-4, VCC の値を、自身が親機の場合は子機へ、子機の場合は親機宛に送信します。
- 受信したパケットの値に応じで DO1-4, PWM1-4 に設定します。
アクトの使い方
必要なTWELITEと配線例
役割 | 例 |
---|---|
親機 | TWELITE DIP最低限 M1=GND, DI1:ボタン, DO1:LEDの配線をしておく。 |
子機 | TWELITE DIP最低限 M1=オープン, DI1:ボタン, DO1:LEDの配線をしておく。 |
アクトの解説
宣言部
インクルード
// use twelite mwx c++ template library
#include <TWELITE>
#include <NWK_SIMPLE>
#include <BRD_APPTWELITE>
#include <STG_STD>
全てのアクトで<TWELITE>
をインクルードします。ここでは、シンプルネットワーク <NWK_SIMPLE>
とボードサポート <BRD_APPTWELITE>
をインクルードしておきます。
またインタラクティブモードを追加するために <STG_STD>
をインクルードしています。
その他
/*** Config part */
// application ID
const uint32_t DEFAULT_APP_ID = 0x1234abcd;
// channel
const uint8_t DEFAULT_CHANNEL = 13;
// option bits
uint32_t OPT_BITS = 0;
// logical id
uint8_t LID = 0;
/*** function prototype */
MWX_APIRET transmit();
void receive();
/*** application defs */
const char APP_FOURCHAR[] = "BAT1";
// sensor values
uint16_t au16AI[5];
uint8_t u8DI_BM;
- サンプルアクト共通宣言
- 長めの処理を関数化しているため、そのプロトタイプ宣言(送信と受信)
- アプリケーション中のデータ保持するための変数
setup()
void setup() {
/*** SETUP section */
// init vars
for(auto&& x : au16AI) x = 0xFFFF;
u8DI_BM = 0xFF;
// load board and settings
auto&& set = the_twelite.settings.use<STG_STD>();
auto&& brd = the_twelite.board.use<BRD_APPTWELITE>();
auto&& nwk = the_twelite.network.use<NWK_SIMPLE>();
// settings: configure items
set << SETTINGS::appname("BRD_APPTWELITE");
set << SETTINGS::appid_default(DEFAULT_APP_ID); // set default appID
set << SETTINGS::ch_default(DEFAULT_CHANNEL); // set default channel
set.hide_items(E_STGSTD_SETID::OPT_DWORD2, E_STGSTD_SETID::OPT_DWORD3, E_STGSTD_SETID::OPT_DWORD4, E_STGSTD_SETID::ENC_KEY_STRING, E_STGSTD_SETID::ENC_MODE);
set.reload(); // load from EEPROM.
OPT_BITS = set.u32opt1(); // this value is not used in this example.
LID = set.u8devid(); // logical ID
// the twelite main class
the_twelite
<< set // apply settings (appid, ch, power)
<< TWENET::rx_when_idle(); // open receive circuit (if not set, it can't listen packts from others)
if (brd.get_M1()) { LID = 0; }
// Register Network
nwk << set // apply settings (LID and retry)
;
// if M1 pin is set, force parent device (LID=0)
nwk << NWK_SIMPLE::logical_id(LID); // write logical id again.
/*** BEGIN section */
// start ADC capture
Analogue.setup(true, ANALOGUE::KICK_BY_TIMER0); // setup analogue read (check every 16ms)
Analogue.begin(pack_bits(
BRD_APPTWELITE::PIN_AI1,
BRD_APPTWELITE::PIN_AI2,
BRD_APPTWELITE::PIN_AI3,
BRD_APPTWELITE::PIN_AI4,
PIN_ANALOGUE::VCC)); // _start continuous adc capture.
// Timer setup
Timer0.begin(32, true); // 32hz timer
// start button check
Buttons.setup(5); // init button manager with 5 history table.
Buttons.begin(pack_bits(
BRD_APPTWELITE::PIN_DI1,
BRD_APPTWELITE::PIN_DI2,
BRD_APPTWELITE::PIN_DI3,
BRD_APPTWELITE::PIN_DI4),
5, // history count
4); // tick delta (change is detected by 5*4=20ms consequtive same values)
the_twelite.begin(); // start twelite!
/*** INIT message */
Serial << "--- BRD_APPTWELITE ---" << mwx::crlf;
Serial << format("-- app:x%08x/ch:%d/lid:%d"
, the_twelite.get_appid()
, the_twelite.get_channel()
, nwk.get_config().u8Lid
)
<< mwx::crlf;
Serial << format("-- pw:%d/retry:%d/opt:x%08x"
, the_twelite.get_tx_power()
, nwk.get_config().u8RetryDefault
, OPT_BITS
)
<< mwx::crlf;
}
大まかな流れは、各部の初期設定、各部の開始となっています。
各種ビヘイビアオブジェクトの登録
auto&& set = the_twelite.settings.use<STG_STD>();
auto&& brd = the_twelite.board.use<BRD_APPTWELITE>();
auto&& nwk = the_twelite.network.use<NWK_SIMPLE>();
システムの振る舞いを決めるためのビヘイビアオブジェクトを登録します。インタラクティブモードの設定管理で合ったり、ボードサポート、また無線パケットのネットワーク記述です。
setup()
内で登録しないと動作しません。インタラクティブモードの設定
// インタラクティブモードの初期化
auto&& set = the_twelite.settings.use<STG_STD>();
set << SETTINGS::appname("BRD_APPTWELITE");
set << SETTINGS::appid_default(DEFAULT_APP_ID); // set default appID
set << SETTINGS::ch_default(DEFAULT_CHANNEL); // set default channel
set.hide_items(E_STGSTD_SETID::OPT_DWORD2, E_STGSTD_SETID::OPT_DWORD3, E_STGSTD_SETID::OPT_DWORD4, E_STGSTD_SETID::ENC_KEY_STRING, E_STGSTD_SETID::ENC_MODE);
set.reload(); // load from EEPROM.
OPT_BITS = set.u32opt1(); // this value is not used in this example.
LID = set.u8devid(); // logical ID;
インタラクティブモードの初期化を行います。まずsetオブジェクトを取得しています。続いて以下の処理を行っています。
- アプリケーション名を
"BRD_APPTWELITE"
に設定(メニューで利用される) - デフォルトのアプリケーションIDとチャネル値を書き換える
- 不要な項目を削除する
set.reload()
により保存された設定値を読み出すOPT_BITS
とLID
の値を変数にコピーする
以下は画面例です。+ + + と + を3回、0.2 秒から 1 秒の間をあけて入力するとインタラクティブモード画面を出すことが出来ます。
[CONFIG/BRD_APPTWELITE:0/SID=8XXYYYYY]
a: (0x1234ABCD) Application ID [HEX:32bit]
i: ( 13) Device ID [1-100,etc]
c: ( 13) Channel [11-26]
x: ( 0x03) RF Power/Retry [HEX:8bit]
o: (0x00000000) Option Bits [HEX:32bit]
[ESC]:Back [!]:Reset System [M]:Extr Menu
the_twelite
このオブジェクトはTWENETの中核としてふるまいます。
auto&& brd = the_twelite.board.use<BRD_APPTWELITE>();
ボードの登録(このアクトでは<BRD_APPTWELITE>
を登録しています)。以下のように use
の後に <>
で登録したいボードの名前を指定します。
ユニバーサル参照(auto&&
)にて得られた戻り値として、参照型でのボードオブジェクトが得られます。このオブジェクトにはボード特有の操作や定義が含まれます。以下ではボードオブジェクトを用い、M1ピンの状態を確認しています。M1ピンがLOWであれば、LID
=0、つまり親機アドレスと設定します。
if (brd.get_M1()) { LID = 0; }
the_twelite
を動作させるには初期設定が必要です。アプリケーションIDや無線チャネルの設定は必須といえます。
// the twelite main class
the_twelite
<< set
<< TWENET::rx_when_idle(); // open receive circuit (if not set, it can't listen packts from others)
the_twelite
に設定を反映するには <<
を用います。
set
はインタラクティブモードから読み出した設定の一部(アプリケーションIDや無線チャネルなど)反映させます。反映される項目は<STG_STD>
の解説を参照してください。TWENET::rx_when_idle()
受信回路をオープンにする指定です。
<<
, >>
演算子はC言語におけるビットシフト演算子ですが、ここではその意味合いとは異なる挿入演算子(stream insertion operator)として使用しています。MWXライブラリでは、C++標準ライブラリの入出力利用にならって上記のような設定やシリアルポートの入出力等で利用します。
ただし、以下の記述は MWX ライブラリでは利用できません。
#include <iostream>
std::cout << "hello world" << std::endl;
次にネットワークを登録します。
auto&& nwk = the_twelite.network.use<NWK_SIMPLE>();
nwk << set;
nwk << NWK_SIMPLE::logical_id(LID);
1行目は、ボードの登録と同じ書き方で <>
には <NWK_SIMPLE>
を指定します。
2,3行目は、<NWK_SIMPLE>
の設定です。先にインタラクティブモードの設定値を反映させます。反映される項目はLID
と再送回数です。このアプリケーションではM1ピンの状態によってLID=0
にする場合があるため、3行目で再度LID
を設定しています。
Analogue
ADC(アナログディジタルコンバータ)を取り扱うクラスオブジェクトです。
Analogue.setup(true, ANALOGUE::KICK_BY_TIMER0);
初期化Analogue.setup()
で行います。パラメータのtrue
はADC回路の安定までその場で待つ指定です。2番目のパラメータは、ADCの開始をTimer0
に同期して行う指定です。
Analogue.begin(pack_bits(
BRD_APPTWELITE::PIN_AI1,
BRD_APPTWELITE::PIN_AI2,
BRD_APPTWELITE::PIN_AI3,
BRD_APPTWELITE::PIN_AI4,
PIN_ANALOGUE::VCC));
ADCを開始するにはAnalogue.begin()
を呼びます。パラメータはADC対象のピンに対応するビットマップです。
ビットマップを指定するのにpack_bits()
関数を用います。可変数引数の関数で、各引数には1を設定するビット位置を指定します。例えばpack_bits(1,3,5)
なら2進数で 101010
の値が戻ります。この関数はconstexpr
指定があるため、パラメータが定数のみであれば定数に展開されます。
パラメータと指定されるBRD_APPTWELITE::
にはPIN_AI1..4
が定義されています。App_Tweliteで用いるAI1..AI4に対応します。AI1=ADC1, AI2=DIO0, AI3=ADC2, AI4=DIO2 と割り当てられています。PIN_ANALOGUE::
にはADCで利用できるピンの一覧が定義されています。
Buttons
DIO (ディジタル入力) の値の変化を検出します。Buttons
では、メカ式のボタンのチャタリング(摺動)の影響を軽減するため、一定回数同じ値が検出されてから、値の変化とします。
Buttons.setup(5);
初期化は Buttons.setup()
で行います。パラメータの 5
は、値の確定に必要な検出回数ですが、設定可能な最大値を指定します。内部的にはこの数値をもとに内部メモリの確保を行っています。
Buttons.begin(pack_bits(
BRD_APPTWELITE::PIN_DI1,
BRD_APPTWELITE::PIN_DI2,
BRD_APPTWELITE::PIN_DI3,
BRD_APPTWELITE::PIN_DI4),
5, // history count
4); // tick delta
開始は Buttons.begin()
で行います。1番目のパラメータは検出対象のDIOです。BRD_APPTWELITE::
に定義されるPIN_DI1-4 (DI1-DI4) を指定しています。2番めのパラメータは状態を確定するのに必要な検出回数です。3番めのパラメータは検出間隔です。4を指定しているので4msごとに5回連続で同じ値が検出できた時点で、HIGH, LOWの状態が確定します。
Buttons
でのDIO状態の検出はイベントハンドラで行います。イベントハンドラは、割り込み発生後にアプリケーションループで呼ばれるため割り込みハンドラに比べ遅延が発生します。Timer0
Timer0.begin(32, true); // 32hz timer
App_Twelite ではアプリケーションの制御をタイマー起点で行っているため、このアクトでも同じようにタイマー割り込み・イベントを動作させます。もちろん1msごとに動作しているシステムのTickTimerを用いても構いません。
上記の例の1番目のパラメータはタイマーの周波数で32Hzを指定しています。2番目のパラメータをtrue
にするとソフトウェア割り込みが有効になります。
Timer0.begin()
を呼び出したあと、タイマーが稼働します。
the_twelite
の動作開始
the_twelite.begin(); // start twelite!
setup()
関数の末尾で the_twelite.begin()
を実行しています。
Serial
Serial
オブジェクトは、初期化や開始手続きなく利用できます。
Serial << "--- BRD_APPTWELITE ---" << mwx::crlf;
Serial << format("-- app:x%08x/ch:%d/lid:%d"
, the_twelite.get_appid()
, the_twelite.get_channel()
, nwk.get_config().u8Lid
)
<< mwx::crlf;
Serial << format("-- pw:%d/retry:%d/opt:x%08x"
, the_twelite.get_tx_power()
, nwk.get_config().u8RetryDefault
, OPT_BITS
)
<< mwx::crlf;
このサンプルでは始動時のメッセージとしていくつかのシステム設定値を表示しています。Serial
オブジェクトには const char*
型の文字列や、int
型(他の整数型はNG)、printf()
とほぼ同じ振る舞いをするformat()
、改行文字を出力するcrlf
などを<<
演算子に与えます。
mwx::
を省略している場合もあります。上記ではmwx::crlf
と記載していますがcrlf
と記載しても構いません。mwx::
名前空間は、一部を省略可能とするように設計しています。loop()
ループ関数は TWENET ライブラリのメインループからコールバック関数として呼び出されます。ここでは、利用するオブジェクトが available になるのを待って、その処理を行うのが基本的な記述です。ここではアクトで使用されているいくつかのオブジェクトの利用について解説します。
TWENET ライブラリのメインループは、事前にFIFOキューに格納された受信パケットや割り込み情報などをイベントとして処理し、そののちloop()
が呼び出されます。loop()
を抜けた後は CPU が DOZE モードに入り、低消費電流で新たな割り込みが発生するまでは待機します。
したがってCPUが常に稼働していることを前提としたコードはうまく動作しません。
/*** loop procedure (called every event) */
void loop() {
if (Buttons.available()) {
uint32_t bp, bc;
Buttons.read(bp, bc);
u8DI_BM = uint8_t(collect_bits(bp,
BRD_APPTWELITE::PIN_DI4, // bit3
BRD_APPTWELITE::PIN_DI3, // bit2
BRD_APPTWELITE::PIN_DI2, // bit1
BRD_APPTWELITE::PIN_DI1)); // bit0
transmit();
}
if (Analogue.available()) {
au16AI[0] = Analogue.read(PIN_ANALOGUE::VCC);
au16AI[1] = Analogue.read_raw(BRD_APPTWELITE::PIN_AI1);
au16AI[2] = Analogue.read_raw(BRD_APPTWELITE::PIN_AI2);
au16AI[3] = Analogue.read_raw(BRD_APPTWELITE::PIN_AI3);
au16AI[4] = Analogue.read_raw(BRD_APPTWELITE::PIN_AI4);
}
if (Timer0.available()) {
static uint8_t u16ct;
u16ct++;
if (u8DI_BM != 0xFF && au16AI[0] != 0xFFFF) { // finished the first capture
if ((u16ct % 32) == 0) { // every 32ticks of Timer0
transmit();
}
}
}
}
Buttons
DIO(ディジタルIO)の入力変化を検出したタイミングで available になり、Buttons.read()
により読み出します。
if (Buttons.available()) {
uint32_t bp, bc;
Buttons.read(bp, bc);
1番目のパラメータは、現在のDIOのHIGH/LOWのビットマップで、bit0から順番にDIO0,1,2,.. と並びます。例えば DIO12 であれば bp & (1UL << 12)
を評価すれば HIGH / LOW が判定できます。ビットが1になっているものがHIGHになります。
次にビットマップから値を取り出してu8DI_BM
に格納しています。ここではMWXライブラリで用意したcollect_bits()
関数を用いています。
u8DI_BM = uint8_t(collect_bits(bp,
BRD_APPTWELITE::PIN_DI4, // bit3
BRD_APPTWELITE::PIN_DI3, // bit2
BRD_APPTWELITE::PIN_DI2, // bit1
BRD_APPTWELITE::PIN_DI1)); // bit0
/* collect_bits は以下の処理を行います。
u8DI_BM = 0;
if (bp & (1UL << BRD_APPTWELITE::PIN_DI1)) u8DI_BM |= 1;
if (bp & (1UL << BRD_APPTWELITE::PIN_DI2)) u8DI_BM |= 2;
if (bp & (1UL << BRD_APPTWELITE::PIN_DI3)) u8DI_BM |= 4;
if (bp & (1UL << BRD_APPTWELITE::PIN_DI4)) u8DI_BM |= 8;
*/
collect_bits()
は、上述のpack_bits()
と同様のビット位置の整数値を引数とします。可変数引数の関数で、必要な数だけパラメータを並べます。上記の処理では bit0 は DI1、bit1 は DI2、bit2 は DI3、bit3 は DI4の値としてu8DI_BM
に格納しています。
App_Twelite では、DI1から4に変化があった場合に無線送信しますので、Buttons.available()
を起点に送信処理を行います。transmit()
処理の内容は後述します。
transmit();
Analogue
ADCのアナログディジタル変換が終了した直後のloop()
で available になります。次の ADC が開始するまでは、データは直前に取得されたものとして読み出すことが出来ます。
ADCのアナログディジタル変換が終了した直後のloop()で available になります。次の ADC が開始するまでは、データは直前に取得されたものとして読み出すことが出来ます。
ADC値を読むには Analogue.read()
または Analogue.read_raw()
メソッドを用います。read()
はmVに変換した値、read_raw()
は 0..1023
のADC値となります。パラメータにはADCのピン番号を指定します。ADCのピン番号はPIN_ANALOGUE::
やBRD_APPTWELITE::
に定義されているので、こちらを利用しています。
周期的に実行されるADCの値は、タイミングによってはavailable通知前のより新しい値が読み出されることがあります。
このアクトでは32Hzと比較的ゆっくりの周期で処理しているため、available判定直後に処理すれば問題にはなりませんが、変換周期が短い場合、loop()
中で比較的長い時間のかかる処理をしている場合は注意が必要です。
Analogue
には、変換終了後に割り込みハンドラ内から呼び出されるコールバック関数を指定することが出来ます。例えば、このコールバック関数からFIFOキューに値を格納する処理を行い、アプリケーションループ内ではキューの値を逐次読み出すといった非同期処理を行います。
Timer0
Timer0は32Hzで動作しています。タイマー割り込みが発生直後の loop()
で available になります。つまり、秒32回の処理をします。ここでは、ちょうど1秒になったところで送信処理をしています。
if (Timer0.available()) {
static uint8_t u16ct;
u16ct++;
if (u8DI_BM != 0xFF && au16AI[0] != 0xFFFF) { // finished the first capture
if ((u16ct % 32) == 0) { // every 32ticks of Timer0
transmit();
}
}
}
AppTweliteでは約1秒おきに定期送信を行っています。Timer0
がavailableになったときにu16ctをインクリメントします。このカウンタ値をもとに、32回カウントが終わればtransmit()
を呼び出し無線パケットを送信しています。
u8DI_BM
とau16AI[]
の値判定は、初期化直後かどうかの判定です。まだDI1..DI4やAI1..AI4の値が格納されていない場合は何もしません。
transmit()
無線パケットの送信要求をTWENETに行う関数です。本関数が終了した時点では、まだ無線パケットの処理は行われません。実際に送信が完了するのは、送信パラメータ次第ですが、数ms後以降になります。ここでは代表的な送信要求方法について解説します。
MWX_APIRET transmit() {
if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
auto&& set = the_twelite.settings.use<STG_STD>();
if (!set.is_screen_opened()) {
Serial << "..DI=" << format("%04b ", u8DI_BM);
Serial << format("ADC=%04d/%04d/%04d/%04d ", au16AI[1], au16AI[2], au16AI[3], au16AI[4]);
Serial << "Vcc=" << format("%04d ", au16AI[0]);
Serial << " --> transmit" << mwx::crlf;
}
// set tx packet behavior
pkt << tx_addr(u8devid == 0 ? 0xFE : 0x00) // 0..0xFF (LID 0:parent, FE:child w/ no id, FF:LID broad cast), 0x8XXXXXXX (long address)
<< tx_retry(0x1) // set retry (0x1 send two times in total)
<< tx_packet_delay(0,50,10); // send packet w/ delay (send first packet with randomized delay from 100 to 200ms, repeat every 20ms)
// prepare packet payload
pack_bytes(pkt.get_payload() // set payload data objects.
, make_pair(APP_FOURCHAR, 4) // string should be paired with length explicitly.
, uint8_t(u8DI_BM)
);
for (auto&& x : au16AI) {
pack_bytes(pkt.get_payload(), uint16_t(x)); // adc values
}
// do transmit
return pkt.transmit();
}
return MWX_APIRET(false, 0);
}
関数プロトタイプ
MWX_APIRET transmit()
MWX_APIRET
はuint32_t
型のデータメンバを持つ戻り値を取り扱うクラスです。MSB(bit31)が成功失敗、それ以外が戻り値として利用するものです。
ネットワークオブジェクトとパケットオブジェクトの取得
if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
ネットワークオブジェクトをthe_twelite.network.use<NWK_SIMPLE>()
で取得します。そのオブジェクトを用いて.prepare_tx_packet()
によりpkt
オブジェクトを取得します。
ここではif
文の条件判定式の中で宣言しています。宣言したpkt
オブジェクトはif
節の終わりまで有効です。pkt
オブジェクトはbool
型の応答をし、ここではTWENETの送信要求キューに空きがあって送信要求を受け付ける場合にtrue
、空きがない場合にfalse
となります。
インタラクティブモード画面表示中の表示抑制
auto&& set = the_twelite.settings.use<STG_STD>();
if (!set.is_screen_opened()) {
//インタラクティブモード画面中ではない!
}
インタラクティブモードの画面が表示されているときは、画面出力を抑制します。
パケットの送信設定
pkt << tx_addr(u8devid == 0 ? 0xFE : 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,50,10); // send packet w/ delay (send first packet with randomized delay from 100 to 200ms, repeat every 20ms)
パケットの設定はthe_twelite
の初期化設定のように<<
演算子を用いて行います。
tx_addr()
パラメータに送信先アドレスを指定します。0x00
なら自分が子機で親機宛に、0xFE
なら自分が親機で任意の子機宛のブロードキャストという意味です。tx_retry()
パラメータに再送回数を指定します。例の1は再送回数が1回、つまり合計2回パケットを送ります。無線パケット1回のみの送信では条件が良くても数%程度の失敗はあります。tx_packet_delay()
送信遅延を設定します。一つ目のパラメータは、送信開始までの最低待ち時間、2番目が最長の待ち時間です。この場合は送信要求を発行後におよそ0msから50msの間で送信を開始します。3番目が再送間隔です。最初のパケットが送信されてから10ms置きに再送を行うという意味です。
パケットのデータペイロード
ペイロードは積載物という意味ですが、無線パケットでは「送りたいデータ本体」という意味でよく使われます。無線パケットのデータにはデータ本体以外にもアドレス情報などいくつかの補助情報が含まれます。
送受信を正しく行うために、データペイロードのデータ並び順を意識するようにしてください。ここでは以下のようなデータ順とします。このデータ順に合わせてデータペイロードを構築します。
# 先頭バイトのインデックス: データ型 : バイト数 : 内容
00: uint8_t[4] : 4 : 4文字識別子
04: uint8_t : 1 : DI1..4のビットマップ
06: uint16_t : 2 : Vccの電圧値
08: uint16_t : 2 : AI1のADC値 (0..1023)
10: uint16_t : 2 : AI2のADC値 (0..1023)
12: uint16_t : 2 : AI3のADC値 (0..1023)
14: uint16_t : 2 : AI4のADC値 (0..1023)
データペイロードには90バイト格納できます(実際にはあと数バイト格納できます)。
IEEE802.15.4の無線パケットの1バイトは貴重です。できるだけ節約して使用することを推奨します。1パケットで送信できるデータ量に限りがあります。パケットを分割する場合は分割パケットの送信失敗などを考慮する必要がありコストは大きくつきます。また1バイト余分に送信するのに、およそ16μ秒×送信時の電流に相当するエネルギーが消費され、特に電池駆動のアプリケーションには大きく影響します。
上記の例は、解説のためある程度の妥協をしています。節約を考える場合 00: の識別子は1バイトの簡単なものにすべきですし、VCCの電圧値は8ビットに丸めてもかまわないでしょう。また各AI1..AI4の値は10bitで、合計40bit=5バイトに対して6バイト使用しています。
上記のデータペイロードのデータ構造を実際に構築してみます。データペイロードは pkt.get_payload()
により simplbuf<uint8_t>
型のコンテナとして参照できます。このコンテナに上記の仕様に基づいてデータを構築します。
auto&& payl = pkt.get_payload();
payl.reserve(16); // 16バイトにリサイズ
payl[00] = APP_FOURCHAR[0];
payl[01] = APP_FOURCHAR[1];
...
payl[08] = (au16AI[0] & 0xFF00) >> 8; //Vcc
payl[09] = (au16AI[0] & 0xFF);
...
payl[14] = (au16AI[4] & 0xFF00) >> 8; // AI4
payl[15] = (au16AI[4] & 0xFF);
上記のように記述できますがMWXライブラリでは、データペイロード構築のための補助関数pack_bytes()
を用意しています。
// prepare packet payload
pack_bytes(pkt.get_payload() // set payload data objects.
, make_pair(APP_FOURCHAR, 4) // string should be paired with length explicitly.
, uint8_t(u8DI_BM)
);
for (auto&& x : au16AI) {
pack_bytes(pkt.get_payload(), uint16_t(x)); // adc values
}
pack_bytes()
の最初のパラメータはコンテナを指定します。この場合はpkt.get_payload()
です。
そのあとのパラメータは可変数引数でpack_bytes()
で対応する型の値を必要な数だけ指定します。pack_bytes()
は内部で.push_back()
メソッドを呼び出して末尾に指定した値を追記していきます。
3行目のmake_pair()
は標準ライブラリの関数でstd::pair
を生成します。文字列型の混乱(具体的にはペイロードの格納時にヌル文字を含めるか含めないか)を避けるための指定です。make_pair()
の1番目のパラメータに文字列型(char*
やuint8_t*
型、uint8_t[]
など)を指定します。2番目のパラメータはペイロードへの格納バイト数です。
4行目はuint8_t
型でDI1..DI4のビットマップを書き込みます。
7-9行目ではau16AI
配列の値を順に書き込んでいます。この値はuint16_t
型で2バイトですが、ビッグエンディアンの並びで書き込みます。
7行目のfor
文はC++で導入された範囲for文です。サイズのわかっている配列やbegin()
, end()
によるイテレータによるアクセスが可能なコンテナクラスなどは、この構文が使用できます。au16AI
の型もコンパイル時に判定できるため auto&&
(ユニバーサル参照)で型の指定も省略してます。
通常のfor
文に書き換えると以下のようになります。
for(int i = 0; i < sizeof(au16AI)/sizeof(uint16_t)); i++) {
pack_bytes(pkt.get_payload(), au16AI[i]);
}
これでパケットの準備は終わりです。あとは、送信要求を行います。
return pkt.transmit();
パケットを送信するにはpkt
オブジェクトのpkt.transmit()
メソッドを用います。戻り値としてMWX_APIRET型を返していますが、このアクトでは使っていません。
on_rx_packet()
無線パケットが受信できたときは、受信イベントとしてon_rx_packet()
が呼び出されます。
the_twelite.receiver
による手続きでは一旦受信パケットを内部キュー(2パケットまで格納)に格納してからの処理でしたが、on_rx_packet()
ではTWENETライブラリからのコールバックから直接呼び出され、より取りこぼし等が発生しにくい手続きです。ただしloop()
文中で長時間処理を止めてしまうような記述を行うと、同じように取りこぼしの原因となります。ここでは、相手方から伝えられたDI1..DI4の値とAI1..AI4の値を、自身のDO1..DO4とPWM1..PWM4に設定します。
void on_rx_packet(packet_rx& rx, bool_t &handled) {
auto&& set = the_twelite.settings.use<STG_STD>();
Serial << format("..receive(%08x/%d) : ", rx.get_addr_src_long(), rx.get_addr_src_lid());
// expand the packet payload
char fourchars[5]{};
auto&& np = expand_bytes(rx.get_payload().begin(), rx.get_payload().end()
, make_pair((uint8_t*)fourchars, 4) // 4bytes of msg
);
// check header
if (strncmp(APP_FOURCHAR, fourchars, 4)) { return; }
// read rest of payload
uint8_t u8DI_BM_remote = 0xff;
uint16_t au16AI_remote[5];
expand_bytes(np, rx.get_payload().end()
, u8DI_BM_remote
, au16AI_remote[0]
, au16AI_remote[1]
, au16AI_remote[2]
, au16AI_remote[3]
, au16AI_remote[4]
);
Serial << format("DI:%04b", u8DI_BM_remote & 0x0F);
for (auto&& x : au16AI_remote) {
Serial << format("/%04d", x);
}
Serial << mwx::crlf;
// set local DO
digitalWrite(BRD_APPTWELITE::PIN_DO1, (u8DI_BM_remote & 1) ? HIGH : LOW);
digitalWrite(BRD_APPTWELITE::PIN_DO2, (u8DI_BM_remote & 2) ? HIGH : LOW);
digitalWrite(BRD_APPTWELITE::PIN_DO3, (u8DI_BM_remote & 4) ? HIGH : LOW);
digitalWrite(BRD_APPTWELITE::PIN_DO4, (u8DI_BM_remote & 8) ? HIGH : LOW);
// set local PWM : duty is set 0..1024, so 1023 is set 1024.
Timer1.change_duty(au16AI_remote[1] == 1023 ? 1024 : au16AI_remote[1]);
Timer2.change_duty(au16AI_remote[2] == 1023 ? 1024 : au16AI_remote[2]);
Timer3.change_duty(au16AI_remote[3] == 1023 ? 1024 : au16AI_remote[3]);
Timer4.change_duty(au16AI_remote[4] == 1023 ? 1024 : au16AI_remote[4]);
}
関数プロトタイプ
void on_rx_packet(packet_rx& rx, bool_t &handled)
受信パケットのデータrx
をパラメータとして渡されます。rx
から無線パケットのアドレス情報やデータペイロードにアクセスします。パラメータhandled
は通常利用しません。
送信元アドレスの表示
if (!set.is_screen_opened()) {
Serial << format("..receive(%08x/%d) : ",
rx.get_addr_src_long(), rx.get_addr_src_lid());
}
受信パケットデータには、送信元のアドレス(32bitのロングアドレスと8bitの論理アドレス)などの情報を参照しています。インタラクティブモード画面が表示されているときは出力を抑制します。
<NWK_SIMPLE>
では、8bitの論理IDと32bitのロングアドレスの2種類が常にやり取りされます。送り先を指定する場合はロングアドレスか論理アドレスのいずれかを指定します。受信時には両方のアドレスが含まれます。パケットの識別
MWXライブラリにはtransmit()
の時に使ったpack_bytes()
の対になる関数expand_bytes()
が用意されています。
char fourchars[5]{};
auto&& np = expand_bytes(rx.get_payload().begin(), rx.get_payload().end()
, make_pair((uint8_t*)fourchars, 4) // 4bytes of msg
);
1行目ではデータ格納のためのchar
型の配列を宣言しています。サイズが5バイトなのは末尾にヌル文字を含め、文字出力などでの利便性を挙げるためです。末尾の{}
は初期化の指定で、5バイト目を0
にすれば良いのですが、ここでは配列全体をデフォルトの方法で初期化、つまり0
にしています。
2行目でexpand_bytes()
により4バイト文字列を取り出しています。パラメータにコンテナ型を指定しない理由は、この続きを読み出すための読み出し位置を把握する必要があるためです。1番目のパラメータでコンテナの先頭イテレータ(uint8_t*
ポインタ)を指定します。.begin()
メソッドにより取得できます。2番目のパラメータはコンテナの末尾の次を指すイテレータで.end()
メソッドで取得できます。2番目はコンテナの末尾を超えた読み出しを行わないようにするためです。
3番目に読み出す変数を指定しますが、ここでもmake_pair()
によって文字列配列とサイズのペアを指定します。
このアクトでは、パケット長が間違っていた場合などのエラーチェックを省いています。チェックを厳格にしたい場合は、expand_bytes()
の戻り値により判定してください。
expand_bytes()
の戻り値は uint8_t*
ですが、末尾を超えたアクセスの場合はnullptr
(ヌルポインタ)を戻します。
読み出した4バイト文字列の識別子が、このアクトで指定した識別子と異なる場合は、このパケットを処理しません。
if (strncmp(APP_FOURCHAR, fourchars, 4)) { return; }
TWENETではアプリケーションIDと物理的な無線チャネルが合致する場合は、どのアプリケーションもたとえ種別が違ったとしても、受信することが出来ます。他のアプリケーションで作成したパケットを意図しない形で受信しない目的で、このような識別子やデータペイロードの構造などのチェックを行い、偶然の一致が起きないように対処することを推奨します。
シンプルネットワーク<NWK_SIMPLE>
でのパケット構造の要件も満足する必要があるため、シンプルネットワークを使用しない他のアプリケーションが同じ構造のパケットを定義しない限り(非常にまれと思われます)、パケットの混在受信は発生しません。
データペイロードの取得
DI1..DI4の値とAI1..AI4の値を別の変数に格納します。
// read rest of payload
uint8_t u8DI_BM_remote = 0xff;
uint16_t au16AI_remote[5];
expand_bytes(np, rx.get_payload().end()
, u8DI_BM_remote
, au16AI_remote[0]
, au16AI_remote[1]
, au16AI_remote[2]
, au16AI_remote[3]
, au16AI_remote[4]
);
先ほどのexpand_bytes()
の戻り値np
を1番目のパラメータにしています。先に読み取った4バイト文字列識別子の次から読み出す指定です。2番目のパラメータは同様です。
3番目以降のパラメータはデータペイロードの並びに一致した型の変数を、送り側のデータ構造と同じ順番で並べています。この処理が終われば、指定した変数にペイロードから読み出した値が格納されます。
取得したデータの表示
確認のためシリアルポートへ出力します。インタラクティブモード画面が表示されているときは出力は抑制します。
auto&& set = the_twelite.settings.use<STG_STD>();
...
Serial << format("DI:%04b", u8DI_BM_remote & 0x0F);
for (auto&& x : au16AI_remote) {
Serial << format("/%04d", x);
}
Serial << mwx::crlf;
数値のフォーマット出力が必要になるのでformat()
を用いています。>>
演算子向けにprintf()
と同じ構文を利用できるようにしたヘルパークラスですが、引数の数が4つまでに制限されています。(Serial.printfmt()
には引数の数の制限がありません。)
3行目の "DI:%04b"
は"DI:0010"のようにDI1..DI4のビットマップを4桁で表示します。
5行目の"/%04d"
は"/3280/0010/0512/1023/1023"のように Vcc/AI1..AI4の値を順に整数で出力します。
7行目のmwx::crlf
は改行文字列を出力します。
信号の出力
これで必要なデータの展開が終わったので、あとはボード上のDO1..DO4とPWM1..PWM4の値を変更します。
// set local DO
digitalWrite(BRD_APPTWELITE::PIN_DO1, (u8DI_BM_remote & 1) ? HIGH : LOW);
digitalWrite(BRD_APPTWELITE::PIN_DO2, (u8DI_BM_remote & 2) ? HIGH : LOW);
digitalWrite(BRD_APPTWELITE::PIN_DO3, (u8DI_BM_remote & 4) ? HIGH : LOW);
digitalWrite(BRD_APPTWELITE::PIN_DO4, (u8DI_BM_remote & 8) ? HIGH : LOW);
// set local PWM : duty is set 0..1024, so 1023 is set 1024.
Timer1.change_duty(au16AI_remote[1] == 1023 ? 1024 : au16AI_remote[1]);
Timer2.change_duty(au16AI_remote[2] == 1023 ? 1024 : au16AI_remote[2]);
Timer3.change_duty(au16AI_remote[3] == 1023 ? 1024 : au16AI_remote[3]);
Timer4.change_duty(au16AI_remote[4] == 1023 ? 1024 : au16AI_remote[4]);
digitalWrite()
はディジタル出力の値を変更します。1番目のパラメータはピン番号で、2番目はHIGH(VCCレベル)かLOW(GNDレベル)を指定します。
Timer?.change_duty()
はPWM出力のデューティ比を変更します。パラメータにデューティ比 0..1024
を指定します。最大値が1023
でないことに注意してください。ライブラリ内で実行される割り算のコストが大きいため、2のべき乗である1024
を最大値としています。0
にするとGNDレベル、1024
にするとVCCレベル相当の出力になります。
1.7 - BRD_I2C_TEMPHUMID
このサンプルでは、当社の 環境センサーパル AMBIENT SENSE PAL あるいは TWELITE ARIA BLUE / RED に搭載の I2C センサーデバイスを利用しています。しかし、I2Cコマンド送受信部分を書き換えることで、その他の一般的な I2C センサーデバイス(図中 Generic I2C Sensor Module
) を利用することもできます。その場合には、以下のように配線してください。
このアクトには以下が含まれます。
- 無線パケットの送受信
- インタラクティブモードによる設定 -
<STG_STD>
- ステートマシンによる状態遷移制御 -
<SM_SIMPLE>
アクトの機能
- I2C デバイスのコマンド送受信を行います。
- コイン電池で動作させるための、スリープ機能を利用します。
アクトの使い方
必要なTWELITE
役割 | 例 |
---|---|
親機 | MONOSTICK BLUE / REDアクトParent_MONOSTICKを動作させる。 |
子機 | - BLUE / RED PAL + 環境センサーパル-AMBIENT SENSE PAL- TWELITE ARIA BLUE / RED |
アクトの解説
インクルード
#include <TWELITE>
#include <NWK_SIMPLE>// ネットワークサポート
#include <STG_STD> // インタラクティブモード
#include <SM_SIMPLE> // 簡易ステートマシン
無線送受信に必要な <NWK_SIMPLE>
、インタラクティブモードを追加するための <STG_STD>
、アプリケーションループの記述を簡素化するための <SM_SIMPLE>
をインクルードしています。
センサードライバ
この例では SHTC3 (TWELITE AMB PAL) と、SHT40 (TWELITE ARIA) の2種類のコードがあり #ifdef
により切り替えています(USE_SHTC3
またはUSE_SHT40
のどちらかを #define
してください)。コードの移植性のため2種類は同じ関数インタフェースとして定義しています。2種類のコードは同メーカ、同系列のセンサーであるため似通っています。
/*** sensor select, define either of USE_SHTC3 or USE_SHT40 */
// use SHTC3 (TWELITE PAL)
#define USE_SHTC3
// use SHT40 (TWELITE ARIA)
#undef USE_SHT40
以下では SHTC3 の例を示します。
#if defined(USE_SHTC3)
// for SHTC3
struct SHTC3 {
uint8_t I2C_ADDR;
uint8_t CONV_TIME;
bool setup() { ... }
bool begin() { ... }
int get_convtime() { return CONV_TIME; }
bool read(int16_t &i16Temp, int16_t &i16Humd) { ... }
} sensor_device;
ここではソースコードを整理するため I2C センサー関連の手続きを構造体(クラス) SHTC3 にまとめています。この構造体には I2C アドレス I2C_ADDR
と、値取得のための待ち時間 CONV_TIME
をメンバー変数として持っており、sensor_device
という実体名で宣言しています。
この構造体(クラス)は以下のメンバー関数を持っています。
関数名 | 解説 |
---|---|
setup() | 構造体の初期化を行う。 |
begin() | センサー値の取得を開始する。開始後、適切なセンサー値が得られるまで一定時間待つ必要がある。 |
get_convtime() | センサー値の取得待ち時間を返す。 |
read(int&, int&) | センサー値を取得する。 |
setup()
を呼び出します。処理を一つ一つ見ていきます。
setup()
bool setup() {
// here, initialize some member vars instead of constructor.
I2C_ADDR = 0x70;
CONV_TIME = 10; // wait time [ms]
return true;
}
メンバー変数に I2C アドレスと、センサー値取得待ち時間(上記は10ms)を設定します。
これらの値は原則として固定値ですので変数設定する必要はありません。変数として扱う有効な例として、設定によってより高精度なセンサー稼働をさせるような場合に必要な変換時間を管理する、設定によって I2C の副アドレスを選択するような場合などが考えられます。
begin()
bool begin() {
// send start trigger command
if (auto&& wrt = Wire.get_writer(I2C_ADDR)) {
wrt << 0x60; // SHTC3_TRIG_H
wrt << 0x9C; // SHTC3_TRIG_L
} else {
return false;
}
return true;
}
センサーを動作させるために指令を書き込みます。
MWXライブラリでは、Wire
クラスオブジェクトを用いたI2Cバスへの読み書きに2種類の異なった記述方法がありますが、こちらはヘルパー関数を用いる方法です。
if
文中で Wire.get_writer(I2C_ADDR)
は、アドレスI2C_ADDR
に対応するI2Cデバイスを開き、その読み書き用のオブジェクトを生成します。読み書きオブジェクト wrt
は if
節の (bool)
評価により、デバイスのオープンに失敗したときなどには false
を返します。true
が戻った時は無事にオープンできたことになり if
節内の処理を行います。
ここでは wrt << 0x60;
のように、ストリーム演算子 <<
を用いて1バイト I2C デバイスに書き込んでいます。このストリーム演算子は原則 uint8_t
型の1バイトを書き込むためのものです。
get_convtime()
int get_convtime() {
return CONV_TIME;
}
CONV_TIME
の値を返すための関数です。
read()
bool read(int16_t &i16Temp, int16_t &i16Humd) {
// read result
uint16_t u16temp, u16humd;
uint8_t u8temp_csum, u8humd_csum;
if (auto&& rdr = Wire.get_reader(I2C_ADDR, 6)) {
rdr >> u16temp; // read two bytes (MSB first)
rdr >> u8temp_csum; // check sum (crc8)
rdr >> u16humd; // read two bytes (MSB first)
rdr >> u8humd_csum; // check sum (crc8)
} else {
return false;
}
// check CRC and save the values
if ( (CRC8_u8CalcU16(u16temp, 0xff) == u8temp_csum)
&& (CRC8_u8CalcU16(u16humd, 0xff) == u8humd_csum))
{
i16Temp = (int16_t)(-4500 + ((17500 * int32_t(u16temp)) >> 16));
i16Humd = (int16_t)((int32_t(u16humd) * 10000) >> 16);
} else {
return false;
}
return true;
}
センサーデータを読み出します。
SHTC3では、begin()
によりセンサー読み出しを開始してから、数ms待ち時間をおいてセンサー値を読み出します。 センサー値の並びは以下のようになっています。
バイト | 解説 |
---|---|
0 | 温度センサー値(上位バイト) |
1 | 温度センサー値(下位バイト) |
2 | バイト0,1のCRC8値 |
3 | 湿度センサー値(上位バイト) |
4 | 湿度センサー値(下位バイト) |
5 | バイト3,4のCRC8値 |
begin()
で書き込んだ0x609C
コマンドで開始した場合は、温度データが先に到着します。begin()
ではデータを書き出していましたが、ここではデータを読み込みます。データを読み込むには同様に Wire.get_reader()
により、ヘルパーオブジェクト rdr
を生成します。エラーがなければ rdr
は if
節中で true
を返します。ここで get_reader(I2C_ADDR, 6)
の2番目に与えたパラメータ 6
は、読み出しバイト数です。このバイト数を読みだした時点で I2C バスの読出しを終了する手続きを行います。(デバイスによっては、こういった手続きを省略しても動作するものもありますが、通常は適切な値を与えるようにしてください)
読み出しはストリーム演算子 >>
により行っています。読み出し方法にはほかにもいくつかあります。詳しくは ヘルパー関数 を参照してください。ストリーム演算子を用いる場合は、事前に宣言した uint8_t
, uint16_t
, uint32_t
型の変数に値を入力します。rdr >> u16temp
は、uint16_t
型の変数に対して2バイトI2Cバスから読み出し**ビッグエンディアン形式(1バイト目は上位バイト)**で格納します。
最終的に i16Temp ・ i16Humd
に温度[℃]の100倍値、および湿度[%]の100倍値を計算して格納しています。計算式について I2C デバイスのデータシートを参照してください。
setup()
setup()
関数は TWELITE が始動したときに1度だけ呼び出される関数です。この関数では、各種初期化を行います。
void setup() {
/*** SETUP section */
...
}
ステートマシン SM_SIMPLE
の初期化
// application state defs
enum class STATE : uint8_t {
INTERACTIVE = 255,
INIT = 0,
SENSOR,
TX,
TX_WAIT_COMP,
GO_SLEEP
};
// simple state machine.
SM_SIMPLE<STATE> step;
void setup() {
...
/// init vars or objects
step.setup(); // initialize state machine
...
}
ステートマシン(状態遷移マシン)は、都度呼び出される loop()
文中の記述を簡素化するために用います。もちろん、アプリケーションの記述を行うのに、この例で使用する SM_SIMPLE
を使用しなくても構いません。
SM_SIMPLE
は、ごく短いコードで実装されており、状態への遷移と、タイムアウトの管理、フラグの管理を簡易的に行えます。状態はあらかじめ列挙体で定義しておきます。上記の例では enum class STATE
です。ステートマシンの実体は定義済みの列挙体 STATE
をパラメータとして SM_SMPLE<STATE> step
のように宣言します。
ビヘイビアの登録
void setup() {
...
/// load board and settings objects
auto&& set = the_twelite.settings.use<STG_STD>(); // load save/load settings(interactive mode) support
auto&& nwk = the_twelite.network.use<NWK_SIMPLE>(); // load network support
...
}
ビヘイビアは、プログラム中で利用する機能のまとまりです。各種イベントが発生したときの振る舞いが記述されています。
ここでは、インタラクティブモード画面 <STG_STD>
と、シンプル中継ネットワーク <NWK_SMPLE>
の2種類のビヘイビアを利用します。
インタラクティブモードの設定 STG_STD
...
/// configure settings
// configure settings
set << SETTINGS::appname(FOURCHARS);
set << SETTINGS::appid_default(DEFAULT_APP_ID); // set default appID
set << SETTINGS::ch_default(DEFAULT_CHANNEL); // set default channel
set.hide_items(E_STGSTD_SETID::OPT_DWORD2, E_STGSTD_SETID::OPT_DWORD3, E_STGSTD_SETID::OPT_DWORD4, E_STGSTD_SETID::ENC_KEY_STRING, E_STGSTD_SETID::ENC_MODE);
記述するアプリケーションに合わせたインタラクティブモードの設定項目にするため、 STG_STD に対して初期設定を行います。
SETTINGS::appname
: アプリケーション名(文字列)を指定します。インタラクティブモード画面上で先頭行に表示されます。画面上の文字数には余裕がないので最小限の文字列にします。SETTINGS::appid_default
: アプリケーションIDの規定値です。独自のアプリケーションで独自の規定アプリケーションIDを持たせたい場合に実行します。SETTINGS::ch_default
: チャネルの規定値です。独自のアプリケーションで既定のチャネルを持たせたい場合に実行します。
続いて set.hide_items()
では、既定のインタラクティブモードの画面上で不要な設定項目を削除しています。すべて表示しても構わない場合は、この呼び出しを行う必要はありません。
// if SET(DIO12)=LOW is detected, start with intaractive mode.
if (digitalRead(PIN_DIGITAL::DIO12) == PIN_STATE::LOW) {
set << SETTINGS::open_at_start();
step.next(STATE::INTERACTIVE);
return;
}
DIO12 のピンが LOW (GNDレベル) で、電源投入またはリセットされた場合は、インタラクティブモードで起動する記述です。digitalRead()
でピンの状態を読み取り、SETTINGS::open_at_start()
を反映させます。
インタラクティブモード中に通常のアプリケーション処理が行われてしまうと不都合であるため、ステートマシンの状態を STATE::INTERACTIVE
に設定します。この状態では、一切の入力等の処理を行わず同じ状態にとどまります。
// load values
set.reload(); // load from EEPROM.
OPT_BITS = set.u32opt1(); // this value is not used in this example.
// LID is configured DIP or settings.
LID = set.u8devid(); // 2nd is setting.
if (LID == 0) LID = 0xFE; // if still 0, set 0xFE (anonymous child)
最後にインタラクティブモードのデータを読み込みます。set.reload()
を呼び出すことで、EEPROM に書き込まれたデータを読み込みます。設定が行われず EEPROM に何も情報がない場合は、規定値として読みだせます。
ここではオプションビット set.u32opt1()
と、8ビットの論理ID set.u8devid()
を読み出します。LID が 0
の場合は、通常親機として運用されるため、この値が記録されている場合は 0xFE
(IDを割り振らない子機) としています。
/// configure system basics
the_twelite << set; // apply settings (from interactive mode)
nwk << set; // apply settings (from interactive mode)
nwk << NWK_SIMPLE::logical_id(LID); // set LID again (LID can also be configured by DIP-SW.)
...
最後に the_twelite
と nwk
に設定情報(の一部)を反映させています。アプリケーションIDやチャネルといった無線通信に必須の情報が反映されます。上記ではこれらの設定に対する明示的な読み出しコードは存在しませんが set.reload()
で、設定がなければ規定値に、あれば設定値が読み出されます。
ペリフェラルの初期化
/*** BEGIN section */
Wire.begin(); // start two wire serial bus.
I2C センサーの初期化設定を行っています。
MWX の開始
// let the TWELITE begin!
the_twelite.begin();
/*** INIT message */
Serial << "--- TEMP&HUMID:" << FOURCHARS << " ---" << mwx::crlf;
Serial << format("-- app:x%08x/ch:%d/lid:%d"
, the_twelite.get_appid()
, the_twelite.get_channel()
, nwk.get_config().u8Lid
)
<< mwx::crlf;
Serial << format("-- pw:%d/retry:%d/opt:x%08x"
, the_twelite.get_tx_power()
, nwk.get_config().u8RetryDefault
, OPT_BITS
)
<< mwx::crlf;
the_twelite.begin()
は MWX ライブラリの初期化完了を宣言する手続きです。この処理を行わないと、MWX ライブラリは適切に動作しません。
起動時のメッセージなどもここで表示します。
loop()
void loop() {
do {
switch (step.state()) {
// 各状態の振る舞い
case STATE::INIT:
...
break;
...
}
while(step.b_more_loop());
}
loop()
は、SM_SIMPLE
ステートマシンstep
を用いた制御を行っています。スリープ復帰からセンサー値取得、無線パケット送信、送信完了待ち、スリープといった一連の流れを簡潔に表現するためです。
上記の do while
文の制御構造を記述しておきます。ステート(状態)は step.state()
で判定します。while
の条件式は step.b_more_loop()
となっています。これは、ある状態からさらに別の状態に遷移したときに、loop()
を抜けずに連続的に処理したい場合があるためです。つまり、別の状態に遷移して switch
節を抜けた場合、次の状態の case
節が呼び出されます。この動作には注意してください。
case STATE::INTERACTIVE:
インタラクティブモード中にメインループが動作するのは都合が悪いため、この状態に固定します。
case STATE::INIT:
// start sensor capture
sensor_device.begin();
step.set_timeout(sensor_device.get_convtime()); // set timeout
step.next(STATE::SENSOR);
センサーのデータ取得を開始します。set_timeout()
で、センサー取得の時間待ちを行います。
時間待ちの非常に長いセンサーなどは、ここでいったんスリープを行うといった処理を記述すると電池寿命を延ばすことができますが、構造が複雑になるためこの例では割愛します。必要な場合はスリープ待ちの例を参照してください。
case STATE::SENSOR:
if (step.is_timeout()) {
// the sensor data should be ready (wait some)
sensor_device.read(sensor.i16temp, sensor.i16humid);
Serial << "..finish sensor capture." << mwx::crlf
<< " : temp=" << div100(sensor.i16temp) << 'C' << mwx::crlf
<< " humd=" << div100(sensor.i16humid) << '%' << mwx::crlf
;
Serial.flush();
step.next(STATE::TX);
}
センサーの値を sensor_device.read()
により取得して sensor
構造体に値を格納します。
最初に step.is_timeout()
によるタイムアウトチェックを行います。タイムアウトの起点は先ほどの step.set_timeout()
です。タイムアウトしない場合は、if
節は実行されず、そのまま loop()
を抜けます。次のハードウェアイベント(多くの場合はシステムタイマーである1ms ごとに割り込みを発生するTickTimerの割り込み)が来るまではTWELITE マイコンは低電力でCPUが待機するDOZE(ドーズ)モードになります。
無線センサーとしてセンサー側の TWELITE のシリアルポートに結果を出力する必要はありませんが、動作確認を容易にするためシリアルポートに取得値を表示しています。ここで Serial.flush()
を行い出力待ちを行っていますが、これは TWELITE がスリープするまでにシリアルポート出力が終わらないことを想定した記述です。この処理も、電池消耗の原因になるためSerial.flush()
を行わないようにするか、シリアルポートへの出力をしないようにします。
ここで使用する div100()
は、低コストで100の除算を行う関数です。TWELITE には除算回路がありませんので、除算処理は極力行わないことを推奨します。
case STATE::TX:
step.next(STATE::GO_SLEEP); // set default next state (for error handling.)
// get new packet instance.
if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
...
}
通信手続きを記述します。この状態で待ち処理などを行うことはなく、処理を実行したら速やかに次の状態に遷移します。あらかじめ step.next(STATE::GO_SLEEP)
と記述しているのは、エラーなどの検出は複数個所で行われるため、すべての場所で同じ記述を行うことを避けるためです。
if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet())
では、送信パケットのオブジェクトを生成し、オブジェクトの生成に成功したら if
節を実行するという処理です。
// 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 (0x1 send two times in total)
<< tx_packet_delay(0, 0, 2); // send packet w/ delay
最初に送信の設定を行います。宛先 tx_addr(0x00)
を親機 0x00
に設定し、再送回数 tx_retry(0x1)
を1回にし、パケットの遅延の設定 tx_packet_delay(0, 0, 2)
を初回送信までの遅延は 0、再送間隔を 2ms と設定しています。
pack_bytes(pkt.get_payload()
, make_pair(FOURCHARS, 4)
, uint16_t(sensor.i16temp)
, uint16_t(sensor.i16humid)
);
パケットのペイロード部に識別子のFOURCHARS
とセンサーデータを格納します。得られた値のうち温度値は int16_t
ですが、送信パケットのデータ構造は符号なしで格納するため、uint16_t
にキャストしています。
// do transmit
MWX_APIRET ret = pkt.transmit();
if (ret) {
step.clear_flag(); // waiting for flag is set.
step.set_timeout(100); // set timeout
step.next(STATE::TX_WAIT_COMP);}
pkt.transmit()
を呼び送信要求を行います。この時点ではまだ送信処理は始らず、送信要求を内部のキューに設定しただけです。MWXライブラリ内で適切なタイミングで送信要求が処理されます。
送信要求に成功した場合 ret
は true
になります。完了を判定するためのフラグの初期化 step.clear_flag()
、送信失敗時など予期しないエラーを処理するためのタイムアウト step.set_timeout(100)
を設定し、次の状態を STATE::TX_WAIT_COMP
にします(STATE::GO_SLEEP
の指定は上書きされます)。
case STATE::TX_WAIT_COMP:
ここでは送信の完了待ちを行います。タイムアウトの判定(エラー時)または送信完了イベントの判定を行います。
if (step.is_timeout()) { // maybe fatal error.
the_twelite.reset_system();
}
if (step.is_flag_ready()) { // when tx is performed
Serial << "..transmit complete." << mwx::crlf;
Serial.flush();
step.next(STATE::GO_SLEEP);
}
STATE::GO_SLEEP:
sleepNow()
の処理を行います。この関数を呼び出すことで TWELITE はスリープ状態になります。
on_tx_comp()
void on_tx_comp(mwx::packet_ev_tx& ev, bool_t &b_handled) {
step.set_flag(ev.bStatus);
}
送信完了時に呼び出されるシステムイベントです。ここでは.set_flag()
を呼び出しstep
のフラグをセットします。
sleepNow()
void sleepNow() {
step.on_sleep(false); // reset state machine.
// randomize sleep duration.
uint32_t u32ct = 1750 + random(0,500);
// output message
Serial << "..sleeping " << int(u32ct) << "ms." << mwx::crlf;
Serial.flush(); // wait until all message printed.
// do sleep.
the_twelite.sleep(u32ct);
}
スリープ前に.on_sleep(false)
によりステートマシンの状態を初期化します。パラメータのfalse
はスリープ復帰後STATE::INIT(=0)
から始めます。
ここでは、起床までの時間を乱数により 1750ms から 2250ms の間に設定しています。これにより他の同じような周期で送信するデバイスのパケットとの連続的な衝突を避けます。
8,9行目、この例ではシリアルポートからの出力を待ってスリープに入ります。通常は消費エネルギーを最小化したいため、スリープ前のシリアルポートの出力は最小限(または無し)にします。
12行目、スリープに入るには the_twelite.sleep()
を呼びます。この呼び出しの中で、ボード上のハードウェアのスリープ前の手続きなどが行われます。パラメータとしてスリープ時間をmsで指定しています。
60000
を超えないように指定してください。wakeup()
void wakeup() {
Serial << mwx::crlf
<< "--- PAL_AMB:" << FOURCHARS << " wake up ---"
<< mwx::crlf
<< "..start sensor capture again."
<< mwx::crlf;
...
}
スリープから復帰し起床すると wakeup()
が呼び出されます。そのあとloop()
が都度呼び出されます。wakeup()
の前に、UARTなどの各ペリフェラルやボード上のデバイスのウェイクアップ処理が行われます。例えばLEDの点灯制御を再始動します。
1.8 - BRD_ARIA
このアクトには以下が含まれます。
- 無線パケットの送受信
- インタラクティブモードによる設定 -
<STG_STD>
- ステートマシンによる状態遷移制御 -
<SM_SIMPLE>
<ARIA>
ボードビヘイビアによるボード操作
アクトの機能
- TWELITE ARIA - トワイライトアリア を用い、センサー値の取得を行います。
- コイン電池で動作させるための、スリープ機能を利用します。
アクトの使い方
必要なTWELITE
役割 | 例 |
---|---|
親機 | MONOSTICK BLUE / REDアクトParent_MONOSTICKを動作させる。 |
子機 | TWELITE ARIA BLUE / RED |
アクトの解説
インクルード
#include <TWELITE>
#include <NWK_SIMPLE>// ネットワークサポート
#include <ARIA> // TWELITE ARIA
#include <STG_STD> // インタラクティブモード
#include <SM_SIMPLE> // 簡易ステートマシン
TWELITE ARIA<ARIA>
のボードビヘイビアをインクルードします。
setup()
void setup(){
/*** SETUP section */
/// init vars or objects
step.setup(); // initialize state machine
/// load board and settings objects
auto&& brd = the_twelite.board.use<ARIA>(); // load board support
auto&& set = the_twelite.settings.use<STG_STD>(); // load save/load settings(interactive mode) support
auto&& nwk = the_twelite.network.use<NWK_SIMPLE>(); // load network support
/// configure settings
// configure settings
set << SETTINGS::appname("ARIA");
set << SETTINGS::appid_default(DEFAULT_APP_ID); // set default appID
set << SETTINGS::ch_default(DEFAULT_CHANNEL); // set default channel
set.hide_items(E_STGSTD_SETID::OPT_DWORD2, E_STGSTD_SETID::OPT_DWORD3, E_STGSTD_SETID::OPT_DWORD4, E_STGSTD_SETID::ENC_KEY_STRING, E_STGSTD_SETID::ENC_MODE);
// if SET=LOW is detected, start with intaractive mode.
if (digitalRead(brd.PIN_SET) == PIN_STATE::LOW) {
set << SETTINGS::open_at_start();
step.next(STATE::INTERACTIVE);
return;
}
// load values
set.reload(); // load from EEPROM.
OPT_BITS = set.u32opt1(); // this value is not used in this example.
LID = set.u8devid(); // set logical ID
/// configure system basics
the_twelite << set; // apply settings (from interactive mode)
/// configure network
nwk << set; // apply settings (from interactive mode)
nwk << NWK_SIMPLE::logical_id(LID); // set LID again (LID can also be configured by DIP-SW.)
/// configure hardware
// LED setup (use periph_led_timer, which will re-start on wakeup() automatically)
brd.set_led(LED_TIMER::BLINK, 10); // blink (on 10ms/ off 10ms)
// let the TWELITE begin!
the_twelite.begin();
/*** INIT message */
Serial << "--- ARIA:" << FOURCHARS << " ---" << mwx::crlf;
Serial << format("-- app:x%08x/ch:%d/lid:%d"
, the_twelite.get_appid()
, the_twelite.get_channel()
, nwk.get_config().u8Lid
)
<< mwx::crlf;
Serial << format("-- pw:%d/retry:%d/opt:x%08x"
, the_twelite.get_tx_power()
, nwk.get_config().u8RetryDefault
, OPT_BITS
)
<< mwx::crlf;
}
最初に変数などの初期化を行います。ここではステートマシンstep
の初期化を行っています。
最初にボードサポート <ARIA>
を登録します。ボードサポートの初期化時にセンサーやDIOの初期化が行われます。
auto&& brd = the_twelite.board.use<ARIA>();
つづいて、インタラクティブモード関連の初期化と読出しを行います。
// configure settings
set << SETTINGS::appname("ARIA");
set << SETTINGS::appid_default(DEFAULT_APP_ID); // set default appID
set << SETTINGS::ch_default(DEFAULT_CHANNEL); // set default channel
set.hide_items(E_STGSTD_SETID::OPT_DWORD2, E_STGSTD_SETID::OPT_DWORD3, E_STGSTD_SETID::OPT_DWORD4, E_STGSTD_SETID::ENC_KEY_STRING, E_STGSTD_SETID::ENC_MODE);
// if SET=LOW is detected, start with intaractive mode.
if (digitalRead(brd.PIN_SET) == PIN_STATE::LOW) {
set << SETTINGS::open_at_start();
step.next(STATE::INTERACTIVE);
return;
}
// load values
set.reload(); // load from EEPROM.
OPT_BITS = set.u32opt1(); // this value is not used in this example.
LID = set.u8devid(); // set logical ID
ここではset
オブジェクトの取得、アプリ名の反映、デフォルトのアプリケーションIDと通信チャネルの反映、設定メニューで不要項目の削除を行います。
次にSETピンの状態を読み出します。このサンプルはスリープによる間欠動作を行うため、+++
入力によるインタラクティブモード遷移は出来ません。替わりに起動時のSETピン=LO状態でインタラクティブモードに遷移します。このときSETTINGS::open_at_start()
を指定していますが、これはsetup()
を終了後速やかにインタラクティブモード画面に遷移する指定です。
最後に.reload()
を実行して設定値をEEPROMから読み出します。設定値を各変数にコピーしています。
このアクトではもっぱら無線パケットを送信しますので、TWENET の設定では動作中に受信回路をオープンにする指定(TWENET::rx_when_idle()
)は含めません。
the_twelite << set; // apply settings (from interactive mode)
続いてLEDの設定を行います。ここでは 10ms おきに ON/OFF の点滅の設定をします(スリープを行い起床時間が短いアプリケーションでは、起床中は点灯するという設定とほぼ同じ意味合いになります)。
brd.set_led(LED_TIMER::BLINK, 10); // blink (on 10ms/ off 10ms)
loop()
void loop() {
auto&& brd = the_twelite.board.use<ARIA>();
do {
switch (step.state()) {
// 各状態の振る舞い
case STATE::INIT:
...
break;
...
}
while(step.b_more_loop());
}
loop()
は、SM_SIMPLE
ステートマシンstep
を用いた制御を行っています。スリープ復帰からセンサー値取得、無線パケット送信、送信完了待ち、スリープといった一連の流れを簡潔に表現するためです。ループの戦闘ではbrd
オブジェクトを取得しています。
case STATE::INTERACTIVE:
インタラクティブモード中にメインループが動作するのは都合が悪いため、この状態に固定します。
case STATE::INIT:
brd.sns_SHT4x.begin();
step.next(STATE::SENSOR);
センサーのデータ取得を開始します。
case STATE::SENSOR:
// wait until sensor capture finish
if (!brd.sns_SHT4x.available()) {
brd.sns_SHT4x.process_ev(E_EVENT_TICK_TIMER);
}else{ // now sensor data is ready.
sensor.i16temp = brd.sns_SHT4x.get_temp_cent();
sensor.i16humid = brd.sns_SHT4x.get_humid_per_dmil();
// read magnet sensor
sensor.b_north = digitalRead(ARIA::PIN_SNS_NORTH);
sensor.b_south = digitalRead(ARIA::PIN_SNS_SOUTH);
Serial << "..finish sensor capture." << mwx::crlf
<< " MAGnet : north=" << int(sensor.b_north) << mwx::crlf
<< " south=" << int(sensor.b_south) << mwx::crlf
<< " SHT4x : temp=" << div100(sensor.i16temp) << 'C' << mwx::crlf
<< " humd=" << div100(sensor.i16humid) << '%' << mwx::crlf
;
Serial.flush();
step.next(STATE::TX);
}
ボード上のセンサーは .sns_SHT4x
という名前でアクセスでき、このオブジェクトに操作を行います。センサーの完了待ちを行います。まだセンサーの取得が終わっていない場合(.available()
がfalse
)はセンサーに対して時間経過のイベント(.process_ev(E_EVENT_TICK_TIMER)
)を送付します。
センサーがavailableになった時点で、センサー値を取得し、STATE_TX
に遷移します。
温湿度センサーは以下のように取得できます。
.get_temp_cent()
:int16_t
: 1℃を100とした温度 (25.6 ℃なら 2560).get_temp()
:float
: float値 (25.6 ℃なら 25.6).get_humid_dmil()
:int16_t
: 1%を100とした湿度 (56.8%なら 5680).get_temp()
:float
: float値 (56.8%なら 56.8)
case STATE::TX:
送信手続きについては他のアクトのサンプルと同様です。ここでは、再送1回、再送遅延を最小にする設定になっています。
pkt << tx_addr(0x00) // 親機0x00宛
<< tx_retry(0x1) // リトライ1回
<< tx_packet_delay(0, 0, 2); // 遅延は最小限
パケットのペイロード部に識別子のFOURCHARS
とセンサーデータを格納します。得られた値のうち温度値は int16_t
ですが、送信パケットのデータ構造は符号なしで格納するため、uint16_t
にキャストしています。
pack_bytes(pkt.get_payload() // set payload data objects.
, make_pair(FOURCHARS, 4) // just to see packet identification, you can design in any.
, uint8_t(sensor.b_north)
, uint8_t(sensor.b_south)
, uint16_t(sensor.i16temp)
, uint16_t(sensor.i16humid)
);
送信要求を行います。送信要求が成功したら送信完了街の準備を行います。完了イベントを待つために.clear_flag()
、万が一のときのタイムアウトをset_timeout(100)
を指定します。パラメータの100
の単位はミリ秒[ms]です。
// do transmit
MWX_APIRET ret = pkt.transmit();
if (ret) {
step.clear_flag(); // waiting for flag is set.
step.set_timeout(100); // set timeout
step.next(STATE::TX_WAIT_COMP);
}
case STATE::TX_WAIT_COMP:
ここではタイムアウトの判定、送信完了イベントの判定を行います。
if (step.is_timeout()) { // maybe fatal error.
the_twelite.reset_system();
}
if (step.is_flag_ready()) { // when tx is performed
Serial << "..transmit complete." << mwx::crlf;
Serial.flush();
step.next(STATE::GO_SLEEP);
}
STATE::GO_SLEEP:
sleepNow()
の処理を行います。
on_tx_comp()
void on_tx_comp(mwx::packet_ev_tx& ev, bool_t &b_handled) {
step.set_flag(ev.bStatus);
}
送信完了時に呼び出されるシステムイベントです。ここでは.set_flag()
により完了としています。
sleepNow()
スリープに入る手続きをまとめています。
void sleepNow() {
step.on_sleep(false); // reset state machine.
// randomize sleep duration.
uint32_t u32ct = 1750 + random(0,500);
// set an interrupt for MAGnet sensor.
pinMode(ARIA::PIN_SNS_OUT1, PIN_MODE::WAKE_FALLING);
pinMode(ARIA::PIN_SNS_OUT2, PIN_MODE::WAKE_FALLING);
// output message
Serial << "..sleeping " << int(u32ct) << "ms." << mwx::crlf;
Serial.flush(); // wait until all message printed.
// do sleep.
the_twelite.sleep(u32ct);
}
スリープ前に.on_sleep(false)
によりステートマシンの状態を初期化します。パラメータのfalse
はスリープ復帰後STATE::INIT(=0)
から始めます。
ここでは、起床までの時間を乱数により 1750ms から 2250ms の間に設定しています。これにより他の同じような周期で送信するデバイスのパケットとの連続的な衝突を避けます。
8,9行目では、スリープに入るまえに磁気センサーのDIOピンの割り込み設定をします。pinMode()
を用います。2番めのパラメータはPIN_MODE::WAKE_FALLING
を指定しています。これはHIGHからLOWへピンの状態が変化したときに起床する設定です。
12,13行目、この例ではシリアルポートからの出力を待ってスリープに入ります。通常は消費エネルギーを最小化したいため、スリープ前のシリアルポートの出力は最小限(または無し)にします。
16行目、スリープに入るには the_twelite.sleep()
を呼びます。この呼び出しの中で、ボード上のハードウェアのスリープ前の手続きなどが行われます。たとえばLEDは消灯します。
パラメータとしてスリープ時間をmsで指定しています。
wakeup()
スリープから復帰し起床すると wakeup()
が呼び出されます。そのあとloop()
が都度呼び出されます。wakeup()
の前に、UARTなどの各ペリフェラルやボード上のデバイスのウェイクアップ処理が行われます。例えばLEDの点灯制御を再始動します。
void wakeup() {
Serial << mwx::crlf
<< "--- ARIA:" << FOURCHARS << " wake up ";
if (the_twelite.is_wokeup_by_wktimer()) {
Serial << "(WakeTimer) ---";
} else
if (the_twelite.is_wokeup_by_dio(ARIA::PIN_SNS_NORTH)) {
Serial << "(MAGnet INT [N]) ---";
} else
if (the_twelite.is_wokeup_by_dio(ARIA::PIN_SNS_SOUTH)) {
Serial << "(MAGnet INT [S]) ---";
} else {
Serial << "(unknown source) ---";
}
Serial << mwx::crlf
<< "..start sensor capture again."
<< mwx::crlf;
}
1.9 - PAL_AMB
このアクトには以下が含まれます。
- 無線パケットの送受信
- インタラクティブモードによる設定 -
<STG_STD>
- ステートマシンによる状態遷移制御 -
<SM_SIMPLE>
<PAL_AMB>
ボードビヘイビアによるボード操作
アクトの機能
- 環境センサーパル AMPIENT SENSE PAL を用い、センサー値の取得を行います。
- コイン電池で動作させるための、スリープ機能を利用します。
アクトの使い方
必要なTWELITE
役割 | 例 |
---|---|
親機 | MONOSTICK BLUE / REDアクトParent_MONOSTICKを動作させる。 |
子機 | BLUE / RED PAL + 環境センサーパル-AMBIENT SENSE PAL |
アクトの解説
インクルード
#include <TWELITE>
#include <NWK_SIMPLE>// ネットワークサポート
#include <PAL_AMB> // PAL_AMB
#include <STG_STD> // インタラクティブモード
#include <SM_SIMPLE> // 簡易ステートマシン
環境センサーパル <PAL_AMB>
のボードビヘイビアをインクルードします。
setup()
void setup() {
/*** SETUP section */
step.setup(); // ステートマシンの初期化
// PAL_AMBのボードビヘイビアを読み込む
auto&& brd = the_twelite.board.use<PAL_AMB>();
// インタラクティブモードを読み込む
auto&& set = the_twelite.settings.use<STG_STD>();
set << SETTINGS::appname(FOURCHARS);
set << SETTINGS::appid_default(APP_ID); // set default appID
set.hide_items(E_STGSTD_SETID::POWER_N_RETRY, E_STGSTD_SETID::OPT_DWORD2, E_STGSTD_SETID::OPT_DWORD3, E_STGSTD_SETID::OPT_DWORD4, E_STGSTD_SETID::ENC_KEY_STRING, E_STGSTD_SETID::ENC_MODE);
// SET ピンを検出した場合は、インタラクティブモードを起動する
if (digitalRead(brd.PIN_BTN) == PIN_STATE::LOW) {
set << SETTINGS::open_at_start();
step.next(STATE::INTERACTIVE);
return;
}
// インタラクティブモードのデータを読み出す
set.reload();
APP_ID = set.u32appid();
CHANNEL = set.u8ch();
OPT_BITS = set.u32opt1();
// DIPスイッチとインタラクティブモードの設定からLIDを決める
LID = (brd.get_DIPSW_BM() & 0x07); // 1st priority is DIP SW
if (LID == 0) LID = set.u8devid(); // 2nd is setting.
if (LID == 0) LID = 0xFE; // if still 0, set 0xFE (anonymous child)
// LED初期化
brd.set_led(LED_TIMER::BLINK, 10); // blink (on 10ms/ off 10ms)
// the twelite main object.
the_twelite
<< TWENET::appid(APP_ID) // set application ID (identify network group)
<< TWENET::channel(CHANNEL); // set channel (pysical channel)
// Register Network
auto&& nwk = the_twelite.network.use<NWK_SIMPLE>();
nwk << NWK_SIMPLE::logical_id(u8ID); // set Logical ID. (0xFE means a child device with no ID)
/*** BEGIN section */
Wire.begin(); // start two wire serial bus.
Analogue.begin(pack_bits(PIN_ANALOGUE::A1, PIN_ANALOGUE::VCC)); // _start continuous adc capture.
the_twelite.begin(); // start twelite!
startSensorCapture(); // start sensor capture!
/*** INIT message */
Serial << "--- PAL_AMB:" << FOURCHARS << " ---" << mwx::crlf;
}
最初に変数などの初期化を行います。ここではステートマシンstep
の初期化を行っています。
最初にボードサポート <PAL_AMB>
を登録します。ボードサポートの初期化時にセンサーやDIOの初期化が行われます。最初に行うのは、ボードのDIP SWなどの状態を確認してから、ネットワークの設定などを行うといった処理が一般的だからです。
auto&& brd = the_twelite.board.use<PAL_AMB>();
つづいて、インタラクティブモード関連の初期化と読出しを行います。
// インタラクティブモードを読み込む
auto&& set = the_twelite.settings.use<STG_STD>();
set << SETTINGS::appname(FOURCHARS);
set << SETTINGS::appid_default(APP_ID); // set default appID
set.hide_items(E_STGSTD_SETID::POWER_N_RETRY, E_STGSTD_SETID::OPT_DWORD2, E_STGSTD_SETID::OPT_DWORD3, E_STGSTD_SETID::OPT_DWORD4, E_STGSTD_SETID::ENC_KEY_STRING, E_STGSTD_SETID::ENC_MODE);
// SET ピンを検出した場合は、インタラクティブモードを起動する
if (digitalRead(brd.PIN_BTN) == PIN_STATE::LOW) {
set << SETTINGS::open_at_start();
step.next(STATE::INTERACTIVE);
return;
}
// インタラクティブモードのデータを読み出す
set.reload();
APP_ID = set.u32appid();
CHANNEL = set.u8ch();
OPT_BITS = set.u32opt1();
// DIPスイッチとインタラクティブモードの設定からLIDを決める
LID = (brd.get_DIPSW_BM() & 0x07); // 1st priority is DIP SW
if (LID == 0) LID = set.u8devid(); // 2nd is setting.
if (LID == 0) LID = 0xFE; // if still 0, set 0xFE (anonymous child)
ここではset
オブジェクトの取得、アプリ名の反映、デフォルトのアプリケーションIDと通信チャネルの反映、設定メニューで不要項目の削除を行います。
次にSETピンの状態を読み出します。このサンプルはスリープによる間欠動作を行うため、+++
入力によるインタラクティブモード遷移は出来ません。替わりに起動時のSETピン=LO状態でインタラクティブモードに遷移します。このときSETTINGS::open_at_start()
を指定していますが、これはsetup()
を終了後速やかにインタラクティブモード画面に遷移する指定です。
最後に.reload()
を実行して設定値をEEPROMから読み出します。設定値を各変数にコピーしています。
続いてLEDの設定を行います。ここでは 10ms おきに ON/OFF の点滅の設定をします(スリープを行い起床時間が短いアプリケーションでは、起床中は点灯するという設定とほぼ同じ意味合いになります)。
brd.set_led(LED_TIMER::BLINK, 10); // blink (on 10ms/ off 10ms)
このアクトではもっぱら無線パケットを送信しますので、TWENET の設定では動作中に受信回路をオープンにする指定(TWENET::rx_when_idle()
)は含めません。
the_twelite
<< TWENET::appid(APP_ID) // set application ID (identify network group)
<< TWENET::channel(CHANNEL); // set channel (pysical channel)
ボード上のセンサーはI2Cバスを用いますので、バスを利用開始しておきます。
Wire.begin(); // start two wire serial bus.
ボード上のセンサーの取得を開始します。startSensorCapture()の解説を参照ください。
startSensorCapture();
loop()
void loop() {
auto&& brd = the_twelite.board.use<PAL_AMB>();
do {
switch (step.state()) {
// 各状態の振る舞い
case STATE::INIT:
...
break;
...
}
while(step.b_more_loop());
}
loop()
は、SM_SIMPLE
ステートマシンstep
を用いた制御を行っています。スリープ復帰からセンサー値取得、無線パケット送信、送信完了待ち、スリープといった一連の流れを簡潔に表現するためです。ループの戦闘ではbrd
オブジェクトを取得しています。
case STATE::INTERACTIVE:
インタラクティブモード中にメインループが動作するのは都合が悪いため、この状態に固定します。
case STATE::INIT:
brd.sns_SHTC3.begin();
brd.sns_LTR308ALS.begin();
step.next(STATE::SENSOR);
センサーのデータ取得を開始します。
case STATE::SENSOR:
if (!brd.sns_LTR308ALS.available()) {
brd.sns_LTR308ALS.process_ev(E_EVENT_TICK_TIMER);
}
if (!brd.sns_SHTC3.available()) {
brd.sns_SHTC3.process_ev(E_EVENT_TICK_TIMER);
}
ボード上のセンサーは .sns_LTR308ALS
または .sns_SHTC3
という名前でアクセスでき、このオブジェクトに操作を行います。センサーの完了待ちを行います。まだセンサーの取得が終わっていない場合(.available()
がfalse
)はセンサーに対して時間経過のイベント(.process_ev(E_EVENT_TICK_TIMER)
)を送付します。
センサーがavailableになった時点で、センサー値を取得し、STATE_TX
に遷移します。
// now sensor data is ready.
if (brd.sns_LTR308ALS.available() && brd.sns_SHTC3.available()) {
sensor.u32luminance = brd.sns_LTR308ALS.get_luminance();
sensor.i16temp = brd.sns_SHTC3.get_temp_cent();
sensor.i16humid = brd.sns_SHTC3.get_humid_per_dmil();
Serial << "..finish sensor capture." << mwx::crlf
<< " LTR308ALS: lumi=" << int(sensor.u32luminance) << mwx::crlf
<< " SHTC3 : temp=" << div100(sensor.i16temp) << 'C' << mwx::crlf
<< " humd=" << div100(sensor.i16humid) << '%' << mwx::crlf
;
Serial.flush();
step.next(STATE::TX);
}
照度センサーは.get_luminance()
: uint32_t
で得られます。
温湿度センサーは以下のように取得できます。
.get_temp_cent()
:int16_t
: 1℃を100とした温度 (25.6 ℃なら 2560).get_temp()
:float
: float値 (25.6 ℃なら 25.6).get_humid_dmil()
:int16_t
: 1%を100とした湿度 (56.8%なら 5680).get_temp()
:float
: float値 (56.8%なら 56.8)
case STATE::TX:
送信手続きについては他のアクトのサンプルと同様です。ここでは、再送1回、再送遅延を最小にする設定になっています。
pkt << tx_addr(0x00) // 親機0x00宛
<< tx_retry(0x1) // リトライ1回
<< tx_packet_delay(0, 0, 2); // 遅延は最小限
パケットのペイロード部に識別子のFOURCHARS
とセンサーデータを格納します。得られた値のうち温度値は int16_t
ですが、送信パケットのデータ構造は符号なしで格納するため、uint16_t
にキャストしています。
pack_bytes(pkt.get_payload()
, make_pair(FOURCHARS, 4)
, uint32_t(sensor.u32luminance)
, uint16_t(sensor.i16temp)
, uint16_t(sensor.i16humid)
);
送信要求を行います。送信要求が成功したら送信完了街の準備を行います。完了イベントを待つために.clear_flag()
、万が一のときのタイムアウトをset_timeout(100)
を指定します。パラメータの100
の単位はミリ秒[ms]です。
// do transmit
MWX_APIRET ret = pkt.transmit();
if (ret) {
step.clear_flag(); // waiting for flag is set.
step.set_timeout(100); // set timeout
step.next(STATE::TX_WAIT_COMP);
}
case STATE::TX_WAIT_COMP:
ここではタイムアウトの判定、送信完了イベントの判定を行います。
if (step.is_timeout()) { // maybe fatal error.
the_twelite.reset_system();
}
if (step.is_flag_ready()) { // when tx is performed
Serial << "..transmit complete." << mwx::crlf;
Serial.flush();
step.next(STATE::GO_SLEEP);
}
STATE::GO_SLEEP:
sleepNow()
の処理を行います。
on_tx_comp()
void on_tx_comp(mwx::packet_ev_tx& ev, bool_t &b_handled) {
step.set_flag(ev.bStatus);
}
送信完了時に呼び出されるシステムイベントです。ここでは.set_flag()
により完了としています。
sleepNow()
スリープに入る手続きをまとめています。
void sleepNow() {
step.on_sleep(false); // reset state machine.
// randomize sleep duration.
uint32_t u32ct = 1750 + random(0,500);
// output message
Serial << "..sleeping " << int(u32ct) << "ms." << mwx::crlf;
Serial.flush(); // wait until all message printed.
// do sleep.
the_twelite.sleep(u32ct);
}
スリープ前に.on_sleep(false)
によりステートマシンの状態を初期化します。パラメータのfalse
はスリープ復帰後STATE::INIT(=0)
から始めます。
ここでは、起床までの時間を乱数により 1750ms から 2250ms の間に設定しています。これにより他の同じような周期で送信するデバイスのパケットとの連続的な衝突を避けます。
8,9行目、この例ではシリアルポートからの出力を待ってスリープに入ります。通常は消費エネルギーを最小化したいため、スリープ前のシリアルポートの出力は最小限(または無し)にします。
12行目、スリープに入るには the_twelite.sleep()
を呼びます。この呼び出しの中で、ボード上のハードウェアのスリープ前の手続きなどが行われます。たとえばLEDは消灯します。
パラメータとしてスリープ時間をmsで指定しています。
wakeup()
スリープから復帰し起床すると wakeup()
が呼び出されます。そのあとloop()
が都度呼び出されます。wakeup()
の前に、UARTなどの各ペリフェラルやボード上のデバイスのウェイクアップ処理が行われます。例えばLEDの点灯制御を再始動します。
void wakeup() {
Serial << mwx::crlf
<< "--- PAL_AMB:" << FOURCHARS << " wake up ---"
<< mwx::crlf
<< "..start sensor capture again."
<< mwx::crlf;
応用編
消費エネルギーの削減
アクト PAL_AMB-UseNap は、センサーのデータ取得待ちをスリープで行い、より低消費エネルギーで動作できます。
1.10 - PAL_AMB-usenap
このアクトには以下が含まれます。
- 無線パケットの送受信
- インタラクティブモードによる設定 -
<STG_STD>
- ステートマシンによる状態遷移制御 -
<SM_SIMPLE>
<PAL_AMB>
ボードビヘイビアによるボード操作
アクトの機能
- 環境センサーパル AMPIENT SENSE PAL を用い、センサー値の取得を行います。
- コイン電池で動作させるための、スリープ機能を利用します。
- センサデータ取得中にもスリープ機能を利用します。
アクトの解説
begin()
begin()
関数はsetup()
関数を終了し(そのあとTWENETの初期化が行われる)一番最初のloop()
の直前で呼ばれます。
void begin() {
sleepNow(); // the first time is just sleeping.
}
setup()
終了後に初回スリープを実行します。setup()
中にセンサーデータ取得を開始していますが、この結果は評価せず、センサーを事前に一度は動かしておくという意味あいで、必ずしも必要な手続きではありません。
wakeup()
起床後の手続きです。以下の処理を行います。
- まだセンサーデータの取得開始をしていない場合、センサーデータ取得を行い、短いスリープに入る。
- 直前にセンサーデータ取得開始を行ったので、データを確認して無線送信する。
void wakeup() {
if (!b_senser_started) {
// delete/make shorter this message if power requirement is harder.
Serial << mwx::crlf
<< "--- PAL_AMB:" << FOURCHARS << " wake up ---"
<< mwx::crlf
<< "..start sensor capture again."
<< mwx::crlf;
startSensorCapture();
b_senser_started = true;
napNow(); // short period sleep.
} else {
Serial << "..wake up from short nap.." << mwx::crlf;
auto&& brd = the_twelite.board.use<PAL_AMB>();
b_senser_started = false;
// tell sensors waking up.
brd.sns_LTR308ALS.process_ev(E_EVENT_START_UP);
brd.sns_SHTC3.process_ev(E_EVENT_START_UP);
}
}
上記の分岐をグローバル変数のb_sensor_started
により制御しています。!b_sensor_started
の場合はセンサー取得開始(startSensorCapture()
)を行い、napNow()
により短いスリープに入ります。時間は100msです。
napNow()
によるスリープ復帰後、b_sensor_started==true
の節が実行されます。ここでは、2つのセンサーに対してE_EVENT_START_UP
イベントを通知しています。このイベントは、センサーの取得が終了するのに十分な時間が経過したことを意味します。この通知をもとにsns_LTR308ALS
とsns_SHTC3
はavailableになります。この後loop()
に移行し、無線パケットが送信されます。
napNow()
で正しい時間を設定したかどうかで決まります。短い時間で起床した場合は、必要とされる時間経過に足りないため、続く処理でセンサーデータが得られないなどのエラーが出ることが想定されます。napNow()
ごく短いスリープを実行する。
void napNow() {
uint32_t u32ct = 100;
Serial << "..nap " << int(u32ct) << "ms." << mwx::crlf;
the_twelite.sleep(u32ct, false, false, TWENET::SLEEP_WAKETIMER_SECONDARY);
}
sleepのパラメータの2番目をtrueにすると前回のスリープ復帰時刻をもとに次の復帰時間を調整します。常に5秒おきに起床したいような場合設定します。
3番目をtrueにするとメモリーを保持しないスリープになります。復帰後はwakeup()は呼び出されじ、電源再投入と同じ処理になります。
4番目はウェイクアップタイマーの2番目を使う指定です。ここでは1番目は通常のスリープに使用して、2番目を短いスリープに用いています。このアクトでは2番目を使う強い理由はありませんが、例えば上述の5秒おきに起床したいような場合、短いスリープに1番目のタイマーを用いてしまうとカウンター値がリセットされてしまい、経過時間の補正計算が煩雑になるため2番目のタイマーを使用します。
1.11 - PAL_AMB-behavior
アクトの機能
- 環境センサーパル AMBIENT SENSE PAL を用い、センサー値の取得を行います。
- コイン電池で動作させるための、スリープ機能を利用します。
アクトの使い方
TWELITEの準備
役割 | 例 |
---|---|
親機 | MONOSTICK BLUEまたはRED |
子機 | BLUE PAL または RED PAL + 環境センサーパル AMBIENT SENSE PAL |
ファイル構成
- PAL_AMB-behavior.hpp :
setup()
のみの定義です。DIP-SWを読み出し、D1..D3が上位置の場合は親機として動作し、それ以外は子機としてDIP SWに対応するIDをセットします。 - Parent/myAppBhvParent.hpp : 親機用のビヘイビアクラス定義
- Parent/myAppBhvParent.cpp : 実装
- Parent/myAppBhvParent-handlers.cpp : ハンドラーの実装
- Parent/myAppBhvParent.hpp : 子機用のビヘイビアクラス定義
- Parent/myAppBhvParent.cpp : 実装
- Parent/myAppBhvParent-handlers.cpp : ハンドラーの実装
親機のビヘイビア名は<MY_APP_PARENT>
、子機は<MY_APP_CHILD>
です。
setup()
// now read DIP sw status can be read.
u8ID = (brd.get_DIPSW_BM() & 0x07);
// Register App Behavior (set differnt Application by DIP SW settings)
if (u8ID == 0) {
// put settings to the twelite main object.
the_twelite
<< TWENET::appid(APP_ID) // set application ID (identify network group)
<< TWENET::channel(CHANNEL) // set channel (pysical channel)
<< TWENET::rx_when_idle(); // open RX channel
the_twelite.app.use<MY_APP_PARENT>();
} else {
// put settings to the twelite main object.
the_twelite
<< TWENET::appid(APP_ID) // set application ID (identify network group)
<< TWENET::channel(CHANNEL); // set channel (pysical channel)
the_twelite.app.use<MY_APP_CHILD>();
}
DIP SWの読み値が0の場合は親機用のビヘイビア<MY_APP_PARENT>
を、それ以外の場合は子機用のビヘイビア<MY_APP_CHILD>
を登録します。
親機のビヘイビア
親機はスリープをしない受信機としてふるまい、子機からのパケットを受信したときにシリアルポートにパケットの情報を出力します。
MY_APP_PARENT::receive()
void MY_APP_PARENT::receive(mwx::packet_rx& rx) {
uint8_t msg[4];
uint32_t lumi;
uint16_t u16temp, u16humid;
// expand packet payload (shall match with sent packet data structure, see pack_bytes())
auto&& np = expand_bytes(rx.get_payload().begin(), rx.get_payload().end(), msg);
// if PING packet, respond pong!
if (!strncmp((const char*)msg, (const char*)FOURCHARS, 4)) {
// get rest of data
expand_bytes(np, rx.get_payload().end(), lumi, u16temp, u16humid);
// print them
Serial << format("Packet(%x:%d/lq=%d/sq=%d): ",
rx.get_addr_src_long(), rx.get_addr_src_lid(),
rx.get_lqi(), rx.get_psRxDataApp()->u8Seq)
<< "temp=" << double(int16_t(u16temp)/100.0)
<< "C humid=" << double(int16_t(u16humid)/100.0)
<< "% lumi=" << int(lumi)
<< mwx::crlf << mwx::flush;
}
}
親機用がパケットを受信したときは、パケットの先頭4文字が照合(FOURCHARS
)できれば、そのパケット内容を表示します。
MY_APP_PARENT::MWX_TICKTIMER_INT()
MWX_TICKTIMER_INT(uint32_t arg, uint8_t& handled) {
// blink LED
digitalWrite(PAL_AMB::PIN_LED,
((millis() >> 9) & 1) ? PIN_STATE::HIGH : PIN_STATE::LOW);
}
親機の割り込みハンドラはLEDの点滅を行います。
MY_APP_PARENT::MWX_DIO_EVENT(PAL_AMB::PIN_BTN)
MWX_DIO_EVENT(PAL_AMB::PIN_BTN, uint32_t arg) {
Serial << "Button Pressed" << mwx::crlf;
static uint32_t u32tick_last;
uint32_t tick = millis();
if (tick - u32tick_last > 100) {
PEV_Process(E_ORDER_KICK, 0UL);
}
u32tick_last = tick;
}
PAL上のボタン(5)が押されたときには、状態マシンに対してE_ORDER_KICK
イベントを発行します。
MY_APP_PARENT::MWX_STATE(E_MWX::STATE_0 .. 3)
状態マシンは、状態遷移の参考として記述したもので、アプリケーションの動作上意味のあるものではありません。ボタンから送付されるE_ORDER_KICK
イベントによる状態遷移や、タイムアウトなどを実行しています。
子機のビヘイビア
子機の動作の流れはPAL_AMB-usenap
と同じです。初回スリープから「起床→センサー動作開始→短いスリープ→起床→センサー値取得→無線送信→無線送信完了待ち→スリープ」を繰り返します。
MY_APP_CHILD::on_begin()
void _begin() {
// sleep immediately.
Serial << "..go into first sleep (1000ms)" << mwx::flush;
the_twelite.sleep(1000);
}
on_begin()
から呼び出される_begin()
関数では、初回スリープを実行しています。
(※_begin()
関数で本処理を記述せずon_begin()
に直接記述してもかまいません)
MY_APP_CHILD::wakeup()
void wakeup(uint32_t & val) {
Serial << mwx::crlf << "..wakeup" << mwx::crlf;
// init wire device.
Wire.begin();
// turn on LED
digitalWrite(PAL_AMB::PIN_LED, PIN_STATE::LOW);
// KICK it!
PEV_Process(E_ORDER_KICK, 0); // pass the event to state machine
}
スリープからの起床処理を記述しています。
ここで初回のWire.begin()
を実行しています。2回目以降のスリープ起床時では冗長な記述です。この処理はon_begin()
に移動してもかまいません。
MY_APP_CHILD::transmit_complete()
void transmit_complete(mwx::packet_ev_tx& txev) {
Serial << "..txcomp=" << int(txev.u8CbId) << mwx::crlf;
PEV_Process(E_ORDER_KICK, txev.u8CbId); // pass the event to state machine
}
送信完了時に状態マシンに対してE_ORDER_KICK
メッセージを処理します。
MY_APP_CHILD::transmit_complete()
static const uint8_t STATE_IDLE = E_MWX::STATE_0;
static const uint8_t STATE_SENSOR = E_MWX::STATE_1;
static const uint8_t STATE_TX = E_MWX::STATE_2;
static const uint8_t STATE_SLEEP = E_MWX::STATE_3;
状態名を定義しています。
MY_APP_CHILD::shtc3_???()
MWX_APIRET MY_APP_CHILD::shtc3_start()
MWX_APIRET MY_APP_CHILD::shtc3_read()
SHTC3用のセンサー取得実装例です。送付コマンド等の詳細はSHTC3のデータシートなどを参考にしてください。
MY_APP_CHILD::ltr308als_???()
MWX_APIRET MY_APP_CHILD::ltr308als_read()
MWX_APIRET MY_APP_CHILD::ltr308als_start()
static MWX_APIRET WireWriteAngGet(uint8_t addr, uint8_t cmd)
LTR308ALSのセンサー取得実装例です。送付コマンド等の詳細はLTR308ALSのデータシートなどを参考にしてください。
WireWriteAndGet()
はaddr
のデバイスに対してcmd
を1バイト送信してから、1バイト受信して値を返します。
MY_APP_CHILD::STATE_IDLE (0)
MWX_STATE(MY_APP_CHILD::STATE_IDLE, uint32_t ev, uint32_t evarg) {
if (PEV_is_coldboot(ev,evarg)) {
Serial << "[STATE_IDLE:START_UP(" << int(evarg) << ")]" << mwx::crlf;
// then perform the first sleep at on_begin().
} else
if (PEV_is_warmboot(ev,evarg)) {
Serial << "[STATE_IDLE:START_UP(" << int(evarg) << ")]" << mwx::crlf;
PEV_SetState(STATE_SENSOR);
}
}
0番の状態は特別な意味を持ちます。起動直後またはスリープ復帰後の状態です。
起動直後PEV_is_coldboot(ev,evarg)
判定がtrue
になって呼び出されます。on_begin()
から、そのままスリープしてしまうため、状態遷移するようなコードも含まれません。**この時点では主要な初期化がまだ終わっていませんので、無線パケットの送信など複雑な処理を行うことが出来ません。**そのような処理を行うための最初の状態遷移を行うためにはon_begin()
からイベントを送り、そのイベントに従って状態遷移を行います。
スリープ復帰後はPEV_is_warmboot(ev,evarg)
がtrue
になる呼び出しが最初にあります。PEV_SetState()
を呼び出しSTATE_SENSOR
状態に遷移します。
MY_APP_CHILD::STATE_SENSOR
MWX_STATE(MY_APP_CHILD::STATE_SENSOR, uint32_t ev, uint32_t evarg) {
if (ev == E_EVENT_NEW_STATE) {
Serial << "[STATE_SENSOR:NEW] Start Sensor." << mwx::crlf;
// start sensor capture
shtc3_start();
ltr308als_start();
// take a nap waiting finish of capture.
Serial << "..nap for 66ms" << mwx::crlf;
Serial.flush();
PEV_KeepStateOnWakeup(); // stay this state on waking up.
the_twelite.sleep(66, false, false, TWENET::SLEEP_WAKETIMER_SECONDARY);
} else
if (PEV_is_warmboot(ev,evarg)) {
// on wakeup, code starts here.
Serial << "[STATE_SENSOR:START_UP] Wakeup." << mwx::crlf;
PEV_SetState(STATE_TX);
}
}
スリープ復帰後STATE_IDLE
から遷移したとき、STATE_SENSOR
の状態ハンドラが続けて呼び出されます。この時のイベントev
はE_EVENT_NEW_STATE
です。
ここではSHTC3, LTR308ALSの2センサーの動作開始を行います。一定時間経過すれば、センサーはデータ取得可能な状態になります。この時間待ちを66
ms設定のスリープで行います。スリープ前にPEV_KeepStateOnWakeup()
が呼ばれている点に注意してください。この呼び出しを行うと、スリープ復帰後の状態はSTATE_IDLE
ではなく、スリープしたときの状態、つまりSTATE_SENSOR
となります。
短いスリープから復帰するとPEV_is_warmboot(ev,evarg)
判定がtrue
となる呼び出しが最初に発生します。この呼び出し時点で、無線パケットの送信などを行うことが出来ます。STATE_TX
に遷移します。
MY_APP_CHILD::STATE_TX
MWX_STATE(MY_APP_CHILD::STATE_TX, uint32_t ev, uint32_t evarg)
static int u8txid;
if (ev == E_EVENT_NEW_STATE) {
Serial << "[STATE_TX:NEW]" << mwx::crlf;
u8txid = -1;
auto&& r1 = shtc3_read();
auto&& r2 = ltr308als_read();
Serial << "..shtc3 t=" << int(i16Temp) << ", h=" << int(i16Humd) << mwx::crlf;
Serial << "..ltr308als l=" << int(u32Lumi) << mwx::crlf;
if (r1 && r2) {
if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
ここではE_EVENT_NEW_STATE
イベントの時に、センサーデータ読み出し、無線パケットの送信手続きに入ります。送信手続きの詳細は他のアクトサンプル例を参考にしてください。
void transmit_complete(mwx::packet_ev_tx& txev) {
Serial << "..txcomp=" << int(txev.u8CbId) << mwx::crlf;
PEV_Process(E_ORDER_KICK, txev.u8CbId); // pass the event to state machine
}
// ↓ ↓ ↓ メッセージ送付
} else if (ev == E_ORDER_KICK && evarg == uint32_t(u8txid)) {
Serial << "[STATE_TX] SUCCESS TX(" << int(evarg) << ')' << mwx::crlf;
PEV_SetState(STATE_SLEEP);
}
送信完了まちの処理はループでのアクト記述と違い、transmit_complete()
からのPEV_Process()
によるメッセージを待つことで完了確認としています。メッセージを受け取った時点でスリープします。スリープ処理はSTATE_SLEEP
に遷移することで行っています。
if (PEV_u32Elaspsed_ms() > 100) {
// does not finish TX!
Serial << "[STATE_TX] FATAL, TX does not finish!" << mwx::crlf << mwx::flush;
the_twelite.reset_system();
}
最後にタイムアウト処理を行っています。万が一送信パケットの完了メッセージが戻ってこなかった場合を想定します。PEV_u32Elaspsed_ms()
はその状態に遷移してからの経過時間を[ms]で返します。時間経過した場合は、上記では(このタイムアウトは余程のことだとして)システムリセットthe_twelite.reset_system()
を行います。
MY_APP_CHILD::STATE_SLEEP
MWX_STATE(MY_APP_CHILD::STATE_SLEEP, uint32_t ev, uint32_t evarg) {
if (ev == E_EVENT_NEW_STATE) {
Serial << "..sleep for 5000ms" << mwx::crlf;
pinMode(PAL_AMB::PIN_BTN, PIN_MODE::WAKE_FALLING_PULLUP);
digitalWrite(PAL_AMB::PIN_LED, PIN_STATE::HIGH);
Serial.flush();
the_twelite.sleep(5000); // regular sleep
}
}
スリープを行います。前の状態から遷移した直後のE_EVENT_NEW_STATE
に記述します。スリープ直前に他のイベントが呼ばれる可能性がありますので、必ず1回だけ実行される判定式の中でthe_twelite.sleep()
を実行してください。
1.12 - PAL_MAG
このアクトの解説の前にBRD_APPTWELITEの解説をご覧ください。
受信の確認のためParent_MONOSTICKの解説もご覧ください。
アクトの機能
- 開閉センサーパル OPEN-CLOSE SENSE PAL を用い、磁気センサーの検出時に割り込み起床し、無線送信します。
- コイン電池で動作させるための、スリープ機能を利用します。
アクトの使い方
必要なTWELITE
役割 | 例 |
---|---|
親機 | アクトParent_MONOSTICKを動作させる。 |
子機 | BLUE PAL または RED PAL + 開閉センサーパル OPEN-CLOSE SENSE PAL |
アクトの解説
インクルード
##include <TWELITE>
##include <NWK_SIMPLE>
##include <PAL_MAG>
開閉センサーパルのボード ビヘイビア<PAL_MAG>
をインクルードします。
setup()
void setup() {
/*** SETUP section */
// use PAL_AMB board support.
auto&& brd = the_twelite.board.use<PAL_MAG>();
// now it can read DIP sw status.
u8ID = (brd.get_DIPSW_BM() & 0x07) + 1;
if (u8ID == 0) u8ID = 0xFE; // 0 is to 0xFE
// LED setup (use periph_led_timer, which will re-start on wakeup() automatically)
brd.set_led(LED_TIMER::BLINK, 10); // blink (on 10ms/ off 10ms)
// the twelite main object.
the_twelite
<< TWENET::appid(APP_ID) // set application ID (identify network group)
<< TWENET::channel(CHANNEL); // set channel (pysical channel)
// Register Network
auto&& nwk = the_twelite.network.use<NWK_SIMPLE>();
nwk << NWK_SIMPLE::logical_id(u8ID); // set Logical ID. (0xFE means a child device with no ID)
/*** BEGIN section */
the_twelite.begin(); // start twelite!
/*** INIT message */
Serial << "--- PAL_MAG:" << FOURCHARS << " ---" << mwx::crlf;
}
最初にボードビヘイビア<PAL_MAG>
を登録します。ボードビヘイビアの初期化時にセンサーやDIOの初期化が行われます。最初に行うのは、ボードのDIP SWなどの状態を確認してから、ネットワークの設定などを行うといった処理が一般的だからです。
auto&& brd = the_twelite.board.use<PAL_MAG>();
u8ID = (brd.get_DIPSW_BM() & 0x07) + 1;
if (u8ID == 0) u8ID = 0xFE; // 0 is to 0xFE
ここでは、ボード上の4ビットDIP SWのうち3ビットを読み出して子機のIDとして設定しています。0の場合は、ID無しの子機(0xFE
)とします。
LEDの設定を行います。ここでは 10ms おきに ON/OFF の点滅の設定をします(スリープを行い起床時間が短いアプリケーションでは、起床中は点灯するという設定とほぼ同じ意味合いになります)。
brd.set_led(LED_TIMER::BLINK, 10); // blink (on 10ms/ off 10ms)
begin()
begin()
関数はsetup()
関数を終了し(そのあとTWENETの初期化が行われる)一番最初のloop()
の直前で呼ばれます。
void begin() {
sleepNow(); // the first time is just sleeping.
}
setup()
終了後にsleepNow()
を呼び出し初回スリープを実行します。
sleepNow()
void sleepNow() {
uint32_t u32ct = 60000;
pinMode(PAL_MAG::PIN_SNS_OUT1, PIN_MODE::WAKE_FALLING);
pinMode(PAL_MAG::PIN_SNS_OUT2, PIN_MODE::WAKE_FALLING);
the_twelite.sleep(u32ct);
}
スリープに入るまえに磁気センサーのDIOピンの割り込み設定をします。pinMode()
を用います。2番めのパラメータはPIN_MODE::WAKE_FALLING
を指定しています。これはHIGHからLOWへピンの状態が変化したときに起床する設定です。
7行目でthe_twelite.sleep()
でスリープを実行します。パラメータの60000は、TWELITE PAL ボードのウォッチドッグをリセットするために必要な起床設定です。リセットしないと60秒経過後にハードリセットがかかります。
wakeup()
スリープから復帰し起床すると wakeup()
が呼び出されます。そのあとloop()
が都度呼び出されます。wakeup()
の前に、UARTなどの各ペリフェラルやボード上のデバイスのウェイクアップ処理(ウォッチドッグタイマーのリセットなど)が行われます。例えばLEDの点灯制御を再始動します。
void wakeup() {
if (the_twelite.is_wokeup_by_wktimer()) {
sleepNow();
}
}
ここではウェイクアップタイマーからの起床の場合(the_twelite.is_wokeup_by_wktimer()
)は再びスリープを実行します。これは上述のウォッチドッグタイマーのリセットを行う目的のみの起床です。
磁気センサーの検出時の起床の場合は、このままloop()
処理に移行します。
loop()
ここでは、検出された磁気センサーのDIOの確認を行い、パケットの送信を行い、パケット送信完了後に再びスリープを実行します。
void loop() {
if (!b_transmit) {
if (auto&& pkt =
the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet())
uint8_t b_north =
the_twelite.is_wokeup_by_dio(PAL_MAG::PIN_SNS_NORTH);
uint8_t b_south =
the_twelite.is_wokeup_by_dio(PAL_MAG::PIN_SNS_SOUTH);
Serial << "..sensor north=" << int(b_north)
<< " south=" << int(b_south) << mwx::crlf;
// set tx packet behavior
pkt << tx_addr(0x00)
<< tx_retry(0x1)
<< tx_packet_delay(0, 0, 2);
// prepare packet payload
pack_bytes(pkt.get_payload()
, make_pair(FOURCHARS, 4)
, b_north
, b_south
);
// do transmit
MWX_APIRET ret = pkt.transmit();
if (ret) {
u8txid = ret.get_value() & 0xFF;
b_transmit = true;
}
else {
// fail to request
sleepNow();
}
} else {
sleepNow();
}
} else {
if (the_twelite.tx_status.is_complete(u8txid)) {
b_transmit = 0;
sleepNow();
}
}
}
b_transmit
変数によってloop()
内の振る舞いを制御しています。送信要求が成功した後、この値を1にセットしパケット送信完了待ちを行います。
if (!b_transmit) {
磁気センサーの検出DIOピンの確認を行います。検出ピンは二種類あります。N極検知とS極検知です。単に磁石が近づいたことだけを知りたいならいずれかのピンの検出されたことが条件となります。
uint8_t b_north =
the_twelite.is_wokeup_by_dio(PAL_MAG::PIN_SNS_NORTH);
uint8_t b_south =
the_twelite.is_wokeup_by_dio(PAL_MAG::PIN_SNS_SOUTH);
起床要因のピンを確認するにはthe_twelite.is_wokeup_by_dio()
を用います。パラメータはピン番号です。戻り値をuint8_t
に格納しているのはパケットのペイロードに格納するためです。
通信条件の設定やペイロードにデータを格納後、送信を行います。
// do transmit
MWX_APIRET ret = pkt.transmit();
その後、loop()
中 b_transmit
が true
になっている場合は、完了チェックを行い、完了すれば sleepNow()
によりスリープします。
if (the_twelite.tx_status.is_complete(u8txid)) {
b_transmit = 0;
sleepNow();
}
送信完了に確認は the_twelite.tx_status.is_complete(u8txid)
で行っています。u8txid
は送信時に戻り値として戻されたID値です。
1.13 - PAL_MOT-single
このアクトには以下が含まれます。
- 無線パケットの送受信
- インタラクティブモードによる設定 - <STG_STD>
- ステートマシンによる状態遷移制御 - <SM_SIMPLE>
- <PAL_MOT>または
ボードビヘイビアによるボード操作
アクトの解説
起床→加速度センサーの取得開始→加速度センサーのFIFO割り込み待ち→加速度センサーのデータの取り出し→無線送信→スリープという流れになります。
宣言部
インクルード
##include <TWELITE> // MWXライブラリ基本
##include <NWK_SIMPLE> // ネットワーク
##include <SM_SIMPLE> // ステートマシン(状態遷移)
##include <STG_STD> // インタラクティブモード
/*** board selection (choose one) */
##define USE_PAL_MOT
//#define USE_CUE
// board dependend definitions.
##if defined(USE_PAL_MOT)
##define BRDN PAL_MOT
##define BRDC <PAL_MOT>
##elif defined(USE_CUE)
##define BRDN CUE
##define BRDC <CUE>
##endif
// include board support
##include BRDC
MOT PALまたはTWELITE CUEに対応するため、インクルード部分はマクロになっています。USE_PAL_MOT
または、USE_CUE
のいずれかを定義します。
USE_PAL_MOT
が定義されている場合は動作センサーパルのボードビヘイビア<PAL_MOT>
をインクルードしています。
状態定義
enum class E_STATE : uint8_t {
INTERACTIVE = 255,
INIT = 0,
START_CAPTURE,
WAIT_CAPTURE,
REQUEST_TX,
WAIT_TX,
EXIT_NORMAL,
EXIT_FATAL
};
SM_SIMPLE<E_STATE> step;
loop()
中の順次処理を行うために状態を定義し、またステートマシンstep
を宣言します。
センサーデータ格納
struct {
int32_t x_ave, y_ave, z_ave;
int32_t x_min, y_min, z_min;
int32_t x_max, y_max, z_max;
uint16_t n_seq;
uint8_t n_samples;
} sensor;
センサーデータを格納するためのデータ構造です。
setup()
/// load board and settings objects
auto&& brd = the_twelite.board.use BRDC (); // load board support
auto&& set = the_twelite.settings.use<STG_STD>(); // load save/load settings(interactive mode) support
auto&& nwk = the_twelite.network.use<NWK_SIMPLE>(); // load network support
ボード、設定、ネットワークの各ビヘイビアオブジェクトの登録を行います。
インタラクティブモード
// settings: configure items
set << SETTINGS::appname("MOT");
set << SETTINGS::appid_default(DEFAULT_APP_ID); // set default appID
set << SETTINGS::ch_default(DEFAULT_CHANNEL); // set default channel
set << SETTINGS::lid_default(0x1); // set default LID
set.hide_items(E_STGSTD_SETID::OPT_DWORD2, E_STGSTD_SETID::OPT_DWORD3, E_STGSTD_SETID::OPT_DWORD4, E_STGSTD_SETID::ENC_KEY_STRING, E_STGSTD_SETID::ENC_MODE);
// if SET=LOW is detected, start with intaractive mode.
if (digitalRead(brd.PIN_SET) == PIN_STATE::LOW) {
set << SETTINGS::open_at_start();
brd.set_led(LED_TIMER::BLINK, 300); // slower blink
step.next(STATE::INTERACTIVE);
return;
}
// load settings
set.reload(); // load from EEPROM.
OPT_BITS = set.u32opt1(); // this value is not used in this example.
インタラクティブモードの初期化を行います。
まず、設定項目の調整を行います。ここでは、メニュー項目で表示されるタイトル名SETTINGS::appname
、アプリケーションIDのデフォルト値の設定SETTINGS::appid_default
、チャネルのデフォルトSETTINGS::ch_default
、論理デバイスIDのデフォルトSETTINGS::lid_default
、非表示項目の設定.hide_items()
を行います。
このサンプルでは起動時にSETピンがLOである場合にインタラクティブモードに遷移します。digitalRead(brd.PIN_SET)
によりピンがLOであることを確認できた場合は、SETTINGS::open_at_start()
を指定します。この指定によりsetup()
を抜けた後に速やかにインタラクティブモード画面が表示されます。画面が表示されてもbegin()
やloop()
が実行されます。このサンプルでは状態STATE::INTERACTIVE
としてloop()
中ではスリープなどの動作はせず何もしないようにします。
続いて設定値を読み出します。設定値を読むには必ず.reload()
を実行します。このサンプルではオプションビット設定.u32opt1()
を読み出します。
the_twelite
the_twelite << set;
the_twelite
は、システムの基本的な振る舞いを管理するクラスオブジェクトです。このオブジェクトは、setup()
内でアプリケーションIDやチャネルなど様々な初期化を行います。
ここではインタラクティブモードの設定値の一部を反映しています。
インタラクティブモード設定で反映した項目を別の設定に変更したい場合は、続いて上書きしたい設定を行います。
the_twelite << set;// インタラクティブモード
the_twelite << twenet::channel(19); // chを19に上書き設定
NWK_SIMPLE
オブジェクト
nwk << set;
ネットワークビヘイビアオブジェクトに対しても設定を行います。インタラクティブモードの論理デバイスID(LID)と再送設定が反映されます。
その他、ハードウェアの初期化など
brd.set_led(LED_TIMER::BLINK, 100);
LEDのブリンク設定などを行います。
begin()
void begin() {
auto&& set = the_twelite.settings.use<STG_STD>();
if (!set.is_screen_opened()) {
// sleep immediately, waiting for the first capture.
sleepNow();
}
}
setup()
を終了した後に呼ばれます。ここでは初回スリープを実行しています。ただしインタラクティブモードの画面が表示される場合はスリープしません。
wakeup()
void wakeup() {
Serial << crlf << "--- PAL_MOT(OneShot):"
<< FOURCHARS << " wake up ---" << crlf;
eState = E_STATE::INIT;
}
起床後は状態変数eState
を初期状態INITにセットしています。この後loop()
が実行されます。
loop()
void loop() {
auto&& brd = the_twelite.board.use<PAL_MOT>();
do {
switch(step.state()) {
case STATE::INTERACTIVE:
break;
...
} while(step.b_more_loop());
}
loop()
の基本構造は<SM_STATE>
ステートマシンstate
を用い_switch … case_節での制御です。初期状態はSTATE::INIT
またはSTATE::INTERACTIVE
です。
STATE::INTERACTIVE
インタラクティブモード画面が表示されているときの状態です。何もしません。この画面ではSerialの入出力はインタラクティブモードが利用します。
STATE::INIT
初期状態のINITです。
case STATE::INIT:
brd.sns_MC3630.get_que().clear(); // clear queue in advance (just in case).
memset(&sensor, 0, sizeof(sensor)); // clear sensor data
step.next(STATE::START_CAPTURE);
break;
状態INITでは、初期化(結果格納用のキューのクリア)や結果格納用のデータ構造の初期化を行います。STATE::START_CAPTUREに遷移します。この遷移設定後、もう一度_while_ループが実行されます。
STATE::CAPTURE
case STATE::START_CAPTURE:
brd.sns_MC3630.begin(
// 400Hz, +/-4G range, get four samples and will average them.
SnsMC3630::Settings(
SnsMC3630::MODE_LP_400HZ, SnsMC3630::RANGE_PLUS_MINUS_4G, N_SAMPLES));
step.set_timeout(100);
step.next(STATE::WAIT_CAPTURE);
break;
状態START_CAPTURE
では、MC3630センサーのFIFO取得を開始します。ここでは400Hzで4サンプル取得できた時点でFIFO割り込みが発生する設定にしています。
例外処理のためのタイムアウトの設定と、次の状態STATE::WAIT_CAPTURE
に遷移します。
STATE::WAIT_CAPTURE
case STATE::WAIT_CAPTURE:
if (brd.sns_MC3630.available()) {
brd.sns_MC3630.end(); // stop now!
状態WAIT_CAPTURE
では、FIFO割り込みを待ちます。割り込みが発生し結果格納用のキューにデータが格納されるとsns_MC3630.available()
がtrue
になります。sns_MC3630.end()
を呼び出し処理を終了します。
sensor.n_samples = brd.sns_MC3630.get_que().size();
if (sensor.n_samples) sensor.n_seq = brd.sns_MC3630.get_que()[0].get_t();
...
サンプル数とサンプルのシーケンス番号を取得します。
// get all samples and average them.
for (auto&& v: brd.sns_MC3630.get_que()) {
sensor.x_ave += v.x;
sensor.y_ave += v.y;
sensor.z_ave += v.z;
}
if (sensor.n_samples == N_SAMPLES) {
// if N_SAMPLES == 2^n, division is much faster.
sensor.x_ave /= N_SAMPLES;
sensor.y_ave /= N_SAMPLES;
sensor.z_ave /= N_SAMPLES;
}
...
すべてのサンプルデータに対して読み出し、平均値をとる処理をします。
// can also be:
// int32_t x_max = -999999, x_min = 999999;
// for (auto&& v: brd.sns_MC3630.get_que()) {
// if (v.x >= x_max) x_max = v.x;
// if (v.y <= x_min) x_min = v.x;
// ...
// }
auto&& x_minmax = std::minmax_element(
get_axis_x_iter(brd.sns_MC3630.get_que().begin()),
get_axis_x_iter(brd.sns_MC3630.get_que().end()));
sensor.x_min = *x_minmax.first;
sensor.x_max = *x_minmax.second;
...
ここでは取得されたサンプルに対して、各軸に対応するイテレータを用い最大・最小を得ています。
std::mimmax_element
紹介していますが、コメント内のようにforループ内で最大、最小を求めても構いません。
if (brd.sns_MC3630.available()) {
...
brd.sns_MC3630.get_que().clear(); // clean up the queue
step.next(STATE::REQUEST_TX); // next state
} else if (step.is_timeout()) {
Serial << crlf << "!!!FATAL: SENSOR CAPTURE TIMEOUT.";
step.next(STATE::EXIT_FATAL);
}
break;
.sns_MC3630.get_que().clear()
を呼び出し、キューにあるデータをクリアします。これを呼び出さないと続くサンプル取得ができません。その後STATE::REQUEST_TX
状態に遷移します。
.is_timeout()
はタイムアウトをチェックします。タイムアウト時は異常としてSTATE::EXIT_FATAL
に遷移します。
STATE::REQUEST_TX
case STATE::REQUEST_TX:
if (TxReq()) {
step.set_timeout(100);
step.clear_flag();
step.next(STATE::WAIT_TX);
} else {
Serial << crlf << "!!!FATAL: TX REQUEST FAILS.";
step.next(STATE::EXIT_FATAL);
}
break;
状態REQUEST_TX
ではローカル定義関数TxReq()
を呼び出し、得られたセンサーデータの処理と送信パケットの生成・送信を行います。送信要求は送信キューの状態などで失敗することがあります。送信要求が成功した場合、TxReq()はtrueとして戻りますが、まだ送信は行われません。送信完了はon_tx_comp()
コールバックが呼び出されます。
また.clear_flag()
により送信完了を知らせるためのフラグをクリアしておきます。同時にタイムアウトも設定します。
E_STATE::WAIT_TX
case STATE::WAIT_TX:
if (step.is_flag_ready()) {
step.next(STATE::EXIT_NORMAL);
}
if (step.is_timeout()) {
Serial << crlf << "!!!FATAL: TX TIMEOUT.";
step.next(STATE::EXIT_FATAL);
}
break;
状態STATE::WAIT_TX
では、無線パケットの送信完了を待ちます。フラグはon_tx_comp()
コールバック関数でセットされ、セット後に.is_flag_ready()
が_true_になります。
E_STATE::EXIT_NORMAL
, E_STATE::EXIT_FATAL
case STATE::EXIT_NORMAL:
sleepNow();
break;
case STATE::EXIT_FATAL:
Serial << flush;
the_twelite.reset_system();
break;
一連の動作が完了したときは状態STATE::EXIT_NORMAL
に遷移しローカル定義の関数sleepNow()
を呼び出しスリープを実行します。またエラーを検出した場合は状態STATE::EXIT_FATAL
に遷移し、システムリセットを行います。
MWX_APIRET TxReq()
MWX_APIRET TxReq() {
auto&& brd = the_twelite.board.use<PAL_MOT>();
MWX_APIRET ret = false;
// prepare tx packet
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 (0x1 send two times in total)
<< tx_packet_delay(0, 0, 2); // send packet w/ delay
// prepare packet (first)
pack_bytes(pkt.get_payload() // set payload data objects.
, make_pair(FOURCHARS, 4) // just to see packet identification, you can design in any.
, uint16_t(sensor.n_seq)
, uint8_t(sensor.n_samples)
, uint16_t(sensor.x_ave)
, uint16_t(sensor.y_ave)
, uint16_t(sensor.z_ave)
, uint16_t(sensor.x_min)
, uint16_t(sensor.y_min)
, uint16_t(sensor.z_min)
, uint16_t(sensor.x_max)
, uint16_t(sensor.y_max)
, uint16_t(sensor.z_max)
);
// perform transmit
ret = pkt.transmit();
if (ret) {
Serial << "..txreq(" << int(ret.get_value()) << ')';
}
}
return ret;
}
最期にパケットの生成と送信を要求を行います。パケットには続き番号、サンプル数、XYZの平均値、XYZの最小サンプル値、XYZの最大サンプル値を含めます。
sleepNow()
void sleepNow() {
Serial << crlf << "..sleeping now.." << crlf;
Serial.flush();
step.on_sleep(false); // reset state machine.
the_twelite.sleep(3000, false); // set longer sleep (PAL must wakeup less than 60sec.)
}
スリープの手続きです。
- シリアルポートはスリープ前に
Serial.flush()
を呼び出してすべて出力しておきます。 <SM_SIMPLE>
ステートマシンはon_sleep()
を行う必要があります。
1.14 - PAL_MOT-fifo
このアクトには以下が含まれます。
- 無線パケットの送受信
- インタラクティブモードによる設定 - <STG_STD>
- ステートマシンによる状態遷移制御 - <SM_SIMPLE>
- <PAL_MOT>または
ボードビヘイビアによるボード操作
アクトの機能
- 動作センサーパル MOTION SENSE PAL を用い、加速度センサーの加速度を連続的に計測し、無線送信します。
- コイン電池で動作させるための、スリープ機能を利用します。
アクトの使い方
必要なTWELITE
役割 | 例 |
---|---|
親機 | MONOSTICK BLUEまたはREDアクトParent_MONOSTICKを動作させる。 |
子機 | BLUE PAL または RED PAL +動作センサーパル MOTION SENSE PAL |
アクトの解説
インクルード
##include <TWELITE>
##include <NWK_SIMPLE>
##include <PAL_MOT>
動作センサーパルのボードビヘイビア<PAL_MOT>
をインクルードします。
setup()
void setup() {
/*** SETUP section */
// board
auto&& brd = the_twelite.board.use<PAL_MOT>();
brd.set_led(LED_TIMER::BLINK, 100);
// the twelite main class
the_twelite
<< TWENET::appid(APP_ID)
<< TWENET::channel(CHANNEL);
// Register Network
auto&& nwk = the_twelite.network.use<NWK_SIMPLE>();
nwk << NWK_SIMPLE::logical_id(0xFE);
/*** BEGIN section */
the_twelite.begin(); // start twelite!
brd.sns_MC3630.begin(SnsMC3630::Settings(
SnsMC3630::MODE_LP_14HZ, SnsMC3630::RANGE_PLUS_MINUS_4G));
/*** INIT message */
Serial << "--- PAL_MOT(Cont):" << FOURCHARS
<< " ---" << mwx::crlf;
}
最初にボードビヘイビア<PAL_MOT>
を登録します。ボードビヘイビアの初期化時にセンサーやDIOの初期化が行われます。最初に行うのは、ボードのDIP SWなどの状態を確認してから、ネットワークの設定などを行うといった処理が一般的だからです。
auto&& brd = the_twelite.board.use<PAL_MOT>();
u8ID = (brd.get_DIPSW_BM() & 0x07) + 1;
if (u8ID == 0) u8ID = 0xFE; // 0 is to 0xFE
ここでは、ボード上の4ビットDIP SWのうち3ビットを読み出して子機のIDとして設定しています。0の場合は、ID無しの子機(0xFE
)とします。
LEDの設定を行います。ここでは 10ms おきに ON/OFF の点滅の設定をします(スリープを行い起床時間が短いアプリケーションでは、起床中は点灯するという設定とほぼ同じ意味合いになります)。
brd.set_led(LED_TIMER::BLINK, 10); // blink (on 10ms/ off 10ms)
加速度センサーの初期化
brd.sns_MC3630.begin(SnsMC3630::Settings(
SnsMC3630::MODE_LP_14HZ, SnsMC3630::RANGE_PLUS_MINUS_4G));
加速度センサーの計測を開始します。加速度センサーの設定(SnsMC3630::Settings
)には計測周波数と測定レンジを指定します。ここでは14HZの計測(SnsMC3630::MODE_LP_14HZ
)で、±4Gのレンジ(SnsMC3630::RANGE_PLUS_MINUS_4G
)で計測します。
開始後は加速度センサーの計測が秒14回行われ、その値はセンサー内部のFIFOキューに保存されます。センサーに28回分の計測が終わった時点で通知されます。
begin()
begin()
関数はsetup()
関数を終了し(そのあとTWENETの初期化が行われる)一番最初のloop()
の直前で呼ばれます。
void begin() {
sleepNow(); // the first time is just sleeping.
}
setup()
終了後にsleepNow()
を呼び出し初回スリープを実行します。
sleepNow()
void sleepNow() {
pinMode(PAL_MOT::PIN_SNS_INT, WAKE_FALLING);
the_twelite.sleep(60000, false);
}
スリープに入るまえに加速度センサーのDIOピンの割り込み設定をします。FIFOキューが一定数まで到達したときに発生する割り込みです。pinMode()
を用います。2番めのパラメータはPIN_MODE::WAKE_FALLING
を指定しています。これはHIGHからLOWへピンの状態が変化したときに起床する設定です。
3行目でthe_twelite.sleep()
でスリープを実行します。パラメータの60000は、TWELITE PAL ボードのウォッチドッグをリセットするために必要な起床設定です。リセットしないと60秒経過後にハードリセットがかかります。
wakeup()
加速度センサーのFIFO割り込みにより、スリープから復帰し起床すると wakeup()
が呼び出されます。そのあとloop()
が都度呼び出されます。wakeup()
の前に、UARTなどの各ペリフェラルやボード上のデバイスのウェイクアップ処理(ウォッチドッグタイマーのリセットなど)が行われます。例えばLEDの点灯制御を再始動します。
void wakeup() {
Serial << "--- PAL_MOT(Cont):" << FOURCHARS
<< " wake up ---" << mwx::crlf;
b_transmit = false;
txid[0] = 0xFFFF;
txid[1] = 0xFFFF;
}
ここではloop()
で使用する変数の初期化を行っています。
loop()
ここでは、加速度センサー内のFIFOキューに格納された加速度情報を取り出し、これをもとにパケット送信を行います。パケット送信完了後に再びスリープを実行します。
void loop() {
auto&& brd = the_twelite.board.use<PAL_MOT>();
if (!b_transmit) {
if (!brd.sns_MC3630.available()) {
Serial << "..sensor is not available."
<< mwx::crlf << mwx::flush;
sleepNow();
}
// send a packet
Serial << "..finish sensor capture." << mwx::crlf
<< " seq=" << int(brd.sns_MC3630.get_que().back().t)
<< "/ct=" << int(brd.sns_MC3630.get_que().size());
// calc average in the queue.
{
int32_t x = 0, y = 0, z = 0;
for (auto&& v: brd.sns_MC3630.get_que()) {
x += v.x;
y += v.y;
z += v.z;
}
x /= brd.sns_MC3630.get_que().size();
y /= brd.sns_MC3630.get_que().size();
z /= brd.sns_MC3630.get_que().size();
Serial << format("/ave=%d,%d,%d", x, y, z) << mwx::crlf;
}
for (int ip = 0; ip < 2; ip++) {
if(auto&& pkt =
the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet())
// set tx packet behavior
pkt << tx_addr(0x00)
<< tx_retry(0x1)
<< tx_packet_delay(0, 0, 2);
// prepare packet (first)
uint8_t siz = (brd.sns_MC3630.get_que().size() >= MAX_SAMP_IN_PKT)
? MAX_SAMP_IN_PKT : brd.sns_MC3630.get_que().size();
uint16_t seq = brd.sns_MC3630.get_que().front().t;
pack_bytes(pkt.get_payload()
, make_pair(FOURCHARS, 4)
, seq
, siz
);
// store sensor data (36bits into 5byts, alas 4bits are not used...)
for (int i = 0; i < siz; i++) {
auto&& v = brd.sns_MC3630.get_que().front();
uint32_t v1;
v1 = ((uint16_t(v.x/2) & 4095) << 20) // X:12bits
| ((uint16_t(v.y/2) & 4095) << 8) // Y:12bits
| ((uint16_t(v.z/2) & 4095) >> 4); // Z:8bits from MSB
uint8_t v2 = (uint16_t(v.z/2) & 255); // Z:4bits from LSB
pack_bytes(pkt.get_payload(), v1, v2); // add into pacekt entry.
brd.sns_MC3630.get_que().pop(); // pop an entry from queue.
}
// perform transmit
MWX_APIRET ret = pkt.transmit();
if (ret) {
Serial << "..txreq(" << int(ret.get_value()) << ')';
txid[ip] = ret.get_value() & 0xFF;
} else {
sleepNow();
}
}
}
// finished tx request
b_transmit = true;
} else {
if( the_twelite.tx_status.is_complete(txid[0])
&& the_twelite.tx_status.is_complete(txid[1]) ) {
sleepNow();
}
}
}
b_transmit
変数によってloop()
内の振る舞いを制御しています。送信要求が成功した後、この値を1にセットしパケット送信完了待ちを行います。
if (!b_transmit) {
最初にセンサーがavailableかどうかを確認します。割り込み起床後であるため、availableでないのは通常ではなく、そのままスリープします。
if (!brd.sns_MC3630.available()) {
Serial << "..sensor is not available."
<< mwx::crlf << mwx::flush;
sleepNow();
}
無線送信パケットでは使用しないのですが、取り出した加速度の情報を確認してみます。
Serial << "..finish sensor capture." << mwx::crlf
<< " seq=" << int(brd.sns_MC3630.get_que().front().t)
<< "/ct=" << int(brd.sns_MC3630.get_que().size());
// calc average in the queue.
{
int32_t x = 0, y = 0, z = 0;
for (auto&& v: brd.sns_MC3630.get_que()) {
x += v.x;
y += v.y;
z += v.z;
}
x /= brd.sns_MC3630.get_que().size();
y /= brd.sns_MC3630.get_que().size();
z /= brd.sns_MC3630.get_que().size();
Serial << format("/ave=%d,%d,%d", x, y, z) << mwx::crlf;
}
加速度センサーの計測結果はbrd.sns_MC3630.get_que()
で得られるFIFOキューに格納されます。
加速度センサーの計測結果を格納している構造体 axis_xyzt
は x, y, z の三軸の情報に加え、続き番号 t が格納されています。
格納されているサンプル数はキューのサイズ(brd.sns_MC3630.get_que().size()
)を読み出すことで確認できます。通常は28サンプルですが処理の遅延等によりもう少し進む場合もあります。最初のサンプルはfront()
で取得することができます。その続き番号はfront().t
になります。
ここでは、サンプルをキューから取り出す前にサンプルの平均をとってみます。キューの各要素にはfor文(for (auto&& v: brd.sns_MC3630.get_que()) { ... }
) でアクセスできます。for文内の v.x, v.y, v.z
が各要素になります。ここでは各要素の合計を計算しています。for文終了後は要素数で割ることで平均を計算しています。
次にパケットを生成して送信要求を行いますが、データ量が大きいため2回に分けて送信します。そのため送信処理がfor文で2回行われます。
for (int ip = 0; ip < 2; ip++) {
送信するパケットに含めるサンプル数とサンプル最初の続き番号をパケットのペイロードの先頭部分に格納します。
// prepare packet (first)
uint8_t siz = (brd.sns_MC3630.get_que().size() >= MAX_SAMP_IN_PKT)
? MAX_SAMP_IN_PKT : brd.sns_MC3630.get_que().size();
uint16_t seq = brd.sns_MC3630.get_que().front().t;
pack_bytes(pkt.get_payload()
, make_pair(FOURCHARS, 4)
, seq
, siz
);
最後に加速度データを格納します。先程は平均値の計算のためにキューの各要素を参照のみしましたが、ここではキューから1サンプルずつ読み出してパケットのペイロードに格納します。
for (int i = 0; i < siz; i++) {
auto&& v = brd.sns_MC3630.get_que().front();
uint32_t v1;
v1 = ((uint16_t(v.x/2) & 4095) << 20) // X:12bits
| ((uint16_t(v.y/2) & 4095) << 8) // Y:12bits
| ((uint16_t(v.z/2) & 4095) >> 4); // Z:8bits from MSB
uint8_t v2 = (uint16_t(v.z/2) & 255); // Z:4bits from LSB
pack_bytes(pkt.get_payload(), v1, v2); // add into pacekt entry.
brd.sns_MC3630.get_que().pop(); // pop an entry from queue.
}
加速度センサーからのデータキューの先頭を読み出すのは.front()
を用います。読みだした後.pop()
を用いて先頭キューを開放します。
加速度センサーから取得されるデータは1Gを1000としたミリGの単位です。レンジを±4Gとしているため、12bitの範囲に入るように2で割って格納します。データ数を節約するため最初の4バイトにX,Y軸とZ軸の上位8bitを格納し、次の1バイトにZ軸の下位4bitの合計5バイトを生成します。
2回分の送信待ちを行うため送信IDはtxid[]
配列に格納します。
MWX_APIRET ret = pkt.transmit();
if (ret) {
Serial << "..txreq(" << int(ret.get_value()) << ')';
txid[ip] = ret.get_value() & 0xFF;
} else {
sleepNow();
}
その後、loop()
中 b_transmit
が true
になっている場合は、完了チェックを行い、完了すれば sleepNow()
によりスリープします。
} else {
if( the_twelite.tx_status.is_complete(txid[0])
&& the_twelite.tx_status.is_complete(txid[1]) ) {
sleepNow();
}
}
送信完了に確認は the_twelite.tx_status.is_complete()
で行っています。txid[]
は送信時に戻り値として戻されたID値です。
1.15 - PulseCounter
パルスカウンターは、マイコンを介在せず信号の立ち上がりまたは立ち下りの回数を計数するものです。不定期のパルスを計数し一定回数までカウントが進んだ時点で無線パケットで回数を送信するといった使用方法が考えられます。
アクトの機能
- 子機側のDIO8に接続したパルスを計数し、一定時間経過後または一定数のカウントを検出した時点で無線送信する。
- 子機側はスリープしながら動作する。
アクトの使い方
必要なTWELITE
役割 | 例 |
---|---|
親機 | MONOSTICK BLUEまたはRED アクト Parent_MONOSTICK を動作させる。 |
子機 | 1.TWELITE DIP 2.BLUE PAL または RED PAL +環境センサーパル AMBIENT SENSE PAL |
アクトの解説
setup()
// Pulse Counter setup
PulseCounter.setup();
パルスカウンターの初期化を行います。
begin()
void begin() {
// start the pulse counter capturing
PulseCounter.begin(
100 // 100 count to wakeup
, PIN_INT_MODE::FALLING // falling edge
);
sleepNow();
}
パルスカウンターの動作を開始し、初回スリープを実行します。PulseCounter.begin()
の最初のパラメータは、起床割り込みを発生させるためのカウント数100
で、2番目は立ち下がり検出PIN_INT_MODE::FALLING
を指定しています。
wakeup()
void wakeup() {
Serial << mwx::crlf
<< "--- Pulse Counter:" << FOURCHARS << " wake up ---"
<< mwx::crlf;
if (!PulseCounter.available()) {
Serial << "..pulse counter does not reach the reference value." << mwx::crlf;
sleepNow();
}
}
起床時にPulseCounter.available()
を確認しています。availableつまりtrue
になっていると、指定したカウント数以上のカウントになっていることを示します。ここではfalse
の場合再スリープしています。
カウント数が指定以上の場合はloop()
で送信処理と送信完了待ちを行います。
loop()
uint16_t u16ct = PulseCounter.read();
パルスカウント値の読み出しを行います。読み出した後カウンタはリセットされます。
1.16 - WirelessUART
アクトの機能
- 2台のUART接続の TWELITE 同士をアスキー書式で通信する。
アクトの使い方
必要なTWELITE
PCにシリアル接続されている以下のデバイスを2台使います。
- MONOSTICK BLUE または RED
- TWELITE R でUART接続されているTWELITE DIPなど
Parent_MONOSTICK
でも受信できます。アクトの解説
setup()
void setup() {
auto&& set = the_twelite.settings.use<STG_STD>();
auto&& nwk = the_twelite.network.use<NWK_SIMPLE>();
/*** INTERACTIE MODE */
// settings: configure items
set << SETTINGS::appname("WirelessUART");
set << SETTINGS::appid_default(DEFAULT_APP_ID); // set default appID
set << SETTINGS::ch_default(DEFAULT_CHANNEL); // set default channel
set << SETTINGS::lid_default(DEFAULT_LID); // set default lid
set.hide_items(E_STGSTD_SETID::OPT_DWORD2, E_STGSTD_SETID::OPT_DWORD3, E_STGSTD_SETID::OPT_DWORD4, E_STGSTD_SETID::ENC_KEY_STRING, E_STGSTD_SETID::ENC_MODE);
set.reload(); // load from EEPROM.
/*** SETUP section */
// the twelite main class
the_twelite
<< set // from iteractive mode (APPID/CH/POWER)
<< TWENET::rx_when_idle(); // open receive circuit (if not set, it can't listen packts from others)
// Register Network
nwk << set; // from interactive mode (LID/REPEAT)
/*** BEGIN section */
SerialParser.begin(PARSER::ASCII, 128); // Initialize the serial parser
the_twelite.begin(); // start twelite!
/*** INIT message */
Serial << "--- WirelessUart (id=" << int(nwk.get_config().u8Lid) << ") ---" << mwx::crlf;
}
インタラクティブモードを初期化しています。このサンプルでは互いに論理デバイスID(LID)が異なるデバイスを2台以上用意します。
SerialParser.begin(PARSER::ASCII, 128);
シリアルパーサーを初期化します。
loop()
while(Serial.available()) {
if (SerialParser.parse(Serial.read())) {
Serial << ".." << SerialParser;
const uint8_t* b = SerialParser.get_buf().begin();
uint8_t addr = *b; ++b; // the first byte is destination address.
transmit(addr, b, SerialParser.get_buf().end());
}
}
シリアルからのデータ入力があった時点で、シリアルパーサーに1バイト入力します。アスキー形式が最後まで受け付けられた時点でSerialParser.parse()
はtrue
を戻します。
SerialParser
は内部バッファに対してsmplbuf
でアクセスできます。上の例ではバッファの1バイト目を送信先のアドレスとして取り出し、2バイト目から末尾までをtransmit()
関数に渡します。
on_rx_packet()
パケットを受信したときには、送信元を先頭バイトにし続くペイロードを格納したバッファsmplbuf_u8<128> buf
を生成し、出力用のシリアルパーサーserparser_attach pout
からシリアルに出力しています。
void on_rx_packet(packet_rx& rx, bool_t &handled) {
// check the packet header.
const uint8_t* p = rx.get_payload().begin();
if (rx.get_length() > 4 && !strncmp((const char*)p, (const char*)FOURCHARS, 4)) {
Serial << format("..rx from %08x/%d", rx.get_addr_src_long(), rx.get_addr_src_lid()) << mwx::crlf;
smplbuf_u8<128> buf;
mwx::pack_bytes(buf
, uint8_t(rx.get_addr_src_lid()) // src addr (LID)
, make_pair(p+4, rx.get_payload().end()) ); // data body
serparser_attach pout;
pout.begin(PARSER::ASCII, buf.begin(), buf.size(), buf.size());
Serial << pout;
}
}
テスト用のコマンド
テストデータは必ずペースト機能を用いてターミナルに入力してください。入力にはタイムアウトがあるためです。
参考: TWE ProgrammerやTeraTermでのペーストはAlt+Vを用います。
入力の末尾にCR LFが必要です。
最初はCR LFが省略できるXで終わる系列を試してください。終端文字列が入力されない場合は、その系列は無視されます。
例
:FE00112233X
:FE001122339C
任意の子機宛に00112233
を送付します。
例
:03AABBCC00112233X
:03AABBCC0011223366
子機3番に対してAABBCC00112233
を送付します。
例
:FF00112233X
:00112233X
任意の親機または子機宛(0xFF
)、親機宛(0x00
)に送付します。
1.17 - ユニバーサル レシーバー
MWXライブラリの twe_twelite.network
に NWK_LAYERED
、 twe_twelite.network2
に NWK_SIMPLE
を動作させることで、レイヤーツリーネット (TWELITE PAL, ARIAなど)のパケットを含む、さまざまな種類のパケットを受信、解釈できます。
ただし無線パケットは、同一チャネルであることと、同一アプリケーションIDであることが条件です。
main.cpp
setup()
, loop()
、受信パケットのコールバック関数 on_rx_packet()
を記述しています。
setup()
これらのオブジェクトは pkt_handler.cpp
で宣言され setup()
中でpnew()
により初期化されます。主にパケットのペイロード(データ)を解釈します。
mwx::pnew(g_pkt_pal);
mwx::pnew(g_pkt_apptwelite);
mwx::pnew(g_pkt_actsamples);
mwx::pnew(g_pkt_unknown);
2つのネットワークオブジェクトを生成しています。必ず the_twelite.network
にNWK_LAYERED
にします。
auto&& nwk_ly = the_twelite.network.use<NWK_LAYERED>();
auto&& nwk_sm = the_twelite.network2.use<NWK_SIMPLE>();
loop()
このサンプルではloop()
の処理で重要なのは約1秒おきに行っている .refresh()
処理です。g_pkt_apptwelite().refresh()
のみ重複チェッカのタイムアウト処理を行っています。それ以外のオブジェクトは何もしません。
if (TickTimer.available()) {
static unsigned t;
if (!(++t & 0x3FF)) {
g_pkt_pal.refresh();
g_pkt_apptwelite.refresh();
g_pkt_actsamples.refresh();
g_pkt_unknown.refresh();
}
}
on_rx_packet()
void on_rx_packet(packet_rx& rx, bool_t &handled) {
auto type = rx.get_network_type();
bool b_handled = false;
// PAL
if (!b_handled
&& type == mwx::NETWORK::LAYERED
&& g_pkt_pal.analyze(rx, b_handled)
) {
g_pkt_pal.display(rx);
}
// Act samples
if (!b_handled
&& type == mwx::NETWORK::SIMPLE
&& g_pkt_actsamples.analyze(rx, b_handled)
) {
g_pkt_actsamples.display(rx);
}
// Standard application (e.g. App_Twelite)
if (!b_handled
&& type == mwx::NETWORK::NONE
&& g_pkt_apptwelite.analyze(rx, b_handled)
) {
g_pkt_apptwelite.display(rx);
}
// unknown
if (!b_handled) {
g_pkt_unknown.analyze(rx, b_handled);
g_pkt_unknown.display(rx);
}
}
このサンプルコードで最も重要な部分です。auto type = rx.get_network_type();
によりパケットの種別を判定しています。
mwx::NETWORK::LAYERED
:NWK_LAYERED
レイヤーツリーネットのケットmwx::NETWORK::SIMPLE
:NWK_SIMPLE
のパケットmwx::NETWORK::NONE
: ネットワークなし(App_Tweliteなど)- その他 : エラーまたは未対応のパケット
mwx::NETWORK::NONE
の場合は、再送などで複数送信されうる同一パケットの重複チェッカ等の処理は MWX ライブラリ内部で行われません。これらの対応を記述する必要があります。本サンプルでは dup_checker.hpp
, dup_checker.cpp
を用意しています。
パケットの解釈はtsRxDataApp*
をラップした packet_rx&
オブジェクトを参照します。packet_rx
クラス自体は特別な機能はなく、get_psRxDataApp()
を用いてtsRxDataApp*
から得られる一部の情報へのアクセス手段を定義しているのみです。
pkt_common.hpp
パケット解釈部分のインタフェースを統一する目的で定義しています。
template <class D>
struct pkt_handler {
D& self() { return static_cast<D&>(*this); }
bool analyze(packet_rx& rx, bool &b_handled) {
return self().pkt.analyze(rx, b_handled);
}
void display(packet_rx& rx) {
Serial
<< crlf
<< format("!PKT_%s(%03d-%08x/S=%d/L=%03d/V=%04d)"
, self().get_label_packet_type()
, self().pkt.data.u8addr_src
, self().pkt.data.u32addr_src
, rx.get_psRxDataApp()->u8Seq
, rx.get_lqi()
, self().pkt.data.u16volt
);
self().disp_detail(rx);
}
void refresh() {
self()._refresh();
}
};
// packet analyzer for App_Twelite
class pkt_handler_apptwelite : public pkt_handler<pkt_handler_apptwelite> {
friend class pkt_handler<pkt_handler_apptwelite>;
pkt_apptwelite pkt;
void disp_detail(packet_rx& rx);
const char* get_label_packet_type() { return "AppTwelite"; }
void _refresh() { pkt.refresh(); }
public:
pkt_handler_apptwelite() : pkt() {}
};
analyze()
: パケットのペイロードを解釈する。display()
: パケット情報を表示する。refresh()
: 1秒おきの処理を記述します。self()
: 派生クラスD
にキャストします。
さらにパケット解釈クラス(上記例 pkt_handler_apptwelite
)には、メンバーオブジェクトの pkt
が含まれます。実際のパケットの解釈部分は、pkt_???.cpp
にある各々の analyze()
の実装を参照してください。
pkt_???.hpp
, pkt_???.cpp
パケット種別ごとのパケット解釈部 analyze()
と、データ構造 data
が定義されています。メンバーdata
は、構造体ですがPktDataCommon
の共通構造体を継承しています。この共通部を用いてパケットのデータのシリアル出力のコードを簡潔に記述しています。
pkt_pal
PAL関連のパケットに対応します。PALのパケット構造は複雑なデータ構造を持っています。ここでは EASTL のコンテナを用いた実装を行っています。
_vect_pal_sensors
:_pal_sensor
オブジェクトのプール。このオブジェクトは instusive map で使用するための専用クラスです。_map_pal_sensors
: センサーデータを効率よく検索するための intrusive map 構造。
1パケット中の複数データに対して各々が追加されるたびに_vect_pal_sensors
にエントリを確保して値を格納します。パケット中のすべてのデータを解釈した時点でセンサータイプをキーとした_map_pal_sensors
を構築します。
dup_checker
重複チェッカーを実装します。チェッカーの動作はテンプレート引数によってカスタマイズできます。
テンプレート引数
MODE
:MODE_REJECT_SAME_SEQ
を指定すると、同じシーケンス番号のパケットを除外します。パケット順が並び変わるような場合に使用します。MODE_REJECT_OLDER_SEQ
はより新しい番号を採用します。TIMEOUT_ms
: 重複データベースの初期化を行う間隔です。1000
と指定すると1秒経過したデータは抹消されます。直前では除外されていたパケットも、重複データベースの初期化されると再び採用されることになります。N_ENTRIES
: データ構造に最大確保される要素数です。N_BUCKET_HASH
: ハッシュ値の最大数です。素数を指定します。受信される無線ノードの種類をもとに決めます。
コンテナ
_mmap_entries
: intrusive ハッシュ マルチ マップ構造です。検索キーは無線ノードのシリアル番号です。_vect_pool
: マップ構造で用いられる要素を固定数(N_ENTRIES
)を確保します。_ring_vecant_idx
:_mmap_entries
に利用されていない_vect_pool
の要素を配列インデックス番号で管理します。リングバッファの構造で、要素を追加するときはリングバッファから値を一つ取り出し、削除するときはリングバッファに値を返します。
重複チェック
bool check_dup(uint32_t u32ser, uint16_t u16val, uint32_t u32_timestamp) {
// find entry by key:u32ser.
auto r = _mmap_entries.equal_range(u32ser);
...
}
マルチマップ構造からデータを検索するには .equal_range()
を呼び出します。得られた r
はイテレータで、同一のシリアル番号の要素を列挙します。
各要素(_dup_checker_entry
)にはタイムスタンプやシーケンス番号が記録されています。この値に従い重複を確認します。
1.18 - Unit_???
Unit_
で始まるアクト(act)は、ごく単機能の記述や動作を確認するためのものです。名前 | 内容 |
---|---|
Unit_ADC | ADCを動作させるサンプルです。100msごとにADCを連続実行しつつ約1秒おきに読み出し表示します。[s] キーでスリープします。 |
Unit_I2Cprobe | I2Cバスをスキャンして、応答のあるデバイス番号を表示します(この手順で応答しないデバイスもあります)。 |
Unit_delayMicoroseconds | delayMicroseconds() の動作を確認します。16MhzのTickTimerのカウントとの比較をします。 |
Unit_brd_CUE | TWELITE CUEの加速度センサー,磁気センサー,LEDの動作確認を行います。ターミナルから[a] ,[s] ,[l] キーを入力します。 |
Unit_brd_ARIA | TWELITE ARIAの温湿度センサー、磁気センサー、LEDの動作確認を行います。ターミナルから[t] ,[s] ,[l] キーを入力します。 |
Unit_brd_PAL_NOTICE | 通知パル(NOTICE PAL)のLED点灯を試します。起動時に全灯フラッシュ、その後はシリアル入力で操作します。 - r ,g ,b ,w : 各色の点灯モードをトグルする- R ,G ,B ,W : 各色の明るさを変更する(消灯・全灯時は無効)- c : 周期を変化させる(点滅時)- C : 点滅時のデューティを変化させる(点滅時) |
Unit_div100 | 10,100,1000の割り算と商を求めるdiv10() ,div100() ,div1000() の動作確認を行います。-99999~99999まで計算を行い通常の/ ,% による除算との経過時間の比較をします。 |
Unit_div_format | div10() ,div100() ,div1000() の結果を文字列出力します。 |
Unit_UART1 | UART1 (Serial1 ) の使用サンプルです。UART0からの入力をUART1に出力し、UART1からの入力をUART0に出力します。 |
Unit_Pkt_Parser | パケット情報のパーサーpktparser の使用サンプルです。App_Wingsの出力を解釈することが出来ます。※ TWELITE無線モジュール同士をシリアル接続して、一方を App_Wings として他方でその出力を解釈したいような場合です。他方をTWELITE無線モジュール以外に接続したい場合は他のプラットフォームを参照ください。 |
Unit_EEPROM | EEPROMの読み書きテストコードです。 |
Unit_Cue_MagBuz | TWELITE CUEの磁石センサーとSETピン(圧電ブザーを接続)を用いた、磁石を離すとブザーが鳴るプログラムです。 |
Unit_doint-bhv | IO割り込みを処理するビヘイビア記述例です。 |
Unit_EASTL | EASTL ライブラリを用いた断片コード集です。 |