メインコンテンツまでスキップ

災危通報の解析

メッセージフォーマット

災危通報のメッセージは、0xB5 0x62 から始まるUBXプロトコルのバイナリ形式で出力され、必ず以下の形で始まります。

b5 -> UBX Preamble sync character 1
62 -> UBX Preamble sync character 2
02 -> Message Class (RBX)
13 -> Message ID (SFRBX)
2c 00 -> Payload Length (Little Endian, 44 bytes)
05 -> GNSS ID (5=QZSS)
ヒント

NMEAセンテンスは \r\n の改行コードで終わりますが、UBXプロトコルのバイナリ形式メッセージはヘッダの Payload Length を読んでメッセージの終端を得る必要があります。

上記のヘッダに続き、メッセージごとのデータおよびチェックサムが入っています。

07 -> Satellite ID (7+182=189=QZS03)
01 -> Signal ID (1=L1S)
00 -> Frequency ID (Only used for GLONASS)
09 -> The number of data words contained in this message (8+9*4=44)
45 -> Tracking channel number
02 -> Message version (0x02)
00 -> Reserved 0
55f5ad9a170580110000008e00000000000000000000000010000000b1aa5aebff9483b2 -> 災危通報データ
e2 -> ck_a (Checksum)
cd -> ck_b (Checksum)

UBXプロトコルに関する詳細については、以下のu-blox社のドキュメントを参照してください。

M10 firmware 5.10 interface description (PDF, 2.28MiB)

ヒント

災危通報メッセージは衛星航法データの出力 UBX-RXM-SFRBX を有効にすると出力されます。
GR-M10-C/Sでは、出荷時に有効にする設定をBBR(Battery Backup RAM)に書き込んでいるため設定作業は不要です。
内蔵バックアップ電池の取り外し、消耗による交換をした場合は再設定が必要です。

備考

出荷時にバックアップ電池を取り付けていないGR-M10-B/S-B45、GR-M10-RPでは、初期状態で衛星航法データの出力 UBX-RXM-SFRBX が無効になっています。
以下のサンプルコードでは、すべてのGR-M10シリーズ受信機に対応させるために UBX-RXM-SFRBX を有効にするコマンドを実行時の最初に送信しています。

センテンスへの変換

UBXプロトコルのバイナリ形式で出力される災危通報メッセージを ユーザインタフェース仕様書(災害・危機管理通報サービス, IS-QZSS-DCR-010)の 4.3.1. Sentence format で示されている $QZQSM から始まるNMEA形式のセンテンスに変換します。

Python3

以下はPython3で災危通報のセンテンスを出力するサンプルコードです。

read.py
import sys
import argparse
import operator
from functools import reduce
import serial
import time

VAL_SET_RAM_UBX_RXM_SFRBX_UART1_ON = bytes([0xB5, 0x62, 0x06, 0x8A, 0x09, 0x00, 0x01, 0x01, 0x00, 0x00, 0x32, 0x02, 0x91, 0x20, 0x01, 0x81, 0x30])

satellite_id = {
184: '56',
185: '57',
189: '61',
183: '55',
186: '58',
}

def nmea_checksum(sentence):
data = sentence.strip("$").split('*', 1)[0]
cksum = reduce(operator.xor, (ord(s) for s in data), 0)
return cksum

def ubx_checksum(message):
ck_a = 0
ck_b = 0
i = 0
while i < len(message):
ck_a = (ck_a + message[i]) & 0xff
ck_b = (ck_b + ck_a) & 0xff
i += 1
return ck_a, ck_b

def ubx2qzqsm(line):
if line[:7] == b'\xB5\x62\x02\x13\x2C\x00\x05': # UBX-RXM-SFRBX, 44 bytes, QZSS
satId = satellite_id[line[7] + 182] # PRN -> Satellite ID
data = b''
for i in range(9):
data += bytes((line[14+3+i*4], line[14+2+i*4], line[14+1+i*4], line[14+0+i*4]))
if data[1] >> 2 == 43 or data[1] >> 2 == 44: # Message Type 43=JMA-DC Report, 44=Other
dcr_message = (data[:31] + bytes((data[31] & 0xC0,))).hex()[:-1] # 256-4=252 bit
sentence = '$QZQSM,' + satId + ',' + dcr_message + '*'
return sentence + format(nmea_checksum(sentence), 'x')


if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Print QZQSM NMEA format sentence')
parser.add_argument('port', help='serial port. ex: /dev/ttyUSB0')
parser.add_argument('baudrate', help='baudrate. ex: 115200')
parser.add_argument('-n', '--nmea', help='print other standard NMEA sentence', action='store_true')
args = parser.parse_args()

with serial.Serial(args.port, args.baudrate) as ser:
print('initializing...')
ser.write(VAL_SET_RAM_UBX_RXM_SFRBX_UART1_ON) # UBX-RXM-SFRBX Output ON
time.sleep(1)
print('start!')

while True:
line = b''
nmea_flag = False
ubx_flag = False
count = 0
payload_length = 0
while True:
if ubx_flag:
if count > 4 and payload_length == 0:
payload_length = int.from_bytes(line[4:5], "little")
if payload_length > 0 and count == payload_length + 8: # header 6 bytes + checksum 2 bytes
break
b = ser.read()
if b == b'$' and not ubx_flag:
nmea_flag = True
if b == b'\x62' and line == b'\xB5':
ubx_flag = True
if b == b'\n':
if line.endswith(b'\r'):
line += b
break
else:
line += b
else:
line += b
count += 1

if args.nmea and nmea_flag:
sentence = line.decode().strip('\r\n')
ck = nmea_checksum(sentence)
if format(ck, 'x') == sentence.split('*', 1)[1]:
print(sentence)

if ubx_flag:
ck_a, ck_b = ubx_checksum(line[2:payload_length+6])
if line[-2] == ck_a and line[-1] == ck_b:
sentence = ubx2qzqsm(line)
if sentence:
print(sentence)

実行

PySerialをインストールしていない場合はpipでインストールしてください。

pip install pyserial

以下のように、シリアルポートとボーレートを指定すると $QZQSM から始まるセンテンスを出力します。

python read.py /dev/tty.usbserial1410 115200
$QZQSM,57,9aadf5b1118002c3f2587f8b101962082c41a588acb1181623500012b979380*20
ヒント

GR-M10-B/S-B45、GR-M10-RPでは初期状態でボーレートが 9600bps または 38400bps (2024年6月以降出荷分) になっているため、以下のように実行してください。

python read.py /dev/tty.usbserial1410 9600

または

python read.py /dev/tty.usbserial1410 38400

115200bpsで受信する場合は、設定が必要です。

-n オプションでNMEA形式の他のセンテンスも出力します。

詳しくは、 -h オプションで表示されるヘルプを参照してください。

usage: read.py [-h] [-n] port baudrate

Print QZQSM NMEA format sentence

positional arguments:
port serial port. ex: /dev/ttyUSB0
baudrate baudrate. ex: 115200

options:
-h, --help show this help message and exit
-n, --nmea print other standard NMEA sentence

Golang

以下はGo言語で災危通報のセンテンスを出力するサンプルコードです。

read.go
package main

import (
"io"
"flag"
"fmt"
"strings"
"bytes"
"log"
"time"
"github.com/tarm/serial"
)

var satelliteID = map[byte]string{
183: "55", // QZS01
184: "56", // QZS02
185: "57", // QZS04
186: "58", // QZS1R
189: "61", // QZS03
}

// VAL_SET_RAM_UBX_RXM_SFRBX_UART1_ON
var valSetRamUbxRxmsfrbxUart1On = []byte{0xB5, 0x62, 0x06, 0x8A, 0x09, 0x00, 0x01, 0x01, 0x00, 0x00, 0x32, 0x02, 0x91, 0x20, 0x01, 0x81, 0x30}

func calculateNmeaChecksum(sentence string) string {
data := sentence[1:]
var cksum uint8
for i := 0; i < len(data); i++ {
cksum ^= data[i]
}
return fmt.Sprintf("%02X", cksum)
}

func calculateUbxChecksum(message []byte) []byte {
var ck_a byte
var ck_b byte
for i:= 0; i < len(message); i++ {
ck_a = (ck_a + message[i]) & 0xff
ck_b = (ck_b + ck_a) & 0xff
}
return []byte{ck_a, ck_b}
}

func main() {
port := flag.String("port", "/dev/ttyUSB0", "serial port. ex: /dev/ttyUSB0")
baud := flag.Int("baud", 9600, "baudrate. ex: 9600")
nmea := flag.Bool("n", false, "print other standard NMEA sentence")
flag.Parse()

config := &serial.Config{Name: *port, Baud: *baud}
s, err := serial.OpenPort(config)
if err != nil {
panic(err)
}
defer s.Close()

_, err = s.Write(valSetRamUbxRxmsfrbxUart1On)
if err != nil {
log.Fatalf("s.Write: %v", err)
}
fmt.Println("Sent VAL_SET_RAM_UBX_RXM_SFRBX_UART1_ON")

time.Sleep(100 * time.Millisecond)

ubxHeader := []byte{0xB5, 0x62}
buf := make([]byte, 1024)
index := 0
messageLength := 0
readingMessage := false

for {
b := make([]byte, 1)
_, err := s.Read(b)
if err != nil {
if err == io.EOF {
continue
} else {
panic(err)
}
}

if !readingMessage && index == 0 && b[0] == ubxHeader[0] {
readingMessage = true
buf[index] = b[0]
index++
continue
} else if readingMessage && index == 1 && b[0] == ubxHeader[1] {
buf[index] = b[0]
index++
continue
}

if readingMessage {
buf[index] = b[0]
index++

if index == 6 {
messageLength = int(buf[4]) | int(buf[5])<<8
}

if index == messageLength+8 {
readingMessage = false
message := buf[:index]

ck := calculateUbxChecksum(message[2 : messageLength+6])
if message[len(message)-2] == ck[0] && message[len(message)-1] == ck[1] {
// UBX-RXM-SFRBX, 44 bytes, QZSS
if bytes.Equal(message[2:7], []byte{0x02, 0x13, 0x2C, 0x00, 0x05}) {
// PRN -> Satellite ID
satID := satelliteID[message[7]+182]
data := make([]byte, 0, 36)
for i := 0; i < 9; i++ {
data = append(data, message[14+3+i*4], message[14+2+i*4], message[14+1+i*4], message[14+0+i*4])
}
// Message Type 43=JMA-DC Report, 44=Other Organization
if data[1]>>2 == 43 || data[1]>>2 == 44 {
dcrMessage := fmt.Sprintf("%X%02X", data[:31], data[31]&0xC0)
sentence := fmt.Sprintf("$QZQSM,%s,%s", satID, dcrMessage[:len(dcrMessage)-1])
cksum := calculateNmeaChecksum(sentence)
fmt.Printf("%s*%s\r\n", sentence, cksum)
}
}
}
index = 0
messageLength = 0
}
} else {
if b[0] == '$' && *nmea {
nmeaSentence := string(b)
for {
_, err := s.Read(b)
if err != nil {
if err == io.EOF {
continue
} else {
panic(err)
}
}
nmeaSentence += string(b)
if b[0] == '\n' {
break
}
}
ckIndex := strings.Index(nmeaSentence, "*")
if ckIndex != -1 {
ck := calculateNmeaChecksum(nmeaSentence[:ckIndex])
if ck == nmeaSentence[ckIndex+1:ckIndex+3] {
fmt.Printf("%s", nmeaSentence)
}
}
}
}
}
}

実行

必要なライブラリをインストールしてください。

go mod init read 
go get github.com/tarm/serial

以下のように、シリアルポートとボーレートを指定すると $QZQSM から始まるセンテンスを出力します。

go run read.go -port /dev/tty.usbserial1410 -baud 115200

または、コンパイルして実行してください。

go build
./read -port /dev/tty.usbserial1410 -baud 115200
ヒント

GR-M10-B/S-B45、GR-M10-RPでは初期状態でボーレートが 9600bps または 38400bps (2024年6月以降出荷分) になっているため、以下のように実行してください。

go run read.go -port /dev/tty.usbserial1410 -baud 9600

または

go run read.go -port /dev/tty.usbserial1410 -baud 38400

115200bpsで受信する場合は、設定が必要です。

-n オプションでNMEA形式の他のセンテンスも出力します。

詳しくは、 -h オプションで表示されるヘルプを参照してください。

Usage of ./read:
-baud int
baudrate. ex: 9600 (default 9600)
-n print other standard NMEA sentence
-port string
serial port. ex: /dev/ttyUSB0 (default "/dev/ttyUSB0")

センテンスの解析

以下のライブラリを使用することで、センテンスを文字情報に変換することができます。

nbtk/azarashi: QZSS DCR Decoder

インストール

pip install azarashi

使用方法

sentenceに上記サンプルコードで得た $QZQSM から始まるセンテンスを入れてください。

import azarashi

sentence = '$QZQSM,57,9aadf5b1118002c3f2587f8b101962082c41a588acb1181623500012b979380*20'
report = azarashi.decode(sentence, msg_type='spresense')
print(report)
防災気象情報(海上)(発表)(通常)
海上警報が発表されました。

発表時刻: 11月12日17時35分

警報等情報要素: 海上濃霧警報
サハリン東方海上

警報等情報要素: 海上濃霧警報
サハリン西方海上

警報等情報要素: 海上濃霧警報
網走沖

警報等情報要素: 海上濃霧警報
宗谷海峡

警報等情報要素: 海上濃霧警報
北海道西方海上

警報等情報要素: 海上濃霧警報
北海道東方海上

警報等情報要素: 海上濃霧警報
釧路沖

警報等情報要素: 海上濃霧警報
日高沖

過去の災危通報データ

過去の配信データは以下に保存しています。ライブラリや自作プログラムの出力テストにご利用ください。

みちびき災危通報 過去配信データ

みちびき災危通報 試験用データ 2022年9月6日