Skip to content

Commit 48b76bb

Browse files
committed
It works
1 parent 4646a78 commit 48b76bb

File tree

9 files changed

+418
-0
lines changed

9 files changed

+418
-0
lines changed

LICENSE

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Copyright 2023 Yoji Shidara
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4+
5+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6+
7+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# unagi: QZSS DC Report decoder for u-blox M8 receivers
2+
3+
4+
## Overview
5+
6+
This software receives [Satellite Report for Disaster and Crisis Management (DC Report; 災危通報)](https://qzss.go.jp/en/overview/services/sv08_dc-report.html) from [Quasi-Zenith Satellite System (QZSS)](https://qzss.go.jp/en/index.html) with u-blox M8 GPS receivers and converts it to NMEA format.
7+
8+
The code is tested with [Akizuki M-14541 GU-902MGG-USB receiver](https://akizukidenshi.com/catalog/g/gM-14541/), which is equipped with UBX-M8030-KT.
9+
10+
When started, it issues two commands to the receiver. The first is to enable L1S signal reception for QZSS (UBX-CFG-GNSS). The second is to dispatch RXM-SFRBX messages to UART1, which is connected to the PC (UBX-CFG-MSG). After that, it waits for the RXM-SFRBX messages to be received and output them in NMEA format.
11+
12+
13+
## Operation environments
14+
15+
unagi has been tested on the following environments:
16+
17+
* Ubuntu 22.04 LTS (amd64)
18+
* Raspberry Pi OS 64-bit bullseye (Raspberry Pi 4 Model B)
19+
20+
unagi is written in Go and should work on any environment that [Go](https://go.dev/) and [go-serial](https://github.com/bugst/go-serial) work.
21+
22+
23+
## Install
24+
25+
Run the following command:
26+
27+
```
28+
go install github.com/darashi/unagi@latest
29+
```
30+
31+
32+
## Usage
33+
34+
Connect to the receiver to the PC and run the following command:
35+
36+
```
37+
unagi
38+
```
39+
40+
If your receiver is connected to a device other than `/dev/ttyUSB0`, specify the device name with the `-device` option. Details of the options can be found by running `unagi -help`.
41+
42+
You will see the outputs like the following:
43+
44+
```
45+
$QZQSM,56,53ADD371878002B8EA60BA7100000000000000000000000000000012BFA3F94*73
46+
$QZQSM,56,9AADF3710F0002C3E8588ACB118162352C474588FCB1F4165DC00012E112C70*0C
47+
$QZQSM,56,C6ADF3710F0002CC1C5A008B4E2169FB2D400A1F5400000000000013151A7EC*07
48+
$QZQSM,56,53ADD371878002B8EA60BA7100000000000000000000000000000013587C3C8*7E
49+
```
50+
51+
The output is to be sent to the standard output. You can redirect it to a file or pipe it to another program. For example [azarashi](https://github.com/nbtk/azarashi) can be used to obtain human readable information from the output.
52+
53+
If nothing is output, check the environment in which the receiver is located. It needs to be in a location that is open to the sky above. It may be better to try it outdoors. If invoked with the `-all` flag, information other than DC reports will also be output. It may be useful for troubleshooting.
54+
55+
56+
## As a library
57+
58+
You can also use unagi as a library. See `main.go` for the details.
59+
60+
61+
## Other devices
62+
63+
With other devices, even if an M8 series chip is used, it may not work if the internal configuration is different (for example, the code assumes that the PC is connected to UART1 inside the device. If not, then you may be able to make it work with a few changes).
64+
65+
66+
## References
67+
68+
* https://content.u-blox.com/sites/default/files/products/documents/u-blox8-M8_ReceiverDescrProtSpec_UBX-13003221.pdf u-blox 8 / u-blox M8 Receiver description Including protocol specification.
69+
* https://qzss.go.jp/en/technical/download/pdf/ps-is-qzss/is-qzss-dcr-010.pdf Quasi-Zenith Satellite System Interface Specification DC Report Service (IS-QZSS-DCR-010).
70+
* https://eleclog.quitsq.com/2022/12/qzqsm-receiver.html This is an article about creating own receiver using M10, another series of chips from u-blox. The python code was helpful, though the details are different from M8.
71+
* https://twitter.com/Seg_Faul/status/1672963884637093890 My first attempt a few weeks ago didn't go well, but thanks to this tweet it did.
72+
* https://github.com/gpsnmeajp/ub2qzqsm Rust implementation by @Seg_Faul (the author of the above tweet). The code was not publicly available when I was writing unagi, but roughly speaking, unagi is doing almost the same thing.
73+
74+
75+
## License
76+
77+
MIT License

go.mod

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module github.com/darashi/unagi
2+
3+
go 1.20
4+
5+
require go.bug.st/serial v1.5.0
6+
7+
require (
8+
github.com/creack/goselect v0.1.2 // indirect
9+
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // indirect
10+
)

go.sum

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0=
2+
github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
3+
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
4+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
5+
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
6+
go.bug.st/serial v1.5.0 h1:ThuUkHpOEmCVXxGEfpoExjQCS2WBVV4ZcUKVYInM9T4=
7+
go.bug.st/serial v1.5.0/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE=
8+
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY=
9+
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
10+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=

main.go

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"log"
7+
8+
"github.com/darashi/unagi/ubloxm8"
9+
)
10+
11+
var (
12+
flagDevice = flag.String("device", "/dev/ttyUSB0", "serial port")
13+
flagBaudRate = flag.Int("baud-rate", 9600, "baud rate")
14+
flagAll = flag.Bool("all", false, "output all messages; not only $QZQSM")
15+
flagVerbose = flag.Bool("verbose", false, "verbose output")
16+
)
17+
18+
func main() {
19+
flag.Parse()
20+
21+
receiver, err := ubloxm8.NewReceiver(*flagDevice, *flagBaudRate)
22+
if err != nil {
23+
log.Fatal(err)
24+
}
25+
defer receiver.Close()
26+
27+
if err := receiver.EnableQZSSL1S(); err != nil {
28+
log.Fatal(err)
29+
}
30+
31+
if err := receiver.EnableRXMSFRBXOnURAT1(); err != nil {
32+
log.Fatal(err)
33+
}
34+
35+
handler := func(msg string) {
36+
fmt.Println(msg)
37+
}
38+
39+
if err := receiver.Receive(handler, *flagAll, *flagVerbose); err != nil {
40+
log.Fatal(err)
41+
}
42+
}

ubloxm8/nmea.go

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package ubloxm8
2+
3+
import "strings"
4+
5+
func nmeaChecksum(sentence string) byte {
6+
// NMEA 0183 - Wikipedia
7+
// https://en.wikipedia.org/wiki/NMEA_0183
8+
sentence = strings.TrimPrefix(sentence, "$")
9+
idx := strings.Index(sentence, "*")
10+
if idx >= 0 {
11+
sentence = sentence[:idx]
12+
}
13+
14+
ck := byte(0)
15+
for _, b := range []byte(sentence) {
16+
ck ^= b
17+
}
18+
return ck
19+
}

ubloxm8/qzqsm.go

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package ubloxm8
2+
3+
import (
4+
"encoding/hex"
5+
"fmt"
6+
"strings"
7+
)
8+
9+
// https://qzss.go.jp/en/technical/download/pdf/ps-is-qzss/is-qzss-dcr-010.pdf p.113
10+
var prn2satelliteID = map[byte]byte{
11+
184: 0x56, // QZS02
12+
185: 0x57, // QZS04
13+
189: 0x61, // QZS03
14+
183: 0x55, // QZS01
15+
186: 0x58, // QZS1R
16+
}
17+
18+
func ubx2qzqsm(ubx []byte) string {
19+
if len(ubx) < 14+8*4+2 {
20+
return ""
21+
}
22+
prn := ubx[7] + 182
23+
satelliteID := prn2satelliteID[prn]
24+
25+
buf := make([]byte, 32)
26+
// swap endian
27+
for i := 0; i < 8; i++ {
28+
for j := 0; j < 4; j++ {
29+
buf[i*4+j] = ubx[14+i*4+(3-j)]
30+
}
31+
}
32+
33+
messageType := buf[1] >> 2
34+
if messageType != 43 && messageType != 44 {
35+
return ""
36+
}
37+
buf[31] &= 0xc0
38+
buf = buf[:32]
39+
s := strings.ToUpper(hex.EncodeToString(buf))
40+
s = fmt.Sprintf("$QZQSM,%02X,%s", satelliteID, s[:len(s)-1])
41+
s += fmt.Sprintf("*%02X", nmeaChecksum(s))
42+
43+
return s
44+
}

ubloxm8/receiver.go

+173
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package ubloxm8
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"encoding/binary"
7+
"log"
8+
9+
"go.bug.st/serial"
10+
)
11+
12+
type Receiver struct {
13+
port serial.Port
14+
}
15+
16+
func NewReceiver(device string, baudrate int) (*Receiver, error) {
17+
mode := &serial.Mode{BaudRate: baudrate}
18+
port, err := serial.Open(device, mode)
19+
if err != nil {
20+
return nil, err
21+
}
22+
23+
return &Receiver{
24+
port: port,
25+
}, nil
26+
}
27+
28+
func (r *Receiver) Close() error {
29+
return r.port.Close()
30+
}
31+
32+
func (r *Receiver) SendUbxCommand(class, id byte, payload []byte) error {
33+
frame := ubxCommand(class, id, payload)
34+
_, err := r.port.Write(frame)
35+
return err
36+
}
37+
38+
func (r *Receiver) EnableQZSSL1S() error {
39+
return r.SendUbxCommand(0x06, 0x3e, // UBX-CFG-GNSS
40+
[]byte{
41+
0x00, // msgVer
42+
0x20, // numTrkChHw
43+
0x20, // numTrkChUse
44+
0x02, // numConfigBlocks = 1
45+
// config block
46+
0x00, // gnssId = 0 (GPS)
47+
0x08, // resTrkCh
48+
0x10, // maxTrkCh
49+
0x00, // reserved1
50+
0x01, 0x00, 0x01, 0x01, // flags
51+
// config block
52+
0x05, // gnssId = 5 (QZSS)
53+
0x00, // resTrkCh
54+
0x03, // maxTrkCh
55+
0x00, // reserved1
56+
0x01, 0x00, 0x05, 0x05, // flags; enable L1C/A and L1S (in sigCfgMask) and enable=1
57+
})
58+
}
59+
60+
func (r *Receiver) EnableRXMSFRBXOnURAT1() error {
61+
return r.SendUbxCommand(0x06, 0x01, // UBX-CFG-MSG; Set message rate(s)
62+
[]byte{
63+
0x02, 0x13, // RXM-SFRBX
64+
0x00, // DDC/I2C
65+
0x01, // UART 1: set 1
66+
0x00, // UART 2
67+
0x00, // USB
68+
0x00, // SPI
69+
})
70+
}
71+
72+
const (
73+
initial = iota
74+
nmea
75+
expect_lf
76+
ubx
77+
)
78+
79+
type MessageHandler func(string)
80+
81+
func (r *Receiver) Receive(onMessage MessageHandler, all bool, verbose bool) error {
82+
reader := bufio.NewReader(r.port)
83+
84+
nmeaBuf := ""
85+
ubxBuf := bytes.Buffer{}
86+
state := initial
87+
ubxBytesToRead := 0
88+
for {
89+
b, err := reader.ReadByte()
90+
if err != nil {
91+
return err
92+
}
93+
94+
switch state {
95+
case initial:
96+
bytes, err := reader.Peek(5) // SYNC CHAR 2 (0x62; 1 byte) + CLASS (1 byte) + ID(1 byte) + LENGTH (2 bytes)
97+
if err != nil {
98+
return err
99+
}
100+
101+
if b == '$' {
102+
state = nmea
103+
nmeaBuf = "$"
104+
} else if b == 0xb5 && bytes[0] == 0x62 {
105+
state = ubx
106+
payloadLength := int(binary.LittleEndian.Uint16(bytes[3:5]))
107+
ubxBytesToRead = 2 + 2 + 2 + payloadLength + 2
108+
// 1 byte SYNC CHAR 1 (0xB5)
109+
// 1 byte SYNC CHAR 2 (0x62)
110+
// 1 byte CLASS
111+
// 1 byte ID
112+
// 2 byte LENGTH
113+
// PAYLOAD
114+
// 1 byte CK_A
115+
// 1 byte CK_B
116+
ubxBuf.Reset()
117+
ubxBuf.WriteByte(b)
118+
ubxBytesToRead -= 1 // already read 1 byte (SYNC CHAR 1; 0xB5)
119+
}
120+
case nmea:
121+
if b == '\r' {
122+
state = expect_lf
123+
} else {
124+
nmeaBuf += string(b)
125+
}
126+
case expect_lf:
127+
if b == '\n' {
128+
state = initial
129+
} else {
130+
// unexpected
131+
log.Println("Warning: LF following CR is missing")
132+
state = initial
133+
}
134+
if all {
135+
onMessage(nmeaBuf)
136+
}
137+
case ubx:
138+
ubxBuf.WriteByte(b)
139+
ubxBytesToRead -= 1
140+
if ubxBytesToRead == 0 {
141+
state = initial
142+
143+
buf := ubxBuf.Bytes()
144+
if ubxBuf.Len() < 8 {
145+
log.Println("Warning: UBX message too short", buf)
146+
continue
147+
}
148+
// Calculate the UBX checksum from the sequence of CLASS, ID, LENGTH, PAYLOAD
149+
ck_a, ck_b := ubxChecksum(buf[2 : len(buf)-2])
150+
if ck_a != buf[len(buf)-2] || ck_b != buf[len(buf)-1] {
151+
log.Println("Warning: UBX checksum error", buf)
152+
continue
153+
}
154+
if buf[2] == 0x05 && buf[3] == 0x01 {
155+
if verbose {
156+
log.Println("UBX-ACK-ACK")
157+
}
158+
} else if buf[2] == 0x05 && buf[3] == 0x00 {
159+
if verbose {
160+
log.Println("UBX-ACK-NAK")
161+
}
162+
} else if buf[2] == 0x02 && buf[3] == 0x13 { // UBX-RXM-SFRBX
163+
if buf[6] == 0x05 { // QZSS
164+
qzqsm := ubx2qzqsm(buf)
165+
if qzqsm != "" {
166+
onMessage(qzqsm)
167+
}
168+
}
169+
}
170+
}
171+
}
172+
}
173+
}

0 commit comments

Comments
 (0)