全てのページ
GitBook提供
1 / 13

サンプルアクト

Sample Acts

アクトの動作を理解するため、いくつかのサンプルを用意しています。

サンプルは MWSDK をインストールしたディレクトリにある Act_samples にあります。

最初にBRD_APPTWELITEとParent_MONOSTICKの解説に目を通すようにしてください。

最新版の入手

最新版のコードや MWSDK バージョン間の修正履歴を確認する目的で Github にソース一式を置いています。以下のリンク先を参照してください。

https://github.com/monowireless/Act_samples

共通の記述

アクトのサンプル中で以下の項目は共通の設定項目になり、以下で解説します。

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バイトで十分ですが、解説のための記述です。こういったシステム特有の識別子やデータ構造を含めるのも混信対策の一つです。

BRD_APPTWELITE

IO通信(標準アプリケーションApp_Tweliteの基本機能)

App_TweLite で必要な配線と同じ配線想定したボードサポート <BRD_APPTWELITE> を用いたサンプルです。

このサンプルは App_TweLite と通信できません。

アクトの機能

  • 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の配線をしておく。

配線例 (AI1-AI4は省略可)

アクトの解説

インクルード

// use twelite mwx c++ template library
#include <TWELITE>
#include <NWK_SIMPLE>
#include <BRD_APPTWELITE>

全てのアクトで<TWELITE>をインクルードします。ここでは、シンプルネットワーク <NWK_SIMPLE> とボードサポート <BRD_APPTWELITE>をインクルードしておきます。

宣言部

/*** Config part */
// application ID
const uint32_t APP_ID = 0x1234abcd;

// channel
const uint8_t CHANNEL = 13;

/*** function prototype */
MWX_APIRET transmit();
void receive();

/*** application defs */
const char APP_FOURCHAR[] = "BAT1";
uint8_t u8devid = 0;

uint16_t au16AI[5];
uint8_t u8DI_BM;
  • サンプルアクト共通宣言

  • 長めの処理を関数化しているため、そのプロトタイプ宣言(送信と受信)

  • アプリケーション中のデータ保持するための変数

セットアップ setup()

void setup() {
	// 変数の初期化
	for(auto&& x : au16AI) x = 0xFFFF;
	u8DI_BM = 0xFF;

	/*** SETUP section */
	// App_Twelite仕様のボード定義をシステムに登録します。
	auto&& brd = the_twelite.board.use<BRD_APPTWELITE>();

	// check DIP sw settings
	u8devid = (brd.get_M1()) ? 0x00 : 0xFE;

	// setup analogue
	Analogue.setup(true, ANALOGUE::KICK_BY_TIMER0); // setup analogue read (check every 16ms)

	// setup buttons
	Buttons.setup(5); // init button manager with 5 history table.

	// 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(u8devid); // set Logical ID. (0x00 means parent device)

	/*** BEGIN section */
	// start ADC capture
	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.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(" << int(u8devid) << ") ---" << mwx::crlf;
}

大まかな流れは、各部の初期設定、各部の開始となっています。

the_twelite

このオブジェクトはTWENETの中核としてふるまいます。

auto&& brd = the_twelite.board.use<BRD_APPTWELITE>();

ボードの登録(このアクトでは<BRD_APPTWELITE>を登録しています)。以下のように use の後に <> で登録したいボードの名前を指定します。

ユニバーサル参照(auto&&)にて得られた戻り値として、参照型でのボードオブジェクトが得られます。このオブジェクトにはボード特有の操作や定義が含まれます。

u8devid = (brd.get_M1()) ? 0x00 : 0xFE;

ここではボードオブジェクトを用いbrd.get_M1()を呼び出すことでM1ピンの設定を読み出します。1がスイッチをセットしGND側になっていることを示します。M1 がセットされた場合にu8devidを0x00に、そうでなければ0xFEを指定しています。これはネットワーク上での役割が親機(0x00)であるか子機(0xFE)あるかを決定します。この値は <NWK_SIMPLE>の設定に使用します。

	// 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() 受信回路をオープンにする指定

<<, >>演算子は本来ビットシフト演算子ですが、その意味合いと違った利用とはなります。MWXライブラリ内では、C++標準ライブラリでの入出力利用に倣ってライブラリ中では上記のような設定やシリアルポートの入出力で利用しています。

// 以下の記述は MWX ライブラリでは利用できません。
#include <iostream>
std::cout << "hello world" << std::endl;

次にネットワークを登録します。

auto&& nwksmpl = the_twelite.network.use<NWK_SIMPLE>();
nwksmpl << NWK_SIMPLE::logical_id(u8devid);

1行目は、ボードの登録と同じ書き方で <> には <NWK_SIMPLE>を指定します。

2行目は、<NWK_SIMPLE>の設定です。先ほどボードから得たM1ピンの状態によって親機アドレス(0x00)か子機アドレス(0xFE)を格納したu8devidを指定します。

the_twelite.begin(); // start twelite!

setup() 関数の末尾で the_twelite.begin() を実行しています。

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で利用できるピンの一覧が定義されています。

初回を除き 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()を呼び出したあと、タイマーが稼働します。

Serial

Serial オブジェクトは、初期化や開始手続きなく利用できます。

Serial << "--- BRD_APPTWELITE("
       << int(u8devid) 
       << ") ---" << mwx::crlf;

上記の例では "--- BRD_APPTWELITE(254) ---\r\n" といった文字列を表示します。ここでは10進数の数値文字列として表示するためにint(u8devid)としています。int型でない場合は出力の意味合いが変わりバイトの書き出しになるので注意してください。

ループ 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();
			}
		}
	}

	// receive RF packet.
  if (the_twelite.receiver.available()) {
		receive();
	}
}

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になります。

初回のIO状態確定時は MSB (bit31) に1がセットされます。スリープ復帰時も初回の確定処理を行います。

次にビットマップから値を取り出して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 が開始するまでは、データは直前に取得されたものとして読み出すことが出来ます。

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);
}

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の値が格納されていない場合は何もしません。

the_twelite.receiver

受信パケットがある場合の処理です。

if (the_twelite.receiver.available()) {
	receive();
}

無線パケットを受信後のloop()ではthe_twelite.receiver.available()がtrueを返します。受信パケットの取り扱いについては receive() 関数の解説で行います。

TWENETがパケットを受信してから時間をたってからの受信パケットの参照は安全ではありません。

内部のデータが新しい受信パケットの内容に上書きされ、この新しい内容を参照することになります。availableになってから速やかに処理するようにloop()を記述してください。内部的には、1パケット分余分に保持できる余裕はあります。

transmit()

無線パケットの送信要求をTWENETに行う関数です。本関数が終了した時点では、まだ無線パケットの処理は行われません。実際に送信が完了するのは、送信パラメータ次第ですが、数ms後以降になります。ここでは代表的な送信要求方法について解説します。

MWX_APIRET transmit() {
	if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
		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となります。

パケットの送信設定

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型を返していますが、このアクトでは使っていません。

戻り値には、要求の成功失敗の情報と要求に対応する番号が格納されています。送信完了まで待つ処理を行う場合は、この戻り値の値を利用します。

receive()

パケットの受信が確認できた(つまりthe_twelite.receiver.available()がtrueになった)ときの処理です。ここでは、相手方から伝えられたDI1..DI4の値とAI1..AI4の値を、自身のDO1..DO4とPWM1..PWM4に設定します。

void receive() {	
	auto&& rx = the_twelite.receiver.read();

  // 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]);
}

まず受信パケットのデータrxを取得します。rxからアドレス情報やデータペイロードにアクセスします。

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()が用意されています。

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番目以降のパラメータはデータペイロードの並びに一致した型の変数を、送り側のデータ構造と同じ順番で並べています。この処理が終われば、指定した変数にペイロードから読み出した値が格納されます。

確認のためシリアルポートへ出力します。

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()には引数の数の制限がありません。)

1行目の "DI:%04b" は"DI:0010"のようにDI1..DI4のビットマップを4桁で表示します。3行目の"/%04d"は"/3280/0010/0512/1023/1023"のように Vcc/AI1..AI4の値を順に整数で出力します。5行目の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レベル相当の出力になります。

PingPong

2台のシリアル接続しているTWELITEの片方からPING(ピン)の無線パケットを送信すると、他方からPONG(ポン)の無線パケットが返ってきます。

アクトの使い方

必要な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() 受信回路をオープンにする指定

<<, >>演算子は本来ビットシフト演算子ですが、その意味合いと違った利用とはなります。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);
		}
	}

	// receive RF packet.
    while (the_twelite.receiver.available()) {
		auto&& rx = the_twelite.receiver.read();

		// rx >> Serial; // debugging (display longer packet information)

		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;
	}
}

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になります。

初回のIO状態確定時は MSB (bit31) に1がセットされます。スリープ復帰時も初回の確定処理を行います。

// 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);

初回確定以外の場合かつPIN_BTNのボタンが離されたタイミングでvTransmit()を呼び出しています。押したタイミングにするには(!(btn_state && (1UL << PIN_BTN)))のように条件を論理反転します。

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の値が格納されていない場合は何もしません。

the_twelite.receiver

受信パケットがある場合の処理です。

while (the_twelite.receiver.available()) {
		auto&& rx = the_twelite.receiver.read();

		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;
	}

無線パケットを受信後のloop()ではthe_twelite.receiver.available()がtrueを返します。受信パケットの取り扱いについては receive() 関数の解説で行います。

TWENETがパケットを受信してから時間をたってからの受信パケットの参照は安全ではありません。

内部のデータが新しい受信パケットの内容に上書きされ、この新しい内容を参照することになります。availableになってから速やかに処理するようにloop()を記述してください。内部的には、1パケット分余分に保持できる余裕はあります。

まず受信パケットのデータ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()と同じ構文を利用できるようにしたヘルパークラスですが、引数の数が4つまでに制限されています。(Serial.printfmt()には引数の数の制限がありません。)

mwx::crlfは改行文字(CR LF)を、mwx::flushは出力完了待ちを指定します。

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()メソッドを用います。

このアクトでは使用しませんが、戻り値には、要求の成功失敗の情報と要求に対応する番号が格納されています。送信完了まで待つ処理を行う場合は、この戻り値の値を利用します。

Parent_MONOSTICK

親機アプリケーション(MONOSTICK用)

MONOSTICKを親機として使用するアクトです。子機からのパケットのデータペイロードをシリアルポートに出力します。

このアクトの解説の前にBRD_APPTWELITEの解説をご覧ください。

アクトの機能

  • サンプルアクトの子機からのパケットを受信して、アスキー形式で出力する。

アクトの使い方

必要なTWELITEと配線

役割

例

親機

MONOSTICK BLUEまたはRED

子機

サンプルアクトの子機設定 (例: BLUE PAL または RED PAL +AMBIENT SENSE PALにPAL_AMBアクトを書き込んだもの)

アクトの解説

インクルード

// use twelite mwx c++ template library
#include <TWELITE>
#include <NWK_SIMPLE>
#include <MONOSTICK>

MONOSTICK用のボードビヘイビア<MONOSTICK>をインクルードしています。このボードサポートには、LEDの制御、ウォッチドッグ対応が含まれます。

setup()

void setup() {
	/*** SETUP section */
	auto&& brd = the_twelite.board.use<MONOSTICK>();
	brd.set_led_red(LED_TIMER::ON_RX, 200); // RED (on receiving)
	brd.set_led_yellow(LED_TIMER::BLINK, 500); // YELLOW (blinking)

	// 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(0x00); // set Logical ID. (0x00 means parent device)

	/*** BEGIN section */
	the_twelite.begin(); // start twelite!

	/*** INIT message */
	Serial << "--- MONOSTICK_Parent act ---" << mwx::crlf;
}

ボードサポートの使用とLEDの動作を設定しています。

auto&& brd = the_twelite.board.use<MONOSTICK>();
brd.set_led_red(LED_TIMER::ON_RX, 200); // RED (on receiving)
brd.set_led_yellow(LED_TIMER::BLINK, 500); // YELLOW (blinking)

2行目では赤色のLEDを無線パケットを受信したら200ms点灯する設定をしています。最初のパラメータはLED_TIMER::ON_RXが無線パケット受信時を意味します。2番目は点灯時間をmsで指定します。

3行目はLEDの点滅指定です。1番目のパラメータはLED_TIMER::BLINKが点滅の指定で、2番目のパラメータは点滅のON/OFF切り替え時間です。500msごとにLEDが点灯、消灯(つまり1秒周期の点滅)を繰り返します。

auto&& nwksmpl = the_twelite.network.use<NWK_SIMPLE>();
nwksmpl << NWK_SIMPLE::logical_id(0x00);

ここではNWK_SIMPLE::logical_id(0x00)を設定し親機(論理ID=0x00)であることを指定します。

loop()

パケットを受信したとき、その内容を表示します。ここでは2種類の表示を行っています。

void loop() {
	// receive RF packet.
    while (the_twelite.receiver.available()) {
		auto&& rx = the_twelite.receiver.read();

		Serial << ".. coming packet (" << int(millis()&0xffff) << ')' << mwx::crlf;

		// output type1 (raw packet)
		//   uint8_t  : 0x01
		//   uint8_t  : src addr (LID)
		//   uint32_t : src addr (long)
		//   uint32_t : dst addr (LID/long)
		//   uint8_t  : repeat count
		//     total 11 bytes of header.
		//     
		//   N        : payload
		{
		  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;
		}

		// output type2 
		{
			smplbuf_u8<128> buf;
			mwx::pack_bytes(buf
				, uint8_t(rx.get_addr_src_lid())		// src addr (LID)
				, uint8_t(0xCC)							        // cmd id (0xCC, fixed)
				, uint8_t(rx.get_psRxDataApp()->u8Seq)	// seqence number
				, uint32_t(rx.get_addr_src_long())	// src addr (long)
				, uint32_t(rx.get_addr_dst())			  // dst addr
				, uint8_t(rx.get_lqi())					    // LQI
				, uint16_t(rx.get_length())				  // payload length
				, rx.get_payload() 					      	// payload
			);

			serparser_attach pout;
			pout.begin(PARSER::ASCII, buf.begin(), buf.size(), buf.size());
			
			Serial << "FMT PACKET -> ";
			pout >> Serial;
			Serial << mwx::flush;
		}
	}
}

最初の出力は<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()も同じ処理です)

2番目の出力はより実践的です。ユーザが定義した並び順で書式を構成します。

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行目は、シリアルパーサーの宣言と設定、出力です。

PAL_AMB

環境センサーパル AMBIENT SENSE PAL を用い、センサー値の取得を行います。

このアクトの解説の前にBRD_APPTWELITEの解説をご覧ください。

受信の確認のためParent_MONOSTICKの解説をご覧ください。

アクトの機能

  • 環境センサーパル AMPIENT SENSE PAL を用い、センサー値の取得を行います。

  • コイン電池で動作させるための、スリープ機能を利用します。

アクトの使い方

必要なTWELITE

役割

例

親機

MONOSTICK BLUEまたはRED

アクトParent_MONOSTICKを動作させる。

子機

BLUE PAL または RED PAL +環境センサーパル AMBIENT SENSE PAL

アクトの解説

インクルード

#include <TWELITE>
#include <NWK_SIMPLE>
#include <PAL_AMB> // include the board support of PAL_AMB

環境センサーパル <PAL_AMB> のボードビヘイビアをインクルードします。

setup()

void setup() {
	/*** SETUP section */
	// use PAL_AMB board support.
	auto&& brd = the_twelite.board.use<PAL_AMB>();
	// 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 */
	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;
}

最初にボードサポート <PAL_AMB> を登録します。ボードサポートの初期化時にセンサーやDIOの初期化が行われます。最初に行うのは、ボードのDIP SWなどの状態を確認してから、ネットワークの設定などを行うといった処理が一般的だからです。

auto&& brd = the_twelite.board.use<PAL_AMB>();

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)

このアクトではもっぱら無線パケットを送信しますので、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>();

	// mostly process every ms.
	if (TickTimer.available()) {
		
		//  wait until sensor capture finish
		if (!brd.sns_LTR308ALS.available()) {
			// this will take around 50ms.
			// note: to save battery life, perform sleeping to wait finish of sensor capture.
			brd.sns_LTR308ALS.process_ev(E_EVENT_TICK_TIMER);
		}

		if (!brd.sns_SHTC3.available()) {
			brd.sns_SHTC3.process_ev(E_EVENT_TICK_TIMER);
		}

		// now sensor data is ready.
		if (brd.sns_LTR308ALS.available() && brd.sns_SHTC3.available() && !b_transmit) {
			Serial << "..finish sensor capture." << mwx::crlf
				<< "  LTR308ALS: lumi=" << int(brd.sns_LTR308ALS.get_luminance()) << mwx::crlf
				<< "  SHTC3    : temp=" << brd.sns_SHTC3.get_temp() << 'C' << mwx::crlf
				<< "             humd=" << brd.sns_SHTC3.get_humid() << '%' << mwx::crlf
				<< mwx::flush;

			 // get new packet instance.
			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 payload
				pack_bytes(pkt.get_payload() // set payload data objects.
					, make_pair(FOURCHARS, 4)  // just to see packet identification, you can design in any.
					, uint32_t(brd.sns_LTR308ALS.get_luminance()) // luminance
					, uint16_t(brd.sns_SHTC3.get_temp())
					, uint16_t(brd.sns_SHTC3.get_humid())
				);

				// do transmit
				MWX_APIRET ret = pkt.transmit();
				Serial << "..transmit request by id = " << int(ret.get_value()) << '.' << mwx::crlf << mwx::flush;

				if (ret) {
					u8txid = ret.get_value() & 0xFF;
					b_transmit = true;
				}
				else {
					// fail to request
					sleepNow();
				}
			}
		}
	}

	// wait to complete transmission.
	if (b_transmit) {
		if (the_twelite.tx_status.is_complete(u8txid)) {		
			Serial << "..transmit complete." << mwx::crlf << mwx::flush;

			// now sleeping
			sleepNow();
		}
	}
}

このアクトでは、1ms周期で呼び出させる(アプリケーションループの呼び出しタイミングは不定期な遅延が発生します)TickTimerのイベントを起点としてセンサーの読み出しや送信要求を行います。TickTimer.available()がtrueになったときに処理を行います。これによりおよそ1msごとにif節内の処理を行います。

	if (TickTimer.available()) {

ボード上のセンサーは .sns_LTR308ALS または .sns_SHTC3 という名前でアクセスでき、このオブジェクトに操作を行います。センサーの完了待ちを行います。まだセンサーの取得が終わっていない場合(.available()がfalse)はセンサーに対して時間経過のイベント(.process_ev(E_EVENT_TICK_TIMER))を送付します。

		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);
		}

上記2つのセンサーがavailableになった時点で、無線送信などの処理を行います。

		if (brd.sns_LTR308ALS.available() 
						&& brd.sns_SHTC3.available() && !b_transmit) {

送信手続きについては他のアクトのサンプルと同様です。ここでは、再送1回、再送遅延を最小にする設定になっています。

	pkt << tx_addr(0x00)  // 親機0x00宛
		<< tx_retry(0x1)    // リトライ1回
		<< tx_packet_delay(0, 0, 2); // 遅延は最小限

パケットにセンサーデータを書き込み際に、センサーデータを取得しています。センサーの値を取得するメソッドはセンサーごとに違います。

	// prepare packet payload
	pack_bytes(pkt.get_payload() 
		, make_pair(FOURCHARS, 4)  
		, uint32_t(brd.sns_LTR308ALS.get_luminance()) // luminance
		, uint16_t(brd.sns_SHTC3.get_temp_cent())
		, uint16_t(brd.sns_SHTC3.get_humid_per_dmil())
	);

照度センサーは.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)

得られた値のうち温度値は int16_t ですが、送信パケットのデータ構造は符号なしで格納するため、uint16_tにキャストしています。

送信が成功すれば b_tansmit を trueにし、u8txidに送信パケットの識別IDを格納し、完了待ちします。送信が失敗すれば sleepNow() によりスリープします。

	// do transmit
	MWX_APIRET ret = pkt.transmit();

	if (ret) {
		u8txid = ret.get_value() & 0xFF;
		b_transmit = true;
	}
	else {
		// fail to request
		sleepNow();
	}

loop() 中 b_transmit が true になっている場合は、送信完了チェックを行い、完了すれば sleepNow() によりスリープします。

// wait to complete transmission.
if (b_transmit) {
    if (the_twelite.tx_status.is_complete(u8txid)) {        
        Serial << "..transmit complete." << mwx::crlf << mwx::flush;

        // now sleeping
        sleepNow();
    }
}

送信完了に確認は the_twelite.tx_status.is_complete(u8txid) で行っています。u8txidは送信時に戻り値として戻されたID値です。

startSensorCapture()

ボード上のセンサーの取得を開始します。多くのセンサーは、取得を開始してから完了するまで数msから数十msの時間を必要とします。

void startSensorCapture() {
	auto&& brd = the_twelite.board.use<PAL_AMB>();

	// start sensor capture
	brd.sns_SHTC3.begin();
	brd.sns_LTR308ALS.begin();
	b_transmit = false;
}

5行目で、温湿度センサー SHTC3 のデータ取得を開始します。

6行目で、照度センサー LTR308ALS のデータ取得を開始します。

7行目は、送信完了フラグをクリアします。

sleepNow()

スリープに入る手続きをまとめています。

void sleepNow() {
	uint32_t u32ct = 1750 + random(0,500);
	Serial << "..sleeping " << int(u32ct) << "ms." << mwx::crlf << mwx::flush;

	the_twelite.sleep(u32ct);
}

ここでは、起床までの時間を乱数により 1750ms から 2250ms の間に設定しています。これにより他の同じような周期で送信するデバイスのパケットとの連続的な衝突を避けます。

周期が完全に一致すると、互いのパケットで衝突が起き通信が困難になります。通常は時間の経過とともにタイマー周期が互いにずれるため、しばらくすると通信が回復し、また時間がたつと衝突が起きるという繰り返しになります。

3行目、この例ではシリアルポートからの出力を待ってスリープに入ります。通常は消費エネルギーを最小化したいため、スリープ前のシリアルポートの出力は最小限(または無し)にします。

スリープ前にflushを行うと、出力が不安定になる場合があります。

スリープに入るには the_twelite.sleep() を呼びます。この呼び出しの中で、ボード上のハードウェアのスリープ前の手続きなどが行われます。たとえばLEDは消灯します。

パラメータとしてスリープ時間をmsで指定しています。

TWELITE PAL では、必ず60秒以内に一度起床し、ウォッチドッグタイマーをリセットしなければなりません。スリープ時間は60000を超えないように指定してください。

wakeup()

スリープから復帰し起床すると wakeup() が呼び出されます。そのあとloop() が都度呼び出されます。wakeup()の前に、UARTなどの各ペリフェラルやボード上のデバイスのウェイクアップ処理が行われます。例えばLEDの点灯制御を再始動します。

void wakeup() {
	Serial	<< mwx::crlf
			<< "--- PAL_AMB:" << FOURCHARS << " wake up ---"
			<< mwx::crlf
			<< "..start sensor capture again."
			<< mwx::crlf;
	startSensorCapture();
}

ここではセンサーのデータ取得を開始しています。

応用編

より安全な実装

より安全な実装として、このアクトでは以下の例外処理を強化します。

  • センサー取得部分でavailableにならなかった場合の例外処理が記述されてません。

  • 送信完了待ちで送信完了が通知されない場合の例外処理が記述されていません。

上記いずれもタイムアウト処理を行います。現在の時間は millis() により取得できます。

uint32_t t_start;

  // 時間待ちの処理を開始した時点でタイムスタンプを保存
  t_start = millis();

...

  // loop() でタイムアウトのチェック
  if (millis() - t_start > 100) {
    sleepNow();
  }

消費エネルギーの削減

アクト PAL_AMB-UseNap は、センサーのデータ取得待ちをスリープで行い、より低消費エネルギーで動作できます。

PAL_AMB-usenap

PAL_AMB のサンプルを少し改良して、センサーデータ取得中の待ち時間(約50ms)を、スリープで待つようにします。

このアクトの解説の前にPAL_AMBのアクトの解説をご覧ください。

アクトの解説

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にするとメモリーを保持しないスリープになります。復帰後はwakup()は呼び出されじ、電源再投入と同じ処理になります。

4番目はウェイクアップタイマーの2番目を使う指定です。ここでは1番目は通常のスリープに使用して、2番目を短いスリープに用いています。このアクトでは2番目を使う強い理由はありませんが、例えば上述の5秒おきに起床したいような場合、短いスリープに1番目のタイマーを用いてしまうとカウンター値がリセットされてしまい、経過時間の補正計算が煩雑になるため2番目のタイマーを使用します。

あまり短いスリープ時間を設定してもスリープ復帰後のシステムの再初期化などのエネルギーコストと釣り合いません。目安として最小時間を30-50ms程度とお考え下さい。

PAL_AMB-behavior

ビヘイビアの記述サンプルです。詳細はこちらを参照ください。

PAL_MAG

開閉センサーパル OPEN-CLOSE SENSE PAL を用い、センサー値の取得を行います。

このアクトの解説の前にBRD_APPTWELITEの解説をご覧ください。

受信の確認のためParent_MONOSTICKの解説をご覧ください。

アクトの機能

  • 開閉センサーパル OPEN-CLOSE SENSE PAL を用い、磁気センサーの検出時に割り込み起床し、無線送信します。

  • コイン電池で動作させるための、スリープ機能を利用します。

アクトの使い方

必要なTWELITE

役割

例

親機

MONOSTICK BLUEまたはRED

アクトParent_MONOSTICKを動作させる。

子機

BLUE PAL または RED PAL +開閉センサーパル OPEN-CLOSE SENSE PAL

アクトの解説

インクルード

#include <TWELITE>
#include <NWK_SIMPLE>
#include <PAL_>

開閉センサーパルのボード ビヘイビア<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値です。

PAL_MOT

動作センサーパル MOTION SENSE PAL を用い、センサー値の取得を行います。

このアクトの解説の前にBRD_APPTWELITEの解説をご覧ください。

受信の確認のためParent_MONOSTICKの解説をご覧ください。

アクトの機能

  • 動作センサーパル MOTION SENSE PAL を用い、加速度センサーの加速度を連続的に計測し、無線送信します。

  • コイン電池で動作させるための、スリープ機能を利用します。

アクトの使い方

必要なTWELITE

役割

例

親機

MONOSTICK BLUEまたはRED

アクトParent_MONOSTICKを動作させる。

子機

BLUE PAL または RED PAL +動作センサーパル MOTION SENSE PAL

アクトの解説

インクルード

#include <TWELITE>
#include <NWK_SIMPLE>
#include <PAL_>

 動作センサーパルのボードビヘイビア<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値です。

PAL_MOT-oneshot

PAL_MOTアクトでは連続的に加速度データを取得して都度無線送信していました。このアクトではスリープ復帰後に数サンプル加速度データを取得しそのデータを送ります。

このアクトの解説の前にPAL_MOTのアクトの解説をご覧ください。

本サンプルは、収録バージョンによって差が大きいため本ページでは2つの解説を記載します。

  • v2 ... 状態変数を用いた loop() 実装に書き換え

  • 初版 ... MWSDK2020_05 版の SDK 添付

アクトの解説 (v2)

※ 最新のコードは「サンプルアクト>最新版の入手」を参照ください。

起床→加速度センサーの取得開始→加速度センサーのFIFO割り込み待ち→加速度センサーのデータの取り出し→無線送信→スリープという

起床→加速度センサーの取得開始→加速度センサーのFIFO割り込み待ち→加速度センサーのデータの取り出し→無線送信→スリープという流れになります。

加速度センサーは、FIFOキューが一杯になるとFIFOキューへのデータ追加を停止します。

状態変数

enum class E_STATE {
	INIT = 0,
	START_CAPTURE,
	WAIT_CAPTURE,
	REQUEST_TX,
	WAIT_TX,
	EXIT_NORMAL,
	EXIT_FATAL
} eState;

列挙体として eState 変数を宣言しています。

begin()

void begin() { 
	// 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>();
	bool loop_more;
	do {
	  loop_more = false;
	  switch(eState) {
 	    ...
	  }
	} while(loop_more);

loop() の基本構造は状態変数eStateによるswitch ... case節です。eStateの初期状態はINITです。loop_moreは状態変数を書き換えた直後、loop()を抜ける前にもう一度実行したいときにtrueにセットします。

以下では各case節を解説します。eStateの初期状態はINITです。

			case E_STATE::INIT:
				brd.sns_MC3630.get_que().clear(); // clear queue in advance (just in case).
				loop_more = true;
				eState = E_STATE::START_CAPTURE;
			break;

状態INITでは、初期化(結果格納用のキューのクリア)を行います。

			case E_STATE::START_CAPTURE:
				u32tick_capture = millis();
				brd.sns_MC3630.begin(
					// 400Hz, +/-4G range, get four samples (can be one sample)
					SnsMC3630::Settings(
						SnsMC3630::MODE_LP_400HZ, SnsMC3630::RANGE_PLUS_MINUS_4G, 4)); 
				eState = E_STATE::WAIT_CAPTURE;
			break;

状態START_CAPTUREでは、MC3630センサーのFIFO取得を開始します。ここでは400Hzで4サンプル取得できた時点でFIFO割り込みが発生する設定にしています。タイムアウトのチェックのため、開始時点のシステム時刻をu32tick_captureに格納します。

			case E_STATE::WAIT_CAPTURE:
				if (brd.sns_MC3630.available()) {
					brd.sns_MC3630.end(); // stop now!
					eState = E_STATE::REQUEST_TX; loop_more = true;
				} else if ((millis() - u32tick_capture) > 100) {
					Serial << crlf << "!!!FATAL: SENSOR CAPTURE TIMEOUT.";
					eState = E_STATE::EXIT_FATAL;
				}
			break;

状態WAIT_CAPTUREでは、FIFO割り込みを待ちます。割り込みが発生し結果格納用のキューにデータが格納されるとsns_MC3630.available()がtrueになります。

タイムアウトした場合は状態EXIT_FATALに遷移します。

			case E_STATE::REQUEST_TX:
				u32tick_tx = millis();
				txid = TxReq();
				if (txid) {
					eState = E_STATE::WAIT_TX;
				} else {
					Serial << crlf << "!!!FATAL: TX REQUEST FAILS.";
					eState = E_STATE::EXIT_FATAL;
				}
			break;

状態REQUEST_TXではローカル定義関数TxReq()を呼び出し、得られたセンサーデータの処理と送信パケットの生成、そうし尿級を行います。タイムアウトのチェックのため、開始時点のシステム時刻をu32tick_txに格納します。

			case E_STATE::WAIT_TX:
				if(the_twelite.tx_status.is_complete(txid.get_value())) {
					eState = E_STATE::EXIT_NORMAL; loop_more = true;
				} else if (millis() - u32tick_tx > 100) {
					Serial << crlf << "!!!FATAL: TX TIMEOUT.";
					eState = E_STATE::EXIT_FATAL;
				}
			break;

状態WAIT_TXでは、無線パケットの送信完了を待ちます。

タイムアウト時には状態EXIT_FATALに遷移します。

			case E_STATE::EXIT_NORMAL:
				sleepNow();
			break;

			case E_STATE::EXIT_FATAL:
				Serial << flush;
				the_twelite.reset_system();
			break;

一連の動作が完了したときは状態EXIT_NORMALに遷移しローカル定義の関数sleepNow()を呼び出しスリープを実行します。またエラーを検出した場合は状態EXIT_FATALに遷移し、システムリセットを行います。

MWX_APIRET TxReq()

この関数では、センサーより得られたサンプル値の取得と、複数サンプルの平均値の計算、

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();

取得サンプルの平均値を計算します。

ここでは除算を行っていますが、TWELITEマイコンには除算回路がないため、計算時間を要する演算となります。例えば以下のような改良が考えられます。

  • サンプル数を2のべき乗として、その数を変数に入れず直接指定した除算を行う(ビットシフトによる演算に最適化されます)。

  • 平均値を求めず、合計値とサンプル数を送り、受信先で計算する。

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()));

brd.sns_MC3630.get_que().clear(); // clean up the queue

X軸の最大値と最小値を計算します。

ここではイテレータとstd::minmax_element()アルゴリズムを用いて計算します。get_axis_x_iterはキューのイテレータをパラメータとして、axis_xyzt構造体の.xにアクセスするものです。

C++ Standard Template Library のアルゴリズムを使用する例としてstd::mimmax_element紹介していますが、上述のforループ内で最大、最小を求めても構いません。

ここでキューをクリア.sns_MC3630.get_que().clear()しています。

	// 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(x)
				, uint16_t(y)
				, uint16_t(z)
				, uint16_t(*x_minmax.first)  // minimum of captured x
				, uint16_t(*x_minmax.second) // maximum of captured x
			);

		// perform transmit
		ret = pkt.transmit();

		if (ret) {
			Serial << "..txreq(" << int(ret.get_value()) << ')';
		}

最期にパケットの生成と送信を要求を行います。パケットには X, Y, Z 軸の加速度、X軸の最小値,Y軸の最小値を含めています。

アクトの解説 (初版)

MWSDK2020_05 版の SDK 添付のサンプルコードです。

※ 最新のコードは「サンプルアクト>最新版の入手」を参照ください。

起床→加速度センサーの取得開始→加速度センサーのFIFO割り込み待ち→加速度センサーのデータの取り出し→無線送信→スリープという流れになります。

加速度センサーは、FIFOキューが一杯になるとFIFOキューへのデータ追加を停止します。

wakeup()

起床後加速度センサーを稼働させます。

void wakeup() {
	Serial << mwx::crlf << "--- PAL_MOT(OneShot):" << FOURCHARS << " wake up ---" << mwx::crlf;
	auto&& brd = the_twelite.board.use<PAL_MOT>();

	brd.sns_MC3630.get_que().clear(); // clear queue in advance (just in case).
	brd.sns_MC3630.begin(SnsMC3630::Settings(
			SnsMC3630::MODE_LP_400HZ, SnsMC3630::RANGE_PLUS_MINUS_4G, 4)); 
				// 400Hz, +/-4G range, get four samples (can be one sample)

	b_transmit = false;
	txid = 0xFFFF;
}

加速度センサーの結果を保存するキューの内容を抹消(.sns_MC3630.get_que().clear())しておきます。加速度センサーのサンプルの取得忘れがあったり、また停止させるまでに次のサンプルが取得したような場合を想定します。

ここで加速度センサーを都度開始します。設定は400Hz,±4G,FIFO割り込みは4サンプルとしています。4サンプルも必要ない場合は1サンプルの設定でも構いません。

loop()

void loop() {
	auto&& brd = the_twelite.board.use<PAL_MOT>();

	if (!b_transmit) {
		if (brd.sns_MC3630.available()) {
			brd.sns_MC3630.end(); // stop now!

			Serial << "..finish sensor capture." << mwx::crlf
				<< "  ct=" << int(brd.sns_MC3630.get_que().size());

			// get all samples and average them.
			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;

			// just see X axis, min and max
			//auto&& x_axis = get_axis_x(brd.sns_MC3630.get_que());
			//auto&& x_minmax = std::minmax_element(x_axis.begin(), x_axis.end());
			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()));

			brd.sns_MC3630.get_que().clear(); // clean up the queue

			// prepare tx packet
			if (auto&& pkt = nwk.the_twelite.network.use<NWK_SIMPLE>()) {
				auto&& pkt = nwk.prepare_tx_packet(); // get new packet instance.

				// 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(x)
						, uint16_t(y)
						, uint16_t(z)
						, uint16_t(*x_minmax.first)  // minimum of captured x
						, uint16_t(*x_minmax.second) // maximum of captured x
					);

				// perform transmit
				MWX_APIRET ret = pkt.transmit();
				
				if (ret) {
					Serial << "..txreq(" << int(ret.get_value()) << ')';
					txid = ret.get_value() & 0xFF;
				} else {
					sleepNow();
				}
				
				// finished tx request
				b_transmit = true;
			}
		}
	} else {
		// wait until transmit completion.
		if(the_twelite.tx_status.is_complete(txid)) {
			sleepNow();
		}
	}
}

このアクトでは、サンプル取得後、すぐに加速度センサーの動作を停止します。

if (brd.sns_MC3630.available()) {
	brd.sns_MC3630.end(); // stop now!

取得サンプルの平均値を計算します。

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();

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()));

brd.sns_MC3630.get_que().clear(); // clean up the queue

ここではイテレータとstd::minmax_element()アルゴリズムを用いて計算します。get_axis_x_iterはキューのイテレータをパラメータとして、axis_xyzt構造体の.xにアクセスするものです。

最後にキューをクリア.sns_MC3630.get_que().clear()しています。

PulseCounter

パルスカウンターを用いたアクト例です。

パルスカウンターは、マイコンを介在せず信号の立ち上がりまたは立ち下りの回数を計数するものです。不定期のパルスを計数し一定回数までカウントが進んだ時点で無線パケットで回数を送信するといった使用方法が考えられます。

このアクトの解説の前にBRD_APPTWELITEの解説をご覧ください。

受信の確認のためParent_MONOSTICKの解説をご覧ください。

アクトの機能

  • 子機側の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();

パルスカウント値の読み出しを行います。読み出した後カウンタはリセットされます。

WirelessUART

WirelessUARTはシリアル通信を行います。

このアクトの解説の前にBRD_APPTWELITEの解説をご覧ください。

アクトの機能

  • 2台のUART接続のTWELITE同士をアスキー書式で通信する。

アクトの使い方

必要なTWELITE

いずれかを2台。

  • MONOSTICK BLUE または RED

  • TWELITE R でUART接続されているTWELITE DIPなど

アクトの解説

setup()

void setup() {
	/*** SETUP section */
	// 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>();
	
	uid = random(1, 5); // set uid by random() (1..4)
	nwk	<< NWK_SIMPLE::logical_id(uid); // set Logical ID. (0xFE means a child device with no ID)

	/*** BEGIN section */
	SerialParser.begin(PARSER::ASCII, 128); // Initialize the serial parser
	the_twelite.begin(); // start twelite!

	/*** INIT message */
	Serial << "--- WirelessUart (id=" << int(uid) << ") ---" << mwx::crlf;
}

論理IDはrandom(1,5)により1,2,3,4のいずれかの値に割り当てています。通常は、論理IDを設定保存したデータ、またはDIP SWといったハードウェアの情報から生成します。

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()関数に渡します。

パケットを受信したときには、送信元を先頭バイトにし続くペイロードを格納したバッファsmplbuf_u8<128> bufを生成し、出力用のシリアルパーサーserparser_attach poutからシリアルに出力しています。

if (the_twelite.receiver.available()) {
	auto&& rx = the_twelite.receiver.read();
	
	// 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を送付します。

TWE Programmerのターミナル機能を用いて送付する場合は、Alt+Vキーを用いてペーストします。

Slp_Wk_and_Tx

Slp_Wk_and_Tx は、定期起床後、何か実行(センサーデータの取得など)を行って、その結果を無線パケットとして送信するようなアプリケーションを想定した、テンプレートソースコードです。

setup(), loop() の形式では、どうしても loop() 中が判読しづらい条件分岐が発生しがちです。本Actでは、loop()中を switch構文による単純な状態遷移を用いることで、コードの見通しを良くしています。

このアクトの解説の前にBRD_APPTWELITEの解説をご覧いただくことを推奨します。

TWELITE STAGE 2020_05 には収録されていません。以下のリンク(GitHub)より入手ください。

https://github.com/monowireless/Act_samples

アクトの機能

  • 起動後、速やかにスリープする

    1. setup() : 初期化する

    2. begin() : スリープに遷移する

  • スリープ起床後、状態変数を初期化し、以下の順に動作を行う

    1. wakeup(): スリープからの起床、各初期化を行う

    2. loop()状態INIT->WORK_JOBに遷移: 何らかの処理を行う(このActでは 1ms ごとの TickCount ごとにカウンタを更新し 100 カウント後にTX状態に進む)

    3. loop() 状態TX: 送信要求を行う

    4. loop() 状態WAIT_TX: 送信完了待ちを行う

    5. loop() 状態EXIT_NORMAL: スリープする (1. に戻る)

  • loop() 状態EXIT_FATAL: エラーが発生した場合は、モジュールリセットする

アクトの解説

インクルード

#include <TWELITE>
#include <NWK_SIMPLE>

#include "Common.h"

パケット送信を行うため <NWK_SIMPLE> をインクルードしています。また、アプリケーションIDなど基本的な定義は "Common.h" に記述しています。

Common.h には基本的な定義に加え、以下の列挙体が定義されています。こちらを状態変数として利用します。

enum class E_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)
};

setup()

void setup() {
	/*** SETUP section */
	txreq_stat = MWX_APIRET(false, 0);

	// 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;
}

the_tweliteクラスオブジェクトの初期化とネットワーク <NWK_SIMPLE> の登録を行います。

begin()

void begin() {
	Serial << "..begin (run once at boot)" << crlf;
	SleepNow();
}

setup()の直後に一度だけ呼び出されます。SleepNow()関数夜を呼び出して初回のスリープ手続きを行います。

wakeup()

void wakeup() {
	Serial << crlf << int(millis()) << ":wake up!" << crlf;
	eState = E_STATE::INIT;
}

起床直後に呼び出されます。ここでは状態変数eStateを初期状態E_STATE::INITにセットします。この後loop()が呼び出されます。

loop()

void loop() {
	bool loop_more; // if set, one more loop on state machine.
	do {
		loop_more = false; // set no-loop at the initial.
		switch(eState) {
			case E_STATE::INIT: 
				eState = E_STATE::WORK_JOB; loop_more = true;
				break;
			case E_STATE::WORK_JOB:
			  if (TickTimer.available())
			    if(--dummy_work_count == 0)
						eState = E_STATE::TX: loop_more = true;
				break;
			case E_STATE::TX:
			  txreq_stat = vTransmit();
				if(txreq_stat) {
				  u32millis_tx = millis();
				  eState = E_STATE::WAIT_TX;
				} else {
				  eState = E_STATE::EXIT_FATAL; loop_more = true;
				}
			  break;
			case E_STATE::WAIT_TX:
			  if (the_twelite.tx_status.is_complete(txreq_stat.get_value())) {
			    eState = E_STATE::EXIT_NORMAL; loop_more = true;
			  } else if (millis() - u32millis_tx > 100) {
			    eState = E_STATE::EXIT_FATAL; loop_more = true;
			  }
			  break;
			case E_STATE::EXIT_NORMAL:
				SleepNow();
				break;
			case E_STATE::EXIT_FATAL:
				the_twelite.reset_system();
				break;
		}
	} while (loop_more);

上記のコードは、実際のコードを簡略化したものです。

このコードではloop_moreを条件としてdo..while() 構文のループになっています。これはloop()を脱出せずに次の状態のコード(case節)を実行する目的です。

wakeup()でINIT状態にセットされていますので初回のloop()ではINITが評価されます。ここは変数の初期化しているだけです。続く状態の処理を行うのにloop()を脱出する必要がないためloop_moreをtrueにしています。

WORK_JOB状態ではTichTimer.available()になるたびにカウンタを減算し0になったら次の状態TXに遷移します。

TX状態ではvTransmit()関数を呼び出しパケット送信要求を行います。この時点ではまだパケットが送信されていないため、この時点でスリープをしてはいけません。パケット送信要求が成功したらWAIT_TX状態に遷移します。タイムアウトの管理のため、この時点でのシステム時間u32millis_txを保存します。失敗したらEXIT_FATALに遷移します(モジュールのリセット実行)。

WAIT_TX状態では送信IDに対応する送信が完了したかを判定し、完了したらEXIT_NORMAL、100ms以上経過したらEXIT_FATAL状態に遷移します。

EXIT_NORMAL状態はSleepNow()を呼び出し、スリープします。

EXIT_FATAL状態ではシステムリセットを行います。

void 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 << mwx::flush;
	the_twelite.sleep(u16dur, false);
}

周期スリープを行います。スリープ時間はrandom()関数を用いて、一定の時間ブレを作っています。これは複数のデバイスの送信周期が同期した場合、著しく失敗率が上がる場合があるためです。

MWX_APIRET vTransmit()

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.
		);
		
		// do transmit 
		//return nwksmpl.transmit(pkt);
		return pkt.transmit(); 
	}

	return MWX_APIRET(false, 0);
}

ID=0x00の親機宛に無線パケットの送信要求を行います。格納されるデータはActサンプルで共通に使われている4文字識別子(FOURCC)に加え、システム時間[ms]が含まれます。

MWX_APIRETはuint32_t型をラップしたクラスで、MSBを失敗成功のフラグとし、以下31ビットをデータとして用いています。pkt.transmit()の戻り型になっており、送信要求の成功と失敗ならびに送信IDをデータ部に格納します。