RubicでGR-CITRUSのmrubyプログラミング事始め3[UDPでNTP編]


前々回前回、を経て以下の予定でいうところの2の部分を行います。

1. WA-MIKANを使ってSDカードにあるMP3データを鳴らしてみます。
2. WA-MIKANを使ってUDP通信を行い、NTPから時刻(Unix-Time)をとる処理を行います。
3. mrbgemsの入れて(めんどい)、mruby-timeを入れて、NTPから取ったUnix-Timeを時刻に変換する処理をします。
4. 今までのことを組み合わせて、1時間ごとにしゃべる時計を作ります。

ようやくインターネットにつなげるところです。TCPなんて高尚?なものは使わないで最初はUDPですよね!

インターネット経由で自動的に時刻を合わせることを行う予定ですが、2回に分けて行います。
まず今回は時刻の情報を取得するところを行い、そして次回に取得した時刻の情報をつかってGR-CITRUSの時計を合わせるところをやります。

今回つかうもの

  • GR-CITRUS
  • WA-MIKAN

SNTPを使う

NTPの簡易版のSNTPにUDP経由で接続して時間を取得します。

SNTPとは、TCP/IPネットワークを通じてコンピュータの時刻を同期させるプロトコルの一つで、NTPの簡易版。RFC 2030で定義されている。NTPは時刻情報サーバを階層的に構成し、情報を交換して時刻を同期するプロトコル。SNTPはNTPの仕様のうち複雑な部分を省略し、クライアントがサーバに正確な時刻を問い合わせる用途に特化している。
出典 IT用語辞典 e-wordsより

SNTPの使い方は、

  1. クライアントはNTPサーバにUDP(ポート123番)を使って要求パケットを送る。
  2. NTPサーバから時刻情報が入った応答パケットが送られてくる。
  3. 応答パケットから時刻情報を取り出す。

となります。

要求パケットを作る。

パケットの構造はこちら~ネットで時刻を合わせるプロトコル---SNTP・その3(第66回)を参考にしましたが難しいことを考えないでとりあえず、以下のような構造の計48バイトのパケットを作ることにします。

先頭8バイト 残り40バイト
0xE30006EC 0x00
main.mrb
#!mruby
NTP_PACKET_SIZE = 48

Usb = Serial.new(0)
#NTP用要求パケットを作る
packetBuffer = Array.new(NTP_PACKET_SIZE , 0)
packetBuffer[0] = 0b11100011   # LI, Version, Mode , 0b11100011は2進法表示で16進表示の0xE3と同じ
packetBuffer[1] = 0     # Stratum, or type of clock
packetBuffer[2] = 6     # Polling Interval
packetBuffer[3] = 0xEC  # Peer Clock Precision

Usb.println "Data:" + packetBuffer.to_s

binary = packetBuffer[0].chr  #binaryが送信するパケットととなる。
for num in 1..NTP_PACKET_SIZE-1 do
    binary += packetBuffer[num].chr
end

ポイントですが、rubyでバイト配列を作る際はpackメソッドを使うらしいのですが、GR-CITRUSのmrubyには標準ではこのメソッドがありません。そのため、chrを使うことでここではバイト配列を作ることにしています。

参考
ツナでもわかるmruby [4回目:バイナリデータフォーマットの実装]
mrubyでのバイナリの扱い

UDP接続して要求パケットを送る&応答パケットを受け取る。

では、要求パケットは作成できたのでUDP接続を行いましょう。
ソースはこちらを参考に作ってみました。
また、以下も参考にしております。
UDPの接続(send)
UDPの接続(recieve)
GR-CITRUS クラス、メソッド早見表

ちなみに遅延補正とかは特にやっていません。とりあえず時間をとるのを目的としています。正確性には欠けますのでご注意を。

ソースはSSIDとPASSの部分はご自身の環境に合わせて適宜修正してください。

main.mrb
#!mruby
SSID="あなたのSSID"
PASS="あなたのPASS"
NTP_PACKET_SIZE = 48
timeServer="132.163.4.101" #time-a.timefreq.bldrdoc.gov
#timeServer="129.6.15.28"  #time.nist.gov
timeZone = 9 #Tokyo
localPort = 8788 #local port to listen for UDP packets
sendPort = 123 # NTP requests are to port 123
pollIntv = 150      # poll every this many ms
maxPoll = 15        # poll up to this many times

Usb = Serial.new(0)

#ESP8266を一度停止させる(リセットと同じ)
pinMode(5,1)
digitalWrite(5,0) # LOW:Disable
delay 500
digitalWrite(5,1) # LOW:Disable

if( System.useWiFi() == 0)then
Usb.println "WiFi Card can't use."
System.exit() 
end

#WiFi接続準備をする。
Usb.println "WiFi Ready"

Usb.println "WiFi Get Version"
Usb.println WiFi.version

Usb.println "WiFi disconnect"
Usb.println WiFi.disconnect

Usb.println "WiFi Mode Setting"
Usb.println WiFi.setMode 3 #Station-Mode & SoftAPI-Mode

Usb.println "WiFi ipconfig"
Usb.println WiFi.ipconfig

Usb.println "WiFi connecting"
Usb.println WiFi.connect(SSID,PASS)

Usb.println "WiFi ipconfig"
Usb.println WiFi.ipconfig

Usb.println "WiFi multiConnect Set"
Usb.println WiFi.multiConnect 1

#NTP用要求パケットを作る
packetBuffer = Array.new(NTP_PACKET_SIZE , 0)
packetBuffer[0] = 0b11100011   # LI, Version, Mode
packetBuffer[1] = 0     # Stratum, or type of clock
packetBuffer[2] = 6     # Polling Interval
packetBuffer[3] = 0xEC  # Peer Clock Precision

binary = packetBuffer[0].chr
for num in 1..NTP_PACKET_SIZE-1 do
    binary += packetBuffer[num].chr
end

#NTPサーバーにUDP接続して要求パケットを送る
Usb.println "UDP OPEN"
Usb.println WiFi.udpOpen(1, timeServer, sendPort, localPort)
Usb.println "UDP SEND " + packetBuffer.length.to_s + " Data:" + packetBuffer.to_s
WiFi.send( 1,binary)

#NTPサーバーからの応答パケットを受け取る。
array = WiFi.recv 1
maxPoll.times do
  delay(pollIntv)    
  if (array[0] >= 0) then
      if (array.length == 48) then
          break
      end  
  end
  array = WiFi.recv 1   
end
WiFi.cClose(1)

#応答パケットが48バイトでなければ終了
if array.length != 48 then
    System.exit() 
end    

Usb.println array.to_s

#40から43バイト目に時刻情報が入っている
time = array[40]
for i in 1..3 do
  time = time << 8 | array[40+i]
end

#UNIXタイムは1970年1月1日0時から始まる
#1900年から1970年の70年を秒で表すと2208988800秒になる
UnixTime    = time - 2208988800 # convert NTP time to Unix time
UnixTimeJST = UnixTime + (timeZone * 60 * 60) #東京の時刻に変更する
Usb.println UnixTimeJST.to_s #1970年からの経過時間(秒)

要求パケットを送信すると、応答パケットも48バイトで帰ってきます。
応答パケット中の40バイトから43バイト目に時刻の情報が入っています。
これはNTPタイムスタンプと呼ばれるものでこれをUNIXタイムに変換しています。
これは1970年1月1日から現在までの経過時間(秒)となります。

Unix-time(最後に表示された値)が以下のサイトで表示された値と大体同じならば成功です。
Unixtime相互変換ツール

UNIXタイムを一般的な時刻(グレゴリオ暦)に自力で変換する。

では、このUNIXタイム(1970年からの経過秒)を頑張って一般的な時刻データ(グレゴリオ暦)に直してみましょう。普通はRubyではTimeメソッドなどが使えるものですが、CITRUSのmrubyに標準ではそんな便利なものはありません。自力で変換していきます。

UNIXタイムのデータから秒、分、時のデータはそれぞれの単位の余りになりますので簡単です。
ただし、UNIXタイムから年月のデータに直すにはうるう年を考えなくてはなりません。
うるう年の定義は国立天文台のHPによると

  1. 西暦年号が4で割り切れる年をうるう年とする。
  2. 1の例外として、西暦年号が100で割り切れて400で割り切れない年は平年とする。

となります。こちらにも参考になりそうなソースがありましたので、知恵を拝借させていただきmrubyで書いていきましょう。

main.mrb
#!mruby

#閏年の判定
def IsLeapYear(year)
    if ((year % 4)==0 && ((year % 100)!=0 || (year % 400)==0)) then
        return true
    else
        return false
    end    
end
# 閏年でない年の各月の日数
dayofm  = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
# 閏年の各月の日数
dayoflm = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
# 各月の省略文字
monthstr = ["Jan", "Feb", "Mar", "Apr", "May", "Jun","Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]


Usb = Serial.new(0)

#ESP8266を一度停止させる(リセットと同じ)
pinMode(5,1)
digitalWrite(5,0) # LOW:Disable
delay 500
digitalWrite(5,1) # LOW:Disable

if( System.useWiFi() == 0)then
Usb.println "WiFi Card can't use."
System.exit() 
end

#WiFi接続準備をする。
Usb.println "WiFi Ready"

Usb.println "WiFi Get Version"
Usb.println WiFi.version

Usb.println "WiFi disconnect"
Usb.println WiFi.disconnect

Usb.println "WiFi Mode Setting"
Usb.println WiFi.setMode 3 #Station-Mode & SoftAPI-Mode

Usb.println "WiFi ipconfig"
Usb.println WiFi.ipconfig

Usb.println "WiFi connecting"
Usb.println WiFi.connect(SSID,PASS)

Usb.println "WiFi ipconfig"
Usb.println WiFi.ipconfig

Usb.println "WiFi multiConnect Set"
Usb.println WiFi.multiConnect 1

#NTP用要求パケットを作る
packetBuffer = Array.new(NTP_PACKET_SIZE , 0)
packetBuffer[0] = 0b11100011   # LI, Version, Mode
packetBuffer[1] = 0     # Stratum, or type of clock
packetBuffer[2] = 6     # Polling Interval
packetBuffer[3] = 0xEC  # Peer Clock Precision

binary = packetBuffer[0].chr
for num in 1..NTP_PACKET_SIZE-1 do
    binary += packetBuffer[num].chr
end

#NTPサーバーにUDP接続して要求パケットを送る
Usb.println "UDP OPEN"
Usb.println WiFi.udpOpen(1, timeServer, sendPort, localPort)
Usb.println "UDP SEND " + packetBuffer.length.to_s + " Data:" + packetBuffer.to_s
WiFi.send( 1,binary)

#NTPサーバーからの応答パケットを受け取る。
array = WiFi.recv 1
maxPoll.times do
  delay(pollIntv)    
  if (array[0] >= 0) then
      if (array.length == 48) then
          break
      end  
  end
  array = WiFi.recv 1   
end
WiFi.cClose(1)

#応答パケットが48バイトでなければ終了
if array.length != 48 then
    System.exit() 
end    

Usb.println array.to_s

#40から43バイト目に時刻情報が入っている
time = array[40]
for i in 1..3 do
  time = time << 8 | array[40+i]
end

#UNIXタイムは1970年1月1日0時から始まる
#1900年から1970年の70年を秒で表すと2208988800秒になる
UnixTime    = time - 2208988800 # convert NTP time to Unix time
UnixTimeJST = UnixTime + (timeZone * 60 * 60) #東京の時刻に変更する
Usb.println UnixTimeJST.to_s #1970年からの経過時間(秒)

# 日付の計算
wt   =   UnixTimeJST           #作業用にコピー
sec  =   wt % 60               #余りが秒になる
wt  -=   sec                   #秒を引く
min  = ( wt.div(60) ) % 60     #秒を分の単位にして、余りが分になる
wt  -=   min                   #分を引く
hour = ( wt.div(60*60)) % 24   #時間の単位にして、余りが時になる
wt  -=   hour                  #時を引く
day  = ( wt.div(60*60*24) )    #この時点では1970年からの経過日数
year = 1970
month = 1

#1970年からの経過年数
while (day>366) do
    #うるう年と通常年で引く日数を分ける
    if IsLeapYear(year) then
        day -= 366
    else
        day -= 365
    end
    year+=1
end
day+=1 # 1月1日は 0 だから。この時点で今年の経過日数が入っている。

#それぞれの月の経過日数を引く
while (1) do                                 
    if (IsLeapYear(year)) then                # もし閏年なら 
        if (day <= dayoflm[month-1]) then     # 月の日数より day が少なければ
            break
        else                              #月の日数より day が多ければ 
            day -= dayoflm[month-1]       #月の日数を引き 
            month+=1                      #月を 1 増やす
        end
    end
    if (!IsLeapYear(year)) then                  #もし閏年でなければ
        if (day <= dayofm[month-1]) then         #以下同上
            break
        else
            day -= dayofm[month-1]
            month+=1
        end
    end
end
#この時点でdayは今月の経過日数が入る
Usb.println year.to_s + "/" + month.to_s + "/" + day.to_s + " " + hour.to_s + ":" + min.to_s + ":" + sec.to_s + " " + monthstr[month-1]

そろそろ結構ソースが長くなってきました。もうちょっと楽したい。

実は、こんなに苦労して自力で書かなくともmrubyには拡張用のmrbgemsがあります。
でもmrbgemsを入れるには自分でファームウェアをコンパイルしなくてはならないのでどっちが楽かは悩むところだったりします(苦笑)

では、次回はmrbgemsの入れ方&mruby-timeを使ったUNIXTIMEの変換方法をやっていきたいと思います。