Skip to content

Commit 0e2fb82

Browse files
committed
gps: improvements and corrections for config commands
This contains some improvements and corrections for the gps driver It adds some additional functions for different modes (automobile, bike, etc) and also ignores the return results from any config commands. Basically due to the fact that there is a constant stream of updates coming from NMEA messages, the results from sending any UBX commands are getting lost. Better to just ignore them for now. Signed-off-by: deadprogram <[email protected]>
1 parent 892265b commit 0e2fb82

File tree

6 files changed

+133
-38
lines changed

6 files changed

+133
-38
lines changed

examples/gps/uart/main.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ func main() {
5252
}
5353
println()
5454
} else {
55-
println("Waiting for fix...")
55+
if fix.Type == gps.GSV {
56+
// GSV sentence provides satellite count even if no fix yet
57+
println(fix.Satellites, "satellites visible")
58+
}
5659
}
5760
time.Sleep(200 * time.Millisecond)
5861
}

gps/gps.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ var (
1515
ErrInvalidNMEASentence = errors.New("invalid NMEA sentence format")
1616
ErrEmptyNMEASentence = errors.New("cannot parse empty NMEA sentence")
1717
ErrUnknownNMEASentence = errors.New("unsupported NMEA sentence type")
18+
errInvalidGSVSentence = errors.New("invalid GSV NMEA sentence")
1819
errInvalidGGASentence = errors.New("invalid GGA NMEA sentence")
1920
errInvalidRMCSentence = errors.New("invalid RMC NMEA sentence")
2021
errInvalidGLLSentence = errors.New("invalid GLL NMEA sentence")

gps/gpsparser.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,28 @@ import (
66
"time"
77
)
88

9+
type NMEASentenceType string
10+
11+
const (
12+
GSA NMEASentenceType = "GSA"
13+
GGA NMEASentenceType = "GGA"
14+
GLL NMEASentenceType = "GLL"
15+
GSV NMEASentenceType = "GSV"
16+
RMC NMEASentenceType = "RMC"
17+
VTG NMEASentenceType = "VTG"
18+
ZDA NMEASentenceType = "ZDA"
19+
TXT NMEASentenceType = "TXT"
20+
)
21+
922
// Parser for GPS NMEA sentences.
1023
type Parser struct {
1124
}
1225

1326
// Fix is a GPS location fix
1427
type Fix struct {
28+
// Type is the NMEA sentence type that provided this fix.
29+
Type NMEASentenceType
30+
1531
// Valid if the fix was valid.
1632
Valid bool
1733

@@ -53,13 +69,31 @@ func (parser *Parser) Parse(sentence string) (Fix, error) {
5369
}
5470
typ := sentence[3:6]
5571
switch typ {
72+
case "GSV":
73+
// https://docs.novatel.com/OEM7/Content/Logs/GPGSV.htm
74+
fields := strings.Split(sentence, ",")
75+
// GSV sentences have at least 4 fields, but typically 8, 12, 16, or 20 depending on satellites in view
76+
if len(fields) < 4 {
77+
return fix, errInvalidGSVSentence
78+
}
79+
80+
fix.Type = GSV
81+
82+
// Number of satellites in view is always field 3
83+
fix.Satellites = findSatellites(fields[3])
84+
85+
// GSV does not provide position, time, or fix validity
86+
fix.Valid = false
87+
88+
return fix, nil
5689
case "GGA":
5790
// https://docs.novatel.com/OEM7/Content/Logs/GPGGA.htm
5891
fields := strings.Split(sentence, ",")
5992
if len(fields) != 15 {
6093
return fix, errInvalidGGASentence
6194
}
6295

96+
fix.Type = GGA
6397
fix.Time = findTime(fields[1])
6498
fix.Latitude = findLatitude(fields[2], fields[3])
6599
fix.Longitude = findLongitude(fields[4], fields[5])
@@ -75,6 +109,7 @@ func (parser *Parser) Parse(sentence string) (Fix, error) {
75109
return fix, errInvalidGLLSentence
76110
}
77111

112+
fix.Type = GLL
78113
fix.Latitude = findLatitude(fields[1], fields[2])
79114
fix.Longitude = findLongitude(fields[3], fields[4])
80115
fix.Time = findTime(fields[5])
@@ -89,6 +124,7 @@ func (parser *Parser) Parse(sentence string) (Fix, error) {
89124
return fix, errInvalidRMCSentence
90125
}
91126

127+
fix.Type = RMC
92128
fix.Time = findTime(fields[1])
93129
fix.Valid = (fields[2] == "A")
94130
fix.Latitude = findLatitude(fields[3], fields[4])

gps/gpsparser_test.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,29 @@ import (
1010
func TestParseUnknownSentence(t *testing.T) {
1111
p := NewParser()
1212

13-
val := "$GPGSV,3,1,09,07,14,317,22,08,31,284,25,10,32,133,39,16,85,232,29*7F"
13+
val := "$GPVTG,89.68,T,,M,0.00,N,0.0,K*5F"
1414
_, err := p.Parse(val)
1515
if err == nil {
1616
t.Error("should have unknown sentence err")
1717
}
1818
}
1919

20+
func TestParseGSV(t *testing.T) {
21+
c := qt.New(t)
22+
23+
p := NewParser()
24+
25+
val := "$GPGSV,3,1,09,07,14,317,22,08,31,284,25,10,32,133,39,16,85,232,29*7F"
26+
fix, err := p.Parse(val)
27+
if err != nil {
28+
t.Error("should have parsed")
29+
}
30+
31+
c.Assert(fix.Type, qt.Equals, GSV)
32+
c.Assert(fix.Satellites, qt.Equals, int16(9))
33+
c.Assert(fix.Valid, qt.Equals, false)
34+
}
35+
2036
func TestParseGGA(t *testing.T) {
2137
c := qt.New(t)
2238

gps/ublox.go

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ import (
44
"time"
55
)
66

7-
// FlightModeCmd is a UBX-CFG-NAV5 command to set the GPS into
8-
// flight mode (airborne <1g)
9-
var flightModeCmd = CfgNav5{
7+
// FlightModeCmd is a UBX-CFG-NAV5 command
8+
var nav5Cmd = CfgNav5{
109
Mask: CfgNav5Dyn | CfgNav5MinEl | CfgNav5PosFixMode,
1110
DynModel: DynModeAirborne1g, // Airborne with <1g acceleration
1211
FixMode: FixModeAuto, // Auto 2D/3D
@@ -29,7 +28,37 @@ var flightModeCmd = CfgNav5{
2928

3029
// SetFlightMode sends UBX-CFG-NAV5 command to set GPS into flight mode
3130
func (d *Device) SetFlightMode() (err error) {
32-
flightModeCmd.Put42Bytes(d.buffer[:])
31+
nav5Cmd.DynModel = DynModeAirborne1g
32+
nav5Cmd.FixMode = FixModeAuto
33+
nav5Cmd.Put42Bytes(d.buffer[:])
34+
35+
return d.SendCommand(d.buffer[:42])
36+
}
37+
38+
// SetPedestrianMode sends UBX-CFG-NAV5 command to set GPS into pedestrian mode
39+
func (d *Device) SetPedestrianMode() (err error) {
40+
nav5Cmd.DynModel = DynModePedestrian
41+
nav5Cmd.FixMode = FixModeAuto
42+
nav5Cmd.Put42Bytes(d.buffer[:])
43+
44+
return d.SendCommand(d.buffer[:42])
45+
}
46+
47+
// SetAutomotiveMode sends UBX-CFG-NAV5 command to set GPS into automotive mode
48+
func (d *Device) SetAutomotiveMode() (err error) {
49+
nav5Cmd.DynModel = DynModeAutomotive
50+
nav5Cmd.FixMode = FixModeAuto
51+
nav5Cmd.Put42Bytes(d.buffer[:])
52+
53+
return d.SendCommand(d.buffer[:42])
54+
}
55+
56+
// SetBikeMode sends UBX-CFG-NAV5 command to set GPS into bike mode
57+
func (d *Device) SetBikeMode() (err error) {
58+
nav5Cmd.DynModel = DynModeBike
59+
nav5Cmd.FixMode = FixModeAuto
60+
nav5Cmd.Put42Bytes(d.buffer[:])
61+
3362
return d.SendCommand(d.buffer[:42])
3463
}
3564

@@ -44,19 +73,19 @@ var (
4473
messageRateGLLCmd = CfgMsg1{
4574
MsgClass: 0xF0,
4675
MsgID: 0x01,
47-
Rate: 0, // Disabled
76+
Rate: 1, // Every position fix
4877
}
4978
// GSA (satellite id list)
5079
messageRateGSACmd = CfgMsg1{
5180
MsgClass: 0xF0,
5281
MsgID: 0x02,
53-
Rate: 1, // Every position fix
82+
Rate: 0, // Disabled
5483
}
5584
// GSV (satellite locations)
5685
messageRateGSVCmd = CfgMsg1{
5786
MsgClass: 0xF0,
5887
MsgID: 0x03,
59-
Rate: 1, // Every position fix
88+
Rate: 0, // Every position fix
6089
}
6190
// RMC (time, lat/lng, speed, course)
6291
messageRateRMCCmd = CfgMsg1{
@@ -84,18 +113,19 @@ var (
84113
}
85114
)
86115

87-
// SetMessageRatesMinimal configures the GPS to output a minimal set of NMEA sentences
116+
// SetMessageRatesMinimal configures the GPS to output a minimal set of NMEA sentences:
117+
// GSV, GGA, GLL, and RMC only.
88118
func SetMessageRatesMinimal(d *Device) (err error) {
89119
commands := []CfgMsg1{
90120
messageRateGSACmd,
91-
messageRateGGACmd,
92121
messageRateGLLCmd,
93-
messageRateGSVCmd,
94-
messageRateRMCCmd,
95122
messageRateVTGCmd,
96123
messageRateZDACmd,
97124
messageRateTXTCmd,
98125
}
126+
for i := range commands {
127+
commands[i].Rate = 0 // Disable
128+
}
99129
return setCfg1s(d, commands)
100130
}
101131

@@ -111,18 +141,26 @@ func SetMessageRatesAllEnabled(d *Device) (err error) {
111141
messageRateZDACmd,
112142
messageRateTXTCmd,
113143
}
144+
for i := range commands {
145+
commands[i].Rate = 1 // Enable
146+
}
114147
return setCfg1s(d, commands)
115148
}
116149

117150
func setCfg1s(d *Device, commands []CfgMsg1) (err error) {
118151
var buf [9]byte
119152
for _, cmd := range commands {
120-
cmd.Put9Bytes(buf[:9])
121-
if err = d.SendCommand(buf[:9]); err != nil {
122-
return err
123-
}
153+
cmd.Put9Bytes(buf[:])
154+
// TODO handle errors differently here?
155+
// This implementation just saves the last error and continues.
156+
// Due to the GPS modules sending updates asynchronously
157+
// the response is interleaved along with regular ASCII
158+
// NMEA messages.
159+
err = d.SendCommand(buf[:])
160+
time.Sleep(100 * time.Millisecond)
124161
}
125-
return nil
162+
163+
return
126164
}
127165

128166
// gnssDisableCmd is a UBX-CFG-GNSS command to disable all GNSS but GPS
@@ -146,6 +184,7 @@ func (d *Device) SetGNSSDisable() (err error) {
146184
if err != nil {
147185
return err
148186
}
187+
149188
return d.SendCommand(d.buffer[:])
150189
}
151190

gps/ublox_test.go

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -65,23 +65,23 @@ func TestAppendChecksumPreservesOriginal(t *testing.T) {
6565
}
6666
}
6767

68-
func TestFlightModeCmdConfig(t *testing.T) {
69-
// Verify FlightModeCmd has expected values
70-
if flightModeCmd.DynModel != 6 {
71-
t.Errorf("expected DynModel 6 (airborne <1g), got %d", flightModeCmd.DynModel)
68+
func TestNav5CmdConfig(t *testing.T) {
69+
// Verify nav5Cmd has expected values
70+
if nav5Cmd.DynModel != 6 {
71+
t.Errorf("expected DynModel 6 (airborne <1g), got %d", nav5Cmd.DynModel)
7272
}
7373

74-
if flightModeCmd.FixMode != 3 {
75-
t.Errorf("expected FixMode 3 (auto 2D/3D), got %d", flightModeCmd.FixMode)
74+
if nav5Cmd.FixMode != 3 {
75+
t.Errorf("expected FixMode 3 (auto 2D/3D), got %d", nav5Cmd.FixMode)
7676
}
7777

7878
expectedMask := CfgNav5Dyn | CfgNav5MinEl | CfgNav5PosFixMode
79-
if flightModeCmd.Mask != expectedMask {
80-
t.Errorf("expected Mask 0x%04X, got 0x%04X", expectedMask, flightModeCmd.Mask)
79+
if nav5Cmd.Mask != expectedMask {
80+
t.Errorf("expected Mask 0x%04X, got 0x%04X", expectedMask, nav5Cmd.Mask)
8181
}
8282

83-
if flightModeCmd.MinElev_deg != 5 {
84-
t.Errorf("expected MinElev_deg 5, got %d", flightModeCmd.MinElev_deg)
83+
if nav5Cmd.MinElev_deg != 5 {
84+
t.Errorf("expected MinElev_deg 5, got %d", nav5Cmd.MinElev_deg)
8585
}
8686
}
8787

@@ -118,9 +118,9 @@ func TestGNSSDisableCmdConfig(t *testing.T) {
118118
}
119119
}
120120

121-
func TestFlightModeCmdWrite(t *testing.T) {
121+
func TestNav5CmdWrite(t *testing.T) {
122122
buf := make([]byte, 64)
123-
flightModeCmd.Put42Bytes(buf)
123+
nav5Cmd.Put42Bytes(buf)
124124

125125
// Verify sync chars
126126
if buf[0] != 0xB5 || buf[1] != 0x62 {
@@ -197,9 +197,9 @@ func TestMessageRateCmdConfigs(t *testing.T) {
197197
rate byte
198198
}{
199199
{"GGA", messageRateGGACmd, 0xF0, 0x00, 1},
200-
{"GLL", messageRateGLLCmd, 0xF0, 0x01, 0},
201-
{"GSA", messageRateGSACmd, 0xF0, 0x02, 1},
202-
{"GSV", messageRateGSVCmd, 0xF0, 0x03, 1},
200+
{"GLL", messageRateGLLCmd, 0xF0, 0x01, 1},
201+
{"GSA", messageRateGSACmd, 0xF0, 0x02, 0},
202+
{"GSV", messageRateGSVCmd, 0xF0, 0x03, 0},
203203
{"RMC", messageRateRMCCmd, 0xF0, 0x04, 1},
204204
{"VTG", messageRateVTGCmd, 0xF0, 0x05, 0},
205205
{"ZDA", messageRateZDACmd, 0xF0, 0x08, 0},
@@ -269,9 +269,9 @@ func TestMinimalMessageRatesConfig(t *testing.T) {
269269
// GGA and RMC should be enabled (rate=1), others disabled (rate=0)
270270
expectedRates := map[byte]byte{
271271
0x00: 1, // GGA - enabled
272-
0x01: 0, // GLL - disabled
273-
0x02: 1, // GSA - enabled
274-
0x03: 1, // GSV - enabled
272+
0x01: 1, // GLL - enabled
273+
0x02: 0, // GSA - disabled
274+
0x03: 0, // GSV - disabled
275275
0x04: 1, // RMC - enabled
276276
0x05: 0, // VTG - disabled
277277
0x08: 0, // ZDA - disabled
@@ -337,9 +337,9 @@ func TestAllMessageRatesWriteCorrectBytes(t *testing.T) {
337337

338338
func TestSetMessageRatesAllEnabledModifiesRate(t *testing.T) {
339339
// Verify that when we copy a command and set Rate=1, it works correctly
340-
cmd := messageRateGLLCmd // This one is disabled by default
340+
cmd := messageRateGSACmd // This one is disabled by default
341341
if cmd.Rate != 0 {
342-
t.Errorf("expected GLL default rate 0, got %d", cmd.Rate)
342+
t.Errorf("expected GSA default rate 0, got %d", cmd.Rate)
343343
}
344344

345345
// Simulate what SetMessageRatesAllEnabled does

0 commit comments

Comments
 (0)