2020年11月25日水曜日

RasberryPiをGNSS/GPSで時刻を合わせる RTC無いの?

この記事は次の環境を想定して書かれています

RaspberryPi WH
RaspberryPi4 
RaspberryPiOS
python3
GNSS/GPSアンテナモジュール シリアルポート接続
GU-902MGG-USB USB-UARTブリッジ(Prolific PL2303SA /秋月電子通商)ZED-F9P USB接続 

液晶LCD1602モジュールと組み合わせた電波時計の作例はこちら 

GPSとGNSS

GPS(グローバルポジショニングシステム)は、日本国内では衛星を使った測位システムとして認識されていますが、本来GPSはアメリカの測位システムの名前です。現在、GPSアンテナと総称されているものは他の測位衛星GLONASS(ロシア)やみちびき(日本)Galileo(欧州)やBeiDou(中国)も利用して測位しています。そのため、利用する測位システムの名称としては、GNSS(Global Navigation Satellite System / 全球測位衛星システム)が正確にあらわしていますので、以下GNSSに統一します。

RaspberryPiにはRTCが無い!

 さてさて、GNSSロガーを作って、実際に山登りしながらロギングしていると、ログファイルのタイムスタンプがずれているではないですか。いつも時刻があっていて、てっきりRaspberryPiには時計が内蔵されているものだと思っていたので、当日のタイムスタンプのファイルが無くGNSSログが取れていないかのかと頭が真っ白になりました。でも前日のタイムスタンプのファイルがログファイルだった(汗)

確かにパソコンに入っているCR2032みたいなボタン電池は入っていないし、当然RTC(リアルタイムクロック:内蔵時計)は無い。

自宅で使っている分にはネットワーク環境があるのでNTPで時間がピッタリあっている。でも、出先で携帯の電波も入らないようなところだと、当然テザリングでの時刻合わせもできない。かといって、携帯電波の入るところでRaspberryPiを起動して自動的に時刻合わせをした上で現地向かうとして、到着後登山リュックにGPSアンテナ配線したりRaspberryPiを仕込んでバッテリー配線したりしているときに、電源の配線抜けでリセットかかってしまうと完全にお手上げ状態になる。

となると、内蔵時計を合わせる方法は…

(a)RTCを搭載する

(b)GNSS信号から時刻を取得する

『登山』が大前提にあり、現在RaspberryPi/WHをメインに使用しているので、回路は簡単にしたい。揺れるし、雑に扱うし。ということで方式(b)GNSSで時刻を合わせることとしました。


GNSSで時刻合わせをするには date -sできないけど 

方法としては、gpspipeなどを使用するのが定石のようですが。。。


GPSDATE="`/usr/bin/gpspipe -w | /usr/bin/head -10 | /bin/grep TPV | /bin/sed -r 's/.*"time":"([^"]*)".*/\1/' | /usr/bin/head -1`"
echo $GPSDATE
/bin/date -s "$GPSDATE"
このgpspipeはgpsdと通信をして標準出力へ出力するというもの。
gpspipe is a tool to connect to gpsd and output the received sentences to stdout. 

GNSSロガーはもうできているので、今の所gpsdは入れたくないし。

GNSSロガーなのでGNSSのデータはもうすでに取得できている状況で、メッセージの解析もpythonを使用すればそれほど難しくはありません。
実際ログを見てみると

$GPGSV,4,2,14,20,52,198,18,21,00,321,,23,59,195,16,24,39,050,30*78
$GPGSV,4,3,14,25,47,170,,31,09,246,20,32,34,308,46,193,72,151,19*4C
$GPGSV,4,4,14,194,69,159,,195,05,171,*7D
$GLGSV,2,1,07,67,29,129,,68,75,069,,69,38,331,45,77,12,033,13*67
$GLGSV,2,2,07,78,65,025,28,79,56,222,21,80,02,217,*51
$GNGLL,3528.05140,N,13358.44323,E,055944.00,A,A*7B
$GNRMC,055945.00,A,3528.05143,N,13358.44320,E,0.202,,241120,,,A*67
$GNVTG,,T,,M,0.202,N,0.374,K,A*3D
$GNGGA,055945.00,3528.05143,N,13358.44320,E,1,10,1.08,72.7,M,30.4,M,,*72
$GNGSA,A,3,15,20,10,24,23,32,,,,,,,2.22,1.08,1.95*1B
$GNGSA,A,3,69,77,79,78,,,,,,,,,2.22,1.08,1.95*14
$GPGSV,4,1,14,10,64,272,21,12,57,100,,15,22,108,15,18,01,200,07*7A
$GPGSV,4,2,14,20,52,198,19,21,00,321,,23,59,195,15,24,39,050,30*7A
$GPGSV,4,3,14,25,47,170,,31,09,246,20,32,34,308,46,193,72,151,19*4C
$GPGSV,4,4,14,194,69,159,,195,05,171,*7D
$GLGSV,2,1,07,67,29,129,,68,75,069,,69,38,331,44,77,12,033,13*66
$GLGSV,2,2,07,78,65,025,27,79,56,222,22,80,02,217,*5D
$GNGLL,3528.05143,N,13358.44320,E,055945.00,A,A*7A
$GNRMC,055946.00,A,3528.05125,N,13358.44322,E,0.412,,241120,,,A*61
$GNVTG,,T,,M,0.412,N,0.763,K,A*38
$GNGGA,055946.00,3528.05125,N,13358.44322,E,1,10,1.08,72.7,M,30.4,M,,*73
$GNGSA,A,3,15,20,10,24,23,32,,,,,,,2.22,1.08,1.95*1B
$GNGSA,A,3,69,77,79,78,,,,,,,,,2.22,1.08,1.95*14
$GPGSV,4,1,14,10,64,272,20,12,57,100,,15,22,108,13,18,01,200,09*73
$GPGSV,4,2,14,20,52,198,19,21,00,321,,23,59,195,15,24,39,050,29*72
$GPGSV,4,3,14,25,47,170,,31,09,246,20,32,34,308,46,193,72,151,19*4C
$GPGSV,4,4,14,194,69,159,,195,05,171,*7D
$GLGSV,2,1,07,67,29,129,,68,75,069,,69,38,331,44,77,12,033,12*67
$GLGSV,2,2,07,78,65,025,26,79,56,222,22,80,02,217,*5C
$GNGLL,3528.05125,N,13358.44322,E,055946.00,A,A*7B
$GNRMC,055947.00,A,3528.05120,N,13358.44324,E,0.238,,241120,,,A*6D

ログを眺めると$RMCメッセージから時間が取得できそうです。

次に/bin/date -sで時刻合わせをする。まずは実験。
 

pi@pi126:~ $ date
2020年 11月 21日 土曜日 11:13:01 JST
pi@pi126:~ $ sudo date -s "2020/11/21 11:00"
2020年 11月 21日 土曜日 11:00:00 JST
よしよし

pi@pi126:~ $ date
2020年 11月 21日 土曜日 11:15:48 JST

あれ??
date -sで設定したはずの時刻がもとに戻っています。というか、設定し直されています。まぁ、いつも時刻があっているのでNTPで合わされているんだろうな。と、NTPを止めてみます。

root@pi126:~# systemctl stop ntp.service
Failed to stop ntp.service: Unit ntp.service not loaded.

えーーー!?ntpサービスで同期していないの?

もうちょっと調べたら、RaspberryPiOSというかLinuxではtimedatectl を使うとのこと。

root@pi126:~# timedatectl set-time '2020-11-21 12:07'
Failed to set time: Automatic time synchronization is enabled
NTPでの時刻を合わせの制御もtimedatectlでするらしい。ntp.serviceじゃないんだ。
NTPサービス止まってるし。ん?ま。。。後で調べるとして...。

timedatectl statusで状況を確認します。

root@pi126:~# timedatectl status
              Local time: 土 2020-11-21 12:11:48 JST
          Universal time: 土 2020-11-21 03:11:48 UTC
                RTC time: n/a
                Time zone: Asia/Tokyo (JST, +0900)
System clock synchronized: yes
              NTP service: active
          RTC in local TZ: no
NTPサービスここで有効になっているようです。
無効にします。

root@pi126:~# timedatectl set-ntp no
root@pi126:~# timedatectl status
              Local time: 土 2020-11-21 12:13:18 JST
          Universal time: 土 2020-11-21 03:13:18 UTC
                RTC time: n/a
                Time zone: Asia/Tokyo (JST, +0900)
System clock synchronized: yes
              NTP service: inactive
          RTC in local TZ: no
NTP service: inactiveで無効になりました。

root@pi126:~# timedatectl set-time '2020-11-21 12:07'
root@pi126:~# date
2020年 11月 21日 土曜日 12:07:10 JST

これでやっと(笑)時刻のセットができるようになりました。

ちなみに書式は

# timedatectl set-time "YYYY-MM-DD HH:MM:SS"
先の日付ならば

# timedatectl set-time "2020-11-21 12:07:10"
 となります。

参考:CentOSの時刻を手動で変更 @suzu6


GNSSのメッセージから時刻を取り出す ~python3を使って

以下、GNSSアンテナからシリアルで送信されたメッセージから時間を取り出して時刻設定するプログラムです。

使用環境:

RaspberryPi WH
RaspberryPiOS
python3
GU-902MGG-USB GNSSアンテナモジュール(秋月電子)
ZED-F9P USB接続 

 今回作成したプログラムです

[gnssdate.py] 

#!/usr/bin/python3
import os
import platform
import sys
import subprocess
import re
import time
from datetime import datetime, timedelta, timezone

import serial
import serial.serialutil as srlutil

def hms2deg(hhmm_mmmm):
    (hh,mm,pmmm) = re.split("(.*)(..)\.(.*)", hhmm_mmmm)[1:4]
    mm_mmmm = mm + "." + pmmm
    deg = float(hh) + float(mm_mmmm) / 60
    return deg

def checksum(line):
    # ex:
    # $GNGGA,004135.00,3634.2352,N,13825.76522,E,2,10,1.91,30.7,M,30.4,M,,0000*7C
    line = line.strip()
    code = 0
    if line[0] != "$":
        return False

    csrange = line[1:-3]
    csraw = line[-3:]       #*7C
    if csraw[0] != "*":
        return False

    csxor = int(csraw[1:], 16)
    for ch in csrange:
        code ^= ord(ch)

    if csxor != code:
        return False

    return True

def rmc_time(line_,tz_):
    if not checksum(line_):
        return None
    line_ = line_.strip()

    chunks = line_.split(",")
    if not re.search("\$..RMC",chunks[0]):
        return None
    print(line_)

    #if "A" not in chunks[2]:    # "A" is active data
    #    return None

    if len(chunks[9]) < 6:
        return None
    if len(chunks[1]) < 6:
        return None

    #print(chunks[9],end=" ")   #191120
    #print(chunks[1])           #090850.40
    (dymmy,day,month,year,dummy)= re.split("(..)(..)(..)",chunks[9])
    (dymmy,hour,minute,sec,msec10,dummy) = re.split("(..)(..)(..)\.(.*)",chunks[1])
    #(hour,min,sec) = re.split("(..)(..)(.*)",chunks[1])

    print("20%s/%s/%s %s:%s:%s.%s"%(year,month,day,hour,minute,sec,msec10))
    dt = datetime(2000 + int(year),int(month),int(day),
               int(hour),int(minute),int(sec),int(msec10)*10000) + \
            timedelta(hours=tz_)

    return dt

if __name__ == '__main__':
    _comport = "COM7"
    _baudrate = 115200
    _timezone = 9

    dtnow = datetime.now()
    print("gnssdate.py at ", end="")
    print(dtnow)

    comport = _comport
    baudrate = _baudrate
    timzn = _timezone
    args = sys.argv
    print(args)
    if len(args) < 3:
        print("gnssdate.py [SerialPort] [BaudRate] [timezone by numeric]")
        #exit(-1)
    else:
        comport = args[1]
        baudrate = int(args[2])
        if len(args) > 3:
            timzn = int(args[3])

    print("waiting boot up of GNSS/GPS unit....")

    if "Linux" in platform.system():
        subprocess.call(["/usr/bin/timedatectl", "set-ntp", "no"])

    time.sleep(10)

    while True:
        dtnow = datetime.now()
        print(dtnow)
        dtstart = dtnow
        portname = comport.split("/")[-1]
        #strdt = dtnow.strftime("_%Y%m%d%H%M.log")

        ser = serial.Serial(comport, baudrate=baudrate, timeout=2)
        line = ""
        while True:
            try:
                bline = ser.readline()
                if bline[0] != ord("$"):    # Charactor of top of line is not "$"
                    ser.reset_input_buffer()
                    continue

            except srlutil.SerialException as sx:
                print("GNSS/GPS Unit is unplugged from SerialPort...")
                print(sx)
                ser.close()
                sys.exit(1)
            else:
                try:
                    line = bline.decode('ascii')
                except UnicodeError as ux:
                    print(ux)
                    ser.close()
                    break

                print(line)
                dtrmc = rmc_time(line, timzn)
                dtnow = datetime.now()
                if dtrmc:
                    delay = dtrmc - dtnow
                    print("GNSS :", end="")
                    print(dtrmc)
                    print("Clock:", end="")
                    print(dtnow)
                    print(" Delay:", end="")
                    print(delay)
                    gnsstime = dtrmc.strftime("%Y-%m-%d %H:%M:%S")

                    if "Linux" in platform.system():
                        #subprocess.call(["/usr/bin/timedatectl", "set-ntp", "no"])
                        subprocess.call(["/usr/bin/timedatectl", "set-time", gnsstime])
                        # subprocess.call(["/usr/bin/timedatectl","set-ntp","yes"])
                        sys.exit(0)

                ser.reset_input_buffer()

 

呼び出し方の例

Linux:
#python3 gnssdate.py /dev/ttyUSB0 115200

Windows:
>python gnssdate.py COM7 115200

 
デフォルトの動作は日本時間ですが、タイムゾーンのオフセットも指定できます。

#python3 gnssdate.py <Serial Port> <baudrate> [TimeZone Offset in Hour]
 

GNSSからの時刻と日付を受信した時点で日時を確定しています。
Windowsで動作させた場合時刻を表示し続けます。
Linuxの場合は時刻をシステム時刻を変更します。
当然電波の入らないところでは確定しませんのでいつまでたっても動作が止まりません。

GNSSアンテナモジュールのボーレートは適宜設定してください。デフォルトでは9600bpsが多いのですが、ここでは、Python3 pyserialですべてのプラットフォームで対応している最高レート115200bpsを例として記載しております。

Windowsで動作させたときは、日時が確定してもそのまま動作し続けます。
Linux環境で動作させたときは、日時が確定するとシステムクロックの修正を行い終了します。
subprocess.call(["/usr/bin/timedatectl", "set-ntp", "no"])

プログラム中でNTPによる自動調整は止めています。通信環境がある通常運用に戻すときはNTPを有効化してください。

/usr/bin/timedatectl set-time で設定できる最小単位が秒なので精度は秒単位となります。

RaspberryPIOSで自動実行するにはrc.localの最終行に以下のように追加します。
[/etc/rc.local]

/usr/bin/python3 /var/samba/gnssdate.py /dev/ttyUSB0 115200

 
SSHでRaspberryPiに接続して動作確認すると0.3秒ぐらい遅れますが、実際にOS起動時にrc.local等で起動させて時刻合わせを試みると、他のタスクが忙しいためか3秒前後の遅れが発生します。

とりあえず日時がざっくり合えばいいと言う方はどうぞ。


GNSSロガーについてもまた後ほど。
NMEA0183対応のテキストバージョンはできたんだけど、UBloxのRAW出力には対応していないもので。