diff --git a/gps/gps.go b/gps/gps.go index a53a446..86bfd6a 100644 --- a/gps/gps.go +++ b/gps/gps.go @@ -42,6 +42,12 @@ func (ge GPSError) Unwrap() error { return ge.Err } +const ( + minimumNMEALength = 7 + startingDelimiter = '$' + checksumDelimiter = '*' +) + // Device wraps a connection to a GPS device. type Device struct { buffer []byte @@ -87,11 +93,11 @@ func (gps *Device) readNextSentence() (sentence string) { gps.sentence.Reset() var b byte = ' ' - for b != '$' { + for b != startingDelimiter { b = gps.readNextByte() } - for b != '*' { + for b != checksumDelimiter { gps.sentence.WriteByte(b) b = gps.readNextByte() } @@ -153,8 +159,13 @@ func (gps *Device) WriteBytes(bytes []byte) { } // validSentence checks if a sentence has been received uncorrupted +// For example, a valid NMEA sentence such as this: +// $GPGLL,3751.65,S,14507.36,E*77 +// It has to start with a '$' character. +// It has to have a 5 character long sentence identifier. +// It has to end with a '*' character following by a checksum. func validSentence(sentence string) error { - if len(sentence) < 7 || sentence[0] != '$' || sentence[len(sentence)-3] != '*' { + if len(sentence) < minimumNMEALength || sentence[0] != startingDelimiter || sentence[len(sentence)-3] != checksumDelimiter { return errInvalidNMEASentenceLength } var cs byte = 0 diff --git a/gps/gpsparser.go b/gps/gpsparser.go index d0232f6..6cd76d5 100644 --- a/gps/gpsparser.go +++ b/gps/gpsparser.go @@ -60,11 +60,11 @@ func (parser *Parser) Parse(sentence string) (Fix, error) { return fix, errInvalidGGASentence } - 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.Latitude = findLatitude(fields[2], fields[3]) + fix.Longitude = findLongitude(fields[4], fields[5]) + fix.Satellites = findSatellites(fields[7]) + fix.Altitude = findAltitude(fields[9]) fix.Valid = (fix.Altitude != -99999) && (fix.Satellites > 0) return fix, nil @@ -75,11 +75,11 @@ func (parser *Parser) Parse(sentence string) (Fix, error) { return fix, errInvalidGLLSentence } - fix.Latitude = findLatitude(fields[2], fields[3]) - fix.Longitude = findLongitude(fields[4], fields[5]) - fix.Time = findTime(fields[6]) + fix.Latitude = findLatitude(fields[1], fields[2]) + fix.Longitude = findLongitude(fields[3], fields[4]) + fix.Time = findTime(fields[5]) - fix.Valid = (fields[7] == "A") + fix.Valid = (fields[6] == "A") return fix, nil case "RMC": @@ -89,12 +89,12 @@ func (parser *Parser) Parse(sentence string) (Fix, error) { return fix, errInvalidRMCSentence } - fix.Longitude = findLongitude(fields[5], fields[6]) - fix.Latitude = findLatitude(fields[3], fields[4]) fix.Time = findTime(fields[1]) + fix.Valid = (fields[2] == "A") + fix.Latitude = findLatitude(fields[3], fields[4]) + fix.Longitude = findLongitude(fields[5], fields[6]) fix.Speed = findSpeed(fields[7]) fix.Heading = findHeading(fields[8]) - fix.Valid = (len(fields[2]) > 0 && fields[2] == "A") return fix, nil } @@ -113,8 +113,8 @@ func findTime(val string) time.Time { m, _ := strconv.ParseInt(val[2:4], 10, 8) s, _ := strconv.ParseInt(val[4:6], 10, 8) ms := int64(0) - if len(val) == 10 { - ms, _ = strconv.ParseInt(val[7:10], 10, 16) + if len(val) > 6 { + ms, _ = strconv.ParseInt(val[7:], 10, 16) } t := time.Date(0, 0, 0, int(h), int(m), int(s), int(ms), time.UTC) @@ -135,11 +135,11 @@ func findAltitude(val string) int32 { // $--GGA,,ddmm.mmmmm,x,,,,,,,,,,,*hh func findLatitude(val, hemi string) float32 { if len(val) > 8 { - var dd = val[0:2] - var mm = val[2:] - var d, _ = strconv.ParseFloat(dd, 32) - var m, _ = strconv.ParseFloat(mm, 32) - var v = float32(d + (m / 60)) + dd := val[0:2] + mm := val[2:] + d, _ := strconv.ParseFloat(dd, 32) + m, _ := strconv.ParseFloat(mm, 32) + v := float32(d + (m / 60)) if hemi == "S" { v *= -1 } @@ -148,7 +148,7 @@ func findLatitude(val, hemi string) float32 { return 0.0 } -// findLatitude returns the longitude from an NMEA sentence: +// findLongitude returns the longitude from an NMEA sentence: // $--GGA,,,,dddmm.mmmmm,x,,,,,,,,,*hh func findLongitude(val, hemi string) float32 { if len(val) > 8 { diff --git a/gps/gpsparser_test.go b/gps/gpsparser_test.go new file mode 100644 index 0000000..73f4591 --- /dev/null +++ b/gps/gpsparser_test.go @@ -0,0 +1,97 @@ +package gps + +import ( + "testing" + "time" + + qt "github.com/frankban/quicktest" +) + +func TestParseUnknownSentence(t *testing.T) { + c := qt.New(t) + + p := NewParser() + + val := "$GPGSV,3,1,09,07,14,317,22,08,31,284,25,10,32,133,39,16,85,232,29*7F" + _, err := p.Parse(val) + c.Assert(err.Error(), qt.Contains, "unsupported NMEA sentence type") +} + +func TestParseGGA(t *testing.T) { + c := qt.New(t) + + p := NewParser() + + val := "$GPGGA,115739.00,4158.8441367,N,09147.4416929," + fix, err := p.Parse(val) + if err != errInvalidGGASentence { + t.Error("should have errInvalidGGASentence error") + } + + val = "$GPGGA,115739.00,4158.8441367,N,09147.4416929,W,4,13,0.9,255.747,M,-32.00,M,01,0000*6E" + fix, err = p.Parse(val) + if err != nil { + t.Error("should have parsed") + } + c.Assert(fix.Latitude, qt.Equals, float32(41.980735778808594)) + c.Assert(fix.Longitude, qt.Equals, float32(-91.79069519042969)) + c.Assert(fix.Altitude, qt.Equals, int32(255)) +} + +func TestParseGLL(t *testing.T) { + c := qt.New(t) + + p := NewParser() + + val := "$GPGLL,3953.88008971,N,10506.7531891" + _, err := p.Parse(val) + if err != errInvalidGLLSentence { + t.Error("should have errInvalidGLLSentence error") + } + + val = "$GPGLL,5109.0262317,N,11401.8407304,W,202725.00,A,D*79" + fix, err := p.Parse(val) + if err != nil { + t.Error("should have parsed") + } + + c.Assert(fix.Latitude, qt.Equals, float32(51.15043640136719)) + c.Assert(fix.Longitude, qt.Equals, float32(-114.03067779541016)) +} + +func TestParseRMC(t *testing.T) { + c := qt.New(t) + + p := NewParser() + + val := "$GPRMC,203522.00,A,5109.0262308,N,11401.8407342," + _, err := p.Parse(val) + if err != errInvalidRMCSentence { + t.Error("should have errInvalidRMCSentence error") + } + + val = "$GPRMC,203522.00,A,5109.0262308,N,11401.8407342,W,0.004,133.4,130522,0.0,E,D*2B" + fix, err := p.Parse(val) + if err != nil { + t.Error("should have parsed") + } + + c.Assert(fix.Latitude, qt.Equals, float32(51.15043640136719)) + c.Assert(fix.Longitude, qt.Equals, float32(-114.03067779541016)) +} + +func TestTime(t *testing.T) { + c := qt.New(t) + + val := "" + tm := findTime(val) + c.Assert(tm, qt.Equals, time.Time{}) + + val = "225446" + tm = findTime(val) + c.Assert(tm, qt.Equals, time.Date(0, 0, 0, 22, 54, 46, 0, time.UTC)) + + val = "124326.02752" + tm = findTime(val) + c.Assert(tm, qt.Equals, time.Date(0, 0, 0, 12, 43, 26, 2752, time.UTC)) +}