TWELITEをプログラムする(BMP280を動かす その1)

疑似電波時計を作ろうと、時計用のサーバーと時計本体とのデータのやり取りにTWELITEを使おうと考えています。
alasixosaka.hatenablog.com

TWELITEはモノワイヤレス社が販売している、無線チップで、ZigBeeと同じく2.4GHz帯の電波を使います。
mono-wireless.com

Bluetoothも2.4GHzを使いますが、Bluetoothよりも電波の届きは良いです。Bluetoothだと離れた部屋に行くだけで無線が切れてしまいますが、TWE-LITEは自分の家では1階から3階まで問題なく電波が届きます。チップはブルーとレッドがあり、レッドの方が出力が大きいですが、家の中だけで使うならブルーで十分です。
TWELITEは標準で各種のアプリが用意されていて、これらを使うことで手軽に無線通信をすることができます。我が家でも温湿度計を家の中と外に置いて、それを親機で受けて温度表示をさせていますが、無線タグアプリを使っています。
TWELITEは目的に合致するアプリがない場合は自分で作ることができます。今回は時計用サーバーを構築するということで、プログラムにチャレンジしてみました。
サーバーの構成は前回書いたように、ESP8266ないしESP32を使ってインターネットから時刻を取得し、それをRTCモジュールに書き込み、TWELITEで送信するという構成です。TWELITEから送信するデータはESP側からシリアル通信で与えることもできますが、TWELITEはI2C通信に対応しているので、TWELITE自身からRTCモジュールにアクセスして時刻を取得して送信するということができます。ただし、そのためには自分でプログラムを書く必要があります。
こうすることで、ESPが起きている時間を極力減らして、1回/日程度に抑えたいと思っています。RTCモジュールは定期的に割り込みを発生させられるので、RTCモジュールから割り込みをかけて1回/日、ESPを起こして時刻を取得、RTCモジュールの時刻を正しい時刻に修正してまたスリープするという動作にすれば極力消費電力を抑えることができます。TWELITEの方も定期的に時刻を送信し、それ以外の時はスリープするようにすれば低消費電力での運用が可能です。そうすると、RTCモジュールがI2Cのスレーブで、マスターがESPとTWELITEの2つという構成になってしまいます。I2Cでは、複数のマスターに対応しているものもあるとのことですが、TWELITEやESPが対応しているかどうかはやってみないとわかりません。PICは対応しているそうなんですが。まあ、だめならESP側からTWELITEを強制的に殺すなどの処理をすることにしようと思っています。

まずはSDKをインストール

プログラミングするにはTWELIET STAGE SDKをインストールする必要があります。下記のページから対応するOSのソフトをダウンロードします。
mono-wireless.com
私はWindowsを使っていますので、Windows版をダウンロードしました。
それを適当なフォルダに解凍するだけです。

解凍はとても時間がかかる

解凍にはめちゃめちゃ時間がかかります。途中で残り2日と出たときはめん玉が飛び出しそうになりました。そんなにはかからなかったですが、ゆうに1時間以上はかかりました。

まずはI2Cの温湿度センサを動かしてみる

解凍したフォルダの中には、サンプルプログラムがいろいろ入っていて参考になります。いきなりRTCと通信するのは無謀なので、まずはおなじI2C通信の温湿度センサを動かしてみます。手持ちのセンサでI2C対応のBMP280というのがあるのでこいつを使ってみます。
ソースコードの記述はVisual Studio Codeがお勧めということなので素直にそれを使うことにします。
解凍したフォルダを MWSTAGE -> MWSDK -> ACT_samples とたどっていくといろいろなサンプルが入っています。 ACT0~ACT4まではLチカなどの無線を使わない初歩的なサンプルです。BRD_I2C_TEMPHUMIDというのがあり、温湿度計を動かすサンプルですが、残念ながらSHTC3とSHT40用のサンプルでそのままでは使えません。もう少し探すと別のACT_extrasというフォルダにACT_Sns_BME280_SHT30というフォルダがありこのサンプルが使えそうです。
まずはそのままTWELITEに書き込んでみます。
用意するものは、TWELITE本体(私はTWELITE DIPを使いました、アンテナが基板にプリントしているタイプのものですが、もうこれは売っていないようです。基本はマッチ棒タイプのアンテナのものだけになってしまいました。マッチ棒アンテナは箱に収める時に邪魔になるので、基板プリントタイプが使いやすくてよかったのですが)、それにTWELITERを使います。これも最新のものはTWELITER2になっています。私は旧版のTWELITERを持っていますのでそれを使いました。
まず、ACT_Sns_BME280_SHT30をフォルダごと、ACT_sampleのフォルダにコピーします。
次に、TWELITE DIPとTWELITERを繋いで、PCのUSB端子にUSBケーブルで接続します。そして、MWSTAGEフォルダ内のTWELITE_Stage.exeを起動します。
すると次のような画面が現れるので、TWELITE Rを選択します。

TWELITE STAGEの起動画面

すると次のような画面になるので、2のアプリ書き換えを選択します。

TWELITE STAGEメニュー画面

そうするとアプリ書き換えの方法を選択する画面が表示されます。ここでは2のActビルド&書換を選択します。

アプリの書換方法を選択する画面

そうすると、アプリを選択する画面が現れますので、先ほどコピーしたACT_Sns_BME280_SHT30を選択すると、コンパイルと書き込みが自動で行われます。

アプリを選択する画面

つぎに親機側のアプリを書き込みます。親機は、同様にTWELITER+TWELITE DIPを使うこともできますが、今回はMONOSTICKというスティック状で直接USBにさせる形状のものを使いました。TWILITERが子機書き込み用に使用されるので、STICKを一つ持っておけば何かと便利です。また、STICKは電波が届きにくいところがあった場合、中継器として役に立ちます。もちろん、DIPなどを中継器にしてもいいのですが、USB対応のACアダプターとSTICKがあれば、コンセントのある所に中継器を置くことができるので便利です。
MONOSTICKをパソコンのUSBポートにさして、同様にTWELITE_Stage.exeを起動します。すると、今度は画面にMONOSTICKと表示されるのでこれを選択します。

MONOSTICKを刺したときの起動画面

そして、同様にメニュー画面からアプリの書換を選択し、書換方法も同様にActビルド&書換を選択し、画面を繰っていって、Parent-MONOSTICKというのを選択します。ちなみに、その下にあるParent-MONOSTIC_2は後で出てきますが、自分で書き換えたプログラムです。
これで準備完了です、子機側のTWELITE DIPとBMP280を下の図のように配線して電源を投入し、親機のMONOSTICKをPCに刺して、TeraTermなどでシリアルポートを開いてモニタします。すると子機側から測定データが送られてきます。

TWELITEとBMP280の配線

もちろん、BMP280を使うだけならこれで使うことができますが、目的はRTCモジュールを動かすことにあるので、まずはプログラムの中身を見てすこしいじくってみたいと思います。あ、送られてきた画面は撮り忘れました。どんなデータがやり取りされているかは後で書きます。

子機用のプログラムを見てみる

まずはVSCODEで子機用のプログラムを見てみます。プログラムは、先のActEx_Sns_BME280_SHT30フォルダ内にある、ActEx_Sns_BME280_SHT30.cppに書かれています。全文を書き出すと長くなるのでポイントだけを見ていきます。
まず、5つのファイルをインクルードしています。

#include <TWELITE>
#include <NWK_SIMPLE>
#include <SNS_BME280>
#include <SNS_SHT3X>
#include <STG_STD>

1行目はTWELITEを使うための基本的な関数群を使うためのライブラリと思われます。ArduinoでもPICでも普通にやっているあれです(たぶん)。
2行目は無線通信用の関数群含むライブラリ、3行目と4行目はそれぞれ温湿度センサーBME280、SHT3xを使うためのライブラリをインクルードしています。
5行目はインタラクティブモードを使うためのライブラリです。
実は、TWELITEにはI2C通信を行うためのWireライブラリがあるのですが、この子機用プログラムでは、Wireライブラリを使わず(センサー用のライブラリ中で使っているのかもしれませんが)、センサー専用のライブラリを使っています。
次の部分は、コメントに書かれている通り、PALというセンサー専用のデバイスを使う場合に有効になるところです。今回の構成では無効にしても問題ありません。

/// if use with PAL board, define this.
#undef USE_PAL 
#ifdef USE_PAL
// X_STR makes string literals.
#define X_STR(s) TO_STR(s)
#define TO_STR(s) #s
#include X_STR(USE_PAL) // just use with PAL board (to handle WDT)
#endif

次の部分は初期設定です。

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

// channel
uint8_t CHANNEL = 13;

// id
uint8_t u8ID = 0;

// application use
const char_t APP_NAME[] = "ENV SENSOR";
const uint8_t FOURCHARS[] = "SBS1";

uint8_t u8txid = 0;
uint32_t u32tick_tx;

アプリケーションIDを1234abcdとしています。これは各サンプルActで共通のようです。もちろん自分で書き換えても構いません。そして、チャンネルは13を使います。これも、親機と子機で同じチャンネルを指定しておけば問題なく通信できます。むしろ同じチャンネルでいろいろ通信が飛び交うと混線するので、ひとつのネットワークにはそれぞれのチャンネルを割り振るのが良いと思います。今回はテストですので、そのまま13で動かしました。次のidは応答IDのことだと思います。次はアプリケーションの名前で"ENV_SENSOR"というのがこのアプリの名前のようです。そして、その次がアプリ固有の4文字で"SBS1"となっています。子機と親機のやり取りでこの4文字でどのアプリからの送信かを見分けているようです。詳しくは親機アプリの所で書きます。
次は送信元のシリアルIDだと思われます。そして、送信用の時間を設定する変数。
次の部分は、ループ中の処理をコントロールする変数です。E_STATEという変数をそれぞれのループ関数内でCase分岐を使って、処理ルーチンに振り分けるのに使っています。INITはイニシャライズ、CAPTURE_PREとCAPTUREで温湿度の取得、TXで無線送信、TX_WAIT_COMPは無線送信の終了待ち、SETTING_MODEはインタラクティブモードに入った状態の時に処理をせずリターンするためのもの、SUCCESSは送信の成功、ERRORは送信エラーです。

// very simple state machine
enum class E_STATE {
    INIT = 0,
    CAPTURE_PRE,
    CAPTURE,
    TX,
    TX_WAIT_COMP,
	SETTING_MODE,
    SUCCESS,
    ERROR
};
E_STATE eState = E_STATE::INIT;

そして、次の

/*** setup procedure (run once at cold boot) */
void setup() {

以下の部分が、初期設定の部分になります。これもArduinoではおなじみの処理です、大きくは違わないと思います。
ここではまずインタラクティブモードの設定をしています。

/// interactive mode settings
	auto&& set = the_twelite.settings.use<STG_STD>();

以下の部分がそうです。まず、setという変数にアプリケーションの名前とアプリケーションのIDを入れています。この2項目がインタラクティブモードで表示される部分です。引き続き、hide_itemsを設定しています。これらはインタラクティブモードで表示されません。

set << SETTINGS::appname(APP_NAME)          // set application name appears in interactive setting menu.
     << SETTINGS::appid_default(DEF_APPID); // set the default application ID.

set.hide_items(
	E_STGSTD_SETID::POWER_N_RETRY
	, E_STGSTD_SETID::OPTBITS
	, E_STGSTD_SETID::OPT_DWORD2
	, E_STGSTD_SETID::OPT_DWORD3
	, E_STGSTD_SETID::OPT_DWORD4
	, E_STGSTD_SETID::ENC_MODE
	, E_STGSTD_SETID::ENC_KEY_STRING
);

次がピンの設定です。DIOの12をインプットにしてプルアップしています。DIOピンをLowにして電源を入れるとインタラクティブモードに直接入るように設定しているようです。

// read SET pin (DIO12)
pinMode(PIN_DIGITAL::DIO12, PIN_MODE::INPUT_PULLUP);
if (digitalRead(PIN_DIGITAL::DIO12) == LOW) {
	set << SETTINGS::open_at_start();      // start interactive mode immediately.
	eState = E_STATE::SETTING_MODE;        // set state in loop() as dedicated mode for settings.
	return;                                // skip standard initialization.
}

そして、EEPROMからデータを読みだして、アプリケーションID、チャンネル、デバイスIDに代入しています。はじめの方で設定してた値との関係はどうなっているのかはよくわかりません。

// acquired EEPROM saved data	
set.reload(); // must call this before getting data, if configuring method is called.

APP_ID = set.u32appid();
CHANNEL = set.u8ch();
u8ID = set.u8devid();

次はそのアプリケーションIDとチャンネルをthe tweliteに設定しています。マニュアルによると初めに設定しておく必要があるみたいです。

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

ネットワークはシンプルモードとレイヤーツリーモードの2種類があり、ここではシンプルモードを設定しています。また、論理デバイスIDを設定しています。
論理デバイスIDは00が親機で01~100、特別なIDを指定しない場合は0xFEとなります。
それから、シリアルでメッセージを送信していますが、内容は省略します。
そして、

// sensors.setup() may call Wire during initialization.
Wire.begin(WIRE_CONF::WIRE_100KHZ);

// setup analogue
Analogue.setup();

I2C通信を開始して、アナログポートを有効にしています。
そのあとSHT3xのセンサーの有無を調べるところがありますが省略します。
次が、BME280のセンサーを調べるところです。BME280には、姉妹品のBMP280というセンサーがあり(今回使ったのはこちらの方)、BMP280は温度は測定出来ますが湿度が測定できませんので、どちらのセンサーかを調べて、対応を取っています。

// check BMx280
{
	bool b_alt_id = false;
	sns_bme280.setup();
	if (!sns_bme280.probe()) {
		b_alt_id = true;
		delayMicroseconds(100); // just in case, wait for devices to listen furthre I2C comm.
		sns_bme280.setup(0x77); // alternative ID
		if (sns_bme280.probe()) b_found_bme280 = true;
	} else {
		b_found_bme280 = true;
	}

	if (b_found_bme280) {
		// check if BME280 or BMP280	
		if ((sns_bme280.sns_stat() & 0xFF) == 0x60) {
			b_bme280_w_humid = true;
		} else
		if ((sns_bme280.sns_stat() & 0xFF) == 0x58) {
			b_bme280_w_humid = false;
			bme280_model[2] = 'P';
		}
		Serial << crlf
			<< format("..found %s ID=%02X", bme280_model, (sns_bme280.sns_stat() & 0xFF))
			<< (b_alt_id ? " at 0x77" : " at 0x76");
	}
}

最後に

/*** let the_twelite begin! */
the_twelite.begin(); // start twelite!

として、デバイスをスタートしています。
続いてループ関数に入ります。まず、アナログピンの状態を調べています。電圧値などを取得しているようです。詳細は省略します。
引き続き、上で書いた、E_STATE関数を使った処理に入ります。まず、SETTIN_MODEであればインタラクティブモードに入っていますので即座にリターンします。
INITの処理は次のようになっています。

case E_STATE::INIT:
		Serial << crlf << format("..%04d/start sensor capture.", millis() & 8191);
			
		// start sensor capture
		Analogue.begin(pack_bits(PIN_ANALOGUE::A1, PIN_ANALOGUE::VCC)); // _start continuous adc capture.

		if (b_found_sht3x) {
			sns_sht3x.begin();
		}
			
		if (b_found_bme280) {
			sns_bme280.begin();
		}

		eState =  E_STATE::CAPTURE_PRE;
		break;

最初の分はシリアルへのメッセージ出力です。次にアナログピンからデータを取得します。そして、SHT3xとBME280の各センサーが見つっていればそれぞれをセンサー値を取得します。
次のCAPTURE_PREでは、センサーの値が取得できるのを待っているようです。

case E_STATE::CAPTURE_PRE: // wait for sensor capture completion
		if (TickTimer.available()) {
			if (b_found_bme280 && !sns_bme280.available()) {
				sns_bme280.process_ev(E_EVENT_TICK_TIMER);
			}
			if (b_found_sht3x && !sns_sht3x.available()) {
				sns_sht3x.process_ev(E_EVENT_TICK_TIMER);
			}

			// both sensors are finished.
			if (	(!b_found_bme280 || (b_found_bme280 && sns_bme280.available()))
				&&	(!b_found_sht3x  || (b_found_sht3x && sns_sht3x.available()))
			) {
				new_state = true; // do next state immediately.
				eState =  E_STATE::CAPTURE;
			}
		}
	break;

そして、取得できれば、次のCAPTUREに移ってデータを取得します。

case E_STATE::CAPTURE: // display sensor results
		if (b_found_sht3x) {
				Serial 
					<< crlf << format("..%04d/finish sensor capture.", millis() & 8191)
					<< crlf << "  SHT3X    : T=" << sns_sht3x.get_temp() << 'C'
					<< " H=" << sns_sht3x.get_humid() << '%';
		}
		if (b_found_bme280) {
				Serial
					<< crlf << "  " << bme280_model << "   : T=" << sns_bme280.get_temp() << 'C'
					<< " P=" << int(sns_bme280.get_press()) << "hP";
			if (b_bme280_w_humid)
				Serial 
					<< " H=" << sns_bme280.get_humid() << '%';
		}
		if (1) {
				Serial
					<< crlf << format("  ADC      : Vcc=%dmV A1=%04dmV", u16_volt_vcc, u16_volt_a1);
		}
			
		new_state = true; // do next state immediately.
		eState =  E_STATE::TX;
		break;

そして送信に移ります。
まず、

case E_STATE::TX: // place TX packet requiest.
		eState = E_STATE::ERROR; // change this when success TX request...

E_STATEをERRORにしておき、通信が正常に終わったらSUCSESSに変えます。
次の部分で通信用のパケットを準備します。

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.
		, uint16_t(sns_sht3x.get_temp_cent()) // temp
		, uint16_t(sns_sht3x.get_humid_per_dmil())
		, uint16_t(sns_bme280.get_temp_cent()) // temp
		, uint16_t(sns_bme280.get_humid_per_dmil())
		, uint16_t(sns_bme280.get_press())
		, uint16_t(u16_volt_vcc)
		, uint16_t(u16_volt_a1)
	);

まず、最初は相手のアドレスを設定します。親機あての設定なので00としています。次が、再送回数で再送回数は1回です。次が送信開始までの時間と再送までの間隔です。引数は3つあり、最初が送信開始までの最短時間、次が送信開始までの最長時間、つまりこの間のどこかで送信されることになります。ここではどちらも0を指定しているので即時送信となります。3つ目が再送までの間隔で2mSです。次がパケットの本体で、4文字のアプリ固有データ、SHT3xの温度、湿度、BME280の温度、湿度、気圧、アナログ値の電圧などとなります。
そして、次でパケットを送信します。

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

そして、通信の状態を調べます。

  if (ret) {
	  u8txid = ret.get_value() & 0xFF;
	  u32tick_tx = millis();
	  eState = E_STATE::TX_WAIT_COMP;
  } else {
	  Serial << crlf << "!FATAL: TX REQUEST FAILS. reset the system." << crlf;
  }
} else {
Serial << crlf << "!FATAL: MWX TX OBJECT FAILS. reset the system." << crlf;
}
break;

成功なら、WAIT_COMPに移行、失敗ならメッセージを出力してループを抜けています。
次が通信の完了待ち

case E_STATE::TX_WAIT_COMP: // wait TX packet completion.
	if (the_twelite.tx_status.is_complete(u8txid)) {
		Serial << crlf << format("..%04d/transmit complete.", millis() & 8191);
		
		// success on TX
		eState = E_STATE::SUCCESS;
		new_state = true;
	} else if (millis() - u32tick_tx > 3000) {
		Serial << crlf << "!FATAL: MWX TX OBJECT FAILS. reset the system." << crlf;
		eState = E_STATE::ERROR;
		new_state = true;
	} 
	break;

成功なら、SUCSESSに移行、失敗ならERROR
次はERRORの処理で、デバイスをリセットしているようです。

case E_STATE::ERROR: // FATAL ERROR
	Serial.flush();
	delay(100);
	the_twelite.reset_system();
	break;

SUCSESSではスリープに入ります。

case E_STATE::SUCCESS: // NORMAL EXIT (go into sleeping...)
	sleepNow();
	break;
}

次はスリープの処理ルーチンです。

// perform sleeping
void sleepNow() {
	uint32_t u32ct = 1750 + random(0,500);
	Serial << crlf << format("..%04d/sleeping %dms.", millis() % 8191, u32ct);
	Serial.flush();

	the_twelite.sleep(u32ct);
}

スリープタイムはmsの設定で、1750mSに0から500msのランダム値を加えた時間スリープします。ちなみに、スリープ時間は32ビットの変数で与えられるので、
最大で4,294,967,296ms、だいたい1193時間まで設定できるようです。
次はスリープから目覚めたときの処理です。

// wakeup procedure
void wakeup() {
	Wire.begin();

	Serial	<< crlf << "--- " << APP_NAME << ":" << FOURCHARS << " wake up ---";

	eState = E_STATE::INIT; // go into INIT state in the loop()
}

I2C通信を開始して、シリアルにメッセージを出力し、ループをINITから開始します。
このプログラムで子機側からは次のようなパケットが送信されます。
:01AA008201029100000000A20012534253310A8E12B30AAA800103EA0D210007BE

これは、ActEx_Sns_BME280_SHT30のフォルダにあったReadMEからコピーしたものです。
まず、初めの1バイトが送信元の論理ID、次の1バイトがACTのパケットここではAAとなっていますが、実際はCCが送られていました。そして、次の1バイトが応答IDで00、次が送信元のシリアルIDで4バイト、次が送信先のシリアルIDで4バイト(親機宛なら00000000)、次がLQIで1バイト、電波の通信品質を表す数値のよう。この例ではA2になっています。次がデータのバイト数0012で18バイト、それからアプリ固有の4文字で"SBS1"のアスキーコードで53425331、2バイトずつのデータが7つ続き、SHT3xの温度、湿度、BME280の温度、湿度、気圧、アナログから読み取った電圧など、最後の1バイトがチェックサムです。

子機のプログラムの概略でした。長くなったので、親機については次回に書きます。