北摂フォトロゲのお手伝いに行ってきました。

昨日、12/4は北摂フォトロゲが開催され、もう毎年恒例になりつつありますが、お手伝いに行ってきました。
フォトロゲとは、というのは昔も書いたかもしれませんが、念のため説明しておくと、ロゲは正式にはロゲイニングと言って、オリエンテーリングなどのナビゲーションゲームズの一種です。フォトロゲは、オリエンテーリングをよく知っている人ならポイントオリエンテーリングに似た競技と言えばわかりやすいと思います。地図上に書かれたポイントを制限時間内に回って、その得点を競う競技です。オリエンテーリングと違ってポイントに行ってもフラッグのような目印はなく、そこに行ったという証拠写真を撮ってくるというものです。地図を使ったナビゲーション競技には、そのほかに、マウンテンマラソンマラニックと呼ばれるもっと長距離のものもありますし、アドベンチャーレースでもランの競技では地図を使ってポイントを回ります。グレートトラバースで有名な田中陽希さんも富士のアドベンチャーレースの前に読図講習会に参加している様子がTVの放送でやっていました。また、この間放送していた、TJAR(トランスジャパンアルプスレース)でも予選会では、地図を使ったナビゲーションのテストの様子が放送されていました。トレランに近い競技ではOMM(オリジナルマウンテンマラソン)なんかもあったりします。
フォトロゲの話に戻ります。お天気は幸いにして雨が少しパラついたくらいで何とか持ってくれました。また、先週は週の半ばに寒気が入ってぐっと冷え込んだんですが、日曜日は気温も少し上がってそれほど寒く無い気候で外でひたすらお仕事をしていた身にとってはありがたかったです。
当日の様子は、公式サイトを見ていただくとして。
http://ooa.pro.tok2.com/rogaine/2022/index.html

フォトロゲの運営をしているときに、いつも気になるのが参加者の皆さんのギアです。どんな服を着ているのか、どんなザックを使っているのか、どんなシューズを履いているのか、結構観察してしまいます。
北摂フォトロゲには、3時間の部と、5時間の部があって、3時間の部だとファミリークラスとかあってハイキングのような恰好の人も結構おられましたが、5時間の部になるとさすがにガチの格好をした人ばかりでしたね。
昨日は、天気予報がそれほど良くなかったというのもあってが、軽量のレインジャケットを羽織っている方を何人か見かけました。見たところモンベルのパーサーライトのような感じでしたね。やっぱり軽くて使いやすいんでしょうか?
ウェアに関しては、ランニング用のウェアが主流のような感じでしたね。下はタイツ姿の人が多かったように思います。中には短パンで走っていた人もおられました。
ザックに関しては、本当にいろいろあって、こんなに種類があるのかと思うくらいです。見たことないのもたくさんありました。基本はトレラン用のベストタイプのものが多かったですね。
シューズは、自分がこの間買った、アルトラのモンブランBOAを使っている人がおられました。やあ、やっぱりいたかという感じですが。私はまだ短い距離しか走っていませんが、いいシューズだと思います。ローンピークよりもグリップが良く、クッションもローピークほど柔らかすぎず適度な感じで自分的には走りやすいですね。これが長い距離走った時にどうなるかは走ってみないとわかりませんが。
そもそも、自分がトレランを始めたきっかけは、この北摂フォトロゲのお手伝いをしたことと、TVの放送で、モンゴルのトレイルレースにシンガポールの65歳(当時)の女性が参加されていて完走していたのを見て、この年でもできるんじゃないかと思って始めたのがきっかけで、その時は還暦を迎えるまでに100kmのトレランを完走したいと思っていましたが、コロナなどいろいろあって、還暦まではあと2か月を切った今の時点で、まだ半分の50kmも走れていません。が、しかし、60を過ぎてもいいから、それでもあきらめずに目標は100kmということで頑張っていきたいと思っています。

TWELITEをプログラムする(BME280を動かす その5)

TWELITEで温湿度センサーを動かすの五回目です。とうとう五回目まで来てしまいましたが、温湿度センサーの記事は今回が最後です。
前回は、BME280用のライブラリを使わず、I2CコマンドのみでBME280のIDを読むところまでやりました。
alasixosaka.hatenablog.com

今回は、実際に温度を測定してみます。湿度も気圧も測れるのですが、最終目的はRTCモジュールをI2Cで動かすことで、BME280を動かすのはあくまでI2Cモジュールをプログラムして動かす練習なので、とりあえず温度だけを測ってみました。
前回IDがちゃんと読めたのですぐに動くと思っていたのですが、単純なことにはまってしまいずいぶん時間がかかりました。
プログラムは、前回のBRD_I2C_TEMPHUMIDに手を入れたのをベースにします。初期設定の書き込みまではできているので、補正係数の読み込み、温度測定、補正係数を使った計算をプログラムすれば良いということになります。前回からの変更点を中心に説明します。

補正係数の読み込み

実はここで引っかかってなかなかうまくいきませんでした。なぜそうなったのかは後で詳しく書きます。
まず、グローバル変数を定義します。sensor driverの始めの部分に書きます。温度用の補正係数は3つで、dig_T1、dig_T2、dig_T3の3つ。dig_T1は符号なし16ビット変数、digT2とdig_T3は符号付き16ビット変数です。何故なのかはわかりません。データシートの例がこうなっています。

uint16_t dig_T1;
int16_t  dig_T2;
int16_t  dig_T3;

続いて、sensor driverのsetupの所で、補正係数を読み込みます。
まず、引数として16ビット変数を2つ用意しています。何に使うのかは後で書きます。

bool setup(uint16_t &i15Temp, uint16_t &i15Humd) {....
}

次に、補正係数読み込み用の変数として、符号なし8ビット変数を6つ用意します。

uint8_t dig_T1tempL;
uint8_t dig_T1tempH;
uint8_t dig_T2tempL;
uint8_t dig_T2tempH;
uint8_t dig_T3tempL;
uint8_t dig_T3tempH;

始めは16ビットなのだからと、そのままdig_T1、dig_T2、dig_T3に読み込もうとしました。まず、符号付き変数をwire.get_readerの後に指定すると何故かコンパイラが途中で落ちてしまいます。この理由を見つけるのに相当悩みました。正常にコンパイルできてたコードに戻して一つ一つチェックしていったところ、符号付き変数を使うとコンパイラが落ちることがわかりました。その後、関数の呼び出しの引数などで、変数の型が一致しない時もコンパイラが落ちることがわかりました。エラーを出してくれるとありがたいのですがまだ完成度が低いようです。そこで、uint16_t dig_T2tempとして、16ビットの符号なし変数でいったん受けて、後で符号付き変数に変換することを行いましたが、そのままでは上位バイトと下位バイトが入れ替わってしまい数値がおかしくなることがわかり、8ビット変数で受けて16ビットに変換するようにしました。これに気づくのにはもっと時間がかかってしまいました。ちゃんと測定できていないのか、補正の計算がおかしいのか、補正係数がおかしいのかいろいろ悩んだ挙句、ESP8266で測っている下記のサイトを参考に(というかそのままま)、ESP8266で順番にプログラムを動かしてみたところ、補正係数を取得して表示するプログラムがあって、TWELITE側でも取得した補正係数をシリアル出力するようにしてみたところ、ようやく上位バイトと下位バイトが入れ替わっていることに気づきました。
www.denshi.club
ESP8266はアマゾンで買ったNodeMCUというのを使いました。

ESP8266モジュールはいろいろ持っているのですが、こいつはUSB端子があって、シリアル変換ができるようになっているのでテスト用に動かすときには便利です。デバイスとして組む時は余分な回路が消費電流を増すのでベアなESP8266に変換基板をくっつけただけのシンプルな奴が良いですが。
念のため配線を書いておきます。

ESP8266はArduino IDEで動かすときはソフトでI2Cをコントロールするので、SCLとSDAを明示的に示す必要があります。今回は、Wire.begin(2,14); というように、GPIOの2番と14番を使いました。この部分さえ変更しておけば参考サイトのスケッチはそのまま動きました。
データシートをもう一度よく見ると、温度補正用の係数dig_T1はレジスタの0x88と0x89に書かれていて、0x88が下位バイト、0x89が上位バイトになっています。つまり、下位バイト上位バイトの順に読みだされます。一方、このサンプルプログラムのベースになっているセンシリオンのSHT40のデータシートを見ると、例えば温度のデータは16ビットで提供されますが、上位バイト、下位バイトの順に読みだされます。この順であれば、TWELITEの読み出しに16ビット変数を使っても問題なく読むことができます。しかし、BME280では逆に下位バイト、上位バイトの順に読みだされるのでそのまま16ビット変数で受けてしまうと上位バイトと下位バイトが入れ替わった数字になってしまいます。これが、うまく測定できない原因でした。
なので、補正係数の取得の部分は、まず、

//get compensation data
		
if (auto&& wrt = Wire.get_writer(I2C_ADDR)) {
	wrt << 0x88; //register address
} else {
	return false;
}

として、温度補正係数のアドレスを書き込み、

if (auto&& rdr = Wire.get_reader(I2C_ADDR, 6)) {
	rdr >> dig_T1tempL; 
	rdr >> dig_T1tempH; 
	rdr >> dig_T2tempL;
	rdr >> dig_T2tempH;
	rdr >> dig_T3tempL;
	rdr >> dig_T3tempH;
	} else {
	return false;
}

として、補正係数6バイトをすべて8ビット変数で受けています。
そして、

dig_T1 = uint16_t(dig_T1tempH<<8)|dig_T1tempL;
dig_T2 = uint16_t(dig_T2tempH<<8)|dig_T2tempL;
dig_T3 = uint16_t(dig_T3tempH<<8)|dig_T3tempL;
i15Temp = dig_T1;
i15Humd = dig_T2;

として、16ビット変数に変換しています。最後の、i15Tenpとi15Humdは、setupの冒頭に書いた、2つに引数に相当していて、それぞれ、dig_T1とdig_T2をシリアル出力するために返す値としています。デバッグ用なので本来不要です。
また、このルーチンの最後に

ctrl_meas_reg = ctrl_meas_reg | 1;  //set one shot mode

として、ワンショットモードをレジスタ(cntl_meas_reg)に書き込む用のデータの第0ビットを立てて、これを測定指示の所で書き込むようにしています。
次が測定を指示する部分です。0xF4レジスタに先ほどのcntl_meas_regを書き込んで測定指示を出します。

bool begin() {
// start read
		
  if (auto&& wrt = Wire.get_writer(I2C_ADDR)) {
	  wrt << 0xF4; // register address
	  wrt << ctrl_meas_reg; // register data
  } else {
	  return false;
  }
  return true;
}

測定データの読み出しの部分です。こちらも引数は2つで16ビット変数のi16Tempとi16Humdです。測定されたデータから補正係数を使って計算して温度、湿度を返すのですが、湿度は測定していませんので0が返ります。温度データは3バイトあり(実際は最下位の4ビットは無効で計20ビット)、8ビット変数を3つ用意します。また、途中の計算で32ビット必要になるので32ビット変数も3つ用意します。

bool read(int32_t &i16Temp, int16_t &i16Humd) {
		// read result
		uint32_t u32temp1, u32temp2, u32temp3;
		uint8_t u8temp_1, u8temp_2, u8temp_3;

そして、温度データの先頭アドレス0xFAを書き込んで、3バイトのデータを順番に読み出します。ここも最初は32ビット変数で受けて4ビットシフトをしていたんですが、どうも値が変になるので8ビットで受けて計算するように変えています。しかしデータシートを見ると、ここの部分は上位バイト、下位バイト、最下位バイトの順で読みだされるのでそのまま32ビットで受けてもよかったと思います。面倒なので直していませんが。
最後は計算用に受けた8ビット変数を32ビット変数にキャストしています。

		if (auto&& wrt = Wire.get_writer(I2C_ADDR)) {
			wrt << 0xFA; // register address for temp
		} else {
			return false;
		}

		if (auto&& rdr = Wire.get_reader(I2C_ADDR, 3)) {
			rdr >> u8temp_1; // read temperature
			rdr >> u8temp_2;
			rdr >> u8temp_3;
		} else {
			return false;
		}
		u32temp1 = uint32_t(u8temp_1);
		u32temp2 = uint32_t(u8temp_2);
		u32temp3 = uint32_t(u8temp_3);

その次が、受けたデータを32ビット変数に直すところで、

temp_raw = (u32temp1 << 12) | (u32temp2 << 4) | (u32temp3 >> 4);

として、最終的にtemp_rawに測定した生データが入ります。
次が補正計算の部分で、参考サイト(前回のブログに記載)やデータシートの式と同じです。

//calculate temperature
int32_t var1, var2, T;
var1 = ((((temp_raw >> 3) - ((int32_t)dig_T1<<1))) * ((int32_t)dig_T2)) >> 11;
var2 = (((((temp_raw >> 4) - ((int32_t)dig_T1)) * ((temp_raw>>4) - ((int32_t)dig_T1))) >> 12) * ((int32_t)dig_T3)) >> 14;
t_fine = var1 + var2;
T = (t_fine * 5 + 128) >> 8;

最後に計算結果を16ビット変数i16Tempにキャストして代入しています。

i16Temp =int_16t(T);

後の部分は上に書いたように、読みだした補正係数をシリアル出力する部分を書き加えているだけでほぼそのままです。
これで、親機で受けたときに正しく温度が表示されました。

温度表示が正しく表示された。

プログラムの全文です。

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

/*** sensor select, define either of USE_SHTC3 or USE_SHT40  */
// use SHTC3 (TWELITE PAL)
#define USE_BMP280
// use SHT40 (TWELITE ARIA)
#undef USE_SHT40

/*** Sensor Driver */
//#if defined(USE_BMP280)
// for SHTC3
struct SHTC3 {
	uint8_t I2C_ADDR;
	uint8_t CONV_TIME;
	uint8_t t_sb;
	uint8_t filter;
	uint8_t spi3or4;
	uint8_t osrs_t;
	uint8_t osrs_p;
	uint8_t Mode;
	uint8_t ctrl_meas_reg;
	uint8_t config_reg;
	uint16_t dig_T1;
	int16_t  dig_T2;
	int16_t  dig_T3;
	int32_t temp_raw;
	int32_t  t_fine;
	uint8_t ID;

	bool setup(uint16_t &i15Temp, uint16_t &i15Humd) {
		// here, initialize some member vars instead of constructor.
		I2C_ADDR = 0x76;
		CONV_TIME = 10;
		t_sb = 0; //stanby 0.5ms
        filter = 0; //filter O = off
    	spi3or4 = 0; //SPI 3wire or 4wire, 0=4wire, 1=3wire
        osrs_t = 1; //OverSampling Temperature x4
  		osrs_p = 1; //OverSampling Pressure x4
  		Mode = 0; //Normal mode
		uint8_t dig_T1tempL;
		uint8_t dig_T1tempH;
		uint8_t dig_T2tempL;
		uint8_t dig_T2tempH;
		uint8_t dig_T3tempL;
		uint8_t dig_T3tempH;
  		ctrl_meas_reg = (osrs_t << 5) | (osrs_p << 2) | Mode;
  		config_reg    = (t_sb << 5) | (filter << 2) | spi3or4;
		if (auto&& wrt = Wire.get_writer(I2C_ADDR)) {
			wrt << 0xF4; // register address
			wrt << ctrl_meas_reg; // register data
		} else {
			return false;
		}
		//return true;
		if (auto&& wrt = Wire.get_writer(I2C_ADDR)) {
			wrt << 0xF5; // register address
			wrt << config_reg; // register data
		} else {
			return false;
		}
		delay(1000);
		
		//get compensation data
		
		if (auto&& wrt = Wire.get_writer(I2C_ADDR)) {
			wrt << 0x88; //register address
		} else {
			return false;
		}
		
		if (auto&& rdr = Wire.get_reader(I2C_ADDR, 6)) {
			rdr >> dig_T1tempL; 
			rdr >> dig_T1tempH; 
			rdr >> dig_T2tempL;
			rdr >> dig_T2tempH;
			rdr >> dig_T3tempL;
			rdr >> dig_T3tempH;
		} else {
			return false;
		}
		
		dig_T1 = uint16_t(dig_T1tempH<<8)|dig_T1tempL;
		dig_T2 = uint16_t(dig_T2tempH<<8)|dig_T2tempL;
		dig_T3 = uint16_t(dig_T3tempH<<8)|dig_T3tempL;
		i15Temp = dig_T1;
		i15Humd = dig_T2;
		

		ctrl_meas_reg = ctrl_meas_reg | 1;  //set one shot mode
		return true;
	}

	bool begin() {
		// start read
		
		if (auto&& wrt = Wire.get_writer(I2C_ADDR)) {
			wrt << 0xF4; // register address
			wrt << ctrl_meas_reg; // register data
		} else {
			return false;
		}
		

		return true;
	}

	int get_convtime() {
		return CONV_TIME;
	}

	bool read(int16_t &i16Temp, int16_t &i16Humd) {
		// read result
		uint32_t u32temp1, u32temp2, u32temp3;
		uint8_t u8temp_1, u8temp_2, u8temp_3;
		if (auto&& wrt = Wire.get_writer(I2C_ADDR)) {
			wrt << 0xFA; // register address for temp
		} else {
			return false;
		}

		if (auto&& rdr = Wire.get_reader(I2C_ADDR, 3)) {
			rdr >> u8temp_1; // read temperature
			rdr >> u8temp_2;
			rdr >> u8temp_3;
		} else {
			return false;
		}
		u32temp1 = uint32_t(u8temp_1);
		u32temp2 = uint32_t(u8temp_2);
		u32temp3 = uint32_t(u8temp_3);

		temp_raw = (u32temp1 << 12) | (u32temp2 << 4) | (u32temp3 >> 4);
		
		//calculate temperature
		int32_t var1, var2, T;
		var1 = ((((temp_raw >> 3) - ((int32_t)dig_T1<<1))) * ((int32_t)dig_T2)) >> 11;
    	var2 = (((((temp_raw >> 4) - ((int32_t)dig_T1)) * ((temp_raw>>4) - ((int32_t)dig_T1))) >> 12) * ((int32_t)dig_T3)) >> 14;
    	t_fine = var1 + var2;
    	T = (t_fine * 5 + 128) >> 8;
		
		i16Temp = int16_t(T);
		
		return true;
	}
} sensor_device;
//#endif

/*** Config part */
// application ID
const uint32_t DEFAULT_APP_ID = 0x1234abcd;
// channel
const uint8_t DEFAULT_CHANNEL = 13;
// option bits
uint32_t OPT_BITS = 0;
// logical id
uint8_t LID = 0;

// application use
const char FOURCHARS[] = "SBS1";

// stores sensor value
struct {
	int16_t i16temp;
	int16_t i16humid;
	uint16_t i15temp;
	uint16_t i15humid;
} sensor;

// application state defs
enum class STATE : uint8_t {
	INTERACTIVE = 255,
	INIT = 0,
	SENSOR,
	TX,
	TX_WAIT_COMP,
	GO_SLEEP
};

// simple state machine.
SM_SIMPLE<STATE> step;

/*** Local function prototypes */
void sleepNow();

/*** setup procedure (run once at cold boot) */
void setup() {
	/*** SETUP section */
	/// init vars or objects
	step.setup(); // initialize state machine
	
	/// load board and settings objects
	auto&& set = the_twelite.settings.use<STG_STD>(); // load save/load settings(interactive mode) support
	auto&& nwk = the_twelite.network.use<NWK_SIMPLE>(); // load network support

	/// configure settings
	// configure settings
	set << SETTINGS::appname(FOURCHARS);
	set << SETTINGS::appid_default(DEFAULT_APP_ID); // set default appID
	set << SETTINGS::ch_default(DEFAULT_CHANNEL); // set default channel
	set.hide_items(E_STGSTD_SETID::OPT_DWORD2, E_STGSTD_SETID::OPT_DWORD3, E_STGSTD_SETID::OPT_DWORD4, E_STGSTD_SETID::ENC_KEY_STRING, E_STGSTD_SETID::ENC_MODE);

	// if SET(DIO12)=LOW is detected, start with intaractive mode.
	if (digitalRead(PIN_DIGITAL::DIO12) == PIN_STATE::LOW) {
		set << SETTINGS::open_at_start();
		step.next(STATE::INTERACTIVE);
		return;
	}

	// load values
	set.reload(); // load from EEPROM.
	OPT_BITS = set.u32opt1(); // this value is not used in this example.
	
	// LID is configured DIP or settings.
	LID = set.u8devid(); // 2nd is setting.
	if (LID == 0) LID = 0xFE; // if still 0, set 0xFE (anonymous child)

	/// configure system basics
	the_twelite << set; // apply settings (from interactive mode)

	/// configure network
	nwk << set; // apply settings (from interactive mode)
	nwk << NWK_SIMPLE::logical_id(LID); // set LID again (LID can also be configured by DIP-SW.)	

	/*** BEGIN section */
	Wire.begin(); // start two wire serial bus.
	
	// let the TWELITE begin!
	the_twelite.begin();

	// setup sensor device
	sensor_device.setup(sensor.i15temp,sensor.i15humid);
	Serial 	<< format("     : T1temp= %04x"   //補正係数のシリアル出力
					,sensor.i15temp)
					<< mwx::crlf;
	Serial	<< format("     : T2temp=%04x"
					,sensor.i15humid)
					<< mwx::crlf;
					
				//Serial.flush();

	/*** INIT message */
	Serial << "--- TEMP&HUMID:" << FOURCHARS << " ---" << mwx::crlf;
	Serial	<< format("-- app:x%08x/ch:%d/lid:%d"
					, the_twelite.get_appid()
					, the_twelite.get_channel()
					, nwk.get_config().u8Lid
				)
			<< mwx::crlf;
	Serial 	<< format("-- pw:%d/retry:%d/opt:x%08x"
					, the_twelite.get_tx_power()
					, nwk.get_config().u8RetryDefault
					, OPT_BITS
			)
			<< mwx::crlf;
}

// wakeup procedure
void wakeup() {
	Serial	<< mwx::crlf
			<< "--- TEMP&HUMID:" << FOURCHARS << " wake up ---"
			<< mwx::crlf
			<< "..start sensor capture again."
			<< mwx::crlf;
}

/*** loop procedure (called every event) */
void loop() {
	do {
		switch (step.state()) {
		case STATE::INTERACTIVE:
		break;
		
		case STATE::INIT: // starting state
			// start sensor capture
			sensor_device.begin();
			Serial << sensor_device.get_convtime() << mwx::crlf;
			step.set_timeout(sensor_device.get_convtime()); // set timeout
			step.next(STATE::SENSOR);
		break;

		case STATE::SENSOR: // starting state
			if (step.is_timeout()) {
				// the sensor data should be ready (wait some)
				sensor_device.read(sensor.i16temp, sensor.i16humid);

				Serial << "..finish sensor capture." << mwx::crlf
					<< "     : temp=" << div100(sensor.i16temp) << 'C' << mwx::crlf
					<< "       humd=" << div100(sensor.i16humid) << '%' << mwx::crlf
					;
				Serial.flush();

				step.next(STATE::TX);
			}
		break;

		case STATE::TX:
			step.next(STATE::GO_SLEEP); // set default next state (for error handling.)

			// get new packet instance.
			if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
				// 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(sensor.i16temp)
					//, uint16_t(sensor.i16humid)
				);

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

				if (ret) {
					step.clear_flag(); // waiting for flag is set.
					step.set_timeout(100); // set timeout
					step.next(STATE::TX_WAIT_COMP);
				}
			}
		break;

		case STATE::TX_WAIT_COMP: // wait for complete of transmit
			if (step.is_timeout()) { // maybe fatal error.
				the_twelite.reset_system();
			}
			if (step.is_flag_ready()) { // when tx is performed
				Serial << "..transmit complete." << mwx::crlf;
				Serial.flush();
				step.next(STATE::GO_SLEEP);
			}
		break;

		case STATE::GO_SLEEP: // now sleeping
			sleepNow();
		break;

		default: // never be here!
			the_twelite.reset_system();
		}
	} while(step.b_more_loop()); // if state is changed, loop more.
}

// when finishing data transmit, set the flag.
void on_tx_comp(mwx::packet_ev_tx& ev, bool_t &b_handled) {
	step.set_flag(ev.bStatus);
}

// perform sleeping
void sleepNow() {
	step.on_sleep(false); // reset state machine.

	// randomize sleep duration.
	uint32_t u32ct = 1750 + random(0,500);

	// output message
	Serial << "..sleeping " << int(u32ct) << "ms." << mwx::crlf;
	Serial.flush(); // wait until all message printed.
	
	// do sleep.
	the_twelite.sleep(u32ct);
}

ようやくBME280が動かせるようになったので、次はRTCモジュールを動かしてみます。一回で終わるかどうかわかりませんが。

東海自然歩道を走る その8

久しぶりの東海自然歩道シリーズです。
調べてみると、4月以来ということになります。
alasixosaka.hatenablog.com
前回は、柘植駅から鈴鹿峠を越えて、土山に降りてきました。
今回は、土山から湯の山温泉駅まで行きました。
本当はもっと早くいくつもりで、9月の連休に予定していたのですが、台風が来るということで9月連休は2度とも延期となり、結局11月後半になってしまいました。
考えてみると今年は、夏の光岳登山が台風で中止になり、9月も2度とも連休が台風の影響を受け、その後は、家族のコロナ感染で隔離生活を送ったり、足を怪我したりと予定していた、比良トレイルやオリエンテーリングの大会、六甲全縦もすべてキャンセルになって散々ことになってしまっています。厄払いが必要ですかね。もっとも足を怪我したことで厄払いになったかもしれませんが。
とにかく、ようやくまともに走れるようになったので、まだ痛みは少し残っていますが、出かけてきました。

準備

今回はちょっと準備不足の感がありましたが、もともと9月に予定をしていたので、まあ何とかなったかなというところです。
ザックはいつものようにRaidlightのResponsive24。今ぐらいの季節になって防寒具を持つようになると容量は20Lクラスが欲しくなってしまいます。大会ではないので、食料、水もまる1日分必要ですし。
こいつは軽くていいんですが、フロントのポケットがもっと多いといいのにといつも思います。それでも愛用はしているんですが。それと、今回は給水にハイドレーションを持って行ったのですが、ハイドレーションのチューブを通す穴がないのでメイン気室のジッパーを少し開けてそこからチューブを通すのですが、だんだんジッパーが開いて中のものが落ちるという事件がありました。ちょっと面倒ですがジッパー同士をカラビナなどで固定してやる必要がありそうです。
靴は、アルトラのローンピークです。これも最近の定番ですが、直近ではモンブランを買ったのでどっちにしようか悩んだのですが、モンブランはまだ10kmくらいの試し履きくらいしかしてないので、いきなり30㎞はちょっと冒険かなと思って履き慣れたローンピークにしました。ちょっと滑りやすいのが欠点なのですが、今回のルートはロードの区間が多そうなのでそれもあってクッションの良いローンピークを選択しました。
服は、アンダーに今回は新調したパタゴニアのキャプリーンサーマルウェイトジップを使ってみた。これはこちらの山旅旅の記事を読んで買ったもので、ポーラテックパワーグリッドという素材を使って、温かく、汗を効率よく逃がす仕組みになっているらしい。
yamatabitabi.com
ちょっと高いと思ったが、ちょうどアウトレットで安くなっているのがあったので買ってみた。まず第一印象は臭いが気になった。何とも言えない動物臭がする。ウールのような素材を使わずポリエステルなのでなんでそんなに匂うのかよくわからないが、1度洗濯しただけでは臭いは取れなかった。肝心の機能の方は確かに暖かく、汗も逃がしてくれる感じがする。普段使っているメリノウールと比べると暖かさは上のような気がするが、汗を逃がす機能はメリノウールの方が良いような気がした。
ミッドレイヤーはファイントラックのフロウラップ、アウターはモンベルのクロスランナー、ズボンもモンベルのクロスランナー。
今回は、食料としていつものおにぎり2個に加えて、チキンラーメンのリフィルを持って行った。リフィルにしてかさを減らして、ジップロックジーと山専サーモスでお湯を持って行って食べた。ジップロックジーはこちらの記事を参考にして自作してみたが、なかなか行ける。お湯を持っていくのは少し重量が増えるが、お湯を沸かす手間が省けるし、何といっても時短になる。今は日没が早い時期なので時間はとても貴重で、少し重量を増やしてもお湯を持っていく価値はあると思っている。ジップロックジーを使うことでカップ麺を持っていくよりもかさがだいぶ減るのでそれもあってこれは非常に良いと思った。
www.yamakei-online.com

出発

朝8時過ぎの電車で出発。高槻で新快速に乗り換え、草津草津線に乗り換え貴生川で下車。考えてみるとこの何回かで何度も貴生川駅を利用している。3回前が西大津から信楽高原鉄道紫香楽宮跡駅まで走って、帰りに貴生川で乗り換えている。前々回は、貴生川からまた信楽高原鉄道に乗ったし、前回は柘植から土山まで走ってそこからコミュニティーバスで貴生川に戻って来た。そして、今回はまた、貴生川からコミュニティーバスで前回のゴールである土山の黒川に向かう。貴生川は鉄道だけでJR、信楽高原鉄道近江鉄道が発着するので、この辺りでは交通の要衝になっているのだろう。
貴生川に向かう電車の中で、なんで自分はこんなことをしているのだろうとふと思った。だだ、トレイルを走るだけならもっと近所に走るところはあるし、わざわざ時間をかけて遠くに向かう価値は何なんだろうかと。しかし、何の目的もなく走るのも面白くないし、モチベーションも上がらないので、何らかのモチベーションが必要なのだろう。百名山を目指す人も、ただ山が好きと言うだけでなく、いろんなところに行ってみたいとかもあるだろうが、一つのモチベーションにしているはずだし。東海自然歩道をたどっていくのも一つのモチベーションなのだろうと自分を納得させる。
前回は、黒川から貴生川までの直通バスに乗れたが、今回は、9時35分の田村神社行きに乗って、近江土山で乗り換え。コミュニティーバスは1回¥250の均一料金なので乗り換えると値段が倍になる。同じところに行くのに値段が倍になるのはちょっとと思ったがまあ、バスとはそういうものなので仕方がない。10時30分頃に前回ゴールした黒川に到着。

今回は黒川のバス停からスタート

黒川~安楽峠

出発して安楽峠に向かう。ここが今日一番の山場。集落を抜けて笹路川を渡ってしばらく行くと東海自然歩道の合流。ここから山女原を抜けて安楽峠を目指す。

地図アプリが不調

ところが、自作の地図アプリが正しく機能しない。起動直後は現在地をちゃんと表示するが、画面をオフにしてバックグランドにしてしばらくして画面をオンにすると現在地が正しく表示されない。どうもGPSの位置を更新したシグナルが拾えていないようだ。おまけに、GPSの精度がめちゃめちゃ下がっていて、GPS精度を表す円が広がっている。へたをすると画面全体に広がっていたりする。最初は位置情報の権限が正しく許可されていないのかと思って、設定を確認したがちゃんと許可されている。一度権限を削除してやり直してみたが症状は改善しない。仕方がないので、めんどくさいがアプリの終了と起動を繰り返して現在地を確認しながら進む。
安楽峠に向かうところで分岐に出た。あらかじめ設定したルートは左を進むことになっているが、左手には大きな柵がある。右手は普通の道になっている。柵は動物除けの柵なのかと思うが、道標を見ても右でも行けそうなので右の道を行く。

安楽越えに向かう道の分岐。左には柵があったので右の道を進む

しばらく登りを登る。いきなりの登りで体が重い。とても走れないので歩いて登る。今日はトレランとしては久しぶりの30㎞越えなので無理をしないことにする。といっても、スタートが遅く、日暮れが早い時期なのであまり時間はないのだが。峠の手前で痛めている右足の親指に痛みが走った。久々に感じる鋭い痛みだったので、これが続くとやばいなと思ったが、幸いなことに痛みは続かず大したことにならずに済んだ。
ようやく安楽峠に到着。滋賀県三重県の県境だ。標識はなかったが。

安楽峠に到着。峠には車がいっぱい止まっていた

峠には狭いところに車がいっぱい止まっていた。11月と言えば狩猟のシーズンなので狩りをしているのだろうか?
安楽峠で最初のおにぎりを食べる。スタータが遅いのでまだ疲れは全然ないので、止まらずに歩きながら食べる。
ここは後で地図を見ると新名神鈴鹿トンネルの丁度真上に当たる場所になっている。

安楽峠~坂本

安楽峠からしばらく稜線を進むものと思っていたが、地図を見るとここからは下って鈴鹿山系の裾に沿って進んでいく感じになる。黒川までバスで結構登ってきていたので、ここの下りで登った距離よりも下った距離の方が長くなる。長く続く下りも傾斜がある程度以上だと結構足に来る。下りで踏ん張ると怪我したところが少し痛むがさっきの登りで感じた鋭い感じではないのであまり気にせず進む。
下っていると石水渓というところに出た。東海自然歩道の道標では石ヶ渓と書いてあったが。ちなみに、この辺りはほぼ500mおきに道標があり、ここまでやらなくてもというくらいあった。あとでこれが油断になってえらい目にあうことになるのだが。
石水渓にはキャンプ場があるが、全然人が見当たらない。営業していないようだ。

石水渓のキャンプ場
石水渓の案内板

キャンプ場のちょっと先で視界が開けて新名神が良く見える。この辺りは新名神が良い目印になっている。

新名神が良く見える。

石水渓を過ぎて下り切ると新名神の真下に出る。ここで新名神をくぐらずに、左に降りて坂本方面に登っていく。

新名神をくぐらず左に曲がる

ここは、仙ヶ岳の登山口のバス停があった。が、バスはなんと1日2本。しかも運航は4月から9月のみ。結局みんな車で行くのだろう。

1日2本のバス

ここからまた登りが始まる。
しばらく行くと一面のお茶畑に出た。

一面のお茶畑

昔はお茶の木と言えば奇麗な半円状をしていたが、最近は機械でお茶の葉を摘むのでドーム状になっている。時代とともに風物詩も変わっていくというところか。個人的には昔の半円状のお茶の木がいかにもお茶という感じがすると思うのだが。ちなみに、昔中国で見たお茶の木は日本のお茶の木のようにきれいに刈り込まれていなくて、自然に生えている気のようにぼうぼうに伸びていた。同じお茶でもところ変われば品変わると思ったものだったが。今はどうなっているか。中国でも機械が導入されて日本と同じようになっているのかもしれない。
お茶畑を過ぎても延々と登りが続く、一旦森の中に入って上り詰めたら下るのかと思ったらその先にも田んぼが広がっていた。ここが坂本の棚田らしい。登り切ったあたりに展望台らしきものが見える。そこから見ると棚田の景色がきれいに見えるのだろう。と思っていたら、東海自然歩道は展望台に向かわずに右にそれていった。お陰で棚田の景色の写真を撮り損ねてしまった。

坂本~桃林寺

坂本の棚田を過ぎてもまだまだ登りが続く。スタートが遅いせいもあってお腹はあまり空いていないがもう12時半を過ぎているのでそろそろお昼にしたいところだ。適当な休憩地があればお昼にしようと思いながら進むがなかなかない。ちょっと寄り道でもさっきの展望台に行けばよかったか。
坂本の集落を過ぎると林道のようなところに出た。ここも舗装されている。ここまでずっとロードでトレイルはなし。
途中に奇麗に紅葉しているもみじの木があった。

もみじの木

今年は急に寒くなっていないのであまり紅葉が奇麗でないが、この木は比較的奇麗に紅葉している。
しばらく行くと野登寺というところに出た。お昼がまだなので、お寺の看板の右にあるお好み処の文字に引かれるがのんびりお好み焼きを食っている時間はないのでスルーして進む。

能登寺の入り口。右の看板にお好み処の文字が

しばらく行くと石灰山の下に出た。あとで地図を見ると庄内白石鉱山とある。石灰が取れるということはこの辺りは昔は海の底だったということか。石灰の加工工場もある日東粉化工業と書いてあった。そこを抜けると扇状地に出る。ここは昔田んぼか畑だったのらしいが、今はタダの空き地になっていて、太陽電池パネルがぎっしり設置されていた。ここの中で道がわからなくなった。
今回は、三重県が公式に出しているガイドマップを参考にせず先人のルートをネットで検索してアプリに入れて来たのだが、道が変わっているのかそのルート上に道がない。適当にあたりをつけて電池パネルの並ぶ扇状地の中を登っていく。地図ではどこかで川を渡って川の向こうを進むことになっているが、渡河地点が扇状地を上り詰めたあたりになっているのでとりあえず上る。そうすると、小さな橋が見えて渡河できそうだ。そして、道標を発見。道標によると扇状地の上の方の道をたどるのが正解のようで、石灰工場のすぐ下あたりを曲がらなければいけなかったらしい。ここまで親切すぎるくらい道標があったのに、ここにきて道標が途切れて道を見失ってしまった。ただ、これはまだ序の口だったことを後で思い知る。

やっと道標を発見

ようやく道がわかってほっとしたので、お昼の休憩場所を探しながら進む。しかし、なかなか良いところが見つからない。道標では次のポイントが桃林寺となっていてまだ5㎞くらいあるが、そこまでないのかもしれない。林道を抜け集落のようなところに出た。桃林寺は近いのかと思ったらまだ先のようだ。集落を抜けまた山の中に入ってしばらく行くと神社に出て、腰かけるのにちょうどよい石があったのでここで休憩することにする。時刻はもう13時半だ。途中でおにぎりを食べたり、補給食を取ったりしたのでそれほどお腹が空いているという感じでもないが、時間も時間なので食べておくことにする。でないと後でおなかがとても空くことになるのは過去の経験でわかっているので。ここで、残りのおにぎり1個とチキンラーメンを食べる。
お昼を済ませてしばらく行くと桃林寺に到着。ここには、休憩所もトイレもあった。ここまで我慢すればよかったと思ったが後の祭り。

桃林寺に到着。
桃林寺山門前の休憩所にあった案内看板。ここにはテーブルとイス、トイレもある。

この先どこでトイレに行けるかわからないのでトイレも済ませておく。

桃林寺~湯の山温泉駅 道迷いの連続

桃林寺前を過ぎて次は椿大社に向かう。ここまでいい感じに来れたのでゴールタイムは5時間を切れるかと思ったが、いきなり道を見失う。
桃林寺を過ぎたところに標識があってV字型に書いてあったのだが意味が解らず、右を見てもそれらしき道がなかったので、あらかじめ調べておいた道が真っすぐだったのでまっすぐ進んだがすぐに行き止まりになってしまった。右手に空き地のようなところがあるのでそっちに出てみると、道らしきものがあったがすぐに途切れてしまう。地図を見ると、本来のルートのところには何もない。
ここで、だましだまし使っていた地図アプリが完全に使えなくなった。今までは停止、起動をすることで現在地が正しく表示されたが、地図すら表示されなくなった。道に迷っているときに地図すらまともに見れなくなってピンチになった。仕方がないので、スマホを再起動することにする。いつも、Yamapアプリを起動しておいて、それと自分の作ったアプリの両方を動かしているが、再起動するとYamapアプリが止まってしまうことになるが緊急事態なので仕方がない。幸いなことに再起動すると地図も現在地も正しく表示された。そして、バックグラウンドでも位置をちゃんと把握できるようになった。Yamapアプリが干渉していたのか、それとも長いこと再起動してなかったのでメモリがパンパンになってうまく起動しなかったのかよくわからない。原因の検証はまた後日することにして、怖いのでYamapアプリは起動せず自作の地図アプリだけで進むことにする。とにかく現在地は判るようになったが、目指す道がわからない。仕方がないので、地図を見て少し下ったところにある道を目指す。そこを本来のルートの方に進むがこの道も途中で行き止まりになる。仕方ないので、戻って、更に下ったところにある太い道を目指す。
あとで地図を見ると、ここは桃林寺前から一旦少し戻って下っていくのが正解のようだ。その先をどう進むのかはわからない。実際にたどったルートは下り切って鈴鹿公園長沢線という自動車がバンバン通る道に出てそこを左に曲がって椿大社を目指したが、ここには全く道標が見当たらなかかった。川を渡って左に折れて椿大社を目指す。椿大社はこの辺りでは有名な神社らしく、門前が栄えていてお店もいっぱい出ていて、参拝客も大勢いた。椿こんにゃくとか椿大福と言ったのが名物らしい。時間と心に余裕があったら椿大福は食べてみたかったが、なんせ道に迷っている最中だし、今日はスタートが遅く時間に余裕がないのでとりあえずルートに復帰することを第一に目指した。あらかじめ調べたルートでは椿大社の中を突っ切って北の方へ向かうルートになっている。椿大社の門前には東海自然歩道の道標が見当たらない。これは東海自然歩道あるあるなのだが、有名な観光地になるほど道標はなく、ちょっと離れたところにひっそりとあったりする。道標を頼りにしていると結構困ることになる。とにかく神社の中に入ってそれらしきところを目指してみる。しかし、神社を参道の途中から外に抜けれそうな道は見つからずぐるっと一周して元に戻ってしまった。仕方がないので、また地図を見て東海自然歩道のルートを目指す。お陰で参拝もせず、写真も撮らずという始末。地図アプリがまともに動くようになったのがせめてもの幸いだ。さっきの桃林寺で迷い、今度は椿大社で迷い結構焦っていた。写真がないのでとりあえず、観光情報のページのリンクを貼っておく。
www.kankomie.or.jp

椿大社の門前に戻って北へ向かう道を進む、当然ここには道標がない。しばらく行って西からくる道の合流点に道標があった。道標によるとこの西からくる道が正解ルートらしい。しかし、この道をたどると、地図によると椿大社の手前で道が終わっている。そこから本当に椿大社に入れるのか確かめてないのでよくわからない。今回は、途中の桃林寺までは順調だったがそこから先はトラブル多発である。そして、この先にもう一つ待っていた。
またしばらく森の中を走り、川を渡るところではまってしまった。あとで地図を見るとこの川は内部川という川らしいが、川を渡るところがわからない。東海自然歩道の道標もあるにはあるが錯綜していてどこが本当のルートかよくわかない。
まず、川の手前で右に曲がるところがいまいち不明瞭で、道標に従うとすぐ川の脇を行く道を指している感じだった。元々のルートはもう少し川の手前を下る感じだったが大差ないので、川のすぐわきを行くことにした。するとこの道は次第に道っぽくなくなってやばいのかと思ったが、三叉路にぶち当たったところにちゃんと道標があってこれでよかったのかと思った。道標によるとここを右に進む(つまり川から離れる)ようになっているので素直にその通りに進んだ。すると、元々のルートでたどる予定だった道まで戻る感じになって、そこにも道標があって、その道標には元々のルートが東海自然歩道のように書いてある。よくわからないが、とりあえず予定の渡河地点はもう少し下流なので下り始める。しかし、いつの間にか分岐を通り過ぎていた。おかしいと思ってしっかり地図を見ながら戻るが、川に近づけそうな分岐は見当たらない。地図を見ても、川を渡れそうな道ははるかに下流の方しかない。だったら、さっきの川のそばの三叉路を川の方に行けばよかったのか(道標のルートとは異なるが)と思って、そちらに行ってみると、道は川のすぐ手前で終わっていて、川の向こうにも道があるにはあるがどう見ても渡れそうにない。流れは緩やかなので、ジャバジャバ川の中を水に使って進めば行けるには行けるがなんかおかしいので、もう少し簡単に渡れそうなところを探すと、少し上ったところに防砂用の堰があってそこが渡れそうなのでいってみた。ただし、渡れるには渡れるが渡った先に道がない。道がないどころか、密集した笹やぶといばらという最悪のコンビが待っていた。と言ってももうわたってしまったし、少し行けば道がありそうなので、藪漕ぎをしてとにかく進む。久しぶりの藪漕ぎだ。何とか道に出ると、東海自然歩道は迂回してくださいという表示があった。しかし、何をどう迂回するのかさっぱりわからない。とりあえず、地図を見て元のルートに戻るべく、川沿いを上流側に登っていく。そうすると道標があってとりあえずルートにのっとことは確認できた。ここにも迂回を指示する看板があったが、やはり何をどう迂回するのか書いてないのでわからない。そして、自分が通ってきた対岸にはそんな看板は一切なかったし。想像するに、川の手前で道が切れていたところが昔は通れていたのかもしれない。そこが通れなくなってしまったので迂回しなさいということなのかもしれないが、う回路を具体的に示していないんのでさっぱりわかないし、西からやってきた自分にはそもそも迂回の指示もなかった。何か釈然としないが渡ってルートに復帰したのでまあいいやと思って先に進む。
しばらく行くと、もみじ谷というところに出た。また、急に俗っぽくなって、出店やらが出てて、観光客がうようよいる。まあ確かにもみじの季節だから、このあたりではもみじの名所なのかもしれない。もみじの季節に嵐山に出るよりましか。そういえば東海自然歩道は嵐山も通っていたなあ。
もみじ谷を抜けるとゴールはもう間近であと4kmくらい。しかし、近づいても全然駅に近づいているという気配が全くない。あと1kmを切ってもまだ山の中を走っている。視界が開けると急に線路が見えて線路を越えると駅が見えた。

突然、という感じで駅の手前に出た。そして東海自然歩道の看板

到着予定はだいたい16時頃を予定していたが、15:40に到着した。だいたい5時間くらいで走破した。もっとも、後半の道迷いがなかったらあと20分くらいは早くゴールできていた感じだったが。まあ、これも東海自然歩道あるあるで仕方がない。これくらいのロスは常に計算に入れておかないといけない。今回は念のためヘッドライトも持参したが、予定よりも早く着いたので不要になって何よりだった。
距離は32kmあったが、それほど厳しいのぼりがあったわけでもないので5時間はまあまあいいとこのタイムではないかと思う。それにしても、前半と後半で道標の親切さがこれほど違うとは、油断大敵であるし、もうちょっと情報を仕入れておきたいところではある。迷ってばかりいたので、後半は写真を全然撮っていない。自分で見返してみても写真がないとつまらんと思ってしまうので、この辺は大いに反省するところですね。

帰路

湯の山温泉発の電車は15:39が行ったばかりで、次は15:59発だった。もう少し頑張って走っていれば間に合ったかもしれなかったが、16時を予定していたのでまあ予定通りというところ。乗換案内で調べて近鉄特急の特急券をネットで予約しようと思ったら、トップに出てきたのは名古屋に出て新幹線で帰るルートだった。近鉄で大阪に出るルートは何と3番目になっていた。たしかに新幹線を使うと近鉄特急で大阪周りで帰るよりも30分以上早い。しかし、値段は¥2000以上高い。どうするか考えたが、やっぱり¥2000は高いと思って、予定通り近鉄特急で帰ることにした。

今後はどうするか

近鉄で帰るとだいたい特急に乗っている時間だけで2時間。湯の山温泉からトータルで3時間かかった。行きが2時間20分くらいかかっているので、交通機関に乗っている時間だけで5時間以上。走っている時間よりも長い。そろそろ何か考えないと日帰りで行くのがしんどくなってきた。特急とか使いだすと交通費もばかにならなくなってくるし。この先進むと三岐鉄道養老鉄道の沿線で鉄道はあるにはあるのでバスほど不便ではないが、さりとて家から近いわけでもなく悩ましいところではある。この先続けるなら泊を入れることも考えないと続かないような気がしている。さてどうしたものか。

追記:その後、三重県のHPを調べたら、内部川のところの通行止めの情報が載っていました。通行止めになっているのは、川を渡った対岸のルートで、元のルートは川から少し離れたところを進むようになっていたのが、道が崩れて通行止めになっているため、川沿いを通るう回路を通るようになっているようです。なので、川を渡る手前に迂回の案内がなくて当然でした。
三重県のHPに出ている、地図によると渡河地点は自分の通った(通ろうとした)地点であっているようです。そこを渡ろうとすると水の中をじゃぶじゃぶと渡ることになりますが。他にルートはないんでしょうか?
それと、渡河地点のすぐ手前の三叉路の道標はどう見ても、川から離れる反対向きを指していましたが、何故なんでしょうか? 
少し疑問が解消しましたが、わからないことも残りました。

三重県のHPに出ていた通行止めの情報

TWELITEをプログラムする(BME280を動かす その4)

いつのまにかこのシリーズも4回目になってしまいましたが、前回は、子機側のプログラムをいじって、余分なデータを送信しないように修正しました。
なお、BMP280だと思っていたセンサーは実はBME280だということがわかりました。なので、今回からタイトルをBME280と改めています。
alasixosaka.hatenablog.com

今回は、さらに子機側のプログラムをいじって、BME280用のライブラリを使わず、I2C通信のコマンドだけでBME280を動かしてみようと思います。
前回までは、BME280のライブラリを使ったActEx_BME280_SHT3xというプログラムをベースにいじくっていましたが、I2C通信のコマンドで温湿度センサーを動かしているプログラムBRD_I2C_TEMPHUMIDというプログラムがあるのでこちらをベースにします。このプログラムはセンサーとしてSHTC3ないしSHT40を使うようになっているのでセンサーへのコマンド送信やデータの読み出しについてはBME280用に修正する必要があります。
なお、今回は、モノワイヤレスのBRD_I2C_TEMPHUMIDのプログラムの解説を主に参考にしました。また、TWELITEのプログラムについては、他に2つのサイトを参考にしました。
BRD_I2C_TEMPHUMID - MWX Library
農業IoTの準備として、TWELITEを使った環境情報の取得をやってみた-その1(環境情報の取得)
トワイライト(TWELITE)のI2C通信を実装し無線通信する | スマートライフを目指すエンジニア
また、BMP280(BME280)のプログラミングについては下記のサイトを参考にしました。
第35回 温湿度・気圧センサ(BME280) 〜データ取得プログラム〜 | ツール・ラボ
BME280 搭載、温度・湿度・気圧センサーを SPI で動かしてみた( ESP-WROOM-02 ( ESP8266 )使用) | ページ 2 | mgo-tec電子工作
温度・気圧センサーBMP280を使ってみる | 東京お気楽カメラ

まずはIDを取得する

参考にしたツールラボのサイトで、リンクを張っているのは第35回ですが、その前の第31回でチップのIDを読み取っている記事があります。その記事に倣って、まずはチップのIDを読み取ってみたいと思います。
プログラムの始めから順番に説明します。
まず、ライブラリを読み込む部分ですが、今回はSM_SIMPLEというのが加わっています。

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

これは、モノワイヤレスのプログラムの説明サイトにも書いてありますが、アプリケーションループの記述を簡素化するためのものとのことです。
前回の子機のプログラムでは少し違った方法でループを記述していました。まあ、概ねは似たようなものなんですが。ActEx_BME280_SHT3xでは、enum class E_STATEという構造文を定義して、それを使って、Switch、Caseで分岐を行っていました。
今回のベースにしたBRD_TEMPHUMIDでは、同じように構造文として enum class STATE  を定義していますが、そのあとに、SM_SIMPLE step;という宣言を行って、ループ中では、step.xxxというように呼び出しています。あまり大きく変わらないのでどちらでもよかったのですが、ループ内でのタイムアウトの設定がこちらの方がやりやすそうだったので、こちらのやり方をそのまま使っています。
次が、センサーの種別を選択するところです。

/*** sensor select, define either of USE_SHTC3 or USE_SHT40  */
// use SHTC3 (TWELITE PAL)
#define USE_BMP280
// use SHT40 (TWELITE ARIA)
#undef USE_SHT40

元のプログラムでは、SHTC3とSHT40のどちらかを選択するようになっていました。デフォルトはSHTC3。その部分を#define USE_BMP280として変更しています。まあ、名前だけなのでどうでもいいといえばどうでもいいんですが。
その次が、センサー用の構造文です。この部分でセンサーへのコマンド送信や、測定データの読み取りを記述していますので、この部分をBME280用に変更します。といっても、今回はとりあえずIDを読めればいいので大幅には変更しません。処理は、setup、begin、get_conv、readの4つです。setupはセンサーの初期化処理ですが、もとのSHTC3では特に必要ないようで、I2Cのアドレスと、タイムアウト待ちの待ち時間のみを設定していました。BME280では色々と初期設定が必要です。IDを読むだけなら必要なさそうですが、ここでは一応初期設定を行っています。
初期設定の詳細は参考にしたサイトを見てもらえばわかると思うので詳細は省略します。
まず、初期設定用の変数を定義します。I2C_ADDRはI2Cのアドレス。CONV_TIMEはタイムアウトの待ち時間、t_sbからはBME280用の変数です。詳細はsetupのところで書きます。

/*** Sensor Driver */
#if defined(USE_BMP280)
// for SHTC3
struct SHTC3 {
	uint8_t I2C_ADDR;
	uint8_t CONV_TIME;
	uint8_t t_sb;
	uint8_t filter;
	uint8_t spi3or4;
	uint8_t osrs_t;
	uint8_t osrs_p;
	uint8_t Mode;
	uint8_t ctrl_meas_reg;
	uint8_t config_reg;

次からがそれぞれの処理ルーチンになります。まず、setupですが、先ほど定義した変数に値を代入しています。I2Cアドレスは0x76です。アドレスは基板のCSBにつながっている半田ジャンパーをカットすれば0x77に変更できるそうですが、特に必要ないのでそのまま0x76を使います。次がタイムアウトの時間で、BME280は後で出てくるオーバーサンプリングの値を設定することができます。その値によって測定に要する時間が変わるのですが、今回はオーバーサンプリングを行わない設定(値を1にする)で動かすことを予定しているので最大でも10ms待てば測定が終わるので10としています。次のt_sbは、測定の間隔です。ここでは最低の0.5msとしました。その次がfilterでノイズ除去のフィルター設定ですが、通常の温度測定ではoffでよいとのことですので、0としてoffにします。そしてその次が、spiを3線式で接続するか4線式にするかの設定ですが、I2cを使う場合には関係がないので0にしておきます。その次の、osrs_tが温度測定のオーバーサンプリング、osrs_pが気圧測定のオーバーサンプリング設定です。上にも書いたように最低の1にします。なお、ここを0にすると、測定をしないという設定になります。また、センサーを湿度測定のできないBMP280だと思っていたので、湿度測定に関する設定を行っていません。最後がModeでいったんスリープモードにしておきます。Modeは2ビットで、00がスリープ。01ないし10がワンショットの測定、11が連続測定です。測定するときはワンショットの測定を指示しデータを取得します。
これらの値をレジスタに書き込む値に変換しているのがその次の部分です。書き込むレジスタはctrl_measとconfigの2つです(湿度も測定するときはcntl_humにも書き込む必要があります)。そして、ヘルパー関数というのを使って、レジスタ番号をまず出力し、続いてレジスタの値を出力しています。ヘルパー関数を使わず、通常のI2C制御用の関数を使うこともできます。その場合は、Wire.begintransmission()、Wire.write()、Wire.endtransmission()と処理を記述する必要があります(この辺はArduinoのI2Cライブラリとほぼ同じでなじみがありますが)。ヘルパー関数を使う利点は、こういった一連の手続きを一文で記述できることにあります。if(auto&& wrt=Wire.get_writer()...と書くことによって、デバイスのオープンに失敗したときはfalseを返し、オープンに成功した時だけif分の中を実行し、実行が終了すれば自動的に閉じる動作を行います。(おそらく、マルチマスタで通信がバッティングしたときはfalseになるのではと思っています。もちろん、ノイズやデバイスが正しく接続されていないとかの状況でエラーになってもfalseになると思います)

	bool setup() {
		// here, initialize some member vars instead of constructor.
		I2C_ADDR = 0x76;
		CONV_TIME = 10;
		t_sb = 0;  //stanby 0.5ms
          filter = 0; //filter O = off
    	  spi3or4 = 0; //SPI 3wire or 4wire, 0=4wire, 1=3wire
          osrs_t = 1; //OverSampling Temperature x1
  		osrs_p = 1; //OverSampling Pressure x1
  		Mode = 0; //Sleep mode
 
  		ctrl_meas_reg = (osrs_t << 5) | (osrs_p << 2) | Mode;
  		config_reg    = (t_sb << 5) | (filter << 2) | spi3or4;
		if (auto&& wrt = Wire.get_writer(I2C_ADDR)) {
			wrt << 0xF4;   //register address
			wrt << ctrl_meas_reg; 
		} else {
			return false;
		}
		//return true;
		if (auto&& wrt = Wire.get_writer(I2C_ADDR)) {
			wrt << 0xF5;   //register address
			wrt << config_reg; 
		} else {
			return false;
		}
		return true;
	}

次は、begin()で測定開始の指示です。
しかし、今回はIDを読むだけなので単にリターンするだけにしてあります。

	bool begin() {
		// start read
		/*
		if (auto&& wrt = Wire.get_writer(I2C_ADDR)) {
			wrt << 0x60; // SHTC3_TRIG_H
			wrt << 0x9C; // SHTC3_TRIG_L
		} else {
			return false;
		}
		*/

		return true;
	}

その次は、タイムアウトの時間を返す関数です。

	int get_convtime() {
		return CONV_TIME;
	}

その次は、read()で測定値の読み込みです。
SHT3Cの場合は、測定値は、6バイトのデータで、温度が16ビット、チェックサムが8ビット、湿度が16ビット、チェックサムが8ビットで計6バイトのデータを読んでいます。今回は、IDを読むだけですので、読みだすデータは1バイトのみです。ただし、BME280の場合は、読みだすレジスタを指定してやる必要があります。レジスタの値を書き込んで、読みだされたデータがそのレジスタのデータです。ですので、先にレジスタの値0xD0を書き込みます。それから、Wire.get_readerを使って、書き込みの時と同様にif(auto && rdr = Wire.get_reader(I2C_ADDR, 1)){ ...} としています。引数の2つ目は読み込むデータのバイト数です。複数のデータを読む場合はここに例えば6といった風に書きます。今回は、温度チェックサム用の変数u8temp_csumが8ビットで定義されていたので、それにIDを読み込みました。そして、i16Temp = (int16_t)(u8temp_csum); として、16ビット変数のi16Tempにキャストしています。TWELITEで送信するデータが2バイトなので、それに合わせるためです。

	bool read(int16_t &i16Temp, int16_t &i16Humd) {
		// read result
		uint16_t u16temp, u16humd;
		uint8_t u8temp_csum, u8humd_csum;
		if (auto&& wrt = Wire.get_writer(I2C_ADDR)) {
			wrt << 0xD0; // reg address for ID
		} else {
			return false;
		}

		if (auto&& rdr = Wire.get_reader(I2C_ADDR, 1)) {
			rdr >> u8temp_csum; // skip the crc8 check
		} else {
			return false;
		}

		// check CRC and save the values
		/*
		if (   (CRC8_u8CalcU16(u16temp, 0xff) == u8temp_csum)
			&& (CRC8_u8CalcU16(u16humd, 0xff) == u8humd_csum))
		{
			i16Temp = (int16_t)(-4500 + ((17500 * int32_t(u16temp)) >> 16));
			i16Humd = (int16_t)((int32_t(u16humd) * 10000) >> 16);
		} else {
			return false;
		}
		*/
		i16Temp = (int16_t)(u8temp_csum);

		return true;
	}
} sensor_device;
#endif

その次の部分はSHT40を使う場合の処理の設定ですので省略します。
次に、無線通信で必要な設定を行います。まず、アプリケーションIDを0x1234sbcdとしています。これは、サンプルACT共通の設定になっています。次がチャンネルで13です。これもサンプルACTでは共通の設定のようです。そして、オプションビットの設定ですが、0ですので特に設定しないということになります。その次が論理IDで0を指定しています。0は親機の指定ですが、後の方の処理で0xFE(IDを割り振らない子機)に設定しなおしています。何故こうなっているのかはわかりません。それからアプリ固有の4文字です。これは元はBTH1でしたが、親機の方で適切に処理するように以前使ったSBS1としています。

/*** Config part */
// application ID
const uint32_t DEFAULT_APP_ID = 0x1234abcd;
// channel
const uint8_t DEFAULT_CHANNEL = 13;
// option bits
uint32_t OPT_BITS = 0;
// logical id
uint8_t LID = 0;

// application use
const char FOURCHARS[] = "SBS1";

次はセンサーの値を格納するための変数構造体です。16ビットの変数を温度用と湿度用に用意しています。sensor という名前を付けています。

// stores sensor value
struct {
	int16_t i16temp;
	int16_t i16humid;
} sensor;

それから次が、上の方で少し書いた、SM_SIMPLEで使う処理の設定です。処理の内容はINTERACTIVE、INIT、SENSOR、TX、TX_WAIT_COMP、GO_SLEEPと前のActEx_BME280_SHT3xとほぼ同じです。そのあとがシンプルステートの宣言でstepという名前を付けていますので、以降はstep.next(STATE::TX)などと使います。

// application state defs
enum class STATE : uint8_t {
	INTERACTIVE = 255,
	INIT = 0,
	SENSOR,
	TX,
	TX_WAIT_COMP,
	GO_SLEEP
};

// simple state machine.
SM_SIMPLE<STATE> step;

その次は、スリープ関数のプロトタイプ宣言です。

/*** Local function prototypes */
void sleepNow();

そして、setupです。以前の子機の所と基本的には同じなので詳細は省略します。中ほどの所で、論理ID(LID)が0の場合、強制的に0xFEを設定しています。はじめにLIRを0としているので、0xFEに設定されてしまいます。インタラクティブモードで書き換えれば違うIDになると思います。

/*** setup procedure (run once at cold boot) */
void setup() {
	/*** SETUP section */
	/// init vars or objects
	step.setup(); // initialize state machine
	
	/// load board and settings objects
	auto&& set = the_twelite.settings.use<STG_STD>(); // load save/load settings(interactive mode) support
	auto&& nwk = the_twelite.network.use<NWK_SIMPLE>(); // load network support

	/// configure settings
	// configure settings
	set << SETTINGS::appname(FOURCHARS);
	set << SETTINGS::appid_default(DEFAULT_APP_ID); // set default appID
	set << SETTINGS::ch_default(DEFAULT_CHANNEL); // set default channel
	set.hide_items(E_STGSTD_SETID::OPT_DWORD2, E_STGSTD_SETID::OPT_DWORD3, E_STGSTD_SETID::OPT_DWORD4, E_STGSTD_SETID::ENC_KEY_STRING, E_STGSTD_SETID::ENC_MODE);

	// if SET(DIO12)=LOW is detected, start with intaractive mode.
	if (digitalRead(PIN_DIGITAL::DIO12) == PIN_STATE::LOW) {
		set << SETTINGS::open_at_start();
		step.next(STATE::INTERACTIVE);
		return;
	}

	// load values
	set.reload(); // load from EEPROM.
	OPT_BITS = set.u32opt1(); // this value is not used in this example.
	
	// LID is configured DIP or settings.
	LID = set.u8devid(); // 2nd is setting.
	if (LID == 0) LID = 0xFE; // if still 0, set 0xFE (anonymous child)

	/// configure system basics
	the_twelite << set; // apply settings (from interactive mode)

	/// configure network
	nwk << set; // apply settings (from interactive mode)
	nwk << NWK_SIMPLE::logical_id(LID); // set LID again (LID can also be configured by DIP-SW.)	

	/*** BEGIN section */
	Wire.begin(); // start two wire serial bus.
	
	// let the TWELITE begin!
	the_twelite.begin();

	// setup sensor device
	sensor_device.setup();

	/*** INIT message */
	Serial << "--- TEMP&HUMID:" << FOURCHARS << " ---" << mwx::crlf;
	Serial	<< format("-- app:x%08x/ch:%d/lid:%d"
					, the_twelite.get_appid()
					, the_twelite.get_channel()
					, nwk.get_config().u8Lid
				)
			<< mwx::crlf;
	Serial 	<< format("-- pw:%d/retry:%d/opt:x%08x"
					, the_twelite.get_tx_power()
					, nwk.get_config().u8RetryDefault
					, OPT_BITS
			)
			<< mwx::crlf;
}

次はスリープから起動したときの処理です。シリアル出力以外何もしていません。

// wakeup procedure
void wakeup() {
	Serial	<< mwx::crlf
			<< "--- TEMP&HUMID:" << FOURCHARS << " wake up ---"
			<< mwx::crlf
			<< "..start sensor capture again."
			<< mwx::crlf;
}

その次がループ関数です。長いので少しずつ説明します。
まず、switch(step.state())として、step.state()の中身で分岐をします。最初はINTERACTIVEでインタラクティブモードの処理ですが、インタラクティブモードでは何もせず終了しています。次がINITで初期化処理です。ここで、sensor_device.bigen()を呼び出して、センサーを初期化しています。そのあと、step.set_timeout( )でタイムアウト待ちをします。タイムアウトの時間は上記で設定した10msです。TWELITEにもArduinoと同様にdelay()関数が用意されているので、delay(10)としてもよさそうなものですが、ここでタイムアウトを使う意味はよくわかりません。センサーなどある時間内に応答がなければ次に進むという処理の場合は意味があるようにも思いますが。

/*** loop procedure (called every event) */
void loop() {
	do {
		switch (step.state()) {
		case STATE::INTERACTIVE:
		break;
		
		case STATE::INIT: // starting state
			// start sensor capture
			sensor_device.begin();
			Serial << sensor_device.get_convtime() << mwx::crlf;
			step.set_timeout(sensor_device.get_convtime()); // set timeout
			step.next(STATE::SENSOR);
		break;

次は、SENSORでセンサーに測定指示をだして、値を読み取っています。本来は温度がi16temp、湿度がi16humidに返されるのですが、上に書いたように、ここではIDだけを読みだしているので、i16tempにIDが返されます。i16humidは何も書き込まれていないので0が返ります。

		case STATE::SENSOR: // starting state
			if (step.is_timeout()) {
				// the sensor data should be ready (wait some)
				sensor_device.read(sensor.i16temp, sensor.i16humid);

				Serial << "..finish sensor capture." << mwx::crlf
					<< "     : temp=" << div100(sensor.i16temp) << 'C' << mwx::crlf
					<< "       humd=" << div100(sensor.i16humid) << '%' << mwx::crlf
					;
				Serial.flush();

				step.next(STATE::TX);
			}
		break;

次が無線パートでTXです。基本は前の子機のプログラムと同じです。送るデータは、通信元のID、通信先のID、アプリ固有の4文字とi16tempとi16humidになりますが、ここではセンサーのIDのみを送信するためにi16humidはコメントアウトしています。

		case STATE::TX:
			step.next(STATE::GO_SLEEP); // set default next state (for error handling.)

			// get new packet instance.
			if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
				// 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(sensor.i16temp)
					//, uint16_t(sensor.i16humid)
				);

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

				if (ret) {
					step.clear_flag(); // waiting for flag is set.
					step.set_timeout(100); // set timeout
					step.next(STATE::TX_WAIT_COMP);
				}
			}
		break;

次が、無線通信の終了待ちです。これも前の子機のプログラムと基本的には同じですが、step.is_flag_ready()を見て終了判断をしています。

		case STATE::TX_WAIT_COMP: // wait for complete of transmit
			if (step.is_timeout()) { // maybe fatal error.
				the_twelite.reset_system();
			}
			if (step.is_flag_ready()) { // when tx is performed
				Serial << "..transmit complete." << mwx::crlf;
				Serial.flush();
				step.next(STATE::GO_SLEEP);
			}
		break;

次がスリープ処理です。

		case STATE::GO_SLEEP: // now sleeping
			sleepNow();
		break;

		default: // never be here!
			the_twelite.reset_system();
		}
	} while(step.b_more_loop()); // if state is changed, loop more.
}

その次は、通信の終了判断のフラグを立てる処理です。割り込み処理で通信終了時にフラグを立てています。上のループ内のTX_WAIT_COMPがstep.is_flag_ready()で、ここではstep.set_flag(ev.bStatus)となっていて少し違うのが何故かよくわかりません。

// when finishing data transmit, set the flag.
void on_tx_comp(mwx::packet_ev_tx& ev, bool_t &b_handled) {
	step.set_flag(ev.bStatus);
}

最後がスリープ処理の実態部分です。前の子機のプログラムと同じです。

// perform sleeping
void sleepNow() {
	step.on_sleep(false); // reset state machine.

	// randomize sleep duration.
	uint32_t u32ct = 1750 + random(0,500);

	// output message
	Serial << "..sleeping " << int(u32ct) << "ms." << mwx::crlf;
	Serial.flush(); // wait until all message printed.
	
	// do sleep.
	the_twelite.sleep(u32ct);
}

IDを読んだらBME280だった

冒頭に結論を書いたのでだいたい想像はついていると思いますが、このプログラムを子機に書き込んで、動かしてやると、親機にIDが送られてくるのですが、それを見るとIDが0x60となっていました(最後から2つ目のバイト)。

IDを読むと0x60でBME280であることが分かった

BMP280であればIDは0x58となるところですが、IDは0x60ですので、BME280ということがわかりました。BME280は温度、気圧に加えて、湿度も測れるセンサーでBMP280よりかなり高いので、てっきりBMP280と思っていました。これは、以前M5Stickで環境センサーを作った時に使ったやつですが、最近は使ってなかったのでここから持ってきました。以前に書いた記事を読むとVOCセンサーを補正するのに温度も湿度も必要ということでBME280を買ったとしっかり書いてありました。
alasixosaka.hatenablog.com
人間の記憶というものはあいまいなものですね。特に年を取るとよく感じます。こうやってブログに書いておいて見返すということが役立つということを改めて感じました。
ちなみに、子機側にUSBシリアル変換器を繋ぐと子機のシリアル出力が見れるのですが、こんな感じになります。

子機のシリアル出力をモニタしているところ

温度が0.96℃と出ていますが、これがIDである0x60を10進数に直して100で割った値です。
IDは無事に読み取れたので、次は温度を測定するプログラムにチャレンジします。

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

TWELITEプログラムの続きです。
前回は、親機のプログラムの中を見て、子機からの通信に対応できてない部分を修正しました。
alasixosaka.hatenablog.com
今回は、子機のプログラムをどう修正したかを書くことにします。いささか、順序が逆になったきらいはありますが。
まず、プログラムを修正するにあたって、プログラムを含むフォルダを丸ごとコピーします。Windowsではコピーしたフォルダには、xxxx コピー といった名前が付きますので名前を修正します。プログラム名に日本語が含まれているとエラーになるようです。今回は、SHT3xに対応した部分を取ってしまって、BMP280(BME280)に特化したプログラムにするので、名前も、ActE_BME280としました。フォルダ内のプログラム本体も同じ名前にしておきます。
プログラムの修正分ですが、上から順に書いていきます。

インクルードや変数などの初期化の部分

まず、インクルードの部分でSNS_SHT3xをコメントアウトします。

// use twelite mwx c++ template library
#include <TWELITE>
#include <NWK_SIMPLE>
#include <SNS_BME280>
//#include <SNS_SHT3X>
#include <STG_STD>

それから、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
*/

それから、Sensor Objectの部分もSHT3xの部分はコメントアウトします。

//SNS_SHT3X sns_sht3x;
//bool b_found_sht3x = false;

そして、アナログポート関連の設定もコメントアウトします。

//uint16_t u16_volt_vcc = 0;
//uint16_t u16_volt_a1 = 0;
Setup関数

Setupの中でまた、PALの設定が出てきますのでこれもコメントアウトします。

/*
#ifdef USE_PAL
	/// use PAL board (for WDT handling)
    auto&& brd = the_twelite.board.use<USE_PAL>(); // register board (PAL)
#endif
*/

アナログポートのセットアップのコメントアウトです。

// setup analogue
//Analogue.setup();

SHT3xセンサーを探す部分もコメントアウト

// check SHT3x
/*
{
	bool b_alt_id = false;
	sns_sht3x.setup();
	if (!sns_sht3x.probe()) {
		bool b_alt_id = false;
		delayMicroseconds(100); // just in case, wait for devices to listen furthre I2C comm.
		sns_sht3x.setup(0x45); // alternative ID
		if (sns_sht3x.probe()) b_found_sht3x = true;
	} else {
		b_found_sht3x = true;
	}
	if (b_found_sht3x) {
		Serial << crlf << "..found sht3x" << (b_alt_id ? " at 0x45" : " at 0x44");
	}
}
*/
loop関数

loopの中でアナログポートの値を読む部分をコメントアウト

/*
if (Analogue.available()) {
	if (!u16_volt_vcc) {
		u16_volt_vcc = Analogue.read(PIN_ANALOGUE::VCC);
		u16_volt_a1 =  Analogue.read(PIN_ANALOGUE::A1);
	}
}
*/

INITの処理の所でSHT3xセンサーに測定指示を出す部分もコメントアウト

/*
if (b_found_sht3x) {
	sns_sht3x.begin();
}
*/

CAPTURE_PREの所で、SHT3xの測定待ち部分をコメントアウト

/*
if (b_found_sht3x && !sns_sht3x.available()) {
	sns_sht3x.process_ev(E_EVENT_TICK_TIMER);
}
*/

センサーの測定終了判定の部分も、SHT3xの部分をコメントアウト

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

CAPTUREの部分で、SHT3x関連のシリアル出力をコメントアウト

/*
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 (1) {
		Serial
			<< crlf << format("  ADC      : Vcc=%dmV A1=%04dmV", u16_volt_vcc, u16_volt_a1);
}
*/

TXの処理の部分で、無線で送るパケットの中身を修正します。アプリ固有の4文字以外は、温度のデータのみにしてしまいます。これで、前回の親機が受け取ったデータの処理の部分と整合が取れるようになります。

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

修正は以上です。

現在リハビリ中

最近電子工作ネタばかりなので、たまには本来のトレランの話をと思ったのですが、表題の通りです。
不注意で右足の親指の骨にひびが入ってしまい、現在リハビリ中です。医者からは2週間と言われたのですが、本日で3週間経ちました。痛みはだいぶ引いてきたものの、まだ患部を強く抑えたり、ぶつけたりするとかなり痛いです。
本来なら今日は六甲山の全縦登山に参加していたはずなのですが、こんな状態ではとても完走(といってもこの大会は走ってはいけないルールになっていますが)はおぼつかないし、おまけに天気も悪く足元も悪いので最悪また痛める可能性もあると思い潔く欠場しました。
ふり返れば、今年はいろいろ計画したものの実行に移せない計画が多く、夏の光岳登山は台風で断念。その後9月の連休も台風の影響で出かけられず。さらに、けがしたお陰で、10月のオリエンテーリングと11月の六甲全縦を欠場。本当はその間に東海自然歩道にも行くつもりだったのですが、全部できずじまい、踏んだり蹴ったりの一年だったような。
足に負担がかからないように昨日は豊能町の余野までチャリで走ってきました。本当は元気があったら妙見山の山頂まで行ってこようかと思っていたのですが、休んでいる間に体力が落ちていたため途中で腰が痛くなって余野まで行って引き返してきました。妙見山は昨年末に徒歩と少しのランでいってきたところで結構懐かしい感じで、しかもお天気も良く暖かかったので気持ちよく走れました。年末と違って季節が良いのでチャリダーやバイカーの人がたくさんいましたね。特に余野のファミマはバイカーのたまり場になっている感じで、まるでバイクの展覧会みたいでした。年末に行ったときは妙見山に登る前にファミマに寄ったのですが、流石に大晦日前日でしたし寒いのもあってバイカーはあまりいませんでしたが。
その時は、降りてきてから、もう一軒あるコンビニのローソンにも寄りました。
今回はファミマの方が人が一杯いそうだったので、ローソンの方に寄って、甘いものを補給して帰ってきました。どちらのお店もイートインがあって座ってくつろげます。なぜ、ファミマの方がバイカーさん達に人気なのかよくわかりませんが、人の少ないローソンの方が個人的にはゆったりできていいです。
来週位から本格的にランが再開できるとよいのですが、初めの一週間で結構よくなったと思ったのですが、そのあとは本当に徐々にしか良くならず、いつから走れるのかちょっと焦りにも似た感じになってきています。とりえあえず、年内には東海自然歩道ランの続きを始めたいと思っているのですが、さてどうなりますか。

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

前回、TWELITE用のサンプルプログラムをダウンロードしてそのまま動かすことをやりました、また、子機用のプログラムを見てだいたいの概略を書きました。
alasixosaka.hatenablog.com

今回はその続きで、親機用のプログラムを見てみます。
親機用のプログラムは、Parent-MONOSTICKという名前のフォルダにあります。プログラムファイルはParent-MONOSTICK.cppです。中身を見ていきましょう。
冒頭の部分は子機とあまり変わりません。まずライブラリのインクルード。

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

子機ではセンサー用のライブラリSNS_BME280とSNS_SHT3Xを読み込んでいましたが、親機はMONOSTICKを読み込んでいます。MONOSTICKを使わない場合は必要ないような気がします。LED制御とウォッチドックタイマー制御を含むとマニュアルに書いてあります。
続いてデバイスの設定です。アプリケーションIDを子機と同じく1234abcdとしています。チャンネルも13です。オプションビット用の変数を0にしています。インタラクティブモードでオプションビットの設定を使用しないということのようです。そして、その次が関数プロトライプの宣言です(実はあまりよくわかっていないのですが、この関数で送られてきたパケットを解析して適切なデータを取り出しています)。

/*** Config part */
// application ID
const uint32_t DEFAULT_APP_ID = 0x1234abcd;
// channel
const uint8_t DEFAULT_CHANNEL = 13;
// option bits
uint32_t OPT_BITS = 0;

/*** function prototype */
bool analyze_payload(packet_rx& rx);

続いてセットアップに入ります。

void setup() {
	/*** SETUP section */
	auto&& brd = the_twelite.board.use<MONOSTICK>();
	auto&& set = the_twelite.settings.use<STG_STD>();
	auto&& nwk = the_twelite.network.use<NWK_SIMPLE>();

まず、モノスティック用の関数群(ビヘイビアというようです)の読み込み、インタラクティブモード関連のビヘイビアの読み込み、通信用ビヘイビアの読み込みです。通信はこちらも子機同様シンプルモードを使います。
次がインタラクティブモードの設定です。インタラクティブモードではアプリケーションの名前、アプリケーションのID、チャンネル、LID(論理ID)を表示します。そして、EEPROMから値をロードします。

// settings: configure items
	set << SETTINGS::appname("PARENT");
	set << SETTINGS::appid_default(DEFAULT_APP_ID); // set default appID
	set << SETTINGS::ch_default(DEFAULT_CHANNEL); // set default channel
	set << SETTINGS::lid_default(0x00); // set default LID
	set.hide_items(E_STGSTD_SETID::OPT_DWORD2, E_STGSTD_SETID::OPT_DWORD3, E_STGSTD_SETID::OPT_DWORD4, E_STGSTD_SETID::ENC_KEY_STRING, E_STGSTD_SETID::ENC_MODE);
	set.reload(); // load from EEPROM.
	OPT_BITS = set.u32opt1(); // this value is not used in this example.

先ほどの設定値が入った変数setをthe_tweliteに設定し、受信を設定します。

// the twelite main class
the_twelite
	<< set                    // apply settings (appid, ch, power)
	<< TWENET::rx_when_idle() // open receive circuit (if not set, it can't listen packts from others)
	;

また、ネットワークの設定も、先ほどのsetで設定した後、論理IDだけは(念のため)0に設定しています。これは、インタラクティブモードで間違って別のIDを設定してしまうと親機として動作できなくなるためと思われます。

// Register Network
nwk << set;					// apply settings (LID and retry)
nwk << NWK_SIMPLE::logical_id(0x00) // set Logical ID. (0x00 means parent device)
  ;

次は、LEDの点滅間隔の設定ですが、説明は省略します。
そして、TWELITEをスタートさせます。

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

続いてループ関数ですが、中身が空です。親機は無線を受信したときの割り込み処理ですべてを処理してしまっているようです。
つまり、常にループで回って割り込み待ちをしておき、割り込みがあった時だけその処理をしているということになります。
その割り込み処理ルーチンですが、

void on_rx_packet(packet_rx& rx, bool_t &handled) {
	Serial << ".. coming packet (" << int(millis()&0xffff) << ')' << mwx::crlf;

から始まります。2行目はシリアルにパケットの着信を書き込んでいるだけです。
次に、

if(0) { // this part is disabed.
	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.flush();
}

という部分がありますが、If文のかっこの中が0なので、このルーチンはコメントにある通り通ることがないようになっています。デバック用の設定のようで、子機から送られてきた生のデータを見る時にかっこの中を1に書き換えると"RAW PCKET"に引き続き表示されるようになります。
その次はif文のかっこ内が1なので、必ず実行されます。ここも理解があいまいですが、bufという変数に、送信元ID、固定のCC、応答ID、送信元のシリアルID、送信先のシリアルID、LQI(通信品質)、データ長、データの順に格納され、それをシリアル出力しているようです。これは、RAW PACKETとあまり変わらないものになります。データの中身は、前回の子機のプログラムの説明を見てください。

if (1) {
	smplbuf_u8<256> buf;
	pack_bytes(buf
		, uint8_t(rx.get_addr_src_lid())		// *1:src addr (LID)
		, uint8_t(0xCC)							// *2:cmd id (0xCC, fixed)
		, uint8_t(rx.get_psRxDataApp()->u8Seq)	// *3:seqence number
		, uint32_t(rx.get_addr_src_long())		// *4:src addr (long)
		, uint32_t(rx.get_addr_dst())			// *5:dst addr
		, uint8_t(rx.get_lqi())				// *6:LQI
		, uint16_t(rx.get_length())			// *7:payload length
		, rx.get_payload() 				// *8:payload
			// , make_pair(rx.get_payload().begin() + 4, rx.get_payload().size() - 4)
			//   note: if you want the part of payload, use make_pair().
	);

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

引き続きパケットの中身を解析する関数を呼び出します。

// packet analyze
analyze_payload(rx);

その関数の中身ですが、
まず冒頭の部分でよくわからない記述があります。#if 1 .... #else ... #endifとあるので、if 1以下の部分しか実行されないように思います。いずれにしても、やっていることは、アプリ固有の4文字の取得です。BMP280とSHT3x用のアプリの場合はこの4文字は"SBS1"でした。実はこれが問題なのです。どう問題なのかは後で書きます。

#if 1
	// expand packet payload (shall match with sent packet data structure, see pack_bytes())
	uint8_t fourchars[4]{}; // init all elements as default (0).
	auto&& np = expand_bytes(rx.get_payload().begin(), rx.get_payload().end()
		, fourchars
    );
#else
	// an example to pass std::pair<char*,int>.
	char fourchars[5]{};
	auto&& np = expand_bytes(
		    rx.get_payload().begin(), rx.get_payload().end()
			, make_pair((char *)fourchars, 4)
		);
#endif

次は、4文字が正しく読めなかった場合です。

// if heading 4 bytes are not present, unexpected packet data.
if (np == nullptr) return false;

それからそれぞれのアプリに対応した処理が続きます。例えば、

// Slp_Wk_and_Tx
if (!b_handled && !strncmp((char*)fourchars, "TXSP", 4)) {

以下の部分は、Slp_Wk_and_Txというアプリに対応した部分で4文字が"TXSP"です。以下、"BAT1"、"PAB1"、"PAB2"、"PMG1"、"PMT1"と続きますが、SBS1はありません。どうも、エクストラのアプリになっていたので、標準では対応していないようです。従って、本当は、測定した温度、湿度を表示してほしいところですが、"..not analyzed.." と表示されてしまいます。
そこで、SBS1に対応するように以下の文をこの関数の中に追記しました。これが、前回ちらっと書いた、Parent-MONOSTICK_2というアプリです。

if 	(!b_handled && 
		(  !strncmp((char*)fourchars, "SBS1", 4) // BMx280		
		)
	)
{
	b_handled = true;

	//uint32_t u32lumi;
	uint16_t u16temp;
	//uint16_t u16humd;

	np = expand_bytes(np, rx.get_payload().end()
		//, u32lumi
		, u16temp
		//, u16humd
	);

	if (np != nullptr) {
		Serial 	//<< format("Lumi:%d", u32lumi)
				<< " Temp:" << div100(int16_t(u16temp))
				//<< " Humid:" << div100(int16_t(u16humd))
				;
	} else {
		Serial << ".. error ..";
	}
}

実は、これは4文字コードでは"PAB1"というアプリの処理そのままなのですが、測定されない、照度と湿度の項目はコメントアウトしています。もちろん、子機側からの送信データも合わせないと正しく表示されないので、子機側から送るデータも余分な部分はカットして、BMP280の温度のデータのみにしてあります。温度のデータは16ビットの符号なし変数u16tempに格納されます。それを符号付きの16ビット変数に変換しさらに100で割ってシリアルから表示します。この処理で合っているのか自信はないのですが、一応室温らしきデータは表示されました。つまり、送られてくるデータは2バイトのデータでそれを一旦符号なしの16ビット変数に格納します。なぜ、いきなり符号ありにしないのかよくわからないのですが、それをさらに符号ありの16ビット変数に変換しています。これでマイナスが表示されるようになるのでしょう。ちなみに湿度も同じ処理をしていますが、湿度がマイナスになることはないはずなのでなぜこのような処理なのかよくわかりません。データは実際の温度を100倍したものなので、100で割って小数点2桁の実数に変換しています。
子機側のプログラムの変更についてはまた、次回に書いてみたいと思います。