pax_global_header00006660000000000000000000000064141166214470014520gustar00rootroot0000000000000052 comment=e64b13d5c8c9634f0937ee51356b69481f30bc26 go-nmea-1.4.0/000077500000000000000000000000001411662144700130455ustar00rootroot00000000000000go-nmea-1.4.0/.github/000077500000000000000000000000001411662144700144055ustar00rootroot00000000000000go-nmea-1.4.0/.github/workflows/000077500000000000000000000000001411662144700164425ustar00rootroot00000000000000go-nmea-1.4.0/.github/workflows/ci.yml000066400000000000000000000017371411662144700175700ustar00rootroot00000000000000name: CI on: push: branches: - master pull_request: branches: - master workflow_dispatch: jobs: build: runs-on: ubuntu-latest strategy: fail-fast: false matrix: go: ["1.17", "1.16", "1.15", "1.14"] steps: - uses: actions/checkout@v2 - name: Set up Go uses: actions/setup-go@v2 with: go-version: ${{ matrix.go }} - name: Install dependencies run: | go get -u golang.org/x/lint/golint@latest go get -u github.com/mattn/goveralls@v0.0.9 - name: Lint run: | go vet ./... golint -set_exit_status ./... - name: Test run: | go test -v -race -covermode=atomic -coverprofile=profile.cov ./... - name: Coverage run: | goveralls -coverprofile=profile.cov -service=github -parallel -flagname="go-${{ matrix.go }}" env: COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} go-nmea-1.4.0/LICENSE000066400000000000000000000020701411662144700140510ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015 Adrian Moreno 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: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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. go-nmea-1.4.0/README.md000066400000000000000000000217061411662144700143320ustar00rootroot00000000000000# go-nmea [![CI](https://github.com/adrianmo/go-nmea/actions/workflows/ci.yml/badge.svg)](https://github.com/adrianmo/go-nmea/actions/workflows/ci.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/adrianmo/go-nmea)](https://goreportcard.com/report/github.com/adrianmo/go-nmea) [![Coverage Status](https://coveralls.io/repos/adrianmo/go-nmea/badge.svg?branch=master&service=github)](https://coveralls.io/github/adrianmo/go-nmea?branch=master) [![GoDoc](https://godoc.org/github.com/adrianmo/go-nmea?status.svg)](https://godoc.org/github.com/adrianmo/go-nmea) This is a NMEA library for the Go programming language (Golang). ## Features - Parse individual NMEA 0183 sentences - Support for sentences with NMEA 4.10 "TAG Blocks" - Register custom parser for unsupported sentence types - User-friendly MIT license ## Installing To install go-nmea use `go get`: ``` go get github.com/adrianmo/go-nmea ``` This will then make the `github.com/adrianmo/go-nmea` package available to you. ### Staying up to date To update go-nmea to the latest version, use `go get -u github.com/adrianmo/go-nmea`. ## Supported sentences At this moment, this library supports the following sentence types: | Sentence type | Description | | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------- | | [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 | | [GLL](http://aprs.gids.nl/nmea/#gll) | Geographic Position, Latitude / Longitude and time | | [VTG](http://aprs.gids.nl/nmea/#vtg) | Track Made Good and Ground Speed | | [ZDA](http://aprs.gids.nl/nmea/#zda) | Date & time data | | [HDT](http://aprs.gids.nl/nmea/#hdt) | Actual vessel heading in degrees True | | [GNS](https://www.trimble.com/oem_receiverhelp/v4.44/en/NMEA-0183messages_GNS.html) | Combined GPS fix for GPS, Glonass, Galileo, and BeiDou | | [PGRME](http://aprs.gids.nl/nmea/#rme) | Estimated Position Error (Garmin proprietary sentence) | | [THS](http://www.nuovamarea.net/pytheas_9.html) | Actual vessel heading in degrees True and status | | [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 | | [DPT](https://gpsd.gitlab.io/gpsd/NMEA.html#_dpt_depth_of_water) | Depth of Water | | [DBS](https://gpsd.gitlab.io/gpsd/NMEA.html#_dbs_depth_below_surface) | Depth Below Surface | | [DBT](https://gpsd.gitlab.io/gpsd/NMEA.html#_dbt_depth_below_transducer) | Depth below transducer | | [MDA](#) | Meteorological Composite | | [MWD](#) | Wind Direction and Speed | | [MWV](#) | Wind Speed and Angle | If you need to parse a message that contains an unsupported sentence type you can implement and register your own message parser and get yourself unblocked immediately. Check the example below to know how to [implement and register a custom message parser](#custom-message-parsing). However, if you think your custom message parser could be beneficial to other users we encourage you to contribute back to the library by submitting a PR and get it included in the list of supported sentences. ## Examples ### Built-in message parsing ```go package main import ( "fmt" "log" "github.com/adrianmo/go-nmea" ) func main() { sentence := "$GPRMC,220516,A,5133.82,N,00042.24,W,173.8,231.8,130694,004.2,W*70" s, err := nmea.Parse(sentence) if err != nil { log.Fatal(err) } if s.DataType() == nmea.TypeRMC { m := s.(nmea.RMC) fmt.Printf("Raw sentence: %v\n", m) fmt.Printf("Time: %s\n", m.Time) fmt.Printf("Validity: %s\n", m.Validity) fmt.Printf("Latitude GPS: %s\n", nmea.FormatGPS(m.Latitude)) fmt.Printf("Latitude DMS: %s\n", nmea.FormatDMS(m.Latitude)) fmt.Printf("Longitude GPS: %s\n", nmea.FormatGPS(m.Longitude)) fmt.Printf("Longitude DMS: %s\n", nmea.FormatDMS(m.Longitude)) fmt.Printf("Speed: %f\n", m.Speed) fmt.Printf("Course: %f\n", m.Course) fmt.Printf("Date: %s\n", m.Date) fmt.Printf("Variation: %f\n", m.Variation) } } ``` Output: ``` $ go run main/main.go Raw sentence: $GPRMC,220516,A,5133.82,N,00042.24,W,173.8,231.8,130694,004.2,W*70 Time: 22:05:16.0000 Validity: A Latitude GPS: 5133.8200 Latitude DMS: 51° 33' 49.200000" Longitude GPS: 042.2400 Longitude DMS: 0° 42' 14.400000" Speed: 173.800000 Course: 231.800000 Date: 13/06/94 Variation: -4.200000 ``` ### TAG Blocks NMEA 4.10 TAG Block values can be accessed via the message's `TagBlock` struct: ```go package main import ( "fmt" "log" "time" "github.com/adrianmo/go-nmea" ) func main() { sentence := "\\s:Satelite_1,c:1553390539*62\\!AIVDM,1,1,,A,13M@ah0025QdPDTCOl`K6`nV00Sv,0*52" s, err := nmea.Parse(sentence) if err != nil { log.Fatal(err) } parsed := s.(nmea.VDMVDO) fmt.Printf("TAG Block timestamp: %v\n", time.Unix(parsed.TagBlock.Time, 0)) fmt.Printf("TAG Block source: %v\n", parsed.TagBlock.Source) } ``` Output (locale/time zone dependent): ``` $ go run main/main.go TAG Block timestamp: 2019-03-24 14:22:19 +1300 NZDT TAG Block source: Satelite_1 ``` ### Custom message parsing If you need to parse a message not supported by the library you can implement your own message parsing. The following example implements a parser for the hypothetical XYZ NMEA sentence type. ```go package main import ( "fmt" "github.com/adrianmo/go-nmea" ) // A type to hold the parsed record type XYZType struct { nmea.BaseSentence Time nmea.Time Counter int64 Label string Value float64 } func main() { // Do this once it will error if you register the same type multiple times err := nmea.RegisterParser("XYZ", func(s nmea.BaseSentence) (nmea.Sentence, error) { // This example uses the package builtin parsing helpers // you can implement your own parsing logic also p := nmea.NewParser(s) return XYZType{ BaseSentence: s, Time: p.Time(0, "time"), Label: p.String(1, "label"), Counter: p.Int64(2, "counter"), Value: p.Float64(3, "value"), }, p.Err() }) if err != nil { panic(err) } sentence := "$00XYZ,220516,A,23,5133.82,W*42" s, err := nmea.Parse(sentence) if err != nil { panic(err) } m, ok := s.(XYZType) if !ok { panic("Could not parse type XYZ") } fmt.Printf("Raw sentence: %v\n", m) fmt.Printf("Time: %s\n", m.Time) fmt.Printf("Label: %s\n", m.Label) fmt.Printf("Counter: %d\n", m.Counter) fmt.Printf("Value: %f\n", m.Value) } ``` Output: ``` $ go run main/main.go Raw sentence: $AAXYZ,220516,A,23,5133.82,W*42 Time: 22:05:16.0000 Label: A Counter: 23 Value: 5133.820000 ``` ## Contributing Please feel free to submit issues or fork the repository and send pull requests to update the library and fix bugs, implement support for new sentence types, refactor code, etc. ## License Check [LICENSE](LICENSE). go-nmea-1.4.0/dbs.go000066400000000000000000000010631411662144700141440ustar00rootroot00000000000000package nmea const ( // TypeDBS type for DBS sentences TypeDBS = "DBS" ) // DBS - Depth Below Surface // https://gpsd.gitlab.io/gpsd/NMEA.html#_dbs_depth_below_surface type DBS struct { BaseSentence DepthFeet float64 DepthMeters float64 DepthFathoms float64 } // newDBS constructor func newDBS(s BaseSentence) (DBS, error) { p := NewParser(s) p.AssertType(TypeDBS) return DBS{ BaseSentence: s, DepthFeet: p.Float64(0, "depth_feet"), DepthMeters: p.Float64(2, "depth_meters"), DepthFathoms: p.Float64(4, "depth_fathoms"), }, p.Err() } go-nmea-1.4.0/dbs_test.go000066400000000000000000000015301411662144700152020ustar00rootroot00000000000000package nmea import ( "testing" "github.com/stretchr/testify/assert" ) var dbstests = []struct { name string raw string err string msg DBS }{ { name: "good sentence", raw: "$23DBS,01.9,f,0.58,M,00.3,F*21", msg: DBS{ DepthFeet: MustParseDecimal("1.9"), DepthMeters: MustParseDecimal("0.58"), DepthFathoms: MustParseDecimal("0.3"), }, }, { name: "bad validity", raw: "$23DBS,01.9,f,0.58,M,00.3,F*25", err: "nmea: sentence checksum mismatch [21 != 25]", }, } func TestDBS(t *testing.T) { for _, tt := range dbstests { 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) dbs := m.(DBS) dbs.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, dbs) } }) } } go-nmea-1.4.0/dbt.go000066400000000000000000000010711411662144700141440ustar00rootroot00000000000000package nmea const ( // TypeDBT type for DBT sentences TypeDBT = "DBT" ) // DBT - Depth below transducer // https://gpsd.gitlab.io/gpsd/NMEA.html#_dbt_depth_below_transducer type DBT struct { BaseSentence DepthFeet float64 DepthMeters float64 DepthFathoms float64 } // newDBT constructor func newDBT(s BaseSentence) (DBT, error) { p := NewParser(s) p.AssertType(TypeDBT) return DBT{ BaseSentence: s, DepthFeet: p.Float64(0, "depth_feet"), DepthMeters: p.Float64(2, "depth_meters"), DepthFathoms: p.Float64(4, "depth_fathoms"), }, p.Err() } go-nmea-1.4.0/dbt_test.go000066400000000000000000000015501411662144700152050ustar00rootroot00000000000000package nmea import ( "testing" "github.com/stretchr/testify/assert" ) var dbttests = []struct { name string raw string err string msg DBT }{ { name: "good sentence", raw: "$IIDBT,032.93,f,010.04,M,005.42,F*2C", msg: DBT{ DepthFeet: MustParseDecimal("32.93"), DepthMeters: MustParseDecimal("10.04"), DepthFathoms: MustParseDecimal("5.42"), }, }, { name: "bad validity", raw: "$IIDBT,032.93,f,010.04,M,005.42,F*22", err: "nmea: sentence checksum mismatch [2C != 22]", }, } func TestDBT(t *testing.T) { for _, tt := range dbttests { 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) dbt := m.(DBT) dbt.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, dbt) } }) } } go-nmea-1.4.0/deprecated.go000066400000000000000000000060241411662144700154760ustar00rootroot00000000000000package nmea type ( // GLGSV represents the GPS Satellites in view http://aprs.gids.nl/nmea/#glgsv // // Deprecated: Use GSV instead GLGSV = GSV // GLGSVInfo represents information about a visible satellite // // Deprecated: Use GSVInfo instead GLGSVInfo = GSVInfo // GNGGA is the Time, position, and fix related data of the receiver. // // Deprecated: Use GGA instead GNGGA = GGA // GNGNS is standard GNSS sentance that combined multiple constellations // // Deprecated: Use GNS instead GNGNS = GNS // GNRMC is the Recommended Minimum Specific GNSS data. http://aprs.gids.nl/nmea/#rmc // // Deprecated: Use RCM instead GNRMC = RMC // GPGGA represents fix data. http://aprs.gids.nl/nmea/#gga // // Deprecated: Use GGA instead GPGGA = GGA // GPGLL is Geographic Position, Latitude / Longitude and time. http://aprs.gids.nl/nmea/#gll // // Deprecated: Use GLL instead GPGLL = GLL // GPGSA represents overview satellite data. http://aprs.gids.nl/nmea/#gsa // // Deprecated: Use GSA instead GPGSA = GSA // GPGSV represents the GPS Satellites in view http://aprs.gids.nl/nmea/#gpgsv // // Deprecated: Use GSV instead GPGSV = GSV // GPGSVInfo represents information about a visible satellite // // Deprecated: Use GSVInfo instead GPGSVInfo = GSVInfo // GPHDT is the Actual vessel heading in degrees True. http://aprs.gids.nl/nmea/#hdt // // Deprecated: Use HDT instead GPHDT = HDT // GPRMC is the Recommended Minimum Specific GNSS data. http://aprs.gids.nl/nmea/#rmc // // Deprecated: Use RMC instead GPRMC = RMC // GPVTG represents track & speed data. http://aprs.gids.nl/nmea/#vtg // // Deprecated: Use VTG instead GPVTG = VTG // GPZDA represents date & time data. http://aprs.gids.nl/nmea/#zda // // Deprecated: Use ZDA instead GPZDA = ZDA ) const ( // PrefixGNGNS prefix // // Deprecated: Use TypeGNS instead PrefixGNGNS = "GNGNS" // PrefixGPGGA prefix // // Deprecated: Use TypeGGA instead PrefixGPGGA = "GPGGA" // PrefixGPGLL prefix for GPGLL sentence type // // Deprecated: Use TypeGLL instead PrefixGPGLL = "GPGLL" // PrefixGPGSA prefix of GPGSA sentence type // // Deprecated: Use TypeGSA instead PrefixGPGSA = "GPGSA" // PrefixGPRMC prefix of GPRMC sentence type // // Deprecated: Use TypeRMC instead PrefixGPRMC = "GPRMC" // PrefixPGRME prefix for PGRME sentence type // // Deprecated: Use TypePGRME instead PrefixPGRME = "PGRME" // PrefixGLGSV prefix // // Deprecated: Use TypeGSV instead PrefixGLGSV = "GLGSV" // PrefixGNGGA prefix // // Deprecated: Use TypeGGA instead PrefixGNGGA = "GNGGA" // PrefixGNRMC prefix of GNRMC sentence type // // Deprecated: Use TypeRMC instead PrefixGNRMC = "GNRMC" // PrefixGPGSV prefix // // Deprecated: Use TypeGSV instead PrefixGPGSV = "GPGSV" // PrefixGPHDT prefix of GPHDT sentence type // // Deprecated: Use TypeHDT instead PrefixGPHDT = "GPHDT" // PrefixGPVTG prefix // // Deprecated: Use TypeVTG instead PrefixGPVTG = "GPVTG" // PrefixGPZDA prefix // // Deprecated: Use TypeZDA instead PrefixGPZDA = "GPZDA" ) go-nmea-1.4.0/deprecated_test.go000066400000000000000000000417171411662144700165450ustar00rootroot00000000000000package nmea import ( "testing" "github.com/stretchr/testify/assert" ) var glgsvtests = []struct { name string raw string err string msg GLGSV }{ { name: "good sentence", raw: "$GLGSV,3,1,11,03,03,111,00,04,15,270,00,06,01,010,12,13,06,292,00*6B", msg: GLGSV{ TotalMessages: 3, MessageNumber: 1, NumberSVsInView: 11, Info: []GLGSVInfo{ {SVPRNNumber: 3, Elevation: 3, Azimuth: 111, SNR: 0}, {SVPRNNumber: 4, Elevation: 15, Azimuth: 270, SNR: 0}, {SVPRNNumber: 6, Elevation: 1, Azimuth: 10, SNR: 12}, {SVPRNNumber: 13, Elevation: 6, Azimuth: 292, SNR: 0}, }, }, }, { name: "short sentence", raw: "$GLGSV,3,1,11,03,03,111,00,04,15,270,00,06,01,010,12*56", msg: GLGSV{ TotalMessages: 3, MessageNumber: 1, NumberSVsInView: 11, Info: []GLGSVInfo{ {SVPRNNumber: 3, Elevation: 3, Azimuth: 111, SNR: 0}, {SVPRNNumber: 4, Elevation: 15, Azimuth: 270, SNR: 0}, {SVPRNNumber: 6, Elevation: 1, Azimuth: 10, SNR: 12}, }, }, }, { name: "invalid number of svs", raw: "$GLGSV,3,1,11.2,03,03,111,00,04,15,270,00,06,01,010,12,13,06,292,00*77", err: "nmea: GLGSV invalid number of SVs in view: 11.2", }, { name: "invalid number of messages", raw: "$GLGSV,A3,1,11,03,03,111,00,04,15,270,00,06,01,010,12,13,06,292,00*2A", err: "nmea: GLGSV invalid total number of messages: A3", }, { name: "invalid message number", raw: "$GLGSV,3,A1,11,03,03,111,00,04,15,270,00,06,01,010,12,13,06,292,00*2A", err: "nmea: GLGSV invalid message number: A1", }, { name: "invalid SV prn number", raw: "$GLGSV,3,1,11,A03,03,111,00,04,15,270,00,06,01,010,12,13,06,292,00*2A", err: "nmea: GLGSV invalid SV prn number: A03", }, { name: "invalid elevation", raw: "$GLGSV,3,1,11,03,A03,111,00,04,15,270,00,06,01,010,12,13,06,292,00*2A", err: "nmea: GLGSV invalid elevation: A03", }, { name: "invalid azimuth", raw: "$GLGSV,3,1,11,03,03,A111,00,04,15,270,00,06,01,010,12,13,06,292,00*2A", err: "nmea: GLGSV invalid azimuth: A111", }, { name: "invalid SNR", raw: "$GLGSV,3,1,11,03,03,111,A00,04,15,270,00,06,01,010,12,13,06,292,00*2A", err: "nmea: GLGSV invalid SNR: A00", }, } func TestGLGSV(t *testing.T) { for _, tt := range glgsvtests { 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) glgsv := m.(GLGSV) glgsv.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, glgsv) } }) } } var gnggatests = []struct { name string raw string err string msg GNGGA }{ { name: "good sentence", raw: "$GNGGA,203415.000,6325.6138,N,01021.4290,E,1,8,2.42,72.5,M,41.5,M,,*7C", msg: GNGGA{ Time: Time{ Valid: true, Hour: 20, Minute: 34, Second: 15, Millisecond: 0, }, Latitude: MustParseLatLong("6325.6138 N"), Longitude: MustParseLatLong("01021.4290 E"), FixQuality: "1", NumSatellites: 8, HDOP: 2.42, Altitude: 72.5, Separation: 41.5, DGPSAge: "", DGPSId: "", }, }, { name: "bad latitude", raw: "$GNGGA,034225.077,A,S,15124.5567,E,1,03,9.7,-25.0,M,21.0,M,,0000*24", err: "nmea: GNGGA invalid latitude: cannot parse [A S], unknown format", }, { name: "bad longitude", raw: "$GNGGA,034225.077,3356.4650,S,A,E,1,03,9.7,-25.0,M,21.0,M,,0000*12", err: "nmea: GNGGA invalid longitude: cannot parse [A E], unknown format", }, { name: "bad fix quality", raw: "$GNGGA,034225.077,3356.4650,S,15124.5567,E,12,03,9.7,-25.0,M,21.0,M,,0000*7D", err: "nmea: GNGGA invalid fix quality: 12", }, } func TestGNGGA(t *testing.T) { for _, tt := range gnggatests { 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) gngga := m.(GNGGA) gngga.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, gngga) } }) } } var gngnstests = []struct { name string raw string err string msg GNGNS }{ { name: "good sentence A", raw: "$GNGNS,014035.00,4332.69262,S,17235.48549,E,RR,13,0.9,25.63,11.24,,*70", msg: GNGNS{ Time: Time{true, 1, 40, 35, 0}, Latitude: MustParseGPS("4332.69262 S"), Longitude: MustParseGPS("17235.48549 E"), Mode: []string{"R", "R"}, SVs: 13, HDOP: 0.9, Altitude: 25.63, Separation: 11.24, Age: 0, Station: 0, }, }, { name: "good sentence B", raw: "$GNGNS,094821.0,4849.931307,N,00216.053323,E,AA,14,0.6,161.5,48.0,,*6D", msg: GNGNS{ Time: Time{true, 9, 48, 21, 0}, Latitude: MustParseGPS("4849.931307 N"), Longitude: MustParseGPS("00216.053323 E"), Mode: []string{"A", "A"}, SVs: 14, HDOP: 0.6, Altitude: 161.5, Separation: 48.0, Age: 0, Station: 0, }, }, { name: "good sentence B", raw: "$GNGNS,094821.0,4849.931307,N,00216.053323,E,AAN,14,0.6,161.5,48.0,,*23", msg: GNGNS{ Time: Time{true, 9, 48, 21, 0}, Latitude: MustParseGPS("4849.931307 N"), Longitude: MustParseGPS("00216.053323 E"), Mode: []string{"A", "A", "N"}, SVs: 14, HDOP: 0.6, Altitude: 161.5, Separation: 48.0, Age: 0, Station: 0, }, }, { name: "bad sentence", raw: "$GNGNS,094821.0,4849.931307,N,00216.053323,E,AAX,14,0.6,161.5,48.0,,*35", err: "nmea: GNGNS invalid mode: AAX", }, } func TestGNGNS(t *testing.T) { for _, tt := range gngnstests { 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) gngns := m.(GNGNS) gngns.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, gngns) } }) } } var gnrmctests = []struct { name string raw string err string msg GNRMC }{ { name: "good sentence A", raw: "$GNRMC,220516,A,5133.82,N,00042.24,W,173.8,231.8,130694,004.2,W*6E", msg: GNRMC{ Time: Time{true, 22, 05, 16, 0}, Validity: "A", Speed: 173.8, Course: 231.8, Date: Date{true, 13, 06, 94}, Variation: -4.2, Latitude: MustParseGPS("5133.82 N"), Longitude: MustParseGPS("00042.24 W"), }, }, { name: "good sentence B", raw: "$GNRMC,142754.0,A,4302.539570,N,07920.379823,W,0.0,,070617,0.0,E,A*21", msg: GNRMC{ Time: Time{true, 14, 27, 54, 0}, Validity: "A", Speed: 0, Course: 0, Date: Date{true, 7, 6, 17}, Variation: 0, Latitude: MustParseGPS("4302.539570 N"), Longitude: MustParseGPS("07920.379823 W"), }, }, { name: "good sentence C", raw: "$GNRMC,100538.00,A,5546.27711,N,03736.91144,E,0.061,,260318,,,A*60", msg: GNRMC{ Time: Time{true, 10, 5, 38, 0}, Validity: "A", Speed: 0.061, Course: 0, Date: Date{true, 26, 3, 18}, Variation: 0, Latitude: MustParseGPS("5546.27711 N"), Longitude: MustParseGPS("03736.91144 E"), }, }, { name: "bad sentence", raw: "$GNRMC,220516,D,5133.82,N,00042.24,W,173.8,231.8,130694,004.2,W*6B", err: "nmea: GNRMC invalid validity: D", }, } func TestGNRMC(t *testing.T) { for _, tt := range gnrmctests { 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) gnrmc := m.(GNRMC) gnrmc.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, gnrmc) } }) } } var gpggatests = []struct { name string raw string err string msg GPGGA }{ { name: "good sentence", raw: "$GPGGA,034225.077,3356.4650,S,15124.5567,E,1,03,9.7,-25.0,M,21.0,M,,0000*51", msg: GPGGA{ Time: Time{true, 3, 42, 25, 77}, Latitude: MustParseLatLong("3356.4650 S"), Longitude: MustParseLatLong("15124.5567 E"), FixQuality: GPS, NumSatellites: 03, HDOP: 9.7, Altitude: -25.0, Separation: 21.0, DGPSAge: "", DGPSId: "0000", }, }, { name: "bad latitude", raw: "$GPGGA,034225.077,A,S,15124.5567,E,1,03,9.7,-25.0,M,21.0,M,,0000*3A", err: "nmea: GPGGA invalid latitude: cannot parse [A S], unknown format", }, { name: "bad longitude", raw: "$GPGGA,034225.077,3356.4650,S,A,E,1,03,9.7,-25.0,M,21.0,M,,0000*0C", err: "nmea: GPGGA invalid longitude: cannot parse [A E], unknown format", }, { name: "bad fix quality", raw: "$GPGGA,034225.077,3356.4650,S,15124.5567,E,12,03,9.7,-25.0,M,21.0,M,,0000*63", err: "nmea: GPGGA invalid fix quality: 12", }, } func TestGPGGA(t *testing.T) { for _, tt := range gpggatests { 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) gpgga := m.(GPGGA) gpgga.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, gpgga) } }) } } var gpglltests = []struct { name string raw string err string msg GPGLL }{ { name: "good sentence", raw: "$GPGLL,3926.7952,N,12000.5947,W,022732,A,A*58", msg: GPGLL{ Latitude: MustParseLatLong("3926.7952 N"), Longitude: MustParseLatLong("12000.5947 W"), Time: Time{ Valid: true, Hour: 2, Minute: 27, Second: 32, Millisecond: 0, }, Validity: "A", }, }, { name: "bad validity", raw: "$GPGLL,3926.7952,N,12000.5947,W,022732,D,A*5D", err: "nmea: GPGLL invalid validity: D", }, } func TestGPGLL(t *testing.T) { for _, tt := range gpglltests { 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) gpgll := m.(GPGLL) gpgll.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, gpgll) } }) } } var gpgsatests = []struct { name string raw string err string msg GPGSA }{ { name: "good sentence", raw: "$GPGSA,A,3,22,19,18,27,14,03,,,,,,,3.1,2.0,2.4*36", msg: GPGSA{ Mode: "A", FixType: "3", SV: []string{"22", "19", "18", "27", "14", "03"}, PDOP: 3.1, HDOP: 2, VDOP: 2.4, }, }, { name: "bad mode", raw: "$GPGSA,F,3,22,19,18,27,14,03,,,,,,,3.1,2.0,2.4*31", err: "nmea: GPGSA invalid selection mode: F", }, { name: "bad fix", raw: "$GPGSA,A,6,22,19,18,27,14,03,,,,,,,3.1,2.0,2.4*33", err: "nmea: GPGSA invalid fix type: 6", }, } func TestGPGSA(t *testing.T) { for _, tt := range gpgsatests { 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) gpgsa := m.(GPGSA) gpgsa.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, gpgsa) } }) } } var gpgsvtests = []struct { name string raw string err string msg GPGSV }{ { name: "good sentence", raw: "$GPGSV,3,1,11,03,03,111,00,04,15,270,00,06,01,010,12,13,06,292,00*77", msg: GPGSV{ TotalMessages: 3, MessageNumber: 1, NumberSVsInView: 11, Info: []GPGSVInfo{ {SVPRNNumber: 3, Elevation: 3, Azimuth: 111, SNR: 0}, {SVPRNNumber: 4, Elevation: 15, Azimuth: 270, SNR: 0}, {SVPRNNumber: 6, Elevation: 1, Azimuth: 10, SNR: 12}, {SVPRNNumber: 13, Elevation: 6, Azimuth: 292, SNR: 0}, }, }, }, { name: "short", raw: "$GPGSV,3,1,11,03,03,111,00,04,15,270,00,06,01,010,12*4A", msg: GPGSV{ TotalMessages: 3, MessageNumber: 1, NumberSVsInView: 11, Info: []GPGSVInfo{ {SVPRNNumber: 3, Elevation: 3, Azimuth: 111, SNR: 0}, {SVPRNNumber: 4, Elevation: 15, Azimuth: 270, SNR: 0}, {SVPRNNumber: 6, Elevation: 1, Azimuth: 10, SNR: 12}, }, }, }, { name: "invalid number of SVs", raw: "$GPGSV,3,1,11.2,03,03,111,00,04,15,270,00,06,01,010,12,13,06,292,00*6b", err: "nmea: GPGSV invalid number of SVs in view: 11.2", }, { name: "invalid total number of messages", raw: "$GPGSV,A3,1,11,03,03,111,00,04,15,270,00,06,01,010,12,13,06,292,00*36", err: "nmea: GPGSV invalid total number of messages: A3", }, { name: "invalid message number", raw: "$GPGSV,3,A1,11,03,03,111,00,04,15,270,00,06,01,010,12,13,06,292,00*36", err: "nmea: GPGSV invalid message number: A1", }, { name: "invalid SV prn number", raw: "$GPGSV,3,1,11,A03,03,111,00,04,15,270,00,06,01,010,12,13,06,292,00*36", err: "nmea: GPGSV invalid SV prn number: A03", }, { name: "invalid elevation", raw: "$GPGSV,3,1,11,03,A03,111,00,04,15,270,00,06,01,010,12,13,06,292,00*36", err: "nmea: GPGSV invalid elevation: A03", }, { name: "invalid azimuth", raw: "$GPGSV,3,1,11,03,03,A111,00,04,15,270,00,06,01,010,12,13,06,292,00*36", err: "nmea: GPGSV invalid azimuth: A111", }, { name: "invalid SNR", raw: "$GPGSV,3,1,11,03,03,111,A00,04,15,270,00,06,01,010,12,13,06,292,00*36", err: "nmea: GPGSV invalid SNR: A00", }, } func TestGPGSV(t *testing.T) { for _, tt := range gpgsvtests { 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) gpgsv := m.(GPGSV) gpgsv.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, gpgsv) } }) } } var gphdttests = []struct { name string raw string err string msg GPHDT }{ { name: "good sentence", raw: "$GPHDT,123.456,T*32", msg: GPHDT{ Heading: 123.456, True: true, }, }, { name: "invalid True", raw: "$GPHDT,123.456,X*3E", err: "nmea: GPHDT invalid true: X", }, { name: "invalid Heading", raw: "$GPHDT,XXX,T*43", err: "nmea: GPHDT invalid heading: XXX", }, } func TestGPHDT(t *testing.T) { for _, tt := range gphdttests { 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) gphdt := m.(GPHDT) gphdt.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, gphdt) } }) } } var gprmctests = []struct { name string raw string err string msg GPRMC }{ { name: "good sentence A", raw: "$GPRMC,220516,A,5133.82,N,00042.24,W,173.8,231.8,130694,004.2,W*70", msg: GPRMC{ Time: Time{true, 22, 5, 16, 0}, Validity: "A", Speed: 173.8, Course: 231.8, Date: Date{true, 13, 6, 94}, Variation: -4.2, Latitude: MustParseGPS("5133.82 N"), Longitude: MustParseGPS("00042.24 W"), }, }, { name: "good sentence B", raw: "$GPRMC,142754.0,A,4302.539570,N,07920.379823,W,0.0,,070617,0.0,E,A*3F", msg: GPRMC{ Time: Time{true, 14, 27, 54, 0}, Validity: "A", Speed: 0, Course: 0, Date: Date{true, 7, 6, 17}, Variation: 0, Latitude: MustParseGPS("4302.539570 N"), Longitude: MustParseGPS("07920.379823 W"), }, }, { name: "bad validity", raw: "$GPRMC,220516,D,5133.82,N,00042.24,W,173.8,231.8,130694,004.2,W*75", err: "nmea: GPRMC invalid validity: D", }, } func TestGPRMC(t *testing.T) { for _, tt := range gprmctests { 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) gprmc := m.(GPRMC) gprmc.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, gprmc) } }) } } var gpvtgtests = []struct { name string raw string err string msg GPVTG }{ { name: "good sentence", raw: "$GPVTG,45.5,T,67.5,M,30.45,N,56.40,K*4B", msg: GPVTG{ TrueTrack: 45.5, MagneticTrack: 67.5, GroundSpeedKnots: 30.45, GroundSpeedKPH: 56.4, }, }, { name: "bad true track", raw: "$GPVTG,T,45.5,67.5,M,30.45,N,56.40,K*4B", err: "nmea: GPVTG invalid true track: T", }, } func TestGPVTG(t *testing.T) { for _, tt := range gpvtgtests { 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) gpvtg := m.(GPVTG) gpvtg.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, gpvtg) } }) } } var gpzdatests = []struct { name string raw string err string msg GPZDA }{ { name: "good sentence", raw: "$GPZDA,172809.456,12,07,1996,00,00*57", msg: GPZDA{ Time: Time{ Valid: true, Hour: 17, Minute: 28, Second: 9, Millisecond: 456, }, Day: 12, Month: 7, Year: 1996, OffsetHours: 0, OffsetMinutes: 0, }, }, { name: "invalid day", raw: "$GPZDA,220516,D,5133.82,N,00042.24,W,173.8,231.8,130694,004.2,W*76", err: "nmea: GPZDA invalid day: D", }, } func TestGPZDA(t *testing.T) { for _, tt := range gpzdatests { 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) gpzda := m.(GPZDA) gpzda.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, gpzda) } }) } } go-nmea-1.4.0/dpt.go000066400000000000000000000010261411662144700141620ustar00rootroot00000000000000package nmea const ( // TypeDPT type for DPT sentences TypeDPT = "DPT" ) // DPT - Depth of Water // https://gpsd.gitlab.io/gpsd/NMEA.html#_dpt_depth_of_water type DPT struct { BaseSentence Depth float64 Offset float64 RangeScale float64 } // newDPT constructor func newDPT(s BaseSentence) (DPT, error) { p := NewParser(s) p.AssertType(TypeDPT) return DPT{ BaseSentence: s, Depth: p.Float64(0, "depth"), Offset: p.Float64(1, "offset"), RangeScale: p.Float64(2, "range scale"), }, p.Err() } go-nmea-1.4.0/dpt_test.go000066400000000000000000000020141411662144700152170ustar00rootroot00000000000000package nmea import ( "testing" "github.com/stretchr/testify/assert" ) var dpttests = []struct { name string raw string err string msg DPT }{ { name: "good sentence", raw: "$SDDPT,0.5,0.5,*7B", msg: DPT{ Depth: MustParseDecimal("0.5"), Offset: MustParseDecimal("0.5"), RangeScale: MustParseDecimal("0"), }, }, { name: "good sentence with scale", raw: "$SDDPT,0.5,0.5,0.1*54", msg: DPT{ Depth: MustParseDecimal("0.5"), Offset: MustParseDecimal("0.5"), RangeScale: MustParseDecimal("0.1"), }, }, { name: "bad validity", raw: "$SDDPT,0.5,0.5,*AA", err: "nmea: sentence checksum mismatch [7B != AA]", }, } func TestDPT(t *testing.T) { for _, tt := range dpttests { 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) dpt := m.(DPT) dpt.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, dpt) } }) } } go-nmea-1.4.0/gga.go000066400000000000000000000027551411662144700141430ustar00rootroot00000000000000package nmea const ( // TypeGGA type for GGA sentences TypeGGA = "GGA" // Invalid fix quality. Invalid = "0" // GPS fix quality GPS = "1" // DGPS fix quality DGPS = "2" // PPS fix PPS = "3" // RTK real time kinematic fix 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. type GGA struct { BaseSentence Time Time // Time of fix. Latitude float64 // Latitude. Longitude float64 // Longitude. FixQuality string // Quality of fix. NumSatellites int64 // Number of satellites in use. HDOP float64 // Horizontal dilution of precision. Altitude float64 // Altitude. Separation float64 // Geoidal separation DGPSAge string // Age of differential GPD data. DGPSId string // DGPS reference station ID. } // newGGA constructor func newGGA(s BaseSentence) (GGA, error) { p := NewParser(s) p.AssertType(TypeGGA) return GGA{ BaseSentence: s, 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, EST), NumSatellites: p.Int64(6, "number of satellites"), HDOP: p.Float64(7, "hdop"), Altitude: p.Float64(8, "altitude"), Separation: p.Float64(10, "separation"), DGPSAge: p.String(12, "dgps age"), DGPSId: p.String(13, "dgps id"), }, p.Err() } go-nmea-1.4.0/gga_test.go000066400000000000000000000050041411662144700151700ustar00rootroot00000000000000package nmea import ( "testing" "github.com/stretchr/testify/assert" ) var ggatests = []struct { name string raw string err string msg GGA }{ { name: "good sentence", raw: "$GNGGA,203415.000,6325.6138,N,01021.4290,E,1,8,2.42,72.5,M,41.5,M,,*7C", msg: GGA{ Time: Time{ Valid: true, Hour: 20, Minute: 34, Second: 15, Millisecond: 0, }, Latitude: MustParseLatLong("6325.6138 N"), Longitude: MustParseLatLong("01021.4290 E"), FixQuality: "1", NumSatellites: 8, HDOP: 2.42, Altitude: 72.5, Separation: 41.5, DGPSAge: "", DGPSId: "", }, }, { name: "bad latitude", raw: "$GNGGA,034225.077,A,S,15124.5567,E,1,03,9.7,-25.0,M,21.0,M,,0000*24", err: "nmea: GNGGA invalid latitude: cannot parse [A S], unknown format", }, { name: "bad longitude", raw: "$GNGGA,034225.077,3356.4650,S,A,E,1,03,9.7,-25.0,M,21.0,M,,0000*12", err: "nmea: GNGGA invalid longitude: cannot parse [A E], unknown format", }, { name: "bad fix quality", raw: "$GNGGA,034225.077,3356.4650,S,15124.5567,E,12,03,9.7,-25.0,M,21.0,M,,0000*7D", err: "nmea: GNGGA invalid fix quality: 12", }, { name: "good sentence", raw: "$GPGGA,034225.077,3356.4650,S,15124.5567,E,1,03,9.7,-25.0,M,21.0,M,,0000*51", msg: GGA{ Time: Time{true, 3, 42, 25, 77}, Latitude: MustParseLatLong("3356.4650 S"), Longitude: MustParseLatLong("15124.5567 E"), FixQuality: GPS, NumSatellites: 03, HDOP: 9.7, Altitude: -25.0, Separation: 21.0, DGPSAge: "", DGPSId: "0000", }, }, { name: "bad latitude", raw: "$GPGGA,034225.077,A,S,15124.5567,E,1,03,9.7,-25.0,M,21.0,M,,0000*3A", err: "nmea: GPGGA invalid latitude: cannot parse [A S], unknown format", }, { name: "bad longitude", raw: "$GPGGA,034225.077,3356.4650,S,A,E,1,03,9.7,-25.0,M,21.0,M,,0000*0C", err: "nmea: GPGGA invalid longitude: cannot parse [A E], unknown format", }, { name: "bad fix quality", raw: "$GPGGA,034225.077,3356.4650,S,15124.5567,E,12,03,9.7,-25.0,M,21.0,M,,0000*63", err: "nmea: GPGGA invalid fix quality: 12", }, } func TestGGA(t *testing.T) { for _, tt := range ggatests { 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) gga := m.(GGA) gga.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, gga) } }) } } go-nmea-1.4.0/gll.go000066400000000000000000000014041411662144700141510ustar00rootroot00000000000000package nmea const ( // TypeGLL type for GLL sentences TypeGLL = "GLL" // ValidGLL character ValidGLL = "A" // InvalidGLL character InvalidGLL = "V" ) // GLL is Geographic Position, Latitude / Longitude and time. // http://aprs.gids.nl/nmea/#gll type GLL struct { BaseSentence Latitude float64 // Latitude Longitude float64 // Longitude Time Time // Time Stamp Validity string // validity - A-valid } // newGLL constructor func newGLL(s BaseSentence) (GLL, error) { p := NewParser(s) p.AssertType(TypeGLL) return GLL{ BaseSentence: s, Latitude: p.LatLong(0, 1, "latitude"), Longitude: p.LatLong(2, 3, "longitude"), Time: p.Time(4, "time"), Validity: p.EnumString(5, "validity", ValidGLL, InvalidGLL), }, p.Err() } go-nmea-1.4.0/gll_test.go000066400000000000000000000017321411662144700152140ustar00rootroot00000000000000package nmea import ( "testing" "github.com/stretchr/testify/assert" ) var glltests = []struct { name string raw string err string msg GLL }{ { name: "good sentence", raw: "$GPGLL,3926.7952,N,12000.5947,W,022732,A,A*58", msg: GLL{ Latitude: MustParseLatLong("3926.7952 N"), Longitude: MustParseLatLong("12000.5947 W"), Time: Time{ Valid: true, Hour: 2, Minute: 27, Second: 32, Millisecond: 0, }, Validity: "A", }, }, { name: "bad validity", raw: "$GPGLL,3926.7952,N,12000.5947,W,022732,D,A*5D", err: "nmea: GPGLL invalid validity: D", }, } func TestGLL(t *testing.T) { for _, tt := range glltests { 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) gll := m.(GLL) gll.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, gll) } }) } } go-nmea-1.4.0/gns.go000066400000000000000000000027631411662144700141730ustar00rootroot00000000000000package nmea const ( // TypeGNS type for GNS sentences TypeGNS = "GNS" // NoFixGNS Character NoFixGNS = "N" // AutonomousGNS Character AutonomousGNS = "A" // DifferentialGNS Character DifferentialGNS = "D" // PreciseGNS Character PreciseGNS = "P" // RealTimeKinematicGNS Character RealTimeKinematicGNS = "R" // FloatRTKGNS RealTime Kinematic Character FloatRTKGNS = "F" // EstimatedGNS Fix Character EstimatedGNS = "E" // ManualGNS Fix Character ManualGNS = "M" // SimulatorGNS Character SimulatorGNS = "S" ) // GNS is standard GNSS sentance that combined multiple constellations type GNS struct { BaseSentence Time Time Latitude float64 Longitude float64 Mode []string SVs int64 HDOP float64 Altitude float64 Separation float64 Age float64 Station int64 } // newGNS Constructor func newGNS(s BaseSentence) (GNS, error) { p := NewParser(s) p.AssertType(TypeGNS) m := GNS{ BaseSentence: s, Time: p.Time(0, "time"), Latitude: p.LatLong(1, 2, "latitude"), Longitude: p.LatLong(3, 4, "longitude"), Mode: p.EnumChars(5, "mode", NoFixGNS, AutonomousGNS, DifferentialGNS, PreciseGNS, RealTimeKinematicGNS, FloatRTKGNS, EstimatedGNS, ManualGNS, SimulatorGNS), SVs: p.Int64(6, "SVs"), HDOP: p.Float64(7, "HDOP"), Altitude: p.Float64(8, "altitude"), Separation: p.Float64(9, "separation"), Age: p.Float64(10, "age"), Station: p.Int64(11, "station"), } return m, p.Err() } go-nmea-1.4.0/gns_test.go000066400000000000000000000036121411662144700152240ustar00rootroot00000000000000package nmea import ( "testing" "github.com/stretchr/testify/assert" ) var gnstests = []struct { name string raw string err string msg GNS }{ { name: "good sentence A", raw: "$GNGNS,014035.00,4332.69262,S,17235.48549,E,RR,13,0.9,25.63,11.24,,*70", msg: GNS{ Time: Time{true, 1, 40, 35, 0}, Latitude: MustParseGPS("4332.69262 S"), Longitude: MustParseGPS("17235.48549 E"), Mode: []string{"R", "R"}, SVs: 13, HDOP: 0.9, Altitude: 25.63, Separation: 11.24, Age: 0, Station: 0, }, }, { name: "good sentence B", raw: "$GNGNS,094821.0,4849.931307,N,00216.053323,E,AA,14,0.6,161.5,48.0,,*6D", msg: GNS{ Time: Time{true, 9, 48, 21, 0}, Latitude: MustParseGPS("4849.931307 N"), Longitude: MustParseGPS("00216.053323 E"), Mode: []string{"A", "A"}, SVs: 14, HDOP: 0.6, Altitude: 161.5, Separation: 48.0, Age: 0, Station: 0, }, }, { name: "good sentence B", raw: "$GNGNS,094821.0,4849.931307,N,00216.053323,E,AAN,14,0.6,161.5,48.0,,*23", msg: GNS{ Time: Time{true, 9, 48, 21, 0}, Latitude: MustParseGPS("4849.931307 N"), Longitude: MustParseGPS("00216.053323 E"), Mode: []string{"A", "A", "N"}, SVs: 14, HDOP: 0.6, Altitude: 161.5, Separation: 48.0, Age: 0, Station: 0, }, }, { name: "bad sentence", raw: "$GNGNS,094821.0,4849.931307,N,00216.053323,E,AAX,14,0.6,161.5,48.0,,*35", err: "nmea: GNGNS invalid mode: AAX", }, } func TestGNS(t *testing.T) { for _, tt := range gnstests { 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) gns := m.(GNS) gns.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, gns) } }) } } go-nmea-1.4.0/go.mod000066400000000000000000000002141411662144700141500ustar00rootroot00000000000000module github.com/adrianmo/go-nmea go 1.14 require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/stretchr/testify v1.5.1 ) go-nmea-1.4.0/go.sum000066400000000000000000000020411411662144700141750ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= go-nmea-1.4.0/gsa.go000066400000000000000000000024341411662144700141510ustar00rootroot00000000000000package nmea const ( // TypeGSA type for GSA sentences TypeGSA = "GSA" // Auto - Field 1, auto or manual fix. Auto = "A" // Manual - Field 1, auto or manual fix. Manual = "M" // FixNone - Field 2, fix type. FixNone = "1" // Fix2D - Field 2, fix type. Fix2D = "2" // Fix3D - Field 2, fix type. Fix3D = "3" ) // GSA represents overview satellite data. // http://aprs.gids.nl/nmea/#gsa type GSA struct { BaseSentence Mode string // The selection mode. FixType string // The fix type. SV []string // List of satellite PRNs used for this fix. PDOP float64 // Dilution of precision. HDOP float64 // Horizontal dilution of precision. VDOP float64 // Vertical dilution of precision. } // newGSA parses the GSA sentence into this struct. func newGSA(s BaseSentence) (GSA, error) { p := NewParser(s) p.AssertType(TypeGSA) m := GSA{ BaseSentence: s, Mode: p.EnumString(0, "selection mode", Auto, Manual), FixType: p.EnumString(1, "fix type", FixNone, Fix2D, Fix3D), } // Satellites in view. for i := 2; i < 14; i++ { if v := p.String(i, "satellite in view"); v != "" { m.SV = append(m.SV, v) } } // Dilution of precision. m.PDOP = p.Float64(14, "pdop") m.HDOP = p.Float64(15, "hdop") m.VDOP = p.Float64(16, "vdop") return m, p.Err() } go-nmea-1.4.0/gsa_test.go000066400000000000000000000020041411662144700152010ustar00rootroot00000000000000package nmea import ( "testing" "github.com/stretchr/testify/assert" ) var gsatests = []struct { name string raw string err string msg GSA }{ { name: "good sentence", raw: "$GPGSA,A,3,22,19,18,27,14,03,,,,,,,3.1,2.0,2.4*36", msg: GSA{ Mode: "A", FixType: "3", SV: []string{"22", "19", "18", "27", "14", "03"}, PDOP: 3.1, HDOP: 2, VDOP: 2.4, }, }, { name: "bad mode", raw: "$GPGSA,F,3,22,19,18,27,14,03,,,,,,,3.1,2.0,2.4*31", err: "nmea: GPGSA invalid selection mode: F", }, { name: "bad fix", raw: "$GPGSA,A,6,22,19,18,27,14,03,,,,,,,3.1,2.0,2.4*33", err: "nmea: GPGSA invalid fix type: 6", }, } func TestGSA(t *testing.T) { for _, tt := range gsatests { 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) gsa := m.(GSA) gsa.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, gsa) } }) } } go-nmea-1.4.0/gsv.go000066400000000000000000000025661411662144700142040ustar00rootroot00000000000000package nmea const ( // TypeGSV type for GSV sentences TypeGSV = "GSV" ) // GSV represents the GPS Satellites in view // http://aprs.gids.nl/nmea/#glgsv type GSV struct { BaseSentence TotalMessages int64 // Total number of messages of this type in this cycle MessageNumber int64 // Message number NumberSVsInView int64 // Total number of SVs in view Info []GSVInfo // visible satellite info (0-4 of these) } // GSVInfo represents information about a visible satellite type GSVInfo struct { SVPRNNumber int64 // SV PRN number, pseudo-random noise or gold code Elevation int64 // Elevation in degrees, 90 maximum Azimuth int64 // Azimuth, degrees from true north, 000 to 359 SNR int64 // SNR, 00-99 dB (null when not tracking) } // newGSV constructor func newGSV(s BaseSentence) (GSV, error) { p := NewParser(s) p.AssertType(TypeGSV) m := GSV{ BaseSentence: s, TotalMessages: p.Int64(0, "total number of messages"), MessageNumber: p.Int64(1, "message number"), NumberSVsInView: p.Int64(2, "number of SVs in view"), } for i := 0; i < 4; i++ { if 5*i+4 > len(m.Fields) { break } m.Info = append(m.Info, GSVInfo{ SVPRNNumber: p.Int64(3+i*4, "SV prn number"), Elevation: p.Int64(4+i*4, "elevation"), Azimuth: p.Int64(5+i*4, "azimuth"), SNR: p.Int64(6+i*4, "SNR"), }) } return m, p.Err() } go-nmea-1.4.0/gsv_test.go000066400000000000000000000107311411662144700152340ustar00rootroot00000000000000package nmea import ( "testing" "github.com/stretchr/testify/assert" ) var gsvtests = []struct { name string raw string err string msg GSV }{ { name: "good sentence", raw: "$GLGSV,3,1,11,03,03,111,00,04,15,270,00,06,01,010,12,13,06,292,00*6B", msg: GSV{ TotalMessages: 3, MessageNumber: 1, NumberSVsInView: 11, Info: []GSVInfo{ {SVPRNNumber: 3, Elevation: 3, Azimuth: 111, SNR: 0}, {SVPRNNumber: 4, Elevation: 15, Azimuth: 270, SNR: 0}, {SVPRNNumber: 6, Elevation: 1, Azimuth: 10, SNR: 12}, {SVPRNNumber: 13, Elevation: 6, Azimuth: 292, SNR: 0}, }, }, }, { name: "short sentence", raw: "$GLGSV,3,1,11,03,03,111,00,04,15,270,00,06,01,010,12*56", msg: GSV{ TotalMessages: 3, MessageNumber: 1, NumberSVsInView: 11, Info: []GSVInfo{ {SVPRNNumber: 3, Elevation: 3, Azimuth: 111, SNR: 0}, {SVPRNNumber: 4, Elevation: 15, Azimuth: 270, SNR: 0}, {SVPRNNumber: 6, Elevation: 1, Azimuth: 10, SNR: 12}, }, }, }, { name: "invalid number of svs", raw: "$GLGSV,3,1,11.2,03,03,111,00,04,15,270,00,06,01,010,12,13,06,292,00*77", err: "nmea: GLGSV invalid number of SVs in view: 11.2", }, { name: "invalid number of messages", raw: "$GLGSV,A3,1,11,03,03,111,00,04,15,270,00,06,01,010,12,13,06,292,00*2A", err: "nmea: GLGSV invalid total number of messages: A3", }, { name: "invalid message number", raw: "$GLGSV,3,A1,11,03,03,111,00,04,15,270,00,06,01,010,12,13,06,292,00*2A", err: "nmea: GLGSV invalid message number: A1", }, { name: "invalid SV prn number", raw: "$GLGSV,3,1,11,A03,03,111,00,04,15,270,00,06,01,010,12,13,06,292,00*2A", err: "nmea: GLGSV invalid SV prn number: A03", }, { name: "invalid elevation", raw: "$GLGSV,3,1,11,03,A03,111,00,04,15,270,00,06,01,010,12,13,06,292,00*2A", err: "nmea: GLGSV invalid elevation: A03", }, { name: "invalid azimuth", raw: "$GLGSV,3,1,11,03,03,A111,00,04,15,270,00,06,01,010,12,13,06,292,00*2A", err: "nmea: GLGSV invalid azimuth: A111", }, { name: "invalid SNR", raw: "$GLGSV,3,1,11,03,03,111,A00,04,15,270,00,06,01,010,12,13,06,292,00*2A", err: "nmea: GLGSV invalid SNR: A00", }, { name: "good sentence", raw: "$GPGSV,3,1,11,03,03,111,00,04,15,270,00,06,01,010,12,13,06,292,00*77", msg: GSV{ TotalMessages: 3, MessageNumber: 1, NumberSVsInView: 11, Info: []GSVInfo{ {SVPRNNumber: 3, Elevation: 3, Azimuth: 111, SNR: 0}, {SVPRNNumber: 4, Elevation: 15, Azimuth: 270, SNR: 0}, {SVPRNNumber: 6, Elevation: 1, Azimuth: 10, SNR: 12}, {SVPRNNumber: 13, Elevation: 6, Azimuth: 292, SNR: 0}, }, }, }, { name: "short", raw: "$GPGSV,3,1,11,03,03,111,00,04,15,270,00,06,01,010,12*4A", msg: GSV{ TotalMessages: 3, MessageNumber: 1, NumberSVsInView: 11, Info: []GSVInfo{ {SVPRNNumber: 3, Elevation: 3, Azimuth: 111, SNR: 0}, {SVPRNNumber: 4, Elevation: 15, Azimuth: 270, SNR: 0}, {SVPRNNumber: 6, Elevation: 1, Azimuth: 10, SNR: 12}, }, }, }, { name: "invalid number of SVs", raw: "$GPGSV,3,1,11.2,03,03,111,00,04,15,270,00,06,01,010,12,13,06,292,00*6b", err: "nmea: GPGSV invalid number of SVs in view: 11.2", }, { name: "invalid total number of messages", raw: "$GPGSV,A3,1,11,03,03,111,00,04,15,270,00,06,01,010,12,13,06,292,00*36", err: "nmea: GPGSV invalid total number of messages: A3", }, { name: "invalid message number", raw: "$GPGSV,3,A1,11,03,03,111,00,04,15,270,00,06,01,010,12,13,06,292,00*36", err: "nmea: GPGSV invalid message number: A1", }, { name: "invalid SV prn number", raw: "$GPGSV,3,1,11,A03,03,111,00,04,15,270,00,06,01,010,12,13,06,292,00*36", err: "nmea: GPGSV invalid SV prn number: A03", }, { name: "invalid elevation", raw: "$GPGSV,3,1,11,03,A03,111,00,04,15,270,00,06,01,010,12,13,06,292,00*36", err: "nmea: GPGSV invalid elevation: A03", }, { name: "invalid azimuth", raw: "$GPGSV,3,1,11,03,03,A111,00,04,15,270,00,06,01,010,12,13,06,292,00*36", err: "nmea: GPGSV invalid azimuth: A111", }, { name: "invalid SNR", raw: "$GPGSV,3,1,11,03,03,111,A00,04,15,270,00,06,01,010,12,13,06,292,00*36", err: "nmea: GPGSV invalid SNR: A00", }, } func TestGSV(t *testing.T) { for _, tt := range gsvtests { 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) gsv := m.(GSV) gsv.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, gsv) } }) } } go-nmea-1.4.0/hdt.go000066400000000000000000000010421411662144700141500ustar00rootroot00000000000000package nmea const ( // TypeHDT type for HDT sentences TypeHDT = "HDT" ) // HDT is the Actual vessel heading in degrees True. // http://aprs.gids.nl/nmea/#hdt type HDT struct { BaseSentence Heading float64 // Heading in degrees True bool // Heading is relative to true north } // newHDT constructor func newHDT(s BaseSentence) (HDT, error) { p := NewParser(s) p.AssertType(TypeHDT) m := HDT{ BaseSentence: s, Heading: p.Float64(0, "heading"), True: p.EnumString(1, "true", "T") == "T", } return m, p.Err() } go-nmea-1.4.0/hdt_test.go000066400000000000000000000015031411662144700152110ustar00rootroot00000000000000package nmea import ( "testing" "github.com/stretchr/testify/assert" ) var hdttests = []struct { name string raw string err string msg HDT }{ { name: "good sentence", raw: "$GPHDT,123.456,T*32", msg: HDT{ Heading: 123.456, True: true, }, }, { name: "invalid True", raw: "$GPHDT,123.456,X*3E", err: "nmea: GPHDT invalid true: X", }, { name: "invalid Heading", raw: "$GPHDT,XXX,T*43", err: "nmea: GPHDT invalid heading: XXX", }, } func TestHDT(t *testing.T) { for _, tt := range hdttests { 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) hdt := m.(HDT) hdt.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, hdt) } }) } } go-nmea-1.4.0/mda.go000066400000000000000000000077751411662144700141550ustar00rootroot00000000000000package nmea /** $WIMDA NMEA 0183 standard Meteorological Composite. Syntax $WIMDA,<1>,<2>,<3>,<4>,<5>,<6>,<7>,<8>,<9>,<10>,<11>, <12>,<13>,<14>,<15>,<16>,<17>,<18>,<19>,<20>*hh Fields <1> Barometric pressure, inches of mercury, to the nearest 0.01 inch <2> I = inches of mercury <3> Barometric pressure, bars, to the nearest .001 bar <4> B = bars <5> Air temperature, degrees C, to the nearest 0.1 degree C <6> C = degrees C <7> Water temperature, degrees C (this field left blank by some WeatherStations) <8> C = degrees C (this field left blank by WeatherStation) <9> Relative humidity, percent, to the nearest 0.1 percent <10> Absolute humidity, percent (this field left blank by some WeatherStations) <11> Dew point, degrees C, to the nearest 0.1 degree C <12> C = degrees C <13> Wind direction, degrees True, to the nearest 0.1 degree <14> T = true <15> Wind direction, degrees Magnetic, to the nearest 0.1 degree <16> M = magnetic <17> Wind speed, knots, to the nearest 0.1 knot <18> N = knots <19> Wind speed, meters per second, to the nearest 0.1 m/s <20> M = meters per second */ const ( // TypeMDA type for MDA sentences TypeMDA = "MDA" // InchMDA for valid pressure in Inches of mercury InchMDA = "I" // BarsMDA for valid pressure in Bars BarsMDA = "B" // DegreesCMDA for valid data in degrees C DegreesCMDA = "C" // TrueMDA for valid data in True direction TrueMDA = "T" // MagneticMDA for valid data in Magnetic direction MagneticMDA = "M" // KnotsMDA for valid data in Knots KnotsMDA = "N" // MetersSecondMDA for valid data in Meters per Second MetersSecondMDA = "M" ) // MDA is the Meteorological Composite // Data of air pressure, air and water temperatures and wind speed and direction type MDA struct { BaseSentence PressureInch float64 InchesValid bool // I PressureBar float64 BarsValid bool // B AirTemp float64 AirTempValid bool // C or empty if no data WaterTemp float64 WaterTempValid bool // C or empty if no data RelativeHum float64 // percent to .1 AbsoluteHum float64 // percent to .1 DewPoint float64 DewPointValid bool // C or empty if no data WindDirectionTrue float64 TrueValid bool // T WindDirectionMagnetic float64 MagneticValid bool // M WindSpeedKnots float64 KnotsValid bool // N WindSpeedMeters float64 MetersValid bool // M } func newMDA(s BaseSentence) (MDA, error) { p := NewParser(s) p.AssertType(TypeMDA) return MDA{ BaseSentence: s, PressureInch: p.Float64(0, "pressure in inch"), InchesValid: p.EnumString(1, "inches valid", InchMDA) == InchMDA, PressureBar: p.Float64(2, "pressure in bar"), BarsValid: p.EnumString(3, "bars valid", BarsMDA) == BarsMDA, AirTemp: p.Float64(4, "air temp"), AirTempValid: p.EnumString(5, "air temp valid", DegreesCMDA) == DegreesCMDA, WaterTemp: p.Float64(6, "water temp"), WaterTempValid: p.EnumString(7, "water temp valid", DegreesCMDA) == DegreesCMDA, RelativeHum: p.Float64(8, "relative humidity"), AbsoluteHum: p.Float64(9, "absolute humidity"), DewPoint: p.Float64(10, "dewpoint"), DewPointValid: p.EnumString(11, "dewpoint valid", DegreesCMDA) == DegreesCMDA, WindDirectionTrue: p.Float64(12, "wind direction true"), TrueValid: p.EnumString(13, "wind direction true valid", TrueMDA) == TrueMDA, WindDirectionMagnetic: p.Float64(14, "wind direction magnetic"), MagneticValid: p.EnumString(15, "wind direction magnetic valid", MagneticMDA) == MagneticMDA, WindSpeedKnots: p.Float64(16, "windspeed knots"), KnotsValid: p.EnumString(17, "windspeed knots valid", KnotsMDA) == KnotsMDA, WindSpeedMeters: p.Float64(18, "windspeed m/s"), MetersValid: p.EnumString(19, "windspeed m/s valid", MetersSecondMDA) == MetersSecondMDA, }, p.Err() } go-nmea-1.4.0/mda_test.go000066400000000000000000000023741411662144700152020ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) var mdatests = []struct { name string raw string err string msg MDA }{ { name: "good sentence", raw: "$WIMDA,3.02,I,1.01,B,23.4,C,,,40.2,,12.1,C,19.3,T,20.1,M,13.1,N,1.1,M*62", msg: MDA{ PressureInch: 3.02, InchesValid: true, PressureBar: 1.01, BarsValid: true, AirTemp: 23.4, AirTempValid: true, WaterTemp: 0, WaterTempValid: false, RelativeHum: 40.2, AbsoluteHum: 0, DewPoint: 12.1, DewPointValid: true, WindDirectionTrue: 19.3, TrueValid: true, WindDirectionMagnetic: 20.1, MagneticValid: true, WindSpeedKnots: 13.1, KnotsValid: true, WindSpeedMeters: 1.1, MetersValid: true, }, }, } func TestMDA(t *testing.T) { for _, tt := range mdatests { 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) mda := m.(MDA) mda.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, mda) } }) } } go-nmea-1.4.0/mtk.go000066400000000000000000000006571411662144700141770ustar00rootroot00000000000000package 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() } go-nmea-1.4.0/mtk_test.go000066400000000000000000000016441411662144700152330ustar00rootroot00000000000000package 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) } }) } } go-nmea-1.4.0/must_test.go000066400000000000000000000021321411662144700154210ustar00rootroot00000000000000package nmea // MustParseLatLong parses the supplied string into the LatLong. // It panics if an error is encountered func MustParseLatLong(s string) float64 { l, err := ParseLatLong(s) if err != nil { panic(err) } return l } // MustParseGPS parses a GPS/NMEA coordinate or panics if it fails. func MustParseGPS(s string) float64 { l, err := ParseGPS(s) if err != nil { panic(err) } return l } // MustParseDMS parses a coordinate in degrees, minutes, seconds and // panics on failure func MustParseDMS(s string) float64 { l, err := ParseDMS(s) if err != nil { panic(err) } return l } // ParseDecimal parses a decimal format coordinate and panics on error. func MustParseDecimal(s string) float64 { l, err := ParseDecimal(s) if err != nil { panic(err) } return l } // MustParseTime parses wall clock and panics on failure func MustParseTime(s string) Time { t, err := ParseTime(s) if err != nil { panic(err) } return t } // MustParseDate parses a date and panics on failure func MustParseDate(s string) Date { d, err := ParseDate(s) if err != nil { panic(err) } return d } go-nmea-1.4.0/mwd.go000066400000000000000000000035301411662144700141640ustar00rootroot00000000000000package nmea /** $WIMWD NMEA 0183 standard Wind Direction and Speed, with respect to north. Syntax $WIMWD,<1>,<2>,<3>,<4>,<5>,<6>,<7>,<8>*hh Fields <1> Wind direction, 0.0 to 359.9 degrees True, to the nearest 0.1 degree <2> T = True <3> Wind direction, 0.0 to 359.9 degrees Magnetic, to the nearest 0.1 degree <4> M = Magnetic <5> Wind speed, knots, to the nearest 0.1 knot. <6> N = Knots <7> Wind speed, meters/second, to the nearest 0.1 m/s. <8> M = Meters/second */ const ( // TypeMWD type for MWD sentences TypeMWD = "MWD" // TrueMWD for valid True Direction TrueMWD = "T" // MagneticMWD for valid Magnetic direction MagneticMWD = "M" // KnotsMWD for valid Knots KnotsMWD = "N" // MetersSecondMWD for valid Meters per Second MetersSecondMWD = "M" ) // MWD Wind Direction and Speed, with respect to north. type MWD struct { BaseSentence WindDirectionTrue float64 TrueValid bool WindDirectionMagnetic float64 MagneticValid bool WindSpeedKnots float64 KnotsValid bool WindSpeedMeters float64 MetersValid bool } func newMWD(s BaseSentence) (MWD, error) { p := NewParser(s) p.AssertType(TypeMWD) return MWD{ BaseSentence: s, WindDirectionTrue: p.Float64(0, "true wind direction"), TrueValid: p.EnumString(1, "true wind valid", TrueMWD) == TrueMWD, WindDirectionMagnetic: p.Float64(2, "magnetic wind direction"), MagneticValid: p.EnumString(3, "magnetic direction valid", MagneticMWD) == MagneticMWD, WindSpeedKnots: p.Float64(4, "windspeed knots"), KnotsValid: p.EnumString(5, "windspeed knots valid", KnotsMWD) == KnotsMWD, WindSpeedMeters: p.Float64(6, "windspeed m/s"), MetersValid: p.EnumString(7, "windspeed m/s valid", MetersSecondMWD) == MetersSecondMWD, }, p.Err() } go-nmea-1.4.0/mwd_test.go000066400000000000000000000022321411662144700152210ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) var mwdtests = []struct { name string raw string err string msg MWD }{ { name: "good sentence", raw: "$WIMWD,10.1,T,10.1,M,12,N,40,M*5D", msg: MWD{ WindDirectionTrue: 10.1, TrueValid: true, WindDirectionMagnetic: 10.1, MagneticValid: true, WindSpeedKnots: 12, KnotsValid: true, WindSpeedMeters: 40, MetersValid: true, }, }, { name: "empty data", raw: "$WIMWD,,,,,,,,*40", msg: MWD{ WindDirectionTrue: 0, TrueValid: false, WindDirectionMagnetic: 0, MagneticValid: false, WindSpeedKnots: 0, KnotsValid: false, WindSpeedMeters: 0, MetersValid: false, }, }, } func TestMWD(t *testing.T) { for _, tt := range mwdtests { 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) mwd := m.(MWD) mwd.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, mwd) } }) } } go-nmea-1.4.0/mwv.go000066400000000000000000000042341411662144700142100ustar00rootroot00000000000000package nmea /** $WIMWV NMEA 0183 standard Wind Speed and Angle, in relation to the vessel’s bow/centerline. Syntax $WIMWV,<1>,<2>,<3>,<4>,<5>*hh Fields <1> Wind angle, 0.0 to 359.9 degrees, in relation to the vessel’s bow/centerline, to the nearest 0.1 degree. If the data for this field is not valid, the field will be blank. <2> Reference: R = Relative (apparent wind, as felt when standing on the moving ship) T = Theoretical (calculated actual wind, as though the vessel were stationary) <3> Wind speed, to the nearest tenth of a unit. If the data for this field is not valid, the field will be blank. <4> Wind speed units: K = km/hr M = m/s N = knots S = statute miles/hr (Most WeatherStations will commonly use "N" (knots)) <5> Status: A = data valid; V = data invalid */ const ( // TypeMWV type for MWV sentences TypeMWV = "MWV" // RelativeMWV for Valid Relative angle data RelativeMWV = "R" // TheoreticalMWV for valid Theoretical angle data TheoreticalMWV = "T" // UnitKMHMWV unit for Kilometer per hour (KM/H) UnitKMHMWV = "K" // KM/H // UnitMSMWV unit for Meters per second (M/S) UnitMSMWV = "M" // M/S // UnitKnotsMWV unit for knots UnitKnotsMWV = "N" // knots // UnitSMilesHMWV unit for Miles per hour (M/H) UnitSMilesHMWV = "S" // ValidMWV data is valid ValidMWV = "A" // InvalidMWV data is invalid InvalidMWV = "V" ) // MWV is the Wind Speed and Angle, in relation to the vessel’s bow/centerline. type MWV struct { BaseSentence WindAngle float64 Reference string WindSpeed float64 WindSpeedUnit string StatusValid bool } func newMWV(s BaseSentence) (MWV, error) { p := NewParser(s) p.AssertType(TypeMWV) return MWV{ BaseSentence: s, WindAngle: p.Float64(0, "wind angle"), Reference: p.EnumString(1, "reference", RelativeMWV, TheoreticalMWV), WindSpeed: p.Float64(2, "wind speed"), WindSpeedUnit: p.EnumString(3, "wind speed unit", UnitKMHMWV, UnitMSMWV, UnitKnotsMWV, UnitSMilesHMWV), StatusValid: p.EnumString(4, "status", ValidMWV, InvalidMWV) == ValidMWV, }, p.Err() } go-nmea-1.4.0/mwv_test.go000066400000000000000000000016071411662144700152500ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) var mwvtests = []struct { name string raw string err string msg MWV }{ { name: "good sentence", raw: "$WIMWV,12.1,T,10.1,N,A*27", msg: MWV{ WindAngle: 12.1, Reference: "T", WindSpeed: 10.1, WindSpeedUnit: "N", StatusValid: true, }, }, { name: "invalid data", raw: "$WIMWV,,T,,N,V*32", msg: MWV{ WindAngle: 0, Reference: "T", WindSpeed: 0, WindSpeedUnit: "N", StatusValid: false, }, }, } func TestMWV(t *testing.T) { for _, tt := range mwvtests { 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) mwv := m.(MWV) mwv.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, mwv) } }) } } go-nmea-1.4.0/parser.go000066400000000000000000000120671411662144700146760ustar00rootroot00000000000000package nmea import ( "fmt" "strconv" ) // Parser provides a simple way of accessing and parsing // sentence fields type Parser struct { BaseSentence err error } // NewParser constructor func NewParser(s BaseSentence) *Parser { return &Parser{BaseSentence: s} } // AssertType makes sure the sentence's type matches the provided one. func (p *Parser) AssertType(typ string) { if p.Type != typ { p.SetErr("type", p.Type) } } // Err returns the first error encountered during the parser's usage. func (p *Parser) Err() error { return p.err } // SetErr assigns an error. Calling this method has no // effect if there is already an error. func (p *Parser) SetErr(context, value string) { if p.err == nil { p.err = fmt.Errorf("nmea: %s invalid %s: %s", p.Prefix(), context, value) } } // String returns the field value at the specified index. func (p *Parser) String(i int, context string) string { if p.err != nil { return "" } if i < 0 || i >= len(p.Fields) { p.SetErr(context, "index out of range") return "" } return p.Fields[i] } // ListString returns a list of all fields from the given start index. // An error occurs if there is no fields after the given start index. func (p *Parser) ListString(from int, context string) (list []string) { if p.err != nil { return []string{} } if from < 0 || from >= len(p.Fields) { p.SetErr(context, "index out of range") return []string{} } return append(list, p.Fields[from:]...) } // EnumString returns the field value at the specified index. // An error occurs if the value is not one of the options and not empty. func (p *Parser) EnumString(i int, context string, options ...string) string { s := p.String(i, context) if p.err != nil || s == "" { return "" } for _, o := range options { if o == s { return s } } p.SetErr(context, s) return "" } // EnumChars returns an array of strings that are matched in the Mode field. // It will only match the number of characters that are in the Mode field. // If the value is empty, it will return an empty array func (p *Parser) EnumChars(i int, context string, options ...string) []string { s := p.String(i, context) if p.err != nil || s == "" { return []string{} } strs := []string{} for _, r := range s { rs := string(r) for _, o := range options { if o == rs { strs = append(strs, o) break } } } if len(strs) != len(s) { p.SetErr(context, s) return []string{} } return strs } // Int64 returns the int64 value at the specified index. // If the value is an empty string, 0 is returned. func (p *Parser) Int64(i int, context string) int64 { s := p.String(i, context) if p.err != nil { return 0 } if s == "" { return 0 } v, err := strconv.ParseInt(s, 10, 64) if err != nil { p.SetErr(context, s) } return v } // Float64 returns the float64 value at the specified index. // If the value is an empty string, 0 is returned. func (p *Parser) Float64(i int, context string) float64 { s := p.String(i, context) if p.err != nil { return 0 } if s == "" { return 0 } v, err := strconv.ParseFloat(s, 64) if err != nil { p.SetErr(context, s) } return v } // Time returns the Time value at the specified index. // If the value is empty, the Time is marked as invalid. func (p *Parser) Time(i int, context string) Time { s := p.String(i, context) if p.err != nil { return Time{} } v, err := ParseTime(s) if err != nil { p.SetErr(context, s) } return v } // Date returns the Date value at the specified index. // If the value is empty, the Date is marked as invalid. func (p *Parser) Date(i int, context string) Date { s := p.String(i, context) if p.err != nil { return Date{} } v, err := ParseDate(s) if err != nil { p.SetErr(context, s) } return v } // LatLong returns the coordinate value of the specified fields. func (p *Parser) LatLong(i, j int, context string) float64 { a := p.String(i, context) b := p.String(j, context) if p.err != nil { return 0 } s := fmt.Sprintf("%s %s", a, b) v, err := ParseLatLong(s) 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 } // SixBitASCIIArmour decodes the 6-bit ascii armor used for VDM and VDO messages func (p *Parser) SixBitASCIIArmour(i int, fillBits int, context string) []byte { if p.err != nil { return nil } if fillBits < 0 || fillBits >= 6 { p.SetErr(context, "fill bits") return nil } payload := []byte(p.String(i, "encoded payload")) numBits := len(payload)*6 - fillBits if numBits < 0 { p.SetErr(context, "num bits") return nil } result := make([]byte, numBits) resultIndex := 0 for _, v := range payload { if v < 48 || v >= 120 { p.SetErr(context, "data byte") return nil } d := v - 48 if d > 40 { d -= 8 } for i := 5; i >= 0 && resultIndex < len(result); i-- { result[resultIndex] = (d >> uint(i)) & 1 resultIndex++ } } return result } go-nmea-1.4.0/parser_test.go000066400000000000000000000151441411662144700157340ustar00rootroot00000000000000package nmea import ( "testing" "github.com/stretchr/testify/assert" ) var parsertests = []struct { name string fields []string expected interface{} hasErr bool parse func(p *Parser) interface{} }{ { name: "Bad Type", fields: []string{}, hasErr: true, parse: func(p *Parser) interface{} { p.AssertType("WRONG_TYPE") return nil }, }, { name: "String", fields: []string{"foo", "bar"}, expected: "bar", parse: func(p *Parser) interface{} { return p.String(1, "") }, }, { name: "String out of range", fields: []string{"wot"}, expected: "", hasErr: true, parse: func(p *Parser) interface{} { return p.String(5, "thing") }, }, { name: "ListString", fields: []string{"wot", "foo", "bar"}, expected: []string{"foo", "bar"}, parse: func(p *Parser) interface{} { return p.ListString(1, "thing") }, }, { name: "ListString out of range", fields: []string{"wot"}, expected: []string{}, hasErr: true, parse: func(p *Parser) interface{} { return p.ListString(10, "thing") }, }, { name: "String with existing error", expected: "", hasErr: true, parse: func(p *Parser) interface{} { p.SetErr("context", "value") return p.String(123, "blah") }, }, { name: "EnumString", fields: []string{"a", "b", "c"}, expected: "b", parse: func(p *Parser) interface{} { return p.EnumString(1, "context", "b", "d") }, }, { name: "EnumString invalid", fields: []string{"a", "b", "c"}, expected: "", hasErr: true, parse: func(p *Parser) interface{} { return p.EnumString(1, "context", "x", "y") }, }, { name: "EnumString with existing error", fields: []string{"a", "b", "c"}, expected: "", hasErr: true, parse: func(p *Parser) interface{} { p.SetErr("context", "value") return p.EnumString(1, "context", "a", "b") }, }, { name: "EnumChars", fields: []string{"AA", "AB", "BA", "BB"}, expected: []string{"A", "B"}, parse: func(p *Parser) interface{} { return p.EnumChars(1, "context", "A", "B") }, }, { name: "EnumChars invalid", fields: []string{"a", "AB", "c"}, expected: []string{}, hasErr: true, parse: func(p *Parser) interface{} { return p.EnumChars(1, "context", "X", "Y") }, }, { name: "EnumChars with existing error", fields: []string{"a", "AB", "c"}, expected: []string{}, hasErr: true, parse: func(p *Parser) interface{} { p.SetErr("context", "value") return p.EnumChars(1, "context", "A", "B") }, }, { name: "Int64", fields: []string{"123"}, expected: int64(123), parse: func(p *Parser) interface{} { return p.Int64(0, "context") }, }, { name: "Int64 empty field is zero", fields: []string{""}, expected: int64(0), parse: func(p *Parser) interface{} { return p.Int64(0, "context") }, }, { name: "Int64 invalid", fields: []string{"abc"}, expected: int64(0), hasErr: true, parse: func(p *Parser) interface{} { return p.Int64(0, "context") }, }, { name: "Int64 with existing error", fields: []string{"123"}, expected: int64(0), hasErr: true, parse: func(p *Parser) interface{} { p.SetErr("context", "value") return p.Int64(0, "context") }, }, { name: "Float64", fields: []string{"123.123"}, expected: float64(123.123), parse: func(p *Parser) interface{} { return p.Float64(0, "context") }, }, { name: "Float64 empty field is zero", fields: []string{""}, expected: float64(0), parse: func(p *Parser) interface{} { return p.Float64(0, "context") }, }, { name: "Float64 invalid", fields: []string{"abc"}, expected: float64(0), hasErr: true, parse: func(p *Parser) interface{} { return p.Float64(0, "context") }, }, { name: "Float64 with existing error", fields: []string{"123.123"}, expected: float64(0), hasErr: true, parse: func(p *Parser) interface{} { p.SetErr("context", "value") return p.Float64(0, "context") }, }, { name: "Time", fields: []string{"123456"}, expected: Time{true, 12, 34, 56, 0}, parse: func(p *Parser) interface{} { return p.Time(0, "context") }, }, { name: "Time empty field is zero", fields: []string{""}, expected: Time{}, parse: func(p *Parser) interface{} { return p.Time(0, "context") }, }, { name: "Time with existing error", fields: []string{"123456"}, expected: Time{}, hasErr: true, parse: func(p *Parser) interface{} { p.SetErr("context", "value") return p.Time(0, "context") }, }, { name: "Time invalid", fields: []string{"wrong"}, expected: Time{}, hasErr: true, parse: func(p *Parser) interface{} { return p.Time(0, "context") }, }, { name: "Date", fields: []string{"010203"}, expected: Date{true, 1, 2, 3}, parse: func(p *Parser) interface{} { return p.Date(0, "context") }, }, { name: "Date empty field is zero", fields: []string{""}, expected: Date{}, parse: func(p *Parser) interface{} { return p.Date(0, "context") }, }, { name: "Date invalid", fields: []string{"Hello"}, expected: Date{}, hasErr: true, parse: func(p *Parser) interface{} { return p.Date(0, "context") }, }, { name: "Date with existing error", fields: []string{"010203"}, expected: Date{}, hasErr: true, parse: func(p *Parser) interface{} { p.SetErr("context", "value") 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) { for _, tt := range parsertests { t.Run(tt.name, func(t *testing.T) { p := NewParser(BaseSentence{ Talker: "talker", Type: "type", Fields: tt.fields, }) assert.Equal(t, tt.expected, tt.parse(p)) if tt.hasErr { assert.Error(t, p.Err()) } else { assert.NoError(t, p.Err()) } }) } } go-nmea-1.4.0/pgrme.go000066400000000000000000000020301411662144700145010ustar00rootroot00000000000000package nmea const ( // TypePGRME type for PGRME sentences TypePGRME = "GRME" // ErrorUnit must be meters (M) ErrorUnit = "M" ) // PGRME is Estimated Position Error (Garmin proprietary sentence) // http://aprs.gids.nl/nmea/#rme type PGRME struct { BaseSentence Horizontal float64 // Estimated horizontal position error (HPE) in metres Vertical float64 // Estimated vertical position error (VPE) in metres Spherical float64 // Overall spherical equivalent position error in meters } // newPGRME constructor func newPGRME(s BaseSentence) (PGRME, error) { p := NewParser(s) p.AssertType(TypePGRME) horizontal := p.Float64(0, "horizontal error") _ = p.EnumString(1, "horizontal error unit", ErrorUnit) vertial := p.Float64(2, "vertical error") _ = p.EnumString(3, "vertical error unit", ErrorUnit) spherical := p.Float64(4, "spherical error") _ = p.EnumString(5, "spherical error unit", ErrorUnit) return PGRME{ BaseSentence: s, Horizontal: horizontal, Vertical: vertial, Spherical: spherical, }, p.Err() } go-nmea-1.4.0/pgrme_test.go000066400000000000000000000024751411662144700155550ustar00rootroot00000000000000package nmea import ( "testing" "github.com/stretchr/testify/assert" ) var pgrmetests = []struct { name string raw string err string msg PGRME }{ { name: "good sentence", raw: "$PGRME,3.3,M,4.9,M,6.0,M*25", msg: PGRME{ Horizontal: 3.3, Vertical: 4.9, Spherical: 6, }, }, { name: "invalid horizontal error", raw: "$PGRME,A,M,4.9,M,6.0,M*4A", err: "nmea: PGRME invalid horizontal error: A", }, { name: "invalid vertical error", raw: "$PGRME,3.3,M,A,M,6.0,M*47", err: "nmea: PGRME invalid vertical error: A", }, { name: "invalid vertical error unit", raw: "$PGRME,3.3,M,4.9,A,6.0,M*29", err: "nmea: PGRME invalid vertical error unit: A", }, { name: "invalid spherical error", raw: "$PGRME,3.3,M,4.9,M,A,M*4C", err: "nmea: PGRME invalid spherical error: A", }, { name: "invalid spherical error unit", raw: "$PGRME,3.3,M,4.9,M,6.0,A*29", err: "nmea: PGRME invalid spherical error unit: A", }, } func TestPGRME(t *testing.T) { for _, tt := range pgrmetests { 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) pgrme := m.(PGRME) pgrme.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, pgrme) } }) } } go-nmea-1.4.0/rmc.go000066400000000000000000000022171411662144700141570ustar00rootroot00000000000000package nmea const ( // TypeRMC type for RMC sentences TypeRMC = "RMC" // ValidRMC character ValidRMC = "A" // InvalidRMC character InvalidRMC = "V" ) // RMC is the Recommended Minimum Specific GNSS data. // http://aprs.gids.nl/nmea/#rmc type RMC struct { BaseSentence Time Time // Time Stamp Validity string // validity - A-ok, V-invalid Latitude float64 // Latitude Longitude float64 // Longitude Speed float64 // Speed in knots Course float64 // True course Date Date // Date Variation float64 // Magnetic variation } // newRMC constructor func newRMC(s BaseSentence) (RMC, error) { p := NewParser(s) p.AssertType(TypeRMC) m := RMC{ BaseSentence: s, Time: p.Time(0, "time"), Validity: p.EnumString(1, "validity", ValidRMC, InvalidRMC), Latitude: p.LatLong(2, 3, "latitude"), Longitude: p.LatLong(4, 5, "longitude"), Speed: p.Float64(6, "speed"), Course: p.Float64(7, "course"), Date: p.Date(8, "date"), Variation: p.Float64(9, "variation"), } if p.EnumString(10, "direction", West, East) == West { m.Variation = 0 - m.Variation } return m, p.Err() } go-nmea-1.4.0/rmc_test.go000066400000000000000000000051511411662144700152160ustar00rootroot00000000000000package nmea import ( "testing" "github.com/stretchr/testify/assert" ) var rmctests = []struct { name string raw string err string msg RMC }{ { name: "good sentence A", raw: "$GNRMC,220516,A,5133.82,N,00042.24,W,173.8,231.8,130694,004.2,W*6E", msg: RMC{ Time: Time{true, 22, 05, 16, 0}, Validity: "A", Speed: 173.8, Course: 231.8, Date: Date{true, 13, 06, 94}, Variation: -4.2, Latitude: MustParseGPS("5133.82 N"), Longitude: MustParseGPS("00042.24 W"), }, }, { name: "good sentence B", raw: "$GNRMC,142754.0,A,4302.539570,N,07920.379823,W,0.0,,070617,0.0,E,A*21", msg: RMC{ Time: Time{true, 14, 27, 54, 0}, Validity: "A", Speed: 0, Course: 0, Date: Date{true, 7, 6, 17}, Variation: 0, Latitude: MustParseGPS("4302.539570 N"), Longitude: MustParseGPS("07920.379823 W"), }, }, { name: "good sentence C", raw: "$GNRMC,100538.00,A,5546.27711,N,03736.91144,E,0.061,,260318,,,A*60", msg: RMC{ Time: Time{true, 10, 5, 38, 0}, Validity: "A", Speed: 0.061, Course: 0, Date: Date{true, 26, 3, 18}, Variation: 0, Latitude: MustParseGPS("5546.27711 N"), Longitude: MustParseGPS("03736.91144 E"), }, }, { name: "bad sentence", raw: "$GNRMC,220516,D,5133.82,N,00042.24,W,173.8,231.8,130694,004.2,W*6B", err: "nmea: GNRMC invalid validity: D", }, { name: "good sentence A", raw: "$GPRMC,220516,A,5133.82,N,00042.24,W,173.8,231.8,130694,004.2,W*70", msg: RMC{ Time: Time{true, 22, 5, 16, 0}, Validity: "A", Speed: 173.8, Course: 231.8, Date: Date{true, 13, 6, 94}, Variation: -4.2, Latitude: MustParseGPS("5133.82 N"), Longitude: MustParseGPS("00042.24 W"), }, }, { name: "good sentence B", raw: "$GPRMC,142754.0,A,4302.539570,N,07920.379823,W,0.0,,070617,0.0,E,A*3F", msg: RMC{ Time: Time{true, 14, 27, 54, 0}, Validity: "A", Speed: 0, Course: 0, Date: Date{true, 7, 6, 17}, Variation: 0, Latitude: MustParseGPS("4302.539570 N"), Longitude: MustParseGPS("07920.379823 W"), }, }, { name: "bad validity", raw: "$GPRMC,220516,D,5133.82,N,00042.24,W,173.8,231.8,130694,004.2,W*75", err: "nmea: GPRMC invalid validity: D", }, } func TestRMC(t *testing.T) { for _, tt := range rmctests { 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) rmc := m.(RMC) rmc.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, rmc) } }) } } go-nmea-1.4.0/rte.go000066400000000000000000000021371411662144700141710ustar00rootroot00000000000000package nmea const ( // TypeRTE type for RTE sentences TypeRTE = "RTE" // ActiveRoute active route ActiveRoute = "c" // WaypointList list containing waypoints WaypointList = "w" ) // RTE is a route of waypoints type RTE struct { BaseSentence NumberOfSentences int64 // Number of sentences in sequence SentenceNumber int64 // Sentence number ActiveRouteOrWaypointList string // Current active route or waypoint list Name string // Name or number of active route Idents []string // List of ident of waypoints } // newRTE constructor func newRTE(s BaseSentence) (RTE, error) { p := NewParser(s) p.AssertType(TypeRTE) return RTE{ BaseSentence: s, NumberOfSentences: p.Int64(0, "number of sentences"), SentenceNumber: p.Int64(1, "sentence number"), ActiveRouteOrWaypointList: p.EnumString(2, "active route or waypoint list", ActiveRoute, WaypointList), Name: p.String(3, "name or number"), Idents: p.ListString(4, "ident of waypoints"), }, p.Err() } go-nmea-1.4.0/rte_test.go000066400000000000000000000026461411662144700152350ustar00rootroot00000000000000package nmea import ( "testing" "github.com/stretchr/testify/assert" ) var rtetests = []struct { name string raw string err string msg RTE }{ { name: "good sentence", raw: "$IIRTE,4,1,c,Rte 1,411,412,413,414,415*6F", msg: RTE{ NumberOfSentences: 4, SentenceNumber: 1, ActiveRouteOrWaypointList: ActiveRoute, Name: "Rte 1", Idents: []string{"411", "412", "413", "414", "415"}, }, }, { name: "index out if range", raw: "$IIRTE,4,1,c,Rte 1*77", err: "nmea: IIRTE invalid ident of waypoints: index out of range", }, { name: "invalid number of sentences", raw: "$IIRTE,X,1,c,Rte 1,411,412,413,414,415*03", err: "nmea: IIRTE invalid number of sentences: X", }, { name: "invalid sentence number", raw: "$IIRTE,4,X,c,Rte 1,411,412,413,414,415*06", err: "nmea: IIRTE invalid sentence number: X", }, { name: "invalid active route or waypoint list", raw: "$IIRTE,4,1,X,Rte 1,411,412,413,414,415*54", err: "nmea: IIRTE invalid active route or waypoint list: X", }, } func TestRTE(t *testing.T) { for _, tt := range rtetests { 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) rte := m.(RTE) rte.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, rte) } }) } } go-nmea-1.4.0/sentence.go000066400000000000000000000125151411662144700152040ustar00rootroot00000000000000package nmea import ( "fmt" "strings" "sync" ) const ( // SentenceStart is the token to indicate the start of a sentence. SentenceStart = "$" // SentenceStartEncapsulated is the token to indicate the start of encapsulated data. SentenceStartEncapsulated = "!" // FieldSep is the token to delimit fields of a sentence. FieldSep = "," // ChecksumSep is the token to delimit the checksum of a sentence. ChecksumSep = "*" ) var ( customParsersMu = &sync.Mutex{} customParsers = map[string]ParserFunc{} ) // ParserFunc callback used to parse specific sentence variants type ParserFunc func(BaseSentence) (Sentence, error) // Sentence interface for all NMEA sentence type Sentence interface { fmt.Stringer Prefix() string DataType() string TalkerID() string } // BaseSentence contains the information about the NMEA sentence type BaseSentence struct { Talker string // The talker id (e.g GP) Type string // The data type (e.g GSA) Fields []string // Array of fields Checksum string // The Checksum Raw string // The raw NMEA sentence received TagBlock TagBlock // NMEA tagblock } // Prefix returns the talker and type of message func (s BaseSentence) Prefix() string { return s.Talker + s.Type } // DataType returns the type of the message func (s BaseSentence) DataType() string { return s.Type } // TalkerID returns the talker of the message func (s BaseSentence) TalkerID() string { return s.Talker } // String formats the sentence into a string func (s BaseSentence) String() string { return s.Raw } // parseSentence parses a raw message into it's fields func parseSentence(raw string) (BaseSentence, error) { raw = strings.TrimSpace(raw) tagBlockParts := strings.SplitN(raw, `\`, 3) var ( tagBlock TagBlock err error ) if len(tagBlockParts) == 3 { tags := tagBlockParts[1] raw = tagBlockParts[2] tagBlock, err = parseTagBlock(tags) if err != nil { return BaseSentence{}, err } } startIndex := strings.IndexAny(raw, SentenceStart+SentenceStartEncapsulated) if startIndex != 0 { return BaseSentence{}, fmt.Errorf("nmea: sentence does not start with a '$' or '!'") } sumSepIndex := strings.Index(raw, ChecksumSep) if sumSepIndex == -1 { return BaseSentence{}, fmt.Errorf("nmea: sentence does not contain checksum separator") } var ( fieldsRaw = raw[startIndex+1 : sumSepIndex] fields = strings.Split(fieldsRaw, FieldSep) checksumRaw = strings.ToUpper(raw[sumSepIndex+1:]) checksum = Checksum(fieldsRaw) ) // Validate the checksum if checksum != checksumRaw { return BaseSentence{}, fmt.Errorf( "nmea: sentence checksum mismatch [%s != %s]", checksum, checksumRaw) } talker, typ := parsePrefix(fields[0]) return BaseSentence{ Talker: talker, Type: typ, Fields: fields[1:], Checksum: checksumRaw, Raw: raw, TagBlock: tagBlock, }, nil } // 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:] } if len(s) < 2 { return s, "" } return s[:2], s[2:] } // Checksum xor all the bytes in a string an return it // as an uppercase hex string func Checksum(s string) string { var checksum uint8 for i := 0; i < len(s); i++ { checksum ^= s[i] } return fmt.Sprintf("%02X", checksum) } // MustRegisterParser register a custom parser or panic func MustRegisterParser(sentenceType string, parser ParserFunc) { if err := RegisterParser(sentenceType, parser); err != nil { panic(err) } } // RegisterParser register a custom parser func RegisterParser(sentenceType string, parser ParserFunc) error { customParsersMu.Lock() defer customParsersMu.Unlock() if _, ok := customParsers[sentenceType]; ok { return fmt.Errorf("nmea: parser for sentence type '%q' already exists", sentenceType) } customParsers[sentenceType] = parser return nil } // Parse parses the given string into the correct sentence type. func Parse(raw string) (Sentence, error) { s, err := parseSentence(raw) if err != nil { return nil, err } // Custom parser allow overriding of existing parsers if parser, ok := customParsers[s.Type]; ok { return parser(s) } 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) case TypeGGA: return newGGA(s) case TypeGSA: return newGSA(s) case TypeGLL: return newGLL(s) case TypeVTG: return newVTG(s) case TypeZDA: return newZDA(s) case TypePGRME: return newPGRME(s) case TypeGSV: return newGSV(s) case TypeHDT: return newHDT(s) case TypeGNS: return newGNS(s) case TypeTHS: return newTHS(s) case TypeWPL: return newWPL(s) case TypeRTE: return newRTE(s) case TypeVHW: return newVHW(s) case TypeDPT: return newDPT(s) case TypeDBT: return newDBT(s) case TypeDBS: return newDBS(s) case TypeMDA: return newMDA(s) case TypeMWD: return newMWD(s) case TypeMWV: return newMWV(s) } } if strings.HasPrefix(s.Raw, SentenceStartEncapsulated) { switch s.Type { case TypeVDM, TypeVDO: return newVDMVDO(s) } } return nil, fmt.Errorf("nmea: sentence prefix '%s' not supported", s.Prefix()) } go-nmea-1.4.0/sentence_customparser_test.go000066400000000000000000000064611411662144700210550ustar00rootroot00000000000000package nmea import ( "fmt" "strconv" "strings" "testing" "github.com/stretchr/testify/assert" ) type TestZZZ struct { BaseSentence NumberValue int StringValue string } var customparsetests = []struct { name string raw string err string msg interface{} }{ { name: "yyy sentence", raw: "$AAYYY,20,one,*13", msg: TestZZZ{ BaseSentence: BaseSentence{ Talker: "AA", Type: "YYY", Fields: []string{"20", "one", ""}, Checksum: "13", Raw: "$AAYYY,20,one,*13", }, NumberValue: 20, StringValue: "one", }, }, { name: "zzz sentence", raw: "$AAZZZ,30,two,*19", msg: TestZZZ{ BaseSentence: BaseSentence{ Talker: "AA", Type: "ZZZ", Fields: []string{"30", "two", ""}, Checksum: "19", Raw: "$AAZZZ,30,two,*19", }, NumberValue: 30, StringValue: "two", }, }, { name: "zzz sentence type", raw: "$INVALID,123,123,*7D", err: "nmea: sentence prefix 'INVALID' not supported", }, { name: "still works", raw: "$GPZDA,172809.456,12,07,1996,00,00*57", msg: ZDA{ BaseSentence: BaseSentence{ Talker: "GP", Type: "ZDA", Fields: []string{"172809.456", "12", "07", "1996", "00", "00"}, Checksum: "57", Raw: "$GPZDA,172809.456,12,07,1996,00,00*57", }, Time: Time{Valid: true, Hour: 17, Minute: 28, Second: 9, Millisecond: 456}, Day: 12, Month: 7, Year: 1996, OffsetHours: 0, OffsetMinutes: 0, }, }, } func init() { // Register some custom parsers MustRegisterParser("YYY", func(s BaseSentence) (Sentence, error) { // Somewhat error prone parser without deps fields := strings.Split(s.Raw, ",") checksum := Checksum(s.Raw[1 : len(s.Raw)-3]) checksumRaw := s.Raw[len(s.Raw)-2:] if checksum != checksumRaw { return nil, fmt.Errorf("nmea: sentence checksum mismatch [%s != %s]", checksum, checksumRaw) } nummericValue, _ := strconv.Atoi(fields[1]) return TestZZZ{ BaseSentence: s, NumberValue: nummericValue, StringValue: fields[2], }, nil }) MustRegisterParser("ZZZ", func(s BaseSentence) (Sentence, error) { // Somewhat error prone parser p := NewParser(s) numberVal := int(p.Int64(0, "number")) stringVal := p.String(1, "str") return TestZZZ{ BaseSentence: s, NumberValue: numberVal, StringValue: stringVal, }, p.Err() }) } func TestCustomParser(t *testing.T) { for _, tt := range customparsetests { t.Run(tt.name, func(t *testing.T) { m, err := Parse(tt.raw) if tt.err != "" { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) assert.Equal(t, tt.msg, m) } }) } } func TestWillReturnErrorOnDuplicateRegistration(t *testing.T) { err := RegisterParser("XXX", func(s BaseSentence) (Sentence, error) { return BaseSentence{}, nil }) assert.NoError(t, err) err = RegisterParser("XXX", func(s BaseSentence) (Sentence, error) { return BaseSentence{}, nil }) assert.Error(t, err) } func TestWillPanicOnDuplicateMustRegister(t *testing.T) { MustRegisterParser("AAA", func(s BaseSentence) (Sentence, error) { return BaseSentence{}, nil }) assert.PanicsWithError(t, "nmea: parser for sentence type '\"AAA\"' already exists", func() { MustRegisterParser("AAA", func(s BaseSentence) (Sentence, error) { return BaseSentence{}, nil }) }) } go-nmea-1.4.0/sentence_test.go000066400000000000000000000123321411662144700162400ustar00rootroot00000000000000package nmea import ( "testing" "github.com/stretchr/testify/assert" ) var sentencetests = []struct { name string raw string datatype string talkerid string prefix string err string sent BaseSentence }{ { name: "checksum ok", raw: "$GPFOO,1,2,3.3,x,y,zz,*51", datatype: "FOO", talkerid: "GP", prefix: "GPFOO", sent: BaseSentence{ Talker: "GP", Type: "FOO", Fields: []string{"1", "2", "3.3", "x", "y", "zz", ""}, Checksum: "51", Raw: "$GPFOO,1,2,3.3,x,y,zz,*51", }, }, { name: "trim leading and trailing spaces", raw: " $GPFOO,1,2,3.3,x,y,zz,*51 ", datatype: "FOO", talkerid: "GP", prefix: "GPFOO", sent: BaseSentence{ Talker: "GP", Type: "FOO", Fields: []string{"1", "2", "3.3", "x", "y", "zz", ""}, Checksum: "51", Raw: "$GPFOO,1,2,3.3,x,y,zz,*51", }, }, { name: "good parsing", raw: "$GPRMC,235236,A,3925.9479,N,11945.9211,W,44.7,153.6,250905,15.2,E,A*0C", datatype: "RMC", talkerid: "GP", prefix: "GPRMC", sent: BaseSentence{ Talker: "GP", Type: "RMC", Fields: []string{"235236", "A", "3925.9479", "N", "11945.9211", "W", "44.7", "153.6", "250905", "15.2", "E", "A"}, Checksum: "0C", Raw: "$GPRMC,235236,A,3925.9479,N,11945.9211,W,44.7,153.6,250905,15.2,E,A*0C", }, }, { name: "valid NMEA 4.10 TAG Block", raw: "\\s:Satelite_1,c:1553390539*62\\!AIVDM,1,1,,A,13M@ah0025QdPDTCOl`K6`nV00Sv,0*52", datatype: "VDM", talkerid: "AI", prefix: "AIVDM", sent: BaseSentence{ Talker: "AI", Type: "VDM", Fields: []string{"1", "1", "", "A", "13M@ah0025QdPDTCOl`K6`nV00Sv", "0"}, Checksum: "52", Raw: "!AIVDM,1,1,,A,13M@ah0025QdPDTCOl`K6`nV00Sv,0*52", TagBlock: TagBlock{ Time: 1553390539, Source: "Satelite_1", }, }, }, { name: "checksum bad", raw: "$GPFOO,1,2,3.4,x,y,zz,*51", err: "nmea: sentence checksum mismatch [56 != 51]", }, { name: "bad start character", raw: "%GPFOO,1,2,3,x,y,z*1A", err: "nmea: sentence does not start with a '$' or '!'", }, { name: "bad checksum delimiter", raw: "$GPFOO,1,2,3,x,y,z", err: "nmea: sentence does not contain checksum separator", }, { name: "no start delimiter", raw: "abc$GPRMC,235236,A,3925.9479,N,11945.9211,W,44.7,153.6,250905,15.2,E,A*0C", err: "nmea: sentence does not start with a '$' or '!'", }, { name: "no contain delimiter", raw: "GPRMC,235236,A,3925.9479,N,11945.9211,W,44.7,153.6,250905,15.2,E,A*0C", err: "nmea: sentence does not start with a '$' or '!'", }, { name: "another bad checksum", raw: "$GPRMC,235236,A,3925.9479,N,11945.9211,W,44.7,153.6,250905,15.2,E,A*0A", err: "nmea: sentence checksum mismatch [0C != 0A]", }, { name: "missing TAG Block start delimiter", raw: "s:Satelite_1,c:1553390539*62\\!AIVDM,1,1,,A,13M@ah0025QdPDTCOl`K6`nV00Sv,0*52", err: "nmea: sentence does not start with a '$' or '!'", }, { name: "missing TAG Block end delimiter", raw: "\\s:Satelite_1,c:1553390539*62!AIVDM,1,1,,A,13M@ah0025QdPDTCOl`K6`nV00Sv,0*52", err: "nmea: sentence does not start with a '$' or '!'", }, { name: "invalid TAG Block contents", raw: "\\\\!AIVDM,1,1,,A,13M@ah0025QdPDTCOl`K6`nV00Sv,0*52", err: "nmea: tagblock does not contain checksum separator", }, } func TestSentences(t *testing.T) { for _, tt := range sentencetests { t.Run(tt.name, func(t *testing.T) { sent, err := parseSentence(tt.raw) if tt.err != "" { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) assert.Equal(t, tt.sent, sent) assert.Equal(t, tt.sent.Raw, sent.String()) assert.Equal(t, tt.datatype, sent.DataType()) assert.Equal(t, tt.talkerid, sent.TalkerID()) assert.Equal(t, tt.prefix, sent.Prefix()) } }) } } var prefixtests = []struct { name string prefix string talker string typ string }{ { name: "normal prefix", prefix: "GPRMC", talker: "GP", typ: "RMC", }, { name: "missing type", prefix: "GP", talker: "GP", typ: "", }, { name: "one character", prefix: "X", talker: "X", typ: "", }, { name: "proprietary talker", prefix: "PGRME", talker: "P", typ: "GRME", }, { name: "short proprietary talker", prefix: "PX", talker: "P", typ: "X", }, } func TestPrefix(t *testing.T) { for _, tt := range prefixtests { t.Run(tt.name, func(t *testing.T) { talker, typ := parsePrefix(tt.prefix) assert.Equal(t, tt.talker, talker) assert.Equal(t, tt.typ, typ) }) } } var parsetests = []struct { name string raw string err string msg interface{} }{ { name: "bad sentence", raw: "SDFSD,2340dfmswd", err: "nmea: sentence does not start with a '$' or '!'", }, { name: "bad sentence type", raw: "$INVALID,123,123,*7D", err: "nmea: sentence prefix 'INVALID' not supported", }, { name: "bad encapsulated sentence type", raw: "!INVALID,1,2,*7E", err: "nmea: sentence prefix 'INVALID' not supported", }, } func TestParse(t *testing.T) { for _, tt := range parsetests { t.Run(tt.name, func(t *testing.T) { m, err := Parse(tt.raw) if tt.err != "" { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) assert.Equal(t, tt.msg, m) } }) } } go-nmea-1.4.0/tagblock.go000066400000000000000000000046421411662144700151700ustar00rootroot00000000000000package nmea import ( "fmt" "strconv" "strings" ) // TagBlock struct type TagBlock struct { Time int64 // TypeUnixTime unix timestamp (unit is likely to be s, but might be ms, YMMV), parameter: -c RelativeTime int64 // TypeRelativeTime relative time, parameter: -r Destination string // TypeDestinationID destination identification 15 char max, parameter: -d Grouping string // TypeGrouping sentence grouping, parameter: -g LineCount int64 // TypeLineCount line count, parameter: -n Source string // TypeSourceID source identification 15 char max, parameter: -s Text string // TypeTextString valid character string, parameter -t } func parseInt64(raw string) (int64, error) { i, err := strconv.ParseInt(raw, 10, 64) if err != nil { return 0, fmt.Errorf("nmea: tagblock unable to parse uint64 [%s]", raw) } return i, nil } // parseTagBlock adds support for tagblocks // https://gpsd.gitlab.io/gpsd/AIVDM.html#_nmea_tag_blocks func parseTagBlock(tags string) (TagBlock, error) { sumSepIndex := strings.Index(tags, ChecksumSep) if sumSepIndex == -1 { return TagBlock{}, fmt.Errorf("nmea: tagblock does not contain checksum separator") } var ( fieldsRaw = tags[0:sumSepIndex] checksumRaw = strings.ToUpper(tags[sumSepIndex+1:]) checksum = Checksum(fieldsRaw) tagBlock TagBlock err error ) // Validate the checksum if checksum != checksumRaw { return TagBlock{}, fmt.Errorf("nmea: tagblock checksum mismatch [%s != %s]", checksum, checksumRaw) } items := strings.Split(tags[:sumSepIndex], ",") for _, item := range items { parts := strings.SplitN(item, ":", 2) if len(parts) != 2 { return TagBlock{}, fmt.Errorf("nmea: tagblock field is malformed (should be :) [%s]", item) } key, value := parts[0], parts[1] switch key { case "c": // UNIX timestamp tagBlock.Time, err = parseInt64(value) if err != nil { return TagBlock{}, err } case "d": // Destination ID tagBlock.Destination = value case "g": // Grouping tagBlock.Grouping = value case "n": // Line count tagBlock.LineCount, err = parseInt64(value) if err != nil { return TagBlock{}, err } case "r": // Relative time tagBlock.RelativeTime, err = parseInt64(value) if err != nil { return TagBlock{}, err } case "s": // Source ID tagBlock.Source = value case "t": // Text string tagBlock.Text = value } } return tagBlock, nil } go-nmea-1.4.0/tagblock_test.go000066400000000000000000000045551411662144700162320ustar00rootroot00000000000000package nmea import ( "testing" "github.com/stretchr/testify/assert" ) var tagblocktests = []struct { name string raw string err string msg TagBlock }{ { name: "Test NMEA tag block", raw: "s:Satelite_1,c:1553390539*62", msg: TagBlock{ Time: 1553390539, Source: "Satelite_1", }, }, { name: "Test NMEA tag block with head", raw: "s:satelite,c:1564827317*25", msg: TagBlock{ Time: 1564827317, Source: "satelite", }, }, { name: "Test unknown tag", raw: "x:NorSat_1,c:1564827317*42", msg: TagBlock{ Time: 1564827317, Source: "", }, }, { name: "Test unix timestamp", raw: "x:NorSat_1,c:1564827317*42", msg: TagBlock{ Time: 1564827317, Source: "", }, }, { name: "Test milliseconds timestamp", raw: "x:NorSat_1,c:1564827317000*72", msg: TagBlock{ Time: 1564827317000, Source: "", }, }, { name: "Test all input types", raw: "s:satelite,c:1564827317,r:1553390539,d:ara,g:bulk,n:13,t:helloworld*3F", msg: TagBlock{ Time: 1564827317, RelativeTime: 1553390539, Destination: "ara", Grouping: "bulk", Source: "satelite", Text: "helloworld", LineCount: 13, }, }, { name: "Test empty tag in tagblock", raw: "s:satelite,,r:1553390539,d:ara,g:bulk,n:13,t:helloworld*68", err: "nmea: tagblock field is malformed (should be :) []", }, { name: "Test Invalid checksum", raw: "s:satelite,c:1564827317*49", err: "nmea: tagblock checksum mismatch [25 != 49]", }, { name: "Test no checksum", raw: "s:satelite,c:156482731749", err: "nmea: tagblock does not contain checksum separator", }, { name: "Test invalid timestamp", raw: "s:satelite,c:gjadslkg*30", err: "nmea: tagblock unable to parse uint64 [gjadslkg]", }, { name: "Test invalid linecount", raw: "s:satelite,n:gjadslkg*3D", err: "nmea: tagblock unable to parse uint64 [gjadslkg]", }, { name: "Test invalid relative time", raw: "s:satelite,r:gjadslkg*21", err: "nmea: tagblock unable to parse uint64 [gjadslkg]", }, } func TestTagBlock(t *testing.T) { for _, tt := range tagblocktests { t.Run(tt.name, func(t *testing.T) { m, err := parseTagBlock(tt.raw) if tt.err != "" { assert.Error(t, err) assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) assert.Equal(t, tt.msg, m) } }) } } go-nmea-1.4.0/ths.go000066400000000000000000000016371411662144700142010ustar00rootroot00000000000000package nmea const ( // TypeTHS type for THS sentences TypeTHS = "THS" // AutonomousTHS autonomous ths heading AutonomousTHS = "A" // EstimatedTHS estimated (dead reckoning) THS heading EstimatedTHS = "E" // ManualTHS manual input THS heading ManualTHS = "M" // SimulatorTHS simulated THS heading SimulatorTHS = "S" // InvalidTHS not valid THS heading (or standby) InvalidTHS = "V" ) // THS is the Actual vessel heading in degrees True with status. // http://www.nuovamarea.net/pytheas_9.html type THS struct { BaseSentence Heading float64 // Heading in degrees Status string // Heading status } // newTHS constructor func newTHS(s BaseSentence) (THS, error) { p := NewParser(s) p.AssertType(TypeTHS) m := THS{ BaseSentence: s, Heading: p.Float64(0, "heading"), Status: p.EnumString(1, "status", AutonomousTHS, EstimatedTHS, ManualTHS, SimulatorTHS, InvalidTHS), } return m, p.Err() } go-nmea-1.4.0/ths_test.go000066400000000000000000000025711411662144700152360ustar00rootroot00000000000000package nmea import ( "testing" "github.com/stretchr/testify/assert" ) var thstests = []struct { name string raw string err string msg THS }{ { name: "good sentence AutonomousTHS", raw: "$INTHS,123.456,A*20", msg: THS{ Heading: 123.456, Status: AutonomousTHS, }, }, { name: "good sentence EstimatedTHS", raw: "$INTHS,123.456,E*24", msg: THS{ Heading: 123.456, Status: EstimatedTHS, }, }, { name: "good sentence ManualTHS", raw: "$INTHS,123.456,M*2C", msg: THS{ Heading: 123.456, Status: ManualTHS, }, }, { name: "good sentence SimulatorTHS", raw: "$INTHS,123.456,S*32", msg: THS{ Heading: 123.456, Status: SimulatorTHS, }, }, { name: "good sentence InvalidTHS", raw: "$INTHS,,V*1E", msg: THS{ Heading: 0.0, Status: InvalidTHS, }, }, { name: "invalid Status", raw: "$INTHS,123.456,B*23", err: "nmea: INTHS invalid status: B", }, { name: "invalid Heading", raw: "$INTHS,XXX,A*51", err: "nmea: INTHS invalid heading: XXX", }, } func TestTHS(t *testing.T) { for _, tt := range thstests { 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) ths := m.(THS) ths.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, ths) } }) } } go-nmea-1.4.0/types.go000066400000000000000000000142771411662144700145530ustar00rootroot00000000000000package nmea // Latitude / longitude representation. import ( "errors" "fmt" "math" "regexp" "strconv" "strings" "unicode" ) const ( // Degrees value Degrees = '\u00B0' // Minutes value Minutes = '\'' // Seconds value Seconds = '"' // Point value Point = '.' // North value North = "N" // South value South = "S" // East value East = "E" // West value West = "W" ) // ParseLatLong parses the supplied string into the LatLong. // // Supported formats are: // - DMS (e.g. 33° 23' 22") // - Decimal (e.g. 33.23454) // - GPS (e.g 15113.4322S) // func ParseLatLong(s string) (float64, error) { var l float64 if v, err := ParseDMS(s); err == nil { l = v } else if v, err := ParseGPS(s); err == nil { l = v } else if v, err := ParseDecimal(s); err == nil { l = v } else { return 0, fmt.Errorf("cannot parse [%s], unknown format", s) } return l, nil } // ParseGPS parses a GPS/NMEA coordinate. // e.g 15113.4322S func ParseGPS(s string) (float64, error) { parts := strings.Split(s, " ") if len(parts) != 2 { return 0, fmt.Errorf("invalid format: %s", s) } dir := parts[1] value, err := strconv.ParseFloat(parts[0], 64) if err != nil { return 0, fmt.Errorf("parse error: %s", err.Error()) } degrees := math.Floor(value / 100) minutes := value - (degrees * 100) value = degrees + minutes/60 if dir == North || dir == East { return value, nil } else if dir == South || dir == West { return 0 - value, nil } else { return 0, fmt.Errorf("invalid direction [%s]", dir) } } // FormatGPS formats a GPS/NMEA coordinate func FormatGPS(l float64) string { padding := "" degrees := math.Floor(math.Abs(l)) fraction := (math.Abs(l) - degrees) * 60 if fraction < 10 { padding = "0" } return fmt.Sprintf("%d%s%.4f", int(degrees), padding, fraction) } // ParseDecimal parses a decimal format coordinate. // e.g: 151.196019 func ParseDecimal(s string) (float64, error) { // Make sure it parses as a float. l, err := strconv.ParseFloat(s, 64) if err != nil || s[0] != '-' && len(strings.Split(s, ".")[0]) > 3 { return 0.0, errors.New("parse error (not decimal coordinate)") } return l, nil } // ParseDMS parses a coordinate in degrees, minutes, seconds. // - e.g. 33° 23' 22" func ParseDMS(s string) (float64, error) { degrees := 0 minutes := 0 seconds := 0.0 // Whether a number has finished parsing (i.e whitespace after it) endNumber := false // Temporary parse buffer. tmpBytes := []byte{} var err error for i, r := range s { switch { case unicode.IsNumber(r) || r == '.': if !endNumber { tmpBytes = append(tmpBytes, s[i]) } else { return 0, errors.New("parse error (no delimiter)") } case unicode.IsSpace(r) && len(tmpBytes) > 0: endNumber = true case r == Degrees: if degrees, err = strconv.Atoi(string(tmpBytes)); err != nil { return 0, errors.New("parse error (degrees)") } tmpBytes = tmpBytes[:0] endNumber = false case s[i] == Minutes: if minutes, err = strconv.Atoi(string(tmpBytes)); err != nil { return 0, errors.New("parse error (minutes)") } tmpBytes = tmpBytes[:0] endNumber = false case s[i] == Seconds: if seconds, err = strconv.ParseFloat(string(tmpBytes), 64); err != nil { return 0, errors.New("parse error (seconds)") } tmpBytes = tmpBytes[:0] endNumber = false case unicode.IsSpace(r) && len(tmpBytes) == 0: continue default: return 0, fmt.Errorf("parse error (unknown symbol [%d])", s[i]) } } if len(tmpBytes) > 0 { return 0, fmt.Errorf("parse error (trailing data [%s])", string(tmpBytes)) } val := float64(degrees) + (float64(minutes) / 60.0) + (float64(seconds) / 60.0 / 60.0) return val, nil } // FormatDMS returns the degrees, minutes, seconds format for the given LatLong. func FormatDMS(l float64) string { val := math.Abs(l) degrees := int(math.Floor(val)) minutes := int(math.Floor(60 * (val - float64(degrees)))) seconds := 3600 * (val - float64(degrees) - (float64(minutes) / 60)) return fmt.Sprintf("%d\u00B0 %d' %f\"", degrees, minutes, seconds) } // Time type type Time struct { Valid bool Hour int Minute int Second int Millisecond int } // String representation of Time func (t Time) String() string { seconds := float64(t.Second) + float64(t.Millisecond)/1000 return fmt.Sprintf("%02d:%02d:%07.4f", t.Hour, t.Minute, seconds) } // timeRe is used to validate time strings var timeRe = regexp.MustCompile(`^\d{6}(\.\d*)?$`) // ParseTime parses wall clock time. // e.g. hhmmss.ssss // An empty time string will result in an invalid time. func ParseTime(s string) (Time, error) { if s == "" { return Time{}, nil } if !timeRe.MatchString(s) { return Time{}, fmt.Errorf("parse time: expected hhmmss.ss format, got '%s'", s) } hour, _ := strconv.Atoi(s[:2]) minute, _ := strconv.Atoi(s[2:4]) second, _ := strconv.ParseFloat(s[4:], 64) whole, frac := math.Modf(second) return Time{true, hour, minute, int(whole), int(round(frac * 1000))}, nil } // round is implemented here because it wasn't added until go1.10 // this code is taken directly from the math.Round documentation // TODO: use math.Round after a reasonable amount of time func round(x float64) float64 { t := math.Trunc(x) if math.Abs(x-t) >= 0.5 { return t + math.Copysign(1, x) } return t } // Date type type Date struct { Valid bool DD int MM int YY int } // String representation of date func (d Date) String() string { return fmt.Sprintf("%02d/%02d/%02d", d.DD, d.MM, d.YY) } // ParseDate field ddmmyy format func ParseDate(ddmmyy string) (Date, error) { if ddmmyy == "" { return Date{}, nil } if len(ddmmyy) != 6 { return Date{}, fmt.Errorf("parse date: exptected ddmmyy format, got '%s'", ddmmyy) } dd, err := strconv.Atoi(ddmmyy[0:2]) if err != nil { return Date{}, errors.New(ddmmyy) } mm, err := strconv.Atoi(ddmmyy[2:4]) if err != nil { return Date{}, errors.New(ddmmyy) } yy, err := strconv.Atoi(ddmmyy[4:6]) if err != nil { return Date{}, errors.New(ddmmyy) } 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 } go-nmea-1.4.0/types_test.go000066400000000000000000000117671411662144700156130ustar00rootroot00000000000000package nmea import ( "fmt" "testing" "github.com/stretchr/testify/assert" ) var nearDistance = 0.001 func TestParseLatLong(t *testing.T) { var tests = []struct { value string expected float64 err bool }{ {"33\u00B0 12' 34.3423\"", 33.209540, false}, // dms {"3345.1232 N", 33.752054, false}, // gps {"151.234532", 151.234532, false}, // decimal } for _, tt := range tests { t.Run(tt.value, func(t *testing.T) { l, err := ParseLatLong(tt.value) if tt.err { assert.Error(t, err) } else { assert.InDelta(t, tt.expected, l, nearDistance) } }) } } func TestParseGPS(t *testing.T) { var tests = []struct { value string expected float64 err bool }{ {"3345.1232 N", 33.752054, false}, {"15145.9877 S", -151.76646, false}, {"12345.1234 X", 0, true}, {"1234.1234", 0, true}, } for _, tt := range tests { t.Run(tt.value, func(t *testing.T) { l, err := ParseGPS(tt.value) if tt.err { assert.Error(t, err) } else { assert.InDelta(t, tt.expected, l, nearDistance) } }) } } func TestParseDMS(t *testing.T) { var tests = []struct { value string expected float64 err bool }{ {"33\u00B0 12' 34.3423\"", 33.209540, false}, {"33\u00B0 1.1' 34.3423\"", 0, true}, {"3.3\u00B0 1' 34.3423\"", 0, true}, {"33\u00B0 1' 34.34.23\"", 0, true}, {"33 1 3434.23", 0, true}, {"123", 0, true}, } for _, tt := range tests { t.Run(tt.value, func(t *testing.T) { l, err := ParseDMS(tt.value) if tt.err { assert.Error(t, err) } else { assert.InDelta(t, tt.expected, l, nearDistance) } }) } } func TestParseDecimal(t *testing.T) { var tests = []struct { value string expected float64 err bool }{ {"151.234532", 151.234532, false}, {"-151.234532", -151.234532, false}, } for _, tt := range tests { t.Run(tt.value, func(t *testing.T) { l, err := ParseDecimal(tt.value) if tt.err { assert.Error(t, err) } else { assert.InDelta(t, tt.expected, l, nearDistance) } }) } } func TestLatLongPrint(t *testing.T) { var tests = []struct { value float64 dms string gps string }{ { value: 151.434367, gps: "15126.0620", dms: "151° 26' 3.721200\"", }, { value: 33.94057166666666, gps: "3356.4343", dms: "33° 56' 26.058000\"", }, { value: 45.0, dms: "45° 0' 0.000000\"", gps: "4500.0000", }, } for _, tt := range tests { t.Run(fmt.Sprintf("%f", tt.value), func(t *testing.T) { assert.Equal(t, tt.dms, FormatDMS(tt.value)) assert.Equal(t, tt.gps, FormatGPS(tt.value)) }) } } func TestTimeParse(t *testing.T) { timetests := []struct { value string expected Time ok bool }{ {"123456", Time{true, 12, 34, 56, 0}, true}, {"", Time{}, true}, {"112233.123", Time{true, 11, 22, 33, 123}, true}, {"010203.04", Time{true, 1, 2, 3, 40}, true}, {"10203.04", Time{}, false}, {"x0u2xd", Time{}, false}, {"xx2233.123", Time{}, false}, {"11xx33.123", Time{}, false}, {"1122xx.123", Time{}, false}, {"112233.xxx", Time{}, false}, } for _, tt := range timetests { actual, err := ParseTime(tt.value) if !tt.ok { if err == nil { t.Errorf("ParseTime(%s) expected error", tt.value) } } else { if err != nil { t.Errorf("ParseTime(%s) %s", tt.value, err) } if actual != tt.expected { t.Errorf("ParseTime(%s) got %s expected %s", tt.value, actual, tt.expected) } } } } func TestTimeString(t *testing.T) { d := Time{ Hour: 1, Minute: 2, Second: 3, Millisecond: 4, } expected := "01:02:03.0040" if s := d.String(); s != expected { t.Fatalf("got %s, expected %s", s, expected) } } func TestDateParse(t *testing.T) { datetests := []struct { value string expected Date ok bool }{ {"010203", Date{true, 1, 2, 3}, true}, {"01003", Date{}, false}, {"", Date{}, true}, {"xx0203", Date{}, false}, {"01xx03", Date{}, false}, {"0102xx", Date{}, false}, } for _, tt := range datetests { actual, err := ParseDate(tt.value) if !tt.ok { if err == nil { t.Errorf("ParseDate(%s) expected error", tt.value) } } else { if err != nil { t.Errorf("ParseDate(%s) %s", tt.value, err) } if actual != tt.expected { t.Errorf("ParseDate(%s) got %s expected %s", tt.value, actual, tt.expected) } } } } func TestDateString(t *testing.T) { d := Date{ DD: 1, MM: 2, YY: 3, } expected := "01/02/03" if s := d.String(); s != expected { 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) } } } go-nmea-1.4.0/vdmvdo.go000066400000000000000000000015451411662144700147000ustar00rootroot00000000000000package nmea const ( // TypeVDM type for VDM sentences TypeVDM = "VDM" // TypeVDO type for VDO sentences TypeVDO = "VDO" ) // VDMVDO is a format used to encapsulate generic binary payloads. It is most commonly used // with AIS data. // http://catb.org/gpsd/AIVDM.html type VDMVDO struct { BaseSentence NumFragments int64 FragmentNumber int64 MessageID int64 Channel string Payload []byte } // newVDMVDO constructor func newVDMVDO(s BaseSentence) (VDMVDO, error) { p := NewParser(s) m := VDMVDO{ BaseSentence: s, NumFragments: p.Int64(0, "number of fragments"), FragmentNumber: p.Int64(1, "fragment number"), MessageID: p.Int64(2, "sequence number"), Channel: p.String(3, "channel ID"), Payload: p.SixBitASCIIArmour(4, int(p.Int64(5, "number of padding bits")), "payload"), } return m, p.Err() } go-nmea-1.4.0/vdmvdo_test.go000066400000000000000000000063051411662144700157360ustar00rootroot00000000000000package nmea import ( "testing" "github.com/stretchr/testify/assert" ) var vdmtests = []struct { name string raw string err string msg VDMVDO }{ { name: "Good single fragment message", raw: "!AIVDM,1,1,,A,13aGt0PP0jPN@9fMPKVDJgwfR>`<,0*55", msg: VDMVDO{ NumFragments: 1, FragmentNumber: 1, MessageID: 0, Channel: "A", Payload: []byte{0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0}, }, }, { name: "Good single fragment message with padding", raw: "!AIVDM,1,1,,A,H77nSfPh4U=