pax_global_header00006660000000000000000000000064146551446240014525gustar00rootroot0000000000000052 comment=af41c6a15533baa3bfe222078510500d9dca2388 go-nmea-1.10.0/000077500000000000000000000000001465514462400131275ustar00rootroot00000000000000go-nmea-1.10.0/.github/000077500000000000000000000000001465514462400144675ustar00rootroot00000000000000go-nmea-1.10.0/.github/workflows/000077500000000000000000000000001465514462400165245ustar00rootroot00000000000000go-nmea-1.10.0/.github/workflows/ci.yml000066400000000000000000000017411465514462400176450ustar00rootroot00000000000000name: CI on: push: branches: - master pull_request: branches: - master workflow_dispatch: jobs: build: runs-on: ubuntu-latest strategy: fail-fast: false matrix: go: ["1.20", "1.19", "1.18", "1.17"] steps: - uses: actions/checkout@v3 - name: Set up Go uses: actions/setup-go@v3 with: go-version: ${{ matrix.go }} - name: Install dependencies run: | go install golang.org/x/lint/golint@latest go install github.com/mattn/goveralls@v0.0.11 - 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.10.0/LICENSE000066400000000000000000000020701465514462400141330ustar00rootroot00000000000000The 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.10.0/Makefile000066400000000000000000000010251465514462400145650ustar00rootroot00000000000000.DEFAULT_GOAL := check check: lint vet test ## Check project lint: ## Lint the files @golint -set_exit_status ./... vet: ## Vet the files @go vet ./... test: ## Run tests with data race detector @go test -race ./... init: @go install golang.org/x/lint/golint@latest goversion ?= "1.19" test_version: ## Run tests inside Docker with given version (defaults to 1.19). Example for Go1.15: make test_version goversion=1.15 @docker run --rm -it -v $(shell pwd):/project golang:$(goversion) /bin/sh -c "cd /project && make test" go-nmea-1.10.0/README.md000066400000000000000000001054421465514462400144140ustar00rootroot00000000000000# 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 Sentence with link is supported by this library. NMEA0183 sentences list is based on [IEC 61162-1:2016 (Edition 5.0 2016-08)](https://webstore.iec.ch/publication/25754) table of contents. | Sentence | Description | References | |--------------------|---------------------------------------------------------------------|------------------------------------------------------------------------------------------------| | [AAM](./aam.go) | Waypoint arrival alarm | [gpsd](https://gpsd.gitlab.io/gpsd/NMEA.html#_aam_waypoint_arrival_alarm) | | ABK | AIS addressed and binary broadcast acknowledgement | | | [ABM](./abm.go) | AIS addressed binary and safety related message | | | ACA | AIS channel assignment message | | | [ACK](./ack.go) | Acknowledge alarm | | | [ACN](./acn.go) | Alert command | | | ACS | AIS channel management information source | | | AIR | AIS interrogation request | | | AKD | Acknowledge detail alarm condition | | | [ALA](./ala.go) | Report detailed alarm condition | | | [ALC](./alc.go) | Cyclic alert list | | | [ALF](./alf.go) | Alert sentence | | | [ALR](./alr.go) | Set alarm state | | | [APB](./apb.go) | Heading/track controller (autopilot) sentence B | [gpsd](https://gpsd.gitlab.io/gpsd/NMEA.html#_apb_autopilot_sentence_b) | | [ARC](./arc.go) | Alert command refused | | | [BBM](./bbm.go) | AIS broadcast binary message | | | [BEC](./bec.go) | Bearing and distance to waypoint, Dead reckoning | [1](http://www.nmea.de/nmea0183datensaetze.html#bec) | | [BOD](./bod.go) | Bearing origin to destination | [gpsd](https://gpsd.gitlab.io/gpsd/NMEA.html#_bod_bearing_waypoint_to_waypoint) | | [BWC](./bwc.go) | Bearing and distance to waypoint, Great circle | [gpsd](https://gpsd.gitlab.io/gpsd/NMEA.html#_bwc_bearing_distance_to_waypoint_great_circle) | | [BWR](./bwr.go) | Bearing and distance to waypoint, Rhumb line | [gpsd](https://gpsd.gitlab.io/gpsd/NMEA.html#_bwr_bearing_and_distance_to_waypoint_rhumb_line) | | [BWW](./bww.go) | Bearing waypoint to waypoint | [gpsd](https://gpsd.gitlab.io/gpsd/NMEA.html#_bww_bearing_waypoint_to_waypoint) | | CUR | Water current layer, Multi-layer water current data | | | [DBK](./dbk.go) | Depth Below Keel (obsolete, use DPT instead) | [gpsd](https://gpsd.gitlab.io/gpsd/NMEA.html#_dbk_depth_below_keel) | | [DBS](./dbs.go) | Depth below transducer | [gpsd](https://gpsd.gitlab.io/gpsd/NMEA.html#_dbs_depth_below_surface) | | [DBT](./dbt.go) | Depth below transducer | [gpsd](https://gpsd.gitlab.io/gpsd/NMEA.html#_dbt_depth_below_transducer) | | DDC | Display dimming control | | | [DOR](./dor.go) | Door status detection | | | [DPT](./dpt.go) | Depth | [gpsd](https://gpsd.gitlab.io/gpsd/NMEA.html#_dpt_depth_of_water) | | [DSC](./dsc.go) | Digital selective calling information | | | [DSE](./dse.go) | Expanded digital selective calling | | | [DTM](./dtm.go) | Datum reference | [gpsd](https://gpsd.gitlab.io/gpsd/NMEA.html#_dtm_datum_reference) | | EPV | Command or report equipment property value | | | ETL | Engine telegraph operation status | | | [EVE](./eve.go) | General event message | | | [FIR](./fir.go) | Fire detection | | | FSI | Frequency set information | | | GBS | GNSS satellite fault detection | | | GEN | Generic binary information | | | GFA | GNSS fix accuracy and integrity | | | [GGA](./gga.go) | Global positioning system (GPS) fix data | [1](http://aprs.gids.nl/nmea/#gga) | | [GLL](./gll.go) | Geographic position, Latitude/longitude | [1](http://aprs.gids.nl/nmea/#gll) | | [GNS](./gns.go) | GNSS fix data | [gpsd](https://gpsd.gitlab.io/gpsd/NMEA.html#_gns_fix_data) | | GRS | GNSS range residuals | | | [GSA](./gsa.go) | GNSS DOP and active satellites | [1](http://aprs.gids.nl/nmea/#gsa) | | GST | GNSS pseudorange noise statistics | | | [GSV](./gsv.go) | GNSS satellites in view | [1](http://aprs.gids.nl/nmea/#gsv) | | [HBT](./hbt.go) | Heartbeat supervision sentence | | | HCR | Heading correction report | | | [HDG](./hdg.go) | Heading, deviation and variation | [gpsd](https://gpsd.gitlab.io/gpsd/NMEA.html#_hdg_heading_deviation_variation) | | HDM | Heading - Magnetic | [gpsd](https://gpsd.gitlab.io/gpsd/NMEA.html#_hdm_heading_magnetic) | | [HDT](./hdt.go) | Heading true | [gpsd](http://aprs.gids.nl/nmea/#hdt) | | HMR | Heading monitor receive | | | HMS | Heading monitor set | | | HRM | heel angle, roll period and roll amplitude measurement device | | | [HSC](./hsc.go) | Heading steering command | [gpsd](https://gpsd.gitlab.io/gpsd/NMEA.html#_hsc_heading_steering_command) | | HSS | Hull stress surveillance systems | | | HTC | Heading/track control command | | | HTD | Heading /track control data | | | LR1 | AIS long-range reply sentence 1 | | | LR2 | AIS long-range reply sentence 2 | | | LR3 | AIS long-range reply sentence 3 | | | LRF | AIS long-range function | | | LRI | AIS long-range interrogation | | | [MDA](./mda.go) | Meteorological Composite | [gpsd](https://gpsd.gitlab.io/gpsd/NMEA.html#_mda_meteorological_composite) | | [MTA](./mta.go) | Air Temperature (obsolete, use XDR instead) | | | MOB | Man over board notification | | | MSK | MSK receiver interface | | | MSS | MSK receiver signal status | | | [MTW](./mtw.go) | Water temperature | [gpsd](https://gpsd.gitlab.io/gpsd/NMEA.html#_mtw_mean_temperature_of_water) | | [MWD](./mwd.go) | Wind direction and speed | [1](https://www.tronico.fi/OH6NT/docs/NMEA0183.pdf) | | [MWV](./mwv.go) | Wind speed and angle | [gpsd](https://gpsd.gitlab.io/gpsd/NMEA.html#_mwv_wind_speed_and_angle) | | NAK | Negative acknowledgement | | | NRM | NAVTEX receiver mask | | | NRX | NAVTEX received message | | | NSR | Navigation status report | | | [OSD](./osd.go) | Own ship data | [gpsd](https://gpsd.gitlab.io/gpsd/NMEA.html#_osd_own_ship_data) | | POS | Device position and ship dimensions report or configuration command | | | PRC | Propulsion remote control status | | | RLM | Return link message | | | RMA | Recommended minimum specific LORAN-C data | | | [RMB](./rmb.go) | Recommended minimum navigation information | [gpsd](https://gpsd.gitlab.io/gpsd/NMEA.html#_rmb_recommended_minimum_navigation_information) | | [RMC](./rmc.go) | Recommended minimum specific GNSS data | [1](http://aprs.gids.nl/nmea/#rmc) | | ROR | Rudder order status | | | [ROT](./rot.go) | Rate of turn | [gpsd](https://gpsd.gitlab.io/gpsd/NMEA.html#_rot_rate_of_turn) | | RRT | Report route transfer | | | [RPM](./rpm.go) | Revolutions | [gpsd](https://gpsd.gitlab.io/gpsd/NMEA.html#_rpm_revolutions) | | [RSA](./rsa.go) | Rudder sensor angle | [gpsd](https://gpsd.gitlab.io/gpsd/NMEA.html#_rsa_rudder_sensor_angle) | | [RSD](./rsd.go) | Radar system data | [gpsd](https://gpsd.gitlab.io/gpsd/NMEA.html#_rsd_radar_system_data) | | [RTE](./rte.go) | Routes | [1](http://aprs.gids.nl/nmea/#rte) | | SFI | Scanning frequency information | | | SMI | SafetyNET Message, All Ships/NavArea | | | SM2 | SafetyNET Message, Coastal Warning Area | | | SM3 | SafetyNET Message, Circular Area address | | | SM4 | SafetyNET Message, Rectangular Area Address | | | SMB | IMO SafetyNET Message Body | | | SPW | Security password sentence | | | SSD | AIS ship static data | | | STN | Multiple data ID | | | [THS](./ths.go) | True heading and status | [1](http://www.nuovamarea.net/pytheas_9.html) | | [TLB](./tlb.go) | Target label | | | [TLL](./tll.go) | Target latitude and longitude | [gpsd](https://gpsd.gitlab.io/gpsd/NMEA.html#_tll_target_latitude_and_longitude) | | TRC | Thruster control data | | | TRL | AIS transmitter-non-functioning log | | | TRD | Thruster response data | | | [TTD](./ttd.go) | Tracked target data | | | [TTM](./ttm.go) | Tracked target message | [gpsd](https://gpsd.gitlab.io/gpsd/NMEA.html#_ttm_tracked_target_message) | | TUT | Transmission of multi-language text | | | [TXT](./txt.go) | Text transmission | [NMEA](https://www.nmea.org/Assets/20160520%20txt%20amendment.pdf) | | UID | User identification code transmission | | | [VBW](./vbw.go) | Dual ground/water speed | [gpsd](https://gpsd.gitlab.io/gpsd/NMEA.html#_vbw_dual_groundwater_speed) | | [VDM](./vdmvdo.go) | AIS VHF data-link message | [gpsd](https://gpsd.gitlab.io/gpsd/AIVDM.html) | | [VDO](./vdmvdo.go) | AIS VHF data-link own-vessel report | [gpsd](https://gpsd.gitlab.io/gpsd/AIVDM.html) | | [VDR](./vdr.go) | Set and drift | [gpsd](https://gpsd.gitlab.io/gpsd/NMEA.html#_vdr_set_and_drift) | | VER | Version | | | [VHW](./vhw.go) | Water speed and heading | [1](https://www.tronico.fi/OH6NT/docs/NMEA0183.pdf) | | [VLW](./vlw.go) | Dual ground/water distance | [gpsd](https://gpsd.gitlab.io/gpsd/NMEA.html#_vlw_distance_traveled_through_water) | | [VPW](./vpw.go) | Speed measured parallel to wind | [gpsd](https://gpsd.gitlab.io/gpsd/NMEA.html#_vpw_speed_measured_parallel_to_wind) | | [VSD](./vsd.go) | AIS voyage static data | | | [VTG](./vtg.go) | Course over ground and ground speed | [1](http://aprs.gids.nl/nmea/#vtg) | | VWR | Relative Wind Speed and Angle | [gpsd](https://gpsd.gitlab.io/gpsd/NMEA.html#_vwr_relative_wind_speed_and_angle) | | VWT | True Wind Speed and Angle | | | WAT | Water level detection | | | WCV | Waypoint closure velocity | | | WNC | Distance waypoint to waypoint | | | [WPL](./wpl.go) | Waypoint location | [1](http://aprs.gids.nl/nmea/#wpl) | | [XDR](./xdr.go) | Transducer measurements | [gpsd](https://gpsd.gitlab.io/gpsd/NMEA.html#_xdr_transducer_measurement) | | [XTE](./xte.go) | Cross-track error, measured | | | XTR | Cross-track error, dead reckoning | | | [ZDA](./zda.go) | Time and date | [1](http://aprs.gids.nl/nmea/#zda) | | ZDL | Time and distance to variable point | | | ZFO | UTC and time from origin waypoint | | | ZTG | UTC and time to destination waypoint | | | Proprietary sentence type | Description | References | |---------------------------|-------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------| | [PNG](./pgn.go) | Transfer NMEA2000 frame as NMEA0183 sentence (ShipModul MiniPlex-3) | [1](https://opencpn.org/wiki/dokuwiki/lib/exe/fetch.php?media=opencpn:software:mxpgn_sentence.pdf) | | [PCDIN](./pcdin.go) | Transfer NMEA2000 frame as NMEA0183 sentence (SeaSmart.Net Protocol) | [1](http://www.seasmart.net/pdf/SeaSmart_HTTP_Protocol_RevG_043012.pdf) | | [PGRME](./pgrme.go) | Estimated Position Error (Garmin proprietary sentence) | [1](http://aprs.gids.nl/nmea/#rme) | | [PHTRO](./phtro.go) | Vessel pitch and roll (Xsens IMU/VRU/AHRS) | | | [PMTK001](./pmtk.go) | Acknowledgement of previously sent command/packet | [1](https://www.rhydolabz.com/documents/25/PMTK_A11.pdf) | | [PRDID](./prdid.go) | Vessel pitch, roll and heading (Xsens IMU/VRU/AHRS) | | | [PSKPDPT](./pskpdpt.go) | Depth of Water for multiple transducer installation | | | [PSONCMS](./psoncms.go) | Quaternion, acceleration, rate of turn, magnetic field, sensor temperature (Xsens IMU/VRU/AHRS) | | 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 ``` ### Customize sentence parser Parser logic can be customized by creating `nmea.SentenceParser` instance and by providing callback implementations. ```go p := nmea.SentenceParser{ CustomParsers: nil, ParsePrefix: nil, CheckCRC: nil, OnTagBlock: nil, } s, err := p.Parse("$GPRMC,220516,A,5133.82,N,00042.24,W,173.8,231.8,130694,004.2,W*70") ``` ### 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) } switch m := s.(type) { case XYZType: 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) default: panic("Could not parse XYZ sentence") } } ``` 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 ``` ### Message parsing with optional values Some messages have optional fields. By default, omitted numeric values are set to 0. In situations where you need finer control to distinguish between an undefined value and an actual 0, you can register types overriding existing sentences, using `nmea.Int64` and `nmea.Float64` instead of `int64` and `float64`. The matching parsing methods are `(*Parser).NullInt64` and `(*Parser).NullFloat64`. Both `nmea.Int64` and `nmea.Float64` contains a numeric field `Value` which is defined only if the field `Valid` is `true`. See below example for a modified VTG sentence parser: ```go package main import ( "fmt" "github.com/adrianmo/go-nmea" ) // VTG represents track & speed data. // http://aprs.gids.nl/nmea/#vtg type VTG struct { nmea.BaseSentence TrueTrack nmea.Float64 MagneticTrack nmea.Float64 GroundSpeedKnots nmea.Float64 GroundSpeedKPH nmea.Float64 } func main() { nmea.MustRegisterParser("VTG", func(s nmea.BaseSentence) (nmea.Sentence, error) { p := nmea.NewParser(s) return VTG{ BaseSentence: s, TrueTrack: p.NullFloat64(0, "true track"), MagneticTrack: p.NullFloat64(2, "magnetic track"), GroundSpeedKnots: p.NullFloat64(4, "ground speed (knots)"), GroundSpeedKPH: p.NullFloat64(6, "ground speed (km/h)"), }, p.Err() }) sentence := "$GPVTG,140.88,T,,M,8.04,N,14.89,K,D*05" s, err := nmea.Parse(sentence) if err != nil { panic(err) } m, ok := s.(VTG) if !ok { panic("Could not parse VTG sentence") } fmt.Printf("Raw sentence: %v\n", m) fmt.Printf("TrueTrack: %v\n", m.TrueTrack) fmt.Printf("MagneticTrack: %v\n", m.MagneticTrack) fmt.Printf("GroundSpeedKnots: %v\n", m.GroundSpeedKnots) fmt.Printf("GroundSpeedKPH: %v\n", m.GroundSpeedKPH) } ``` Output: ``` $ go run main/main.go Raw sentence: $GPVTG,140.88,T,,M,8.04,N,14.89,K,D*05 TrueTrack: {140.88 true} MagneticTrack: {0 false} GroundSpeedKnots: {8.04 true} GroundSpeedKPH: {14.89 true} ``` ## 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.10.0/aam.go000066400000000000000000000035471465514462400142250ustar00rootroot00000000000000package nmea const ( // TypeAAM type of AAM sentence for Waypoint Arrival Alarm TypeAAM = "AAM" ) // AAM - Waypoint Arrival Alarm // This sentence is generated by some units to indicate the status of arrival (entering the arrival circle, or passing // the perpendicular of the course line) at the destination waypoint (source: GPSD). // https://gpsd.gitlab.io/gpsd/NMEA.html#_aam_waypoint_arrival_alarm // // Format: $--AAM,A,A,x.x,N,c--c*hh // Example: $GPAAM,A,A,0.10,N,WPTNME*43 type AAM struct { BaseSentence // StatusArrivalCircleEntered is warning of arrival to waypoint circle // * A = Arrival Circle Entered // * V = not entered StatusArrivalCircleEntered string // StatusPerpendicularPassed is warning for perpendicular passing of waypoint // * A = Perpendicular passed at waypoint // * V = not passed StatusPerpendicularPassed string // ArrivalCircleRadius is radius for arrival circle ArrivalCircleRadius float64 // ArrivalCircleRadiusUnit is unit for arrival circle radius ArrivalCircleRadiusUnit string // DestinationWaypointID is destination waypoint ID DestinationWaypointID string } // newAAM constructor func newAAM(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeAAM) return AAM{ BaseSentence: s, StatusArrivalCircleEntered: p.EnumString(0, "arrival circle entered status", WPStatusArrivalCircleEnteredA, WPStatusArrivalCircleEnteredV), StatusPerpendicularPassed: p.EnumString(1, "perpendicularly passed status", WPStatusPerpendicularPassedA, WPStatusPerpendicularPassedV), ArrivalCircleRadius: p.Float64(2, "arrival circle radius"), ArrivalCircleRadiusUnit: p.EnumString(3, "arrival circle radius units", DistanceUnitKilometre, DistanceUnitNauticalMile, DistanceUnitStatuteMile, DistanceUnitMetre), DestinationWaypointID: p.String(4, "destination waypoint ID"), }, p.Err() } go-nmea-1.10.0/aam_test.go000066400000000000000000000025211465514462400152530ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestAAM(t *testing.T) { var tests = []struct { name string raw string err string msg AAM }{ { name: "good sentence", raw: "$GPAAM,A,A,0.10,N,WPTNME*32", msg: AAM{ StatusArrivalCircleEntered: WPStatusArrivalCircleEnteredA, StatusPerpendicularPassed: WPStatusPerpendicularPassedA, ArrivalCircleRadius: 0.1, ArrivalCircleRadiusUnit: DistanceUnitNauticalMile, DestinationWaypointID: "WPTNME", }, }, { name: "invalid nmea: StatusArrivalCircleEntered", raw: "$GPAAM,x,A,0.10,N,WPTNME*0B", err: "nmea: GPAAM invalid arrival circle entered status: x", }, { name: "invalid nmea: StatusPerpendicularPassed", raw: "$GPAAM,A,x,0.10,N,WPTNME*0B", err: "nmea: GPAAM invalid perpendicularly passed status: x", }, { name: "invalid nmea: DistanceUnitNauticalMile", raw: "$GPAAM,A,A,0.10,x,WPTNME*04", err: "nmea: GPAAM invalid arrival circle radius units: x", }, } for _, tt := range tests { 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) aam := m.(AAM) aam.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, aam) } }) } } go-nmea-1.10.0/abm.go000066400000000000000000000034731465514462400142240ustar00rootroot00000000000000package nmea const ( // TypeABM type of ABM sentence for AIS addressed binary and safety related message TypeABM = "ABM" ) // ABM - AIS addressed binary and safety related message // https://fcc.report/FCC-ID/ADB9ZWRTR100/2768717.pdf (page 6) FURUNO MARINE RADAR, model FAR-15XX manual // // Format: !--ABM,x,x,x,xxxxxxxxx,x,xx,s--s,x,*hh // Example: !AIABM,26,2,1,3381581370,3,8,177KQJ5000G?tO`K>RA1wUbN0TKH,0*02 type ABM struct { BaseSentence // NumFragments is total number of fragments/sentences need to transfer the message (1 - 9) NumFragments int64 // 0 // FragmentNumber is current fragment/sentence number (1 - 9) FragmentNumber int64 // 1 // MessageID is sequential message identifier (0 - 3) MessageID int64 // 2 // MMSI is The MMSI of destination AIS unit for the ITU-R M.1371 message (10 digits or empty) MMSI string // 3 // Channel is AIS channel for broadcast of the radio message (0 - 3) // 0 - no broadcast // 1 - on AIS channel A // 2 - on AIS channel B // 3 - broadcast on both AIS channels Channel string // 4 // VDLMessageNumber is VDL message number (6/12), see ITU-R M.1371 VDLMessageNumber int64 // 5 // Payload is encapsulated data (6 bit binary-converted data) (1 - 63 bytes) Payload []byte // 6 // 7 - Number of fill bits (0 - 5) } // newABM constructor func newABM(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeABM) return ABM{ BaseSentence: s, NumFragments: p.Int64(0, "number of fragments"), FragmentNumber: p.Int64(1, "fragment number"), MessageID: p.Int64(2, "message ID"), MMSI: p.String(3, "MMSI"), Channel: p.String(4, "channel"), VDLMessageNumber: p.Int64(5, "VDL message number"), Payload: p.SixBitASCIIArmour(6, int(p.Int64(7, "number of padding bits")), "payload"), }, p.Err() } go-nmea-1.10.0/abm_test.go000066400000000000000000000077621465514462400152700ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestABM(t *testing.T) { var tests = []struct { name string raw string err string msg ABM }{ { name: "Good single fragment message", raw: "!AIABM,26,2,1,3381581370,3,8,177KQJ5000G?tO`K>RA1wUbN0TKH,0*02", msg: ABM{ NumFragments: 26, FragmentNumber: 2, MessageID: 1, MMSI: "3381581370", Channel: "3", VDLMessageNumber: 8, Payload: []byte{ 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, // 10 0x1, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x0, 0x1, // 20 0x1, 0x0, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, // 30 0x0, 0x1, 0x1, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, // 40 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // 50 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // 60 0x0, 0x1, 0x0, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, // 70 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x1, // 80 0x1, 0x1, 0x1, 0x1, 0x1, 0x0, 0x1, 0x0, 0x0, 0x0, // 90 0x0, 0x1, 0x1, 0x0, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, // 100 0x1, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x1, // 110 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, // 120 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, // 130 0x0, 0x1, 0x1, 0x0, 0x1, 0x0, 0x1, 0x0, 0x0, 0x1, // 140 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // 150 0x1, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, // 160 0x1, 0x1, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, // 168 }, }, }, { name: "Good single fragment message with padding", raw: "!AIABM,26,2,1,3381581370,3,8,H77nSfPh4U=RA1wUbN0TKH,0*7e", err: "nmea: AIABM invalid number of fragments: x", }, { name: "Invalid VDLMessageNumber", raw: "!AIABM,26,2,1,3381581370,3,x,177KQJ5000G?tO`K>RA1wUbN0TKH,0*42", err: "nmea: AIABM invalid VDL message number: x", }, { name: "Invalid symbol in payload", raw: "!AIABM,26,2,1,3381581370,3,8,1 1,0*5b", err: "nmea: AIABM invalid payload: data byte", }, { name: "Negative number of fill bits", raw: "!AIABM,26,2,1,3381581370,3,8,177KQJ5000G?tO`K>RA1wUbN0TKH,-1*2e", err: "nmea: AIABM invalid payload: fill bits", }, { name: "Too high number of fill bits", raw: "!AIABM,26,2,1,3381581370,3,8,177KQJ5000G?tO`K>RA1wUbN0TKH,20*30", err: "nmea: AIABM invalid payload: fill bits", }, { name: "Negative number of bits", raw: "!AIABM,26,2,1,3381581370,3,8,,2*79", err: "nmea: AIABM invalid payload: num bits", }, } for _, tt := range tests { 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) abm := m.(ABM) abm.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, abm) } }) } } go-nmea-1.10.0/ack.go000066400000000000000000000015711465514462400142200ustar00rootroot00000000000000package nmea const ( // TypeACK type of ACK sentence for alert acknowledge TypeACK = "ACK" ) // ACK - Acknowledge. This sentence is used to acknowledge an alarm condition reported by a device. // http://www.nmea.de/nmea0183datensaetze.html#ack // https://www.furuno.it/docs/INSTALLATION%20MANUALgp170_installation_manual.pdf GPS NAVIGATOR Model GP-170 (page 42) // https://www.manualslib.com/manual/2226813/Jrc-Jln-900.html?page=239#manual (JRC JLN-900: Installation And Instruction Manual) // // Format: $--ACK,xxx*hh // Example: $VRACK,001*50 type ACK struct { BaseSentence // AlertIdentifier is alert identifier (001 to 99999) AlertIdentifier int64 // 0 } // newACKN constructor func newACK(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeACK) return ACK{ BaseSentence: s, AlertIdentifier: p.Int64(0, "alert identifier"), }, p.Err() } go-nmea-1.10.0/ack_test.go000066400000000000000000000013441465514462400152550ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestACK(t *testing.T) { var tests = []struct { name string raw string err string msg ACK }{ { name: "good sentence", raw: "$VRACK,001*50", msg: ACK{ AlertIdentifier: 1, }, }, { name: "invalid nmea: AlertIdentifier", raw: "$VRACK,x*19", err: "nmea: VRACK invalid alert identifier: x", }, } for _, tt := range tests { 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) ack := m.(ACK) ack.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, ack) } }) } } go-nmea-1.10.0/acn.go000066400000000000000000000035111465514462400142170ustar00rootroot00000000000000package nmea const ( // TypeACN type of ACN sentence for alert command TypeACN = "ACN" ) // ACN - Alert command. Used for acknowledge, silence, responsibility transfer and to request repeat of alert. // https://www.furuno.it/docs/INSTALLATION%20MANUALIME44900D_FA170.pdf Furuno CLASS A AIS Model FA-170 (page 49) // https://www.furuno.it/docs/INSTALLATION%20MANUALgp170_installation_manual.pdf GPS NAVIGATOR Model GP-170 (page 42) // // Format: $--ACN,hhmmss.ss,AAA,x.x,x.x,A,A*hh // Example: $VRACN,220516,BPMP1,A,A,Bilge pump alarm1*43 type ACN struct { BaseSentence // Time is time of alarm condition change, UTC (000000.00 - 240001.00) Time Time // 0 // ManufacturerMnemonicCode is manufacturer mnemonic code ManufacturerMnemonicCode string // 1 // AlertIdentifier is alert identifier (001 to 99999) AlertIdentifier int64 // 2 // AlertInstance is alert instance AlertInstance int64 // 3 // Command is Alert command // * A - acknowledge, // * Q - request/repeat information // * O - responsibility transfer // * S - silence Command string // 4 // State is alarm state // * C - command // * possible more classifier values but these are not mentioned in manual State string // 5 } // newACN constructor func newACN(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeACN) return ACN{ BaseSentence: s, Time: p.Time(0, "time"), ManufacturerMnemonicCode: p.String(1, "manufacturer mnemonic code"), AlertIdentifier: p.Int64(2, "alert identifier"), AlertInstance: p.Int64(3, "alert instance"), Command: p.EnumString(4, "alert command", AlertCommandAcknowledge, AlertCommandRequestRepeatInformation, AlertCommandResponsibilityTransfer, AlertCommandSilence), State: p.String(5, "alarm state"), }, p.Err() } go-nmea-1.10.0/acn_test.go000066400000000000000000000027161465514462400152640ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestACN(t *testing.T) { var tests = []struct { name string raw string err string msg ACN }{ { name: "good sentence", raw: "$RAACN,220516,TCK,002,1,A,C*00", msg: ACN{ Time: Time{ Valid: true, Hour: 22, Minute: 05, Second: 16, Millisecond: 0, }, ManufacturerMnemonicCode: "TCK", AlertIdentifier: 2, AlertInstance: 1, Command: AlertCommandAcknowledge, State: "C", }, }, { name: "invalid nmea: Time", raw: "$RAACN,2x0516,TCK,002,1,A,C*4a", err: "nmea: RAACN invalid time: 2x0516", }, { name: "invalid nmea: AlertIdentifier", raw: "$RAACN,220516,TCK,x02,1,A,C*48", err: "nmea: RAACN invalid alert identifier: x02", }, { name: "invalid nmea: AlertInstance", raw: "$RAACN,220516,TCK,002,x,A,C*49", err: "nmea: RAACN invalid alert instance: x", }, { name: "invalid nmea: Command", raw: "$RAACN,220516,TCK,002,1,x,C*39", err: "nmea: RAACN invalid alert command: x", }, } for _, tt := range tests { 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) acn := m.(ACN) acn.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, acn) } }) } } go-nmea-1.10.0/ala.go000066400000000000000000000042301465514462400142120ustar00rootroot00000000000000package nmea const ( // TypeALA type of ALA sentence for System Faults and alarms TypeALA = "ALA" ) // ALA - System Faults and alarms // Source: "Interfacing Voyage Data Recorder Systems, AutroSafe Interactive Fire-Alarm System, 116-P-BSL336/EE, RevA 2007-01-25, // Autronica Fire and Security AS " (page 31 | p.8.1.3) // https://product.autronicafire.com/fileshare/fileupload/14251/bsl336_ee.pdf // // Format: $FRALA,hhmmss,aa,aa,xx,xxx,a,a,c-cc*hh // Example: $FRALA,143955,FR,OT,00,901,N,V,Syst Fault : AutroSafe comm. OK*4F type ALA struct { BaseSentence // Time is Event Time Time Time // SystemIndicator is system indicator of original alarm source. Detector system type with 2 char identifier. // Values not known // https://www.nmea.org/Assets/20190303%20nmea%200183%20talker%20identifier%20mnemonics.pdf SystemIndicator string // SubSystemIndicator is sub system equipment indicator of original alarm source SubSystemIndicator string // InstanceNumber is instance number of equipment/unit/item (00-99) InstanceNumber int64 // Type is alarm type (000-999) Type int64 // Condition describes the condition triggering current message // * N – Normal state (OK) // * H - Alarm state (fault); // could be more Condition string // AlarmAckState is Alarm's acknowledge state // * A – Acknowledged // * H - Harbour mode // * V – Not acknowledged // * O - Override // could be more AlarmAckState string // Message's description text (could be cut to fit max packet length) Message string } // newALA constructor func newALA(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeALA) return ALA{ BaseSentence: s, Time: p.Time(0, "time"), SystemIndicator: p.String(1, "system indicator"), SubSystemIndicator: p.String(2, "subsystem indicator"), InstanceNumber: p.Int64(3, "instance number"), Type: p.Int64(4, "type"), Condition: p.String(5, "condition"), // string as there could be more AlarmAckState: p.String(6, "alarm acknowledgement state"), // string as there could be more Message: p.String(7, "message"), }, p.Err() } go-nmea-1.10.0/ala_test.go000066400000000000000000000027471465514462400152640ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestALA(t *testing.T) { var tests = []struct { name string raw string err string msg ALA }{ { name: "good sentence", raw: "$FRALA,143955,FR,OT,00,901,N,V,Syst Fault : AutroSafe comm. OK*4F", msg: ALA{ Time: Time{ Valid: true, Hour: 14, Minute: 39, Second: 55, Millisecond: 0, }, SystemIndicator: "FR", SubSystemIndicator: "OT", InstanceNumber: 0, Type: 901, Condition: "N", AlarmAckState: "V", Message: "Syst Fault : AutroSafe comm. OK", }, }, { name: "invalid nmea: Time", raw: "$FRALA,1x3955,FR,OT,00,901,N,V,Syst Fault : AutroSafe comm. OK*03", err: "nmea: FRALA invalid time: 1x3955", }, { name: "invalid nmea: InstanceNumber", raw: "$FRALA,143955,FR,OT,x0,901,N,V,Syst Fault : AutroSafe comm. OK*07", err: "nmea: FRALA invalid instance number: x0", }, { name: "invalid nmea: Type", raw: "$FRALA,143955,FR,OT,00,9x1,N,V,Syst Fault : AutroSafe comm. OK*07", err: "nmea: FRALA invalid type: 9x1", }, } for _, tt := range tests { 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) ala := m.(ALA) ala.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, ala) } }) } } go-nmea-1.10.0/alc.go000066400000000000000000000045411465514462400142210ustar00rootroot00000000000000package nmea import "errors" const ( // TypeALC type of ALC sentence for cyclic alert list TypeALC = "ALC" ) // ALC - Cyclic alert list // https://fcc.report/FCC-ID/ADB9ZWRTR100/2768717.pdf (page 6) FURUNO MARINE RADAR, model FAR-15XX manual // // Format: $--ALC,xx,xx,xx,xx, aaa,x.x,x.x,x.x,’’’’’’’’’,*hh // Example: $FBALC,02,01,03,01,FEB,01,02,03*0A type ALC struct { BaseSentence // NumFragments is total number of ALC sentences this message (01, 16) NumFragments int64 // 0 // FragmentNumber is current fragment/sentence number (01 - 16) FragmentNumber int64 // 1 // MessageID is sequential message identifier (00 - 99) MessageID int64 // 2 // Number of alert entries (0 - 3) EntriesNumber int64 // 3 // Additional alert entries. Each entry identifies a certain alert with a certain state. // It is not allowed that an alert entry is split between two ALC sentences AlertEntries []ALCAlertEntry // 4 } // ALCAlertEntry is instance of alert entry for ALC sentence type ALCAlertEntry struct { // ManufacturerMnemonicCode is manufacturer mnemonic code ManufacturerMnemonicCode string // i+4 // AlertIdentifier is alert identifier (001 to 99999) AlertIdentifier int64 // i+5 // AlertInstance is alert instance AlertInstance int64 // i+6 // RevisionCounter is revision counter (1 - 99) RevisionCounter int64 // i+7 } // newALC constructor func newALC(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeALC) alc := ALC{ BaseSentence: s, NumFragments: p.Int64(0, "number of fragments"), FragmentNumber: p.Int64(1, "fragment number"), MessageID: p.Int64(2, "message ID"), EntriesNumber: p.Int64(3, "entries number"), AlertEntries: nil, } fieldCount := len(p.Fields) if fieldCount == 4 { return alc, p.Err() } if fieldCount%4 != 0 { return alc, errors.New("ALC data set field count is not exactly dividable by 4") } alc.AlertEntries = make([]ALCAlertEntry, 0, (fieldCount-4)/4) for i := 4; i < fieldCount; i = i + 4 { tmp := ALCAlertEntry{ ManufacturerMnemonicCode: p.String(i, "manufacturer mnemonic code"), AlertIdentifier: p.Int64(i+1, "alert identifier"), AlertInstance: p.Int64(i+2, "alert instance"), RevisionCounter: p.Int64(i+3, "revision counter"), } alc.AlertEntries = append(alc.AlertEntries, tmp) } return alc, p.Err() } go-nmea-1.10.0/alc_test.go000066400000000000000000000055651465514462400152670ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestALC(t *testing.T) { var tests = []struct { name string raw string err string msg ALC }{ { name: "good sentence, single entry", raw: "$FBALC,02,01,03,01,FEB,01,02,03*0A", msg: ALC{ NumFragments: 2, FragmentNumber: 1, MessageID: 3, EntriesNumber: 1, AlertEntries: []ALCAlertEntry{ { ManufacturerMnemonicCode: "FEB", AlertIdentifier: 01, AlertInstance: 02, RevisionCounter: 03, }, }, }, }, { name: "good sentence, multiple entries", raw: "$FBALC,02,01,03,02,FEB,01,02,03,TEB,02,03,04*5f", msg: ALC{ NumFragments: 2, FragmentNumber: 1, MessageID: 3, EntriesNumber: 2, AlertEntries: []ALCAlertEntry{ { ManufacturerMnemonicCode: "FEB", AlertIdentifier: 01, AlertInstance: 02, RevisionCounter: 03, }, { ManufacturerMnemonicCode: "TEB", AlertIdentifier: 02, AlertInstance: 03, RevisionCounter: 04, }, }, }, }, { name: "good sentence, no entries", raw: "$FBALC,02,01,03,00*4a", msg: ALC{ NumFragments: 2, FragmentNumber: 1, MessageID: 3, EntriesNumber: 0, AlertEntries: nil, }, }, { name: "invalid nmea: invalid number of fields", raw: "$FBALC,02,01,03,01,FEB,01,02*25", err: "ALC data set field count is not exactly dividable by 4", }, { name: "invalid nmea: NumFragments", raw: "$FBALC,0x,01,03,01,FEB,01,02,03*40", err: "nmea: FBALC invalid number of fragments: 0x", }, { name: "invalid nmea: FragmentNumber", raw: "$FBALC,02,0a,03,01,FEB,01,02,03*5a", err: "nmea: FBALC invalid fragment number: 0a", }, { name: "invalid nmea: MessageID", raw: "$FBALC,02,01,0x,01,FEB,01,02,03*41", err: "nmea: FBALC invalid message ID: 0x", }, { name: "invalid nmea: EntriesNumber", raw: "$FBALC,02,01,03,0x,FEB,01,02,03*43", err: "nmea: FBALC invalid entries number: 0x", }, { name: "invalid nmea: AlertIdentifier", raw: "$FBALC,02,01,03,01,FEB,0x,02,03*43", err: "nmea: FBALC invalid alert identifier: 0x", }, { name: "invalid nmea: AlertInstance", raw: "$FBALC,02,01,03,01,FEB,01,0x,03*40", err: "nmea: FBALC invalid alert instance: 0x", }, { name: "invalid nmea: RevisionCounter", raw: "$FBALC,02,01,03,01,FEB,01,02,0x*41", err: "nmea: FBALC invalid revision counter: 0x", }, } for _, tt := range tests { 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) arc := m.(ALC) arc.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, arc) } }) } } go-nmea-1.10.0/alf.go000066400000000000000000000054621465514462400142270ustar00rootroot00000000000000package nmea const ( // TypeALF type of ALF sentence for alert sentence TypeALF = "ALF" ) // ALF - Alert sentence // https://fcc.report/FCC-ID/ADB9ZWRTR100/2768717.pdf (page 6) FURUNO MARINE RADAR, model FAR-15XX manual // https://www.rcom.nl/wp-content/uploads/2017/09/NSR-NGR-3000-Users-Manual.pdf (page 47) // // Format: $--ALF,x,x,x,hhmmss.ss,a,a,a,aaa,x.x,x.x,x.x,x,c--c,*hh // Example: $VDALF,1,0,1,220516,B,A,S,SAL,001,1,2,0,My alarm*2c type ALF struct { BaseSentence // NumFragments is total number of ALF sentences this message (1, 2) NumFragments int64 // 0 // FragmentNumber is current fragment/sentence number (1 - 2) FragmentNumber int64 // 1 // MessageID is sequential message identifier (0 - 9) MessageID int64 // 2 // Time is time of last change (000000.00 - 240001.00 / null) Time Time // 3 // Category is alert category (A/B/C/null) // A - Alert category A, // B - Alert category B, // C - Alert category C, Category string // 4 // Priority is alert priority (A/W/C/null) // E - Emergency Alarm: E, for use with Bridge alert management // A - Alarm, // W - Warning, // C - Caution, // null Priority string // 5 // State is alert state (A/S/O/U/V/N/null) // A - Acknowledged // S - Silence // O - Active-responsiblity transferred // U - Rectified-unacknowledged // V - Not acknowledged // N - Normal state // null State string // 6 // ManufacturerMnemonicCode is manufacturer mnemonic code ManufacturerMnemonicCode string // 7 // AlertIdentifier is alert identifier (001 to 99999) AlertIdentifier int64 // 8 // AlertInstance is alert instance AlertInstance int64 // 9 // RevisionCounter is revision counter (1 - 99) RevisionCounter int64 // 10 // EscalationCounter is escalation counter (0 - 9) EscalationCounter int64 // 11 // Text is alarm text Text string // 12 } // newALF constructor func newALF(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeALF) return ALF{ BaseSentence: s, NumFragments: p.Int64(0, "number of fragments"), FragmentNumber: p.Int64(1, "fragment number"), MessageID: p.Int64(2, "message ID"), Time: p.Time(3, "time"), Category: p.EnumString(4, "alarm category", "A", "B", "C"), Priority: p.EnumString(5, "alarm priority", "E", "A", "C", "W"), State: p.EnumString(6, "alarm state", "A", "S", "O", "U", "V", "N"), ManufacturerMnemonicCode: p.String(7, "manufacturer mnemonic code"), AlertIdentifier: p.Int64(8, "alert identifier"), AlertInstance: p.Int64(9, "alert instance"), RevisionCounter: p.Int64(10, "revision counter"), EscalationCounter: p.Int64(11, "escalation counter"), Text: p.String(12, "alert text"), }, p.Err() } go-nmea-1.10.0/alf_test.go000066400000000000000000000055501465514462400152640ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestALF(t *testing.T) { var tests = []struct { name string raw string err string msg ALF }{ { name: "good sentence", raw: "$VDALF,1,0,1,220516,B,A,S,SAL,001,1,2,0,My alarm*2c", msg: ALF{ NumFragments: 1, FragmentNumber: 0, MessageID: 1, Time: Time{ Valid: true, Hour: 22, Minute: 05, Second: 16, Millisecond: 0, }, Category: "B", Priority: "A", State: "S", ManufacturerMnemonicCode: "SAL", AlertIdentifier: 1, AlertInstance: 1, RevisionCounter: 2, EscalationCounter: 0, Text: "My alarm", }, }, { name: "invalid nmea: NumFragments", raw: "$VDALF,x,0,1,220516,B,A,S,SAL,001,1,2,0,My alarm*65", err: "nmea: VDALF invalid number of fragments: x", }, { name: "invalid nmea: FragmentNumber", raw: "$VDALF,1,x,1,220516,B,A,S,SAL,001,1,2,0,My alarm*64", err: "nmea: VDALF invalid fragment number: x", }, { name: "invalid nmea: MessageID", raw: "$VDALF,1,0,x,220516,B,A,S,SAL,001,1,2,0,My alarm*65", err: "nmea: VDALF invalid message ID: x", }, { name: "invalid nmea: Time", raw: "$VDALF,1,0,1,2x0516,B,A,S,SAL,001,1,2,0,My alarm*66", err: "nmea: VDALF invalid time: 2x0516", }, { name: "invalid nmea: Category", raw: "$VDALF,1,0,1,220516,x,A,S,SAL,001,1,2,0,My alarm*16", err: "nmea: VDALF invalid alarm category: x", }, { name: "invalid nmea: Priority", raw: "$VDALF,1,0,1,220516,B,x,S,SAL,001,1,2,0,My alarm*15", err: "nmea: VDALF invalid alarm priority: x", }, { name: "invalid nmea: State", raw: "$VDALF,1,0,1,220516,B,A,x,SAL,001,1,2,0,My alarm*07", err: "nmea: VDALF invalid alarm state: x", }, { name: "invalid nmea: AlertIdentifier", raw: "$VDALF,1,0,1,220516,B,A,S,SAL,x01,1,2,0,My alarm*64", err: "nmea: VDALF invalid alert identifier: x01", }, { name: "invalid nmea: AlertInstance", raw: "$VDALF,1,0,1,220516,B,A,S,SAL,001,x,2,0,My alarm*65", err: "nmea: VDALF invalid alert instance: x", }, { name: "invalid nmea: RevisionCounter", raw: "$VDALF,1,0,1,220516,B,A,S,SAL,001,1,x,0,My alarm*66", err: "nmea: VDALF invalid revision counter: x", }, { name: "invalid nmea: EscalationCounter", raw: "$VDALF,1,0,1,220516,B,A,S,SAL,001,1,2,x,My alarm*64", err: "nmea: VDALF invalid escalation counter: x", }, } for _, tt := range tests { 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) alf := m.(ALF) alf.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, alf) } }) } } go-nmea-1.10.0/alr.go000066400000000000000000000024411465514462400142350ustar00rootroot00000000000000package nmea const ( // TypeALR type of ALR sentence for alert command refused TypeALR = "ALR" ) // ALR - Set alarm state // https://fcc.report/FCC-ID/ADB9ZWRTR100/2768717.pdf (page 7) FURUNO MARINE RADAR, model FAR-15XX manual // // Format: $--ALR,hhmmss.ss,xxx,A,A,c--c,*hh // Example: $RAALR,220516,BPMP1,A,A,Bilge pump alarm1*43 type ALR struct { BaseSentence // Time is time of alarm condition change, UTC (000000.00 - 240001.00) Time Time // 0 // AlarmIdentifier is unique alarm number (identifier) at alarm source AlarmIdentifier int64 // 1 // AlarmCondition is alarm condition (A/V) // A - threshold exceeded // V - not exceeded Condition string // 2 // State is alarm state (A/V) // A - acknowledged // V - not acknowledged State string // 3 // Description is alarm description text Description string // 4 } // newALR constructor func newALR(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeALR) return ALR{ BaseSentence: s, Time: p.Time(0, "time"), AlarmIdentifier: p.Int64(1, "unique alarm number"), Condition: p.EnumString(2, "alarm condition", StatusValid, StatusInvalid), State: p.EnumString(3, "alarm state", StatusValid, StatusInvalid), Description: p.String(4, "description"), }, p.Err() } go-nmea-1.10.0/alr_test.go000066400000000000000000000032111465514462400152700ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestALR(t *testing.T) { var tests = []struct { name string raw string err string msg ALR }{ { name: "good sentence", raw: "$RAALR,220516,001,A,A,Bilge pump alarm1*4c", msg: ALR{ Time: Time{ Valid: true, Hour: 22, Minute: 05, Second: 16, Millisecond: 0, }, AlarmIdentifier: 1, Condition: StatusValid, State: StatusValid, Description: "Bilge pump alarm1", }, }, { name: "nmea: Description empty", raw: "$RAALR,220516,001,A,A,*53", msg: ALR{ Time: Time{ Valid: true, Hour: 22, Minute: 05, Second: 16, Millisecond: 0, }, AlarmIdentifier: 1, Condition: StatusValid, State: StatusValid, Description: "", }, }, { name: "invalid nmea: Time", raw: "$RAALR,2x0516,001,A,A,Bilge pump alarm1*06", err: "nmea: RAALR invalid time: 2x0516", }, { name: "invalid nmea: Condition", raw: "$RAALR,220516,001,x,A,Bilge pump alarm1*75", err: "nmea: RAALR invalid alarm condition: x", }, { name: "invalid nmea: State", raw: "$RAALR,220516,001,A,x,Bilge pump alarm1*75", err: "nmea: RAALR invalid alarm state: x", }, } for _, tt := range tests { 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) alr := m.(ALR) alr.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, alr) } }) } } go-nmea-1.10.0/apb.go000066400000000000000000000120721465514462400142220ustar00rootroot00000000000000package nmea const ( // TypeAPB type of APB sentence for Autopilot Sentence "B" TypeAPB = "APB" // StatusWarningASetAPB indicates LORAN-C Blink or SNR warning StatusWarningASetAPB = "V" // StatusWarningAClearORNotUsedAPB general warning flag or other navigation systems when a reliable fix is not available StatusWarningAClearORNotUsedAPB = "A" // StatusWarningBSetAPB means Loran-C Cycle Lock warning OK or not used StatusWarningBSetAPB = "A" // StatusWarningBClearAPB means Loran-C Cycle Lock warning flag StatusWarningBClearAPB = "V" ) // Autopilot related constants (used in APB, APA, AAM) const ( // WPStatusPerpendicularPassedA is warning for passing the perpendicular of the course line of waypoint WPStatusPerpendicularPassedA = "A" // WPStatusPerpendicularPassedV indicates for not passing of the perpendicular of the course line of waypoint WPStatusPerpendicularPassedV = "V" // WPStatusArrivalCircleEnteredA is warning of entering to waypoint circle WPStatusArrivalCircleEnteredA = "A" // WPStatusArrivalCircleEnteredV indicates of not yet entered into waypoint circle WPStatusArrivalCircleEnteredV = "V" ) // APB - Autopilot Sentence "B" for heading/tracking // https://gpsd.gitlab.io/gpsd/NMEA.html#_apb_autopilot_sentence_b // https://www.tronico.fi/OH6NT/docs/NMEA0183.pdf (page 5) // // Format: $--APB,A,A,x.x,a,N,A,A,x.x,a,c--c,x.x,a,x.x,a*hh // Format NMEA 2.3+: $--APB,A,A,x.x,a,N,A,A,x.x,a,c--c,x.x,a,x.x,a,a*hh // Example: $GPAPB,A,A,0.10,R,N,V,V,011,M,DEST,011,M,011,M*82 // $ECAPB,A,A,0.0,L,M,V,V,175.2,T,Antechamber_Bay,175.2,T,175.2,T*48 type APB struct { BaseSentence // StatusGeneralWarning is used for warnings // * V = LORAN-C Blink or SNR warning // * A = general warning flag or other navigation systems when a reliable fix is not available StatusGeneralWarning string // StatusLockWarning is used for lock warning // * V = Loran-C Cycle Lock warning flag // * A = OK or not used StatusLockWarning string // CrossTrackErrorMagnitude is Cross Track Error Magnitude CrossTrackErrorMagnitude float64 // DirectionToSteer is Direction to steer, // * L = left // * R = right DirectionToSteer string // CrossTrackUnits is cross track units // * N = nautical miles // * K = for kilometers CrossTrackUnits string // StatusArrivalCircleEntered is warning of arrival to waypoint circle // * A = Arrival Circle Entered // * V = not entered StatusArrivalCircleEntered string // StatusPerpendicularPassed is warning for perpendicular passing of waypoint // * A = Perpendicular passed at waypoint // * V = not passed StatusPerpendicularPassed string // BearingOriginToDest is Bearing origin to destination BearingOriginToDest float64 // BearingOriginToDestType is Bearing origin to dest type // * M = Magnetic // * T = True BearingOriginToDestType string // DestinationWaypointID is Destination waypoint ID DestinationWaypointID string // BearingPresentToDest is Bearing, present position to Destination BearingPresentToDest float64 // BearingPresentToDestType is Bearing present to dest type // * M = Magnetic // * T = True BearingPresentToDestType string // Heading is heading to steer to destination waypoint Heading float64 // HeadingType is Heading type // * M = Magnetic // * T = True HeadingType string // FAA mode indicator (filled in NMEA 2.3 and later) FFAMode string } // newAPB constructor func newAPB(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeAPB) apb := APB{ BaseSentence: s, StatusGeneralWarning: p.EnumString(0, "general warning", StatusWarningAClearORNotUsedAPB, StatusWarningASetAPB), StatusLockWarning: p.EnumString(1, "lock warning", StatusWarningBSetAPB, StatusWarningBClearAPB), CrossTrackErrorMagnitude: p.Float64(2, "cross track error magnitude"), DirectionToSteer: p.EnumString(3, "direction to steer", Left, Right), CrossTrackUnits: p.EnumString(4, "cross track units", DistanceUnitKilometre, DistanceUnitNauticalMile, DistanceUnitStatuteMile, DistanceUnitMetre), StatusArrivalCircleEntered: p.EnumString(5, "arrival circle entered status", WPStatusArrivalCircleEnteredA, WPStatusArrivalCircleEnteredV), StatusPerpendicularPassed: p.EnumString(6, "perpendicularly passed status", WPStatusPerpendicularPassedA, WPStatusPerpendicularPassedV), BearingOriginToDest: p.Float64(7, "origin bearing to destination"), BearingOriginToDestType: p.EnumString(8, "origin bearing to destination type", HeadingMagnetic, HeadingTrue), DestinationWaypointID: p.String(9, "destination waypoint ID"), BearingPresentToDest: p.Float64(10, "present bearing to destination"), BearingPresentToDestType: p.EnumString(11, "present bearing to destination type", HeadingMagnetic, HeadingTrue), Heading: p.Float64(12, "heading"), HeadingType: p.EnumString(13, "heading type", HeadingMagnetic, HeadingTrue), } if len(p.Fields) > 14 { apb.FFAMode = p.String(14, "FAA mode") // not enum because some devices have proprietary "non-nmea" values } return apb, p.Err() } go-nmea-1.10.0/apb_test.go000066400000000000000000000051771465514462400152710ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestAPB(t *testing.T) { var tests = []struct { name string raw string err string msg APB }{ { name: "good sentence", raw: "$GPAPB,A,A,0.10,R,N,V,V,011,M,DEST,011,M,011,M*3C", msg: APB{ StatusGeneralWarning: "A", StatusLockWarning: "A", CrossTrackErrorMagnitude: 0.1, DirectionToSteer: "R", CrossTrackUnits: "N", StatusArrivalCircleEntered: "V", StatusPerpendicularPassed: "V", BearingOriginToDest: 11, BearingOriginToDestType: "M", DestinationWaypointID: "DEST", BearingPresentToDest: 11, BearingPresentToDestType: "M", Heading: 11, HeadingType: "M", FFAMode: "", }, }, { name: "good sentence b with FAA mode", raw: "$ECAPB,A,A,0.0,L,M,V,V,175.2,T,Antechamber_Bay,175.2,T,175.2,T,V*32", msg: APB{ StatusGeneralWarning: "A", StatusLockWarning: "A", CrossTrackErrorMagnitude: 0, DirectionToSteer: "L", CrossTrackUnits: "M", StatusArrivalCircleEntered: "V", StatusPerpendicularPassed: "V", BearingOriginToDest: 175.2, BearingOriginToDestType: "T", DestinationWaypointID: "Antechamber_Bay", BearingPresentToDest: 175.2, BearingPresentToDestType: "T", Heading: 175.2, HeadingType: "T", FFAMode: "V", }, }, { name: "invalid nmea: CrossTrackErrorMagnitude", raw: "$ECAPB,A,A,x.0,L,M,V,V,175.2,T,Antechamber_Bay,175.2,T,175.2,T,V*7A", err: "nmea: ECAPB invalid cross track error magnitude: x.0", }, { name: "invalid nmea: BearingOriginToDest", raw: "$ECAPB,A,A,0.0,L,M,V,V,175.x,T,Antechamber_Bay,175.2,T,175.2,T,V*78", err: "nmea: ECAPB invalid origin bearing to destination: 175.x", }, { name: "invalid nmea: BearingPresentToDest", raw: "$ECAPB,A,A,0.0,L,M,V,V,175.2,T,Antechamber_Bay,175.x,T,175.2,T,V*78", err: "nmea: ECAPB invalid present bearing to destination: 175.x", }, { name: "invalid nmea: Heading", raw: "$ECAPB,A,A,0.0,L,M,V,V,175.2,T,Antechamber_Bay,175.2,T,175.x,T,V*78", err: "nmea: ECAPB invalid heading: 175.x", }, } for _, tt := range tests { 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) apb := m.(APB) apb.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, apb) } }) } } go-nmea-1.10.0/arc.go000066400000000000000000000034011465514462400142210ustar00rootroot00000000000000package nmea const ( // TypeARC type of ARC sentence for alert command refused TypeARC = "ARC" ) const ( // AlertCommandAcknowledge means acknowledge AlertCommandAcknowledge = "A" // AlertCommandRequestRepeatInformation means request/repeat information AlertCommandRequestRepeatInformation = "Q" // AlertCommandResponsibilityTransfer means responsibility transfer AlertCommandResponsibilityTransfer = "O" // AlertCommandSilence means silence AlertCommandSilence = "S" ) // ARC - Alert command refused // https://fcc.report/FCC-ID/ADB9ZWRTR100/2768717.pdf (page 7) FURUNO MARINE RADAR, model FAR-15XX manual // // Format: $--ARC,hhmmss.ss,aaa,x.x,x.x,c*hh // Example: $RAARC,220516,TCK,002,1,A*73 type ARC struct { BaseSentence // Time is UTC Time Time Time // 0 // ManufacturerMnemonicCode is manufacturer mnemonic code ManufacturerMnemonicCode string // 1 // AlertIdentifier is alert identifier (001 to 99999) AlertIdentifier int64 // 2 // AlertInstance is alert instance AlertInstance int64 // 3 // Command is Refused alert command // A - acknowledge // Q - request/repeat information // O - responsibility transfer // S - silence Command string // 4 } // newARC constructor func newARC(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeARC) return ARC{ BaseSentence: s, Time: p.Time(0, "time"), ManufacturerMnemonicCode: p.String(1, "manufacturer mnemonic code"), AlertIdentifier: p.Int64(2, "alert identifier"), AlertInstance: p.Int64(3, "alert instance"), Command: p.EnumString(4, "refused alert command", AlertCommandAcknowledge, AlertCommandRequestRepeatInformation, AlertCommandResponsibilityTransfer, AlertCommandSilence), }, p.Err() } go-nmea-1.10.0/arc_test.go000066400000000000000000000026511465514462400152660ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestARC(t *testing.T) { var tests = []struct { name string raw string err string msg ARC }{ { name: "good sentence", raw: "$RAARC,220516,TCK,002,1,A*73", msg: ARC{ Time: Time{ Valid: true, Hour: 22, Minute: 05, Second: 16, Millisecond: 0, }, ManufacturerMnemonicCode: "TCK", AlertIdentifier: 2, AlertInstance: 1, Command: AlertCommandAcknowledge, }, }, { name: "invalid nmea: Time", raw: "$RAARC,2x0516,TCK,002,1,A*39", err: "nmea: RAARC invalid time: 2x0516", }, { name: "invalid nmea: AlertIdentifier", raw: "$RAARC,220516,TCK,x02,1,A*3b", err: "nmea: RAARC invalid alert identifier: x02", }, { name: "invalid nmea: AlertInstance", raw: "$RAARC,220516,TCK,002,x,A*3a", err: "nmea: RAARC invalid alert instance: x", }, { name: "invalid nmea: Command", raw: "$RAARC,220516,TCK,002,1,x*4a", err: "nmea: RAARC invalid refused alert command: x", }, } for _, tt := range tests { 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) arc := m.(ARC) arc.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, arc) } }) } } go-nmea-1.10.0/bbm.go000066400000000000000000000031441465514462400142200ustar00rootroot00000000000000package nmea const ( // TypeBBM type of BBM sentence for AIS broadcast binary message TypeBBM = "BBM" ) // BBM - AIS broadcast binary message // https://fcc.report/FCC-ID/ADB9ZWRTR100/2768717.pdf (page 7) FURUNO MARINE RADAR, model FAR-15XX manual // // Format: !--BBM,x,x,x,x,xx,s—s,x*hh // Example: !AIBBM,26,2,1,3,8,177KQJ5000G?tO`K>RA1wUbN0TKH,0*2C type BBM struct { BaseSentence // NumFragments is total number of fragments/sentences need to transfer the message (1 - 9) NumFragments int64 // 0 // FragmentNumber is current fragment/sentence number (1 - 9) FragmentNumber int64 // 1 // MessageID is sequential message identifier (0 - 9) MessageID int64 // 2 // Channel is AIS channel for broadcast of the radio message (0 - 3) // 0 - no broadcast // 1 - on AIS channel A // 2 - on AIS channel B // 3 - broadcast on both AIS channels Channel string // 3 // VDLMessageNumber is ITU-r M.1371 message number (8/14) VDLMessageNumber int64 // 4 // Payload is encapsulated data (6 bit binary-converted data) (1 - 63 bytes) Payload []byte // 5 // 6 - Number of fill bits (0 - 5) } // newBBM constructor func newBBM(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeBBM) m := BBM{ BaseSentence: s, NumFragments: p.Int64(0, "number of fragments"), FragmentNumber: p.Int64(1, "fragment number"), MessageID: p.Int64(2, "message ID"), Channel: p.String(3, "channel"), VDLMessageNumber: p.Int64(4, "VDL message number"), Payload: p.SixBitASCIIArmour(5, int(p.Int64(6, "number of padding bits")), "payload"), } return m, p.Err() } go-nmea-1.10.0/bbm_test.go000066400000000000000000000071771465514462400152710ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestBBM(t *testing.T) { var tests = []struct { name string raw string err string msg BBM }{ { name: "Good single fragment message", raw: "!AIBBM,26,2,1,3,8,177KQJ5000G?tO`K>RA1wUbN0TKH,0*2C", msg: BBM{ NumFragments: 26, FragmentNumber: 2, MessageID: 1, Channel: "3", VDLMessageNumber: 8, Payload: []byte{ 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, // 10 0x1, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x0, 0x1, // 20 0x1, 0x0, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, // 30 0x0, 0x1, 0x1, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, // 40 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // 50 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // 60 0x0, 0x1, 0x0, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, // 70 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x1, // 80 0x1, 0x1, 0x1, 0x1, 0x1, 0x0, 0x1, 0x0, 0x0, 0x0, // 90 0x0, 0x1, 0x1, 0x0, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, // 100 0x1, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x1, // 110 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, // 120 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, // 130 0x0, 0x1, 0x1, 0x0, 0x1, 0x0, 0x1, 0x0, 0x0, 0x1, // 140 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // 150 0x1, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, // 160 0x1, 0x1, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, // 168 }, }, }, { name: "Good single fragment message with padding", raw: "!AIBBM,26,2,1,3,8,H77nSfPh4U=RA1wUbN0TKH,0*50", err: "nmea: AIBBM invalid number of fragments: x", }, { name: "Invalid symbol in payload", raw: "!AIBBM,26,2,1,3,8,1 1,0*75", err: "nmea: AIBBM invalid payload: data byte", }, { name: "Negative number of fill bits", raw: "!AIBBM,26,2,1,3,8,177KQJ5000G?tO`K>RA1wUbN0TKH,-1*00", err: "nmea: AIBBM invalid payload: fill bits", }, { name: "Too high number of fill bits", raw: "!AIBBM,26,2,1,3,8,177KQJ5000G?tO`K>RA1wUbN0TKH,20*1e", err: "nmea: AIBBM invalid payload: fill bits", }, { name: "Negative number of bits", raw: "!AIBBM,26,2,1,3,8,,2*57", err: "nmea: AIBBM invalid payload: num bits", }, } for _, tt := range tests { 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) bbm := m.(BBM) bbm.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, bbm) } }) } } go-nmea-1.10.0/bec.go000066400000000000000000000041701465514462400142110ustar00rootroot00000000000000package nmea const ( // TypeBEC type of BEC sentence for bearing and distance to waypoint (dead reckoning) TypeBEC = "BEC" ) // BEC - bearing and distance to waypoint (dead reckoning) // http://www.nmea.de/nmea0183datensaetze.html#bec // https://www.eye4software.com/hydromagic/documentation/nmea0183/ // // Format: $--BEC,hhmmss.ss,llll.ll,a,yyyyy.yy,a,x.x,T,x.x,M,x.x,N,c--c*hh // Example: $GPBEC,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*33 type BEC struct { BaseSentence Time Time // UTC Time Latitude float64 // latitude of waypoint Longitude float64 // longitude of waypoint BearingTrue float64 // true bearing in degrees BearingTrueValid bool // is unit of true bearing valid BearingMagnetic float64 // magnetic bearing in degrees BearingMagneticValid bool // is unit of magnetic bearing valid DistanceNauticalMiles float64 // distance to waypoint in nautical miles DistanceNauticalMilesValid bool // is unit of distance to waypoint nautical miles valid DestinationWaypointID string // destination waypoint ID } // newBEC constructor func newBEC(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeBEC) return BEC{ BaseSentence: s, Time: p.Time(0, "time"), Latitude: p.LatLong(1, 2, "latitude"), Longitude: p.LatLong(3, 4, "longitude"), BearingTrue: p.Float64(5, "true bearing"), BearingTrueValid: p.EnumString(6, "true bearing unit valid", BearingTrue) == BearingTrue, BearingMagnetic: p.Float64(7, "magnetic bearing"), BearingMagneticValid: p.EnumString(8, "magnetic bearing unit valid", BearingMagnetic) == BearingMagnetic, DistanceNauticalMiles: p.Float64(9, "distance to waypoint is nautical miles"), DistanceNauticalMilesValid: p.EnumString(10, "is distance to waypoint nautical miles valid", DistanceUnitNauticalMile) == DistanceUnitNauticalMile, DestinationWaypointID: p.String(11, "destination waypoint ID"), }, p.Err() } go-nmea-1.10.0/bec_test.go000066400000000000000000000036041465514462400152510ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestBEC(t *testing.T) { var tests = []struct { name string raw string err string msg BEC }{ { name: "good sentence", raw: "$GPBEC,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*33", msg: BEC{ Time: Time{ Valid: true, Hour: 22, Minute: 5, Second: 16, Millisecond: 0, }, Latitude: 51.50033333333334, Longitude: -0.7723333333333334, BearingTrue: 213.8, BearingTrueValid: true, BearingMagnetic: 218, BearingMagneticValid: true, DistanceNauticalMiles: 4.6, DistanceNauticalMilesValid: true, DestinationWaypointID: "EGLM", }, }, { name: "invalid nmea: Time", raw: "$GPBEC,2x0516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*79", err: "nmea: GPBEC invalid time: 2x0516", }, { name: "invalid nmea: BearingTrueValid", raw: "$GPBEC,220516,5130.02,N,00046.34,W,213.8,M,218.0,M,0004.6,N,EGLM*2A", err: "nmea: GPBEC invalid true bearing unit valid: M", }, { name: "invalid nmea: BearingMagneticValid", raw: "$GPBEC,220516,5130.02,N,00046.34,W,213.8,T,218.0,T,0004.6,N,EGLM*2A", err: "nmea: GPBEC invalid magnetic bearing unit valid: T", }, { name: "invalid nmea: DistanceNauticalMilesValid", raw: "$GPBEC,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,K,EGLM*36", err: "nmea: GPBEC invalid is distance to waypoint nautical miles valid: K", }, } for _, tt := range tests { 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) bec := m.(BEC) bec.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, bec) } }) } } go-nmea-1.10.0/bod.go000066400000000000000000000033131465514462400142220ustar00rootroot00000000000000package nmea const ( // TypeBOD type of BOD sentence for bearing waypoint to waypoint TypeBOD = "BOD" ) // BOD - bearing waypoint to waypoint (origin to destination). // Replaced by BWW in NMEA4+ (according to GPSD docs) // If your system supports RMB it is better to use RMB as it is more common (according to OpenCPN docs) // https://gpsd.gitlab.io/gpsd/NMEA.html#_bod_bearing_waypoint_to_waypoint // // Format: $--BOD,x.x,T,x.x,M,c--c,c--c*hh // Example: $GPBOD,099.3,T,105.6,M,POINTB*64 // $GPBOD,097.0,T,103.2,M,POINTB,POINTA*4A type BOD struct { BaseSentence BearingTrue float64 // true bearing in degrees BearingTrueType string // is type of true bearing BearingMagnetic float64 // magnetic bearing in degrees BearingMagneticType string // is type of magnetic bearing DestinationWaypointID string // destination waypoint ID OriginWaypointID string // origin waypoint ID } // newBOD constructor func newBOD(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeBOD) bod := BOD{ BaseSentence: s, BearingTrue: p.Float64(0, "true bearing"), BearingTrueType: p.EnumString(1, "true bearing type", BearingTrue), BearingMagnetic: p.Float64(2, "magnetic bearing"), BearingMagneticType: p.EnumString(3, "magnetic bearing type", BearingMagnetic), DestinationWaypointID: p.String(4, "destination waypoint ID"), OriginWaypointID: "", } // According to GSPD docs: OriginWaypointID is not transmitted in the GOTO mode, without an active route on your GPS. // in that case you have only DestinationWaypointID if len(p.Fields) > 5 { bod.OriginWaypointID = p.String(5, "origin waypoint ID") } return bod, p.Err() } go-nmea-1.10.0/bod_test.go000066400000000000000000000027471465514462400152730ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestBOD(t *testing.T) { var tests = []struct { name string raw string err string msg BOD }{ { name: "good sentence with both WPs", raw: "$GPBOD,097.0,T,103.2,M,POINTB,POINTA*4A", msg: BOD{ BearingTrue: 97.0, BearingTrueType: BearingTrue, BearingMagnetic: 103.2, BearingMagneticType: BearingMagnetic, DestinationWaypointID: "POINTB", OriginWaypointID: "POINTA", }, }, { name: "good sentence onyl destination", raw: "$GPBOD,099.3,T,105.6,M,POINTB*64", msg: BOD{ BearingTrue: 99.3, BearingTrueType: BearingTrue, BearingMagnetic: 105.6, BearingMagneticType: BearingMagnetic, DestinationWaypointID: "POINTB", OriginWaypointID: "", }, }, { name: "invalid nmea: BearingTrueValid", raw: "$GPBOD,097.0,M,103.2,M,POINTB,POINTA*53", err: "nmea: GPBOD invalid true bearing type: M", }, { name: "invalid nmea: BearingMagneticValid", raw: "$GPBOD,097.0,T,103.2,T,POINTB,POINTA*53", err: "nmea: GPBOD invalid magnetic bearing type: T", }, } for _, tt := range tests { 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) bod := m.(BOD) bod.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, bod) } }) } } go-nmea-1.10.0/bwc.go000066400000000000000000000046101465514462400142320ustar00rootroot00000000000000package nmea const ( // TypeBWC type of BWC sentence for bearing and distance to waypoint, great circle TypeBWC = "BWC" ) // BWC - bearing and distance to waypoint, great circle // https://gpsd.gitlab.io/gpsd/NMEA.html#_bwc_bearing_distance_to_waypoint_great_circle // http://aprs.gids.nl/nmea/#bwc // // Format: $--BWC,hhmmss.ss,llll.ll,a,yyyyy.yy,a,x.x,T,x.x,M,x.x,N,c--c*hh // Format (NMEA 2.3+): $--BWC,hhmmss.ss,llll.ll,a,yyyyy.yy,a,x.x,T,x.x,M,x.x,N,c--c,m*hh // Example: $GPBWC,081837,,,,,,T,,M,,N,*13 // $GPBWC,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*21 type BWC struct { BaseSentence Time Time // UTC Time Latitude float64 // latitude of waypoint Longitude float64 // longitude of waypoint BearingTrue float64 // true bearing in degrees BearingTrueType string // is type of true bearing BearingMagnetic float64 // magnetic bearing in degrees BearingMagneticType string // is type of magnetic bearing DistanceNauticalMiles float64 // distance to waypoint in nautical miles DistanceNauticalMilesUnit string // is unit of distance to waypoint nautical miles DestinationWaypointID string // destination waypoint ID FFAMode string // FAA mode indicator (filled in NMEA 2.3 and later) } // newBWC constructor func newBWC(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeBWC) bwc := BWC{ BaseSentence: s, Time: p.Time(0, "time"), Latitude: p.LatLong(1, 2, "latitude"), Longitude: p.LatLong(3, 4, "longitude"), BearingTrue: p.Float64(5, "true bearing"), BearingTrueType: p.EnumString(6, "true bearing type", BearingTrue), BearingMagnetic: p.Float64(7, "magnetic bearing"), BearingMagneticType: p.EnumString(8, "magnetic bearing type", BearingMagnetic), DistanceNauticalMiles: p.Float64(9, "distance to waypoint is nautical miles"), DistanceNauticalMilesUnit: p.EnumString(10, "is distance to waypoint nautical miles unit", DistanceUnitNauticalMile), DestinationWaypointID: p.String(11, "destination waypoint ID"), } if len(p.Fields) > 12 { bwc.FFAMode = p.String(12, "FAA mode") // not enum because some devices have proprietary "non-nmea" values } return bwc, p.Err() } go-nmea-1.10.0/bwc_test.go000066400000000000000000000063521465514462400152760ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestBWC(t *testing.T) { var tests = []struct { name string raw string err string msg BWC }{ { name: "good sentence", raw: "$GPBWC,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*21", msg: BWC{ Time: Time{ Valid: true, Hour: 22, Minute: 5, Second: 16, Millisecond: 0, }, Latitude: 51.50033333333334, Longitude: -0.7723333333333334, BearingTrue: 213.8, BearingTrueType: BearingTrue, BearingMagnetic: 218, BearingMagneticType: BearingMagnetic, DistanceNauticalMiles: 4.6, DistanceNauticalMilesUnit: DistanceUnitNauticalMile, DestinationWaypointID: "EGLM", FFAMode: "", }, }, { name: "good sentence no waypoint", raw: "$GPBWC,081837,,,,,,T,,M,,N,*13", msg: BWC{ Time: Time{Valid: true, Hour: 8, Minute: 18, Second: 37, Millisecond: 0}, Latitude: 0, Longitude: 0, BearingTrue: 0, BearingTrueType: BearingTrue, BearingMagnetic: 0, BearingMagneticType: BearingMagnetic, DistanceNauticalMiles: 0, DistanceNauticalMilesUnit: DistanceUnitNauticalMile, DestinationWaypointID: "", FFAMode: "", }, }, { name: "good sentence with FAAMode", raw: "$GPBWC,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM,D*49", msg: BWC{ Time: Time{ Valid: true, Hour: 22, Minute: 5, Second: 16, Millisecond: 0, }, Latitude: 51.50033333333334, Longitude: -0.7723333333333334, BearingTrue: 213.8, BearingTrueType: BearingTrue, BearingMagnetic: 218, BearingMagneticType: BearingMagnetic, DistanceNauticalMiles: 4.6, DistanceNauticalMilesUnit: DistanceUnitNauticalMile, DestinationWaypointID: "EGLM", FFAMode: FAAModeDifferential, }, }, { name: "invalid nmea: Time", raw: "$GPBWC,2x0516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*6B", err: "nmea: GPBWC invalid time: 2x0516", }, { name: "invalid nmea: BearingTrueValid", raw: "$GPBWC,220516,5130.02,N,00046.34,W,213.8,M,218.0,M,0004.6,N,EGLM*38", err: "nmea: GPBWC invalid true bearing type: M", }, { name: "invalid nmea: BearingMagneticValid", raw: "$GPBWC,220516,5130.02,N,00046.34,W,213.8,T,218.0,T,0004.6,N,EGLM*38", err: "nmea: GPBWC invalid magnetic bearing type: T", }, { name: "invalid nmea: DistanceNauticalMilesValid", raw: "$GPBWC,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,K,EGLM*24", err: "nmea: GPBWC invalid is distance to waypoint nautical miles unit: K", }, } for _, tt := range tests { 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) bwc := m.(BWC) bwc.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, bwc) } }) } } go-nmea-1.10.0/bwr.go000066400000000000000000000046561465514462400142630ustar00rootroot00000000000000package nmea const ( // TypeBWR type of BWR sentence for bearing and distance to waypoint (Rhumb Line) TypeBWR = "BWR" ) // BWR - bearing and distance to waypoint (Rhumb Line). This is calculated along rumb line instead of along the great circle. // https://gpsd.gitlab.io/gpsd/NMEA.html#_bwr_bearing_and_distance_to_waypoint_rhumb_line // // Format: $--BWR,hhmmss.ss,llll.ll,a,yyyyy.yy,a,x.x,T,x.x,M,x.x,N,c--c*hh // Format (NMEA 2.3+): $--BWR,hhmmss.ss,llll.ll,a,yyyyy.yy,a,x.x,T,x.x,M,x.x,N,c--c,m*hh // Example: $GPBWR,081837,,,,,,T,,M,,N,*02 // $GPBWR,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*30 type BWR struct { BaseSentence Time Time // UTC Time Latitude float64 // latitude of waypoint Longitude float64 // longitude of waypoint BearingTrue float64 // true bearing in degrees BearingTrueType string // is type of true bearing BearingMagnetic float64 // magnetic bearing in degrees BearingMagneticType string // is type of magnetic bearing DistanceNauticalMiles float64 // distance to waypoint in nautical miles DistanceNauticalMilesUnit string // is unit of distance to waypoint nautical miles DestinationWaypointID string // destination waypoint ID FFAMode string // FAA mode indicator (filled in NMEA 2.3 and later) } // newBWR constructor func newBWR(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeBWR) bwc := BWR{ BaseSentence: s, Time: p.Time(0, "time"), Latitude: p.LatLong(1, 2, "latitude"), Longitude: p.LatLong(3, 4, "longitude"), BearingTrue: p.Float64(5, "true bearing"), BearingTrueType: p.EnumString(6, "true bearing type", BearingTrue), BearingMagnetic: p.Float64(7, "magnetic bearing"), BearingMagneticType: p.EnumString(8, "magnetic bearing type", BearingMagnetic), DistanceNauticalMiles: p.Float64(9, "distance to waypoint is nautical miles"), DistanceNauticalMilesUnit: p.EnumString(10, "is distance to waypoint nautical miles unit", DistanceUnitNauticalMile), DestinationWaypointID: p.String(11, "destination waypoint ID"), } if len(p.Fields) > 12 { bwc.FFAMode = p.String(12, "FAA mode") // not enum because some devices have proprietary "non-nmea" values } return bwc, p.Err() } go-nmea-1.10.0/bwr_test.go000066400000000000000000000063471465514462400153210ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestBWR(t *testing.T) { var tests = []struct { name string raw string err string msg BWR }{ { name: "good sentence", raw: "$GPBWR,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*30", msg: BWR{ Time: Time{ Valid: true, Hour: 22, Minute: 5, Second: 16, Millisecond: 0, }, Latitude: 51.50033333333334, Longitude: -0.7723333333333334, BearingTrue: 213.8, BearingTrueType: BearingTrue, BearingMagnetic: 218, BearingMagneticType: BearingMagnetic, DistanceNauticalMiles: 4.6, DistanceNauticalMilesUnit: DistanceUnitNauticalMile, DestinationWaypointID: "EGLM", FFAMode: "", }, }, { name: "good sentence no waypoint", raw: "$GPBWR,081837,,,,,,T,,M,,N,*02", msg: BWR{ Time: Time{Valid: true, Hour: 8, Minute: 18, Second: 37, Millisecond: 0}, Latitude: 0, Longitude: 0, BearingTrue: 0, BearingTrueType: BearingTrue, BearingMagnetic: 0, BearingMagneticType: BearingMagnetic, DistanceNauticalMiles: 0, DistanceNauticalMilesUnit: DistanceUnitNauticalMile, DestinationWaypointID: "", FFAMode: "", }, }, { name: "good sentence with FAAMode", raw: "$GPBWR,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM,D*58", msg: BWR{ Time: Time{ Valid: true, Hour: 22, Minute: 5, Second: 16, Millisecond: 0, }, Latitude: 51.50033333333334, Longitude: -0.7723333333333334, BearingTrue: 213.8, BearingTrueType: BearingTrue, BearingMagnetic: 218, BearingMagneticType: BearingMagnetic, DistanceNauticalMiles: 4.6, DistanceNauticalMilesUnit: DistanceUnitNauticalMile, DestinationWaypointID: "EGLM", FFAMode: FAAModeDifferential, }, }, { name: "invalid nmea: Time", raw: "$GPBWR,2x0516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*7A", err: "nmea: GPBWR invalid time: 2x0516", }, { name: "invalid nmea: BearingTrueType", raw: "$GPBWR,220516,5130.02,N,00046.34,W,213.8,M,218.0,M,0004.6,N,EGLM*29", err: "nmea: GPBWR invalid true bearing type: M", }, { name: "invalid nmea: BearingMagneticType", raw: "$GPBWR,220516,5130.02,N,00046.34,W,213.8,T,218.0,T,0004.6,N,EGLM*29", err: "nmea: GPBWR invalid magnetic bearing type: T", }, { name: "invalid nmea: DistanceNauticalMilesUnit", raw: "$GPBWR,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,K,EGLM*35", err: "nmea: GPBWR invalid is distance to waypoint nautical miles unit: K", }, } for _, tt := range tests { 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) bwr := m.(BWR) bwr.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, bwr) } }) } } go-nmea-1.10.0/bww.go000066400000000000000000000030501465514462400142530ustar00rootroot00000000000000package nmea const ( // TypeBWW type of BWW sentence for bearing (from destination) destination waypoint to origin waypoint TypeBWW = "BWW" ) // BWW - bearing (from destination) destination waypoint to origin waypoint // Replaces by BOD in NMEA4+ (according to GPSD docs) // If your system supports RMB it is better to use RMB as it is more common (according to OpenCPN docs) // https://gpsd.gitlab.io/gpsd/NMEA.html#_bww_bearing_waypoint_to_waypoint // http://www.nmea.de/nmea0183datensaetze.html#bww // // Format: $--BWW,x.x,T,x.x,M,c--c,c--c*hh // Example: $GPBWW,097.0,T,103.2,M,POINTB,POINTA*41 type BWW struct { BaseSentence BearingTrue float64 // true bearing in degrees BearingTrueType string // is type of true bearing BearingMagnetic float64 // magnetic bearing in degrees BearingMagneticType string // is type of magnetic bearing DestinationWaypointID string // destination waypoint ID OriginWaypointID string // origin waypoint ID } // newBWW constructor func newBWW(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeBWW) bod := BWW{ BaseSentence: s, BearingTrue: p.Float64(0, "true bearing"), BearingTrueType: p.EnumString(1, "true bearing type", BearingTrue), BearingMagnetic: p.Float64(2, "magnetic bearing"), BearingMagneticType: p.EnumString(3, "magnetic bearing type", BearingMagnetic), DestinationWaypointID: p.String(4, "destination waypoint ID"), OriginWaypointID: p.String(5, "origin waypoint ID"), } return bod, p.Err() } go-nmea-1.10.0/bww_test.go000066400000000000000000000022121465514462400153110ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestBWW(t *testing.T) { var tests = []struct { name string raw string err string msg BWW }{ { name: "good sentence", raw: "$GPBWW,097.0,T,103.2,M,POINTB,POINTA*41", msg: BWW{ BearingTrue: 97.0, BearingTrueType: BearingTrue, BearingMagnetic: 103.2, BearingMagneticType: BearingMagnetic, DestinationWaypointID: "POINTB", OriginWaypointID: "POINTA", }, }, { name: "invalid nmea: BearingTrueValid", raw: "$GPBWW,097.0,M,103.2,M,POINTB,POINTA*58", err: "nmea: GPBWW invalid true bearing type: M", }, { name: "invalid nmea: BearingMagneticValid", raw: "$GPBWW,097.0,T,103.2,T,POINTB,POINTA*58", err: "nmea: GPBWW invalid magnetic bearing type: T", }, } for _, tt := range tests { 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) bww := m.(BWW) bww.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, bww) } }) } } go-nmea-1.10.0/dbk.go000066400000000000000000000022641465514462400142220ustar00rootroot00000000000000package nmea const ( // TypeDBK type of DBK sentence for Depth Below Keel TypeDBK = "DBK" ) // DBK - Depth Below Keel (obsolete, use DPT instead) // https://gpsd.gitlab.io/gpsd/NMEA.html#_dbk_depth_below_keel // https://wiki.openseamap.org/wiki/OpenSeaMap-dev:NMEA#DBK_-_Depth_below_keel // // Format: $--DBK,x.x,f,x.x,M,x.x,F*hh // Example: $SDDBK,12.3,f,3.7,M,2.0,F*2F type DBK struct { BaseSentence DepthFeet float64 // Depth, feet DepthFeetUnit string // f = feet DepthMeters float64 // Depth, meters DepthMetersUnit string // M = meters DepthFathoms float64 // Depth, Fathoms DepthFathomsUnit string // F = Fathoms } // newDBK constructor func newDBK(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeDBK) return DBK{ BaseSentence: s, DepthFeet: p.Float64(0, "depth feet"), DepthFeetUnit: p.EnumString(1, "depth feet unit", DistanceUnitFeet), DepthMeters: p.Float64(2, "depth meters"), DepthMetersUnit: p.EnumString(3, "depth meters unit", DistanceUnitMetre), DepthFathoms: p.Float64(4, "depth fathom"), DepthFathomsUnit: p.EnumString(5, "depth fathom unit", DistanceUnitFathom), }, p.Err() } go-nmea-1.10.0/dbk_test.go000066400000000000000000000023251465514462400152570ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestDBK(t *testing.T) { var tests = []struct { name string raw string err string msg DBK }{ { name: "good sentence", raw: "$SDDBK,12.3,f,3.7,M,2.0,F*2F", msg: DBK{ DepthFeet: 12.3, DepthFeetUnit: DistanceUnitFeet, DepthMeters: 3.7, DepthMetersUnit: DistanceUnitMetre, DepthFathoms: 2, DepthFathomsUnit: DistanceUnitFathom, }, }, { name: "invalid nmea: DepthFeetUnit", raw: "$SDDBK,12.3,x,3.7,M,2.0,F*31", err: "nmea: SDDBK invalid depth feet unit: x", }, { name: "invalid nmea: DepthMeterUnit", raw: "$SDDBK,12.3,f,3.7,x,2.0,F*1A", err: "nmea: SDDBK invalid depth meters unit: x", }, { name: "invalid nmea: DepthFathomUnit", raw: "$SDDBK,12.3,f,3.7,M,2.0,x*11", err: "nmea: SDDBK invalid depth fathom unit: x", }, } for _, tt := range tests { 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) dbk := m.(DBK) dbk.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, dbk) } }) } } go-nmea-1.10.0/dbs.go000066400000000000000000000022671465514462400142350ustar00rootroot00000000000000package nmea const ( // TypeDBS is type of DBS sentence for Depth Below Surface TypeDBS = "DBS" ) // DBS - Depth Below Surface (obsolete, use DPT instead) // https://gpsd.gitlab.io/gpsd/NMEA.html#_dbs_depth_below_surface // https://wiki.openseamap.org/wiki/OpenSeaMap-dev:NMEA#DBS_-_Depth_below_surface // // Format: $--DBS,x.x,f,x.x,M,x.x,F*hh // Example: $23DBS,01.9,f,0.58,M,00.3,F*21 type DBS struct { BaseSentence DepthFeet float64 // Depth, feet DepthFeetUnit string // f = feet DepthMeters float64 // Depth, meters DepthMeterUnit string // M = meters DepthFathoms float64 // Depth, Fathoms DepthFathomUnit string // F = Fathoms } // newDBS constructor func newDBS(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeDBS) return DBS{ BaseSentence: s, DepthFeet: p.Float64(0, "depth feet"), DepthFeetUnit: p.EnumString(1, "depth feet unit", DistanceUnitFeet), DepthMeters: p.Float64(2, "depth meters"), DepthMeterUnit: p.EnumString(3, "depth feet unit", DistanceUnitMetre), DepthFathoms: p.Float64(4, "depth fathoms"), DepthFathomUnit: p.EnumString(5, "depth fathom unit", DistanceUnitFathom), }, p.Err() } go-nmea-1.10.0/dbs_test.go000066400000000000000000000023161465514462400152670ustar00rootroot00000000000000package nmea import ( "testing" "github.com/stretchr/testify/assert" ) func TestDBS(t *testing.T) { 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: 1.9, DepthFeetUnit: DistanceUnitFeet, DepthMeters: 0.58, DepthMeterUnit: DistanceUnitMetre, DepthFathoms: 0.3, DepthFathomUnit: DistanceUnitFathom, }, }, { name: "good sentence 2", raw: "$SDDBS,,,0187.5,M,,*1A", // Simrad ITI Trawl System msg: DBS{ DepthFeet: 0, DepthFeetUnit: "", DepthMeters: 187.5, DepthMeterUnit: DistanceUnitMetre, DepthFathoms: 0, DepthFathomUnit: "", }, }, { name: "bad validity", raw: "$23DBS,01.9,f,0.58,M,00.3,F*25", err: "nmea: sentence checksum mismatch [21 != 25]", }, } 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.10.0/dbt.go000066400000000000000000000012411465514462400142250ustar00rootroot00000000000000package nmea const ( // TypeDBT type for DBT sentences TypeDBT = "DBT" ) // DBT - Depth below transducer // https://gpsd.gitlab.io/gpsd/NMEA.html#_dbt_depth_below_transducer // // Format: $--DBT,x.x,f,x.x,M,x.x,F*hh // Example: $IIDBT,032.93,f,010.04,M,005.42,F*2C type DBT struct { BaseSentence DepthFeet float64 DepthMeters float64 DepthFathoms float64 } // newDBT constructor func newDBT(s BaseSentence) (Sentence, 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.10.0/dbt_test.go000066400000000000000000000014541465514462400152720ustar00rootroot00000000000000package 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: 32.93, DepthMeters: 10.04, DepthFathoms: 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.10.0/deprecated.go000066400000000000000000000060241465514462400155600ustar00rootroot00000000000000package 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.10.0/deprecated_test.go000066400000000000000000000423201465514462400166160ustar00rootroot00000000000000package 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", Latitude: MustParseGPS("5133.82 N"), Longitude: MustParseGPS("00042.24 W"), Speed: 173.8, Course: 231.8, Date: Date{true, 13, 06, 94}, Variation: -4.2, FFAMode: "", NavStatus: "", }, }, { 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", Latitude: MustParseGPS("4302.539570 N"), Longitude: MustParseGPS("07920.379823 W"), Speed: 0, Course: 0, Date: Date{true, 7, 6, 17}, Variation: 0, FFAMode: FAAModeAutonomous, NavStatus: "", }, }, { 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", Latitude: MustParseGPS("5546.27711 N"), Longitude: MustParseGPS("03736.91144 E"), Speed: 0.061, Course: 0, Date: Date{true, 26, 3, 18}, Variation: 0, FFAMode: FAAModeAutonomous, NavStatus: "", }, }, { 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", FFAMode: FAAModeAutonomous, }, }, { 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", Latitude: MustParseGPS("5133.82 N"), Longitude: MustParseGPS("00042.24 W"), Speed: 173.8, Course: 231.8, Date: Date{true, 13, 6, 94}, Variation: -4.2, FFAMode: "", NavStatus: "", }, }, { 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", Latitude: MustParseGPS("4302.539570 N"), Longitude: MustParseGPS("07920.379823 W"), Speed: 0, Course: 0, Date: Date{true, 7, 6, 17}, Variation: 0, FFAMode: FAAModeAutonomous, NavStatus: "", }, }, { 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.10.0/dor.go000066400000000000000000000063351465514462400142510ustar00rootroot00000000000000package nmea const ( // TypeDOR type of DOR sentence for Door Status Detection TypeDOR = "DOR" // TypeSingleDoorDOR is type for single door related event TypeSingleDoorDOR = "E" // TypeFaultDOR is type for fault with door TypeFaultDOR = "F" // TypeSectionDOR is type for section of doors related event TypeSectionDOR = "S" // DoorStatusOpenDOR is status for open door DoorStatusOpenDOR = "O" // DoorStatusClosedDOR is status for closed door DoorStatusClosedDOR = "C" // DoorStatusFaultDOR is status for fault with door DoorStatusFaultDOR = "X" // SwitchSettingHarbourModeDOR is setting for Harbour mode (allowed open) SwitchSettingHarbourModeDOR = "O" // SwitchSettingSeaModeDOR is setting for Sea mode (ordered closed) SwitchSettingSeaModeDOR = "C" ) // DOR - Door Status Detection // Source: "Interfacing Voyage Data Recorder Systems, AutroSafe Interactive Fire-Alarm System, 116-P-BSL336/EE, RevA 2007-01-25, // Autronica Fire and Security AS " (page 32 | p.8.1.4) // https://product.autronicafire.com/fileshare/fileupload/14251/bsl336_ee.pdf // // Format: $FRDOR,a,hhmmss,aa,aa,xxx,xxx,a,a,c--c*hh // Example: $FRDOR,E,233042,FD,FP,000,010,C,C,Door Closed : TEST FPA Name*4D type DOR struct { BaseSentence // Type is type of the message // * E – Single door // * F – Fault // * S – Section (whole or part of section) Type string // Time is Event Time Time Time // SystemIndicator is system indicator. Detector system type with 2 char identifier. // * WT - watertight // * WS - semi watertight // * FD - fire door // * HD - hull door // * OT - other // could be more // https://www.nmea.org/Assets/20190303%20nmea%200183%20talker%20identifier%20mnemonics.pdf SystemIndicator string // DivisionIndicator1 is first division indicator for locating origin detector for this message DivisionIndicator1 string // DivisionIndicator2 is second division indicator for locating origin detector for this message DivisionIndicator2 int64 // DoorNumberOrCount is Door number or activated door count (seems to be field with overloaded meaning) DoorNumberOrCount int64 // DoorStatus is Door status // * O – Open // * C – Closed // * X – Fault // could be more DoorStatus string // SwitchSetting is Mode switch setting // * O – Harbour mode (allowed open) // * C – Sea mode (ordered closed) SwitchSetting string // Message's description text (could be cut to fit max packet length) Message string } // newDOR constructor func newDOR(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeDOR) return DOR{ BaseSentence: s, Type: p.EnumString(0, "message type", TypeSingleDoorDOR, TypeFaultDOR, TypeSectionDOR), Time: p.Time(1, "time"), SystemIndicator: p.String(2, "system indicator"), DivisionIndicator1: p.String(3, "division indicator 1"), DivisionIndicator2: p.Int64(4, "division indicator 2"), DoorNumberOrCount: p.Int64(5, "door number or count"), DoorStatus: p.EnumString(6, "door state", DoorStatusOpenDOR, DoorStatusClosedDOR, DoorStatusFaultDOR), SwitchSetting: p.EnumString(7, "switch setting mode", SwitchSettingHarbourModeDOR, SwitchSettingSeaModeDOR), Message: p.String(8, "message"), }, p.Err() } go-nmea-1.10.0/dor_test.go000066400000000000000000000033201465514462400152770ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestDOR(t *testing.T) { var tests = []struct { name string raw string err string msg DOR }{ { name: "good sentence", raw: "$FRDOR,E,233042,FD,FP,000,010,C,C,Door Closed : TEST FPA Name*4D", msg: DOR{ Type: TypeSingleDoorDOR, Time: Time{ Valid: true, Hour: 23, Minute: 30, Second: 42, Millisecond: 0, }, SystemIndicator: "FD", DivisionIndicator1: "FP", DivisionIndicator2: 0, DoorNumberOrCount: 10, DoorStatus: DoorStatusClosedDOR, SwitchSetting: SwitchSettingSeaModeDOR, Message: "Door Closed : TEST FPA Name", }, }, { name: "invalid nmea: Type", raw: "$FRDOR,x,233042,FD,FP,000,010,C,C,Door Closed : TEST FPA Name*70", err: "nmea: FRDOR invalid message type: x", }, { name: "invalid nmea: Time", raw: "$FRDOR,E,2x3042,FD,FP,000,010,C,C,Door Closed : TEST FPA Name*06", err: "nmea: FRDOR invalid time: 2x3042", }, { name: "invalid nmea: DoorStatus", raw: "$FRDOR,E,233042,FD,FP,000,010,_,C,Door Closed : TEST FPA Name*51", err: "nmea: FRDOR invalid door state: _", }, { name: "invalid nmea: SwitchSetting", raw: "$FRDOR,E,233042,FD,FP,000,010,C,_,Door Closed : TEST FPA Name*51", err: "nmea: FRDOR invalid switch setting mode: _", }, } for _, tt := range tests { 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) dor := m.(DOR) dor.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, dor) } }) } } go-nmea-1.10.0/dpt.go000066400000000000000000000014621465514462400142500ustar00rootroot00000000000000package nmea const ( // TypeDPT type for DPT sentences TypeDPT = "DPT" ) // DPT - Depth of Water // https://gpsd.gitlab.io/gpsd/NMEA.html#_dpt_depth_of_water // // Format: $--DPT,x.x,x.x,x.x*hh // Example: $SDDPT,0.5,0.5,*7B // $INDPT,2.3,0.0*46 type DPT struct { BaseSentence Depth float64 // Water depth relative to transducer, meters Offset float64 // offset from transducer RangeScale float64 // OPTIONAL, Maximum range scale in use (NMEA 3.0 and above) } // newDPT constructor func newDPT(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeDPT) dpt := DPT{ BaseSentence: s, Depth: p.Float64(0, "depth"), Offset: p.Float64(1, "offset"), } if len(p.Fields) > 2 { dpt.RangeScale = p.Float64(2, "range scale") } return dpt, p.Err() } go-nmea-1.10.0/dpt_test.go000066400000000000000000000020501465514462400153010ustar00rootroot00000000000000package 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: 0.5, Offset: 0.5, RangeScale: 0, }, }, { name: "good sentence with scale", raw: "$SDDPT,0.5,0.5,0.1*54", msg: DPT{ Depth: 0.5, Offset: 0.5, RangeScale: 0.1, }, }, { name: "good sentence with 2 fields", raw: "$INDPT,2.3,0.0*46", msg: DPT{ Depth: 2.3, Offset: 0, RangeScale: 0, }, }, { 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.10.0/dsc.go000066400000000000000000000137651465514462400142430ustar00rootroot00000000000000package nmea import "strings" const ( // TypeDSC type of DSC sentence for Digital Selective Calling Information TypeDSC = "DSC" // AcknowledgementRequestDSC is type for Acknowledge request AcknowledgementRequestDSC = "R" // AcknowledgementDSC is type for Acknowledgement AcknowledgementDSC = "B" // AcknowledgementNeitherDSC is type for Neither (end of sequence) AcknowledgementNeitherDSC = "S" ) // DSC – Digital Selective Calling Information // https://opencpn.org/wiki/dokuwiki/doku.php?id=opencpn:opencpn_user_manual:advanced_features:nmea_sentences // https://web.archive.org/web/20190303170916/http://continuouswave.com/whaler/reference/DSC_Datagrams.html // http://www.busse-yachtshop.de/pdf/icom-GM600-handbuch.pdf // https://github.com/mariokonrad/marnav/blob/master/src/marnav/nmea/dsc.cpp (marnav has interesting enums worth checking) // // Note: many fields of DSC are conditional with double meaning and we only map raw sentence to fields without any // logic/checking of those conditions. We could have specific fields if we only knew the rules to populate them. // // Format: $--DSC,xx,xxxxxxxxxx,xx,xx,xx,x.x, x.x,xxxxxxxxxx,xx, a,a*hh // Example: $CDDSC,20,3380400790,00,21,26,1423108312,2021,,,B, E*73 type DSC struct { BaseSentence // Note: all fields are strings even if specified as digits as int can not express "00" and would be 0 which is different // Source of quotes: https://web.archive.org/web/20190303170916/http://continuouswave.com/whaler/reference/DSC_Datagrams.html // FormatSpecifier is Format specifier (2 digits) // > The call content is first described by a "format specifier" element. The format specifier is explained in // > ITU-Rec. M.493-13 Section 4, with various symbol codes in the "service command" range of symbols representing // > various message formats, as shown in Table 3 (by symbol number, then meaning of symbol) as follows: // > * 102 = selective call to a group of ships in particular geographic area // > * 112 = distress alert call // > * 114 = selective call to a group of ships having common interest // > * 116 = all ships call // > * 120 = selective call to particular individual station // > * 123 = selective call to a particular individual using automatic service FormatSpecifier string // Address (10 digits) Address string // Category (2 digits or empty) // > The call content is next described by a "category element" in Section 6. Again, various symbol codes in the // > "service command" range of symbols represent various categories, as follows from Table 3 (by symbol number, // > then meaning of symbol): // > * 100 = routine // > * 108 = safety // > * 110 = urgency // > * 112 = distress Category string // DistressCauseOrTeleCommand1 is The cause of the distress or first telecommand (2 digits or empty) // > Nature of Distress is to be encoded, again using Table 3, as follows // > * 100 = Fire, explosion // > * 101 = Flooding // > * 102 = Collision // > * 103 = Grounding // > * 104 = Listing, in danger of capsize // > * 105 = Sinking // > * 106 = Disabled and adrift // > * 107 = Undesignated distres // > * 108 = Abandoning ship // > * 109 = Piracy/armed robbery attack // > * 110 = Man overboard // > * 111 = unassigned symbol; take no action // > * 112 = EPRIB emission // > * 113 through 27 = unassigned symbol; take no action DistressCauseOrTeleCommand1 string // CommandTypeOrTeleCommand2 is Type of communication or second telecommand (2 digits) CommandTypeOrTeleCommand2 string // PositionOrCanal is Position (lat+lon) or Canal/frequency (Maximum 16 digits) // > Distress coordinates are to be encoded five parts, sent as a string of ten digits. The first digit indicates // > the direction of the latitude and longitude, with "0" for North and East, "1" for North and West, // > "2" for South and East, and "3" for South and West. The next two digits are the latitude in degrees. // > The next two digits are the latitude in whole minutes. The next three digits are the longitude in degrees. // > The next two digits are longitude in whole minutes. PositionOrCanal string // Position (lat+lon) or Canal/frequency (Maximum 16 digits) // TimeOrTelephoneNumber is Time or Telephone Number (Maximum 16 digits) // > The time in universal coordinated time is to be sent in 24-hour format in two parts, a total of four digits. // > The first two digits are the hours. The next two are the minutes. TimeOrTelephoneNumber string // MMSI of ship in distress (10 digits or empty) // > The call content is next described as having a "self-identification" element. This is simply the sending // > station's MMSI, encoded like the address element. This identifies who sent the message. MMSI string // DistressCause is The cause of the distress (2 digits or empty) DistressCause string // Acknowledgement (R=Acknowledge request, B=Acknowledgement, S=Neither (end of sequence)) Acknowledgement string // Expansion indicator (E or empty) ExpansionIndicator string } // newDSC constructor func newDSC(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeDSC) return DSC{ BaseSentence: s, FormatSpecifier: p.String(0, "format specifier"), Address: p.String(1, "address"), Category: p.String(2, "category"), DistressCauseOrTeleCommand1: p.String(3, "cause of the distress or first telecommand"), CommandTypeOrTeleCommand2: p.String(4, "type of communication or second telecommand"), PositionOrCanal: p.String(5, "position or canal"), TimeOrTelephoneNumber: p.String(6, "time or telephone"), MMSI: p.String(7, "MMSI"), DistressCause: p.String(8, "distress cause"), Acknowledgement: strings.TrimSpace(p.EnumString( 9, "acknowledgement", AcknowledgementRequestDSC, " "+AcknowledgementRequestDSC, AcknowledgementDSC, " "+AcknowledgementDSC, AcknowledgementNeitherDSC, " "+AcknowledgementNeitherDSC, )), ExpansionIndicator: p.String(10, "expansion indicator"), }, p.Err() } go-nmea-1.10.0/dsc_test.go000066400000000000000000000046701465514462400152750ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestDSC(t *testing.T) { var tests = []struct { name string raw string err string msg DSC }{ { name: "good sentence", raw: "$CDDSC,12,3380400790,12,06,00,1423108312,2019, , , S, E *4a", msg: DSC{ FormatSpecifier: "12", Address: "3380400790", Category: "12", DistressCauseOrTeleCommand1: "06", CommandTypeOrTeleCommand2: "00", PositionOrCanal: "1423108312", TimeOrTelephoneNumber: "2019", MMSI: " ", DistressCause: " ", Acknowledgement: "S", ExpansionIndicator: " E ", }, }, { name: "good sentence Distress Alert Cancel", raw: "$CDDSC,12,3381581370,12,06,00,1423108312,0236,3381581370, , S, *20", msg: DSC{ FormatSpecifier: "12", Address: "3381581370", Category: "12", DistressCauseOrTeleCommand1: "06", CommandTypeOrTeleCommand2: "00", PositionOrCanal: "1423108312", TimeOrTelephoneNumber: "0236", MMSI: "3381581370", DistressCause: " ", Acknowledgement: "S", ExpansionIndicator: " ", }, }, { name: "good sentence Non-Distress Call - Reply to Position Request\n", raw: "$CDDSC,20,3381581370,00,21,26,1423108312,1902, , , B, E *7B", msg: DSC{ FormatSpecifier: "20", Address: "3381581370", Category: "00", DistressCauseOrTeleCommand1: "21", CommandTypeOrTeleCommand2: "26", PositionOrCanal: "1423108312", TimeOrTelephoneNumber: "1902", MMSI: " ", DistressCause: " ", Acknowledgement: "B", ExpansionIndicator: " E ", }, }, { name: "invalid nmea: Acknowledgement", raw: "$CDDSC,20,3380400790,00,21,26,1423108312,2021,,,x, E*69", err: "nmea: CDDSC invalid acknowledgement: x", }, } for _, tt := range tests { 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) dsc := m.(DSC) dsc.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, dsc) } }) } } go-nmea-1.10.0/dse.go000066400000000000000000000062561465514462400142420ustar00rootroot00000000000000package nmea import "errors" const ( // TypeDSE type of DSE sentence for Expanded digital selective calling TypeDSE = "DSE" // AcknowledgementAutomaticDSE is type for automatic AcknowledgementAutomaticDSE = "A" // AcknowledgementRequestDSE is type for request AcknowledgementRequestDSE = "R" // AcknowledgementQueryDSE is type for query AcknowledgementQueryDSE = "Q" ) // DSE – Expanded digital selective calling. Is sentence that follows DSC sentence to provide additional (extended) data. // https://opencpn.org/wiki/dokuwiki/doku.php?id=opencpn:opencpn_user_manual:advanced_features:nmea_sentences // http://www.busse-yachtshop.de/pdf/icom-GM600-handbuch.pdf // // Format: $CDDSE, x, x, a, xxxxxxxxxx, xx, c--c, .........., xx, c--c*hh // Example: $CDDSE,1,1,A,3380400790,00,46504437*15 type DSE struct { BaseSentence TotalNumber int64 // total number of sentences, 01 to 99 Number int64 // number of current sentence, 01 to 99 Acknowledgement string // Acknowledgement (R=Acknowledge request, B=Acknowledgement, S=Neither (end of sequence)) MMSI string // MMSI of vessel (10 digits) DataSets []DSEDataSet } // DSEDataSet is pair of DSE sets of data containing code + its data type DSEDataSet struct { // Code is code field, 2 digits // From OpenCPN wiki: // > 00–this field of two-digits appears to be the expansion data specifier described in Table 1 of ITU-Rec.M821-1, // > but with the symbol representation in two-digits instead of three-digits. The leading “1” seems to not be used. // > (See modified table, above.) This field identifies the data that will follow in the next field. In this message, // > the data will be “enhanced position resolution.” Code string // Data is data field, Enhanced position resolution, Maximum 8 characters, could be empty // From OpenCPN wiki: // > 45894494–the data payload, which is eight digits. The first four are the decimal portion of the latitude // > minutes; the last four are the decimal portion of the longitude minutes. The latitude and longitude whole // > minutes were sent in the immediately preceding datagram. This is as specified in the ITU-Rec. M.821-1 in // > section 2.1.2.1 Data string } // newDSE constructor func newDSE(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeDSE) dse := DSE{ BaseSentence: s, TotalNumber: p.Int64(0, "total number of sentences"), Number: p.Int64(1, "sentence number"), Acknowledgement: p.EnumString(2, "acknowledgement", AcknowledgementAutomaticDSE, AcknowledgementRequestDSE, AcknowledgementQueryDSE), MMSI: p.String(3, "MMSI"), DataSets: nil, } datasetFieldCount := len(p.Fields) - 4 if datasetFieldCount < 2 { return dse, errors.New("DSE is missing fields for parsing data sets") } if datasetFieldCount%2 != 0 { return dse, errors.New("DSE data set field count is not exactly dividable by 2") } dse.DataSets = make([]DSEDataSet, 0, datasetFieldCount/2) for i := 0; i < datasetFieldCount; i = i + 2 { tmp := DSEDataSet{ Code: p.String(4+i, "data set code"), Data: p.String(5+i, "data set data"), } dse.DataSets = append(dse.DataSets, tmp) } return dse, p.Err() } go-nmea-1.10.0/dse_test.go000066400000000000000000000032331465514462400152710ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestDSE(t *testing.T) { var tests = []struct { name string raw string err string msg DSE }{ { name: "good sentence, single dataset", raw: "$CDDSE,1,1,A,3380400790,00,46504437*15", msg: DSE{ TotalNumber: 1, Number: 1, Acknowledgement: AcknowledgementAutomaticDSE, MMSI: "3380400790", DataSets: []DSEDataSet{ {Code: "00", Data: "46504437"}, }, }, }, { name: "good sentence, single dataset", raw: "$CDDSE,1,1,A,3380400790,00,46504437,01,16501437*17", msg: DSE{ TotalNumber: 1, Number: 1, Acknowledgement: AcknowledgementAutomaticDSE, MMSI: "3380400790", DataSets: []DSEDataSet{ {Code: "00", Data: "46504437"}, {Code: "01", Data: "16501437"}, }, }, }, { name: "invalid nmea: field count", raw: "$CDDSE,1,1,x,3380400790,46504437*00", err: "DSE is missing fields for parsing data sets", }, { name: "invalid nmea: data set field count", raw: "$CDDSE,1,1,A,3380400790,00,46504437,01*38", err: "DSE data set field count is not exactly dividable by 2", }, { name: "invalid nmea: Acknowledgement", raw: "$CDDSE,1,1,x,3380400790,00,46504437*2c", err: "nmea: CDDSE invalid acknowledgement: x", }, } for _, tt := range tests { 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) dse := m.(DSE) dse.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, dse) } }) } } go-nmea-1.10.0/dtm.go000066400000000000000000000030441465514462400142430ustar00rootroot00000000000000package nmea const ( // TypeDTM type of DTM sentence for Datum Reference TypeDTM = "DTM" ) // DTM - Datum Reference // https://gpsd.gitlab.io/gpsd/NMEA.html#_dtm_datum_reference // // Format: $--DTM,ref,x,llll,c,llll,c,aaa,ref*hh // Example: $GPDTM,W84,,0.0,N,0.0,E,0.0,W84*6F // Example: $GPDTM,W84,,00.0000,N,00.0000,W,,W84*53 type DTM struct { BaseSentence LocalDatumCode string // Local datum code (W84,W72,S85,P90,999) LocalDatumSubcode string // Local datum subcode. May be blank. LatitudeOffsetMinute float64 // Latitude offset (minutes) (negative if south) LongitudeOffsetMinute float64 // Longitude offset (minutes) (negative if west) AltitudeOffsetMeters float64 // Altitude offset in meters DatumName string // Reference datum name. What’s usually seen here is "W84", the standard WGS84 datum used by GPS. } // newDTM constructor func newDTM(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeDTM) m := DTM{ BaseSentence: s, LocalDatumCode: p.String(0, "local datum code"), LocalDatumSubcode: p.String(1, "local datum subcode"), LatitudeOffsetMinute: p.Float64(2, "latitude offset minutes"), LongitudeOffsetMinute: p.Float64(4, "longitude offset minutes"), AltitudeOffsetMeters: p.Float64(6, "altitude offset offset"), DatumName: p.String(7, "datum name"), } if p.String(3, "latitude offset direction") == South { m.LatitudeOffsetMinute *= -1 } if p.String(5, "longitude offset direction") == West { m.LongitudeOffsetMinute *= -1 } return m, p.Err() } go-nmea-1.10.0/dtm_test.go000066400000000000000000000032131465514462400153000ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestDTM(t *testing.T) { var tests = []struct { name string raw string err string msg DTM }{ { name: "good sentence 1", raw: "$GPDTM,W84,,0.0,N,0.0,E,0.0,W84*6F", msg: DTM{ BaseSentence: BaseSentence{}, LocalDatumCode: "W84", LocalDatumSubcode: "", LatitudeOffsetMinute: 0, LongitudeOffsetMinute: 0, AltitudeOffsetMeters: 0, DatumName: "W84", }, }, { name: "good sentence 2", raw: "$GPDTM,W84,X,00.1200,S,12.0000,W,100,W84*27", msg: DTM{ BaseSentence: BaseSentence{}, LocalDatumCode: "W84", LocalDatumSubcode: "X", LatitudeOffsetMinute: -0.12, LongitudeOffsetMinute: -12, AltitudeOffsetMeters: 100, DatumName: "W84", }, }, { name: "invalid nmea: LatitudeOffsetMinute", raw: "$GPDTM,W84,,x,N,0.0,E,0.0,W84*39", err: "nmea: GPDTM invalid latitude offset minutes: x", }, { name: "invalid nmea: LongitudeOffsetMinute", raw: "$GPDTM,W84,,0.0,N,x,E,0.0,W84*39", err: "nmea: GPDTM invalid longitude offset minutes: x", }, { name: "invalid nmea: AltitudeOffsetMeters", raw: "$GPDTM,W84,,0.0,N,0.0,E,x,W84*39", err: "nmea: GPDTM invalid altitude offset offset: x", }, } for _, tt := range tests { 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) mm := m.(DTM) mm.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, mm) } }) } } go-nmea-1.10.0/eve.go000066400000000000000000000016301465514462400142350ustar00rootroot00000000000000package nmea const ( // TypeEVE type of EVE sentence for General Event Message TypeEVE = "EVE" ) // EVE - General Event Message // Source: "Interfacing Voyage Data Recorder Systems, AutroSafe Interactive Fire-Alarm System, 116-P-BSL336/EE, RevA 2007-01-25, // Autronica Fire and Security AS " (page 34 | p.8.1.5) // https://product.autronicafire.com/fileshare/fileupload/14251/bsl336_ee.pdf // // Format: $FREVE,hhmmss,c--c,c--c*hh // Example: $FREVE,000001,DZ00513,Fire Alarm On: TEST DZ201 Name*0A type EVE struct { BaseSentence Time Time // Event Time TagCode string // Tag code Message string // Event text } // newEVE constructor func newEVE(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeEVE) return EVE{ BaseSentence: s, Time: p.Time(0, "time"), TagCode: p.String(1, "tag code"), Message: p.String(2, "event message text"), }, p.Err() } go-nmea-1.10.0/eve_test.go000066400000000000000000000017321465514462400152770ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestEVE(t *testing.T) { var tests = []struct { name string raw string err string msg EVE }{ { name: "good sentence", raw: "$FREVE,000001,DZ00513,Fire Alarm On: TEST DZ201 Name*0A", msg: EVE{ Time: Time{ Valid: true, Hour: 0, Minute: 0, Second: 1, Millisecond: 0, }, TagCode: "DZ00513", Message: "Fire Alarm On: TEST DZ201 Name", }, }, { name: "invalid nmea: Time", raw: "$FREVE,0x0001,DZ00513,Fire Alarm On: TEST DZ201 Name*42", err: "nmea: FREVE invalid time: 0x0001", }, } for _, tt := range tests { 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) eve := m.(EVE) eve.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, eve) } }) } } go-nmea-1.10.0/fir.go000066400000000000000000000070431465514462400142420ustar00rootroot00000000000000package nmea const ( // TypeFIR type of FIR sentence for Fire Detection TypeFIR = "FIR" // TypeEventOrAlarmFIR is Event, Fire Alarm type TypeEventOrAlarmFIR = "E" // TypeFaultFIR is type for Fault TypeFaultFIR = "F" // TypeDisablementFIR is type for detector disablement TypeDisablementFIR = "D" // ConditionActivationFIR is activation condition ConditionActivationFIR = "A" // ConditionNonActivationFIR is non-activation condition ConditionNonActivationFIR = "V" // ConditionUnknownFIR is unknown condition ConditionUnknownFIR = "X" // AlarmStateAcknowledgedFIR is value for alarm acknowledgement AlarmStateAcknowledgedFIR = "A" // AlarmStateNotAcknowledgedFIR is value for alarm being not acknowledged AlarmStateNotAcknowledgedFIR = "V" ) // FIR - Fire Detection event with time and location // Source: "Interfacing Voyage Data Recorder Systems, AutroSafe Interactive Fire-Alarm System, 116-P-BSL336/EE, RevA 2007-01-25, // Autronica Fire and Security AS " (page 39 | p.8.1.6) // https://product.autronicafire.com/fileshare/fileupload/14251/bsl336_ee.pdf // // Format: $FRFIR,a,hhmmss,aa,aa,xxx,xxx,a,a,c--c*hh // Example: $FRFIR,E,103000,FD,PT,000,007,A,V,Fire Alarm : TEST PT7 Name TEST DZ2 Name*7A type FIR struct { BaseSentence // Type is type of the message // * E – Event, Fire Alarm // * F – Fault // * D – Disablement Type string // Time is Event Time Time Time // SystemIndicator is system indicator. Detector system type with 2 char identifier. // * FD Generic fire detector // * FH Heat detector // * FS Smoke detector // * FD Smoke and heat detector // * FM Manual call point // * GD Any gas detector // * GO Oxygen gas detector // * GS Hydrogen sulphide gas detector // * GH Hydro-carbon gas detector // * SF Sprinkler flow switch // * SV Sprinkler manual valve release // * CO CO2 manual release // * OT Other SystemIndicator string // DivisionIndicator1 is first division indicator for locating origin detector for this message DivisionIndicator1 string // DivisionIndicator2 is second division indicator for locating origin detector for this message DivisionIndicator2 int64 // FireDetectorNumberOrCount is Fire detector number or activated detectors count (seems to be field with overloaded meaning) FireDetectorNumberOrCount int64 // Condition describes the condition triggering current message // * A – Activation // * V – Non-activation // * X – State unknown Condition string // AlarmAckState is Alarm's acknowledge state // * A – Acknowledged // * V – Not acknowledged AlarmAckState string // Message's description text (could be cut to fit max packet length) Message string } // newFIR constructor func newFIR(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeFIR) return FIR{ BaseSentence: s, Type: p.EnumString(0, "message type", TypeEventOrAlarmFIR, TypeFaultFIR, TypeDisablementFIR), Time: p.Time(1, "time"), SystemIndicator: p.String(2, "system indicator"), DivisionIndicator1: p.String(3, "division indicator 1"), DivisionIndicator2: p.Int64(4, "division indicator 2"), FireDetectorNumberOrCount: p.Int64(5, "fire detector number or count"), Condition: p.EnumString(6, "condition", ConditionActivationFIR, ConditionNonActivationFIR, ConditionUnknownFIR), AlarmAckState: p.EnumString(7, "alarm acknowledgement state", AlarmStateAcknowledgedFIR, AlarmStateNotAcknowledgedFIR), Message: p.String(8, "message"), }, p.Err() } go-nmea-1.10.0/fir_test.go000066400000000000000000000035351465514462400153030ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestFIR(t *testing.T) { var tests = []struct { name string raw string err string msg FIR }{ { name: "good sentence", raw: "$FRFIR,E,103000,FD,PT,000,007,A,V,Fire Alarm : TEST PT7 Name TEST DZ2 Name*7A", msg: FIR{ Type: TypeEventOrAlarmFIR, Time: Time{ Valid: true, Hour: 10, Minute: 30, Second: 0, Millisecond: 0, }, SystemIndicator: "FD", DivisionIndicator1: "PT", DivisionIndicator2: 0, FireDetectorNumberOrCount: 7, Condition: ConditionActivationFIR, AlarmAckState: AlarmStateNotAcknowledgedFIR, Message: "Fire Alarm : TEST PT7 Name TEST DZ2 Name", }, }, { name: "invalid nmea: Type", raw: "$FRFIR,x,103000,FD,PT,000,007,A,V,Fire Alarm : TEST PT7 Name TEST DZ2 Name*47", err: "nmea: FRFIR invalid message type: x", }, { name: "invalid nmea: Time", raw: "$FRFIR,E,1x3000,FD,PT,000,007,A,V,Fire Alarm : TEST PT7 Name TEST DZ2 Name*32", err: "nmea: FRFIR invalid time: 1x3000", }, { name: "invalid nmea: Condition", raw: "$FRFIR,E,103000,FD,PT,000,007,_,V,Fire Alarm : TEST PT7 Name TEST DZ2 Name*64", err: "nmea: FRFIR invalid condition: _", }, { name: "invalid nmea: AlarmAckState", raw: "$FRFIR,E,103000,FD,PT,000,007,A,_,Fire Alarm : TEST PT7 Name TEST DZ2 Name*73", err: "nmea: FRFIR invalid alarm acknowledgement state: _", }, } for _, tt := range tests { 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) fir := m.(FIR) fir.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, fir) } }) } } go-nmea-1.10.0/gga.go000066400000000000000000000034231465514462400142160ustar00rootroot00000000000000package 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. // http://aprs.gids.nl/nmea/#gga // https://gpsd.gitlab.io/gpsd/NMEA.html#_gga_global_positioning_system_fix_data // // Format: $--GGA,hhmmss.ss,ddmm.mm,a,ddmm.mm,a,x,xx,x.x,x.x,M,x.x,M,x.x,xxxx*hh // Example: $GNGGA,203415.000,6325.6138,N,01021.4290,E,1,8,2.42,72.5,M,41.5,M,,*7C 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) (Sentence, 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.10.0/gga_test.go000066400000000000000000000050041465514462400152520ustar00rootroot00000000000000package 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.10.0/gll.go000066400000000000000000000024151465514462400142360ustar00rootroot00000000000000package 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 // https://gpsd.gitlab.io/gpsd/NMEA.html#_gll_geographic_position_latitudelongitude // // Format : $--GLL,ddmm.mm,a,dddmm.mm,a,hhmmss.ss,a*hh // Format (NMEA 2.3+): $--GLL,ddmm.mm,a,dddmm.mm,a,hhmmss.ss,a,m*hh // Example: $IIGLL,5924.462,N,01030.048,E,062216,A*38 // Example: $GNGLL,4404.14012,N,12118.85993,W,001037.00,A,A*67 type GLL struct { BaseSentence Latitude float64 // Latitude Longitude float64 // Longitude Time Time // Time Stamp Validity string // validity - A=valid, V=invalid FFAMode string // FAA mode indicator (filled in NMEA 2.3 and later) } // newGLL constructor func newGLL(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeGLL) gll := 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), } if len(p.Fields) > 6 { gll.FFAMode = p.String(6, "FAA mode") } return gll, p.Err() } go-nmea-1.10.0/gll_test.go000066400000000000000000000025571465514462400153040ustar00rootroot00000000000000package 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", FFAMode: FAAModeAutonomous, }, }, { name: "good sentence without FAA mode", raw: "$IIGLL,5924.462,N,01030.048,E,062216,A*38", msg: GLL{ Latitude: MustParseLatLong("5924.462 N"), Longitude: MustParseLatLong("01030.048 E"), Time: Time{ Valid: true, Hour: 6, Minute: 22, Second: 16, Millisecond: 0, }, Validity: "A", FFAMode: "", }, }, { 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.10.0/gns.go000066400000000000000000000053541465514462400142540ustar00rootroot00000000000000package nmea const ( // TypeGNS type for GNS sentences TypeGNS = "GNS" ) // GNS mode values. These are same values ans GLL/RMC FAAMode* values. // Note: there can be other values (proprietary). const ( // 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 // https://gpsd.gitlab.io/gpsd/NMEA.html#_gns_fix_data // // Format: $--GNS,hhmmss.ss,ddmm.mm,a,dddmm.mm,a,c--c,xx,x.x,x.x,x.x,x.x,x.x*hh // Example: $GNGNS,014035.00,4332.69262,S,17235.48549,E,RR,13,0.9,25.63,11.24,,*70 // $GPGNS,224749.00,3333.4268304,N,11153.3538273,W,D,19,0.6,406.110,-26.294,6.0,0138,S*6A type GNS struct { BaseSentence Time Time // UTC of position Latitude float64 Longitude float64 // FAA mode indicator for each satellite navigation system (constellation) supported by device. // // May be up to six characters (according to GPSD). // '1' - GPS // '2' - GLONASS // '3' - Galileo // '4' - BDS // '5' - QZSS // '6' - NavIC (IRNSS) Mode []string SVs int64 // Total number of satellites in use, 00-99 HDOP float64 // Horizontal Dilution of Precision Altitude float64 // Antenna altitude, meters, re:mean-sea-level(geoid). Separation float64 // Geoidal separation meters Age float64 // Age of differential data Station int64 // Differential reference station ID NavStatus string // Navigation status (NMEA 4.1+). See NavStats* (`NavStatusAutonomous` etc) constants for possible values. } // newGNS Constructor func newGNS(s BaseSentence) (Sentence, 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"), } if len(p.Fields) >= 13 { m.NavStatus = p.EnumString( 12, "navigation status", NavStatusSafe, NavStatusCaution, NavStatusUnsafe, NavStatusNotValid, ) } return m, p.Err() } go-nmea-1.10.0/gns_test.go000066400000000000000000000046601465514462400153120ustar00rootroot00000000000000package 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, NavStatus: "", }, }, { 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, NavStatus: "", }, }, { name: "good sentence C", 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, NavStatus: "", }, }, { name: "good sentence D with nav status", raw: "$GPGNS,224749.00,3333.4268304,N,11153.3538273,W,D,19,0.6,406.110,-26.294,6.0,0138,S*6A", msg: GNS{ Time: Time{Valid: true, Hour: 22, Minute: 47, Second: 49, Millisecond: 0}, Latitude: 33.55711384000001, Longitude: -111.88923045499999, Mode: []string{"D"}, SVs: 19, HDOP: 0.6, Altitude: 406.11, Separation: -26.294, Age: 6, Station: 138, NavStatus: "S", }, }, { 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.10.0/go.mod000066400000000000000000000002141465514462400142320ustar00rootroot00000000000000module 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.10.0/go.sum000066400000000000000000000105241465514462400142640ustar00rootroot00000000000000github.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= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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.10.0/gsa.go000066400000000000000000000036121465514462400142320ustar00rootroot00000000000000package 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 // https://gpsd.gitlab.io/gpsd/NMEA.html#_gsa_gps_dop_and_active_satellites // // Format: $--GSA,a,a,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x.x,x.x,x.x*hh // Format (NMEA 4.1+): $--GSA,a,a,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x.x,x.x,x.x,x*hh // Example: $GNGSA,A,3,80,71,73,79,69,,,,,,,,1.83,1.09,1.47*17 // Example (NMEA 4.1+): $GNGSA,A,3,13,12,22,19,08,21,,,,,,,1.05,0.64,0.83,4*0B 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. // SystemID is (GNSS) System ID (NMEA 4.1+) // 1 - GPS // 2 - GLONASS // 3 - Galileo // 4 - BeiDou // 5 - QZSS // 6 - NavID (IRNSS) SystemID int64 } // newGSA parses the GSA sentence into this struct. func newGSA(s BaseSentence) (Sentence, 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") if len(p.Fields) > 17 { m.SystemID = p.Int64(17, "system ID") } return m, p.Err() } go-nmea-1.10.0/gsa_test.go000066400000000000000000000024561465514462400152760ustar00rootroot00000000000000package 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: "good sentence with system id", raw: "$GNGSA,A,3,13,12,22,19,08,21,,,,,,,1.05,0.64,0.83,4*0B", msg: GSA{ Mode: "A", FixType: "3", SV: []string{"13", "12", "22", "19", "08", "21"}, PDOP: 1.05, HDOP: 0.64, VDOP: 0.83, SystemID: 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.10.0/gsv.go000066400000000000000000000040031465514462400142520ustar00rootroot00000000000000package nmea const ( // TypeGSV type of GSV sentences for satellites in view TypeGSV = "GSV" ) // GSV represents the GPS Satellites in view // http://aprs.gids.nl/nmea/#glgsv // https://gpsd.gitlab.io/gpsd/NMEA.html#_gsv_satellites_in_view // // Format: $--GSV,x,x,x,x,x,x,x,...*hh // Format (NMEA 4.1+): $--GSV,x,x,x,x,x,x,x,...,x*hh // Example: $GPGSV,3,1,11,09,76,148,32,05,55,242,29,17,33,054,30,14,27,314,24*71 // Example (NMEA 4.1+): $GAGSV,3,1,09,02,00,179,,04,09,321,,07,11,134,11,11,10,227,,7*7F 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) // SystemID is (GNSS) System ID (NMEA 4.1+) // 1 - GPS // 2 - GLONASS // 3 - Galileo // 4 - BeiDou // 5 - QZSS // 6 - NavID (IRNSS) SystemID int64 } // 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) (Sentence, 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"), } i := 0 for ; i < 4; i++ { if 6+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"), }) } idxSID := (6 + (i-1)*4) + 1 if len(p.Fields) == idxSID+1 { m.SystemID = p.Int64(idxSID, "system ID") } return m, p.Err() } go-nmea-1.10.0/gsv_test.go000066400000000000000000000122021465514462400153110ustar00rootroot00000000000000package 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: "sentence with no satellite in view", raw: "$GBGSV,1,1,00,0*77", msg: GSV{ TotalMessages: 1, MessageNumber: 1, NumberSVsInView: 0, Info: nil, }, }, { name: "good sentence with system id", raw: "$GAGSV,3,1,09,02,00,179,,04,09,321,,07,11,134,11,11,10,227,,7*7F", msg: GSV{ TotalMessages: 3, MessageNumber: 1, NumberSVsInView: 9, Info: []GSVInfo{ {SVPRNNumber: 2, Elevation: 0, Azimuth: 179, SNR: 0}, {SVPRNNumber: 4, Elevation: 9, Azimuth: 321, SNR: 0}, {SVPRNNumber: 7, Elevation: 11, Azimuth: 134, SNR: 11}, {SVPRNNumber: 11, Elevation: 10, Azimuth: 227, SNR: 0}, }, SystemID: 7, }, }, { 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.10.0/hbt.go000066400000000000000000000020301465514462400142260ustar00rootroot00000000000000package nmea const ( // TypeHBT type of HBT sentence for heartbeat supervision sentence. TypeHBT = "HBT" ) // HBT is heartbeat supervision sentence to indicate if equipment is operating normally. // https://fcc.report/FCC-ID/ADB9ZWRTR100/2768717.pdf (page 1) FURUNO MARINE RADAR, model FAR-15XX manual // // Format: $--HBT,x.x,A,x*hh // Example: $HCHBT,98.3,0.0,E,12.6,W*57 type HBT struct { BaseSentence // Interval is configured repeat interval in seconds (1 - 999, null) Interval float64 // OperationStatus is equipment operation status: A = ok, V = not ok OperationStatus string // MessageID is sequential message identifier (0 - 9). Counts to 9 and resets to 0. MessageID int64 } // newHBT constructor func newHBT(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeHBT) m := HBT{ BaseSentence: s, Interval: p.Float64(0, "interval"), OperationStatus: p.EnumString(1, "operation status", StatusValid, StatusInvalid), MessageID: p.Int64(2, "message ID"), } return m, p.Err() } go-nmea-1.10.0/hbt_test.go000066400000000000000000000023131465514462400152710ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestHBT(t *testing.T) { var tests = []struct { name string raw string err string msg HBT }{ { name: "good sentence", raw: "$HCHBT,1.5,A,1*23", msg: HBT{ Interval: 1.5, OperationStatus: StatusValid, MessageID: 1, }, }, { name: "invalid interval and status", raw: "$HCHBT,,V,1*1E", msg: HBT{ Interval: 0, OperationStatus: StatusInvalid, MessageID: 1, }, }, { name: "invalid interval", raw: "$HCHBT,x.5,A,1*6A", err: "nmea: HCHBT invalid interval: x.5", }, { name: "invalid operation status", raw: "$HCHBT,1.5,X,1*3A", err: "nmea: HCHBT invalid operation status: X", }, { name: "invalid sequence identification", raw: "$HCHBT,1.5,A,x*6A", err: "nmea: HCHBT invalid message ID: x", }, } for _, tt := range tests { 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) hbt := m.(HBT) hbt.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, hbt) } }) } } go-nmea-1.10.0/hdg.go000066400000000000000000000024511465514462400142220ustar00rootroot00000000000000package nmea const ( // TypeHDG type of HDG sentence for vessel heading, deviation and variation with respect to magnetic north. TypeHDG = "HDG" ) // HDG is vessel heading (in degrees), deviation and variation with respect to magnetic north produced by any // device or system producing magnetic reading. // https://gpsd.gitlab.io/gpsd/NMEA.html#_hdg_heading_deviation_variation // // Format: $--HDG,x.x,y.y,a,z.z,a*hr // Example: $HCHDG,98.3,0.0,E,12.6,W*57 type HDG struct { BaseSentence Heading float64 // Heading in degrees Deviation float64 // Magnetic Deviation in degrees DeviationDirection string // Magnetic Deviation direction, E = Easterly, W = Westerly Variation float64 // Magnetic Variation in degrees VariationDirection string // Magnetic Variation direction, E = Easterly, W = Westerly } // newHDG constructor func newHDG(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeHDG) m := HDG{ BaseSentence: s, Heading: p.Float64(0, "heading"), Deviation: p.Float64(1, "deviation"), DeviationDirection: p.EnumString(2, "deviation direction", East, West), Variation: p.Float64(3, "variation"), VariationDirection: p.EnumString(4, "variation direction", East, West), } return m, p.Err() } go-nmea-1.10.0/hdg_test.go000066400000000000000000000025761465514462400152710ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestHDG(t *testing.T) { var tests = []struct { name string raw string err string msg HDG }{ { name: "good sentence", raw: "$HCHDG,98.3,0.1,E,12.6,W*56", msg: HDG{ Heading: 98.3, Deviation: 0.1, DeviationDirection: East, Variation: 12.6, VariationDirection: West, }, }, { name: "invalid Heading", raw: "$HCHDG,X,0.1,E,12.6,W*12", err: "nmea: HCHDG invalid heading: X", }, { name: "invalid Deviation", raw: "$HCHDG,98.3,x.1,E,12.6,W*1E", err: "nmea: HCHDG invalid deviation: x.1", }, { name: "invalid DeviationDirection", raw: "$HCHDG,98.3,0.1,X,12.6,W*4B", err: "nmea: HCHDG invalid deviation direction: X", }, { name: "invalid Variation", raw: "$HCHDG,98.3,0.1,E,x.1,W*2A", err: "nmea: HCHDG invalid variation: x.1", }, { name: "invalid VariationDirection", raw: "$HCHDG,98.3,0.1,E,12.6,X*59", err: "nmea: HCHDG invalid variation direction: X", }, } for _, tt := range tests { 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) hdg := m.(HDG) hdg.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, hdg) } }) } } go-nmea-1.10.0/hdm.go000066400000000000000000000015661465514462400142360ustar00rootroot00000000000000package nmea const ( // TypeHDM type of HDM sentence for vessel heading in degrees with respect to magnetic north TypeHDM = "HDM" // MagneticHDM for valid Magnetic heading MagneticHDM = "M" ) // HDM is vessel heading in degrees with respect to magnetic north produced by any device or system producing magnetic heading. // https://gpsd.gitlab.io/gpsd/NMEA.html#_hdm_heading_magnetic // // Format: $--HDM,xxx.xx,M*hh // Example: $HCHDM,093.8,M*2B type HDM struct { BaseSentence Heading float64 // Heading in degrees MagneticValid bool // Heading is respect to magnetic north } // newHDM constructor func newHDM(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeHDM) m := HDM{ BaseSentence: s, Heading: p.Float64(0, "heading"), MagneticValid: p.EnumString(1, "magnetic", MagneticHDM) == MagneticHDM, } return m, p.Err() } go-nmea-1.10.0/hdm_test.go000066400000000000000000000015451465514462400152720ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestHDM(t *testing.T) { var tests = []struct { name string raw string err string msg HDM }{ { name: "good sentence", raw: "$HCHDM,093.8,M*2B", msg: HDM{ Heading: 93.8, MagneticValid: true, }, }, { name: "invalid Magnetic", raw: "$HCHDM,093.8,X*3E", err: "nmea: HCHDM invalid magnetic: X", }, { name: "invalid Heading", raw: "$HCHDM,09X.X,M*20", err: "nmea: HCHDM invalid heading: 09X.X", }, } for _, tt := range tests { 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) hdm := m.(HDM) hdm.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, hdm) } }) } } go-nmea-1.10.0/hdt.go000066400000000000000000000012551465514462400142400ustar00rootroot00000000000000package nmea const ( // TypeHDT type for HDT sentences TypeHDT = "HDT" ) // HDT is the Actual vessel heading in degrees True. // http://aprs.gids.nl/nmea/#hdt // https://gpsd.gitlab.io/gpsd/NMEA.html#_gsv_satellites_in_view // // Format: $--HDT,x.x,T*hh // Example: $GPHDT,274.07,T*03 type HDT struct { BaseSentence Heading float64 // Heading in degrees True bool // Heading is relative to true north } // newHDT constructor func newHDT(s BaseSentence) (Sentence, 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.10.0/hdt_test.go000066400000000000000000000015031465514462400152730ustar00rootroot00000000000000package 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.10.0/hsc.go000066400000000000000000000017771465514462400142470ustar00rootroot00000000000000package nmea const ( // TypeHSC type of HSC sentence for Heading steering command TypeHSC = "HSC" ) // HSC - Heading steering command // https://gpsd.gitlab.io/gpsd/NMEA.html#_hsc_heading_steering_command // https://www.tronico.fi/OH6NT/docs/NMEA0183.pdf (page 11) // // Format: $--HSC, x.x, T, x.x, M,a*hh // Example: $FTHSC,40.12,T,39.11,M*5E type HSC struct { BaseSentence TrueHeading float64 // Heading Degrees, True TrueHeadingType string // T = True MagneticHeading float64 // Heading Degrees, Magnetic MagneticHeadingType string // M = Magnetic } // newHSC constructor func newHSC(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeHSC) return HSC{ BaseSentence: s, TrueHeading: p.Float64(0, "true heading"), TrueHeadingType: p.EnumString(1, "true heading type", HeadingTrue), MagneticHeading: p.Float64(2, "magnetic heading"), MagneticHeadingType: p.EnumString(3, "magnetic heading type", HeadingMagnetic), }, p.Err() } go-nmea-1.10.0/hsc_test.go000066400000000000000000000024371465514462400153000ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestHSC(t *testing.T) { var tests = []struct { name string raw string err string msg HSC }{ { name: "good sentence", raw: "$FTHSC,40.12,T,39.11,M*5E", msg: HSC{ TrueHeading: 40.12, TrueHeadingType: HeadingTrue, MagneticHeading: 39.11, MagneticHeadingType: HeadingMagnetic, }, }, { name: "invalid nmea: TrueHeading", raw: "$FTHSC,40.1x,T,39.11,M*14", err: "nmea: FTHSC invalid true heading: 40.1x", }, { name: "invalid nmea: TrueHeadingType", raw: "$FTHSC,40.12,x,39.11,M*72", err: "nmea: FTHSC invalid true heading type: x", }, { name: "invalid nmea: MagneticHeading", raw: "$FTHSC,40.12,T,x,M*02", err: "nmea: FTHSC invalid magnetic heading: x", }, { name: "invalid nmea: MagneticHeadingType", raw: "$FTHSC,40.12,T,39.11,x*6b", err: "nmea: FTHSC invalid magnetic heading type: x", }, } for _, tt := range tests { 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) hsc := m.(HSC) hsc.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, hsc) } }) } } go-nmea-1.10.0/mda.go000066400000000000000000000105551465514462400142250ustar00rootroot00000000000000package 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 // https://gpsd.gitlab.io/gpsd/NMEA.html#_mda_meteorological_composite // https://opencpn.org/wiki/dokuwiki/doku.php?id=opencpn:opencpn_user_manual:advanced_features:nmea_sentences#mda // // Format: $--MDA,n.nn,I,n.nnn,B,n.n,C,n.C,n.n,n,n.n,C,n.n,T,n.n,M,n.n,N,n.n,M*hh // Example: $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 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) (Sentence, 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.10.0/mda_test.go000066400000000000000000000023741465514462400152640ustar00rootroot00000000000000package 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.10.0/mta.go000066400000000000000000000013401465514462400142350ustar00rootroot00000000000000package nmea const ( // TypeMTA type of MTA sentence for Air Temperature TypeMTA = "MTA" ) // MTA - Air Temperature (obsolete, use XDR instead) // https://www.nmea.org/Assets/100108_nmea_0183_sentences_not_recommended_for_new_designs.pdf (page 7) // // Format: $--MTA,x.x,C*hh // Example: $IIMTA,13.3,C*04 type MTA struct { BaseSentence Temperature float64 // temperature Unit string // unit of temperature, should be degrees Celsius } // newMTA constructor func newMTA(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeMTA) return MTA{ BaseSentence: s, Temperature: p.Float64(0, "temperature"), Unit: p.EnumString(1, "temperature unit", TemperatureCelsius), }, p.Err() } go-nmea-1.10.0/mta_test.go000066400000000000000000000016011465514462400152740ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestMTA(t *testing.T) { var tests = []struct { name string raw string err string msg MTA }{ { name: "good sentence", raw: "$IIMTA,13.3,C*04", msg: MTA{ Temperature: 13.3, Unit: TemperatureCelsius, }, }, { name: "invalid nmea: Temperature", raw: "$IIMTA,x.x,C*35", err: "nmea: IIMTA invalid temperature: x.x", }, { name: "invalid nmea: Unit", raw: "$IIMTA,13.3,F*01", err: "nmea: IIMTA invalid temperature unit: F", }, } for _, tt := range tests { 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) mta := m.(MTA) mta.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, mta) } }) } } go-nmea-1.10.0/mtk.go000066400000000000000000000031461465514462400142550ustar00rootroot00000000000000package nmea const ( // TypeMTK type for PMTK sentences // Deprecated: use PMTK001 instead. PMTK protocol contains actually many commands. This struct is for MTK 001 ACK command. TypeMTK = "MTK001" ) // MTK is sentence for NMEA embedded command packet protocol, command type 001 - ACK. // https://www.rhydolabz.com/documents/25/PMTK_A11.pdf // https://www.sparkfun.com/datasheets/GPS/Modules/PMTK_Protocol.pdf // // The maximum length of each packet is restricted to 255 bytes which is longer than NMEA0183 82 bytes. // // Format: $PMTKxxx,c-c*hh // Example: $PMTK000*32 // // $PMTK001,101,0*33 // // Deprecated: use PMTK001 instead. PMTK protocol contains actually many commands. This struct is for MTK 001 ACK command. type MTK struct { BaseSentence Cmd, // Three bytes character string. From "000" to "999". An identifier used to tell the decoder how to decode the packet // Flag is flag field in PMTK001 packet. // Note: this field on only relevant for `PMTK001,Cmd,Flag` sentence. // Actual MTK protocol has variable amount of fields (whole sentence can be up to 255 bytes) // // Actual docs say: // DataField: The DataField has variable length depending on the packet type. A comma symbol ‘,’ must be inserted // ahead each data filed to help the decoder process the DataField. Flag int64 } // newMTK constructor // Deprecated: use newPMTK001 instead func newMTK(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeMTK) cmd := p.Int64(0, "command") flag := p.Int64(1, "flag") return MTK{ BaseSentence: s, Cmd: cmd, Flag: flag, }, p.Err() } go-nmea-1.10.0/mtk_test.go000066400000000000000000000015601465514462400153120ustar00rootroot00000000000000package nmea import ( "testing" "github.com/stretchr/testify/assert" ) func TestMTK(t *testing.T) { var tests = []struct { name string raw string err string msg MTK }{ { name: "good: Packet Type: 001 PMTK_ACK", raw: "$PMTK001,604,3*32", msg: MTK{ Cmd: 604, Flag: 3, }, }, { name: "missing flag", raw: "$PMTK001,604*2d", err: "nmea: PMTK001 invalid flag: index out of range", }, { name: "missing cmd", raw: "$PMTK001*33", err: "nmea: PMTK001 invalid command: index out of range", }, } for _, tt := range tests { 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.10.0/mtw.go000066400000000000000000000014431465514462400142670ustar00rootroot00000000000000package nmea const ( // TypeMTW type of MWT sentence describing mean temperature of water TypeMTW = "MTW" // CelsiusMTW is MTW unit of measurement in celsius CelsiusMTW = "C" ) // MTW is sentence for mean temperature of water. // https://gpsd.gitlab.io/gpsd/NMEA.html#_mtw_mean_temperature_of_water // // Format: $--MTW,TT.T,C*hh // Example: $INMTW,17.9,C*1B type MTW struct { BaseSentence Temperature float64 // Temperature, degrees CelsiusValid bool // Is unit of measurement Celsius } // newMTW constructor func newMTW(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeMTW) return MTW{ BaseSentence: s, Temperature: p.Float64(0, "temperature"), CelsiusValid: p.EnumString(1, "unit of measurement celsius", CelsiusMTW) == CelsiusMTW, }, p.Err() } go-nmea-1.10.0/mtw_test.go000066400000000000000000000015741465514462400153330ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestMTW(t *testing.T) { var tests = []struct { name string raw string err string msg MTW }{ { name: "good sentence", raw: "$INMTW,17.9,C*1B", msg: MTW{ Temperature: 17.9, CelsiusValid: true, }, }, { name: "invalid Temperature", raw: "$INMTW,x.9,C*65", err: "nmea: INMTW invalid temperature: x.9", }, { name: "invalid CelsiusValid", raw: "$INMTW,17.9,x*20", err: "nmea: INMTW invalid unit of measurement celsius: x", }, } for _, tt := range tests { 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) mtw := m.(MTW) mtw.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, mtw) } }) } } go-nmea-1.10.0/must_test.go000066400000000000000000000016371465514462400155140ustar00rootroot00000000000000package 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 } // 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.10.0/mwd.go000066400000000000000000000041111465514462400142420ustar00rootroot00000000000000package 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. // https://www.tronico.fi/OH6NT/docs/NMEA0183.pdf // http://gillinstruments.com/data/manuals/OMC-140_Operator_Manual_v1.04_131117.pdf // // Format: $--MWD,x.x,T,x.x,M,x.x,N,x.x,M*hh // Example: $WIMWD,10.1,T,10.1,M,12,N,40,M*5D type MWD struct { BaseSentence WindDirectionTrue float64 TrueValid bool WindDirectionMagnetic float64 MagneticValid bool WindSpeedKnots float64 KnotsValid bool WindSpeedMeters float64 MetersValid bool } func newMWD(s BaseSentence) (Sentence, 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.10.0/mwd_test.go000066400000000000000000000022321465514462400153030ustar00rootroot00000000000000package 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.10.0/mwv.go000066400000000000000000000044661465514462400143010ustar00rootroot00000000000000package 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. // https://gpsd.gitlab.io/gpsd/NMEA.html#_mwv_wind_speed_and_angle // // Format: $--MWV,x.x,a,x.x,a*hh // Example: $WIMWV,12.1,T,10.1,N,A*27 type MWV struct { BaseSentence WindAngle float64 Reference string WindSpeed float64 WindSpeedUnit string StatusValid bool } func newMWV(s BaseSentence) (Sentence, 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.10.0/mwv_test.go000066400000000000000000000016071465514462400153320ustar00rootroot00000000000000package 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.10.0/osd.go000066400000000000000000000060531465514462400142470ustar00rootroot00000000000000package nmea const ( // TypeOSD type for OSD sentence for Own Ship Data TypeOSD = "OSD" // OSDReferenceBottomTrackingLog is reference for bottom tracking log OSDReferenceBottomTrackingLog = "B" // OSDReferenceManual is reference for manually entered OSDReferenceManual = "M" // OSDReferenceWaterReferenced is reference for water referenced OSDReferenceWaterReferenced = "W" // OSDReferenceRadarTracking is reference for radar tracking of fixed target OSDReferenceRadarTracking = "R" // OSDReferencePositioningSystemGroundReference is reference for positioning system ground reference OSDReferencePositioningSystemGroundReference = "P" ) // OSD - Own Ship Data // https://gpsd.gitlab.io/gpsd/NMEA.html#_osd_own_ship_data // https://github.com/nohal/OpenCPN/wiki/ARPA-targets-tracking-implementation#osd---own-ship-data // // Format: $--OSD,x.x,A,x.x,a,x.x,a,x.x,x.x,a*hh // Example: $RAOSD,179.0,A,179.0,M,00.0,M,,,N*76 type OSD struct { BaseSentence // Heading is Heading in degrees Heading float64 // HeadingStatus is Heading status // * A - data valid // * V - data invalid HeadingStatus string // VesselTrueCourse is Vessel Course, degrees True VesselTrueCourse float64 // CourseReference is Course Reference, B/M/W/R/P // * B - bottom tracking log // * M - manually entered // * W - water referenced // * R - radar tracking of fixed target // * P - positioning system ground reference CourseReference string // VesselSpeed is Vessel Speed VesselSpeed float64 // SpeedReference is Speed Reference, B/M/W/R/P // * B - bottom tracking log // * M - manually entered // * W - water referenced // * R - radar tracking of fixed target // * P - positioning system ground reference. SpeedReference string // VesselSetTrue is Vessel Set, degrees True - Manually entered VesselSetTrue float64 // VesselDrift is Vessel drift (speed) - Manually entered VesselDrift float64 // SpeedUnits is Speed Units // * K - km/h // * N - Knots // * S - statute miles/h SpeedUnits string } // newOSD constructor func newOSD(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeOSD) m := OSD{ BaseSentence: s, Heading: p.Float64(0, "heading"), HeadingStatus: p.EnumString(1, "heading status", StatusValid, StatusInvalid), VesselTrueCourse: p.Float64(2, "vessel course true"), CourseReference: p.EnumString( 3, "course reference", OSDReferenceBottomTrackingLog, OSDReferenceManual, OSDReferenceWaterReferenced, OSDReferenceRadarTracking, OSDReferencePositioningSystemGroundReference, ), VesselSpeed: p.Float64(4, "vessel speed"), SpeedReference: p.EnumString( 5, "speed reference", OSDReferenceBottomTrackingLog, OSDReferenceManual, OSDReferenceWaterReferenced, OSDReferenceRadarTracking, OSDReferencePositioningSystemGroundReference, ), VesselSetTrue: p.Float64(6, "vessel set"), VesselDrift: p.Float64(7, "vessel drift"), SpeedUnits: p.EnumString(8, "speed units", DistanceUnitKilometre, DistanceUnitNauticalMile, DistanceUnitStatuteMile), } return m, p.Err() } go-nmea-1.10.0/osd_test.go000066400000000000000000000042611465514462400153050ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestOSD(t *testing.T) { var tests = []struct { name string raw string err string msg OSD }{ { name: "good sentence", raw: "$RAOSD,179.0,A,179.0,M,00.0,M,,,N*76", msg: OSD{ BaseSentence: BaseSentence{}, Heading: 179, HeadingStatus: "A", VesselTrueCourse: 179, CourseReference: "M", VesselSpeed: 0, SpeedReference: "M", VesselSetTrue: 0, VesselDrift: 0, SpeedUnits: "N", }, }, { name: "invalid nmea: Heading", raw: "$RAOSD,x179.0,A,179.0,M,00.0,M,,,N*0e", err: "nmea: RAOSD invalid heading: x179.0", }, { name: "invalid nmea: HeadingStatus", raw: "$RAOSD,179.0,xA,179.0,M,00.0,M,,,N*0e", err: "nmea: RAOSD invalid heading status: xA", }, { name: "invalid nmea: VesselTrueCourse", raw: "$RAOSD,179.0,A,x179.0,M,00.0,M,,,N*0e", err: "nmea: RAOSD invalid vessel course true: x179.0", }, { name: "invalid nmea: CourseReference", raw: "$RAOSD,179.0,A,179.0,xM,00.0,M,,,N*0e", err: "nmea: RAOSD invalid course reference: xM", }, { name: "invalid nmea: VesselSpeed", raw: "$RAOSD,179.0,A,179.0,M,x00.0,M,,,N*0e", err: "nmea: RAOSD invalid vessel speed: x00.0", }, { name: "invalid nmea: SpeedReference", raw: "$RAOSD,179.0,A,179.0,M,00.0,xM,,,N*0e", err: "nmea: RAOSD invalid speed reference: xM", }, { name: "invalid nmea: VesselSetTrue", raw: "$RAOSD,179.0,A,179.0,M,00.0,M,x,,N*0e", err: "nmea: RAOSD invalid vessel set: x", }, { name: "invalid nmea: VesselDrift", raw: "$RAOSD,179.0,A,179.0,M,00.0,M,,x,N*0e", err: "nmea: RAOSD invalid vessel drift: x", }, { name: "invalid nmea: SpeedUnits", raw: "$RAOSD,179.0,A,179.0,M,00.0,M,,,xN*0e", err: "nmea: RAOSD invalid speed units: xN", }, } for _, tt := range tests { 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) mm := m.(OSD) mm.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, mm) } }) } } go-nmea-1.10.0/parser.go000066400000000000000000000137411465514462400147600ustar00rootroot00000000000000package 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 } // HexInt64 returns the hex encoded int64 value at the specified index. // If the value is an empty string, 0 is returned. func (p *Parser) HexInt64(i int, context string) int64 { s := p.String(i, context) if p.err != nil { return 0 } if s == "" { return 0 } value, err := strconv.ParseInt(s, 16, 64) if err != nil { p.SetErr(context, s) } return value } // 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 { return p.NullInt64(i, context).Value } // NullInt64 returns the int64 value at the specified index. // If the value is an empty string, Valid is set to false func (p *Parser) NullInt64(i int, context string) Int64 { s := p.String(i, context) if p.err != nil { return Int64{} } if s == "" { return Int64{} } v, err := strconv.ParseInt(s, 10, 64) if err != nil { p.SetErr(context, s) return Int64{} } return Int64{Value: v, Valid: true} } // 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 { return p.NullFloat64(i, context).Value } // NullFloat64 returns the Float64 value at the specified index. // If the value is an empty string, Valid is set to false. func (p *Parser) NullFloat64(i int, context string) Float64 { s := p.String(i, context) if p.err != nil { return Float64{} } if s == "" { return Float64{} } v, err := strconv.ParseFloat(s, 64) if err != nil { p.SetErr(context, s) return Float64{} } return Float64{Value: v, Valid: true} } // 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.10.0/parser_test.go000066400000000000000000000227571465514462400160260ustar00rootroot00000000000000package nmea import ( "testing" "github.com/stretchr/testify/assert" ) func TestParser(t *testing.T) { 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: "NullInt64", fields: []string{"123"}, expected: Int64{Value: 123, Valid: true}, parse: func(p *Parser) interface{} { return p.NullInt64(0, "context") }, }, { name: "NullInt64 empty field is invalid", fields: []string{""}, expected: Int64{}, parse: func(p *Parser) interface{} { return p.NullInt64(0, "context") }, }, { name: "NullInt64 invalid", fields: []string{"abc"}, expected: Int64{}, hasErr: true, parse: func(p *Parser) interface{} { return p.NullInt64(0, "context") }, }, { name: "NullInt64 with existing error", fields: []string{"123"}, expected: Int64{}, hasErr: true, parse: func(p *Parser) interface{} { p.SetErr("context", "value") return p.NullInt64(0, "context") }, }, { name: "NullFloat64", fields: []string{"123.123"}, expected: Float64{Value: 123.123, Valid: true}, parse: func(p *Parser) interface{} { return p.NullFloat64(0, "context") }, }, { name: "NullFloat64 empty field is invalid", fields: []string{""}, expected: Float64{}, parse: func(p *Parser) interface{} { return p.NullFloat64(0, "context") }, }, { name: "NullFloat64 invalid", fields: []string{"abc"}, expected: Float64{}, hasErr: true, parse: func(p *Parser) interface{} { return p.NullFloat64(0, "context") }, }, { name: "NullFloat64 with existing error", fields: []string{"123.123"}, expected: Float64{}, hasErr: true, parse: func(p *Parser) interface{} { p.SetErr("context", "value") return p.NullFloat64(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") }, }, { name: "HexInt64", fields: []string{"FF"}, expected: int64(255), hasErr: false, parse: func(p *Parser) interface{} { return p.HexInt64(0, "context") }, }, { name: "HexInt64 empty field", fields: []string{""}, expected: int64(0), hasErr: false, parse: func(p *Parser) interface{} { return p.HexInt64(0, "context") }, }, { name: "HexInt64 negative", fields: []string{"-FF"}, expected: int64(-255), hasErr: false, parse: func(p *Parser) interface{} { return p.HexInt64(0, "context") }, }, { name: "HexInt64 longer", fields: []string{"8000"}, expected: int64(32768), hasErr: false, parse: func(p *Parser) interface{} { return p.HexInt64(0, "context") }, }, { name: "HexInt64 not hex", fields: []string{"xF"}, expected: int64(0), hasErr: true, parse: func(p *Parser) interface{} { return p.HexInt64(0, "context") }, }, } 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.10.0/pcdin.go000066400000000000000000000035371465514462400145630ustar00rootroot00000000000000package nmea import ( "encoding/hex" "fmt" "strconv" ) const ( // TypePCDIN is type of PCDIN sentence for SeaSmart.Net Protocol TypePCDIN = "CDIN" ) // PCDIN - SeaSmart.Net Protocol transfers NMEA2000 message as NMEA0183 sentence // http://www.seasmart.net/pdf/SeaSmart_HTTP_Protocol_RevG_043012.pdf (SeaSmart.Net Protocol Specification Version 1.7) // // Note: older SeaSmart.Net Protocol versions have different amount of fields // // Format: $PCDIN,hhhhhh,hhhhhhhh,hh,h--h*hh // Example: $PCDIN,01F112,000C72EA,09,28C36A0000B40AFD*56 type PCDIN struct { BaseSentence PGN uint32 // PGN of NMEA2000 packet Timestamp uint32 // ticks since something Source uint8 // 0-255 Data []byte // can be more than 8 bytes i.e can contain assembled fast packets } // newPCDIN constructor func newPCDIN(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypePCDIN) if len(p.Fields) != 4 { p.SetErr("fields", "invalid number of fields in sentence") return nil, p.Err() } pgn, err := strconv.ParseUint(p.Fields[0], 16, 24) if err != nil { p.err = fmt.Errorf("nmea: %s failed to parse PGN field: %w", p.Prefix(), err) return nil, p.Err() } timestamp, err := strconv.ParseUint(p.Fields[1], 16, 32) if err != nil { p.err = fmt.Errorf("nmea: %s failed to parse timestamp field: %w", p.Prefix(), err) return nil, p.Err() } source, err := strconv.ParseUint(p.Fields[2], 16, 8) if err != nil { p.err = fmt.Errorf("nmea: %s failed to parse source field: %w", p.Prefix(), err) return nil, p.Err() } data, err := hex.DecodeString(p.Fields[3]) if err != nil { p.err = fmt.Errorf("nmea: %s failed to decode data: %w", p.Prefix(), err) return nil, p.Err() } return PCDIN{ BaseSentence: s, PGN: uint32(pgn), Timestamp: uint32(timestamp), Source: uint8(source), Data: data, }, p.Err() } go-nmea-1.10.0/pcdin_test.go000066400000000000000000000033641465514462400156200ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestPCDIN(t *testing.T) { var tests = []struct { name string raw string err string msg PCDIN }{ { name: "good sentence", raw: "$PCDIN,01F112,000C72EA,09,28C36A0000B40AFD*56", msg: PCDIN{ PGN: 127250, // 0x1F112 Vessel Heading Timestamp: 815850, Source: 9, Data: []byte{0x28, 0xC3, 0x6A, 0x00, 0x00, 0xB4, 0x0A, 0xFD}, }, }, { name: "invalid number of fields", raw: "$PCDIN,01F112,000C72EA,28C36A0000B40AFD*73", err: "nmea: PCDIN invalid fields: invalid number of fields in sentence", }, { name: "invalid PGN field", raw: "$PCDIN,x1F112,000C72EA,09,28C36A0000B40AFD*1e", err: "nmea: PCDIN failed to parse PGN field: strconv.ParseUint: parsing \"x1F112\": invalid syntax", }, { name: "invalid timestamp field", raw: "$PCDIN,01F112,x00C72EA,09,28C36A0000B40AFD*1e", err: "nmea: PCDIN failed to parse timestamp field: strconv.ParseUint: parsing \"x00C72EA\": invalid syntax", }, { name: "invalid source field", raw: "$PCDIN,01F112,000C72EA,x9,28C36A0000B40AFD*1e", err: "nmea: PCDIN failed to parse source field: strconv.ParseUint: parsing \"x9\": invalid syntax", }, { name: "invalid hex data", raw: "$PCDIN,01F112,000C72EA,09,x8C36A0000B40AFD*1c", err: "nmea: PCDIN failed to decode data: encoding/hex: invalid byte: U+0078 'x'", }, } for _, tt := range tests { 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.(PCDIN) pgrme.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, pgrme) } }) } } go-nmea-1.10.0/pgn.go000066400000000000000000000040401465514462400142400ustar00rootroot00000000000000package nmea import ( "encoding/hex" "fmt" "strconv" ) const ( // TypePGN is type of PGN sentence for transferring single NMEA2000 frame as NMEA0183 sentence TypePGN = "PGN" ) // PGN - transferring single NMEA2000 frame as NMEA0183 sentence // https://opencpn.org/wiki/dokuwiki/lib/exe/fetch.php?media=opencpn:software:mxpgn_sentence.pdf // // Format: $--PGN,pppppp,aaaa,c--c*hh // Example: $MXPGN,01F112,2807,FC7FFF7FFF168012*11 type PGN struct { BaseSentence PGN uint32 // PGN of NMEA2000 packet IsSend bool // is this sentence received or for sending Priority uint8 // 0-7 Address uint8 // depending on the IsSend field this is Source Address of received packet or Destination for send packet Data []byte // 1-8 bytes. This is single N2K frame. N2K Fast-packets should be assembled from individual frames } // newPGN constructor func newPGN(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypePGN) if len(p.Fields) != 3 { p.SetErr("fields", "invalid number of fields in sentence") return nil, p.Err() } pgn, err := strconv.ParseUint(p.Fields[0], 16, 24) if err != nil { p.err = fmt.Errorf("nmea: %s failed to parse PGN field: %w", p.Prefix(), err) return nil, p.Err() } attributes, err := strconv.ParseUint(p.Fields[1], 16, 16) if err != nil { p.err = fmt.Errorf("nmea: %s failed to parse attributes field: %w", p.Prefix(), err) return nil, p.Err() } dataLength := int((attributes >> 8) & 0b1111) // bits 8-11 if dataLength*2 != (len(p.Fields[2])) { p.SetErr("dlc", "data length does not match actual data length") return nil, p.Err() } data, err := hex.DecodeString(p.Fields[2]) if err != nil { p.err = fmt.Errorf("nmea: %s failed to decode data: %w", p.Prefix(), err) return nil, p.Err() } return PGN{ BaseSentence: s, PGN: uint32(pgn), IsSend: attributes>>15 == 1, // bit 15 Priority: uint8((attributes >> 12) & 0b111), // bits 12,13,14 Address: uint8(attributes), // bits 0-7 Data: data, }, p.Err() } go-nmea-1.10.0/pgn_test.go000066400000000000000000000032721465514462400153050ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestPGN(t *testing.T) { var tests = []struct { name string raw string err string msg PGN }{ { name: "good sentence", raw: "$MXPGN,01F112,2807,FC7FFF7FFF168012*11", msg: PGN{ PGN: 127250, // 0x1F112 Vessel Heading IsSend: false, Priority: 2, Address: 7, Data: []byte{0xFC, 0x7f, 0xFF, 0x7f, 0xFF, 0x16, 0x80, 0x12}, }, }, { name: "invalid number of fields", raw: "$MXPGN,01F112,FC7FFF7FFF168012*30", err: "nmea: MXPGN invalid fields: invalid number of fields in sentence", }, { name: "invalid PGN field", raw: "$MXPGN,0xF112,2807,FC7FFF7FFF168012*58", err: "nmea: MXPGN failed to parse PGN field: strconv.ParseUint: parsing \"0xF112\": invalid syntax", }, { name: "invalid attributes field", raw: "$MXPGN,01F112,x807,FC7FFF7FFF168012*5b", err: "nmea: MXPGN failed to parse attributes field: strconv.ParseUint: parsing \"x807\": invalid syntax", }, { name: "invalid data length field", raw: "$MXPGN,01F112,2207,FC7FFF7FFF168012*1b", err: "nmea: MXPGN invalid dlc: data length does not match actual data length", }, { name: "invalid hex data", raw: "$MXPGN,01F112,2807,xC7FFF7FFF168012*2f", err: "nmea: MXPGN failed to decode data: encoding/hex: invalid byte: U+0078 'x'", }, } for _, tt := range tests { 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.(PGN) pgrme.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, pgrme) } }) } } go-nmea-1.10.0/pgrme.go000066400000000000000000000022741465514462400145750ustar00rootroot00000000000000package 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 // https://gpsd.gitlab.io/gpsd/NMEA.html#_pgrme_garmin_estimated_error // // Format: $PGRME,hhh,M,vvv,M,ttt,M*hh // Example: $PGRME,3.3,M,4.9,M,6.0,M*25 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) (Sentence, 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.10.0/pgrme_test.go000066400000000000000000000024751465514462400156370ustar00rootroot00000000000000package 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.10.0/phtro.go000066400000000000000000000022661465514462400146200ustar00rootroot00000000000000package nmea const ( // TypePHTRO type of PHTRO sentence for vessel pitch and roll TypePHTRO = "HTRO" // PHTROBowUP for bow up PHTROBowUP = "M" // PHTROBowDown for bow down PHTROBowDown = "P" // PHTROPortUP for port up PHTROPortUP = "T" // PHTROPortDown for port down PHTROPortDown = "B" ) // PHTRO is proprietary sentence for vessel pitch and roll. // https://www.igp.de/manuals/7-INS-InterfaceLibrary_MU-INSIII-AN-001-O.pdf (page 172) // // Format: $PHTRO,x.xx,a,y.yy,b*hh // Example: $PHTRO,10.37,P,177.62,T*65 type PHTRO struct { BaseSentence Pitch float64 // Pitch in degrees Bow string // "M" for bow up and "P" for bow down (2 digits after the decimal point) Roll float64 // Roll in degrees Port string // "B" for port down and "T" for port up (2 digits after the decimal point) } // newPHTRO constructor func newPHTRO(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypePHTRO) m := PHTRO{ BaseSentence: s, Pitch: p.Float64(0, "pitch"), Bow: p.EnumString(1, "bow", PHTROBowUP, PHTROBowDown), Roll: p.Float64(2, "roll"), Port: p.EnumString(3, "port", PHTROPortUP, PHTROPortDown), } return m, p.Err() } go-nmea-1.10.0/phtro_test.go000066400000000000000000000021711465514462400156520ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestPHTRO(t *testing.T) { var tests = []struct { name string raw string err string msg PHTRO }{ { name: "good sentence", raw: "$PHTRO,10.37,P,177.62,T*65", msg: PHTRO{ Pitch: 10.37, Bow: PHTROBowDown, Roll: 177.62, Port: PHTROPortUP, }, }, { name: "invalid Pitch", raw: "$PHTRO,x,P,177.62,T*36", err: "nmea: PHTRO invalid pitch: x", }, { name: "invalid Bow", raw: "$PHTRO,10.37,x,177.62,T*4D", err: "nmea: PHTRO invalid bow: x", }, { name: "invalid Roll", raw: "$PHTRO,10.37,P,x,T*06", err: "nmea: PHTRO invalid roll: x", }, { name: "invalid Port", raw: "$PHTRO,10.37,P,177.62,x*49", err: "nmea: PHTRO invalid port: x", }, } for _, tt := range tests { 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) phtro := m.(PHTRO) phtro.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, phtro) } }) } } go-nmea-1.10.0/pmtk.go000066400000000000000000000022201465514462400144250ustar00rootroot00000000000000package nmea const ( // TypePMTK001 type of acknowledgement sentence for MTK protocol TypePMTK001 = "MTK001" ) // PMTK001 is sentence for acknowledgement of previously sent command/packet // https://www.rhydolabz.com/documents/25/PMTK_A11.pdf // https://www.sparkfun.com/datasheets/GPS/Modules/PMTK_Protocol.pdf // // The maximum length of each packet is restricted to 255 bytes which is longer than NMEA0183 82 bytes. // // Format: $PMTK001,c-c,d*hh // Example: $PMTK001,101,0*33 type PMTK001 struct { BaseSentence // Cmd is command/packet acknowledgement is sent for. // Three bytes character string. From "000" to "999". Cmd int64 // Flag is acknowledgement status for previously sent command/packet // 0 = invalid command/packet type // 1 = unsupported command packet type // 2 = valid command/packet, but action failed // 3 = valid command/packet and action succeeded Flag int64 } // newPMTK001 constructor func newPMTK001(s BaseSentence) (Sentence, error) { p := NewParser(s) cmd := p.Int64(0, "command") flag := p.Int64(1, "flag") return PMTK001{ BaseSentence: s, Cmd: cmd, Flag: flag, }, p.Err() } go-nmea-1.10.0/pmtk_test.go000066400000000000000000000021371465514462400154730ustar00rootroot00000000000000package nmea import ( "testing" "github.com/stretchr/testify/assert" ) func TestPMTK001(t *testing.T) { var tests = []struct { name string raw string err string msg PMTK001 }{ { name: "good: Packet Type: 001 PMTK_ACK", raw: "$PMTK001,604,3*32", msg: PMTK001{ Cmd: 604, Flag: 3, }, }, { name: "missing flag", raw: "$PMTK001,604*2d", err: "nmea: PMTK001 invalid flag: index out of range", }, { name: "missing cmd", raw: "$PMTK001*33", err: "nmea: PMTK001 invalid command: index out of range", }, } p := SentenceParser{} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { m, err := p.Parse(tt.raw) if tt.err != "" { assert.Error(t, err) assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) mtk := m.(PMTK001) // is used by non-global SentenceParser instance mtk.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, mtk) } }) } } func TestDefaultParserUsesDeprecatedMTK(t *testing.T) { m, err := Parse("$PMTK001,604,3*32") assert.NoError(t, err) assert.IsType(t, MTK{}, m) } go-nmea-1.10.0/prdid.go000066400000000000000000000015271465514462400145650ustar00rootroot00000000000000package nmea const ( // TypePRDID type of PRDID sentence for vessel pitch, roll and heading TypePRDID = "RDID" ) // PRDID is proprietary sentence for vessel pitch, roll and heading. // https://www.xsens.com/hubfs/Downloads/Manuals/MT_Low-Level_Documentation.pdf (page 37) // // Format: $PRDID,aPPP.PP,bRRR.RR,HHH.HH*hh // Example: $PRDID,-10.37,2.34,230.34*AA type PRDID struct { BaseSentence Pitch float64 // Pitch in degrees (positive bow up) Roll float64 // Roll in degrees (positive port up) Heading float64 // True heading in degrees } // newPRDID constructor func newPRDID(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypePRDID) m := PRDID{ BaseSentence: s, Pitch: p.Float64(0, "pitch"), Roll: p.Float64(1, "roll"), Heading: p.Float64(2, "heading"), } return m, p.Err() } go-nmea-1.10.0/prdid_test.go000066400000000000000000000020151465514462400156150ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestPRDID(t *testing.T) { var tests = []struct { name string raw string err string msg PRDID }{ { name: "good sentence", raw: "$PRDID,-10.37,2.34,230.34*62", msg: PRDID{ Pitch: -10.37, Roll: 2.34, Heading: 230.34, }, }, { name: "invalid Pitch", raw: "$PRDID,x.37,2.34,230.34*36", err: "nmea: PRDID invalid pitch: x.37", }, { name: "invalid Roll", raw: "$PRDID,-10.37,x.34,230.34*28", err: "nmea: PRDID invalid roll: x.34", }, { name: "invalid Heading", raw: "$PRDID,-10.37,2.34,x.34*2B", err: "nmea: PRDID invalid heading: x.34", }, } for _, tt := range tests { 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) prdid := m.(PRDID) prdid.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, prdid) } }) } } go-nmea-1.10.0/pskpdpt.go000066400000000000000000000033031465514462400151420ustar00rootroot00000000000000package nmea const ( // TypePSKPDPT type for proprietary Skipper PSKPDPT sentences TypePSKPDPT = "SKPDPT" ) // PSKPDPT - Depth of Water for multiple transducer installation // https://www.alphatronmarine.com/files/products/120-echonav-skipper-gds101-instoper-manual-12-6-2017_1556099135_47f5f8d1.pdf (page 56, Edition: 2017.06.12) // https://www.kongsberg.com/globalassets/maritime/km-products/product-documents/164821aa_rd301_instruction_manual_lr.pdf (page 2, 857-164821aa) // // Format: $PSKPDPT,x.x,x.x,x.x,xx,xx,c--c*hh // Example: $PSKPDPT,0002.5,+00.0,0010,10,03,*77 type PSKPDPT struct { BaseSentence // Depth is water depth relative to transducer, meters Depth float64 // Offset from transducer, meters Offset float64 // RangeScale is Maximum range scale in use, meters RangeScale float64 // BottomEchoStrength is Bottom echo strength (0,9) BottomEchoStrength int64 // ChannelNumber is Echo sounder channel number (0-99) (1 = 38 kHz. 2 = 50 kHz. 3 = 200 kHz) ChannelNumber int64 // TransducerLocation is Transducer location. Text string, indicating transducer position: FWD/AFT/PORT/STB. // If position is not preset by operator, empty field is provided. TransducerLocation string } // newPSKPDPT constructor func newPSKPDPT(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypePSKPDPT) sentence := PSKPDPT{ BaseSentence: s, Depth: p.Float64(0, "depth"), Offset: p.Float64(1, "offset"), RangeScale: p.Float64(2, "range scale"), BottomEchoStrength: p.Int64(3, "bottom echo strength"), ChannelNumber: p.Int64(4, "channel number"), TransducerLocation: p.String(5, "transducer location"), } return sentence, p.Err() } go-nmea-1.10.0/pskpdpt_test.go000066400000000000000000000035141465514462400162050ustar00rootroot00000000000000package nmea import ( "testing" "github.com/stretchr/testify/assert" ) func TestPSKPDPT(t *testing.T) { var testcases = []struct { name string raw string err string msg PSKPDPT }{ { name: "good sentence, empty location", raw: "$PSKPDPT,0002.5,+00.0,0010,10,03,*77", msg: PSKPDPT{ Depth: 2.5, Offset: 0, RangeScale: 10, BottomEchoStrength: 10, ChannelNumber: 03, TransducerLocation: "", }, }, { name: "good sentence", raw: "$PSKPDPT,0002.5,-01.1,0010,10,03,AFT*22", msg: PSKPDPT{ Depth: 2.5, Offset: -1.1, RangeScale: 10, BottomEchoStrength: 10, ChannelNumber: 03, TransducerLocation: "AFT", }, }, { name: "invalid nmea: Depth", raw: "$PSKPDPT,x0002.5,+00.0,0010,10,03,*0f", err: "nmea: PSKPDPT invalid depth: x0002.5", }, { name: "invalid nmea: Offset", raw: "$PSKPDPT,0002.5,+x00.0,0010,10,03,*0f", err: "nmea: PSKPDPT invalid offset: +x00.0", }, { name: "invalid nmea: RangeScale", raw: "$PSKPDPT,0002.5,+00.0,x0010,10,03,*0f", err: "nmea: PSKPDPT invalid range scale: x0010", }, { name: "invalid nmea: BottomEchoStrength", raw: "$PSKPDPT,0002.5,+00.0,0010,10x,03,*0f", err: "nmea: PSKPDPT invalid bottom echo strength: 10x", }, { name: "invalid nmea: ChannelNumber", raw: "$PSKPDPT,0002.5,+00.0,0010,10,0x3,*0f", err: "nmea: PSKPDPT invalid channel number: 0x3", }, } for _, tt := range testcases { 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) sentence := m.(PSKPDPT) sentence.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, sentence) } }) } } go-nmea-1.10.0/psoncms.go000066400000000000000000000045331465514462400151450ustar00rootroot00000000000000package nmea const ( // TypePSONCMS is type of PSONCMS sentence for proprietary Xsens IMU/VRU/AHRS device TypePSONCMS = "SONCMS" ) // PSONCMS is proprietary Xsens IMU/VRU/AHRS device sentence for quaternion, acceleration, rate of turn, // magnetic Field, sensor temperature. // https://www.xsens.com/hubfs/Downloads/Manuals/MT_Low-Level_Documentation.pdf (page 37) // // Format: $PSONCMS,Q.QQQQ,P.PPPP,R.RRRR,S.SSSS,XX.XXXX,YY.YYYY,ZZ.ZZZZ, // FF.FFFF,GG.GGGG,HH.HHHH,NN.NNNN,MM,MMMM,PP.PPPP,TT.T*hh // Example: $PSONCMS,0.0905,0.4217,0.9020,-0.0196,-1.7685,0.3861,-9.6648,-0.0116,0.0065,-0.0080,0.0581,0.3846,0.7421,33.1*76 type PSONCMS struct { BaseSentence Quaternion0 float64 // q0 from quaternions Quaternion1 float64 // q1 from quaternions Quaternion2 float64 // q2 from quaternions Quaternion3 float64 // q3 from quaternions AccelerationX float64 // acceleration X in m/s2 AccelerationY float64 // acceleration Y in m/s2 AccelerationZ float64 // acceleration Z in m/s2 RateOfTurnX float64 // rate of turn X in rad/s RateOfTurnY float64 // rate of turn Y in rad/s RateOfTurnZ float64 // rate of turn Z in rad/s MagneticFieldX float64 // magnetic field X in a.u. MagneticFieldY float64 // magnetic field Y in a.u. MagneticFieldZ float64 // magnetic field Z in a.u. SensorTemperature float64 // sensor temperature in degrees Celsius } // newPSONCMS constructor func newPSONCMS(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypePSONCMS) m := PSONCMS{ BaseSentence: s, Quaternion0: p.Float64(0, "q0 from quaternions"), Quaternion1: p.Float64(1, "q1 from quaternions"), Quaternion2: p.Float64(2, "q2 from quaternions"), Quaternion3: p.Float64(3, "q3 from quaternions"), AccelerationX: p.Float64(4, "acceleration X"), AccelerationY: p.Float64(5, "acceleration Y"), AccelerationZ: p.Float64(6, "acceleration Z"), RateOfTurnX: p.Float64(7, "rate of turn X"), RateOfTurnY: p.Float64(8, "rate of turn Y"), RateOfTurnZ: p.Float64(9, "rate of turn Z"), MagneticFieldX: p.Float64(10, "magnetic field X"), MagneticFieldY: p.Float64(11, "magnetic field Y"), MagneticFieldZ: p.Float64(12, "magnetic field Z"), SensorTemperature: p.Float64(13, "sensor temperature"), } return m, p.Err() } go-nmea-1.10.0/psoncms_test.go000066400000000000000000000031341465514462400162000ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestPSONCMS(t *testing.T) { var tests = []struct { name string raw string err string msg PSONCMS }{ { name: "good sentence", raw: "$PSONCMS,0.0905,0.4217,0.9020,-0.0196,-1.7685,0.3861,-9.6648,-0.0116,0.0065,-0.0080,0.0581,0.3846,0.7421,33.1*76", msg: PSONCMS{ BaseSentence: BaseSentence{}, Quaternion0: 0.0905, Quaternion1: 0.4217, Quaternion2: 0.9020, Quaternion3: -0.0196, AccelerationX: -1.7685, AccelerationY: 0.3861, AccelerationZ: -9.6648, RateOfTurnX: -0.0116, RateOfTurnY: 0.0065, RateOfTurnZ: -0.0080, MagneticFieldX: 0.0581, MagneticFieldY: 0.3846, MagneticFieldZ: 0.7421, SensorTemperature: 33.1, }, }, { name: "invalid Quaternion0", raw: "$PSONCMS,x,0.4217,0.9020,-0.0196,-1.7685,0.3861,-9.6648,-0.0116,0.0065,-0.0080,0.0581,0.3846,0.7421,33.1*1C", err: "nmea: PSONCMS invalid q0 from quaternions: x", }, { name: "invalid Quaternion1", raw: "$PSONCMS,0.0905,x,0.9020,-0.0196,-1.7685,0.3861,-9.6648,-0.0116,0.0065,-0.0080,0.0581,0.3846,0.7421,33.1*10", err: "nmea: PSONCMS invalid q1 from quaternions: x", }, } for _, tt := range tests { 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) psoncms := m.(PSONCMS) psoncms.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, psoncms) } }) } } go-nmea-1.10.0/query.go000066400000000000000000000013441465514462400146250ustar00rootroot00000000000000package nmea const ( // TypeQuery type of Query sentence for a listener to request a particular sentence from a talker TypeQuery = "Q" ) // Query sentences is special type of sentence for a listener to request a particular sentence from a talker. // https://www.tronico.fi/OH6NT/docs/NMEA0183.pdf (page 3) // // Format: $ttllQ,sss*hh // Example: $CCGPQ,GGA*2B type Query struct { BaseSentence DestinationTalkerID string RequestedSentence string } // newQuery constructor func newQuery(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeQuery) return Query{ BaseSentence: s, DestinationTalkerID: s.Raw[3:5], RequestedSentence: p.String(0, "requested sentence"), }, p.Err() } go-nmea-1.10.0/query_test.go000066400000000000000000000015151465514462400156640ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestQuery(t *testing.T) { var tests = []struct { name string raw string err string msg Query }{ { name: "good sentence", raw: "$CCGPQ,GGA*2B", msg: Query{ BaseSentence: BaseSentence{}, DestinationTalkerID: "GP", RequestedSentence: "GGA", }, }, { name: "invalid nmea: RequestedSentence", raw: "$CCGPQ*46", err: "nmea: CCQ invalid requested sentence: index out of range", }, } for _, tt := range tests { 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) rmb := m.(Query) rmb.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, rmb) } }) } } go-nmea-1.10.0/rmb.go000066400000000000000000000063731465514462400142470ustar00rootroot00000000000000package nmea const ( // TypeRMB type of RMB sentence for recommended minimum navigation information TypeRMB = "RMB" // DataStatusWarningClearRMB means data is OK DataStatusWarningClearRMB = "A" // DataStatusWarningSetRMB means warning flag set DataStatusWarningSetRMB = "V" ) // RMB - Recommended Minimum Navigation Information. To be sent by a navigation receiver when a destination waypoint // is active. Alternative to BOD and BWW sentences. // https://gpsd.gitlab.io/gpsd/NMEA.html#_rmb_recommended_minimum_navigation_information // http://aprs.gids.nl/nmea/#rmb // // Format: $--RMB,A,x.x,a,c--c,c--c,llll.ll,a,yyyyy.yy,a,x.x,x.x,x.x,A*hh // Format (NMEA2.3+): $--RMB,A,x.x,a,c--c,c--c,llll.ll,a,yyyyy.yy,a,x.x,x.x,x.x,A,m*hh // Example: $GPRMB,A,0.66,L,003,004,4917.24,N,12309.57,W,001.3,052.5,000.5,V*0B type RMB struct { BaseSentence // DataStatus is status of data, // * A = OK // * V = Navigation receiver warning DataStatus string // Cross Track error (nautical miles, 9.9 max) CrossTrackErrorNauticalMiles float64 // DirectionToSteer is Direction to steer, // * L = left // * R = right DirectionToSteer string // OriginWaypointID is origin (FROM) waypoint ID OriginWaypointID string // DestinationWaypointID is destination (TO) waypoint ID DestinationWaypointID string // DestinationLatitude is destination waypoint latitude DestinationLatitude float64 // DestinationLongitude is destination waypoint longitude DestinationLongitude float64 // RangeToDestinationNauticalMiles is range to destination, nautical miles (999,9 max) RangeToDestinationNauticalMiles float64 // TrueBearingToDestination is true bearing to destination, degrees TrueBearingToDestination float64 // VelocityToDestinationKnots is velocity towards destination, knots VelocityToDestinationKnots float64 // ArrivalStatus is Arrival Status // * A = arrival circle entered // * V = not arrived ArrivalStatus string // FAA mode indicator (filled in NMEA 2.3 and later) FFAMode string } // newRMB constructor func newRMB(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeRMB) rmb := RMB{ BaseSentence: s, DataStatus: p.EnumString(0, "data status", DataStatusWarningClearRMB, DataStatusWarningSetRMB), CrossTrackErrorNauticalMiles: p.Float64(1, "cross track error"), DirectionToSteer: p.EnumString(2, "direction to steer", Left, Right), OriginWaypointID: p.String(3, "origin waypoint ID"), DestinationWaypointID: p.String(4, "destination waypoint ID"), DestinationLatitude: p.LatLong(5, 6, "latitude"), DestinationLongitude: p.LatLong(7, 8, "latitude"), RangeToDestinationNauticalMiles: p.Float64(9, "range to destination"), TrueBearingToDestination: p.Float64(10, "true bearing to destination"), VelocityToDestinationKnots: p.Float64(11, "velocity to destination"), ArrivalStatus: p.EnumString(12, "arrival status", WPStatusArrivalCircleEnteredA, WPStatusArrivalCircleEnteredV), FFAMode: "", } if len(p.Fields) > 13 { rmb.FFAMode = p.String(13, "FAA mode") // not enum because some devices have proprietary "non-nmea" values } return rmb, p.Err() } go-nmea-1.10.0/rmb_test.go000066400000000000000000000073021465514462400152770ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestRMB(t *testing.T) { var tests = []struct { name string raw string err string msg RMB }{ { name: "good sentence", raw: "$GPRMB,A,0.66,L,003,004,4917.24,N,12309.57,W,001.3,052.5,000.5,V*20", msg: RMB{ DataStatus: DataStatusWarningClearRMB, CrossTrackErrorNauticalMiles: 0.66, DirectionToSteer: Left, OriginWaypointID: "003", DestinationWaypointID: "004", DestinationLatitude: 49.28733333333333, DestinationLongitude: -123.1595, RangeToDestinationNauticalMiles: 1.3, TrueBearingToDestination: 52.5, VelocityToDestinationKnots: 0.5, ArrivalStatus: WPStatusArrivalCircleEnteredV, FFAMode: "", }, }, { name: "good sentence with FAAMode", raw: "$GPRMB,A,0.66,L,003,004,4917.24,N,12309.57,W,001.3,052.5,000.5,V,D*48", msg: RMB{ DataStatus: DataStatusWarningClearRMB, CrossTrackErrorNauticalMiles: 0.66, DirectionToSteer: Left, OriginWaypointID: "003", DestinationWaypointID: "004", DestinationLatitude: 49.28733333333333, DestinationLongitude: -123.1595, RangeToDestinationNauticalMiles: 1.3, TrueBearingToDestination: 52.5, VelocityToDestinationKnots: 0.5, ArrivalStatus: WPStatusArrivalCircleEnteredV, FFAMode: FAAModeDifferential, }, }, { name: "invalid nmea: DataStatus", raw: "$GPRMB,x,0.66,L,003,004,4917.24,N,12309.57,W,001.3,052.5,000.5,V,D*71", err: "nmea: GPRMB invalid data status: x", }, { name: "invalid nmea: CrossTrackErrorNauticalMiles", raw: "$GPRMB,A,x.66,L,003,004,4917.24,N,12309.57,W,001.3,052.5,000.5,V,D*00", err: "nmea: GPRMB invalid cross track error: x.66", }, { name: "invalid nmea: DirectionToSteer", raw: "$GPRMB,A,0.66,x,003,004,4917.24,N,12309.57,W,001.3,052.5,000.5,V,D*7C", err: "nmea: GPRMB invalid direction to steer: x", }, { name: "invalid nmea: DestinationLatitude", raw: "$GPRMB,A,0.66,L,003,004,4x17.24,N,12309.57,W,001.3,052.5,000.5,V,D*09", err: "nmea: GPRMB invalid latitude: cannot parse [4x17.24 N], unknown format", }, { name: "invalid nmea: DestinationLongitude", raw: "$GPRMB,A,0.66,L,003,004,4917.24,N,12x09.57,W,001.3,052.5,000.5,V,D*03", err: "nmea: GPRMB invalid latitude: cannot parse [12x09.57 W], unknown format", }, { name: "invalid nmea: RangeToDestinationNauticalMiles", raw: "$GPRMB,A,0.66,L,003,004,4917.24,N,12309.57,W,x01.3,052.5,000.5,V,D*00", err: "nmea: GPRMB invalid range to destination: x01.3", }, { name: "invalid nmea: TrueBearingToDestination", raw: "$GPRMB,A,0.66,L,003,004,4917.24,N,12309.57,W,001.3,052.x,000.5,V,D*05", err: "nmea: GPRMB invalid true bearing to destination: 052.x", }, { name: "invalid nmea: VelocityToDestinationKnots", raw: "$GPRMB,A,0.66,L,003,004,4917.24,N,12309.57,W,001.3,052.5,000.x,V,D*05", err: "nmea: GPRMB invalid velocity to destination: 000.x", }, { name: "invalid nmea: ArrivalStatus", raw: "$GPRMB,A,0.66,L,003,004,4917.24,N,12309.57,W,001.3,052.5,000.5,x,D*66", err: "nmea: GPRMB invalid arrival status: x", }, } for _, tt := range tests { 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) rmb := m.(RMB) rmb.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, rmb) } }) } } go-nmea-1.10.0/rmc.go000066400000000000000000000042601465514462400142410ustar00rootroot00000000000000package 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 // https://gpsd.gitlab.io/gpsd/NMEA.html#_rmc_recommended_minimum_navigation_information // // Format: $--RMC,hhmmss.ss,A,ddmm.mm,a,dddmm.mm,a,x.x,x.x,xxxx,x.x,a*hh // Format NMEA 2.3: $--RMC,hhmmss.ss,A,ddmm.mm,a,dddmm.mm,a,x.x,x.x,xxxx,x.x,a,m*hh // Format NMEA 4.1: $--RMC,hhmmss.ss,A,ddmm.mm,a,dddmm.mm,a,x.x,x.x,xxxx,x.x,a,m,s*hh // Example: $GNRMC,220516,A,5133.82,N,00042.24,W,173.8,231.8,130694,004.2,W*6E // $GNRMC,142754.0,A,4302.539570,N,07920.379823,W,0.0,,070617,0.0,E,A*21 // $GNRMC,102014.00,A,5550.6082,N,03732.2488,E,000.00000,092.9,300518,,,A,V*3B 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 FFAMode string // FAA mode indicator (filled in NMEA 2.3 and later) NavStatus string // Nav Status (NMEA 4.1 and later) } // newRMC constructor func newRMC(s BaseSentence) (Sentence, 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 } if len(p.Fields) > 11 { m.FFAMode = p.String(11, "FAA mode") // not enum because some devices have proprietary "non-nmea" values } if len(p.Fields) > 12 { m.NavStatus = p.EnumString( 12, "navigation status", NavStatusSafe, NavStatusCaution, NavStatusUnsafe, NavStatusNotValid, ) } return m, p.Err() } go-nmea-1.10.0/rmc_test.go000066400000000000000000000074541465514462400153100ustar00rootroot00000000000000package 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", Latitude: MustParseGPS("5133.82 N"), Longitude: MustParseGPS("00042.24 W"), Speed: 173.8, Course: 231.8, Date: Date{true, 13, 06, 94}, Variation: -4.2, FFAMode: "", NavStatus: "", }, }, { 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", Latitude: MustParseGPS("4302.539570 N"), Longitude: MustParseGPS("07920.379823 W"), Speed: 0, Course: 0, Date: Date{true, 7, 6, 17}, Variation: 0, FFAMode: FAAModeAutonomous, NavStatus: "", }, }, { 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", Latitude: MustParseGPS("5546.27711 N"), Longitude: MustParseGPS("03736.91144 E"), Speed: 0.061, Course: 0, Date: Date{true, 26, 3, 18}, Variation: 0, FFAMode: FAAModeAutonomous, NavStatus: "", }, }, { 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 D", 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", Latitude: MustParseGPS("5133.82 N"), Longitude: MustParseGPS("00042.24 W"), Speed: 173.8, Course: 231.8, Date: Date{true, 13, 6, 94}, Variation: -4.2, FFAMode: "", NavStatus: "", }, }, { name: "good sentence E", 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", Latitude: MustParseGPS("4302.539570 N"), Longitude: MustParseGPS("07920.379823 W"), Speed: 0, Course: 0, Date: Date{true, 7, 6, 17}, Variation: 0, FFAMode: FAAModeAutonomous, NavStatus: "", }, }, { name: "good sentence F with nav status", raw: "$GNRMC,102014.00,A,5550.6082,N,03732.2488,E,000.00000,092.9,300518,,,A,V*3B", msg: RMC{ Time: Time{Valid: true, Hour: 10, Minute: 20, Second: 14, Millisecond: 0}, Validity: "A", Latitude: 55.843469999999996, Longitude: 37.537479999999995, Speed: 0, Course: 92.9, Date: Date{Valid: true, DD: 30, MM: 5, YY: 18}, Variation: 0, FFAMode: FAAModeAutonomous, NavStatus: NavStatusNotValid, }, }, { 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", }, { name: "good sentence G with nav status", raw: "$YDRMC,124014.00,A,5520.2848,N,01321.5108,E,0.0,0.0,230623,4.4,E,A,C*5D", msg: RMC{ Time: Time{Valid: true, Hour: 12, Minute: 40, Second: 14, Millisecond: 0}, Validity: "A", Latitude: 55.338080000000005, Longitude: 13.358513333333333, Speed: 0, Course: 0, Date: Date{Valid: true, DD: 23, MM: 6, YY: 23}, Variation: 4.4, FFAMode: FAAModeAutonomous, NavStatus: NavStatusCaution, }, }, } 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.10.0/rot.go000066400000000000000000000014311465514462400142610ustar00rootroot00000000000000package nmea const ( // TypeROT type of ROT sentence for vessel rate of turn TypeROT = "ROT" // ValidROT data is valid ValidROT = "A" // InvalidROT data is invalid InvalidROT = "V" ) // ROT is sentence for rate of turn. // https://gpsd.gitlab.io/gpsd/NMEA.html#_rot_rate_of_turn // // Format: $HEROT,-xxx.x,A*hh // Example: $HEROT,-11.23,A*07 type ROT struct { BaseSentence RateOfTurn float64 // rate of turn Z in deg/min (- means bow turns to port) Valid bool // "A" data valid, "V" invalid data } func newROT(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeROT) return ROT{ BaseSentence: s, RateOfTurn: p.Float64(0, "rate of turn"), Valid: p.EnumString(1, "status valid", ValidROT, InvalidROT) == ValidROT, }, p.Err() } go-nmea-1.10.0/rot_test.go000066400000000000000000000015441465514462400153250ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestROT(t *testing.T) { var tests = []struct { name string raw string err string msg ROT }{ { name: "good sentence", raw: "$HEROT,-11.23,A*07", msg: ROT{ RateOfTurn: -11.23, Valid: true, }, }, { name: "invalid RateOfTurn", raw: "$HEROT,x,A*7D", err: "nmea: HEROT invalid rate of turn: x", }, { name: "invalid Valid", raw: "$HEROT,-11.23,X*1E", err: "nmea: HEROT invalid status valid: X", }, } for _, tt := range tests { 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) rot := m.(ROT) rot.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, rot) } }) } } go-nmea-1.10.0/rpm.go000066400000000000000000000023041465514462400142530ustar00rootroot00000000000000package nmea const ( // TypeRPM type of RPM sentence for Engine or Shaft revolutions and pitch TypeRPM = "RPM" // SourceEngineRPM is value for case when source is Engine SourceEngineRPM = "E" // SourceShaftRPM is value for case when source is Shaft SourceShaftRPM = "S" ) // RPM - Engine or Shaft revolutions and pitch // https://gpsd.gitlab.io/gpsd/NMEA.html#_rpm_revolutions // // Format: $--RPM,a,x,x.x,x.x,A*hh // Example: $RCRPM,S,0,74.6,30.0,A*56 type RPM struct { BaseSentence Source string // Source, S = Shaft, E = Engine EngineNumber int64 // Engine or shaft number SpeedRPM float64 // Speed, Revolutions per minute PitchPercent float64 // Propeller pitch, % of maximum, "-" means astern Status string // Status, A = Valid, V = Invalid } // newRPM constructor func newRPM(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeRPM) return RPM{ BaseSentence: s, Source: p.EnumString(0, "source", SourceEngineRPM, SourceShaftRPM), EngineNumber: p.Int64(1, "engine number"), SpeedRPM: p.Float64(2, "speed"), PitchPercent: p.Float64(3, "pitch"), Status: p.EnumString(4, "status", StatusValid, StatusInvalid), }, p.Err() } go-nmea-1.10.0/rpm_test.go000066400000000000000000000017211465514462400153140ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestRPM(t *testing.T) { var tests = []struct { name string raw string err string msg RPM }{ { name: "good sentence", raw: "$RCRPM,S,0,74.6,30.0,A*56", msg: RPM{ Source: SourceShaftRPM, EngineNumber: 0, SpeedRPM: 74.6, PitchPercent: 30, Status: StatusValid, }, }, { name: "invalid nmea: Source", raw: "$RCRPM,x,0,74.6,30.0,A*7D", err: "nmea: RCRPM invalid source: x", }, { name: "invalid nmea: Status", raw: "$RCRPM,S,0,74.6,30.0,x*6F", err: "nmea: RCRPM invalid status: x", }, } for _, tt := range tests { 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) rpm := m.(RPM) rpm.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, rpm) } }) } } go-nmea-1.10.0/rsa.go000066400000000000000000000021401465514462400142400ustar00rootroot00000000000000package nmea const ( // TypeRSA type of RSA sentence for Rudder Sensor Angle TypeRSA = "RSA" ) // RSA - Rudder Sensor Angle // https://gpsd.gitlab.io/gpsd/NMEA.html#_rsa_rudder_sensor_angle // // Format: $--RSA,x.x,A,x.x,A*hh // Example: $IIRSA,10.5,A,,V*4D type RSA struct { BaseSentence StarboardRudderAngle float64 // Starboard (or single) rudder sensor, "-" means Turn To Port StarboardRudderAngleStatus string // Status, A = valid, V = Invalid PortRudderAngle float64 // Port rudder sensor PortRudderAngleStatus string // Status, A = valid, V = Invalid } // newRSA constructor func newRSA(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeRSA) return RSA{ BaseSentence: s, StarboardRudderAngle: p.Float64(0, "starboard rudder angle"), StarboardRudderAngleStatus: p.EnumString(1, "starboard rudder angle status", StatusValid, StatusInvalid), PortRudderAngle: p.Float64(2, "port rudder angle"), PortRudderAngleStatus: p.EnumString(3, "port rudder angle status", StatusValid, StatusInvalid), }, p.Err() } go-nmea-1.10.0/rsa_test.go000066400000000000000000000024561465514462400153110ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestRSA(t *testing.T) { var tests = []struct { name string raw string err string msg RSA }{ { name: "good sentence 1", raw: "$IIRSA,10.5,A,0.4,A*70", msg: RSA{ StarboardRudderAngle: 10.5, StarboardRudderAngleStatus: StatusValid, PortRudderAngle: 0.4, PortRudderAngleStatus: StatusValid, }, }, { name: "good sentence 2", raw: "$IIRSA,10.5,A,,V*4D", msg: RSA{ StarboardRudderAngle: 10.5, StarboardRudderAngleStatus: StatusValid, PortRudderAngle: 0, PortRudderAngleStatus: StatusInvalid, }, }, { name: "invalid nmea: StarboardRudderAngleStatus", raw: "$IIRSA,10.5,x,,V*74", err: "nmea: IIRSA invalid starboard rudder angle status: x", }, { name: "invalid nmea: PortRudderAngleStatus", raw: "$IIRSA,10.5,A,,x*63", err: "nmea: IIRSA invalid port rudder angle status: x", }, } for _, tt := range tests { 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) rsa := m.(RSA) rsa.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, rsa) } }) } } go-nmea-1.10.0/rsd.go000066400000000000000000000053471465514462400142570ustar00rootroot00000000000000package nmea const ( // TypeRSD type of RSD sentence for RADAR System Data TypeRSD = "RSD" // RSDDisplayRotationCourseUp is when display rotation is course up RSDDisplayRotationCourseUp = "C" // RSDDisplayRotationHeadingUp is when display rotation is ship heading up RSDDisplayRotationHeadingUp = "H" // RSDDisplayRotationNorthUp is when display rotation is (true) north up RSDDisplayRotationNorthUp = "N" ) // RSD - RADAR System Data // https://gpsd.gitlab.io/gpsd/NMEA.html#_rsd_radar_system_data // https://github.com/nohal/OpenCPN/wiki/ARPA-targets-tracking-implementation#rsd---radar-system-data // // Format: $--RSD,x.x,x.x,x.x,x.x,x.x,x.x,x.x,x.x,x.x,x.x,x.x,a,a*hh // Example: $RARSD,0.00,,2.50,005.0,0.00,,4.50,355.0,,,3.0,N,H*51 // Example: $RARSD,,,,,,,,,0.808,326.9,0.750,N,N*58 // Example: $RARSD,0.00,,0.40,,,,,,,,3.0,N,N*53 type RSD struct { BaseSentence Origin1Range float64 // Origin 1 range Origin1Bearing float64 // Origin 1 bearing (degrees from 0°) VariableRangeMarker1 float64 // Variable Range Marker 1 BearingLine1 float64 // Bearing Line 1 Origin2Range float64 // Origin 2 range Origin2Bearing float64 // Origin 2 bearing (degrees from 0°) VariableRangeMarker2 float64 // Variable Range Marker 2 BearingLine2 float64 // Bearing Line 2 CursorRangeFromOwnShip float64 // Cursor Range From Own Ship CursorBearingDegrees float64 // Cursor Bearing (degrees clockwise from 0°) RangeScale float64 // Range scale RangeUnit string // Range units (K = kilometers, N = nautical miles, S = statute miles) DisplayRotation string // Display rotation (C = course up, H = heading up, N - North up) } // newRSD constructor func newRSD(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeRSD) return RSD{ BaseSentence: s, Origin1Range: p.Float64(0, "origin 1 range"), Origin1Bearing: p.Float64(1, "origin 1 bearing"), VariableRangeMarker1: p.Float64(2, "variable range marker 1"), BearingLine1: p.Float64(3, "bearing line 1"), Origin2Range: p.Float64(4, "origin 2 range"), Origin2Bearing: p.Float64(5, "origin 2 bearing"), VariableRangeMarker2: p.Float64(6, "variable range marker 2"), BearingLine2: p.Float64(7, "bearing line 2"), CursorRangeFromOwnShip: p.Float64(8, "cursor range from own ship"), CursorBearingDegrees: p.Float64(9, "cursor bearing"), RangeScale: p.Float64(10, "range scale"), RangeUnit: p.EnumString(11, "range units", DistanceUnitKilometre, DistanceUnitNauticalMile, DistanceUnitStatuteMile), DisplayRotation: p.EnumString(12, "display rotation", RSDDisplayRotationCourseUp, RSDDisplayRotationHeadingUp, RSDDisplayRotationNorthUp), }, p.Err() } go-nmea-1.10.0/rsd_test.go000066400000000000000000000073661465514462400153210ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestRSD(t *testing.T) { var tests = []struct { name string raw string err string msg RSD }{ { name: "good sentence", raw: "$RARSD,0.00,,2.50,005.0,0.00,,4.50,355.0,,,3.0,N,H*51", msg: RSD{ BaseSentence: BaseSentence{}, Origin1Range: 0, Origin1Bearing: 0, VariableRangeMarker1: 2.5, BearingLine1: 5, Origin2Range: 0, Origin2Bearing: 0, VariableRangeMarker2: 4.5, BearingLine2: 355, CursorRangeFromOwnShip: 0, CursorBearingDegrees: 0, RangeScale: 3, RangeUnit: "N", DisplayRotation: "H", }, }, { name: "good sentence 2", raw: "$RARSD,,,,,,,,,0.808,326.9,0.750,N,N*58", msg: RSD{ BaseSentence: BaseSentence{}, Origin1Range: 0, Origin1Bearing: 0, VariableRangeMarker1: 0, BearingLine1: 0, Origin2Range: 0, Origin2Bearing: 0, VariableRangeMarker2: 0, BearingLine2: 0, CursorRangeFromOwnShip: 0.808, CursorBearingDegrees: 326.9, RangeScale: 0.75, RangeUnit: "N", DisplayRotation: "N", }, }, { name: "invalid nmea: Origin1Range", raw: "$RARSD,x,,2.50,005.0,0.00,,4.50,355.0,,,3.0,N,H*37", err: "nmea: RARSD invalid origin 1 range: x", }, { name: "invalid nmea: Origin1Bearing", raw: "$RARSD,,x,2.50,005.0,0.00,,4.50,355.0,,,3.0,N,H*37", err: "nmea: RARSD invalid origin 1 bearing: x", }, { name: "invalid nmea: VariableRangeMarker1", raw: "$RARSD,,,x2.50,005.0,0.00,,4.50,355.0,,,3.0,N,H*37", err: "nmea: RARSD invalid variable range marker 1: x2.50", }, { name: "invalid nmea: BearingLine1", raw: "$RARSD,,,2.50,x005.0,0.00,,4.50,355.0,,,3.0,N,H*37", err: "nmea: RARSD invalid bearing line 1: x005.0", }, { name: "invalid nmea: Origin2Range", raw: "$RARSD,,,2.50,005.0,x0.00,,4.50,355.0,,,3.0,N,H*37", err: "nmea: RARSD invalid origin 2 range: x0.00", }, { name: "invalid nmea: Origin2Bearing", raw: "$RARSD,,,2.50,005.0,0.00,x,4.50,355.0,,,3.0,N,H*37", err: "nmea: RARSD invalid origin 2 bearing: x", }, { name: "invalid nmea: VariableRangeMarker2", raw: "$RARSD,,,2.50,005.0,0.00,,x4.50,355.0,,,3.0,N,H*37", err: "nmea: RARSD invalid variable range marker 2: x4.50", }, { name: "invalid nmea: BearingLine2", raw: "$RARSD,,,2.50,005.0,0.00,,4.50,x355.0,,,3.0,N,H*37", err: "nmea: RARSD invalid bearing line 2: x355.0", }, { name: "invalid nmea: CursorRangeFromOwnShip", raw: "$RARSD,,,2.50,005.0,0.00,,4.50,355.0,x,,3.0,N,H*37", err: "nmea: RARSD invalid cursor range from own ship: x", }, { name: "invalid nmea: CursorBearingDegrees", raw: "$RARSD,,,2.50,005.0,0.00,,4.50,355.0,,x,3.0,N,H*37", err: "nmea: RARSD invalid cursor bearing: x", }, { name: "invalid nmea: RangeUnit", raw: "$RARSD,,,2.50,005.0,0.00,,4.50,355.0,,,3.0,X,H*59", err: "nmea: RARSD invalid range units: X", }, { name: "invalid nmea: RangeUnit", raw: "$RARSD,,,2.50,005.0,0.00,,4.50,355.0,,,3.0,X,H*59", err: "nmea: RARSD invalid range units: X", }, { name: "invalid nmea: DisplayRotation", raw: "$RARSD,,,2.50,005.0,0.00,,4.50,355.0,,,3.0,N,X*5f", err: "nmea: RARSD invalid display rotation: X", }, } for _, tt := range tests { 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) mm := m.(RSD) mm.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, mm) } }) } } go-nmea-1.10.0/rte.go000066400000000000000000000025261465514462400142550ustar00rootroot00000000000000package 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 // http://aprs.gids.nl/nmea/#rte // https://gpsd.gitlab.io/gpsd/NMEA.html#_rte_routes // // Format: $--RTE,x.x,x.x,a,c--c,c--c, ..... c--c*hh // Example: $GPRTE,2,1,c,0,PBRCPK,PBRTO,PTELGR,PPLAND,PYAMBU,PPFAIR,PWARRN,PMORTL,PLISMR*73 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) (Sentence, 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.10.0/rte_test.go000066400000000000000000000026461465514462400153170ustar00rootroot00000000000000package 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.10.0/sentence.go000066400000000000000000000302711465514462400152650ustar00rootroot00000000000000package nmea import ( "errors" "fmt" "strings" "sync" ) const ( // TagBlockSep is the separator (slash `\`) that indicates start and end of tag block TagBlockSep = '\\' // SentenceStart is the token to indicate the start of a sentence. SentenceStart = "$" // SentenceStartEncapsulated is the token to indicate the start of encapsulated data. SentenceStartEncapsulated = "!" // ProprietarySentencePrefix is the token to indicate the start of parametric sentences. ProprietarySentencePrefix = 'P' // QuerySentencePostfix is the suffix token to indicate the Query sentences. QuerySentencePostfix = 'Q' // FieldSep is the token to delimit fields of a sentence. FieldSep = "," // ChecksumSep is the token to delimit the checksum of a sentence. ChecksumSep = "*" ) // ParserFunc callback used to parse specific sentence variants type ParserFunc func(s BaseSentence) (Sentence, error) // NotSupportedError is returned when parsed sentence is not supported type NotSupportedError struct { Prefix string } // Error returns error message func (p *NotSupportedError) Error() string { return fmt.Sprintf("nmea: sentence prefix '%s' not supported", p.Prefix) } // 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 (raw) 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 } // SentenceParser is configurable parser instance to parse raw input into NMEA0183 Sentence // // SentenceParser fields/methods are not co-routine safe! type SentenceParser struct { // CustomParsers allows registering additional parsers CustomParsers map[string]ParserFunc // ParsePrefix takes in the sentence first field (NMEA0183 address) and splits it into a talker id and sentence type ParsePrefix func(prefix string) (talkerID string, sentence string, err error) // CheckCRC allows custom handling of checksum checking based on parsed sentence CheckCRC func(sentence BaseSentence, rawFields string) error // OnTagBlock is callback to handle all parsed tag blocks even for lines containing only a tag block and // allows to track multiline tag group separate lines. Logic how to combine/assemble multiline tag group // should be implemented outside SentenceParser // OnTagBlock is called before actual sentence part is parsed. When callback returns an error sentence parsing will // not be done and Parse returns early with the returned error. // // Example of group of 3: // \g:1-3-1234,s:r3669961,c:1120959341*hh\ // \g:2-3-1234*hh\!ABVDM,1,1,1,B,.....,0*hh // \g:3-3-1234*hh\$ABVSI,r3669961,1,013536.96326433,1386,-98,,*hh OnTagBlock func(tagBlock TagBlock) error // OnBaseSentence is a callback for accessing/modifying the base sentence // before further parsing is done. OnBaseSentence func(sentence *BaseSentence) error } func (p *SentenceParser) parseBaseSentence(raw string) (BaseSentence, error) { raw = strings.TrimSpace(raw) if raw == "" { return BaseSentence{}, errors.New("nmea: can not parse empty input") } tagBlock, tagBlockLen, err := ParseTagBlock(raw) if err != nil { return BaseSentence{}, err } if tagBlockLen > 0 && p.OnTagBlock != nil { if err := p.OnTagBlock(tagBlock); err != nil { return BaseSentence{}, err } } raw = raw[tagBlockLen:] startIndex := strings.IndexAny(raw, SentenceStart+SentenceStartEncapsulated) if startIndex != 0 { return BaseSentence{}, errors.New("nmea: sentence does not start with a '$' or '!'") } checksumSepIndex := strings.Index(raw, ChecksumSep) rawFields := raw[startIndex+1:] checksumRaw := "" if checksumSepIndex != -1 { rawFields = raw[startIndex+1 : checksumSepIndex] checksumRaw = strings.ToUpper(raw[checksumSepIndex+1:]) } // TODO: fields can contain encoded chars that need to be unescaped. `^` 0x5e is escape character for HEX representation of ISO/IEC 8859-1 (ASCII) characters. // TODO: `^` itself is escaped as `^5e` and `,` is escaped as `^2c` etc. All reserved characters should be escaped/unescaped (See wikipedia https://en.wikipedia.org/wiki/NMEA_0183#Message_structure) fields := strings.Split(rawFields, FieldSep) var ( talkerID string typ string ) if p.ParsePrefix == nil { talkerID, typ, err = ParsePrefix(fields[0]) } else { talkerID, typ, err = p.ParsePrefix(fields[0]) } if err != nil { return BaseSentence{}, err } sentence := BaseSentence{ Talker: talkerID, Type: typ, Fields: fields[1:], Checksum: checksumRaw, Raw: raw, TagBlock: tagBlock, } if p.CheckCRC == nil { err = CheckCRC(sentence, rawFields) } else { err = p.CheckCRC(sentence, rawFields) } if err != nil { return BaseSentence{}, err } return sentence, nil } // CheckCRC is default implementation for checking sentence Checksum func CheckCRC(sentence BaseSentence, rawFields string) error { if sentence.Checksum == "" { return fmt.Errorf("nmea: sentence does not contain checksum separator") } if checksum := Checksum(rawFields); checksum != sentence.Checksum { return fmt.Errorf("nmea: sentence checksum mismatch [%s != %s]", checksum, sentence.Checksum) } return nil } // ParsePrefix is default implementation for prefix parsing. It takes in the sentence first field and splits it into // a talker id and sentence type. func ParsePrefix(prefix string) (string, string, error) { if prefix == "" { return "", "", errors.New("nmea: sentence prefix is empty") } // proprietary sentences start with `P` + sentence type. By NMEA0183 spec they should be 5 character long, // In this case we allow sentence type to be longer as there are plenty of examples with longer sentence // types (PSKPDPT, PMTK001 etc) if prefix[0] == ProprietarySentencePrefix { return string(ProprietarySentencePrefix), prefix[1:], nil } // valid prefix, by the NMEA0183 standard, is 5 character long: // a) talkerID (2) + sentence identifier (3) // b) talkerID (2) + destinationID (2) + 'Q' (1) if len(prefix) == 5 { // query sentence is a special type of sentence in NMEA standard that is used for a listener to request a // particular sentence from a talker. // Query prefix consist of: XXYYQ, XX - requester talker ID, YY - requestee/destination talker ID, `Q` - query type // Destination talker ID is handled/parsed by newQuery function if prefix[4] == QuerySentencePostfix { return prefix[:2], string(QuerySentencePostfix), nil } return prefix[:2], prefix[2:], nil } // this is catch all for other invalid NMEA0183 implementations. When prefix is shorter or longer than 5 characters // we use everything as sentence type. This way custom parser could be created that matches this off-spec prefix. return "", prefix, nil } // Checksum xor all the bytes in a string and 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) } var defaultSentenceParserMu = new(sync.Mutex) // defaultSentenceParser exists for backwards compatibility reasons to allow global Parse/RegisterParser/MustRegisterParser // to work as they did before SentenceParser was added. var defaultSentenceParser = SentenceParser{ CustomParsers: map[string]ParserFunc{ TypeMTK: newMTK, // for backwards compatibility support MTK. PMTK001 is correct an supported when using SentenceParser instance }, } // 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 { defaultSentenceParserMu.Lock() defer defaultSentenceParserMu.Unlock() if _, ok := defaultSentenceParser.CustomParsers[sentenceType]; ok { return fmt.Errorf("nmea: parser for sentence type '%q' already exists", sentenceType) } defaultSentenceParser.CustomParsers[sentenceType] = parser return nil } // Parse parses the given string into the correct sentence type. func Parse(raw string) (Sentence, error) { defaultSentenceParserMu.Lock() defer defaultSentenceParserMu.Unlock() return defaultSentenceParser.Parse(raw) } // Parse parses the given string into the correct sentence type. // This method is not co-routine safe! func (p *SentenceParser) Parse(raw string) (Sentence, error) { s, err := p.parseBaseSentence(raw) if err != nil { return nil, err } if p.OnBaseSentence != nil { if err := p.OnBaseSentence(&s); err != nil { return nil, err } } // Custom parser allow overriding of existing parsers if parser, ok := p.CustomParsers[s.Type]; ok { return parser(s) } if s.Raw[0] == SentenceStart[0] { switch s.Type { case TypeRMC: return newRMC(s) case TypeAAM: return newAAM(s) case TypeACK: return newACK(s) case TypeACN: return newACN(s) case TypeALA: return newALA(s) case TypeALC: return newALC(s) case TypeALF: return newALF(s) case TypeALR: return newALR(s) case TypeAPB: return newAPB(s) case TypeARC: return newARC(s) case TypeBEC: return newBEC(s) case TypeBOD: return newBOD(s) case TypeBWC: return newBWC(s) case TypeBWR: return newBWR(s) case TypeBWW: return newBWW(s) case TypeDOR: return newDOR(s) case TypeDSC: return newDSC(s) case TypeDSE: return newDSE(s) case TypeDTM: return newDTM(s) case TypeEVE: return newEVE(s) case TypeFIR: return newFIR(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 TypePGN: return newPGN(s) case TypePCDIN: return newPCDIN(s) case TypePGRME: return newPGRME(s) case TypePHTRO: return newPHTRO(s) case TypePMTK001: return newPMTK001(s) case TypePRDID: return newPRDID(s) case TypePSKPDPT: return newPSKPDPT(s) case TypePSONCMS: return newPSONCMS(s) case TypeQuery: return newQuery(s) case TypeGSV: return newGSV(s) case TypeHBT: return newHBT(s) case TypeHDG: return newHDG(s) case TypeHDT: return newHDT(s) case TypeHDM: return newHDM(s) case TypeHSC: return newHSC(s) case TypeGNS: return newGNS(s) case TypeTHS: return newTHS(s) case TypeTLB: return newTLB(s) case TypeTLL: return newTLL(s) case TypeTTM: return newTTM(s) case TypeTXT: return newTXT(s) case TypeWPL: return newWPL(s) case TypeRMB: return newRMB(s) case TypeRPM: return newRPM(s) case TypeRSA: return newRSA(s) case TypeRSD: return newRSD(s) case TypeRTE: return newRTE(s) case TypeROT: return newROT(s) case TypeVBW: return newVBW(s) case TypeVDR: return newVDR(s) case TypeVHW: return newVHW(s) case TypeVSD: return newVSD(s) case TypeVPW: return newVPW(s) case TypeVLW: return newVLW(s) case TypeVWR: return newVWR(s) case TypeVWT: return newVWT(s) case TypeDPT: return newDPT(s) case TypeDBT: return newDBT(s) case TypeDBK: return newDBK(s) case TypeDBS: return newDBS(s) case TypeMDA: return newMDA(s) case TypeMTA: return newMTA(s) case TypeMTW: return newMTW(s) case TypeMWD: return newMWD(s) case TypeMWV: return newMWV(s) case TypeOSD: return newOSD(s) case TypeXDR: return newXDR(s) case TypeXTE: return newXTE(s) } } if s.Raw[0] == SentenceStartEncapsulated[0] { switch s.Type { case TypeABM: return newABM(s) case TypeBBM: return newBBM(s) case TypeTTD: return newTTD(s) case TypeVDM, TypeVDO: return newVDMVDO(s) } } return nil, &NotSupportedError{Prefix: s.Prefix()} } go-nmea-1.10.0/sentence_customparser_test.go000066400000000000000000000064611465514462400211370ustar00rootroot00000000000000package 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.10.0/sentence_test.go000066400000000000000000000421321465514462400163230ustar00rootroot00000000000000package nmea import ( "encoding/hex" "errors" "strings" "testing" "github.com/stretchr/testify/assert" ) func TestSentences(t *testing.T) { 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: "valid IEC 61162-450 message with tag block", // IEC 61162-450 (NMEA0183 over multicast UDP) has prefix `UdPbC` + 0x0 (null character) raw: func() string { // `UdPbC` + 0x0 + `\s:SI1001*53\$IIHDG,,,,,*67` str, _ := hex.DecodeString("5564506243005c733a5349313030312a35335c2449494844472c2c2c2c2c2a3637") return string(str) }(), datatype: "HDG", talkerid: "II", prefix: "IIHDG", sent: BaseSentence{ Talker: "II", Type: "HDG", Fields: []string{"", "", "", "", ""}, Checksum: "67", Raw: "$IIHDG,,,,,*67", TagBlock: TagBlock{ Source: "SI1001", }, }, }, { 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: "too short prefix", raw: "$,1,2,3,x,y,z*4B", err: "nmea: sentence prefix is empty", }, { 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 tag block is missing '\' at the end`, }, { 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 tag block is missing '\\' at the end", }, { name: "invalid TAG Block contents", raw: "\\\\!AIVDM,1,1,,A,13M@ah0025QdPDTCOl`K6`nV00Sv,0*52", err: "nmea: tagblock does not contain checksum separator", }, { name: "invalid TAG Block contents", raw: "\\\\!AIVDM,1,1,,A,13M@ah0025QdPDTCOl`K6`nV00Sv,0*52", err: "nmea: tagblock does not contain checksum separator", }, } for _, tt := range sentencetests { t.Run(tt.name, func(t *testing.T) { sent, err := defaultSentenceParser.parseBaseSentence(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()) } }) } } func TestParsePrefix(t *testing.T) { var prefixtests = []struct { name string prefix string talker string typ string expectedError string }{ { name: "normal prefix", prefix: "GPRMC", talker: "GP", typ: "RMC", }, { name: "can not be empty", prefix: "", talker: "", typ: "", expectedError: `nmea: sentence prefix is empty`, }, { name: "invalid NMEA0183 spec prefix, use everything as type", prefix: "GP", talker: "", typ: "GP", }, { name: "too long to be valid NMEA0183 prefix, use everything as type", prefix: "GPXXXX", talker: "", typ: "GPXXXX", }, { name: "proprietary talker", prefix: "PGRME", talker: "P", typ: "GRME", }, { name: "short proprietary talker", prefix: "PX", talker: "P", typ: "X", }, { name: "long proprietary type", prefix: "PXXXXX", talker: "P", typ: "XXXXX", }, { name: "query", prefix: "CCGPQ", talker: "CC", typ: "Q", }, } for _, tt := range prefixtests { t.Run(tt.name, func(t *testing.T) { talker, typ, err := ParsePrefix(tt.prefix) assert.Equal(t, tt.talker, talker) assert.Equal(t, tt.typ, typ) if tt.expectedError != "" { assert.EqualError(t, err, tt.expectedError) } else { assert.NoError(t, err) } }) } } func TestSentenceParser_ParsePrefix(t *testing.T) { var testCases = []struct { name string given func(prefix string) (talkerID string, sentence string, err error) when string expected interface{} expectedError string }{ { name: "ok, use default implementation as custom", given: ParsePrefix, when: `$VRACK,001*50`, expected: ACK{ BaseSentence: BaseSentence{ Talker: "VR", Type: "ACK", Fields: []string{"001"}, Checksum: "50", Raw: "$VRACK,001*50", }, AlertIdentifier: 1, }, }, { name: "ok, use custom implementation ", given: func(prefix string) (talkerID string, sentence string, err error) { return "VR", "ACK", nil }, when: `$VRACK,001*50`, expected: ACK{ BaseSentence: BaseSentence{ Talker: "VR", Type: "ACK", Fields: []string{"001"}, Checksum: "50", Raw: "$VRACK,001*50", }, AlertIdentifier: 1, }, }, { name: "nok, custom prefix parsing error", given: func(prefix string) (talkerID string, sentence string, err error) { return "", "", errors.New("failed_to_parse_prefix") }, when: `$VRACK,001*50`, expected: nil, expectedError: "failed_to_parse_prefix", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { p := SentenceParser{ParsePrefix: tc.given} result, err := p.Parse(tc.when) assert.Equal(t, tc.expected, result) if tc.expectedError != "" { assert.EqualError(t, err, tc.expectedError) } else { assert.NoError(t, err) } }) } } var parsetests = []struct { name string raw string err error msg interface{} }{ { name: "bad sentence", raw: "SDFSD,2340dfmswd", err: errors.New("nmea: sentence does not start with a '$' or '!'"), }, { name: "bad sentence type", raw: "$INVALID,123,123,*7D", err: &NotSupportedError{Prefix: "INVALID"}, }, { name: "bad encapsulated sentence type", raw: "!INVALID,1,2,*7E", err: &NotSupportedError{Prefix: "INVALID"}, }, } 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 != nil { assert.Equal(t, err, tt.err) } else { assert.NoError(t, err) assert.Equal(t, tt.msg, m) } }) } } func TestSentenceParser_Parse(t *testing.T) { var testCases = []struct { name string givenParser *SentenceParser whenInput string expected Sentence expectedError string }{ { name: "ok, parse parametric sentence", whenInput: "$HEROT,-11.23,A*07", expected: ROT{ BaseSentence: BaseSentence{ Talker: "HE", Type: "ROT", Fields: []string{"-11.23", "A"}, Checksum: "07", Raw: "$HEROT,-11.23,A*07", TagBlock: TagBlock{}, }, RateOfTurn: -11.23, Valid: true, }, }, { name: "ok, parse encapsulated sentence", whenInput: "!AIVDM,1,1,,A,13aGt0PP0jPN@9fMPKVDJgwfR>`<,0*55", expected: VDMVDO{ BaseSentence: BaseSentence{ Talker: "AI", Type: "VDM", Fields: []string{"1", "1", "", "A", "13aGt0PP0jPN@9fMPKVDJgwfR>`<", "0"}, Checksum: "55", Raw: "!AIVDM,1,1,,A,13aGt0PP0jPN@9fMPKVDJgwfR>`<,0*55", TagBlock: TagBlock{}, }, NumFragments: 1, FragmentNumber: 1, MessageID: 0, Channel: "A", Payload: []byte{ 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, // 10 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, // 50 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, // 100 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, // 150 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, }, }, }, { name: "ok, parse query sentence", whenInput: "$CCGPQ,GGA*2B", expected: Query{ BaseSentence: BaseSentence{ Talker: "CC", Type: "Q", Fields: []string{"GGA"}, Checksum: "2B", Raw: "$CCGPQ,GGA*2B", TagBlock: TagBlock{Time: 0, RelativeTime: 0, Destination: "", Grouping: "", LineCount: 0, Source: "", Text: ""}, }, DestinationTalkerID: "GP", RequestedSentence: "GGA", }, }, { name: "ok, parse custom sentence", givenParser: &SentenceParser{ CustomParsers: map[string]ParserFunc{ "YYY": func(s BaseSentence) (Sentence, error) { p := NewParser(s) return TestZZZ{ BaseSentence: s, NumberValue: int(p.Int64(0, "number")), StringValue: p.String(1, "str"), }, p.Err() }, }, }, whenInput: "$AAYYY,20,one,*13", expected: TestZZZ{ BaseSentence: BaseSentence{ Talker: "AA", Type: "YYY", Fields: []string{"20", "one", ""}, Checksum: "13", Raw: "$AAYYY,20,one,*13", }, NumberValue: 20, StringValue: "one", }, }, { name: "nok, unsupported custom type, too short prefix to be valid NMEA0183 prefix", whenInput: "$XXXX,20,one,*4A", expected: nil, expectedError: "nmea: sentence prefix 'XXXX' not supported", }, { name: "nok, empty sentence", whenInput: "", expected: nil, expectedError: "nmea: can not parse empty input", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { parser := tc.givenParser if parser == nil { parser = &SentenceParser{} } result, err := parser.Parse(tc.whenInput) assert.Equal(t, tc.expected, result) if tc.expectedError != "" { assert.EqualError(t, err, tc.expectedError) } else { assert.NoError(t, err) } }) } } func TestSentenceParser_OnTagBlock(t *testing.T) { var testCases = []struct { name string given string whenReturnError error expectCalled bool expectTagBlock TagBlock expectError string }{ { name: "ok, called", given: `\g:1-3-1234,s:r3669961,c:1120959341*0c\`, expectCalled: true, expectTagBlock: TagBlock{Time: 1120959341, RelativeTime: 0, Destination: "", Grouping: "1-3-1234", LineCount: 0, Source: "r3669961", Text: ""}, expectError: `nmea: sentence does not start with a '$' or '!'`, }, { name: "ok, return custom error", given: `\g:1-3-1234,s:r3669961,c:1120959341*0c\`, whenReturnError: errors.New("custom_error"), expectCalled: true, expectTagBlock: TagBlock{Time: 1120959341, RelativeTime: 0, Destination: "", Grouping: "1-3-1234", LineCount: 0, Source: "r3669961", Text: ""}, expectError: `custom_error`, }, { name: "nok, not called, invalid CRC stops", given: `\g:1-3-1234,s:r3669961,c:1120959341*xx\`, expectCalled: false, expectError: "nmea: tagblock checksum mismatch [0C != XX]", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { tbCalled := false var actualBlock TagBlock p := SentenceParser{ OnTagBlock: func(tb TagBlock) error { tbCalled = true actualBlock = tb return tc.whenReturnError }, } result, err := p.Parse(tc.given) assert.Equal(t, tc.expectCalled, tbCalled) assert.Equal(t, tc.expectTagBlock, actualBlock) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } assert.Nil(t, result) }) } } func TestSentenceParser_CheckCRC(t *testing.T) { var testCases = []struct { name string givenCheckCRC func(t *testing.T, sentence BaseSentence, fieldsRaw string) error whenInput string expectError string expectCalled bool }{ { name: "ok, custom CRC check allows invalid CRC", whenInput: `$HEROT,-11.23,A*FF`, givenCheckCRC: func(t *testing.T, sentence BaseSentence, rawFields string) error { assert.Equal(t, "HEROT,-11.23,A", rawFields) assert.Equal(t, "FF", sentence.Checksum) assert.Equal(t, "ROT", sentence.Type) return nil }, expectCalled: true, }, { name: "ok, custom CRC check allows no CRC", whenInput: `$HEROT,-11.23,A`, givenCheckCRC: func(t *testing.T, sentence BaseSentence, rawFields string) error { assert.Equal(t, "HEROT,-11.23,A", rawFields) assert.Equal(t, "", sentence.Checksum) assert.Equal(t, "ROT", sentence.Type) return nil }, expectCalled: true, }, { name: "nok, custom CRC check returns an error", whenInput: `$HEROT,-11.23,A`, givenCheckCRC: func(t *testing.T, sentence BaseSentence, rawFields string) error { assert.Equal(t, "HEROT,-11.23,A", rawFields) assert.Equal(t, "", sentence.Checksum) assert.Equal(t, "ROT", sentence.Type) return errors.New("invalid CRC") }, expectCalled: true, expectError: "invalid CRC", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { called := false p := SentenceParser{ CheckCRC: func(sentence BaseSentence, rawFields string) error { called = true return tc.givenCheckCRC(t, sentence, rawFields) }, } _, err := p.Parse(tc.whenInput) assert.Equal(t, tc.expectCalled, called) if tc.expectError != "" { assert.EqualError(t, err, tc.expectError) } else { assert.NoError(t, err) } }) } } func TestSentenceParser_OnBaseSentence(t *testing.T) { testErr := errors.New("this is a test") tests := []struct { name string fn func(*BaseSentence) error raw string sentence Sentence err error }{ { name: "can modify prefix", fn: func(s *BaseSentence) error { if s.Type == "VDM" && strings.HasPrefix(s.Raw, "$") { s.Raw = "!" + s.Raw[1:] } return nil }, raw: "\\s:somewhere,c:1720289719*4D\\$AIVDM,1,1,,A,,0*26", sentence: VDMVDO{ BaseSentence: BaseSentence{ Talker: "AI", Type: "VDM", Fields: []string{"1", "1", "", "A", "", "0"}, Checksum: "26", Raw: "!AIVDM,1,1,,A,,0*26", TagBlock: TagBlock{ Time: 1720289719, RelativeTime: 0, Destination: "", Grouping: "", LineCount: 0, Source: "somewhere", Text: "", }, }, NumFragments: 1, FragmentNumber: 1, MessageID: 0, Channel: "A", Payload: []uint8{}, }, }, { name: "should return error", fn: func(_ *BaseSentence) error { return testErr }, raw: "$GNRMC,142754.0,A,4302.539570,N,07920.379823,W,0.0,,070617,0.0,E,A*21", err: testErr, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := SentenceParser{OnBaseSentence: tt.fn} s, err := p.Parse(tt.raw) if tt.err == nil { assert.NoError(t, err) assert.Equal(t, tt.sentence, s) } else { assert.Equal(t, tt.err, err) } }) } } go-nmea-1.10.0/tagblock.go000066400000000000000000000061711465514462400152510ustar00rootroot00000000000000package 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 } // ParseTagBlock parses tag blocks from a sentence string. // The second return value is the length of the tag block prefix. // See: https://gpsd.gitlab.io/gpsd/AIVDM.html#_nmea_tag_blocks func ParseTagBlock(raw string) (TagBlock, int, error) { startOfTagBlock := strings.IndexByte(raw, TagBlockSep) if startOfTagBlock == -1 { return TagBlock{}, 0, nil } // tag block is always at the start of line (unless IEC 61162-450). Starts with `\` and ends with `\` and has valid sentence // following or // // Note: tag block group can span multiple lines but we only parse ones that have sentence endOfTagBlock := strings.LastIndexByte(raw, TagBlockSep) if endOfTagBlock <= startOfTagBlock { return TagBlock{}, 0, fmt.Errorf("nmea: sentence tag block is missing '\\' at the end") } tags := raw[startOfTagBlock+1 : endOfTagBlock] sumSepIndex := strings.Index(tags, ChecksumSep) if sumSepIndex == -1 { return TagBlock{}, 0, 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{}, 0, 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{}, 0, 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{}, 0, 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{}, 0, err } case "r": // Relative time tagBlock.RelativeTime, err = parseInt64(value) if err != nil { return TagBlock{}, 0, err } case "s": // Source ID tagBlock.Source = value case "t": // Text string tagBlock.Text = value } } return tagBlock, endOfTagBlock + 1, nil } 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 } go-nmea-1.10.0/tagblock_test.go000066400000000000000000000053061465514462400163070ustar00rootroot00000000000000package nmea import ( "testing" "github.com/stretchr/testify/assert" ) var tagblocktests = []struct { name string raw string err string block TagBlock len int }{ { name: "Test NMEA tag block", raw: "\\s:Satelite_1,c:1553390539*62\\!AIVDM,1,2,3", block: TagBlock{ Time: 1553390539, Source: "Satelite_1", }, len: 30, }, { name: "Test NMEA tag block with head", raw: "\\s:satelite,c:1564827317*25\\!AIVDM,1,2,3", block: TagBlock{ Time: 1564827317, Source: "satelite", }, len: 28, }, { name: "Test unknown tag", raw: "\\x:NorSat_1,c:1564827317*42\\!AIVDM,1,2,3", block: TagBlock{ Time: 1564827317, Source: "", }, len: 28, }, { name: "Test unix timestamp", raw: "\\x:NorSat_1,c:1564827317*42\\!AIVDM,1,2,3", block: TagBlock{ Time: 1564827317, Source: "", }, len: 28, }, { name: "Test milliseconds timestamp", raw: "\\x:NorSat_1,c:1564827317000*72\\!AIVDM,1,2,3", block: TagBlock{ Time: 1564827317000, Source: "", }, len: 31, }, { name: "Test all input types", raw: "\\s:satelite,c:1564827317,r:1553390539,d:ara,g:bulk,n:13,t:helloworld*3F\\!AIVDM,1,2,3", block: TagBlock{ Time: 1564827317, RelativeTime: 1553390539, Destination: "ara", Grouping: "bulk", Source: "satelite", Text: "helloworld", LineCount: 13, }, len: 72, }, { name: "Test empty tag in tagblock", raw: "\\s:satelite,,r:1553390539,d:ara,g:bulk,n:13,t:helloworld*68\\!AIVDM,1,2,3", err: "nmea: tagblock field is malformed (should be :) []", }, { name: "Test Invalid checksum", raw: "\\s:satelite,c:1564827317*49\\!AIVDM,1,2,3", err: "nmea: tagblock checksum mismatch [25 != 49]", }, { name: "Test no checksum", raw: "\\s:satelite,c:156482731749\\!AIVDM,1,2,3", err: "nmea: tagblock does not contain checksum separator", }, { name: "Test invalid timestamp", raw: "\\s:satelite,c:gjadslkg*30\\!AIVDM,1,2,3", err: "nmea: tagblock unable to parse uint64 [gjadslkg]", }, { name: "Test invalid linecount", raw: "\\s:satelite,n:gjadslkg*3D\\!AIVDM,1,2,3", err: "nmea: tagblock unable to parse uint64 [gjadslkg]", }, { name: "Test invalid relative time", raw: "\\s:satelite,r:gjadslkg*21\\!AIVDM,1,2,3", err: "nmea: tagblock unable to parse uint64 [gjadslkg]", }, { name: "Test no tagblock", raw: "!AIVDM,1,2,3", }, } func TestParseTagBlock(t *testing.T) { for _, tt := range tagblocktests { t.Run(tt.name, func(t *testing.T) { b, n, err := ParseTagBlock(tt.raw) if tt.err != "" { assert.Error(t, err) } else { assert.NoError(t, err) } assert.Equal(t, tt.block, b) assert.Equal(t, tt.len, n) }) } } go-nmea-1.10.0/ths.go000066400000000000000000000020561465514462400142570ustar00rootroot00000000000000package 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 // http://manuals.spectracom.com/VSP/Content/VSP/NMEA_THSmess.htm // // Format: $--THS,xxx.xx,c*hh // Example: $GPTHS,338.01,A*36 type THS struct { BaseSentence Heading float64 // Heading in degrees Status string // Heading status } // newTHS constructor func newTHS(s BaseSentence) (Sentence, 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.10.0/ths_test.go000066400000000000000000000025711465514462400153200ustar00rootroot00000000000000package 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.10.0/tlb.go000066400000000000000000000025751465514462400142500ustar00rootroot00000000000000package nmea import "errors" const ( // TypeTLB type of TLB target label. TypeTLB = "TLB" ) // TLB is sentence for target label. // https://fcc.report/FCC-ID/ADB9ZWRTR100/2768717.pdf (page 8) FURUNO MARINE RADAR, model FAR-15XX manual // // Format: $--TLB,x.x,c--c,x.x,c--c,...x.x,c--c*hh // Example: $CDTLB,1,XXX,2.0,YYY*41 type TLB struct { BaseSentence Targets []TLBTarget } // TLBTarget is instance of target for TLB sentence type TLBTarget struct { // TargetNumber is target number “n” reported by the device (1 - 1023) TargetNumber float64 // TargetLabel is label assigned to target “n” (TT=000 - 999, AIS=000000000 - 999999999). Could be empty. TargetLabel string } // newTLB constructor func newTLB(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeTLB) tlb := TLB{ BaseSentence: s, Targets: make([]TLBTarget, 0), } fieldCount := len(p.Fields) if fieldCount < 2 { return tlb, errors.New("TLB is missing fields for parsing target pairs") } if fieldCount%2 != 0 { return tlb, errors.New("TLB data set field count is not exactly dividable by 2") } tlb.Targets = make([]TLBTarget, 0, fieldCount/2) for i := 0; i < fieldCount; i = i + 2 { tmp := TLBTarget{ TargetNumber: p.Float64(0+i, "target number"), TargetLabel: p.String(1+i, "target label"), } tlb.Targets = append(tlb.Targets, tmp) } return tlb, p.Err() } go-nmea-1.10.0/tlb_test.go000066400000000000000000000024531465514462400153020ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestTLB(t *testing.T) { var tests = []struct { name string raw string err string msg TLB }{ { name: "good sentence, single target", raw: "$RATLB,1,XXX*20", msg: TLB{ Targets: []TLBTarget{ {TargetNumber: 1, TargetLabel: "XXX"}, }, }, }, { name: "good sentence, multiple targets", raw: "$RATLB,1,XXX,2.0,YYY*55", msg: TLB{ Targets: []TLBTarget{ {TargetNumber: 1, TargetLabel: "XXX"}, {TargetNumber: 2, TargetLabel: "YYY"}, }, }, }, { name: "invalid nmea: field count", raw: "$RATLB,1*54", err: "TLB is missing fields for parsing target pairs", }, { name: "invalid nmea: data set field count", raw: "$RATLB,1,XXX,2.0*20", err: "TLB data set field count is not exactly dividable by 2", }, { name: "invalid nmea: target number", raw: "$RATLB,x,XXX*69", err: "nmea: RATLB invalid target number: x", }, } for _, tt := range tests { 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) tlb := m.(TLB) tlb.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, tlb) } }) } } go-nmea-1.10.0/tll.go000066400000000000000000000033341465514462400142540ustar00rootroot00000000000000package nmea const ( // TypeTLL type of TLL sentence for Target latitude and longitude TypeTLL = "TLL" // RadarTargetLost is used when target is lost RadarTargetLost = "L" // RadarTargetAcquisition is used when target is acquired RadarTargetAcquisition = "Q" // RadarTargetTracking is used when tracking target RadarTargetTracking = "T" ) // TLL - Target latitude and longitude // https://gpsd.gitlab.io/gpsd/NMEA.html#_tll_target_latitude_and_longitude // https://github.com/nohal/OpenCPN/wiki/ARPA-targets-tracking-implementation#tll---target-latitude-and-longitude // // Format: $--TLL,xx,llll.ll,a,yyyyy.yy,a,c--c,hhmmss.ss,a,a*hh // Example: $RATLL,,3647.422,N,01432.592,E,,,,*58 type TLL struct { BaseSentence TargetNumber int64 // Target number 00 – 99 TargetLatitude float64 // Target latitude + N/S TargetLongitude float64 // Target longitude + E/W TargetName string // Target name TimeUTC Time // UTC of data, hh is hours, mm is minutes, ss.ss is seconds. TargetStatus string // Target status (L=lost, Q=acquisition, T=tracking) ReferenceTarget string // Reference target, R= reference target; null (,,)= otherwise } // newTLL constructor func newTLL(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeTLL) return TLL{ BaseSentence: s, TargetNumber: p.Int64(0, "target number"), TargetLatitude: p.LatLong(1, 2, "latitude"), TargetLongitude: p.LatLong(3, 4, "longitude"), TargetName: p.String(5, "target name"), TimeUTC: p.Time(6, "UTC time"), TargetStatus: p.EnumString(7, "target status", RadarTargetLost, RadarTargetAcquisition, RadarTargetTracking), ReferenceTarget: p.EnumString(8, "reference target", "R"), }, p.Err() } go-nmea-1.10.0/tll_test.go000066400000000000000000000045561465514462400153220ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestTLL(t *testing.T) { var tests = []struct { name string raw string err string msg TLL }{ { name: "good sentence", raw: "$RATLL,,3647.422,N,01432.592,E,,,,*58", msg: TLL{ BaseSentence: BaseSentence{}, TargetNumber: 0, TargetLatitude: 36.790366666666664, TargetLongitude: 14.543200000000002, TargetName: "", TimeUTC: Time{ Valid: false, Hour: 0, Minute: 0, Second: 0, Millisecond: 0, }, TargetStatus: "", ReferenceTarget: "", }, }, { name: "good sentence 2", raw: "$RATLL,1,3646.54266,N,00235.37778,W,test,020915,L,R*78", msg: TLL{ BaseSentence: BaseSentence{}, TargetNumber: 1, TargetLatitude: 36.775711, TargetLongitude: -2.5896296666666667, TargetName: "test", TimeUTC: Time{Valid: true, Hour: 2, Minute: 9, Second: 15, Millisecond: 0}, TargetStatus: "L", ReferenceTarget: "R", }, }, { name: "invalid nmea: TargetNumber", raw: "$RATLL,x,3647.422,N,01432.592,E,,,,*20", err: "nmea: RATLL invalid target number: x", }, { name: "invalid nmea: TargetLatitude", raw: "$RATLL,1,x3647.422,N,01432.592,E,,,,*11", err: "nmea: RATLL invalid latitude: cannot parse [x3647.422 N], unknown format", }, { name: "invalid nmea: TargetLongitude", raw: "$RATLL,1,3647.422,N,x01432.592,E,,,,*11", err: "nmea: RATLL invalid longitude: cannot parse [x01432.592 E], unknown format", }, { name: "invalid nmea: TimeUTC", raw: "$RATLL,1,3646.54266,N,00235.37778,W,test,x020915,L,R*00", err: "nmea: RATLL invalid UTC time: x020915", }, { name: "invalid nmea: TargetStatus", raw: "$RATLL,1,3646.54266,N,00235.37778,W,test,020915,xL,R*00", err: "nmea: RATLL invalid target status: xL", }, { name: "invalid nmea: ReferenceTarget", raw: "$RATLL,1,3646.54266,N,00235.37778,W,test,020915,L,xR*00", err: "nmea: RATLL invalid reference target: xR", }, } for _, tt := range tests { 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) mm := m.(TLL) mm.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, mm) } }) } } go-nmea-1.10.0/ttd.go000066400000000000000000000023671465514462400142610ustar00rootroot00000000000000package nmea const ( // TypeTTD type of TTD sentence for tracked target data. TypeTTD = "TTD" ) // TTD is sentence used by radars to transmit tracked targets data. // https://fcc.report/FCC-ID/ADB9ZWRTR100/2768717.pdf (page 1) FURUNO MARINE RADAR, model FAR-15XX manual // // Format: !--TTD,hh,hh,x,s--s,x*hh // Example: !RATTD,1A,01,1,177KQJ5000G?tO`K>RA1wUbN0TKH,0*5C type TTD struct { BaseSentence // NumFragments is total hex number of fragments/sentences need to transfer the message (1 - FF) NumFragments int64 // 0 // FragmentNumber is current fragment/sentence number (1 - FF) FragmentNumber int64 // 1 // MessageID is sequential message identifier (0 - 9, null) MessageID int64 // 2 // Payload is encapsulated tracked target data (6 bit binary-converted data) Payload []byte // 3 // 4 - Number of fill bits (0 - 5) } // newTTD constructor func newTTD(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeTTD) m := TTD{ BaseSentence: s, NumFragments: p.HexInt64(0, "number of fragments"), FragmentNumber: p.HexInt64(1, "fragment number"), MessageID: p.Int64(2, "sequence number"), Payload: p.SixBitASCIIArmour(3, int(p.Int64(4, "number of padding bits")), "payload"), } return m, p.Err() } go-nmea-1.10.0/ttd_test.go000066400000000000000000000065501465514462400153160ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestTTD(t *testing.T) { var tests = []struct { name string raw string err string msg TTD }{ { name: "Good single fragment message", raw: "!RATTD,1A,01,1,177KQJ5000G?tO`K>RA1wUbN0TKH,0*72", msg: TTD{ NumFragments: 26, FragmentNumber: 1, MessageID: 1, Payload: []byte{ 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, // 10 0x1, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x1, 0x0, 0x1, // 20 0x1, 0x0, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, // 30 0x0, 0x1, 0x1, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x1, // 40 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // 50 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // 60 0x0, 0x1, 0x0, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, // 70 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x1, // 80 0x1, 0x1, 0x1, 0x1, 0x1, 0x0, 0x1, 0x0, 0x0, 0x0, // 90 0x0, 0x1, 0x1, 0x0, 0x1, 0x1, 0x0, 0x0, 0x1, 0x1, // 100 0x1, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x1, // 110 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, // 120 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x0, 0x0, 0x1, // 130 0x0, 0x1, 0x1, 0x0, 0x1, 0x0, 0x1, 0x0, 0x0, 0x1, // 140 0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // 150 0x1, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x1, 0x0, // 160 0x1, 0x1, 0x0, 0x1, 0x1, 0x0, 0x0, 0x0, // 168 }, }, }, { name: "Good single fragment message with padding", raw: "!RATTD,1A,01,1,H77nSfPh4U=RA1wUbN0TKH,0*7A", err: "nmea: RATTD invalid number of fragments: x", }, { name: "Invalid symbol in payload", raw: "!RATTD,1A,01,1,1 1,0*2b", err: "nmea: RATTD invalid payload: data byte", }, { name: "Negative number of fill bits", raw: "!RATTD,1A,01,1,177KQJ5000G?tO`K>RA1wUbN0TKH,-1*5e", err: "nmea: RATTD invalid payload: fill bits", }, { name: "Too high number of fill bits", raw: "!RATTD,1A,01,1,177KQJ5000G?tO`K>RA1wUbN0TKH,20*40", err: "nmea: RATTD invalid payload: fill bits", }, { name: "Negative number of bits", raw: "!RATTD,1A,01,1,,2*09", err: "nmea: RATTD invalid payload: num bits", }, } for _, tt := range tests { 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) ttd := m.(TTD) ttd.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, ttd) } }) } } go-nmea-1.10.0/ttm.go000066400000000000000000000051721465514462400142670ustar00rootroot00000000000000package nmea const ( // TypeTTM type of TTM sentence for Tracked Target Message TypeTTM = "TTM" ) // TTM - Tracked Target Message // https://gpsd.gitlab.io/gpsd/NMEA.html#_ttm_tracked_target_message // https://github.com/nohal/OpenCPN/wiki/ARPA-targets-tracking-implementation#ttm---tracked-target-message // // Format: $--TTM,xx,x.x,x.x,a,x.x,x.x,a,x.x,x.x,a,c--c,a,a*hh // Format: $--TTM,xx,x.x,x.x,a,x.x,x.x,a,x.x,x.x,a,c--c,a,a,hhmmss.ss,a*hh // Example: $RATTM,02,1.43,170.5,T,0.16,264.4,T,1.42,36.9,N,,T,,,M*2A type TTM struct { BaseSentence TargetNumber int64 // Target number 00 – 99 TargetDistance float64 // Target Distance Bearing float64 // Bearing from own ship, degrees BearingType string // Type of target Bearing, T = True, R = Relative TargetSpeed float64 // Target Speed TargetCourse float64 // Target Course CourseType string // target course type, T = True, R = Relative DistanceCPA float64 // Distance of closest-point-of-approach TimeCPA float64 // Time until closest-point-of-approach "-" means increasing SpeedUnits string // Speed/distance units, K/N/S TargetName string // Target name TargetStatus string // Target status (L=lost, Q=acquisition, T=tracking) ReferenceTarget string // Reference target, R= reference target; null (,,)= otherwise TimeUTC Time // UTC of data, hh is hours, mm is minutes, ss.ss is seconds. TypeOfAcquisition string // Type, A = Auto, M = Manual, R = Reported } // newTTM constructor func newTTM(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeTTM) return TTM{ BaseSentence: s, TargetNumber: p.Int64(0, "target number"), TargetDistance: p.Float64(1, "target Distance"), Bearing: p.Float64(2, "bearing"), BearingType: p.EnumString(3, "bearing type", "T", "R"), TargetSpeed: p.Float64(4, "target speed"), TargetCourse: p.Float64(5, "target course"), CourseType: p.EnumString(6, "course type", "T", "R"), DistanceCPA: p.Float64(7, "distance CPA"), TimeCPA: p.Float64(8, "time of CPA"), SpeedUnits: p.EnumString(9, "speed units", DistanceUnitKilometre, DistanceUnitNauticalMile, DistanceUnitStatuteMile), TargetName: p.String(10, "target name"), TargetStatus: p.EnumString(11, "target status", RadarTargetLost, RadarTargetAcquisition, RadarTargetTracking), ReferenceTarget: p.EnumString(12, "reference target", "R"), TimeUTC: p.Time(13, "UTC time"), TypeOfAcquisition: p.EnumString(14, "type of acquisition", "A", "M", "R"), }, p.Err() } go-nmea-1.10.0/ttm_test.go000066400000000000000000000066701465514462400153320ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestTTM(t *testing.T) { var tests = []struct { name string raw string err string msg TTM }{ { name: "good sentence", raw: "$RATTM,02,1.43,170.5,T,0.16,264.4,T,1.42,36.9,N,,T,,,M*2A", msg: TTM{ BaseSentence: BaseSentence{}, TargetNumber: 2, TargetDistance: 1.43, Bearing: 170.5, BearingType: "T", TargetSpeed: 0.16, TargetCourse: 264.4, CourseType: "T", DistanceCPA: 1.42, TimeCPA: 36.9, SpeedUnits: "N", TargetName: "", TargetStatus: "T", ReferenceTarget: "", TimeUTC: Time{Valid: false, Hour: 0, Minute: 0, Second: 0, Millisecond: 0}, TypeOfAcquisition: "M", }, }, { name: "invalid nmea: TargetNumber", raw: "$RATTM,x02,1.43,170.5,T,0.16,264.4,T,1.42,36.9,N,,T,,,M*52", err: "nmea: RATTM invalid target number: x02", }, { name: "invalid nmea: TargetDistance", raw: "$RATTM,02,x1.43,170.5,T,0.16,264.4,T,1.42,36.9,N,,T,,,M*52", err: "nmea: RATTM invalid target Distance: x1.43", }, { name: "invalid nmea: Bearing", raw: "$RATTM,02,1.43,x170.5,T,0.16,264.4,T,1.42,36.9,N,,T,,,M*52", err: "nmea: RATTM invalid bearing: x170.5", }, { name: "invalid nmea: BearingType", raw: "$RATTM,02,1.43,170.5,xT,0.16,264.4,T,1.42,36.9,N,,T,,,M*52", err: "nmea: RATTM invalid bearing type: xT", }, { name: "invalid nmea: TargetSpeed", raw: "$RATTM,02,1.43,170.5,T,x0.16,264.4,T,1.42,36.9,N,,T,,,M*52", err: "nmea: RATTM invalid target speed: x0.16", }, { name: "invalid nmea: TargetCourse", raw: "$RATTM,02,1.43,170.5,T,0.16,x264.4,T,1.42,36.9,N,,T,,,M*52", err: "nmea: RATTM invalid target course: x264.4", }, { name: "invalid nmea: CourseType", raw: "$RATTM,02,1.43,170.5,T,0.16,264.4,xT,1.42,36.9,N,,T,,,M*52", err: "nmea: RATTM invalid course type: xT", }, { name: "invalid nmea: DistanceCPA", raw: "$RATTM,02,1.43,170.5,T,0.16,264.4,T,x1.42,36.9,N,,T,,,M*52", err: "nmea: RATTM invalid distance CPA: x1.42", }, { name: "invalid nmea: TimeCPA", raw: "$RATTM,02,1.43,170.5,T,0.16,264.4,T,1.42,x36.9,N,,T,,,M*52", err: "nmea: RATTM invalid time of CPA: x36.9", }, { name: "invalid nmea: SpeedUnits", raw: "$RATTM,02,1.43,170.5,T,0.16,264.4,T,1.42,36.9,xN,,T,,,M*52", err: "nmea: RATTM invalid speed units: xN", }, { name: "invalid nmea: ReferenceTarget", raw: "$RATTM,02,1.43,170.5,T,0.16,264.4,T,1.42,36.9,N,,T,x,,M*52", err: "nmea: RATTM invalid reference target: x", }, { name: "invalid nmea: ReferenceTarget", raw: "$RATTM,02,1.43,170.5,T,0.16,264.4,T,1.42,36.9,N,,T,x,,M*52", err: "nmea: RATTM invalid reference target: x", }, { name: "invalid nmea: TimeUTC", raw: "$RATTM,02,1.43,170.5,T,0.16,264.4,T,1.42,36.9,N,,T,,x,M*52", err: "nmea: RATTM invalid UTC time: x", }, { name: "invalid nmea: TypeOfAcquisition", raw: "$RATTM,02,1.43,170.5,T,0.16,264.4,T,1.42,36.9,N,,T,,x,M*52", err: "nmea: RATTM invalid UTC time: x", }, } for _, tt := range tests { 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) mm := m.(TTM) mm.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, mm) } }) } } go-nmea-1.10.0/txt.go000066400000000000000000000026021465514462400142750ustar00rootroot00000000000000package nmea import "strings" const ( // TypeTXT type for TXT sentences for the transmission of text messages TypeTXT = "TXT" ) // TXT is sentence for the transmission of short text messages, longer text messages may be transmitted by using // multiple sentences. This sentence is intended to convey human readable textual information for display purposes. // The TXT sentence shall not be used for sending commands and making device configuration changes. // https://www.nmea.org/Assets/20160520%20txt%20amendment.pdf // // Format: $--TXT,xx,xx,xx,c-c*hh // Example: $GNTXT,01,01,02,u-blox AG - www.u-blox.com*4E type TXT struct { BaseSentence TotalNumber int64 // total number of sentences, 01 to 99 Number int64 // number of current sentences, 01 to 99 ID int64 // identifier of the text message, 01 to 99 // Message contains ASCII characters, and code delimiters if needed, up to the maximum permitted sentence length // (i.e., up to 61 characters including any code delimiters) Message string } // newTXT constructor func newTXT(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeTXT) m := TXT{ BaseSentence: s, TotalNumber: p.Int64(0, "total number of sentences"), Number: p.Int64(1, "sentence number"), ID: p.Int64(2, "sentence identifier"), Message: strings.Join(p.Fields[3:], FieldSep), } return m, p.Err() } go-nmea-1.10.0/txt_test.go000066400000000000000000000022271465514462400153370ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestTXT(t *testing.T) { var tests = []struct { name string raw string err string msg TXT }{ { name: "good sentence", raw: "$GNTXT,01,01,02,u-blox AG - www.u-blox.com*4E", msg: TXT{ TotalNumber: 1, Number: 1, ID: 2, Message: "u-blox AG - www.u-blox.com", }, }, { name: "invalid TotalNumber", raw: "$GNTXT,x,01,02,u-blox AG - www.u-blox.com*37", err: "nmea: GNTXT invalid total number of sentences: x", }, { name: "invalid Number", raw: "$GNTXT,01,X,02,u-blox AG - www.u-blox.com*17", err: "nmea: GNTXT invalid sentence number: X", }, { name: "invalid ID", raw: "$GNTXT,01,01,X,u-blox AG - www.u-blox.com*14", err: "nmea: GNTXT invalid sentence identifier: X", }, } for _, tt := range tests { 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) txt := m.(TXT) txt.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, txt) } }) } } go-nmea-1.10.0/types.go000066400000000000000000000317031465514462400146260ustar00rootroot00000000000000package nmea // Latitude / longitude representation. import ( "errors" "fmt" "math" "regexp" "strconv" "strings" "time" "unicode" ) const ( // StatusValid indicated status having valid value StatusValid = "A" // StatusInvalid indicated status having invalid value StatusInvalid = "V" ) const ( // UnitAmpere is unit for current in Amperes UnitAmpere = "A" // UnitBars is unit for pressure in Bars UnitBars = "B" // UnitBinary is unit for binary data UnitBinary = "B" // UnitCelsius is unit for temperature in Celsius UnitCelsius = TemperatureCelsius // UnitFahrenheit is unit for temperature in Fahrenheit UnitFahrenheit = TemperatureFahrenheit // UnitDegrees is unit for angular displacement in Degrees UnitDegrees = "D" // UnitHertz is unit for frequency in Hertz UnitHertz = "H" // UnitLitresPerSecond is unit for volumetric flow in Litres per second UnitLitresPerSecond = "I" // UnitKelvin is unit of temperature in Kelvin UnitKelvin = TemperatureKelvin // UnitKilogramPerCubicMetre is unit of density in kilogram per cubic metre UnitKilogramPerCubicMetre = "K" // UnitMeters is unit of distance in Meters UnitMeters = DistanceUnitMetre // UnitNewtons is unit of force in Newtons (1 kg*m/s2) UnitNewtons = "N" // UnitCubicMeters is unit of volume in cubic meters UnitCubicMeters = "M" // UnitRevolutionsPerMinute is unit of rotational speed or the frequency of rotation around a fixed axis in revolutions per minute (RPM) UnitRevolutionsPerMinute = "R" // UnitPercent is percent of full range UnitPercent = "P" // UnitPascal is unit of pressure in Pascals UnitPascal = "P" // UnitPartsPerThousand is in parts-per notation set of pseudo-unit to describe small values of miscellaneous dimensionless quantities, e.g. mole fraction or mass fraction. UnitPartsPerThousand = "S" // UnitVolts is unit of voltage in Volts UnitVolts = "V" ) const ( // SpeedKnots is a unit of speed equal to one nautical mile per hour, exactly 1.852 km/h (approximately 1.151 mph or 0.514 m/s) SpeedKnots = "N" // SpeedMeterPerSecond is unit of speed of 1 meter per second SpeedMeterPerSecond = "M" // SpeedKilometerPerHour is unit of speed of 1 kilometer per hour SpeedKilometerPerHour = "K" ) const ( // TemperatureCelsius is unit of temperature measured in celsius. °C = (°F − 32) / 1,8 TemperatureCelsius = "C" // TemperatureFahrenheit is unit of temperature measured in fahrenheits. °F = °C * 1,8 + 32 TemperatureFahrenheit = "F" // TemperatureKelvin is unit of temperature measured in kelvins. K = °C + 273,15 TemperatureKelvin = "K" ) // In navigation, the heading of a vessel or object is the compass direction in which the craft's bow or nose is pointed. // Note that the heading may not necessarily be the direction that the vehicle actually travels, which is known as // its course or track. // https://en.wikipedia.org/wiki/Heading_(navigation) const ( // HeadingMagnetic - Magnetic heading is your direction relative to magnetic north, read from your magnetic compass. // Magnetic north is the point on the Earth's surface where its magnetic field points directly downwards. HeadingMagnetic = "M" // HeadingTrue - True heading is your direction relative to true north, or the geographic north pole. // True north is the northern axis of rotation of the Earth. It is the point where the lines of longitude converge // on maps. HeadingTrue = "T" ) // In nautical navigation the absolute bearing is the clockwise angle between north and an object observed from the vessel. // https://en.wikipedia.org/wiki/Bearing_(angle) const ( // BearingMagnetic is the clockwise angle between Earth's magnetic north and an object observed from the vessel. BearingMagnetic = "M" // BearingTrue is the clockwise angle between Earth's true (geographical) north and an object observed from the vessel. BearingTrue = "T" ) // FAAMode is type for FAA mode indicator (NMEA 2.3 and later). // In NMEA 2.3, several sentences (APB, BWC, BWR, GLL, RMA, RMB, RMC, VTG, WCV, and XTE) got a new last field carrying // the signal integrity information needed by the FAA. // Source: https://www.xj3.nl/dokuwiki/doku.php?id=nmea // Note: there can be other values (proprietary). const ( // FAAModeAutonomous is Autonomous mode FAAModeAutonomous = "A" // FAAModeDifferential is Differential Mode FAAModeDifferential = "D" // FAAModeEstimated is Estimated (dead-reckoning) mode FAAModeEstimated = "E" // FAAModeRTKFloat is RTK Float mode FAAModeRTKFloat = "F" // FAAModeManualInput is Manual Input Mode FAAModeManualInput = "M" // FAAModeDataNotValid is Data Not Valid FAAModeDataNotValid = "N" // FAAModePrecise is Precise (NMEA4.00+) FAAModePrecise = "P" // FAAModeRTKInteger is RTK Integer mode FAAModeRTKInteger = "R" // FAAModeSimulated is Simulated Mode FAAModeSimulated = "S" ) // Navigation Status (NMEA 4.1 and later) const ( // NavStatusSimulated is a deprecated placeholder for backwards // compatibility. There is no such status in NMEA. // Deprecated: use NavStatusSafe NavStatusSimulated = "S" // NavStatusDataValid is a deprecated placeholder for backwards // compatibility. There is no such status in NMEA. // Deprecated: use NavStatusNotValid NavStatusDataValid = "V" // NavStatusSafe is Safe (within selected accuracy level) NavStatusSafe = "S" // NavStatusCaution is Caution (integrity not available) NavStatusCaution = "C" // NavStatusUnsafe is Unsafe (outside selected accuracy level) NavStatusUnsafe = "U" // NavStatusNotValid is Not Valid (equipment does not provide navigation status information) NavStatusNotValid = "V" ) const ( // DistanceUnitKilometre is unit for distance in kilometres (1km = 1000m) DistanceUnitKilometre = "K" // DistanceUnitNauticalMile is unit for distance in nautical miles (1nmi = 1852m) DistanceUnitNauticalMile = "N" // DistanceUnitStatuteMile is unit for distance in statute miles (1smi = 5,280 feet = 1609.344m) DistanceUnitStatuteMile = "S" // DistanceUnitMetre is unit for distance in metres DistanceUnitMetre = "M" // DistanceUnitFeet is unit for distance in feets (1f = 0.3048m) DistanceUnitFeet = "f" // DistanceUnitFathom is unit for distance in fathoms (1fm = 6ft = 1,8288m) DistanceUnitFathom = "F" ) 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" // Left value Left = "L" // Right value Right = "R" ) // 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.4322 S) 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.4322 S` 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(math.Round(frac * 1000))}, nil } // 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 } // DateTime converts the provided Date and Time values to a standard UTC time.Time. // The referenceYear parameter is used to determine the offset (century) for the two-digit year in Date. // For example, if the referenceYear is 2024, the offset used is 2000; and the input date's year is prepended with 20. // If referenceYear is 0, the current UTC year is used. // If either Date or Time is not valid, DateTime returns the zero time.Time. func DateTime(referenceYear int, d Date, t Time) time.Time { if !d.Valid || !t.Valid { return time.Time{} } if referenceYear == 0 { referenceYear = time.Now().UTC().Year() } century := referenceYear / 100 * 100 // truncate the last two digits (year within century); keep first two digits of the full year return time.Date(century+d.YY, time.Month(d.MM), d.DD, t.Hour, t.Minute, t.Second, t.Millisecond*1e6, time.UTC) } // 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 West } return East } // Float64 is a nullable float64 value type Float64 struct { Value float64 Valid bool } // Int64 is a nullable int64 value type Int64 struct { Value int64 Valid bool } go-nmea-1.10.0/types_test.go000066400000000000000000000153521465514462400156670ustar00rootroot00000000000000package nmea import ( "fmt" "testing" "time" "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 TestDateTime(t *testing.T) { for i, testCase := range []struct { refYear int date Date time Time expect time.Time }{ { refYear: 2024, date: Date{DD: 1, MM: 2, YY: 3, Valid: true}, time: Time{Hour: 1, Minute: 2, Second: 3, Millisecond: 4, Valid: true}, expect: time.Date(2003, time.February, 1, 1, 2, 3, 4e6, time.UTC), }, { refYear: 1999, date: Date{DD: 1, MM: 2, YY: 3, Valid: true}, time: Time{Hour: 1, Minute: 2, Second: 3, Millisecond: 4, Valid: true}, expect: time.Date(1903, time.February, 1, 1, 2, 3, 4e6, time.UTC), }, { refYear: 2025, date: Date{DD: 23, MM: 7, YY: 24, Valid: true}, time: Time{Hour: 18, Minute: 5, Second: 12, Millisecond: 1, Valid: true}, expect: time.Date(2024, time.July, 23, 18, 5, 12, 1e6, time.UTC), }, // zero reference year; this test will fail starting in the year 3000 { refYear: 0, date: Date{DD: 1, MM: 2, YY: 3, Valid: true}, time: Time{Hour: 1, Minute: 2, Second: 3, Millisecond: 4, Valid: true}, expect: time.Date(2003, time.February, 1, 1, 2, 3, 4e6, time.UTC), }, // invalid date { refYear: 2024, date: Date{DD: 1, MM: 2, YY: 3}, time: Time{Hour: 1, Minute: 2, Second: 3, Millisecond: 4, Valid: true}, expect: time.Time{}, }, // invalid time { refYear: 2024, date: Date{DD: 1, MM: 2, YY: 3, Valid: true}, time: Time{Hour: 1, Minute: 2, Second: 3, Millisecond: 4}, expect: time.Time{}, }, } { actual := DateTime(testCase.refYear, testCase.date, testCase.time) if !actual.Equal(testCase.expect) { t.Fatalf("Test %d (refYear=%d date=%s time=%s): Expected %s but got %s", i, testCase.refYear, testCase.date, testCase.time, testCase.expect, actual) } } } 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, "E"}, {-100.0, "W"}, } for _, tt := range tests { if s := LonDir(tt.value); s != tt.expected { t.Fatalf("got %s expected %s", s, tt.expected) } } } go-nmea-1.10.0/vbw.go000066400000000000000000000055061465514462400142620ustar00rootroot00000000000000package nmea const ( // TypeVBW type of VBW sentence for Dual Ground/Water Speed TypeVBW = "VBW" ) // VBW - Dual Ground/Water Speed // https://gpsd.gitlab.io/gpsd/NMEA.html#_vbw_dual_groundwater_speed // // Format: $--VBW,x.x,x.x,A,x.x,x.x,A,x.x,A,x.x,A*hh // Example: $VMVBW,-7.1,0.1,A,,,V,,V,,V*65 type VBW struct { BaseSentence LongitudinalWaterSpeedKnots float64 // longitudinal water speed, "-" means astern, knots TransverseWaterSpeedKnots float64 // transverse water speed, "-" means port, knots WaterSpeedStatusValid bool // A = true WaterSpeedStatus string // A = valid, V = invalid LongitudinalGroundSpeedKnots float64 // longitudinal ground speed, "-" means astern, knots TransverseGroundSpeedKnots float64 // transverse ground speed, "-" means port, knots GroundSpeedStatusValid bool // A = true GroundSpeedStatus string // A = valid, V = invalid SternTraverseWaterSpeedKnots float64 // Stern traverse water speed, knots (NMEA 3 and above) SternTraverseWaterSpeedStatusValid bool // A = true SternTraverseWaterSpeedStatus string // A = valid, V = invalid (NMEA 3 and above) SternTraverseGroundSpeedKnots float64 // Stern traverse ground speed, knots (NMEA 3 and above) SternTraverseGroundSpeedStatusValid bool // A = true SternTraverseGroundSpeedStatus string // A = valid, V = invalid (NMEA 3 and above) } // newVBW constructor func newVBW(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeVBW) m := VBW{ BaseSentence: s, LongitudinalWaterSpeedKnots: p.Float64(0, "longitudinal water speed"), TransverseWaterSpeedKnots: p.Float64(1, "transverse water speed"), WaterSpeedStatusValid: p.String(2, "water speed status valid") == StatusValid, WaterSpeedStatus: p.EnumString(2, "water speed status", StatusValid, StatusInvalid), LongitudinalGroundSpeedKnots: p.Float64(3, "longitudinal ground speed"), TransverseGroundSpeedKnots: p.Float64(4, "transverse ground speed"), GroundSpeedStatusValid: p.String(5, "ground speed status valid") == StatusValid, GroundSpeedStatus: p.EnumString(5, "ground speed status", StatusValid, StatusInvalid), } if len(p.Fields) > 6 { m.SternTraverseWaterSpeedKnots = p.Float64(6, "stern traverse water speed") m.SternTraverseWaterSpeedStatusValid = p.String(7, "stern water speed status valid") == StatusValid m.SternTraverseWaterSpeedStatus = p.EnumString(7, "stern water speed status", StatusValid, StatusInvalid) m.SternTraverseGroundSpeedKnots = p.Float64(8, "stern traverse ground speed") m.SternTraverseGroundSpeedStatusValid = p.String(9, "stern ground speed status valid") == StatusValid m.SternTraverseGroundSpeedStatus = p.EnumString(9, "stern ground speed status", StatusValid, StatusInvalid) } return m, p.Err() } go-nmea-1.10.0/vbw_test.go000066400000000000000000000056571465514462400153300ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestVBW(t *testing.T) { var tests = []struct { name string raw string err string msg VBW }{ { name: "good sentence", raw: "$VMVBW,-7.1,0.1,A,,,V,,V,,V*65", msg: VBW{ LongitudinalWaterSpeedKnots: -7.1, TransverseWaterSpeedKnots: 0.1, WaterSpeedStatusValid: true, WaterSpeedStatus: "A", LongitudinalGroundSpeedKnots: 0, TransverseGroundSpeedKnots: 0, GroundSpeedStatusValid: false, GroundSpeedStatus: "V", SternTraverseWaterSpeedKnots: 0, SternTraverseWaterSpeedStatusValid: false, SternTraverseWaterSpeedStatus: "V", SternTraverseGroundSpeedKnots: 0, SternTraverseGroundSpeedStatusValid: false, SternTraverseGroundSpeedStatus: "V", }, }, { name: "invalid nmea: LongitudinalWaterSpeedKnots", raw: "$VMVBW,x,0.1,A,,,V,,V,,V*18", err: "nmea: VMVBW invalid longitudinal water speed: x", }, { name: "invalid nmea: TransverseWaterSpeedKnots", raw: "$VMVBW,0.1,x,A,0.3,0.4,A,0.5,A,0.6,A*0b", err: "nmea: VMVBW invalid transverse water speed: x", }, { name: "invalid nmea: WaterSpeedStatusValid", raw: "$VMVBW,0.1,0.2,X,0.3,0.4,A,0.5,A,0.6,A*46", err: "nmea: VMVBW invalid water speed status: X", }, { name: "invalid nmea: LongitudinalGroundSpeedKnots", raw: "$VMVBW,0.1,0.2,A,X,0.4,A,0.5,A,0.6,A*2a", err: "nmea: VMVBW invalid longitudinal ground speed: X", }, { name: "invalid nmea: TransverseGroundSpeedKnots", raw: "$VMVBW,0.1,0.2,A,0.3,X,A,0.5,A,0.6,A*2d", err: "nmea: VMVBW invalid transverse ground speed: X", }, { name: "invalid nmea: GroundSpeedStatusValid", raw: "$VMVBW,0.1,0.2,A,0.3,0.4,X,0.5,A,0.6,A*46", err: "nmea: VMVBW invalid ground speed status: X", }, { name: "invalid nmea: SternTraverseWaterSpeedKnots", raw: "$VMVBW,0.1,0.2,A,0.3,0.4,A,X,A,0.6,A*2c", err: "nmea: VMVBW invalid stern traverse water speed: X", }, { name: "invalid nmea: SternTraverseWaterSpeedStatusValid", raw: "$VMVBW,0.1,0.2,A,0.3,0.4,A,0.5,X,0.6,A*46", err: "nmea: VMVBW invalid stern water speed status: X", }, { name: "invalid nmea: SternTraverseGroundSpeedKnots", raw: "$VMVBW,0.1,0.2,A,0.3,0.4,A,0.5,A,X,A*2f", err: "nmea: VMVBW invalid stern traverse ground speed: X", }, { name: "invalid nmea: SternTraverseGroundSpeedStatusValid", raw: "$VMVBW,0.1,0.2,A,0.3,0.4,A,0.5,A,0.6,X*46", err: "nmea: VMVBW invalid stern ground speed status: X", }, } for _, tt := range tests { 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) mm := m.(VBW) mm.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, mm) } }) } } go-nmea-1.10.0/vdmvdo.go000066400000000000000000000020251465514462400147540ustar00rootroot00000000000000package nmea const ( // TypeVDM type for VDM sentences TypeVDM = "VDM" // TypeVDO type for VDO sentences TypeVDO = "VDO" ) // VDMVDO is sentence ($--VDM or $--VDO) used to encapsulate generic binary payloads. It is most commonly used with AIS data. // https://gpsd.gitlab.io/gpsd/AIVDM.html // // Format: !--VDO,x,x,x,a,s--s,x*hh // Format: !--VDM,x,x,x,a,s--s,x*hh // Example: !AIVDM,1,1,,B,177KQJ5000G?tO`K>RA1wUbN0TKH,0*5C type VDMVDO struct { BaseSentence NumFragments int64 FragmentNumber int64 MessageID int64 Channel string Payload []byte } // newVDMVDO constructor func newVDMVDO(s BaseSentence) (Sentence, 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.10.0/vdmvdo_test.go000066400000000000000000000063051465514462400160200ustar00rootroot00000000000000package 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= // Example: $IIVDR,10.1,T,12.3,M,1.2,N*3A type VDR struct { BaseSentence SetDegreesTrue float64 // Direction degrees, True SetDegreesTrueUnit string // T = True SetDegreesMagnetic float64 // Direction degrees, True SetDegreesMagneticUnit string // M = Magnetic DriftKnots float64 // Current speed, knots DriftUnit string // N = Knots } // newVDR constructor func newVDR(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeVDR) return VDR{ BaseSentence: s, SetDegreesTrue: p.Float64(0, "true set degrees"), SetDegreesTrueUnit: p.EnumString(1, "true set unit", BearingTrue), SetDegreesMagnetic: p.Float64(2, "magnetic set degrees"), SetDegreesMagneticUnit: p.EnumString(3, "magnetic set unit", BearingMagnetic), DriftKnots: p.Float64(4, "drift knots"), DriftUnit: p.EnumString(5, "drift unit", SpeedKnots), }, p.Err() } go-nmea-1.10.0/vdr_test.go000066400000000000000000000023571465514462400153170ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestVDR(t *testing.T) { var tests = []struct { name string raw string err string msg VDR }{ { name: "good sentence", raw: "$IIVDR,10.1,T,12.3,M,1.2,N*3A", msg: VDR{ SetDegreesTrue: 10.1, SetDegreesTrueUnit: BearingTrue, SetDegreesMagnetic: 12.3, SetDegreesMagneticUnit: BearingMagnetic, DriftKnots: 1.2, DriftUnit: SpeedKnots, }, }, { name: "invalid nmea: SetDegreesTrueUnit", raw: "$IIVDR,10.1,x,12.3,M,1.2,N*16", err: "nmea: IIVDR invalid true set unit: x", }, { name: "invalid nmea: SetDegreesMagneticUnit", raw: "$IIVDR,10.1,T,12.3,x,1.2,N*0f", err: "nmea: IIVDR invalid magnetic set unit: x", }, { name: "invalid nmea: DriftUnit", raw: "$IIVDR,10.1,T,12.3,M,1.2,x*0c", err: "nmea: IIVDR invalid drift unit: x", }, } for _, tt := range tests { 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) vdr := m.(VDR) vdr.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, vdr) } }) } } go-nmea-1.10.0/vhw.go000066400000000000000000000016221465514462400142630ustar00rootroot00000000000000package nmea const ( // TypeVHW type for VHW sentences TypeVHW = "VHW" ) // VHW contains information about water speed and heading // https://gpsd.gitlab.io/gpsd/NMEA.html#_vhw_water_speed_and_heading // // Format: $--VHW,x.x,T,x.x,M,x.x,N,x.x,K*hh // Example: $VWVHW,45.0,T,43.0,M,3.5,N,6.4,K*56 type VHW struct { BaseSentence TrueHeading float64 MagneticHeading float64 SpeedThroughWaterKnots float64 SpeedThroughWaterKPH float64 } // newVHW constructor func newVHW(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeVHW) return VHW{ BaseSentence: s, TrueHeading: p.Float64(0, "true heading"), MagneticHeading: p.Float64(2, "magnetic heading"), SpeedThroughWaterKnots: p.Float64(4, "speed through water in knots"), SpeedThroughWaterKPH: p.Float64(6, "speed through water in kilometers per hour"), }, p.Err() } go-nmea-1.10.0/vhw_test.go000066400000000000000000000015231465514462400153220ustar00rootroot00000000000000package nmea import ( "testing" "github.com/stretchr/testify/assert" ) var vhw = []struct { name string raw string err string msg VHW }{ { name: "good sentence", raw: "$VWVHW,45.0,T,43.0,M,3.5,N,6.4,K*56", msg: VHW{ TrueHeading: 45.0, MagneticHeading: 43.0, SpeedThroughWaterKnots: 3.5, SpeedThroughWaterKPH: 6.4, }, }, { name: "bad sentence", raw: "$VWVHW,T,45.0,43.0,M,3.5,N,6.4,K*56", err: "nmea: VWVHW invalid true heading: T", }, } func TestVHW(t *testing.T) { for _, tt := range vhw { t.Run(tt.name, func(t *testing.T) { m, err := Parse(tt.raw) if tt.err != "" { assert.Error(t, err) assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) vhw := m.(VHW) vhw.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, vhw) } }) } } go-nmea-1.10.0/vlw.go000066400000000000000000000036271465514462400142760ustar00rootroot00000000000000package nmea const ( // TypeVLW type of VLW sentence for Distance Traveled through Water TypeVLW = "VLW" ) // VLW - Distance Traveled through Water // https://gpsd.gitlab.io/gpsd/NMEA.html#_vlw_distance_traveled_through_water // // Format: $--VLW,x.x,N,x.x,N*hh // Format (NMEA 3+): $--VLW,x.x,N,x.x,N,x.x,N,x.x,N*hh // Example: $IIVLW,10.1,N,3.2,N*7C // Example: $IIVLW,10.1,N,3.2,N,0,N,0,N*7C type VLW struct { BaseSentence TotalInWater float64 // Total cumulative water distance, nm TotalInWaterUnit string // N = Nautical Miles SinceResetInWater float64 // Water distance since Reset, nm SinceResetInWaterUnit string // N = Nautical Miles TotalOnGround float64 // Total cumulative ground distance, nm (NMEA 3 and above) TotalOnGroundUnit string // N = Nautical Miles (NMEA 3 and above) SinceResetOnGround float64 // Ground distance since reset, nm (NMEA 3 and above) SinceResetOnGroundUnit string // N = Nautical Miles (NMEA 3 and above) } // newVLW constructor func newVLW(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeVLW) vlw := VLW{ BaseSentence: s, TotalInWater: p.Float64(0, "total cumulative water distance"), TotalInWaterUnit: p.EnumString(1, "total cumulative water distance unit", DistanceUnitNauticalMile), SinceResetInWater: p.Float64(2, "water distance since reset"), SinceResetInWaterUnit: p.EnumString(3, "water distance since reset unit", DistanceUnitNauticalMile), } if len(p.Fields) > 4 { vlw.TotalOnGround = p.Float64(4, "total cumulative ground distance") vlw.TotalOnGroundUnit = p.EnumString(5, "total cumulative ground distance unit", DistanceUnitNauticalMile) vlw.SinceResetOnGround = p.Float64(6, "ground distance since reset") vlw.SinceResetOnGroundUnit = p.EnumString(7, "ground distance since reset unit", DistanceUnitNauticalMile) } return vlw, p.Err() } go-nmea-1.10.0/vlw_test.go000066400000000000000000000035571465514462400153370ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestVLW(t *testing.T) { var tests = []struct { name string raw string err string msg VLW }{ { name: "good sentence 1", raw: "$IIVLW,10.1,N,3.2,N*7C", msg: VLW{ TotalInWater: 10.1, TotalInWaterUnit: "N", SinceResetInWater: 3.2, SinceResetInWaterUnit: "N", TotalOnGround: 0, TotalOnGroundUnit: "", SinceResetOnGround: 0, SinceResetOnGroundUnit: "", }, }, { name: "good sentence 2", raw: "$IIVLW,10.1,N,3.2,N,1,N,0.1,N*62", msg: VLW{ TotalInWater: 10.1, TotalInWaterUnit: "N", SinceResetInWater: 3.2, SinceResetInWaterUnit: "N", TotalOnGround: 1, TotalOnGroundUnit: "N", SinceResetOnGround: 0.1, SinceResetOnGroundUnit: "N", }, }, { name: "invalid nmea: TotalInWaterUnit", raw: "$IIVLW,10.1,x,3.2,N,1,N,0.1,N*54", err: "nmea: IIVLW invalid total cumulative water distance unit: x", }, { name: "invalid nmea: SinceResetInWaterUnit", raw: "$IIVLW,10.1,N,3.2,x,1,N,0.1,N*54", err: "nmea: IIVLW invalid water distance since reset unit: x", }, { name: "invalid nmea: TotalOnGroundUnit", raw: "$IIVLW,10.1,N,3.2,N,1,x,0.1,N*54", err: "nmea: IIVLW invalid total cumulative ground distance unit: x", }, { name: "invalid nmea: SinceResetOnGroundUnit", raw: "$IIVLW,10.1,N,3.2,N,1,N,0.1,x*54", err: "nmea: IIVLW invalid ground distance since reset unit: x", }, } for _, tt := range tests { 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) vlw := m.(VLW) vlw.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, vlw) } }) } } go-nmea-1.10.0/vpw.go000066400000000000000000000017321465514462400142750ustar00rootroot00000000000000package nmea const ( // TypeVPW type of VPW sentence for Speed Measured Parallel to Wind TypeVPW = "VPW" ) // VPW - Speed Measured Parallel to Wind // https://gpsd.gitlab.io/gpsd/NMEA.html#_vpw_speed_measured_parallel_to_wind // // Format: $--VPW,x.x,N,x.x,M*hh // Example: $IIVPW,4.5,N,6.7,M*52 type VPW struct { BaseSentence SpeedKnots float64 // Speed, "-" means downwind, knots SpeedKnotsUnit string // N = knots SpeedMPS float64 // Speed, "-" means downwind, m/s SpeedMPSUnit string // M = m/s } // newVPW constructor func newVPW(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeVPW) return VPW{ BaseSentence: s, SpeedKnots: p.Float64(0, "wind speed in knots"), SpeedKnotsUnit: p.EnumString(1, "wind speed in knots unit", SpeedKnots), SpeedMPS: p.Float64(2, "wind speed in meters per second"), SpeedMPSUnit: p.EnumString(3, "wind speed in meters per second unit", SpeedMeterPerSecond), }, p.Err() } go-nmea-1.10.0/vpw_test.go000066400000000000000000000017721465514462400153400ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestVPW(t *testing.T) { var tests = []struct { name string raw string err string msg VPW }{ { name: "good sentence", raw: "$IIVPW,4.5,N,6.7,M*52", msg: VPW{ SpeedKnots: 4.5, SpeedKnotsUnit: SpeedKnots, SpeedMPS: 6.7, SpeedMPSUnit: SpeedMeterPerSecond, }, }, { name: "invalid nmea: SpeedKnotsUnit", raw: "$IIVPW,4.5,x,6.7,M*64", err: "nmea: IIVPW invalid wind speed in knots unit: x", }, { name: "invalid nmea: SpeedMPSUnit", raw: "$IIVPW,4.5,N,6.7,x*67", err: "nmea: IIVPW invalid wind speed in meters per second unit: x", }, } for _, tt := range tests { 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) vpw := m.(VPW) vpw.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, vpw) } }) } } go-nmea-1.10.0/vsd.go000066400000000000000000000063511465514462400142570ustar00rootroot00000000000000package nmea const ( // TypeVSD type of VSD sentence for AIS voyage static data. TypeVSD = "VSD" ) // VSD is sentence for AIS voyage static data. // https://fcc.report/FCC-ID/ADB9ZWRTR100/2768717.pdf (page 10) FURUNO MARINE RADAR, model FAR-15XX manual // http://www.annoyingdesigns.com/vsd/VSDControl.pdf // // Format: $--VSD,x.x,x.x,x.x,c--c,hhmmss.ss,xx,xx,x.x,x.x*hh // Example: $RAVSD,0,4.5,6,@@@@@@@@@@@@@@@@@@@@,220516,01,02,8,*12 type VSD struct { BaseSentence // From: https://www.itu.int/rec/R-REC-M.1371-5-201402-I/en (page 113) // * null means unchanged // * 0 = not available or no ship = default // * 1-99 = as defined in § 3.3.2 // * 100-199 = reserved, for regional use // * 200-255 = reserved, for future use TypeOfShipAndCargo Int64 // 0 // StaticDraughtMeters is maximum present static draught in meters (0 - 25.5, null). null means unchanged, 0 means not available StaticDraughtMeters Float64 // 1 // Persons on-board (0 - 8191, null) PersonsOnBoard Int64 // 2 // Destination (Alphanumeric character, null) Destination string // 3 // NOTE: we are not combining time+day+month here to time.Time because - some of these fields can be empty. // Estimated UTC of arrival at destination (000000.00 - 246000.00*, null), null means unchanged EstimatedArrivalTime Int64 // 4 // Estimated day of arrival at destination (00 - 31) (UTC), null means unchanged EstimatedArrivalDay Int64 // 5 // Estimated month of arrival at destination (00 - 12) (UTC), null means unchanged EstimatedArrivalMonth Int64 // 6 // Navigational status (0 - 15), null means unchanged. Reference ITU-R M.1371, Message 1, navigational status. // Source: https://www.itu.int/dms_pubrec/itu-r/rec/m/R-REC-M.1371-5-201402-I!!PDF-E.pdf (page 111) // null - unchanged // 0 - Under way using engine // 1 - At anchor // 2 - Not under command // 3 - Restricted maneuverability // 4 - Constrained by her draught // 5 - Moored // 6 - Aground // 7 - Engaged in Fishing // 8 - Under way sailing // 9 - HSC // 10 - WIG // 11 - Power-driven vessel towing astern // 12 - Power-driven vessel pushing ahead or towing alongside // 13 - Reserved for future use // 14 - AIS-SART (active), MOB-AIS, EPIRB-AIS // 15 - Undefined = default (also used by AIS-SART, MOB-AIS and EPIRB AIS under test) NavigationalStatus Int64 // 7 // RegionalApplication is Regional application flags, null means unchanged. Reference ITU-R M.1371, Message 1, reserved for regional applications. RegionalApplication Int64 // 8 } // newVSD constructor func newVSD(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeVSD) m := VSD{ BaseSentence: s, TypeOfShipAndCargo: p.NullInt64(0, "type of ship and cargo"), StaticDraughtMeters: p.NullFloat64(1, "maximum present static draught"), PersonsOnBoard: p.NullInt64(2, "persons on-board"), Destination: p.String(3, "destination"), EstimatedArrivalTime: p.NullInt64(4, "estimated arrival time"), EstimatedArrivalDay: p.NullInt64(5, "estimated arrival day"), EstimatedArrivalMonth: p.NullInt64(6, "estimated arrival month"), NavigationalStatus: p.NullInt64(7, "navigational status"), RegionalApplication: p.NullInt64(8, "Regional application"), } return m, p.Err() } go-nmea-1.10.0/vsd_test.go000066400000000000000000000050361465514462400153150ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestVSD(t *testing.T) { var tests = []struct { name string raw string err string msg VSD }{ { name: "good sentence", raw: "$RAVSD,0,4.5,6,@@@@@@@@@@@@@@@@@@@@,220516,01,02,8,*6E", msg: VSD{ TypeOfShipAndCargo: Int64{Value: 0, Valid: true}, StaticDraughtMeters: Float64{Value: 4.5, Valid: true}, PersonsOnBoard: Int64{Value: 6, Valid: true}, Destination: "@@@@@@@@@@@@@@@@@@@@", EstimatedArrivalTime: Int64{Value: 220516, Valid: true}, EstimatedArrivalDay: Int64{Value: 1, Valid: true}, EstimatedArrivalMonth: Int64{Value: 2, Valid: true}, NavigationalStatus: Int64{Value: 8, Valid: true}, RegionalApplication: Int64{Value: 0, Valid: false}, }, }, { name: "invalid nmea: TypeOfShipAndCargo", raw: "$RAVSD,x,4.5,6,@@@@@@@@@@@@@@@@@@@@,220516,01,02,8,*26", err: "nmea: RAVSD invalid type of ship and cargo: x", }, { name: "invalid nmea: StaticDraughtMeters", raw: "$RAVSD,0,4.x,6,@@@@@@@@@@@@@@@@@@@@,220516,01,02,8,*23", err: "nmea: RAVSD invalid maximum present static draught: 4.x", }, { name: "invalid nmea: PersonsOnBoard", raw: "$RAVSD,0,4.5,x,@@@@@@@@@@@@@@@@@@@@,220516,01,02,8,*20", err: "nmea: RAVSD invalid persons on-board: x", }, { name: "invalid nmea: EstimatedArrivalTime", raw: "$RAVSD,0,4.5,6,@@@@@@@@@@@@@@@@@@@@,22051x,01,02,8,*20", err: "nmea: RAVSD invalid estimated arrival time: 22051x", }, { name: "invalid nmea: EstimatedArrivalDay", raw: "$RAVSD,0,4.5,6,@@@@@@@@@@@@@@@@@@@@,220516,x1,02,8,*26", err: "nmea: RAVSD invalid estimated arrival day: x1", }, { name: "invalid nmea: EstimatedArrivalMonth", raw: "$RAVSD,0,4.5,6,@@@@@@@@@@@@@@@@@@@@,220516,01,x2,8,*26", err: "nmea: RAVSD invalid estimated arrival month: x2", }, { name: "invalid nmea: NavigationalStatus", raw: "$RAVSD,0,4.5,6,@@@@@@@@@@@@@@@@@@@@,220516,01,02,x,*2E", err: "nmea: RAVSD invalid navigational status: x", }, { name: "invalid nmea: RegionalApplication", raw: "$RAVSD,0,4.5,6,@@@@@@@@@@@@@@@@@@@@,220516,01,02,8,x*16", err: "nmea: RAVSD invalid Regional application: x", }, } for _, tt := range tests { 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) vsd := m.(VSD) vsd.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, vsd) } }) } } go-nmea-1.10.0/vtg.go000066400000000000000000000024071465514462400142610ustar00rootroot00000000000000package nmea const ( // TypeVTG type for VTG sentences TypeVTG = "VTG" ) // VTG represents track & speed data. // http://aprs.gids.nl/nmea/#vtg // https://gpsd.gitlab.io/gpsd/NMEA.html#_vtg_track_made_good_and_ground_speed // // Format: $--VTG,x.x,T,x.x,M,x.x,N,x.x,K*hh // Format (NMEA 2.3+): $--VTG,x.x,T,x.x,M,x.x,N,x.x,K,m*hh // Example: $GPVTG,45.5,T,67.5,M,30.45,N,56.40,K*4B // $GPVTG,220.86,T,,M,2.550,N,4.724,K,A*34 type VTG struct { BaseSentence TrueTrack float64 MagneticTrack float64 GroundSpeedKnots float64 GroundSpeedKPH float64 FFAMode string // FAA mode indicator (filled in NMEA 2.3 and later) } // newVTG parses the VTG sentence into this struct. // e.g: $GPVTG,360.0,T,348.7,M,000.0,N,000.0,K*43 func newVTG(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeVTG) vtg := VTG{ BaseSentence: s, TrueTrack: p.Float64(0, "true track"), MagneticTrack: p.Float64(2, "magnetic track"), GroundSpeedKnots: p.Float64(4, "ground speed (knots)"), GroundSpeedKPH: p.Float64(6, "ground speed (km/h)"), } if len(p.Fields) > 8 { vtg.FFAMode = p.String(8, "FAA mode") // not enum because some devices have proprietary "non-nmea" values } return vtg, p.Err() } go-nmea-1.10.0/vtg_test.go000066400000000000000000000021411465514462400153130ustar00rootroot00000000000000package nmea import ( "testing" "github.com/stretchr/testify/assert" ) var vtgtests = []struct { name string raw string err string msg VTG }{ { name: "good sentence", raw: "$GPVTG,45.5,T,67.5,M,30.45,N,56.40,K*4B", msg: VTG{ TrueTrack: 45.5, MagneticTrack: 67.5, GroundSpeedKnots: 30.45, GroundSpeedKPH: 56.4, FFAMode: "", }, }, { name: "good sentence with FAA mode", raw: "$GPVTG,220.86,T,,M,2.550,N,4.724,K,A*34", msg: VTG{ TrueTrack: 220.86, MagneticTrack: 0, GroundSpeedKnots: 2.55, GroundSpeedKPH: 4.724, FFAMode: "A", }, }, { 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 TestVTG(t *testing.T) { for _, tt := range vtgtests { 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) vtg := m.(VTG) vtg.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, vtg) } }) } } go-nmea-1.10.0/vwr.go000066400000000000000000000035561465514462400143050ustar00rootroot00000000000000package nmea const ( // TypeVWR type of VWR sentence for Relative Wind Speed and Angle TypeVWR = "VWR" ) // VWR - Relative Wind Speed and Angle. Speed is measured relative to the moving vessel. // According to NMEA: use of $--MWV is recommended. // https://gpsd.gitlab.io/gpsd/NMEA.html#_vwr_relative_wind_speed_and_angle // https://www.nmea.org/Assets/100108_nmea_0183_sentences_not_recommended_for_new_designs.pdf (page 16) // // Format: $--VWR,x.x,a,x.x,N,x.x,M,x.x,K*hh // Example: $IIVWR,75,R,1.0,N,0.51,M,1.85,K*6C // $IIVWR,024,L,018,N,,,,*5e // $IIVWR,,,,,,,,*53 type VWR struct { BaseSentence MeasuredAngle float64 // Measured Wind direction magnitude in degrees (0 to 180 deg) MeasuredDirectionBow string // Measured Wind direction Left/Right of bow SpeedKnots float64 // Measured wind Speed, knots SpeedKnotsUnit string // N = knots SpeedMPS float64 // Wind speed, meters/second SpeedMPSUnit string // M = m/s SpeedKPH float64 // Wind speed, km/hour SpeedKPHUnit string // M = km/h } // newVWR constructor func newVWR(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeVWR) return VWR{ BaseSentence: s, MeasuredAngle: p.Float64(0, "measured wind angle"), MeasuredDirectionBow: p.EnumString(1, "measured wind direction to bow", Left, Right), SpeedKnots: p.Float64(2, "wind speed in knots"), SpeedKnotsUnit: p.EnumString(3, "wind speed in knots unit", SpeedKnots), SpeedMPS: p.Float64(4, "wind speed in meters per second"), SpeedMPSUnit: p.EnumString(5, "wind speed in meters per second unit", SpeedMeterPerSecond), SpeedKPH: p.Float64(6, "wind speed in kilometers per hour"), SpeedKPHUnit: p.EnumString(7, "wind speed in kilometers per hour unit", SpeedKilometerPerHour), }, p.Err() } go-nmea-1.10.0/vwr_test.go000066400000000000000000000043671465514462400153450ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestVWR(t *testing.T) { var tests = []struct { name string raw string err string msg VWR }{ { // these examples are from SignalK name: "good sentence", raw: "$IIVWR,75,R,1.0,N,0.51,M,1.85,K*6C", msg: VWR{ MeasuredAngle: 75, MeasuredDirectionBow: Right, SpeedKnots: 1, SpeedKnotsUnit: SpeedKnots, SpeedMPS: 0.51, SpeedMPSUnit: SpeedMeterPerSecond, SpeedKPH: 1.85, SpeedKPHUnit: SpeedKilometerPerHour, }, }, { name: "good sentence, shorter but still valid", raw: "$IIVWR,024,L,018,N,,,,*5e", msg: VWR{ MeasuredAngle: 24, MeasuredDirectionBow: Left, SpeedKnots: 18, SpeedKnotsUnit: SpeedKnots, SpeedMPS: 0, SpeedMPSUnit: "", SpeedKPH: 0, SpeedKPHUnit: "", }, }, { name: "good sentence, handle empty values", raw: "$IIVWR,,,,,,,,*53", msg: VWR{ MeasuredAngle: 0, MeasuredDirectionBow: "", SpeedKnots: 0, SpeedKnotsUnit: "", SpeedMPS: 0, SpeedMPSUnit: "", SpeedKPH: 0, SpeedKPHUnit: "", }, }, { name: "invalid nmea: DirectionBow", raw: "$IIVWR,75,x,1.0,N,0.51,M,1.85,K*46", err: "nmea: IIVWR invalid measured wind direction to bow: x", }, { name: "invalid nmea: SpeedKnotsUnit", raw: "$IIVWR,75,R,1.0,x,0.51,M,1.85,K*5a", err: "nmea: IIVWR invalid wind speed in knots unit: x", }, { name: "invalid nmea: SpeedMPSUnit", raw: "$IIVWR,75,R,1.0,N,0.51,x,1.85,K*59", err: "nmea: IIVWR invalid wind speed in meters per second unit: x", }, { name: "invalid nmea: SpeedKPHUnit", raw: "$IIVWR,75,R,1.0,N,0.51,M,1.85,x*5f", err: "nmea: IIVWR invalid wind speed in kilometers per hour unit: x", }, } for _, tt := range tests { 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) vwr := m.(VWR) vwr.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, vwr) } }) } } go-nmea-1.10.0/vwt.go000066400000000000000000000032541465514462400143020ustar00rootroot00000000000000package nmea const ( // TypeVWT type of VWT sentence for True Wind Speed and Angle TypeVWT = "VWT" ) // VWT - True Wind Speed and Angle // https://www.nmea.org/Assets/100108_nmea_0183_sentences_not_recommended_for_new_designs.pdf // https://www.rubydoc.info/gems/nmea_plus/1.0.20/NMEAPlus/Message/NMEA/VWT // https://lists.gnu.org/archive/html/gpsd-dev/2012-04/msg00048.html // // Format: $--VWT,x.x,a,x.x,N,x.x,M,x.x,K*hh // Example: $IIVWT,75,x,1.0,N,0.51,M,1.85,K*40 type VWT struct { BaseSentence TrueAngle float64 // true Wind direction magnitude in degrees (0 to 180 deg) TrueDirectionBow string // true Wind direction Left/Right of bow SpeedKnots float64 // true wind Speed, knots SpeedKnotsUnit string // N = knots SpeedMPS float64 // Wind speed, meters/second SpeedMPSUnit string // M = m/s SpeedKPH float64 // Wind speed, km/hour SpeedKPHUnit string // M = km/h } // newVWT constructor func newVWT(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeVWT) return VWT{ BaseSentence: s, TrueAngle: p.Float64(0, "true wind angle"), TrueDirectionBow: p.EnumString(1, "true wind direction to bow", Left, Right), SpeedKnots: p.Float64(2, "wind speed in knots"), SpeedKnotsUnit: p.EnumString(3, "wind speed in knots unit", SpeedKnots), SpeedMPS: p.Float64(4, "wind speed in meters per second"), SpeedMPSUnit: p.EnumString(5, "wind speed in meters per second unit", SpeedMeterPerSecond), SpeedKPH: p.Float64(6, "wind speed in kilometers per hour"), SpeedKPHUnit: p.EnumString(7, "wind speed in kilometers per hour unit", SpeedKilometerPerHour), }, p.Err() } go-nmea-1.10.0/vwt_test.go000066400000000000000000000042231465514462400153360ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestVWT(t *testing.T) { var tests = []struct { name string raw string err string msg VWT }{ { // these examples are from SignalK name: "good sentence", raw: "$IIVWT,75,R,1.0,N,0.51,M,1.85,K*6A", msg: VWT{ TrueAngle: 75, TrueDirectionBow: Right, SpeedKnots: 1, SpeedKnotsUnit: SpeedKnots, SpeedMPS: 0.51, SpeedMPSUnit: SpeedMeterPerSecond, SpeedKPH: 1.85, SpeedKPHUnit: SpeedKilometerPerHour, }, }, { name: "good sentence, shorter but still valid", raw: "$IIVWT,024,L,018,N,,,,*58", msg: VWT{ TrueAngle: 24, TrueDirectionBow: Left, SpeedKnots: 18, SpeedKnotsUnit: SpeedKnots, SpeedMPS: 0, SpeedMPSUnit: "", SpeedKPH: 0, SpeedKPHUnit: "", }, }, { name: "good sentence, handle empty values", raw: "$IIVWT,,,,,,,,*55", msg: VWT{ TrueAngle: 0, TrueDirectionBow: "", SpeedKnots: 0, SpeedKnotsUnit: "", SpeedMPS: 0, SpeedMPSUnit: "", SpeedKPH: 0, SpeedKPHUnit: "", }, }, { name: "invalid nmea: DirectionBow", raw: "$IIVWT,75,x,1.0,N,0.51,M,1.85,K*40", err: "nmea: IIVWT invalid true wind direction to bow: x", }, { name: "invalid nmea: SpeedKnotsUnit", raw: "$IIVWT,75,R,1.0,x,0.51,M,1.85,K*5c", err: "nmea: IIVWT invalid wind speed in knots unit: x", }, { name: "invalid nmea: SpeedMPSUnit", raw: "$IIVWT,75,R,1.0,N,0.51,x,1.85,K*5f", err: "nmea: IIVWT invalid wind speed in meters per second unit: x", }, { name: "invalid nmea: SpeedKPHUnit", raw: "$IIVWT,75,R,1.0,N,0.51,M,1.85,x*59", err: "nmea: IIVWT invalid wind speed in kilometers per hour unit: x", }, } for _, tt := range tests { 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) vwt := m.(VWT) vwt.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, vwt) } }) } } go-nmea-1.10.0/wpl.go000066400000000000000000000014171465514462400142630ustar00rootroot00000000000000package nmea const ( // TypeWPL type for WPL sentences TypeWPL = "WPL" ) // WPL contains information about a waypoint location // http://aprs.gids.nl/nmea/#wpl // https://gpsd.gitlab.io/gpsd/NMEA.html#_wpl_waypoint_location // // Format: $--WPL,llll.ll,a,yyyyy.yy,a,c--c*hh // Example: $IIWPL,5503.4530,N,01037.2742,E,411*6F type WPL struct { BaseSentence Latitude float64 // Latitude Longitude float64 // Longitude Ident string // Ident of nth waypoint } // newWPL constructor func newWPL(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeWPL) return WPL{ BaseSentence: s, Latitude: p.LatLong(0, 1, "latitude"), Longitude: p.LatLong(2, 3, "longitude"), Ident: p.String(4, "ident of nth waypoint"), }, p.Err() } go-nmea-1.10.0/wpl_test.go000066400000000000000000000030021465514462400153120ustar00rootroot00000000000000package nmea import ( "testing" "github.com/stretchr/testify/assert" ) var wpltests = []struct { name string raw string err string msg WPL }{ { name: "good sentence", raw: "$IIWPL,5503.4530,N,01037.2742,E,411*6F", msg: WPL{ Latitude: MustParseLatLong("5503.4530 N"), Longitude: MustParseLatLong("01037.2742 E"), Ident: "411", }, }, { name: "bad latitude", raw: "$IIWPL,A,N,01037.2742,E,411*01", err: "nmea: IIWPL invalid latitude: cannot parse [A N], unknown format", }, { name: "bad longitude", raw: "$IIWPL,5503.4530,N,A,E,411*36", err: "nmea: IIWPL invalid longitude: cannot parse [A E], unknown format", }, { name: "good sentence", raw: "$IIWPL,3356.4650,S,15124.5567,E,411*73", msg: WPL{ Latitude: MustParseLatLong("3356.4650 S"), Longitude: MustParseLatLong("15124.5567 E"), Ident: "411", }, }, { name: "bad latitude", raw: "$IIWPL,A,S,15124.5567,E,411*18", err: "nmea: IIWPL invalid latitude: cannot parse [A S], unknown format", }, { name: "bad longitude", raw: "$IIWPL,3356.4650,S,A,E,411*2E", err: "nmea: IIWPL invalid longitude: cannot parse [A E], unknown format", }, } func TestWPL(t *testing.T) { for _, tt := range wpltests { 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) wpl := m.(WPL) wpl.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, wpl) } }) } } go-nmea-1.10.0/xdr.go000066400000000000000000000106651465514462400142630ustar00rootroot00000000000000package nmea import "errors" const ( // TypeXDR type of XDR sentence for Transducer Measurement TypeXDR = "XDR" ) const ( // TransducerAngularDisplacementXDR is transducer type for Angular displacement TransducerAngularDisplacementXDR = "A" // TransducerTemperatureXDR is transducer type for Temperature TransducerTemperatureXDR = "C" // TransducerDepthXDR is transducer type for Depth TransducerDepthXDR = "D" // TransducerFrequencyXDR is transducer type for Frequency TransducerFrequencyXDR = "F" // TransducerHumidityXDR is transducer type for Humidity TransducerHumidityXDR = "H" // TransducerForceXDR is transducer type for Force TransducerForceXDR = "N" // TransducerPressureXDR is transducer type for Pressure TransducerPressureXDR = "P" // TransducerFlowXDR is transducer type for Flow TransducerFlowXDR = "R" // TransducerAbsoluteHumidityXDR is transducer type for Absolute humidity TransducerAbsoluteHumidityXDR = "B" // TransducerGenericXDR is transducer type for Generic TransducerGenericXDR = "G" // TransducerCurrentXDR is transducer type for Current TransducerCurrentXDR = "I" // TransducerSalinityXDR is transducer type for Salinity TransducerSalinityXDR = "L" // TransducerSwitchValveXDR is transducer type for Switch, valve TransducerSwitchValveXDR = "S" // TransducerTachometerXDR is transducer type for Tachometer TransducerTachometerXDR = "T" // TransducerVoltageXDR is transducer type for Voltage TransducerVoltageXDR = "U" // TransducerVolumeXDR is transducer type for Volume TransducerVolumeXDR = "V" ) // XDR - Transducer Measurement // https://gpsd.gitlab.io/gpsd/NMEA.html#_xdr_transducer_measurement // https://www.eye4software.com/hydromagic/documentation/articles-and-howtos/handling-nmea0183-xdr/ // // Format: $--XDR,a,x.x,a,c--c, ..... *hh // Example: $HCXDR,A,171,D,PITCH,A,-37,D,ROLL,G,367,,MAGX,G,2420,,MAGY,G,-8984,,MAGZ*41 // $SDXDR,C,23.15,C,WTHI*70 type XDR struct { BaseSentence Measurements []XDRMeasurement } // XDRMeasurement is measurement recorded by transducer type XDRMeasurement struct { // TransducerType is type of transducer // * A - Angular displacement // * C - Temperature // * D - Depth // * F - Frequency // * H - Humidity // * N - Force // * P - Pressure // * R - Flow // * B - Absolute humidity // * G - Generic // * I - Current // * L - Salinity // * S - Switch, valve // * T - Tachometer // * U - Voltage // * V - Volume // could be more TransducerType string // Value of measurement Value float64 // Unit of measurement // * "" - could be empty! // * A - Amperes // * B - Bars | Binary // * C - Celsius // * D - Degrees // * H - Hertz // * I - liters/second // * K - Kelvin | Density, kg/m3 kilogram per cubic metre // * M - Meters | Cubic Meters (m3) // * N - Newton // * P - percent of full range | Pascal // * R - RPM // * S - Parts per thousand // * V - Volts // could be more Unit string // TransducerName is name of transducer where measurement was recorded TransducerName string } // newXDR constructor func newXDR(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeXDR) xdr := XDR{ BaseSentence: s, Measurements: nil, } if len(p.Fields)%4 != 0 { return xdr, errors.New("XDR field count is not exactly dividable by 4") } xdr.Measurements = make([]XDRMeasurement, 0, len(s.Fields)/4) for i := 0; i < len(s.Fields); { tmp := XDRMeasurement{ TransducerType: p.EnumString( i, "transducer type", TransducerAngularDisplacementXDR, TransducerTemperatureXDR, TransducerDepthXDR, TransducerFrequencyXDR, TransducerHumidityXDR, TransducerForceXDR, TransducerPressureXDR, TransducerFlowXDR, TransducerAbsoluteHumidityXDR, TransducerGenericXDR, TransducerCurrentXDR, TransducerSalinityXDR, TransducerSwitchValveXDR, TransducerTachometerXDR, TransducerVoltageXDR, TransducerVolumeXDR, ), Value: p.Float64(i+1, "measurement value"), Unit: p.EnumString( i+2, "measurement unit", UnitAmpere, UnitBars, UnitBinary, UnitCelsius, UnitDegrees, UnitHertz, UnitLitresPerSecond, UnitKelvin, UnitKilogramPerCubicMetre, UnitNewtons, UnitMeters, UnitCubicMeters, UnitRevolutionsPerMinute, UnitPercent, UnitPascal, UnitPartsPerThousand, UnitVolts, ), TransducerName: p.String(i+3, "transducer name"), } xdr.Measurements = append(xdr.Measurements, tmp) i += 4 } return xdr, p.Err() } go-nmea-1.10.0/xdr_test.go000066400000000000000000000050011465514462400153060ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestXDR(t *testing.T) { var tests = []struct { name string raw string err string msg XDR }{ { name: "good sentence with 1 measurement", raw: "$SDXDR,C,23.15,C,WTHI*70", msg: XDR{ Measurements: []XDRMeasurement{ { TransducerType: "C", Value: 23.15, Unit: "C", TransducerName: "WTHI", }, }, }, }, { name: "good sentence with 5 measurements", raw: "$HCXDR,A,171,D,PITCH,A,-37,D,ROLL,G,367,,MAGX,G,2420,,MAGY,G,-8984,,MAGZ*41", msg: XDR{ Measurements: []XDRMeasurement{ {TransducerType: "A", Value: 171, Unit: "D", TransducerName: "PITCH"}, {TransducerType: "A", Value: -37, Unit: "D", TransducerName: "ROLL"}, {TransducerType: "G", Value: 367, Unit: "", TransducerName: "MAGX"}, {TransducerType: "G", Value: 2420, Unit: "", TransducerName: "MAGY"}, {TransducerType: "G", Value: -8984, Unit: "", TransducerName: "MAGZ"}, }, }, }, { name: "good sentence with 4 measurements", raw: "$WIXDR,C,9.7,C,2,U,24.1,N,0,U,24.4,V,1,U,3.510,V,2*46", msg: XDR{ Measurements: []XDRMeasurement{ {TransducerType: "C", Value: 9.7, Unit: "C", TransducerName: "2"}, // U+N - Voltage+Newtons? This is real sentence from actual vessel nmea0183 bus. Maybe misconfigured device? {TransducerType: "U", Value: 24.1, Unit: "N", TransducerName: "0"}, {TransducerType: "U", Value: 24.4, Unit: "V", TransducerName: "1"}, {TransducerType: "U", Value: 3.510, Unit: "V", TransducerName: "2"}, }, }, }, { name: "invalid nmea: odd number of fields", raw: "$HCXDR,A,171,D,PITCH,A,-37,D,ROLL,G,367,,MAGX,G,2420,MAGY,G,-8984,,MAGZ*6d", err: "XDR field count is not exactly dividable by 4", }, { name: "invalid nmea: TransducerType", raw: "$SDXDR,x,23.15,C,WTHI*4b", err: "nmea: SDXDR invalid transducer type: x", }, { name: "invalid nmea: Value", raw: "$SDXDR,C,23.x,C,WTHI*0C", err: "nmea: SDXDR invalid measurement value: 23.x", }, { name: "invalid nmea: Unit", raw: "$SDXDR,C,23.15,x,WTHI*4b", err: "nmea: SDXDR invalid measurement unit: x", }, } for _, tt := range tests { 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) xdr := m.(XDR) xdr.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, xdr) } }) } } go-nmea-1.10.0/xte.go000066400000000000000000000037051465514462400142630ustar00rootroot00000000000000package nmea const ( // TypeXTE type of XTE sentence for Cross-track error, measured TypeXTE = "XTE" ) // XTE - Cross-track error, measured // https://gpsd.gitlab.io/gpsd/NMEA.html#_xte_cross_track_error_measured // // Format: $--XTE,A,A,x.x,a,N*hh // Format (NMEA 2.3): $--XTE,A,A,x.x,a,N,m*hh // Example: $GPXTE,V,V,,,N,S*43 type XTE struct { BaseSentence // StatusGeneralWarning is used for warnings // * V = LORAN-C Blink or SNR warning // * A = general warning flag or other navigation systems when a reliable fix is not available StatusGeneralWarning string // StatusLockWarning is used for lock warning // * V = Loran-C Cycle Lock warning flag // * A = OK or not used StatusLockWarning string // CrossTrackErrorMagnitude is Cross Track Error Magnitude CrossTrackErrorMagnitude float64 // DirectionToSteer is Direction to steer, // * L = left // * R = right DirectionToSteer string // CrossTrackUnits is cross track units // * N = nautical miles // * K = for kilometers CrossTrackUnits string // FAA mode indicator (filled in NMEA 2.3 and later) FFAMode string } // newXTE constructor func newXTE(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeXTE) xte := XTE{ BaseSentence: s, StatusGeneralWarning: p.EnumString(0, "general warning", StatusWarningAClearORNotUsedAPB, StatusWarningASetAPB), StatusLockWarning: p.EnumString(1, "lock warning", StatusWarningBSetAPB, StatusWarningBClearAPB), CrossTrackErrorMagnitude: p.Float64(2, "cross track error magnitude"), DirectionToSteer: p.EnumString(3, "direction to steer", Left, Right), CrossTrackUnits: p.EnumString(4, "cross track units", DistanceUnitKilometre, DistanceUnitNauticalMile, DistanceUnitStatuteMile, DistanceUnitMetre), } if len(p.Fields) > 5 { xte.FFAMode = p.String(5, "FAA mode") // not enum because some devices have proprietary "non-nmea" values } return xte, p.Err() } go-nmea-1.10.0/xte_test.go000066400000000000000000000031751465514462400153230ustar00rootroot00000000000000package nmea import ( "github.com/stretchr/testify/assert" "testing" ) func TestXTE(t *testing.T) { var tests = []struct { name string raw string err string msg XTE }{ { name: "good sentence", raw: "$GPXTE,V,V,10.1,L,N*6E", msg: XTE{ StatusGeneralWarning: "V", StatusLockWarning: "V", CrossTrackErrorMagnitude: 10.1, DirectionToSteer: "L", CrossTrackUnits: "N", FFAMode: "", }, }, { name: "good sentence with FAAMode", raw: "$GPXTE,V,V,,,N,S*43", msg: XTE{ StatusGeneralWarning: "V", StatusLockWarning: "V", CrossTrackErrorMagnitude: 0, DirectionToSteer: "", CrossTrackUnits: "N", FFAMode: "S", }, }, { name: "invalid nmea: StatusGeneralWarning", raw: "$GPXTE,x,V,,,N,S*6d", err: "nmea: GPXTE invalid general warning: x", }, { name: "invalid nmea: StatusLockWarning", raw: "$GPXTE,V,x,,,N,S*6d", err: "nmea: GPXTE invalid lock warning: x", }, { name: "invalid nmea: DirectionToSteer", raw: "$GPXTE,V,V,,x,N,S*3b", err: "nmea: GPXTE invalid direction to steer: x", }, { name: "invalid nmea: CrossTrackUnits", raw: "$GPXTE,V,V,,,x,S*75", err: "nmea: GPXTE invalid cross track units: x", }, } for _, tt := range tests { 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) xte := m.(XTE) xte.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, xte) } }) } } go-nmea-1.10.0/zda.go000066400000000000000000000017461465514462400142440ustar00rootroot00000000000000package nmea const ( // TypeZDA type for ZDA sentences TypeZDA = "ZDA" ) // ZDA represents date & time data. // http://aprs.gids.nl/nmea/#zda // https://gpsd.gitlab.io/gpsd/NMEA.html#_zda_time_date_utc_day_month_year_and_local_time_zone // // Format: $--ZDA,hhmmss.ss,xx,xx,xxxx,xx,xx*hh // Example: $GPZDA,172809.456,12,07,1996,00,00*57 type ZDA struct { BaseSentence Time Time Day int64 Month int64 Year int64 OffsetHours int64 // Local time zone offset from GMT, hours OffsetMinutes int64 // Local time zone offset from GMT, minutes } // newZDA constructor func newZDA(s BaseSentence) (Sentence, error) { p := NewParser(s) p.AssertType(TypeZDA) return ZDA{ BaseSentence: s, Time: p.Time(0, "time"), Day: p.Int64(1, "day"), Month: p.Int64(2, "month"), Year: p.Int64(3, "year"), OffsetHours: p.Int64(4, "offset (hours)"), OffsetMinutes: p.Int64(5, "offset (minutes)"), }, p.Err() } go-nmea-1.10.0/zda_test.go000066400000000000000000000017371465514462400153030ustar00rootroot00000000000000package nmea import ( "testing" "github.com/stretchr/testify/assert" ) var zdatests = []struct { name string raw string err string msg ZDA }{ { name: "good sentence", raw: "$GPZDA,172809.456,12,07,1996,00,00*57", msg: ZDA{ 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 TestZDA(t *testing.T) { for _, tt := range zdatests { 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) zda := m.(ZDA) zda.BaseSentence = BaseSentence{} assert.Equal(t, tt.msg, zda) } }) } }