hunamizawa’s blog

無い物は作りたい人のメモ帳

ESP8266/ESP32 でうるう秒を扱えるライブラリ『ESPPerfectTime』を作ったので使ってほしい

github.com

ESP8266/Arduino で置時計を作った。せっかくなので、うるう秒(午前8時59分60秒)を表示できるようにしたくなった。

SNTP とうるう秒

ESP8266/ESP32 で時計を作る利点として、SDK に SNTP クライアントが含まれているので、Arduino なら configTime(...) と1行書くだけで簡単に時刻合わせができる。時刻同期やその管理を気にしなくていいのは楽だ。

ところで NTP パケットには、うるう秒の挿入・削除を予告する Leap Indicator (LI) というフィールドが含まれている。残念ながら ESP8266/ESP32 組み込みの SNTP クライアントは、うるう秒に対応していない。なので自前で NTP パケットを解析して、LI の値を見ればいい、という話。

もう一つの問題:Round-Trip Delay

(ぶっちゃけ、時計を作るならこっちの方が大きな問題になる)

SNTP は、サーバーの現在時刻をネットワーク越しにクライアントに送ってもらうことで、時刻を合わせている。ということは、ネットワークの遅延が時計の精度にモロに影響してくるわけである。極端な例えをすると、RTT = 10秒の通信路なら、サーバーが送ったパケットがクライアントに届くまで平均5秒かかるので、何も考えなければクライアントの時計は5秒遅れてしまう*1

そこでクライアントは、リクエストを送る瞬間の時刻をパケットに記録しておいて、レスポンスが帰ってきた時に引き算で RTT を求めて、遅延を補正する処理をしている。詳しくは下の記事を見てほしい。

milestone-of-se.nesuke.com

で、手元で実験したところ、ESP8266/ESP32 組み込みの SNTP クライアントは、残念ながら RTT を補正する処理を実装していないようだ*2。ついでに、こちらも実装することにしよう。

実装

lwip に含まれている sntp.c を、うるう秒対応&RTT を補正するように改造した。

リクエストパケットを生成する initialize_request() の中で Transmit Timestamp を記録している。本来は UNIX time から NTP time に変換しなきゃいけないけど、RFC4330 の中で「サーバーは、クライアントから送られてきた Transmit Timestamp を、そのまま Originate Timestamp にコピーして送り返さねばならない」と規定されている*3ので、この変換をケチって UNIX time をそのまま記録している。

実際の RTT の計算は、レスポンスが帰ってきた後の process() で実行している。NTP のタイムスタンプは 1 bit = 1/(232) [sec] なので、上位 32 bit はそのまま秒として読めるし、下位 32 bit は 4295 で割るとマイクロ秒になる*4

使い方

README.md とか、Sample を見ればわかると思う。

gmtime() localtime() の使い方が2種類あることには注意してほしい。

// 現在時刻を取得する使い方
struct tm current = *pftime::localtime(nullptr);

// time_t の値を struct tm に変換する関数としての使い方
// time_t t;
struct tm tm = *pftime::localtime(t);

// 現在時刻を取得するのに、こういう使い方はしてほしくない
time_t t = time(nullptr);
struct tm current = *pftime::localtime(t);

欠点

世の中には、うるう秒が原因で障害を起こすシステムもある*5。そういうところ向けに、時計を少しずつ、何時間もかけてずらすことで、うるう秒の存在を消し去ってくれる(SLEW モード)NTP サーバーもある(例えば google のとか)。SLEW モードで運用されている NTP サーバーを同期先として設定していると、当然 LI = 0 しか帰ってこないので、「うるう秒を表示したい」という当初の目的は達成できない*6

NICT の NTP サーバー ntp.nict.jp のように、今まで STEP モード(うるう秒を正しく挿入する)で運用されてきた NTP サーバーでも、次回のうるう秒挿入時も STEP モードで運用してくれる保証はない。うるう秒の瞬間を確実に見たい人は、うるう秒挿入が近づいたら、サーバー管理者の告知をチェックするべきだろう。

*1:現実には、ESP8266 のデフォルトでは RTT >= 3秒でタイムアウトするので、1.5 秒以上遅れることはないが。

*2:まあ、普通の組み込み環境で、時刻の精度にそこまでこだわる必要は無いし……

*3:RFC から引用: If the server is unsynchronized or first coming up, ... the Transmit Timestamp field of the request is copied unchanged to the Originate Timestamp field of the reply. It is important that this field be copied intact, as an NTP or SNTP client uses it to avoid bogus messages. ... If the server is synchronized, ... The Originate Timestamp field is set as in the unsynchronized case above.

*4: (1/106) ÷ (1/232) = 4294.6...

*5:障害の原因になるのでうるう秒を廃止しよう、という議論があるくらいである。

*6:このような場合でも、デフォルトで1時間ごとに同期するので、時刻が大きくズレることはない。