Skip to content

Commit

Permalink
Merge branch 'master' of github.com:klyve/go-nmea
Browse files Browse the repository at this point in the history
* 'master' of github.com:klyve/go-nmea:
  Fix checksum ref after branch update
  Correct copy paste typo
  Add VHW water speed and heading
  functions returning a direction sign have been added
  added latitude range checking
  added estimated fix which is returned by MTK3339
  add mtk test
  expose xorChecksum
  revert xorChecksum expose
  use the parser
  att support for parsing MTK commands and update go mod files
  • Loading branch information
klyve committed Feb 22, 2020
2 parents 75d6121 + 7ae4adf commit 1270711
Show file tree
Hide file tree
Showing 14 changed files with 272 additions and 10 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ After this command *go-nmea* is ready to use. Its source will be in:
At this moment, this library supports the following sentence types:

- [RMC](http://aprs.gids.nl/nmea/#rmc) - Recommended Minimum Specific GPS/Transit data
- [PMTK](https://www.rhydolabz.com/documents/25/PMTK_A11.pdf) - Messages for setting and reading commands for MediaTek gps modules.
- [GGA](http://aprs.gids.nl/nmea/#gga) - GPS Positioning System Fix Data
- [GSA](http://aprs.gids.nl/nmea/#gsa) - GPS DOP and active satellites
- [GSV](http://aprs.gids.nl/nmea/#gsv) - GPS Satellites in view
Expand All @@ -30,6 +31,7 @@ At this moment, this library supports the following sentence types:
- [VDM/VDO](http://catb.org/gpsd/AIVDM.html) - Encapsulated binary payload
- [WPL](http://aprs.gids.nl/nmea/#wpl) - Waypoint location
- [RTE](http://aprs.gids.nl/nmea/#rte) - Route
- [VHW](https://www.tronico.fi/OH6NT/docs/NMEA0183.pdf) - Water Speed and Heading

## Example

Expand Down
4 changes: 3 additions & 1 deletion gga.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const (
RTK = "4"
// FRTK float RTK fix
FRTK = "5"
// EST estimated fix.
EST = "6"
)

// GGA is the Time, position, and fix related data of the receiver.
Expand All @@ -41,7 +43,7 @@ func newGGA(s BaseSentence) (GGA, error) {
Time: p.Time(0, "time"),
Latitude: p.LatLong(1, 2, "latitude"),
Longitude: p.LatLong(3, 4, "longitude"),
FixQuality: p.EnumString(5, "fix quality", Invalid, GPS, DGPS, PPS, RTK, FRTK),
FixQuality: p.EnumString(5, "fix quality", Invalid, GPS, DGPS, PPS, RTK, FRTK, EST),
NumSatellites: p.Int64(6, "number of satellites"),
HDOP: p.Float64(7, "hdop"),
Altitude: p.Float64(8, "altitude"),
Expand Down
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
module github.com/klyve/go-nmea

require github.com/stretchr/testify v1.2.1
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/testify v1.2.1
)
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.2.1 h1:52QO5WkIUcHGIR7EnGagH88x1bUzqGXTC5/1bDTUQ7U=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
25 changes: 25 additions & 0 deletions mtk.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package nmea

const (
// TypeMTK type for PMTK sentences
TypeMTK = "PMTK"
)

// MTK is the Time, position, and fix related data of the receiver.
type MTK struct {
BaseSentence
Cmd,
Flag int64
}

// newMTK constructor
func newMTK(s BaseSentence) (MTK, error) {
p := newParser(s)
cmd := p.Int64(0, "command")
flag := p.Int64(1, "flag")
return MTK{
BaseSentence: s,
Cmd: cmd,
Flag: flag,
}, p.Err()
}
50 changes: 50 additions & 0 deletions mtk_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package nmea

import (
"testing"

"github.com/stretchr/testify/assert"
)

var mtktests = []struct {
name string
raw string
err string
msg MTK
}{
{
name: "good: Packet Type: 001 PMTK_ACK",
raw: "$PMTK001,604,3*" + Checksum("PMTK001,604,3"),
msg: MTK{
Cmd: 604,
Flag: 3,
},
},
{
name: "missing flag",
raw: "$PMTK001,604*" + Checksum("PMTK001,604"),
err: "nmea: PMTK001 invalid flag: index out of range",
},
{
name: "missing cmd",
raw: "$PMTK001*" + Checksum("PMTK001"),
err: "nmea: PMTK001 invalid command: index out of range",
},
}

func TestMTK(t *testing.T) {
for _, tt := range mtktests {
t.Run(tt.name, func(t *testing.T) {
m, err := Parse(tt.raw)
if tt.err != "" {
assert.Error(t, err)
assert.EqualError(t, err, tt.err)
} else {
assert.NoError(t, err)
mtk := m.(MTK)
mtk.BaseSentence = BaseSentence{}
assert.Equal(t, tt.msg, mtk)
}
})
}
}
9 changes: 9 additions & 0 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,15 @@ func (p *parser) LatLong(i, j int, context string) float64 {
if err != nil {
p.SetErr(context, err.Error())
}

if (b == North || b == South) && (v < -90.0 || 90.0 < v) {
p.SetErr(context, "latitude is not in range (-90, 90)")
return 0
} else if (b == West || b == East) && (v < -180.0 || 180.0 < v) {
p.SetErr(context, "longitude is not in range (-180, 180)")
return 0
}

return v
}

Expand Down
36 changes: 36 additions & 0 deletions parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,42 @@ var parsertests = []struct {
return p.Date(0, "context")
},
},
{
name: "LatLong",
fields: []string{"5000.0000", "N"},
expected: 50.0,
parse: func(p *parser) interface{} {
return p.LatLong(0, 1, "context")
},
},
{
name: "LatLong - latitude out of range",
fields: []string{"9100.0000", "N"},
expected: 0.0,
hasErr: true,
parse: func(p *parser) interface{} {
return p.LatLong(0, 1, "context")
},
},
{
name: "LatLong - longitude out of range",
fields: []string{"18100.0000", "W"},
expected: 0.0,
hasErr: true,
parse: func(p *parser) interface{} {
return p.LatLong(0, 1, "context")
},
},
{
name: "LatLong with existing error",
fields: []string{"5000.0000", "W"},
expected: 0.0,
hasErr: true,
parse: func(p *parser) interface{} {
p.SetErr("context", "value")
return p.LatLong(0, 1, "context")
},
},
}

func TestParser(t *testing.T) {
Expand Down
17 changes: 14 additions & 3 deletions sentence.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func parseSentence(raw string) (BaseSentence, error) {
fieldsRaw = raw[startIndex+1 : sumSepIndex]
fields = strings.Split(fieldsRaw, FieldSep)
checksumRaw = strings.ToUpper(raw[sumSepIndex+1:])
checksum = xorChecksum(fieldsRaw)
checksum = Checksum(fieldsRaw)
)
// Validate the checksum
if checksum != checksumRaw {
Expand All @@ -96,6 +96,9 @@ func parseSentence(raw string) (BaseSentence, error) {

// parsePrefix takes the first field and splits it into a talker id and data type.
func parsePrefix(s string) (string, string) {
if strings.HasPrefix(s, "PMTK") {
return "PMTK", s[4:]
}
if strings.HasPrefix(s, "P") {
return "P", s[1:]
}
Expand All @@ -105,9 +108,9 @@ func parsePrefix(s string) (string, string) {
return s[:2], s[2:]
}

// xor all the bytes in a string an return it
// Checksum xor all the bytes in a string an return it
// as an uppercase hex string
func xorChecksum(s string) string {
func Checksum(s string) string {
var checksum uint8
for i := 0; i < len(s); i++ {
checksum ^= s[i]
Expand All @@ -122,6 +125,12 @@ func Parse(raw string) (Sentence, error) {
return nil, err
}
if strings.HasPrefix(s.Raw, SentenceStart) {
// MTK message types share the same format
// so we return the same struct for all types.
switch s.Talker {
case TypeMTK:
return newMTK(s)
}
switch s.Type {
case TypeRMC:
return newRMC(s)
Expand Down Expand Up @@ -149,6 +158,8 @@ func Parse(raw string) (Sentence, error) {
return newWPL(s)
case TypeRTE:
return newRTE(s)
case TypeVHW:
return newVHW(s)
}
}
if strings.HasPrefix(s.Raw, SentenceStartEncapsulated) {
Expand Down
2 changes: 1 addition & 1 deletion tagblock.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ func parseTagBlock(raw string) (TagBlock, string, error) {
var (
fieldsRaw = tags[0:sumSepIndex]
checksumRaw = strings.ToUpper(tags[sumSepIndex+1:])
checksum = xorChecksum(fieldsRaw)
checksum = Checksum(fieldsRaw)
err error
)

Expand Down
19 changes: 16 additions & 3 deletions types.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,6 @@ func ParseLatLong(s string) (float64, error) {
} else {
return 0, fmt.Errorf("cannot parse [%s], unknown format", s)
}
if l < -180.0 || 180.0 < l {
return 0, errors.New("coordinate is not in range -180, 180")
}
return l, nil
}

Expand Down Expand Up @@ -246,3 +243,19 @@ func ParseDate(ddmmyy string) (Date, error) {
}
return Date{true, dd, mm, yy}, nil
}

// LatDir returns the latitude direction symbol
func LatDir(l float64) string {
if l < 0.0 {
return South
}
return North
}

// LonDir returns the longitude direction symbol
func LonDir(l float64) string {
if l < 0.0 {
return East
}
return West
}
31 changes: 30 additions & 1 deletion types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ func TestParseLatLong(t *testing.T) {
{"33\u00B0 12' 34.3423\"", 33.209540, false}, // dms
{"3345.1232 N", 33.752054, false}, // gps
{"151.234532", 151.234532, false}, // decimal
{"200.000", 0, true}, // out of range
}
for _, tt := range tests {
t.Run(tt.value, func(t *testing.T) {
Expand Down Expand Up @@ -220,3 +219,33 @@ func TestDateString(t *testing.T) {
t.Fatalf("got %s expected %s", s, expected)
}
}

func TestLatDir(t *testing.T) {
tests := []struct {
value float64
expected string
}{
{50.0, "N"},
{-50.0, "S"},
}
for _, tt := range tests {
if s := LatDir(tt.value); s != tt.expected {
t.Fatalf("got %s expected %s", s, tt.expected)
}
}
}

func TestLonDir(t *testing.T) {
tests := []struct {
value float64
expected string
}{
{100.0, "W"},
{-100.0, "E"},
}
for _, tt := range tests {
if s := LonDir(tt.value); s != tt.expected {
t.Fatalf("got %s expected %s", s, tt.expected)
}
}
}
28 changes: 28 additions & 0 deletions vhw.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package nmea

const (
// TypeVHW type for VHW sentences
TypeVHW = "VHW"
)

// VHW contains information about water speed and heading
type VHW struct {
BaseSentence
TrueHeading float64
MagneticHeading float64
SpeedThroughWaterKnots float64
SpeedThroughWaterKPH float64
}

// newVHW constructor
func newVHW(s BaseSentence) (VHW, error) {
p := newParser(s)
p.AssertType(TypeVHW)
return VHW{
BaseSentence: s,
TrueHeading: p.Float64(0, "true heading"),
MagneticHeading: p.Float64(2, "magnetic heading"),
SpeedThroughWaterKnots: p.Float64(4, "speed through water in knots"),
SpeedThroughWaterKPH: p.Float64(6, "speed through water in kilometers per hour"),
}, p.Err()
}
47 changes: 47 additions & 0 deletions vhw_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package nmea

import (
"testing"

"github.com/stretchr/testify/assert"
)

var vhw = []struct {
name string
raw string
err string
msg VHW
}{
{
name: "good sentence",
raw: "$VWVHW,45.0,T,43.0,M,3.5,N,6.4,K*56",
msg: VHW{
TrueHeading: 45.0,
MagneticHeading: 43.0,
SpeedThroughWaterKnots: 3.5,
SpeedThroughWaterKPH: 6.4,
},
},
{
name: "bad sentence",
raw: "$VWVHW,T,45.0,43.0,M,3.5,N,6.4,K*56",
err: "nmea: VWVHW invalid true heading: T",
},
}

func TestVHW(t *testing.T) {
for _, tt := range vhw {
t.Run(tt.name, func(t *testing.T) {
m, err := Parse(tt.raw)
if tt.err != "" {
assert.Error(t, err)
assert.EqualError(t, err, tt.err)
} else {
assert.NoError(t, err)
vhw := m.(VHW)
vhw.BaseSentence = BaseSentence{}
assert.Equal(t, tt.msg, vhw)
}
})
}
}

0 comments on commit 1270711

Please sign in to comment.