ESP8266でNTPサーバーから時刻を取得し、RTCに書き込み、TWELITEで送信する

前回、ESP8266をスリープさせてTWELITEで起こすことをやりました。
alasixosaka.hatenablog.com

前々回は、ESP8266をWiFiに繋いで、NTPサーバーから時刻を取得し、RTC-8564に書き込みました。
alasixosaka.hatenablog.com

今回は、ESP8266が書き込んだ時刻をTWELITEで読み取って、無線で送信することをやってみます。
これができれば、時刻サーバーの基本部分が完成です。
配線は、前回の配線に、TWELITEとRTC-8564のI2C接続を追加します。

心配なのは、この接続だとI2Cのマスターが2つになってしまうのでシグナルがかち合ったときにうまく動いてくれるかという問題です。
こればっかりはやってみないとわからないのでとりあえずやってみることにします。
ESP8266側のプログラムは、前々回のNTPサーバーから時刻を取得するプログラムをほぼそのまま使います。違っている点は、ディープスリープ時間を1時間としていたのを ESP.deepSleep(0); として、時間を無限にしておくことです。変更点はそれだけです。もっとも、動き出して1分後にはリセットがかかってしまうので、どちらにしても結果は同じことになりますが。

TWELITEのプログラム

まずは無線なしで

TWELITE側は、今回も、温湿度センサーBME280を使ったときのプログラムをベースにした、前回のプログラムを改良しますが、結構変える必要があります。
いきなり全部変えてしまってうまく動かす自信もないので、まずはRTC-8564の値を読み取ってシリアルに出力するところまでやります。
まず、RTCの読み取り部分ですが、struct SHTC3 {....}の中のbool read(... の所を前回はコメントアウトしていたのですが、下記のように書き換えます。
変数がいっぱい出てきますが、i8sec, i8min, i8hour, i8day, i8wday, i8month, i8yearの7つは読み取った値の戻り値です。
また、u8sec, u8min, u8hour, u8day, u8wday, u8month, u8yearの7つはRTC-8564のレジスタから読みだした値を一時的に格納する変数です。

bool read(uint8_t &i8sec, uint8_t &i8min, uint8_t&i8hour, uint8_t &i8day, uint8_t &i8wday, uint8_t &i8month, uint8_t &i8year) {
	// read result
	uint8_t u8sec, u8min, u8hour, u8day, u8wday, u8month, u8year;
	if (auto&& wrt = Wire.get_writer(I2C_ADDR)) {
		wrt << 0x02; // register address for temp
	} else {
		return false;
	}

	if (auto&& rdr = Wire.get_reader(I2C_ADDR, 7)) {
		rdr >> u8sec; // read temperature
		rdr >> u8min;
		rdr >> u8hour;
		rdr >> u8day;
		rdr >> u8wday;
		rdr >> u8month;
		rdr >> u8year;
	} else {
		return false;
	}
	i8sec = u8sec & 0x7F;
	i8min = u8min & 0x7F;
	i8hour = u8hour & 0x3F;
	i8day = u8day & 0x3F;
	i8wday = u8wday & 0x07;
	i8month = u8month & 0x1F;
	i8year = u8year;

	return true;
}

日時が格納されているレジスタは2番からの連番ですので、まずRTCに2を書き込んで、レジスタの番号を2番にします。

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

の部分がそうです。
そして、連続で7つのデータを読み出します。

if (auto&& rdr = Wire.get_reader(I2C_ADDR, 7)) {

以下の部分がそうです。
最後に、読み取ったデータを有効ビットにするためにそれぞれアンドを取っています。色々試してみましたが、今回はこの部分が肝で、なぜだかわからないのですが、無効なビットが立ったりクリアされたりと不可解な動作をします。ESP8266だとこんなことにはならないのですが、一応有効ビットの部分は安定して読み出せているので今のところはこの処理でよいのかなと思っています。
I2C通信用のWire関数はこのプログラムで使っているヘルパークラスと通常のArduinoなどのプログラムで使うのとほぼ同じメンバー関数があります。
メンバー関数は、Wire.beginTransmission(xx); で始まり、Wire.requestFrom(xx, y); で読み込みます(xxはI2Cアドレス、yは読み込むバイト数)。
RTC-8564から読みだした値が不安定だったので、メンバー関数の方も試してみましたが、メンバー関数を使うとなぜかうまくデータ読み出せませんでした。書き込みの方はエラーなしで帰ってくるのですが、読み込みを行うとエラーになり得られたデータも7つすべての値が同じという不可解なことになってしまいました。なので、諦めてヘルパークラスの方を使って、得られたデータの有効ビットだけを残すというこの方法を使います。
シリアルの出力はこんな感じです。

RTC-8564の値を読み取ってシリアル出力されたデータ

RTCC Alarmと出ているところが、RTCでアラームが起動したことを検知している部分です。これで、ESP8266にリセットシグナルを送ってディープスリープから起こしています。ESP8266はディップスリープから起きると再びWiFiに接続しに行くため、その間はI2CにアクセスしないようにTWELITEの方は10秒のディレイを入れています。

ESP8266ではシリアル接続が必要?

ある程度安定に動いたら 、ESP8266側はシリアルをモニタする必要がないので、シリアル変換器のついた開発ボード(NodeMCU)からノーマルのESP8266に変えてみました。すると、USBシリアル変換器(FT-232RL)につながっているPCのUSB端子を抜くとESP8266が止まってしまうという現象が起こりました。最初は何故なのかよくわからず、ネットで調べても類似の現象が見つけられなくて結構悩みました。結論から言うと、USBシリアル変換器がつながっているとだめのようです。
シリアル送信の所がネックになるのかと思い、シリアル送信のコマンドを全部コメントアウトしてもやっぱりだめで、結局USBシリアル変換器をブレッドボードから引っこ抜いたら動くようになりました。

ESP8266のリセットが安定しない。

これで、しばらく動かしていると、最初のうちは1分おきにちゃんとリセットがかかったのですが、そのうちにリセットに失敗することが発生することがわかりました。はじめは、RTC-8564のINT端子にプルアップ抵抗を繋いでいなかったので、TWELITEがINTの出力をちゃんととらえられていないのかと思い、プルアップ抵抗を入れてみましたが症状が改善しないので、ネットで調べてみました。ESP8266のリセット動作が安定しない問題は割と有名らしく、いろいろな記事が見つかりました。結論としては、EN端子とGNDの間にコンデンサを入れてやればよいようです。
ehbtj.com
ただ、テストに使ったESP8266はマイクロテクニカの変換ボードに載っているやつで、残念ながらEN端子が引き出されていません。こいつは、リセットとIO0のスイッチ、抵抗などがボード上に実装されているので、外付けの部品が不要でUSBシリアル変換器だけ繋げばよかったので便利だったのですが、仕方ありません。
deviceplus.jp
もう売ってないみたいですが。

なので、また別のESP8266を引っ張り出してきて、今度は秋月電子で買ったESP8266を使いました。これは本当に素のESP8266をブレッドボードに刺せるように変換しているだけなので、スイッチも抵抗も一切実装されていないので、全部ブレッドボードに実装する必要があります。
こちらのサイトを参考に配線しました。
lipoyang.hatenablog.com
ただし、EN端子の部分だけは、上記のサイトのように、リセット対策として10kΩの抵抗でプルアップして、1μFのコンデンサをGNDとの間に入れておきます。
これで、リセットが安定してかかるようになりました。パワーオンリセットではないのでコンデンサに意味があるような気がしないのですが、結果的によくなりました。ひょっとしたらESP8266の個体差なのかもしれません。

無線送信をする

TWELITEのプログラムに戻ります。
RTC-8564からデータが読み出せたら、あとはそれほど難しくありません。
loop関数の中のSTATE::TXの部分を下記のようにします。やっていることはBME280の時とほぼ同じです。pack_bytes(pkt.get_payload() 以下の部分で、送信するパケットを作っています。ここに、秒、分、時、日、月、年、曜日の各データを1バイトずつ加えていっているだけです。

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(0xFF)  // 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.
			, uint8_t(sensor.i8sec)
			, uint8_t(sensor.i8min)
			, uint8_t(sensor.i8hour)
			, uint8_t(sensor.i8day)
			, uint8_t(sensor.i8month)
			, uint8_t(sensor.i8year)
			, uint8_t(sensor.i8wday)
		);

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

これで送信ができるようになります。