gps: improvements and bugfixes (#186)

* gps: buxfixes and refactoring of API to separate device from parser

Signed-off-by: deadprogram <ron@hybridgroup.com>

* gps: simplify time parser

Signed-off-by: deadprogram <ron@hybridgroup.com>

* gps: add support for RMC sentences

Signed-off-by: deadprogram <ron@hybridgroup.com>

* gps: small renaming to remove reduntant use of word GPS

Signed-off-by: deadprogram <ron@hybridgroup.com>
This commit is contained in:
Ron Evans
2020-08-20 11:22:07 +02:00
committed by GitHub
parent 88aeec9f69
commit 5df96c8138
6 changed files with 174 additions and 107 deletions
+19 -6
View File
@@ -1,8 +1,8 @@
package main package main
import ( import (
"fmt"
"machine" "machine"
"time"
"tinygo.org/x/drivers/gps" "tinygo.org/x/drivers/gps"
) )
@@ -11,19 +11,32 @@ func main() {
println("GPS I2C Example") println("GPS I2C Example")
machine.I2C0.Configure(machine.I2CConfig{}) machine.I2C0.Configure(machine.I2CConfig{})
ublox := gps.NewI2C(&machine.I2C0) ublox := gps.NewI2C(&machine.I2C0)
parser := gps.Parser(ublox) parser := gps.NewParser()
var fix gps.Fix var fix gps.Fix
for { for {
fix = parser.NextFix() s, err := ublox.NextSentence()
if err != nil {
println(err)
continue
}
fix, err = parser.Parse(s)
if err != nil {
println(err)
continue
}
if fix.Valid { if fix.Valid {
print(fix.Time.Format("15:04:05")) print(fix.Time.Format("15:04:05"))
print(", lat=", fmt.Sprintf("%f", fix.Latitude)) print(", lat=")
print(", long=", fmt.Sprintf("%f", fix.Longitude)) print(fix.Latitude)
print(", altitude:=", fix.Altitude) print(", long=")
print(fix.Longitude)
print(", altitude=", fix.Altitude)
print(", satellites=", fix.Satellites) print(", satellites=", fix.Satellites)
println() println()
} else { } else {
println("No fix") println("No fix")
} }
time.Sleep(200 * time.Millisecond)
} }
} }
+19 -6
View File
@@ -1,8 +1,8 @@
package main package main
import ( import (
"fmt"
"machine" "machine"
"time"
"tinygo.org/x/drivers/gps" "tinygo.org/x/drivers/gps"
) )
@@ -11,19 +11,32 @@ func main() {
println("GPS UART Example") println("GPS UART Example")
machine.UART1.Configure(machine.UARTConfig{BaudRate: 9600}) machine.UART1.Configure(machine.UARTConfig{BaudRate: 9600})
ublox := gps.NewUART(&machine.UART1) ublox := gps.NewUART(&machine.UART1)
parser := gps.Parser(ublox) parser := gps.NewParser()
var fix gps.Fix var fix gps.Fix
for { for {
fix = parser.NextFix() s, err := ublox.NextSentence()
if err != nil {
println(err)
continue
}
fix, err = parser.Parse(s)
if err != nil {
println(err)
continue
}
if fix.Valid { if fix.Valid {
print(fix.Time.Format("15:04:05")) print(fix.Time.Format("15:04:05"))
print(", lat=", fmt.Sprintf("%f", fix.Latitude)) print(", lat=")
print(", long=", fmt.Sprintf("%f", fix.Longitude)) print(fix.Latitude)
print(", altitude:=", fix.Altitude) print(", long=")
print(fix.Longitude)
print(", altitude=", fix.Altitude)
print(", satellites=", fix.Satellites) print(", satellites=", fix.Satellites)
println() println()
} else { } else {
println("No fix") println("No fix")
} }
time.Sleep(200 * time.Millisecond)
} }
} }
+30 -20
View File
@@ -3,13 +3,19 @@ package gps // import "tinygo.org/x/drivers/gps"
import ( import (
"encoding/hex" "encoding/hex"
"errors"
"machine" "machine"
"strings" "strings"
"time" "time"
) )
var (
errInvalidNMEASentenceLength = errors.New("invalid NMEA sentence length")
errInvalidNMEAChecksum = errors.New("invalid NMEA sentence checksum")
)
// Device wraps a connection to a GPS device. // Device wraps a connection to a GPS device.
type GPSDevice struct { type Device struct {
buffer []byte buffer []byte
bufIdx int bufIdx int
sentence strings.Builder sentence strings.Builder
@@ -19,8 +25,8 @@ type GPSDevice struct {
} }
// NewUART creates a new UART GPS connection. The UART must already be configured. // NewUART creates a new UART GPS connection. The UART must already be configured.
func NewUART(uart *machine.UART) GPSDevice { func NewUART(uart *machine.UART) Device {
return GPSDevice{ return Device{
uart: uart, uart: uart,
buffer: make([]byte, bufferSize), buffer: make([]byte, bufferSize),
bufIdx: bufferSize, bufIdx: bufferSize,
@@ -29,8 +35,8 @@ func NewUART(uart *machine.UART) GPSDevice {
} }
// NewI2C creates a new I2C GPS connection. // NewI2C creates a new I2C GPS connection.
func NewI2C(bus *machine.I2C) GPSDevice { func NewI2C(bus *machine.I2C) Device {
return GPSDevice{ return Device{
bus: bus, bus: bus,
address: I2C_ADDRESS, address: I2C_ADDRESS,
buffer: make([]byte, bufferSize), buffer: make([]byte, bufferSize),
@@ -39,17 +45,17 @@ func NewI2C(bus *machine.I2C) GPSDevice {
} }
} }
// ReadNextSentence returns the next valid NMEA sentence from the GPS device. // NextSentence returns the next valid NMEA sentence from the GPS device.
func (gps *GPSDevice) NextSentence() (sentence string) { func (gps *Device) NextSentence() (sentence string, err error) {
sentence = gps.readNextSentence() sentence = gps.readNextSentence()
for !validSentence(sentence) { if err = validSentence(sentence); err != nil {
sentence = gps.readNextSentence() return "", err
} }
return sentence return sentence, nil
} }
// readNextSentence returns the next sentence from the GPS device. // readNextSentence returns the next sentence from the GPS device.
func (gps *GPSDevice) readNextSentence() (sentence string) { func (gps *Device) readNextSentence() (sentence string) {
gps.sentence.Reset() gps.sentence.Reset()
var b byte = ' ' var b byte = ' '
@@ -69,7 +75,7 @@ func (gps *GPSDevice) readNextSentence() (sentence string) {
return sentence return sentence
} }
func (gps *GPSDevice) readNextByte() (b byte) { func (gps *Device) readNextByte() (b byte) {
gps.bufIdx += 1 gps.bufIdx += 1
if gps.bufIdx >= bufferSize { if gps.bufIdx >= bufferSize {
gps.fillBuffer() gps.fillBuffer()
@@ -77,7 +83,7 @@ func (gps *GPSDevice) readNextByte() (b byte) {
return gps.buffer[gps.bufIdx] return gps.buffer[gps.bufIdx]
} }
func (gps *GPSDevice) fillBuffer() { func (gps *Device) fillBuffer() {
if gps.uart != nil { if gps.uart != nil {
gps.uartFillBuffer() gps.uartFillBuffer()
} else { } else {
@@ -85,7 +91,7 @@ func (gps *GPSDevice) fillBuffer() {
} }
} }
func (gps *GPSDevice) uartFillBuffer() { func (gps *Device) uartFillBuffer() {
for gps.uart.Buffered() < bufferSize { for gps.uart.Buffered() < bufferSize {
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
} }
@@ -93,7 +99,7 @@ func (gps *GPSDevice) uartFillBuffer() {
gps.bufIdx = 0 gps.bufIdx = 0
} }
func (gps *GPSDevice) i2cFillBuffer() { func (gps *Device) i2cFillBuffer() {
for gps.available() < bufferSize { for gps.available() < bufferSize {
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
} }
@@ -102,7 +108,7 @@ func (gps *GPSDevice) i2cFillBuffer() {
} }
// Available returns how many bytes of GPS data are currently available. // Available returns how many bytes of GPS data are currently available.
func (gps *GPSDevice) available() (available int) { func (gps *Device) available() (available int) {
var lengthBytes [2]byte var lengthBytes [2]byte
gps.bus.Tx(gps.address, []byte{BYTES_AVAIL_REG}, lengthBytes[0:2]) gps.bus.Tx(gps.address, []byte{BYTES_AVAIL_REG}, lengthBytes[0:2])
available = int(lengthBytes[0])*256 + int(lengthBytes[1]) available = int(lengthBytes[0])*256 + int(lengthBytes[1])
@@ -110,7 +116,7 @@ func (gps *GPSDevice) available() (available int) {
} }
// WriteBytes sends data/commands to the GPS device // WriteBytes sends data/commands to the GPS device
func (gps *GPSDevice) WriteBytes(bytes []byte) { func (gps *Device) WriteBytes(bytes []byte) {
if gps.uart != nil { if gps.uart != nil {
gps.uart.Write(bytes) gps.uart.Write(bytes)
} else { } else {
@@ -119,14 +125,18 @@ func (gps *GPSDevice) WriteBytes(bytes []byte) {
} }
// validSentence checks if a sentence has been received uncorrupted // validSentence checks if a sentence has been received uncorrupted
func validSentence(sentence string) bool { func validSentence(sentence string) error {
if len(sentence) < 4 || sentence[0] != '$' || sentence[len(sentence)-3] != '*' { if len(sentence) < 4 || sentence[0] != '$' || sentence[len(sentence)-3] != '*' {
return false return errInvalidNMEASentenceLength
} }
var cs byte = 0 var cs byte = 0
for i := 1; i < len(sentence)-3; i++ { for i := 1; i < len(sentence)-3; i++ {
cs ^= sentence[i] cs ^= sentence[i]
} }
checksum := hex.EncodeToString([]byte{cs}) checksum := hex.EncodeToString([]byte{cs})
return (checksum[0] == sentence[len(sentence)-2]) && (checksum[1] == sentence[len(sentence)-1]) if (checksum[0] != sentence[len(sentence)-2]) || (checksum[1] != sentence[len(sentence)-1]) {
return errInvalidNMEAChecksum
}
return nil
} }
+94 -63
View File
@@ -1,92 +1,123 @@
package gps package gps
import ( import (
"errors"
"strconv" "strconv"
"strings" "strings"
"time" "time"
) )
type GPSParser struct { var (
gpsDevice GPSDevice errEmptyNMEASentence = errors.New("cannot parse empty NMEA sentence")
errUnknownNMEASentence = errors.New("unsupported NMEA sentence type")
errInvalidGGASentence = errors.New("invalid GGA NMEA sentence")
errInvalidRMCSentence = errors.New("invalid RMC NMEA sentence")
)
// Parser for GPS NMEA sentences.
type Parser struct {
} }
// fix is a GPS location fix // Fix is a GPS location fix
type Fix struct { type Fix struct {
Valid bool // Valid if the fix was valid.
Time time.Time Valid bool
Latitude float32
Longitude float32 // Time that the fix was taken, in UTC time.
Altitude int32 Time time.Time
// Latitude is the decimal latitude. Negative numbers indicate S.
Latitude float32
// Longitude is the decimal longitude. Negative numbers indicate E.
Longitude float32
// Altitude is only returned for GGA sentences.
Altitude int32
// Satellites is the number of visible satellites, but is only returned for GGA sentences.
Satellites int16 Satellites int16
} }
func Parser(gpsDevice GPSDevice) GPSParser { // NewParser returns a GPS NMEA Parser.
return GPSParser{ func NewParser() Parser {
gpsDevice: gpsDevice, return Parser{}
}
// Parse parses a NMEA sentence looking for fix info.
func (parser *Parser) Parse(sentence string) (fix Fix, err error) {
if sentence == "" {
err = errEmptyNMEASentence
return
} }
} typ := sentence[3:6]
switch typ {
// NextFix returns the next GPS location Fix from the GPS device case "GGA":
func (parser *GPSParser) NextFix() (fix Fix) { fields := strings.Split(sentence, ",")
var ggaSentence = nextGGA(parser.gpsDevice) if len(fields) != 15 {
var ggaFields = strings.Split(ggaSentence, ",") err = errInvalidGGASentence
fix.Altitude = findAltitude(ggaFields) return
fix.Satellites = findSatellites(ggaFields)
fix.Longitude = findLongitude(ggaFields)
fix.Latitude = findLatitude(ggaFields)
fix.Time = findTime(ggaFields)
fix.Valid = (fix.Altitude != -99999) && (fix.Satellites > 0)
return fix
}
// nextGGA returns the next GGA type sentence from the GPS device
// $--GGA,,,,,,,,,,,,,,*hh
func nextGGA(gpsDevice GPSDevice) (sentence string) {
for {
sentence = gpsDevice.NextSentence()
if sentence[3:6] == "GGA" {
return sentence
} }
fix.Altitude = findAltitude(fields[9])
fix.Satellites = findSatellites(fields[7])
fix.Longitude = findLongitude(fields[4], fields[5])
fix.Latitude = findLatitude(fields[2], fields[3])
fix.Time = findTime(fields[1])
fix.Valid = (fix.Altitude != -99999) && (fix.Satellites > 0)
case "RMC":
fields := strings.Split(sentence, ",")
if len(fields) != 13 {
err = errInvalidRMCSentence
return
}
fix.Longitude = findLongitude(fields[5], fields[6])
fix.Latitude = findLatitude(fields[3], fields[4])
fix.Time = findTime(fields[1])
fix.Valid = (len(fields[2]) > 0 && fields[2][0:1] == "A")
default:
err = errUnknownNMEASentence
} }
return
} }
// findTime returns the time from a GGA sentence: // findTime returns the time from an NMEA sentence:
// $--GGA,hhmmss.ss,,,,,,,,,,,,,*xx // $--GGA,hhmmss.ss,,,,,,,,,,,,,*xx
func findTime(ggaFields []string) time.Time { func findTime(val string) time.Time {
if len(ggaFields) < 1 || len(ggaFields[1]) < 6 { if len(val) < 6 {
return time.Time{} return time.Time{}
} }
ts := strings.Builder{}
ts.WriteString(ggaFields[1][0:2]) h, _ := strconv.ParseInt(val[0:2], 10, 8)
ts.WriteString(":") m, _ := strconv.ParseInt(val[2:4], 10, 8)
ts.WriteString(ggaFields[1][2:4]) s, _ := strconv.ParseInt(val[4:6], 10, 8)
ts.WriteString(":") ms, _ := strconv.ParseInt(val[7:10], 10, 16)
ts.WriteString(ggaFields[1][4:6]) t := time.Date(0, 0, 0, int(h), int(m), int(s), int(ms), time.UTC)
var t, _ = time.Parse("15:04:05", ts.String())
return t return t
} }
// findAltitude returns the altitude from a GGA sentence: // findAltitude returns the altitude from an NMEA sentence:
// $--GGA,,,,,,,,,25.8,,,,,*63 // $--GGA,,,,,,,,,25.8,,,,,*63
func findAltitude(ggaFields []string) int32 { func findAltitude(val string) int32 {
if len(ggaFields) > 8 && len(ggaFields[9]) > 0 { if len(val) > 0 {
var v, _ = strconv.ParseFloat(ggaFields[9], 32) var v, _ = strconv.ParseFloat(val, 32)
return int32(v) return int32(v)
} }
return -99999 return -99999
} }
// findLatitude returns the Latitude from a GGA sentence: // findLatitude returns the Latitude from an NMEA sentence:
// $--GGA,,ddmm.mmmmm,x,,,,,,,,,,,*hh // $--GGA,,ddmm.mmmmm,x,,,,,,,,,,,*hh
func findLatitude(ggaFields []string) float32 { func findLatitude(val, hemi string) float32 {
if len(ggaFields) > 2 && len(ggaFields[2]) > 8 { if len(val) > 8 {
var dd = ggaFields[2][0:2] var dd = val[0:2]
var mm = ggaFields[2][2:] var mm = val[2:]
var d, _ = strconv.ParseFloat(dd, 32) var d, _ = strconv.ParseFloat(dd, 32)
var m, _ = strconv.ParseFloat(mm, 32) var m, _ = strconv.ParseFloat(mm, 32)
var v = float32(d + (m / 60)) var v = float32(d + (m / 60))
if ggaFields[3] == "S" { if hemi == "S" {
v *= -1 v *= -1
} }
return v return v
@@ -94,16 +125,16 @@ func findLatitude(ggaFields []string) float32 {
return 0.0 return 0.0
} }
// findLatitude returns the longitude from a GGA sentence: // findLatitude returns the longitude from an NMEA sentence:
// $--GGA,,,,dddmm.mmmmm,x,,,,,,,,,*hh // $--GGA,,,,dddmm.mmmmm,x,,,,,,,,,*hh
func findLongitude(ggaFields []string) float32 { func findLongitude(val, hemi string) float32 {
if len(ggaFields) > 4 && len(ggaFields[4]) > 8 { if len(val) > 8 {
var ddd = ggaFields[4][0:3] var ddd = val[0:3]
var mm = ggaFields[4][3:] var mm = val[3:]
var d, _ = strconv.ParseFloat(ddd, 32) var d, _ = strconv.ParseFloat(ddd, 32)
var m, _ = strconv.ParseFloat(mm, 32) var m, _ = strconv.ParseFloat(mm, 32)
var v = float32(d + (m / 60)) var v = float32(d + (m / 60))
if ggaFields[5] == "W" { if hemi == "W" {
v *= -1 v *= -1
} }
return v return v
@@ -111,11 +142,11 @@ func findLongitude(ggaFields []string) float32 {
return 0.0 return 0.0
} }
// findSatellites returns the satellites from a GGA sentence: // findSatellites returns the satellites from an NMEA sentence:
// $--GGA,,,,,,,nn,,,,,,,*hh // $--GGA,,,,,,,nn,,,,,,,*hh
func findSatellites(ggaFields []string) (n int16) { func findSatellites(val string) (n int16) {
if len(ggaFields) > 6 && len(ggaFields[7]) > 0 { if len(val) > 0 {
var nn = ggaFields[7] var nn = val
var v, _ = strconv.ParseInt(nn, 10, 32) var v, _ = strconv.ParseInt(nn, 10, 32)
n = int16(v) n = int16(v)
return n return n
+1 -1
View File
@@ -13,5 +13,5 @@ const (
) )
const ( const (
bufferSize = 32 bufferSize = 100
) )
+11 -11
View File
@@ -24,25 +24,25 @@ var cfg_gnss_cmd = [...]byte{
0x01, 0x01, 0x06, 0x08, 0x0E, 0x00, 0x00, 0x00, 0x01, 0x01, 0x06, 0x08, 0x0E, 0x00, 0x00, 0x00,
0x01, 0x01, 0xFC, 0x11} 0x01, 0x01, 0xFC, 0x11}
func FlightMode(gpsDevice GPSDevice) (err error) { func FlightMode(d Device) (err error) {
err = sendCommand(gpsDevice, flight_mode_cmd[:]) err = sendCommand(d, flight_mode_cmd[:])
return err return err
} }
func SetCfgGNSS(gpsDevice GPSDevice) (err error) { func SetCfgGNSS(d Device) (err error) {
err = sendCommand(gpsDevice, cfg_gnss_cmd[:]) err = sendCommand(d, cfg_gnss_cmd[:])
return err return err
} }
func sendCommand(gpsDevice GPSDevice, command []byte) (err error) { func sendCommand(d Device, command []byte) (err error) {
gpsDevice.WriteBytes(command) d.WriteBytes(command)
start := time.Now() start := time.Now()
for time.Now().Sub(start) < 1000 { for time.Now().Sub(start) < 1000 {
if gpsDevice.readNextByte() == '\n' { if d.readNextByte() == '\n' {
if gpsDevice.readNextByte() == 0xB5 { if d.readNextByte() == 0xB5 {
gpsDevice.readNextByte() d.readNextByte()
if gpsDevice.readNextByte() == 0x05 { if d.readNextByte() == 0x05 {
if gpsDevice.readNextByte() == 0x01 { if d.readNextByte() == 0x01 {
return return
} }
} }