New upstream version 1.7.0
Francisco Vilmar Cardoso Ruviaro
2 years ago
0 | .DEFAULT_GOAL := check | |
1 | check: lint vet test ## Check project | |
2 | ||
3 | lint: ## Lint the files | |
4 | @golint -set_exit_status ./... | |
5 | ||
6 | vet: ## Vet the files | |
7 | @go vet ./... | |
8 | ||
9 | test: ## Run tests with data race detector | |
10 | @go test -race ./... | |
11 | ||
12 | init: | |
13 | @go get -u golang.org/x/lint/golint@latest | |
14 | ||
15 | goversion ?= "1.17" | |
16 | test_version: ## Run tests inside Docker with given version (defaults to 1.17). Example for Go1.15: make test_version goversion=1.15 | |
17 | @docker run --rm -it -v $(shell pwd):/project golang:$(goversion) /bin/sh -c "cd /project && make test" |
28 | 28 | |
29 | 29 | At this moment, this library supports the following sentence types: |
30 | 30 | |
31 | | Sentence type | Description | | |
32 | | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------- | | |
33 | | [RMC](http://aprs.gids.nl/nmea/#rmc) | Recommended Minimum Specific GPS/Transit data | | |
34 | | [PMTK](https://www.rhydolabz.com/documents/25/PMTK_A11.pdf) | Messages for setting and reading commands for MediaTek gps modules. | | |
35 | | [GGA](http://aprs.gids.nl/nmea/#gga) | GPS Positioning System Fix Data | | |
36 | | [GSA](http://aprs.gids.nl/nmea/#gsa) | GPS DOP and active satellites | | |
37 | | [GSV](http://aprs.gids.nl/nmea/#gsv) | GPS Satellites in view | | |
38 | | [GLL](http://aprs.gids.nl/nmea/#gll) | Geographic Position, Latitude / Longitude and time | | |
39 | | [VTG](http://aprs.gids.nl/nmea/#vtg) | Track Made Good and Ground Speed | | |
40 | | [ZDA](http://aprs.gids.nl/nmea/#zda) | Date & time data | | |
41 | | [HDT](http://aprs.gids.nl/nmea/#hdt) | Actual vessel heading in degrees True | | |
42 | | [GNS](https://www.trimble.com/oem_receiverhelp/v4.44/en/NMEA-0183messages_GNS.html) | Combined GPS fix for GPS, Glonass, Galileo, and BeiDou | | |
43 | | [PGRME](http://aprs.gids.nl/nmea/#rme) | Estimated Position Error (Garmin proprietary sentence) | | |
44 | | [THS](http://www.nuovamarea.net/pytheas_9.html) | Actual vessel heading in degrees True and status | | |
45 | | [VDM/VDO](http://catb.org/gpsd/AIVDM.html) | Encapsulated binary payload | | |
46 | | [WPL](http://aprs.gids.nl/nmea/#wpl) | Waypoint location | | |
47 | | [RTE](http://aprs.gids.nl/nmea/#rte) | Route | | |
48 | | [VHW](https://www.tronico.fi/OH6NT/docs/NMEA0183.pdf) | Water Speed and Heading | | |
49 | | [DPT](https://gpsd.gitlab.io/gpsd/NMEA.html#_dpt_depth_of_water) | Depth of Water | | |
50 | | [DBS](https://gpsd.gitlab.io/gpsd/NMEA.html#_dbs_depth_below_surface) | Depth Below Surface | | |
51 | | [DBT](https://gpsd.gitlab.io/gpsd/NMEA.html#_dbt_depth_below_transducer) | Depth below transducer | | |
52 | | [MDA](#) | Meteorological Composite | | |
53 | | [MWD](#) | Wind Direction and Speed | | |
54 | | [MWV](#) | Wind Speed and Angle | | |
55 | ||
56 | 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. | |
31 | | Sentence type | Description | | |
32 | |-----------------------------------------------------------------------------------------------|-----------------------------------------------------------| | |
33 | | [AAM](https://gpsd.gitlab.io/gpsd/NMEA.html#_aam_waypoint_arrival_alarm) | Waypoint Arrival Alarm | | |
34 | | [ALA](./ala.go) | System Faults and Alarms | | |
35 | | [APB](https://gpsd.gitlab.io/gpsd/NMEA.html#_apb_autopilot_sentence_b) | Autopilot Sentence "B" | | |
36 | | [BEC](http://www.nmea.de/nmea0183datensaetze.html#bec) | Bearing and distance to waypoint (dead reckoning) | | |
37 | | [BOD](https://gpsd.gitlab.io/gpsd/NMEA.html#_bod_bearing_waypoint_to_waypoint) | Bearing waypoint to waypoint (origin to destination) | | |
38 | | [BWC](https://gpsd.gitlab.io/gpsd/NMEA.html#_bwc_bearing_distance_to_waypoint_great_circle) | Bearing and distance to waypoint (great circle) | | |
39 | | [BWR](https://gpsd.gitlab.io/gpsd/NMEA.html#_bwr_bearing_and_distance_to_waypoint_rhumb_line) | Bearing and distance to waypoint (Rhumb Line) | | |
40 | | [BWW](https://gpsd.gitlab.io/gpsd/NMEA.html#_bww_bearing_waypoint_to_waypoint) | Bearing from destination waypoint to origin waypoint | | |
41 | | [DBK](https://gpsd.gitlab.io/gpsd/NMEA.html#_dbk_depth_below_keel) | Depth Below Keel (obsolete, use DPT instead) | | |
42 | | [DBS](https://gpsd.gitlab.io/gpsd/NMEA.html#_dbs_depth_below_surface) | Depth Below Surface (obsolete, use DPT instead) | | |
43 | | [DBT](https://gpsd.gitlab.io/gpsd/NMEA.html#_dbt_depth_below_transducer) | Depth below transducer | | |
44 | | [DOR](./dor.go) | Door Status Detection | | |
45 | | [DPT](https://gpsd.gitlab.io/gpsd/NMEA.html#_dpt_depth_of_water) | Depth of Water | | |
46 | | [DSC](./dsc.go) | Digital Selective Calling Information | | |
47 | | [DSE](./dse.go) | Expanded digital selective calling | | |
48 | | [DTM](https://gpsd.gitlab.io/gpsd/NMEA.html#_dtm_datum_reference) | Datum Reference | | |
49 | | [EVE](./eve.go) | General Event Message | | |
50 | | [FIR](./fir.go) | Fire Detection event with time and location | | |
51 | | [GGA](http://aprs.gids.nl/nmea/#gga) | GPS Positioning System Fix Data | | |
52 | | [GLL](http://aprs.gids.nl/nmea/#gll) | Geographic Position, Latitude / Longitude and time | | |
53 | | [GNS](https://gpsd.gitlab.io/gpsd/NMEA.html#_gns_fix_data) | Combined GPS fix for GPS, Glonass, Galileo, and BeiDou | | |
54 | | [GSA](http://aprs.gids.nl/nmea/#gsa) | GPS DOP and active satellites | | |
55 | | [GSV](http://aprs.gids.nl/nmea/#gsv) | GPS Satellites in view | | |
56 | | [HDG](https://gpsd.gitlab.io/gpsd/NMEA.html#_hdg_heading_deviation_variation) | Heading, Deviation & Variation | | |
57 | | [HDM](https://gpsd.gitlab.io/gpsd/NMEA.html#_hdm_heading_magnetic) | Heading - Magnetic | | |
58 | | [HDT](http://aprs.gids.nl/nmea/#hdt) | Actual vessel heading in degrees True | | |
59 | | [HSC](https://gpsd.gitlab.io/gpsd/NMEA.html#_hsc_heading_steering_command) | Heading steering command | | |
60 | | [MDA](https://gpsd.gitlab.io/gpsd/NMEA.html#_mda_meteorological_composite) | Meteorological Composite | | |
61 | | [MTA](./mta.go) | Air Temperature (obsolete, use XDR instead) | | |
62 | | [MTW](https://gpsd.gitlab.io/gpsd/NMEA.html#_mtw_mean_temperature_of_water) | Mean Temperature of Water | | |
63 | | [MWD](https://www.tronico.fi/OH6NT/docs/NMEA0183.pdf) | Wind Direction and Speed | | |
64 | | [MWV](https://gpsd.gitlab.io/gpsd/NMEA.html#_mwv_wind_speed_and_angle) | Wind Speed and Angle | | |
65 | | [OSD](https://gpsd.gitlab.io/gpsd/NMEA.html#_osd_own_ship_data) | Own Ship Data | | |
66 | | [RMB](https://gpsd.gitlab.io/gpsd/NMEA.html#_rmb_recommended_minimum_navigation_information) | Recommended Minimum Navigation Information | | |
67 | | [RMC](http://aprs.gids.nl/nmea/#rmc) | Recommended Minimum Specific GPS/Transit data | | |
68 | | [ROT](https://gpsd.gitlab.io/gpsd/NMEA.html#_rot_rate_of_turn) | Rate of turn | | |
69 | | [RPM](https://gpsd.gitlab.io/gpsd/NMEA.html#_rpm_revolutions) | Engine or Shaft revolutions and pitch | | |
70 | | [RSA](https://gpsd.gitlab.io/gpsd/NMEA.html#_rsa_rudder_sensor_angle) | Rudder Sensor Angle | | |
71 | | [RSD](https://gpsd.gitlab.io/gpsd/NMEA.html#_rsd_radar_system_data) | RADAR System Data | | |
72 | | [RTE](http://aprs.gids.nl/nmea/#rte) | Route | | |
73 | | [THS](http://www.nuovamarea.net/pytheas_9.html) | Actual vessel heading in degrees True and status | | |
74 | | [TLL](https://gpsd.gitlab.io/gpsd/NMEA.html#_tll_target_latitude_and_longitude) | Target latitude and longitude | | |
75 | | [TTM](https://gpsd.gitlab.io/gpsd/NMEA.html#_ttm_tracked_target_message) | Tracked Target Message | | |
76 | | [TXT](https://www.nmea.org/Assets/20160520%20txt%20amendment.pdf) | Sentence is for the transmission of text messages | | |
77 | | [VBW](https://gpsd.gitlab.io/gpsd/NMEA.html#_vbw_dual_groundwater_speed) | Dual Ground/Water Speed | | |
78 | | [VDM/VDO](https://gpsd.gitlab.io/gpsd/AIVDM.html) | Encapsulated binary payload (commonly used with AIS data) | | |
79 | | [VDR](https://gpsd.gitlab.io/gpsd/NMEA.html#_vdr_set_and_drift) | Set and Drift | | |
80 | | [VHW](https://www.tronico.fi/OH6NT/docs/NMEA0183.pdf) | Water Speed and Heading | | |
81 | | [VLW](https://gpsd.gitlab.io/gpsd/NMEA.html#_vlw_distance_traveled_through_water) | Distance Traveled through Water | | |
82 | | [VPW](https://gpsd.gitlab.io/gpsd/NMEA.html#_vpw_speed_measured_parallel_to_wind) | Speed Measured Parallel to Wind | | |
83 | | [VTG](http://aprs.gids.nl/nmea/#vtg) | Track Made Good and Ground Speed | | |
84 | | [VWR](https://gpsd.gitlab.io/gpsd/NMEA.html#_vwr_relative_wind_speed_and_angle) | Relative Wind Speed and Angle | | |
85 | | [VWT](./vwt.go) | True Wind Speed and Angle | | |
86 | | [WPL](http://aprs.gids.nl/nmea/#wpl) | Waypoint location | | |
87 | | [XDR](https://gpsd.gitlab.io/gpsd/NMEA.html#_xdr_transducer_measurement) | Transducer Measurement | | |
88 | | [ZDA](http://aprs.gids.nl/nmea/#zda) | Date & time data | | |
89 | ||
90 | | Proprietary sentence type | Description | | |
91 | |-------------------------------------------------------------|-------------------------------------------------------------------------------------------------| | |
92 | | [PGRME](http://aprs.gids.nl/nmea/#rme) | Estimated Position Error (Garmin proprietary sentence) | | |
93 | | [PHTRO](#) | Vessel pitch and roll (Xsens IMU/VRU/AHRS) | | |
94 | | [PMTK](https://www.rhydolabz.com/documents/25/PMTK_A11.pdf) | Messages for setting and reading commands for MediaTek gps modules. | | |
95 | | [PRDID](#) | Vessel pitch, roll and heading (Xsens IMU/VRU/AHRS) | | |
96 | | [PSONCMS](#) | Quaternion, acceleration, rate of turn, magnetic field, sensor temperature (Xsens IMU/VRU/AHRS) | | |
97 | ||
98 | If you need to parse a message that contains an unsupported sentence type you can implement and register your own | |
99 | message parser and get yourself unblocked immediately. Check the example below to know how | |
100 | to [implement and register a custom message parser](#custom-message-parsing). However, if you think your custom message | |
101 | parser could be beneficial to other users we encourage you to contribute back to the library by submitting a PR and get | |
102 | it included in the list of supported sentences. | |
57 | 103 | |
58 | 104 | ## Examples |
59 | 105 | |
146 | 192 | |
147 | 193 | ### Custom message parsing |
148 | 194 | |
149 | If you need to parse a message not supported by the library you can implement your own message parsing. | |
150 | The following example implements a parser for the hypothetical XYZ NMEA sentence type. | |
195 | If you need to parse a message not supported by the library you can implement your own message parsing. The following | |
196 | example implements a parser for the hypothetical XYZ NMEA sentence type. | |
151 | 197 | |
152 | 198 | ```go |
153 | 199 | package main |
192 | 238 | panic(err) |
193 | 239 | } |
194 | 240 | |
195 | m, ok := s.(XYZType) | |
196 | if !ok { | |
197 | panic("Could not parse type XYZ") | |
198 | } | |
199 | ||
200 | fmt.Printf("Raw sentence: %v\n", m) | |
201 | fmt.Printf("Time: %s\n", m.Time) | |
202 | fmt.Printf("Label: %s\n", m.Label) | |
203 | fmt.Printf("Counter: %d\n", m.Counter) | |
204 | fmt.Printf("Value: %f\n", m.Value) | |
241 | switch m := s.(type) { | |
242 | case XYZType: | |
243 | fmt.Printf("Raw sentence: %v\n", m) | |
244 | fmt.Printf("Time: %s\n", m.Time) | |
245 | fmt.Printf("Label: %s\n", m.Label) | |
246 | fmt.Printf("Counter: %d\n", m.Counter) | |
247 | fmt.Printf("Value: %f\n", m.Value) | |
248 | default: | |
249 | panic("Could not parse XYZ sentence") | |
250 | } | |
205 | 251 | } |
206 | 252 | ``` |
207 | 253 | |
219 | 265 | |
220 | 266 | ## Contributing |
221 | 267 | |
222 | 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. | |
268 | Please feel free to submit issues or fork the repository and send pull requests to update the library and fix bugs, | |
269 | implement support for new sentence types, refactor code, etc. | |
223 | 270 | |
224 | 271 | ## License |
225 | 272 |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypeAAM type of AAM sentence for Waypoint Arrival Alarm | |
4 | TypeAAM = "AAM" | |
5 | ) | |
6 | ||
7 | // AAM - Waypoint Arrival Alarm | |
8 | // This sentence is generated by some units to indicate the status of arrival (entering the arrival circle, or passing | |
9 | // the perpendicular of the course line) at the destination waypoint (source: GPSD). | |
10 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_aam_waypoint_arrival_alarm | |
11 | // | |
12 | // Format: $--AAM,A,A,x.x,N,c--c*hh<CR><LF> | |
13 | // Example: $GPAAM,A,A,0.10,N,WPTNME*43 | |
14 | type AAM struct { | |
15 | BaseSentence | |
16 | // StatusArrivalCircleEntered is warning of arrival to waypoint circle | |
17 | // * A = Arrival Circle Entered | |
18 | // * V = not entered | |
19 | StatusArrivalCircleEntered string | |
20 | ||
21 | // StatusPerpendicularPassed is warning for perpendicular passing of waypoint | |
22 | // * A = Perpendicular passed at waypoint | |
23 | // * V = not passed | |
24 | StatusPerpendicularPassed string | |
25 | ||
26 | // ArrivalCircleRadius is radius for arrival circle | |
27 | ArrivalCircleRadius float64 | |
28 | ||
29 | // ArrivalCircleRadiusUnit is unit for arrival circle radius | |
30 | ArrivalCircleRadiusUnit string | |
31 | ||
32 | // DestinationWaypointID is destination waypoint ID | |
33 | DestinationWaypointID string | |
34 | } | |
35 | ||
36 | // newAAM constructor | |
37 | func newAAM(s BaseSentence) (AAM, error) { | |
38 | p := NewParser(s) | |
39 | p.AssertType(TypeAAM) | |
40 | return AAM{ | |
41 | BaseSentence: s, | |
42 | StatusArrivalCircleEntered: p.EnumString(0, "arrival circle entered status", WPStatusArrivalCircleEnteredA, WPStatusArrivalCircleEnteredV), | |
43 | StatusPerpendicularPassed: p.EnumString(1, "perpendicularly passed status", WPStatusPerpendicularPassedA, WPStatusPerpendicularPassedV), | |
44 | ArrivalCircleRadius: p.Float64(2, "arrival circle radius"), | |
45 | ArrivalCircleRadiusUnit: p.EnumString(3, "arrival circle radius units", DistanceUnitKilometre, DistanceUnitNauticalMile, DistanceUnitStatuteMile, DistanceUnitMetre), | |
46 | DestinationWaypointID: p.String(4, "destination waypoint ID"), | |
47 | }, p.Err() | |
48 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestAAM(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg AAM | |
13 | }{ | |
14 | { | |
15 | name: "good sentence", | |
16 | raw: "$GPAAM,A,A,0.10,N,WPTNME*32", | |
17 | msg: AAM{ | |
18 | StatusArrivalCircleEntered: WPStatusArrivalCircleEnteredA, | |
19 | StatusPerpendicularPassed: WPStatusPerpendicularPassedA, | |
20 | ArrivalCircleRadius: 0.1, | |
21 | ArrivalCircleRadiusUnit: DistanceUnitNauticalMile, | |
22 | DestinationWaypointID: "WPTNME", | |
23 | }, | |
24 | }, | |
25 | { | |
26 | name: "invalid nmea: StatusArrivalCircleEntered", | |
27 | raw: "$GPAAM,x,A,0.10,N,WPTNME*0B", | |
28 | err: "nmea: GPAAM invalid arrival circle entered status: x", | |
29 | }, | |
30 | { | |
31 | name: "invalid nmea: StatusPerpendicularPassed", | |
32 | raw: "$GPAAM,A,x,0.10,N,WPTNME*0B", | |
33 | err: "nmea: GPAAM invalid perpendicularly passed status: x", | |
34 | }, | |
35 | { | |
36 | name: "invalid nmea: DistanceUnitNauticalMile", | |
37 | raw: "$GPAAM,A,A,0.10,x,WPTNME*04", | |
38 | err: "nmea: GPAAM invalid arrival circle radius units: x", | |
39 | }, | |
40 | } | |
41 | for _, tt := range tests { | |
42 | t.Run(tt.name, func(t *testing.T) { | |
43 | m, err := Parse(tt.raw) | |
44 | if tt.err != "" { | |
45 | assert.Error(t, err) | |
46 | assert.EqualError(t, err, tt.err) | |
47 | } else { | |
48 | assert.NoError(t, err) | |
49 | aam := m.(AAM) | |
50 | aam.BaseSentence = BaseSentence{} | |
51 | assert.Equal(t, tt.msg, aam) | |
52 | } | |
53 | }) | |
54 | } | |
55 | } |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypeALA type of ALA sentence for System Faults and alarms | |
4 | TypeALA = "ALA" | |
5 | ) | |
6 | ||
7 | // ALA - System Faults and alarms | |
8 | // Source: "Interfacing Voyage Data Recorder Systems, AutroSafe Interactive Fire-Alarm System, 116-P-BSL336/EE, RevA 2007-01-25, | |
9 | // Autronica Fire and Security AS " (page 31 | p.8.1.3) | |
10 | // https://product.autronicafire.com/fileshare/fileupload/14251/bsl336_ee.pdf | |
11 | // | |
12 | // Format: $FRALA,hhmmss,aa,aa,xx,xxx,a,a,c-cc*hh<CR><LF> | |
13 | // Example: $FRALA,143955,FR,OT,00,901,N,V,Syst Fault : AutroSafe comm. OK*4F | |
14 | type ALA struct { | |
15 | BaseSentence | |
16 | ||
17 | // Time is Event Time | |
18 | Time Time | |
19 | ||
20 | // SystemIndicator is system indicator of original alarm source. Detector system type with 2 char identifier. | |
21 | // Values not known | |
22 | // https://www.nmea.org/Assets/20190303%20nmea%200183%20talker%20identifier%20mnemonics.pdf | |
23 | SystemIndicator string | |
24 | ||
25 | // SubSystemIndicator is sub system equipment indicator of original alarm source | |
26 | SubSystemIndicator string | |
27 | ||
28 | // InstanceNumber is instance number of equipment/unit/item (00-99) | |
29 | InstanceNumber int64 | |
30 | ||
31 | // Type is alarm type (000-999) | |
32 | Type int64 | |
33 | ||
34 | // Condition describes the condition triggering current message | |
35 | // * N – Normal state (OK) | |
36 | // * H - Alarm state (fault); | |
37 | // could be more | |
38 | Condition string | |
39 | ||
40 | // AlarmAckState is Alarm's acknowledge state | |
41 | // * A – Acknowledged | |
42 | // * H - Harbour mode | |
43 | // * V – Not acknowledged | |
44 | // * O - Override | |
45 | // could be more | |
46 | AlarmAckState string | |
47 | ||
48 | // Message's description text (could be cut to fit max packet length) | |
49 | Message string | |
50 | } | |
51 | ||
52 | // newALA constructor | |
53 | func newALA(s BaseSentence) (ALA, error) { | |
54 | p := NewParser(s) | |
55 | p.AssertType(TypeALA) | |
56 | return ALA{ | |
57 | BaseSentence: s, | |
58 | Time: p.Time(0, "time"), | |
59 | SystemIndicator: p.String(1, "system indicator"), | |
60 | SubSystemIndicator: p.String(2, "subsystem indicator"), | |
61 | InstanceNumber: p.Int64(3, "instance number"), | |
62 | Type: p.Int64(4, "type"), | |
63 | Condition: p.String(5, "condition"), // string as there could be more | |
64 | AlarmAckState: p.String(6, "alarm acknowledgement state"), // string as there could be more | |
65 | Message: p.String(7, "message"), | |
66 | }, p.Err() | |
67 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestALA(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg ALA | |
13 | }{ | |
14 | { | |
15 | name: "good sentence", | |
16 | raw: "$FRALA,143955,FR,OT,00,901,N,V,Syst Fault : AutroSafe comm. OK*4F", | |
17 | msg: ALA{ | |
18 | Time: Time{ | |
19 | Valid: true, | |
20 | Hour: 14, | |
21 | Minute: 39, | |
22 | Second: 55, | |
23 | Millisecond: 0, | |
24 | }, | |
25 | SystemIndicator: "FR", | |
26 | SubSystemIndicator: "OT", | |
27 | InstanceNumber: 0, | |
28 | Type: 901, | |
29 | Condition: "N", | |
30 | AlarmAckState: "V", | |
31 | Message: "Syst Fault : AutroSafe comm. OK", | |
32 | }, | |
33 | }, | |
34 | { | |
35 | name: "invalid nmea: Time", | |
36 | raw: "$FRALA,1x3955,FR,OT,00,901,N,V,Syst Fault : AutroSafe comm. OK*03", | |
37 | err: "nmea: FRALA invalid time: 1x3955", | |
38 | }, | |
39 | { | |
40 | name: "invalid nmea: InstanceNumber", | |
41 | raw: "$FRALA,143955,FR,OT,x0,901,N,V,Syst Fault : AutroSafe comm. OK*07", | |
42 | err: "nmea: FRALA invalid instance number: x0", | |
43 | }, | |
44 | { | |
45 | name: "invalid nmea: Type", | |
46 | raw: "$FRALA,143955,FR,OT,00,9x1,N,V,Syst Fault : AutroSafe comm. OK*07", | |
47 | err: "nmea: FRALA invalid type: 9x1", | |
48 | }, | |
49 | } | |
50 | for _, tt := range tests { | |
51 | t.Run(tt.name, func(t *testing.T) { | |
52 | m, err := Parse(tt.raw) | |
53 | if tt.err != "" { | |
54 | assert.Error(t, err) | |
55 | assert.EqualError(t, err, tt.err) | |
56 | } else { | |
57 | assert.NoError(t, err) | |
58 | ala := m.(ALA) | |
59 | ala.BaseSentence = BaseSentence{} | |
60 | assert.Equal(t, tt.msg, ala) | |
61 | } | |
62 | }) | |
63 | } | |
64 | } |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypeAPB type of APB sentence for Autopilot Sentence "B" | |
4 | TypeAPB = "APB" | |
5 | ||
6 | // StatusWarningASetAPB indicates LORAN-C Blink or SNR warning | |
7 | StatusWarningASetAPB = "V" | |
8 | // StatusWarningAClearORNotUsedAPB general warning flag or other navigation systems when a reliable fix is not available | |
9 | StatusWarningAClearORNotUsedAPB = "A" | |
10 | ||
11 | // StatusWarningBSetAPB means Loran-C Cycle Lock warning OK or not used | |
12 | StatusWarningBSetAPB = "A" | |
13 | // StatusWarningBClearAPB means Loran-C Cycle Lock warning flag | |
14 | StatusWarningBClearAPB = "V" | |
15 | ) | |
16 | ||
17 | // Autopilot related constants (used in APB, APA, AAM) | |
18 | const ( | |
19 | // WPStatusPerpendicularPassedA is warning for passing the perpendicular of the course line of waypoint | |
20 | WPStatusPerpendicularPassedA = "A" | |
21 | // WPStatusPerpendicularPassedV indicates for not passing of the perpendicular of the course line of waypoint | |
22 | WPStatusPerpendicularPassedV = "V" | |
23 | ||
24 | // WPStatusArrivalCircleEnteredA is warning of entering to waypoint circle | |
25 | WPStatusArrivalCircleEnteredA = "A" | |
26 | // WPStatusArrivalCircleEnteredV indicates of not yet entered into waypoint circle | |
27 | WPStatusArrivalCircleEnteredV = "V" | |
28 | ) | |
29 | ||
30 | // APB - Autopilot Sentence "B" for heading/tracking | |
31 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_apb_autopilot_sentence_b | |
32 | // https://www.tronico.fi/OH6NT/docs/NMEA0183.pdf (page 5) | |
33 | // | |
34 | // Format: $--APB,A,A,x.x,a,N,A,A,x.x,a,c--c,x.x,a,x.x,a*hh<CR><LF> | |
35 | // 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<CR><LF> | |
36 | // Example: $GPAPB,A,A,0.10,R,N,V,V,011,M,DEST,011,M,011,M*82 | |
37 | // $ECAPB,A,A,0.0,L,M,V,V,175.2,T,Antechamber_Bay,175.2,T,175.2,T*48 | |
38 | type APB struct { | |
39 | BaseSentence | |
40 | ||
41 | // StatusGeneralWarning is used for warnings | |
42 | // * V = LORAN-C Blink or SNR warning | |
43 | // * A = general warning flag or other navigation systems when a reliable fix is not available | |
44 | StatusGeneralWarning string | |
45 | ||
46 | // StatusLockWarning is used for lock warning | |
47 | // * V = Loran-C Cycle Lock warning flag | |
48 | // * A = OK or not used | |
49 | StatusLockWarning string | |
50 | ||
51 | // CrossTrackErrorMagnitude is Cross Track Error Magnitude | |
52 | CrossTrackErrorMagnitude float64 | |
53 | ||
54 | // DirectionToSteer is Direction to steer, | |
55 | // * L = left | |
56 | // * R = right | |
57 | DirectionToSteer string | |
58 | ||
59 | // CrossTrackUnits is cross track units | |
60 | // * N = nautical miles | |
61 | // * K = for kilometers | |
62 | CrossTrackUnits string | |
63 | ||
64 | // StatusArrivalCircleEntered is warning of arrival to waypoint circle | |
65 | // * A = Arrival Circle Entered | |
66 | // * V = not entered | |
67 | StatusArrivalCircleEntered string | |
68 | ||
69 | // StatusPerpendicularPassed is warning for perpendicular passing of waypoint | |
70 | // * A = Perpendicular passed at waypoint | |
71 | // * V = not passed | |
72 | StatusPerpendicularPassed string | |
73 | ||
74 | // BearingOriginToDest is Bearing origin to destination | |
75 | BearingOriginToDest float64 | |
76 | ||
77 | // BearingOriginToDestType is Bearing origin to dest type | |
78 | // * M = Magnetic | |
79 | // * T = True | |
80 | BearingOriginToDestType string | |
81 | ||
82 | // DestinationWaypointID is Destination waypoint ID | |
83 | DestinationWaypointID string | |
84 | ||
85 | // BearingPresentToDest is Bearing, present position to Destination | |
86 | BearingPresentToDest float64 | |
87 | ||
88 | // BearingPresentToDestType is Bearing present to dest type | |
89 | // * M = Magnetic | |
90 | // * T = True | |
91 | BearingPresentToDestType string | |
92 | ||
93 | // Heading is heading to steer to destination waypoint | |
94 | Heading float64 | |
95 | ||
96 | // HeadingType is Heading type | |
97 | // * M = Magnetic | |
98 | // * T = True | |
99 | HeadingType string | |
100 | ||
101 | // FAA mode indicator (filled in NMEA 2.3 and later) | |
102 | FFAMode string | |
103 | } | |
104 | ||
105 | // newAPB constructor | |
106 | func newAPB(s BaseSentence) (APB, error) { | |
107 | p := NewParser(s) | |
108 | p.AssertType(TypeAPB) | |
109 | apb := APB{ | |
110 | BaseSentence: s, | |
111 | StatusGeneralWarning: p.EnumString(0, "general warning", StatusWarningAClearORNotUsedAPB, StatusWarningASetAPB), | |
112 | StatusLockWarning: p.EnumString(1, "lock warning", StatusWarningBSetAPB, StatusWarningBClearAPB), | |
113 | CrossTrackErrorMagnitude: p.Float64(2, "cross track error magnitude"), | |
114 | DirectionToSteer: p.EnumString(3, "direction to steer", Left, Right), | |
115 | CrossTrackUnits: p.EnumString(4, "cross track units", DistanceUnitKilometre, DistanceUnitNauticalMile, DistanceUnitStatuteMile, DistanceUnitMetre), | |
116 | StatusArrivalCircleEntered: p.EnumString(5, "arrival circle entered status", WPStatusArrivalCircleEnteredA, WPStatusArrivalCircleEnteredV), | |
117 | StatusPerpendicularPassed: p.EnumString(6, "perpendicularly passed status", WPStatusPerpendicularPassedA, WPStatusPerpendicularPassedV), | |
118 | BearingOriginToDest: p.Float64(7, "origin bearing to destination"), | |
119 | BearingOriginToDestType: p.EnumString(8, "origin bearing to destination type", HeadingMagnetic, HeadingTrue), | |
120 | DestinationWaypointID: p.String(9, "destination waypoint ID"), | |
121 | BearingPresentToDest: p.Float64(10, "present bearing to destination"), | |
122 | BearingPresentToDestType: p.EnumString(11, "present bearing to destination type", HeadingMagnetic, HeadingTrue), | |
123 | Heading: p.Float64(12, "heading"), | |
124 | HeadingType: p.EnumString(13, "heading type", HeadingMagnetic, HeadingTrue), | |
125 | } | |
126 | if len(p.Fields) > 14 { | |
127 | apb.FFAMode = p.String(14, "FAA mode") // not enum because some devices have proprietary "non-nmea" values | |
128 | } | |
129 | return apb, p.Err() | |
130 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestAPB(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg APB | |
13 | }{ | |
14 | { | |
15 | name: "good sentence", | |
16 | raw: "$GPAPB,A,A,0.10,R,N,V,V,011,M,DEST,011,M,011,M*3C", | |
17 | msg: APB{ | |
18 | StatusGeneralWarning: "A", | |
19 | StatusLockWarning: "A", | |
20 | CrossTrackErrorMagnitude: 0.1, | |
21 | DirectionToSteer: "R", | |
22 | CrossTrackUnits: "N", | |
23 | StatusArrivalCircleEntered: "V", | |
24 | StatusPerpendicularPassed: "V", | |
25 | BearingOriginToDest: 11, | |
26 | BearingOriginToDestType: "M", | |
27 | DestinationWaypointID: "DEST", | |
28 | BearingPresentToDest: 11, | |
29 | BearingPresentToDestType: "M", | |
30 | Heading: 11, | |
31 | HeadingType: "M", | |
32 | FFAMode: "", | |
33 | }, | |
34 | }, | |
35 | { | |
36 | name: "good sentence b with FAA mode", | |
37 | raw: "$ECAPB,A,A,0.0,L,M,V,V,175.2,T,Antechamber_Bay,175.2,T,175.2,T,V*32", | |
38 | msg: APB{ | |
39 | StatusGeneralWarning: "A", | |
40 | StatusLockWarning: "A", | |
41 | CrossTrackErrorMagnitude: 0, | |
42 | DirectionToSteer: "L", | |
43 | CrossTrackUnits: "M", | |
44 | StatusArrivalCircleEntered: "V", | |
45 | StatusPerpendicularPassed: "V", | |
46 | BearingOriginToDest: 175.2, | |
47 | BearingOriginToDestType: "T", | |
48 | DestinationWaypointID: "Antechamber_Bay", | |
49 | BearingPresentToDest: 175.2, | |
50 | BearingPresentToDestType: "T", | |
51 | Heading: 175.2, | |
52 | HeadingType: "T", | |
53 | FFAMode: "V", | |
54 | }, | |
55 | }, | |
56 | { | |
57 | name: "invalid nmea: CrossTrackErrorMagnitude", | |
58 | raw: "$ECAPB,A,A,x.0,L,M,V,V,175.2,T,Antechamber_Bay,175.2,T,175.2,T,V*7A", | |
59 | err: "nmea: ECAPB invalid cross track error magnitude: x.0", | |
60 | }, | |
61 | { | |
62 | name: "invalid nmea: BearingOriginToDest", | |
63 | raw: "$ECAPB,A,A,0.0,L,M,V,V,175.x,T,Antechamber_Bay,175.2,T,175.2,T,V*78", | |
64 | err: "nmea: ECAPB invalid origin bearing to destination: 175.x", | |
65 | }, | |
66 | { | |
67 | name: "invalid nmea: BearingPresentToDest", | |
68 | raw: "$ECAPB,A,A,0.0,L,M,V,V,175.2,T,Antechamber_Bay,175.x,T,175.2,T,V*78", | |
69 | err: "nmea: ECAPB invalid present bearing to destination: 175.x", | |
70 | }, | |
71 | { | |
72 | name: "invalid nmea: Heading", | |
73 | raw: "$ECAPB,A,A,0.0,L,M,V,V,175.2,T,Antechamber_Bay,175.2,T,175.x,T,V*78", | |
74 | err: "nmea: ECAPB invalid heading: 175.x", | |
75 | }, | |
76 | } | |
77 | for _, tt := range tests { | |
78 | t.Run(tt.name, func(t *testing.T) { | |
79 | m, err := Parse(tt.raw) | |
80 | if tt.err != "" { | |
81 | assert.Error(t, err) | |
82 | assert.EqualError(t, err, tt.err) | |
83 | } else { | |
84 | assert.NoError(t, err) | |
85 | apb := m.(APB) | |
86 | apb.BaseSentence = BaseSentence{} | |
87 | assert.Equal(t, tt.msg, apb) | |
88 | } | |
89 | }) | |
90 | } | |
91 | } |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypeBEC type of BEC sentence for bearing and distance to waypoint (dead reckoning) | |
4 | TypeBEC = "BEC" | |
5 | ) | |
6 | ||
7 | // BEC - bearing and distance to waypoint (dead reckoning) | |
8 | // http://www.nmea.de/nmea0183datensaetze.html#bec | |
9 | // https://www.eye4software.com/hydromagic/documentation/nmea0183/ | |
10 | // | |
11 | // Format: $--BEC,hhmmss.ss,llll.ll,a,yyyyy.yy,a,x.x,T,x.x,M,x.x,N,c--c*hh<CR><LF> | |
12 | // Example: $GPBEC,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*33 | |
13 | type BEC struct { | |
14 | BaseSentence | |
15 | Time Time // UTC Time | |
16 | Latitude float64 // latitude of waypoint | |
17 | Longitude float64 // longitude of waypoint | |
18 | BearingTrue float64 // true bearing in degrees | |
19 | BearingTrueValid bool // is unit of true bearing valid | |
20 | BearingMagnetic float64 // magnetic bearing in degrees | |
21 | BearingMagneticValid bool // is unit of magnetic bearing valid | |
22 | DistanceNauticalMiles float64 // distance to waypoint in nautical miles | |
23 | DistanceNauticalMilesValid bool // is unit of distance to waypoint nautical miles valid | |
24 | DestinationWaypointID string // destination waypoint ID | |
25 | } | |
26 | ||
27 | // newBEC constructor | |
28 | func newBEC(s BaseSentence) (BEC, error) { | |
29 | p := NewParser(s) | |
30 | p.AssertType(TypeBEC) | |
31 | return BEC{ | |
32 | BaseSentence: s, | |
33 | Time: p.Time(0, "time"), | |
34 | Latitude: p.LatLong(1, 2, "latitude"), | |
35 | Longitude: p.LatLong(3, 4, "longitude"), | |
36 | BearingTrue: p.Float64(5, "true bearing"), | |
37 | BearingTrueValid: p.EnumString(6, "true bearing unit valid", BearingTrue) == BearingTrue, | |
38 | BearingMagnetic: p.Float64(7, "magnetic bearing"), | |
39 | BearingMagneticValid: p.EnumString(8, "magnetic bearing unit valid", BearingMagnetic) == BearingMagnetic, | |
40 | DistanceNauticalMiles: p.Float64(9, "distance to waypoint is nautical miles"), | |
41 | DistanceNauticalMilesValid: p.EnumString(10, "is distance to waypoint nautical miles valid", DistanceUnitNauticalMile) == DistanceUnitNauticalMile, | |
42 | DestinationWaypointID: p.String(11, "destination waypoint ID"), | |
43 | }, p.Err() | |
44 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestBEC(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg BEC | |
13 | }{ | |
14 | { | |
15 | name: "good sentence", | |
16 | raw: "$GPBEC,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*33", | |
17 | msg: BEC{ | |
18 | Time: Time{ | |
19 | Valid: true, | |
20 | Hour: 22, | |
21 | Minute: 5, | |
22 | Second: 16, | |
23 | Millisecond: 0, | |
24 | }, | |
25 | Latitude: 51.50033333333334, | |
26 | Longitude: -0.7723333333333334, | |
27 | BearingTrue: 213.8, | |
28 | BearingTrueValid: true, | |
29 | BearingMagnetic: 218, | |
30 | BearingMagneticValid: true, | |
31 | DistanceNauticalMiles: 4.6, | |
32 | DistanceNauticalMilesValid: true, | |
33 | DestinationWaypointID: "EGLM", | |
34 | }, | |
35 | }, | |
36 | { | |
37 | name: "invalid nmea: Time", | |
38 | raw: "$GPBEC,2x0516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*79", | |
39 | err: "nmea: GPBEC invalid time: 2x0516", | |
40 | }, | |
41 | { | |
42 | name: "invalid nmea: BearingTrueValid", | |
43 | raw: "$GPBEC,220516,5130.02,N,00046.34,W,213.8,M,218.0,M,0004.6,N,EGLM*2A", | |
44 | err: "nmea: GPBEC invalid true bearing unit valid: M", | |
45 | }, | |
46 | { | |
47 | name: "invalid nmea: BearingMagneticValid", | |
48 | raw: "$GPBEC,220516,5130.02,N,00046.34,W,213.8,T,218.0,T,0004.6,N,EGLM*2A", | |
49 | err: "nmea: GPBEC invalid magnetic bearing unit valid: T", | |
50 | }, | |
51 | { | |
52 | name: "invalid nmea: DistanceNauticalMilesValid", | |
53 | raw: "$GPBEC,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,K,EGLM*36", | |
54 | err: "nmea: GPBEC invalid is distance to waypoint nautical miles valid: K", | |
55 | }, | |
56 | } | |
57 | for _, tt := range tests { | |
58 | t.Run(tt.name, func(t *testing.T) { | |
59 | m, err := Parse(tt.raw) | |
60 | if tt.err != "" { | |
61 | assert.Error(t, err) | |
62 | assert.EqualError(t, err, tt.err) | |
63 | } else { | |
64 | assert.NoError(t, err) | |
65 | bec := m.(BEC) | |
66 | bec.BaseSentence = BaseSentence{} | |
67 | assert.Equal(t, tt.msg, bec) | |
68 | } | |
69 | }) | |
70 | } | |
71 | } |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypeBOD type of BOD sentence for bearing waypoint to waypoint | |
4 | TypeBOD = "BOD" | |
5 | ) | |
6 | ||
7 | // BOD - bearing waypoint to waypoint (origin to destination). | |
8 | // Replaced by BWW in NMEA4+ (according to GPSD docs) | |
9 | // If your system supports RMB it is better to use RMB as it is more common (according to OpenCPN docs) | |
10 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_bod_bearing_waypoint_to_waypoint | |
11 | // | |
12 | // Format: $--BOD,x.x,T,x.x,M,c--c,c--c*hh<CR><LF> | |
13 | // Example: $GPBOD,099.3,T,105.6,M,POINTB*64 | |
14 | // $GPBOD,097.0,T,103.2,M,POINTB,POINTA*4A | |
15 | type BOD struct { | |
16 | BaseSentence | |
17 | BearingTrue float64 // true bearing in degrees | |
18 | BearingTrueType string // is type of true bearing | |
19 | BearingMagnetic float64 // magnetic bearing in degrees | |
20 | BearingMagneticType string // is type of magnetic bearing | |
21 | DestinationWaypointID string // destination waypoint ID | |
22 | OriginWaypointID string // origin waypoint ID | |
23 | } | |
24 | ||
25 | // newBOD constructor | |
26 | func newBOD(s BaseSentence) (BOD, error) { | |
27 | p := NewParser(s) | |
28 | p.AssertType(TypeBOD) | |
29 | bod := BOD{ | |
30 | BaseSentence: s, | |
31 | BearingTrue: p.Float64(0, "true bearing"), | |
32 | BearingTrueType: p.EnumString(1, "true bearing type", BearingTrue), | |
33 | BearingMagnetic: p.Float64(2, "magnetic bearing"), | |
34 | BearingMagneticType: p.EnumString(3, "magnetic bearing type", BearingMagnetic), | |
35 | DestinationWaypointID: p.String(4, "destination waypoint ID"), | |
36 | OriginWaypointID: "", | |
37 | } | |
38 | // According to GSPD docs: OriginWaypointID is not transmitted in the GOTO mode, without an active route on your GPS. | |
39 | // in that case you have only DestinationWaypointID | |
40 | if len(p.Fields) > 5 { | |
41 | bod.OriginWaypointID = p.String(5, "origin waypoint ID") | |
42 | } | |
43 | return bod, p.Err() | |
44 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestBOD(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg BOD | |
13 | }{ | |
14 | { | |
15 | name: "good sentence with both WPs", | |
16 | raw: "$GPBOD,097.0,T,103.2,M,POINTB,POINTA*4A", | |
17 | msg: BOD{ | |
18 | BearingTrue: 97.0, | |
19 | BearingTrueType: BearingTrue, | |
20 | BearingMagnetic: 103.2, | |
21 | BearingMagneticType: BearingMagnetic, | |
22 | DestinationWaypointID: "POINTB", | |
23 | OriginWaypointID: "POINTA", | |
24 | }, | |
25 | }, | |
26 | { | |
27 | name: "good sentence onyl destination", | |
28 | raw: "$GPBOD,099.3,T,105.6,M,POINTB*64", | |
29 | msg: BOD{ | |
30 | BearingTrue: 99.3, | |
31 | BearingTrueType: BearingTrue, | |
32 | BearingMagnetic: 105.6, | |
33 | BearingMagneticType: BearingMagnetic, | |
34 | DestinationWaypointID: "POINTB", | |
35 | OriginWaypointID: "", | |
36 | }, | |
37 | }, | |
38 | { | |
39 | name: "invalid nmea: BearingTrueValid", | |
40 | raw: "$GPBOD,097.0,M,103.2,M,POINTB,POINTA*53", | |
41 | err: "nmea: GPBOD invalid true bearing type: M", | |
42 | }, | |
43 | { | |
44 | name: "invalid nmea: BearingMagneticValid", | |
45 | raw: "$GPBOD,097.0,T,103.2,T,POINTB,POINTA*53", | |
46 | err: "nmea: GPBOD invalid magnetic bearing type: T", | |
47 | }, | |
48 | } | |
49 | for _, tt := range tests { | |
50 | t.Run(tt.name, func(t *testing.T) { | |
51 | m, err := Parse(tt.raw) | |
52 | if tt.err != "" { | |
53 | assert.Error(t, err) | |
54 | assert.EqualError(t, err, tt.err) | |
55 | } else { | |
56 | assert.NoError(t, err) | |
57 | bod := m.(BOD) | |
58 | bod.BaseSentence = BaseSentence{} | |
59 | assert.Equal(t, tt.msg, bod) | |
60 | } | |
61 | }) | |
62 | } | |
63 | } |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypeBWC type of BWC sentence for bearing and distance to waypoint, great circle | |
4 | TypeBWC = "BWC" | |
5 | ) | |
6 | ||
7 | // BWC - bearing and distance to waypoint, great circle | |
8 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_bwc_bearing_distance_to_waypoint_great_circle | |
9 | // http://aprs.gids.nl/nmea/#bwc | |
10 | // | |
11 | // Format: $--BWC,hhmmss.ss,llll.ll,a,yyyyy.yy,a,x.x,T,x.x,M,x.x,N,c--c*hh<CR><LF> | |
12 | // 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<CR><LF> | |
13 | // Example: $GPBWC,081837,,,,,,T,,M,,N,*13 | |
14 | // $GPBWC,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*21 | |
15 | type BWC struct { | |
16 | BaseSentence | |
17 | Time Time // UTC Time | |
18 | Latitude float64 // latitude of waypoint | |
19 | Longitude float64 // longitude of waypoint | |
20 | BearingTrue float64 // true bearing in degrees | |
21 | BearingTrueType string // is type of true bearing | |
22 | BearingMagnetic float64 // magnetic bearing in degrees | |
23 | BearingMagneticType string // is type of magnetic bearing | |
24 | DistanceNauticalMiles float64 // distance to waypoint in nautical miles | |
25 | DistanceNauticalMilesUnit string // is unit of distance to waypoint nautical miles | |
26 | DestinationWaypointID string // destination waypoint ID | |
27 | FFAMode string // FAA mode indicator (filled in NMEA 2.3 and later) | |
28 | } | |
29 | ||
30 | // newBWC constructor | |
31 | func newBWC(s BaseSentence) (BWC, error) { | |
32 | p := NewParser(s) | |
33 | p.AssertType(TypeBWC) | |
34 | bwc := BWC{ | |
35 | BaseSentence: s, | |
36 | Time: p.Time(0, "time"), | |
37 | Latitude: p.LatLong(1, 2, "latitude"), | |
38 | Longitude: p.LatLong(3, 4, "longitude"), | |
39 | BearingTrue: p.Float64(5, "true bearing"), | |
40 | BearingTrueType: p.EnumString(6, "true bearing type", BearingTrue), | |
41 | BearingMagnetic: p.Float64(7, "magnetic bearing"), | |
42 | BearingMagneticType: p.EnumString(8, "magnetic bearing type", BearingMagnetic), | |
43 | DistanceNauticalMiles: p.Float64(9, "distance to waypoint is nautical miles"), | |
44 | DistanceNauticalMilesUnit: p.EnumString(10, "is distance to waypoint nautical miles unit", DistanceUnitNauticalMile), | |
45 | DestinationWaypointID: p.String(11, "destination waypoint ID"), | |
46 | } | |
47 | if len(p.Fields) > 12 { | |
48 | bwc.FFAMode = p.String(12, "FAA mode") // not enum because some devices have proprietary "non-nmea" values | |
49 | } | |
50 | return bwc, p.Err() | |
51 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestBWC(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg BWC | |
13 | }{ | |
14 | { | |
15 | name: "good sentence", | |
16 | raw: "$GPBWC,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*21", | |
17 | msg: BWC{ | |
18 | Time: Time{ | |
19 | Valid: true, | |
20 | Hour: 22, | |
21 | Minute: 5, | |
22 | Second: 16, | |
23 | Millisecond: 0, | |
24 | }, | |
25 | Latitude: 51.50033333333334, | |
26 | Longitude: -0.7723333333333334, | |
27 | BearingTrue: 213.8, | |
28 | BearingTrueType: BearingTrue, | |
29 | BearingMagnetic: 218, | |
30 | BearingMagneticType: BearingMagnetic, | |
31 | DistanceNauticalMiles: 4.6, | |
32 | DistanceNauticalMilesUnit: DistanceUnitNauticalMile, | |
33 | DestinationWaypointID: "EGLM", | |
34 | FFAMode: "", | |
35 | }, | |
36 | }, | |
37 | { | |
38 | name: "good sentence no waypoint", | |
39 | raw: "$GPBWC,081837,,,,,,T,,M,,N,*13", | |
40 | msg: BWC{ | |
41 | Time: Time{Valid: true, Hour: 8, Minute: 18, Second: 37, Millisecond: 0}, | |
42 | Latitude: 0, | |
43 | Longitude: 0, | |
44 | BearingTrue: 0, | |
45 | BearingTrueType: BearingTrue, | |
46 | BearingMagnetic: 0, | |
47 | BearingMagneticType: BearingMagnetic, | |
48 | DistanceNauticalMiles: 0, | |
49 | DistanceNauticalMilesUnit: DistanceUnitNauticalMile, | |
50 | DestinationWaypointID: "", | |
51 | FFAMode: "", | |
52 | }, | |
53 | }, | |
54 | { | |
55 | name: "good sentence with FAAMode", | |
56 | raw: "$GPBWC,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM,D*49", | |
57 | msg: BWC{ | |
58 | Time: Time{ | |
59 | Valid: true, | |
60 | Hour: 22, | |
61 | Minute: 5, | |
62 | Second: 16, | |
63 | Millisecond: 0, | |
64 | }, | |
65 | Latitude: 51.50033333333334, | |
66 | Longitude: -0.7723333333333334, | |
67 | BearingTrue: 213.8, | |
68 | BearingTrueType: BearingTrue, | |
69 | BearingMagnetic: 218, | |
70 | BearingMagneticType: BearingMagnetic, | |
71 | DistanceNauticalMiles: 4.6, | |
72 | DistanceNauticalMilesUnit: DistanceUnitNauticalMile, | |
73 | DestinationWaypointID: "EGLM", | |
74 | FFAMode: FAAModeDifferential, | |
75 | }, | |
76 | }, | |
77 | { | |
78 | name: "invalid nmea: Time", | |
79 | raw: "$GPBWC,2x0516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*6B", | |
80 | err: "nmea: GPBWC invalid time: 2x0516", | |
81 | }, | |
82 | { | |
83 | name: "invalid nmea: BearingTrueValid", | |
84 | raw: "$GPBWC,220516,5130.02,N,00046.34,W,213.8,M,218.0,M,0004.6,N,EGLM*38", | |
85 | err: "nmea: GPBWC invalid true bearing type: M", | |
86 | }, | |
87 | { | |
88 | name: "invalid nmea: BearingMagneticValid", | |
89 | raw: "$GPBWC,220516,5130.02,N,00046.34,W,213.8,T,218.0,T,0004.6,N,EGLM*38", | |
90 | err: "nmea: GPBWC invalid magnetic bearing type: T", | |
91 | }, | |
92 | { | |
93 | name: "invalid nmea: DistanceNauticalMilesValid", | |
94 | raw: "$GPBWC,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,K,EGLM*24", | |
95 | err: "nmea: GPBWC invalid is distance to waypoint nautical miles unit: K", | |
96 | }, | |
97 | } | |
98 | for _, tt := range tests { | |
99 | t.Run(tt.name, func(t *testing.T) { | |
100 | m, err := Parse(tt.raw) | |
101 | if tt.err != "" { | |
102 | assert.Error(t, err) | |
103 | assert.EqualError(t, err, tt.err) | |
104 | } else { | |
105 | assert.NoError(t, err) | |
106 | bwc := m.(BWC) | |
107 | bwc.BaseSentence = BaseSentence{} | |
108 | assert.Equal(t, tt.msg, bwc) | |
109 | } | |
110 | }) | |
111 | } | |
112 | } |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypeBWR type of BWR sentence for bearing and distance to waypoint (Rhumb Line) | |
4 | TypeBWR = "BWR" | |
5 | ) | |
6 | ||
7 | // BWR - bearing and distance to waypoint (Rhumb Line). This is calculated along rumb line instead of along the great circle. | |
8 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_bwr_bearing_and_distance_to_waypoint_rhumb_line | |
9 | // | |
10 | // Format: $--BWR,hhmmss.ss,llll.ll,a,yyyyy.yy,a,x.x,T,x.x,M,x.x,N,c--c*hh<CR><LF> | |
11 | // 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<CR><LF> | |
12 | // Example: $GPBWR,081837,,,,,,T,,M,,N,*02 | |
13 | // $GPBWR,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*30 | |
14 | type BWR struct { | |
15 | BaseSentence | |
16 | Time Time // UTC Time | |
17 | Latitude float64 // latitude of waypoint | |
18 | Longitude float64 // longitude of waypoint | |
19 | BearingTrue float64 // true bearing in degrees | |
20 | BearingTrueType string // is type of true bearing | |
21 | BearingMagnetic float64 // magnetic bearing in degrees | |
22 | BearingMagneticType string // is type of magnetic bearing | |
23 | DistanceNauticalMiles float64 // distance to waypoint in nautical miles | |
24 | DistanceNauticalMilesUnit string // is unit of distance to waypoint nautical miles | |
25 | DestinationWaypointID string // destination waypoint ID | |
26 | FFAMode string // FAA mode indicator (filled in NMEA 2.3 and later) | |
27 | } | |
28 | ||
29 | // newBWR constructor | |
30 | func newBWR(s BaseSentence) (BWR, error) { | |
31 | p := NewParser(s) | |
32 | p.AssertType(TypeBWR) | |
33 | bwc := BWR{ | |
34 | BaseSentence: s, | |
35 | Time: p.Time(0, "time"), | |
36 | Latitude: p.LatLong(1, 2, "latitude"), | |
37 | Longitude: p.LatLong(3, 4, "longitude"), | |
38 | BearingTrue: p.Float64(5, "true bearing"), | |
39 | BearingTrueType: p.EnumString(6, "true bearing type", BearingTrue), | |
40 | BearingMagnetic: p.Float64(7, "magnetic bearing"), | |
41 | BearingMagneticType: p.EnumString(8, "magnetic bearing type", BearingMagnetic), | |
42 | DistanceNauticalMiles: p.Float64(9, "distance to waypoint is nautical miles"), | |
43 | DistanceNauticalMilesUnit: p.EnumString(10, "is distance to waypoint nautical miles unit", DistanceUnitNauticalMile), | |
44 | DestinationWaypointID: p.String(11, "destination waypoint ID"), | |
45 | } | |
46 | if len(p.Fields) > 12 { | |
47 | bwc.FFAMode = p.String(12, "FAA mode") // not enum because some devices have proprietary "non-nmea" values | |
48 | } | |
49 | return bwc, p.Err() | |
50 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestBWR(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg BWR | |
13 | }{ | |
14 | { | |
15 | name: "good sentence", | |
16 | raw: "$GPBWR,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*30", | |
17 | msg: BWR{ | |
18 | Time: Time{ | |
19 | Valid: true, | |
20 | Hour: 22, | |
21 | Minute: 5, | |
22 | Second: 16, | |
23 | Millisecond: 0, | |
24 | }, | |
25 | Latitude: 51.50033333333334, | |
26 | Longitude: -0.7723333333333334, | |
27 | BearingTrue: 213.8, | |
28 | BearingTrueType: BearingTrue, | |
29 | BearingMagnetic: 218, | |
30 | BearingMagneticType: BearingMagnetic, | |
31 | DistanceNauticalMiles: 4.6, | |
32 | DistanceNauticalMilesUnit: DistanceUnitNauticalMile, | |
33 | DestinationWaypointID: "EGLM", | |
34 | FFAMode: "", | |
35 | }, | |
36 | }, | |
37 | { | |
38 | name: "good sentence no waypoint", | |
39 | raw: "$GPBWR,081837,,,,,,T,,M,,N,*02", | |
40 | msg: BWR{ | |
41 | Time: Time{Valid: true, Hour: 8, Minute: 18, Second: 37, Millisecond: 0}, | |
42 | Latitude: 0, | |
43 | Longitude: 0, | |
44 | BearingTrue: 0, | |
45 | BearingTrueType: BearingTrue, | |
46 | BearingMagnetic: 0, | |
47 | BearingMagneticType: BearingMagnetic, | |
48 | DistanceNauticalMiles: 0, | |
49 | DistanceNauticalMilesUnit: DistanceUnitNauticalMile, | |
50 | DestinationWaypointID: "", | |
51 | FFAMode: "", | |
52 | }, | |
53 | }, | |
54 | { | |
55 | name: "good sentence with FAAMode", | |
56 | raw: "$GPBWR,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM,D*58", | |
57 | msg: BWR{ | |
58 | Time: Time{ | |
59 | Valid: true, | |
60 | Hour: 22, | |
61 | Minute: 5, | |
62 | Second: 16, | |
63 | Millisecond: 0, | |
64 | }, | |
65 | Latitude: 51.50033333333334, | |
66 | Longitude: -0.7723333333333334, | |
67 | BearingTrue: 213.8, | |
68 | BearingTrueType: BearingTrue, | |
69 | BearingMagnetic: 218, | |
70 | BearingMagneticType: BearingMagnetic, | |
71 | DistanceNauticalMiles: 4.6, | |
72 | DistanceNauticalMilesUnit: DistanceUnitNauticalMile, | |
73 | DestinationWaypointID: "EGLM", | |
74 | FFAMode: FAAModeDifferential, | |
75 | }, | |
76 | }, | |
77 | { | |
78 | name: "invalid nmea: Time", | |
79 | raw: "$GPBWR,2x0516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*7A", | |
80 | err: "nmea: GPBWR invalid time: 2x0516", | |
81 | }, | |
82 | { | |
83 | name: "invalid nmea: BearingTrueType", | |
84 | raw: "$GPBWR,220516,5130.02,N,00046.34,W,213.8,M,218.0,M,0004.6,N,EGLM*29", | |
85 | err: "nmea: GPBWR invalid true bearing type: M", | |
86 | }, | |
87 | { | |
88 | name: "invalid nmea: BearingMagneticType", | |
89 | raw: "$GPBWR,220516,5130.02,N,00046.34,W,213.8,T,218.0,T,0004.6,N,EGLM*29", | |
90 | err: "nmea: GPBWR invalid magnetic bearing type: T", | |
91 | }, | |
92 | { | |
93 | name: "invalid nmea: DistanceNauticalMilesUnit", | |
94 | raw: "$GPBWR,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,K,EGLM*35", | |
95 | err: "nmea: GPBWR invalid is distance to waypoint nautical miles unit: K", | |
96 | }, | |
97 | } | |
98 | for _, tt := range tests { | |
99 | t.Run(tt.name, func(t *testing.T) { | |
100 | m, err := Parse(tt.raw) | |
101 | if tt.err != "" { | |
102 | assert.Error(t, err) | |
103 | assert.EqualError(t, err, tt.err) | |
104 | } else { | |
105 | assert.NoError(t, err) | |
106 | bwr := m.(BWR) | |
107 | bwr.BaseSentence = BaseSentence{} | |
108 | assert.Equal(t, tt.msg, bwr) | |
109 | } | |
110 | }) | |
111 | } | |
112 | } |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypeBWW type of BWW sentence for bearing (from destination) destination waypoint to origin waypoint | |
4 | TypeBWW = "BWW" | |
5 | ) | |
6 | ||
7 | // BWW - bearing (from destination) destination waypoint to origin waypoint | |
8 | // Replaces by BOD in NMEA4+ (according to GPSD docs) | |
9 | // If your system supports RMB it is better to use RMB as it is more common (according to OpenCPN docs) | |
10 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_bww_bearing_waypoint_to_waypoint | |
11 | // http://www.nmea.de/nmea0183datensaetze.html#bww | |
12 | // | |
13 | // Format: $--BWW,x.x,T,x.x,M,c--c,c--c*hh<CR><LF> | |
14 | // Example: $GPBWW,097.0,T,103.2,M,POINTB,POINTA*41 | |
15 | type BWW struct { | |
16 | BaseSentence | |
17 | BearingTrue float64 // true bearing in degrees | |
18 | BearingTrueType string // is type of true bearing | |
19 | BearingMagnetic float64 // magnetic bearing in degrees | |
20 | BearingMagneticType string // is type of magnetic bearing | |
21 | DestinationWaypointID string // destination waypoint ID | |
22 | OriginWaypointID string // origin waypoint ID | |
23 | } | |
24 | ||
25 | // newBWW constructor | |
26 | func newBWW(s BaseSentence) (BWW, error) { | |
27 | p := NewParser(s) | |
28 | p.AssertType(TypeBWW) | |
29 | bod := BWW{ | |
30 | BaseSentence: s, | |
31 | BearingTrue: p.Float64(0, "true bearing"), | |
32 | BearingTrueType: p.EnumString(1, "true bearing type", BearingTrue), | |
33 | BearingMagnetic: p.Float64(2, "magnetic bearing"), | |
34 | BearingMagneticType: p.EnumString(3, "magnetic bearing type", BearingMagnetic), | |
35 | DestinationWaypointID: p.String(4, "destination waypoint ID"), | |
36 | OriginWaypointID: p.String(5, "origin waypoint ID"), | |
37 | } | |
38 | return bod, p.Err() | |
39 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestBWW(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg BWW | |
13 | }{ | |
14 | { | |
15 | name: "good sentence", | |
16 | raw: "$GPBWW,097.0,T,103.2,M,POINTB,POINTA*41", | |
17 | msg: BWW{ | |
18 | BearingTrue: 97.0, | |
19 | BearingTrueType: BearingTrue, | |
20 | BearingMagnetic: 103.2, | |
21 | BearingMagneticType: BearingMagnetic, | |
22 | DestinationWaypointID: "POINTB", | |
23 | OriginWaypointID: "POINTA", | |
24 | }, | |
25 | }, | |
26 | { | |
27 | name: "invalid nmea: BearingTrueValid", | |
28 | raw: "$GPBWW,097.0,M,103.2,M,POINTB,POINTA*58", | |
29 | err: "nmea: GPBWW invalid true bearing type: M", | |
30 | }, | |
31 | { | |
32 | name: "invalid nmea: BearingMagneticValid", | |
33 | raw: "$GPBWW,097.0,T,103.2,T,POINTB,POINTA*58", | |
34 | err: "nmea: GPBWW invalid magnetic bearing type: T", | |
35 | }, | |
36 | } | |
37 | for _, tt := range tests { | |
38 | t.Run(tt.name, func(t *testing.T) { | |
39 | m, err := Parse(tt.raw) | |
40 | if tt.err != "" { | |
41 | assert.Error(t, err) | |
42 | assert.EqualError(t, err, tt.err) | |
43 | } else { | |
44 | assert.NoError(t, err) | |
45 | bww := m.(BWW) | |
46 | bww.BaseSentence = BaseSentence{} | |
47 | assert.Equal(t, tt.msg, bww) | |
48 | } | |
49 | }) | |
50 | } | |
51 | } |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypeDBK type of DBK sentence for Depth Below Keel | |
4 | TypeDBK = "DBK" | |
5 | ) | |
6 | ||
7 | // DBK - Depth Below Keel (obsolete, use DPT instead) | |
8 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_dbk_depth_below_keel | |
9 | // https://wiki.openseamap.org/wiki/OpenSeaMap-dev:NMEA#DBK_-_Depth_below_keel | |
10 | // | |
11 | // Format: $--DBK,x.x,f,x.x,M,x.x,F*hh<CR><LF> | |
12 | // Example: $SDDBK,12.3,f,3.7,M,2.0,F*2F | |
13 | type DBK struct { | |
14 | BaseSentence | |
15 | DepthFeet float64 // Depth, feet | |
16 | DepthFeetUnit string // f = feet | |
17 | DepthMeters float64 // Depth, meters | |
18 | DepthMetersUnit string // M = meters | |
19 | DepthFathoms float64 // Depth, Fathoms | |
20 | DepthFathomsUnit string // F = Fathoms | |
21 | } | |
22 | ||
23 | // newDBK constructor | |
24 | func newDBK(s BaseSentence) (DBK, error) { | |
25 | p := NewParser(s) | |
26 | p.AssertType(TypeDBK) | |
27 | return DBK{ | |
28 | BaseSentence: s, | |
29 | DepthFeet: p.Float64(0, "depth feet"), | |
30 | DepthFeetUnit: p.EnumString(1, "depth feet unit", DistanceUnitFeet), | |
31 | DepthMeters: p.Float64(2, "depth meters"), | |
32 | DepthMetersUnit: p.EnumString(3, "depth meters unit", DistanceUnitMetre), | |
33 | DepthFathoms: p.Float64(4, "depth fathom"), | |
34 | DepthFathomsUnit: p.EnumString(5, "depth fathom unit", DistanceUnitFathom), | |
35 | }, p.Err() | |
36 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestDBK(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg DBK | |
13 | }{ | |
14 | { | |
15 | name: "good sentence", | |
16 | raw: "$SDDBK,12.3,f,3.7,M,2.0,F*2F", | |
17 | msg: DBK{ | |
18 | DepthFeet: 12.3, | |
19 | DepthFeetUnit: DistanceUnitFeet, | |
20 | DepthMeters: 3.7, | |
21 | DepthMetersUnit: DistanceUnitMetre, | |
22 | DepthFathoms: 2, | |
23 | DepthFathomsUnit: DistanceUnitFathom, | |
24 | }, | |
25 | }, | |
26 | { | |
27 | name: "invalid nmea: DepthFeetUnit", | |
28 | raw: "$SDDBK,12.3,x,3.7,M,2.0,F*31", | |
29 | err: "nmea: SDDBK invalid depth feet unit: x", | |
30 | }, | |
31 | { | |
32 | name: "invalid nmea: DepthMeterUnit", | |
33 | raw: "$SDDBK,12.3,f,3.7,x,2.0,F*1A", | |
34 | err: "nmea: SDDBK invalid depth meters unit: x", | |
35 | }, | |
36 | { | |
37 | name: "invalid nmea: DepthFathomUnit", | |
38 | raw: "$SDDBK,12.3,f,3.7,M,2.0,x*11", | |
39 | err: "nmea: SDDBK invalid depth fathom unit: x", | |
40 | }, | |
41 | } | |
42 | for _, tt := range tests { | |
43 | t.Run(tt.name, func(t *testing.T) { | |
44 | m, err := Parse(tt.raw) | |
45 | if tt.err != "" { | |
46 | assert.Error(t, err) | |
47 | assert.EqualError(t, err, tt.err) | |
48 | } else { | |
49 | assert.NoError(t, err) | |
50 | dbk := m.(DBK) | |
51 | dbk.BaseSentence = BaseSentence{} | |
52 | assert.Equal(t, tt.msg, dbk) | |
53 | } | |
54 | }) | |
55 | } | |
56 | } |
0 | 0 | package nmea |
1 | 1 | |
2 | 2 | const ( |
3 | // TypeDBS type for DBS sentences | |
3 | // TypeDBS is type of DBS sentence for Depth Below Surface | |
4 | 4 | TypeDBS = "DBS" |
5 | 5 | ) |
6 | 6 | |
7 | // DBS - Depth Below Surface | |
7 | // DBS - Depth Below Surface (obsolete, use DPT instead) | |
8 | 8 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_dbs_depth_below_surface |
9 | // https://wiki.openseamap.org/wiki/OpenSeaMap-dev:NMEA#DBS_-_Depth_below_surface | |
10 | // | |
11 | // Format: $--DBS,x.x,f,x.x,M,x.x,F*hh<CR><LF> | |
12 | // Example: $23DBS,01.9,f,0.58,M,00.3,F*21 | |
9 | 13 | type DBS struct { |
10 | 14 | BaseSentence |
11 | DepthFeet float64 | |
12 | DepthMeters float64 | |
13 | DepthFathoms float64 | |
15 | DepthFeet float64 // Depth, feet | |
16 | DepthFeetUnit string // f = feet | |
17 | DepthMeters float64 // Depth, meters | |
18 | DepthMeterUnit string // M = meters | |
19 | DepthFathoms float64 // Depth, Fathoms | |
20 | DepthFathomUnit string // F = Fathoms | |
14 | 21 | } |
15 | 22 | |
16 | 23 | // newDBS constructor |
18 | 25 | p := NewParser(s) |
19 | 26 | p.AssertType(TypeDBS) |
20 | 27 | return DBS{ |
21 | BaseSentence: s, | |
22 | DepthFeet: p.Float64(0, "depth_feet"), | |
23 | DepthMeters: p.Float64(2, "depth_meters"), | |
24 | DepthFathoms: p.Float64(4, "depth_fathoms"), | |
28 | BaseSentence: s, | |
29 | DepthFeet: p.Float64(0, "depth feet"), | |
30 | DepthFeetUnit: p.EnumString(1, "depth feet unit", DistanceUnitFeet), | |
31 | DepthMeters: p.Float64(2, "depth meters"), | |
32 | DepthMeterUnit: p.EnumString(3, "depth feet unit", DistanceUnitMetre), | |
33 | DepthFathoms: p.Float64(4, "depth fathoms"), | |
34 | DepthFathomUnit: p.EnumString(5, "depth fathom unit", DistanceUnitFathom), | |
25 | 35 | }, p.Err() |
26 | 36 | } |
5 | 5 | "github.com/stretchr/testify/assert" |
6 | 6 | ) |
7 | 7 | |
8 | var dbstests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg DBS | |
13 | }{ | |
14 | { | |
15 | name: "good sentence", | |
16 | raw: "$23DBS,01.9,f,0.58,M,00.3,F*21", | |
17 | msg: DBS{ | |
18 | DepthFeet: MustParseDecimal("1.9"), | |
19 | DepthMeters: MustParseDecimal("0.58"), | |
20 | DepthFathoms: MustParseDecimal("0.3"), | |
8 | func TestDBS(t *testing.T) { | |
9 | var dbstests = []struct { | |
10 | name string | |
11 | raw string | |
12 | err string | |
13 | msg DBS | |
14 | }{ | |
15 | { | |
16 | name: "good sentence", | |
17 | raw: "$23DBS,01.9,f,0.58,M,00.3,F*21", | |
18 | msg: DBS{ | |
19 | DepthFeet: 1.9, | |
20 | DepthFeetUnit: DistanceUnitFeet, | |
21 | DepthMeters: 0.58, | |
22 | DepthMeterUnit: DistanceUnitMetre, | |
23 | DepthFathoms: 0.3, | |
24 | DepthFathomUnit: DistanceUnitFathom, | |
25 | }, | |
21 | 26 | }, |
22 | }, | |
23 | { | |
24 | name: "bad validity", | |
25 | raw: "$23DBS,01.9,f,0.58,M,00.3,F*25", | |
26 | err: "nmea: sentence checksum mismatch [21 != 25]", | |
27 | }, | |
28 | } | |
27 | { | |
28 | name: "good sentence 2", | |
29 | raw: "$SDDBS,,,0187.5,M,,*1A", // Simrad ITI Trawl System | |
30 | msg: DBS{ | |
31 | DepthFeet: 0, | |
32 | DepthFeetUnit: "", | |
33 | DepthMeters: 187.5, | |
34 | DepthMeterUnit: DistanceUnitMetre, | |
35 | DepthFathoms: 0, | |
36 | DepthFathomUnit: "", | |
37 | }, | |
38 | }, | |
39 | { | |
40 | name: "bad validity", | |
41 | raw: "$23DBS,01.9,f,0.58,M,00.3,F*25", | |
42 | err: "nmea: sentence checksum mismatch [21 != 25]", | |
43 | }, | |
44 | } | |
29 | 45 | |
30 | func TestDBS(t *testing.T) { | |
31 | 46 | for _, tt := range dbstests { |
32 | 47 | t.Run(tt.name, func(t *testing.T) { |
33 | 48 | m, err := Parse(tt.raw) |
6 | 6 | |
7 | 7 | // DBT - Depth below transducer |
8 | 8 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_dbt_depth_below_transducer |
9 | // | |
10 | // Format: $--DBT,x.x,f,x.x,M,x.x,F*hh<CR><LF> | |
11 | // Example: $IIDBT,032.93,f,010.04,M,005.42,F*2C | |
9 | 12 | type DBT struct { |
10 | 13 | BaseSentence |
11 | 14 | DepthFeet float64 |
15 | 15 | name: "good sentence", |
16 | 16 | raw: "$IIDBT,032.93,f,010.04,M,005.42,F*2C", |
17 | 17 | msg: DBT{ |
18 | DepthFeet: MustParseDecimal("32.93"), | |
19 | DepthMeters: MustParseDecimal("10.04"), | |
20 | DepthFathoms: MustParseDecimal("5.42"), | |
18 | DepthFeet: 32.93, | |
19 | DepthMeters: 10.04, | |
20 | DepthFathoms: 5.42, | |
21 | 21 | }, |
22 | 22 | }, |
23 | 23 | { |
246 | 246 | msg: GNRMC{ |
247 | 247 | Time: Time{true, 22, 05, 16, 0}, |
248 | 248 | Validity: "A", |
249 | Latitude: MustParseGPS("5133.82 N"), | |
250 | Longitude: MustParseGPS("00042.24 W"), | |
249 | 251 | Speed: 173.8, |
250 | 252 | Course: 231.8, |
251 | 253 | Date: Date{true, 13, 06, 94}, |
252 | 254 | Variation: -4.2, |
253 | Latitude: MustParseGPS("5133.82 N"), | |
254 | Longitude: MustParseGPS("00042.24 W"), | |
255 | FFAMode: "", | |
256 | NavStatus: "", | |
255 | 257 | }, |
256 | 258 | }, |
257 | 259 | { |
260 | 262 | msg: GNRMC{ |
261 | 263 | Time: Time{true, 14, 27, 54, 0}, |
262 | 264 | Validity: "A", |
265 | Latitude: MustParseGPS("4302.539570 N"), | |
266 | Longitude: MustParseGPS("07920.379823 W"), | |
263 | 267 | Speed: 0, |
264 | 268 | Course: 0, |
265 | 269 | Date: Date{true, 7, 6, 17}, |
266 | 270 | Variation: 0, |
267 | Latitude: MustParseGPS("4302.539570 N"), | |
268 | Longitude: MustParseGPS("07920.379823 W"), | |
271 | FFAMode: FAAModeAutonomous, | |
272 | NavStatus: "", | |
269 | 273 | }, |
270 | 274 | }, |
271 | 275 | { |
274 | 278 | msg: GNRMC{ |
275 | 279 | Time: Time{true, 10, 5, 38, 0}, |
276 | 280 | Validity: "A", |
281 | Latitude: MustParseGPS("5546.27711 N"), | |
282 | Longitude: MustParseGPS("03736.91144 E"), | |
277 | 283 | Speed: 0.061, |
278 | 284 | Course: 0, |
279 | 285 | Date: Date{true, 26, 3, 18}, |
280 | 286 | Variation: 0, |
281 | Latitude: MustParseGPS("5546.27711 N"), | |
282 | Longitude: MustParseGPS("03736.91144 E"), | |
287 | FFAMode: FAAModeAutonomous, | |
288 | NavStatus: "", | |
283 | 289 | }, |
284 | 290 | }, |
285 | 291 | { |
382 | 388 | Millisecond: 0, |
383 | 389 | }, |
384 | 390 | Validity: "A", |
391 | FFAMode: FAAModeAutonomous, | |
385 | 392 | }, |
386 | 393 | }, |
387 | 394 | { |
599 | 606 | msg: GPRMC{ |
600 | 607 | Time: Time{true, 22, 5, 16, 0}, |
601 | 608 | Validity: "A", |
609 | Latitude: MustParseGPS("5133.82 N"), | |
610 | Longitude: MustParseGPS("00042.24 W"), | |
602 | 611 | Speed: 173.8, |
603 | 612 | Course: 231.8, |
604 | 613 | Date: Date{true, 13, 6, 94}, |
605 | 614 | Variation: -4.2, |
606 | Latitude: MustParseGPS("5133.82 N"), | |
607 | Longitude: MustParseGPS("00042.24 W"), | |
615 | FFAMode: "", | |
616 | NavStatus: "", | |
608 | 617 | }, |
609 | 618 | }, |
610 | 619 | { |
613 | 622 | msg: GPRMC{ |
614 | 623 | Time: Time{true, 14, 27, 54, 0}, |
615 | 624 | Validity: "A", |
625 | Latitude: MustParseGPS("4302.539570 N"), | |
626 | Longitude: MustParseGPS("07920.379823 W"), | |
616 | 627 | Speed: 0, |
617 | 628 | Course: 0, |
618 | 629 | Date: Date{true, 7, 6, 17}, |
619 | 630 | Variation: 0, |
620 | Latitude: MustParseGPS("4302.539570 N"), | |
621 | Longitude: MustParseGPS("07920.379823 W"), | |
631 | FFAMode: FAAModeAutonomous, | |
632 | NavStatus: "", | |
622 | 633 | }, |
623 | 634 | }, |
624 | 635 | { |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypeDOR type of DOR sentence for Door Status Detection | |
4 | TypeDOR = "DOR" | |
5 | ||
6 | // TypeSingleDoorDOR is type for single door related event | |
7 | TypeSingleDoorDOR = "E" | |
8 | // TypeFaultDOR is type for fault with door | |
9 | TypeFaultDOR = "F" | |
10 | // TypeSectionDOR is type for section of doors related event | |
11 | TypeSectionDOR = "S" | |
12 | ||
13 | // DoorStatusOpenDOR is status for open door | |
14 | DoorStatusOpenDOR = "O" | |
15 | // DoorStatusClosedDOR is status for closed door | |
16 | DoorStatusClosedDOR = "C" | |
17 | // DoorStatusFaultDOR is status for fault with door | |
18 | DoorStatusFaultDOR = "X" | |
19 | ||
20 | // SwitchSettingHarbourModeDOR is setting for Harbour mode (allowed open) | |
21 | SwitchSettingHarbourModeDOR = "O" | |
22 | // SwitchSettingSeaModeDOR is setting for Sea mode (ordered closed) | |
23 | SwitchSettingSeaModeDOR = "C" | |
24 | ) | |
25 | ||
26 | // DOR - Door Status Detection | |
27 | // Source: "Interfacing Voyage Data Recorder Systems, AutroSafe Interactive Fire-Alarm System, 116-P-BSL336/EE, RevA 2007-01-25, | |
28 | // Autronica Fire and Security AS " (page 32 | p.8.1.4) | |
29 | // https://product.autronicafire.com/fileshare/fileupload/14251/bsl336_ee.pdf | |
30 | // | |
31 | // Format: $FRDOR,a,hhmmss,aa,aa,xxx,xxx,a,a,c--c*hh<CR><LF> | |
32 | // Example: $FRDOR,E,233042,FD,FP,000,010,C,C,Door Closed : TEST FPA Name*4D | |
33 | type DOR struct { | |
34 | BaseSentence | |
35 | ||
36 | // Type is type of the message | |
37 | // * E – Single door | |
38 | // * F – Fault | |
39 | // * S – Section (whole or part of section) | |
40 | Type string | |
41 | ||
42 | // Time is Event Time | |
43 | Time Time | |
44 | ||
45 | // SystemIndicator is system indicator. Detector system type with 2 char identifier. | |
46 | // * WT - watertight | |
47 | // * WS - semi watertight | |
48 | // * FD - fire door | |
49 | // * HD - hull door | |
50 | // * OT - other | |
51 | // could be more | |
52 | // https://www.nmea.org/Assets/20190303%20nmea%200183%20talker%20identifier%20mnemonics.pdf | |
53 | SystemIndicator string | |
54 | ||
55 | // DivisionIndicator1 is first division indicator for locating origin detector for this message | |
56 | DivisionIndicator1 string | |
57 | ||
58 | // DivisionIndicator2 is second division indicator for locating origin detector for this message | |
59 | DivisionIndicator2 int64 | |
60 | ||
61 | // DoorNumberOrCount is Door number or activated door count (seems to be field with overloaded meaning) | |
62 | DoorNumberOrCount int64 | |
63 | ||
64 | // DoorStatus is Door status | |
65 | // * O – Open | |
66 | // * C – Closed | |
67 | // * X – Fault | |
68 | // could be more | |
69 | DoorStatus string | |
70 | ||
71 | // SwitchSetting is Mode switch setting | |
72 | // * O – Harbour mode (allowed open) | |
73 | // * C – Sea mode (ordered closed) | |
74 | SwitchSetting string | |
75 | ||
76 | // Message's description text (could be cut to fit max packet length) | |
77 | Message string | |
78 | } | |
79 | ||
80 | // newDOR constructor | |
81 | func newDOR(s BaseSentence) (DOR, error) { | |
82 | p := NewParser(s) | |
83 | p.AssertType(TypeDOR) | |
84 | return DOR{ | |
85 | BaseSentence: s, | |
86 | Type: p.EnumString(0, "message type", TypeSingleDoorDOR, TypeFaultDOR, TypeSectionDOR), | |
87 | Time: p.Time(1, "time"), | |
88 | SystemIndicator: p.String(2, "system indicator"), | |
89 | DivisionIndicator1: p.String(3, "division indicator 1"), | |
90 | DivisionIndicator2: p.Int64(4, "division indicator 2"), | |
91 | DoorNumberOrCount: p.Int64(5, "door number or count"), | |
92 | DoorStatus: p.EnumString(6, "door state", DoorStatusOpenDOR, DoorStatusClosedDOR, DoorStatusFaultDOR), | |
93 | SwitchSetting: p.EnumString(7, "switch setting mode", SwitchSettingHarbourModeDOR, SwitchSettingSeaModeDOR), | |
94 | Message: p.String(8, "message"), | |
95 | }, p.Err() | |
96 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestDOR(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg DOR | |
13 | }{ | |
14 | { | |
15 | name: "good sentence", | |
16 | raw: "$FRDOR,E,233042,FD,FP,000,010,C,C,Door Closed : TEST FPA Name*4D", | |
17 | msg: DOR{ | |
18 | Type: TypeSingleDoorDOR, | |
19 | Time: Time{ | |
20 | Valid: true, | |
21 | Hour: 23, | |
22 | Minute: 30, | |
23 | Second: 42, | |
24 | Millisecond: 0, | |
25 | }, | |
26 | SystemIndicator: "FD", | |
27 | DivisionIndicator1: "FP", | |
28 | DivisionIndicator2: 0, | |
29 | DoorNumberOrCount: 10, | |
30 | DoorStatus: DoorStatusClosedDOR, | |
31 | SwitchSetting: SwitchSettingSeaModeDOR, | |
32 | Message: "Door Closed : TEST FPA Name", | |
33 | }, | |
34 | }, | |
35 | { | |
36 | name: "invalid nmea: Type", | |
37 | raw: "$FRDOR,x,233042,FD,FP,000,010,C,C,Door Closed : TEST FPA Name*70", | |
38 | err: "nmea: FRDOR invalid message type: x", | |
39 | }, | |
40 | { | |
41 | name: "invalid nmea: Time", | |
42 | raw: "$FRDOR,E,2x3042,FD,FP,000,010,C,C,Door Closed : TEST FPA Name*06", | |
43 | err: "nmea: FRDOR invalid time: 2x3042", | |
44 | }, | |
45 | { | |
46 | name: "invalid nmea: DoorStatus", | |
47 | raw: "$FRDOR,E,233042,FD,FP,000,010,_,C,Door Closed : TEST FPA Name*51", | |
48 | err: "nmea: FRDOR invalid door state: _", | |
49 | }, | |
50 | { | |
51 | name: "invalid nmea: SwitchSetting", | |
52 | raw: "$FRDOR,E,233042,FD,FP,000,010,C,_,Door Closed : TEST FPA Name*51", | |
53 | err: "nmea: FRDOR invalid switch setting mode: _", | |
54 | }, | |
55 | } | |
56 | for _, tt := range tests { | |
57 | t.Run(tt.name, func(t *testing.T) { | |
58 | m, err := Parse(tt.raw) | |
59 | if tt.err != "" { | |
60 | assert.Error(t, err) | |
61 | assert.EqualError(t, err, tt.err) | |
62 | } else { | |
63 | assert.NoError(t, err) | |
64 | dor := m.(DOR) | |
65 | dor.BaseSentence = BaseSentence{} | |
66 | assert.Equal(t, tt.msg, dor) | |
67 | } | |
68 | }) | |
69 | } | |
70 | } |
6 | 6 | |
7 | 7 | // DPT - Depth of Water |
8 | 8 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_dpt_depth_of_water |
9 | // | |
10 | // Format: $--DPT,x.x,x.x,x.x*hh<CR><LF> | |
11 | // Example: $SDDPT,0.5,0.5,*7B | |
12 | // $INDPT,2.3,0.0*46 | |
9 | 13 | type DPT struct { |
10 | 14 | BaseSentence |
11 | Depth float64 | |
12 | Offset float64 | |
13 | RangeScale float64 | |
15 | Depth float64 // Water depth relative to transducer, meters | |
16 | Offset float64 // offset from transducer | |
17 | RangeScale float64 // OPTIONAL, Maximum range scale in use (NMEA 3.0 and above) | |
14 | 18 | } |
15 | 19 | |
16 | 20 | // newDPT constructor |
17 | 21 | func newDPT(s BaseSentence) (DPT, error) { |
18 | 22 | p := NewParser(s) |
19 | 23 | p.AssertType(TypeDPT) |
20 | return DPT{ | |
24 | dpt := DPT{ | |
21 | 25 | BaseSentence: s, |
22 | 26 | Depth: p.Float64(0, "depth"), |
23 | 27 | Offset: p.Float64(1, "offset"), |
24 | RangeScale: p.Float64(2, "range scale"), | |
25 | }, p.Err() | |
28 | } | |
29 | if len(p.Fields) > 2 { | |
30 | dpt.RangeScale = p.Float64(2, "range scale") | |
31 | } | |
32 | return dpt, p.Err() | |
26 | 33 | } |
15 | 15 | name: "good sentence", |
16 | 16 | raw: "$SDDPT,0.5,0.5,*7B", |
17 | 17 | msg: DPT{ |
18 | Depth: MustParseDecimal("0.5"), | |
19 | Offset: MustParseDecimal("0.5"), | |
20 | RangeScale: MustParseDecimal("0"), | |
18 | Depth: 0.5, | |
19 | Offset: 0.5, | |
20 | RangeScale: 0, | |
21 | 21 | }, |
22 | 22 | }, |
23 | 23 | { |
24 | 24 | name: "good sentence with scale", |
25 | 25 | raw: "$SDDPT,0.5,0.5,0.1*54", |
26 | 26 | msg: DPT{ |
27 | Depth: MustParseDecimal("0.5"), | |
28 | Offset: MustParseDecimal("0.5"), | |
29 | RangeScale: MustParseDecimal("0.1"), | |
27 | Depth: 0.5, | |
28 | Offset: 0.5, | |
29 | RangeScale: 0.1, | |
30 | }, | |
31 | }, | |
32 | { | |
33 | name: "good sentence with 2 fields", | |
34 | raw: "$INDPT,2.3,0.0*46", | |
35 | msg: DPT{ | |
36 | Depth: 2.3, | |
37 | Offset: 0, | |
38 | RangeScale: 0, | |
30 | 39 | }, |
31 | 40 | }, |
32 | 41 | { |
0 | package nmea | |
1 | ||
2 | import "strings" | |
3 | ||
4 | const ( | |
5 | // TypeDSC type of DSC sentence for Digital Selective Calling Information | |
6 | TypeDSC = "DSC" | |
7 | ||
8 | // AcknowledgementRequestDSC is type for Acknowledge request | |
9 | AcknowledgementRequestDSC = "R" | |
10 | // AcknowledgementDSC is type for Acknowledgement | |
11 | AcknowledgementDSC = "B" | |
12 | // AcknowledgementNeitherDSC is type for Neither (end of sequence) | |
13 | AcknowledgementNeitherDSC = "S" | |
14 | ) | |
15 | ||
16 | // DSC – Digital Selective Calling Information | |
17 | // https://opencpn.org/wiki/dokuwiki/doku.php?id=opencpn:opencpn_user_manual:advanced_features:nmea_sentences | |
18 | // https://web.archive.org/web/20190303170916/http://continuouswave.com/whaler/reference/DSC_Datagrams.html | |
19 | // http://www.busse-yachtshop.de/pdf/icom-GM600-handbuch.pdf | |
20 | // https://github.com/mariokonrad/marnav/blob/master/src/marnav/nmea/dsc.cpp (marnav has interesting enums worth checking) | |
21 | // | |
22 | // Note: many fields of DSC are conditional with double meaning and we only map raw sentence to fields without any | |
23 | // logic/checking of those conditions. We could have specific fields if we only knew the rules to populate them. | |
24 | // | |
25 | // Format: $--DSC,xx,xxxxxxxxxx,xx,xx,xx,x.x, x.x,xxxxxxxxxx,xx, a,a*hh<CR><LF> | |
26 | // Example: $CDDSC,20,3380400790,00,21,26,1423108312,2021,,,B, E*73 | |
27 | type DSC struct { | |
28 | BaseSentence | |
29 | // Note: all fields are strings even if specified as digits as int can not express "00" and would be 0 which is different | |
30 | // Source of quotes: https://web.archive.org/web/20190303170916/http://continuouswave.com/whaler/reference/DSC_Datagrams.html | |
31 | ||
32 | // FormatSpecifier is Format specifier (2 digits) | |
33 | // > The call content is first described by a "format specifier" element. The format specifier is explained in | |
34 | // > ITU-Rec. M.493-13 Section 4, with various symbol codes in the "service command" range of symbols representing | |
35 | // > various message formats, as shown in Table 3 (by symbol number, then meaning of symbol) as follows: | |
36 | // > * 102 = selective call to a group of ships in particular geographic area | |
37 | // > * 112 = distress alert call | |
38 | // > * 114 = selective call to a group of ships having common interest | |
39 | // > * 116 = all ships call | |
40 | // > * 120 = selective call to particular individual station | |
41 | // > * 123 = selective call to a particular individual using automatic service | |
42 | FormatSpecifier string | |
43 | ||
44 | // Address (10 digits) | |
45 | Address string | |
46 | ||
47 | // Category (2 digits or empty) | |
48 | // > The call content is next described by a "category element" in Section 6. Again, various symbol codes in the | |
49 | // > "service command" range of symbols represent various categories, as follows from Table 3 (by symbol number, | |
50 | // > then meaning of symbol): | |
51 | // > * 100 = routine | |
52 | // > * 108 = safety | |
53 | // > * 110 = urgency | |
54 | // > * 112 = distress | |
55 | Category string | |
56 | ||
57 | // DistressCauseOrTeleCommand1 is The cause of the distress or first telecommand (2 digits or empty) | |
58 | // > Nature of Distress is to be encoded, again using Table 3, as follows | |
59 | // > * 100 = Fire, explosion | |
60 | // > * 101 = Flooding | |
61 | // > * 102 = Collision | |
62 | // > * 103 = Grounding | |
63 | // > * 104 = Listing, in danger of capsize | |
64 | // > * 105 = Sinking | |
65 | // > * 106 = Disabled and adrift | |
66 | // > * 107 = Undesignated distres | |
67 | // > * 108 = Abandoning ship | |
68 | // > * 109 = Piracy/armed robbery attack | |
69 | // > * 110 = Man overboard | |
70 | // > * 111 = unassigned symbol; take no action | |
71 | // > * 112 = EPRIB emission | |
72 | // > * 113 through 27 = unassigned symbol; take no action | |
73 | DistressCauseOrTeleCommand1 string | |
74 | ||
75 | // CommandTypeOrTeleCommand2 is Type of communication or second telecommand (2 digits) | |
76 | CommandTypeOrTeleCommand2 string | |
77 | ||
78 | // PositionOrCanal is Position (lat+lon) or Canal/frequency (Maximum 16 digits) | |
79 | // > Distress coordinates are to be encoded five parts, sent as a string of ten digits. The first digit indicates | |
80 | // > the direction of the latitude and longitude, with "0" for North and East, "1" for North and West, | |
81 | // > "2" for South and East, and "3" for South and West. The next two digits are the latitude in degrees. | |
82 | // > The next two digits are the latitude in whole minutes. The next three digits are the longitude in degrees. | |
83 | // > The next two digits are longitude in whole minutes. | |
84 | PositionOrCanal string // Position (lat+lon) or Canal/frequency (Maximum 16 digits) | |
85 | ||
86 | // TimeOrTelephoneNumber is Time or Telephone Number (Maximum 16 digits) | |
87 | // > The time in universal coordinated time is to be sent in 24-hour format in two parts, a total of four digits. | |
88 | // > The first two digits are the hours. The next two are the minutes. | |
89 | TimeOrTelephoneNumber string | |
90 | ||
91 | // MMSI of ship in distress (10 digits or empty) | |
92 | // > The call content is next described as having a "self-identification" element. This is simply the sending | |
93 | // > station's MMSI, encoded like the address element. This identifies who sent the message. | |
94 | MMSI string | |
95 | ||
96 | // DistressCause is The cause of the distress (2 digits or empty) | |
97 | DistressCause string | |
98 | ||
99 | // Acknowledgement (R=Acknowledge request, B=Acknowledgement, S=Neither (end of sequence)) | |
100 | Acknowledgement string | |
101 | ||
102 | // Expansion indicator (E or empty) | |
103 | ExpansionIndicator string | |
104 | } | |
105 | ||
106 | // newDSC constructor | |
107 | func newDSC(s BaseSentence) (DSC, error) { | |
108 | p := NewParser(s) | |
109 | p.AssertType(TypeDSC) | |
110 | return DSC{ | |
111 | BaseSentence: s, | |
112 | FormatSpecifier: p.String(0, "format specifier"), | |
113 | Address: p.String(1, "address"), | |
114 | Category: p.String(2, "category"), | |
115 | DistressCauseOrTeleCommand1: p.String(3, "cause of the distress or first telecommand"), | |
116 | CommandTypeOrTeleCommand2: p.String(4, "type of communication or second telecommand"), | |
117 | PositionOrCanal: p.String(5, "position or canal"), | |
118 | TimeOrTelephoneNumber: p.String(6, "time or telephone"), | |
119 | MMSI: p.String(7, "MMSI"), | |
120 | DistressCause: p.String(8, "distress cause"), | |
121 | Acknowledgement: strings.TrimSpace(p.EnumString( | |
122 | 9, | |
123 | "acknowledgement", | |
124 | AcknowledgementRequestDSC, | |
125 | " "+AcknowledgementRequestDSC, | |
126 | AcknowledgementDSC, | |
127 | " "+AcknowledgementDSC, | |
128 | AcknowledgementNeitherDSC, | |
129 | " "+AcknowledgementNeitherDSC, | |
130 | )), | |
131 | ExpansionIndicator: p.String(10, "expansion indicator"), | |
132 | }, p.Err() | |
133 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestDSC(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg DSC | |
13 | }{ | |
14 | { | |
15 | name: "good sentence", | |
16 | raw: "$CDDSC,12,3380400790,12,06,00,1423108312,2019, , , S, E *4a", | |
17 | msg: DSC{ | |
18 | FormatSpecifier: "12", | |
19 | Address: "3380400790", | |
20 | Category: "12", | |
21 | DistressCauseOrTeleCommand1: "06", | |
22 | CommandTypeOrTeleCommand2: "00", | |
23 | PositionOrCanal: "1423108312", | |
24 | TimeOrTelephoneNumber: "2019", | |
25 | MMSI: " ", | |
26 | DistressCause: " ", | |
27 | Acknowledgement: "S", | |
28 | ExpansionIndicator: " E ", | |
29 | }, | |
30 | }, | |
31 | { | |
32 | name: "good sentence Distress Alert Cancel", | |
33 | raw: "$CDDSC,12,3381581370,12,06,00,1423108312,0236,3381581370, , S, *20", | |
34 | msg: DSC{ | |
35 | FormatSpecifier: "12", | |
36 | Address: "3381581370", | |
37 | Category: "12", | |
38 | DistressCauseOrTeleCommand1: "06", | |
39 | CommandTypeOrTeleCommand2: "00", | |
40 | PositionOrCanal: "1423108312", | |
41 | TimeOrTelephoneNumber: "0236", | |
42 | MMSI: "3381581370", | |
43 | DistressCause: " ", | |
44 | Acknowledgement: "S", | |
45 | ExpansionIndicator: " ", | |
46 | }, | |
47 | }, | |
48 | { | |
49 | name: "good sentence Non-Distress Call - Reply to Position Request\n", | |
50 | raw: "$CDDSC,20,3381581370,00,21,26,1423108312,1902, , , B, E *7B", | |
51 | msg: DSC{ | |
52 | FormatSpecifier: "20", | |
53 | Address: "3381581370", | |
54 | Category: "00", | |
55 | DistressCauseOrTeleCommand1: "21", | |
56 | CommandTypeOrTeleCommand2: "26", | |
57 | PositionOrCanal: "1423108312", | |
58 | TimeOrTelephoneNumber: "1902", | |
59 | MMSI: " ", | |
60 | DistressCause: " ", | |
61 | Acknowledgement: "B", | |
62 | ExpansionIndicator: " E ", | |
63 | }, | |
64 | }, | |
65 | { | |
66 | name: "invalid nmea: Acknowledgement", | |
67 | raw: "$CDDSC,20,3380400790,00,21,26,1423108312,2021,,,x, E*69", | |
68 | err: "nmea: CDDSC invalid acknowledgement: x", | |
69 | }, | |
70 | } | |
71 | for _, tt := range tests { | |
72 | t.Run(tt.name, func(t *testing.T) { | |
73 | m, err := Parse(tt.raw) | |
74 | if tt.err != "" { | |
75 | assert.Error(t, err) | |
76 | assert.EqualError(t, err, tt.err) | |
77 | } else { | |
78 | assert.NoError(t, err) | |
79 | dsc := m.(DSC) | |
80 | dsc.BaseSentence = BaseSentence{} | |
81 | assert.Equal(t, tt.msg, dsc) | |
82 | } | |
83 | }) | |
84 | } | |
85 | } |
0 | package nmea | |
1 | ||
2 | import "errors" | |
3 | ||
4 | const ( | |
5 | // TypeDSE type of DSE sentence for Expanded digital selective calling | |
6 | TypeDSE = "DSE" | |
7 | ||
8 | // AcknowledgementAutomaticDSE is type for automatic | |
9 | AcknowledgementAutomaticDSE = "A" | |
10 | // AcknowledgementRequestDSE is type for request | |
11 | AcknowledgementRequestDSE = "R" | |
12 | // AcknowledgementQueryDSE is type for query | |
13 | AcknowledgementQueryDSE = "Q" | |
14 | ) | |
15 | ||
16 | // DSE – Expanded digital selective calling. Is sentence that follows DSC sentence to provide additional (extended) data. | |
17 | // https://opencpn.org/wiki/dokuwiki/doku.php?id=opencpn:opencpn_user_manual:advanced_features:nmea_sentences | |
18 | // http://www.busse-yachtshop.de/pdf/icom-GM600-handbuch.pdf | |
19 | // | |
20 | // Format: $CDDSE, x, x, a, xxxxxxxxxx, xx, c--c, .........., xx, c--c*hh<CR><LF> | |
21 | // Example: $CDDSE,1,1,A,3380400790,00,46504437*15 | |
22 | type DSE struct { | |
23 | BaseSentence | |
24 | TotalNumber int64 // total number of sentences, 01 to 99 | |
25 | Number int64 // number of current sentence, 01 to 99 | |
26 | Acknowledgement string // Acknowledgement (R=Acknowledge request, B=Acknowledgement, S=Neither (end of sequence)) | |
27 | MMSI string // MMSI of vessel (10 digits) | |
28 | DataSets []DSEDataSet | |
29 | } | |
30 | ||
31 | // DSEDataSet is pair of DSE sets of data containing code + its data | |
32 | type DSEDataSet struct { | |
33 | // Code is code field, 2 digits | |
34 | // From OpenCPN wiki: | |
35 | // > 00–this field of two-digits appears to be the expansion data specifier described in Table 1 of ITU-Rec.M821-1, | |
36 | // > but with the symbol representation in two-digits instead of three-digits. The leading “1” seems to not be used. | |
37 | // > (See modified table, above.) This field identifies the data that will follow in the next field. In this message, | |
38 | // > the data will be “enhanced position resolution.” | |
39 | Code string | |
40 | // Data is data field, Enhanced position resolution, Maximum 8 characters, could be empty | |
41 | // From OpenCPN wiki: | |
42 | // > 45894494–the data payload, which is eight digits. The first four are the decimal portion of the latitude | |
43 | // > minutes; the last four are the decimal portion of the longitude minutes. The latitude and longitude whole | |
44 | // > minutes were sent in the immediately preceding datagram. This is as specified in the ITU-Rec. M.821-1 in | |
45 | // > section 2.1.2.1 | |
46 | Data string | |
47 | } | |
48 | ||
49 | // newDSE constructor | |
50 | func newDSE(s BaseSentence) (DSE, error) { | |
51 | p := NewParser(s) | |
52 | p.AssertType(TypeDSE) | |
53 | dse := DSE{ | |
54 | BaseSentence: s, | |
55 | TotalNumber: p.Int64(0, "total number of sentences"), | |
56 | Number: p.Int64(1, "sentence number"), | |
57 | Acknowledgement: p.EnumString(2, "acknowledgement", AcknowledgementAutomaticDSE, AcknowledgementRequestDSE, AcknowledgementQueryDSE), | |
58 | MMSI: p.String(3, "MMSI"), | |
59 | DataSets: nil, | |
60 | } | |
61 | datasetFieldCount := len(p.Fields) - 4 | |
62 | if datasetFieldCount < 2 { | |
63 | return dse, errors.New("DSE is missing fields for parsing data sets") | |
64 | } | |
65 | if datasetFieldCount%2 != 0 { | |
66 | return dse, errors.New("DSE data set field count is not exactly dividable by 2") | |
67 | } | |
68 | dse.DataSets = make([]DSEDataSet, 0, datasetFieldCount/2) | |
69 | for i := 0; i < datasetFieldCount; i = i + 2 { | |
70 | tmp := DSEDataSet{ | |
71 | Code: p.String(4+i, "data set code"), | |
72 | Data: p.String(5+i, "data set data"), | |
73 | } | |
74 | dse.DataSets = append(dse.DataSets, tmp) | |
75 | } | |
76 | return dse, p.Err() | |
77 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestDSE(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg DSE | |
13 | }{ | |
14 | { | |
15 | name: "good sentence, single dataset", | |
16 | raw: "$CDDSE,1,1,A,3380400790,00,46504437*15", | |
17 | msg: DSE{ | |
18 | TotalNumber: 1, | |
19 | Number: 1, | |
20 | Acknowledgement: AcknowledgementAutomaticDSE, | |
21 | MMSI: "3380400790", | |
22 | DataSets: []DSEDataSet{ | |
23 | {Code: "00", Data: "46504437"}, | |
24 | }, | |
25 | }, | |
26 | }, | |
27 | { | |
28 | name: "good sentence, single dataset", | |
29 | raw: "$CDDSE,1,1,A,3380400790,00,46504437,01,16501437*17", | |
30 | msg: DSE{ | |
31 | TotalNumber: 1, | |
32 | Number: 1, | |
33 | Acknowledgement: AcknowledgementAutomaticDSE, | |
34 | MMSI: "3380400790", | |
35 | DataSets: []DSEDataSet{ | |
36 | {Code: "00", Data: "46504437"}, | |
37 | {Code: "01", Data: "16501437"}, | |
38 | }, | |
39 | }, | |
40 | }, | |
41 | { | |
42 | name: "invalid nmea: field count", | |
43 | raw: "$CDDSE,1,1,x,3380400790,46504437*00", | |
44 | err: "DSE is missing fields for parsing data sets", | |
45 | }, | |
46 | { | |
47 | name: "invalid nmea: data set field count", | |
48 | raw: "$CDDSE,1,1,A,3380400790,00,46504437,01*38", | |
49 | err: "DSE data set field count is not exactly dividable by 2", | |
50 | }, | |
51 | { | |
52 | name: "invalid nmea: Acknowledgement", | |
53 | raw: "$CDDSE,1,1,x,3380400790,00,46504437*2c", | |
54 | err: "nmea: CDDSE invalid acknowledgement: x", | |
55 | }, | |
56 | } | |
57 | for _, tt := range tests { | |
58 | t.Run(tt.name, func(t *testing.T) { | |
59 | m, err := Parse(tt.raw) | |
60 | if tt.err != "" { | |
61 | assert.Error(t, err) | |
62 | assert.EqualError(t, err, tt.err) | |
63 | } else { | |
64 | assert.NoError(t, err) | |
65 | dse := m.(DSE) | |
66 | dse.BaseSentence = BaseSentence{} | |
67 | assert.Equal(t, tt.msg, dse) | |
68 | } | |
69 | }) | |
70 | } | |
71 | } |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypeDTM type of DTM sentence for Datum Reference | |
4 | TypeDTM = "DTM" | |
5 | ) | |
6 | ||
7 | // DTM - Datum Reference | |
8 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_dtm_datum_reference | |
9 | // | |
10 | // Format: $--DTM,ref,x,llll,c,llll,c,aaa,ref*hh<CR><LF> | |
11 | // Example: $GPDTM,W84,,0.0,N,0.0,E,0.0,W84*6F | |
12 | // Example: $GPDTM,W84,,00.0000,N,00.0000,W,,W84*53 | |
13 | type DTM struct { | |
14 | BaseSentence | |
15 | LocalDatumCode string // Local datum code (W84,W72,S85,P90,999) | |
16 | LocalDatumSubcode string // Local datum subcode. May be blank. | |
17 | ||
18 | LatitudeOffsetMinute float64 // Latitude offset (minutes) (negative if south) | |
19 | LongitudeOffsetMinute float64 // Longitude offset (minutes) (negative if west) | |
20 | ||
21 | AltitudeOffsetMeters float64 // Altitude offset in meters | |
22 | DatumName string // Reference datum name. What’s usually seen here is "W84", the standard WGS84 datum used by GPS. | |
23 | } | |
24 | ||
25 | // newDTM constructor | |
26 | func newDTM(s BaseSentence) (DTM, error) { | |
27 | p := NewParser(s) | |
28 | p.AssertType(TypeDTM) | |
29 | m := DTM{ | |
30 | BaseSentence: s, | |
31 | LocalDatumCode: p.String(0, "local datum code"), | |
32 | LocalDatumSubcode: p.String(1, "local datum subcode"), | |
33 | ||
34 | LatitudeOffsetMinute: p.Float64(2, "latitude offset minutes"), | |
35 | LongitudeOffsetMinute: p.Float64(4, "longitude offset minutes"), | |
36 | ||
37 | AltitudeOffsetMeters: p.Float64(6, "altitude offset offset"), | |
38 | DatumName: p.String(7, "datum name"), | |
39 | } | |
40 | if p.String(3, "latitude offset direction") == South { | |
41 | m.LatitudeOffsetMinute *= -1 | |
42 | } | |
43 | if p.String(5, "longitude offset direction") == West { | |
44 | m.LongitudeOffsetMinute *= -1 | |
45 | } | |
46 | return m, p.Err() | |
47 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestDTM(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg DTM | |
13 | }{ | |
14 | { | |
15 | name: "good sentence 1", | |
16 | raw: "$GPDTM,W84,,0.0,N,0.0,E,0.0,W84*6F", | |
17 | msg: DTM{ | |
18 | BaseSentence: BaseSentence{}, | |
19 | LocalDatumCode: "W84", | |
20 | LocalDatumSubcode: "", | |
21 | LatitudeOffsetMinute: 0, | |
22 | LongitudeOffsetMinute: 0, | |
23 | AltitudeOffsetMeters: 0, | |
24 | DatumName: "W84", | |
25 | }, | |
26 | }, | |
27 | { | |
28 | name: "good sentence 2", | |
29 | raw: "$GPDTM,W84,X,00.1200,S,12.0000,W,100,W84*27", | |
30 | msg: DTM{ | |
31 | BaseSentence: BaseSentence{}, | |
32 | LocalDatumCode: "W84", | |
33 | LocalDatumSubcode: "X", | |
34 | LatitudeOffsetMinute: -0.12, | |
35 | LongitudeOffsetMinute: -12, | |
36 | AltitudeOffsetMeters: 100, | |
37 | DatumName: "W84", | |
38 | }, | |
39 | }, | |
40 | { | |
41 | name: "invalid nmea: LatitudeOffsetMinute", | |
42 | raw: "$GPDTM,W84,,x,N,0.0,E,0.0,W84*39", | |
43 | err: "nmea: GPDTM invalid latitude offset minutes: x", | |
44 | }, | |
45 | { | |
46 | name: "invalid nmea: LongitudeOffsetMinute", | |
47 | raw: "$GPDTM,W84,,0.0,N,x,E,0.0,W84*39", | |
48 | err: "nmea: GPDTM invalid longitude offset minutes: x", | |
49 | }, | |
50 | { | |
51 | name: "invalid nmea: AltitudeOffsetMeters", | |
52 | raw: "$GPDTM,W84,,0.0,N,0.0,E,x,W84*39", | |
53 | err: "nmea: GPDTM invalid altitude offset offset: x", | |
54 | }, | |
55 | } | |
56 | for _, tt := range tests { | |
57 | t.Run(tt.name, func(t *testing.T) { | |
58 | m, err := Parse(tt.raw) | |
59 | if tt.err != "" { | |
60 | assert.Error(t, err) | |
61 | assert.EqualError(t, err, tt.err) | |
62 | } else { | |
63 | assert.NoError(t, err) | |
64 | mm := m.(DTM) | |
65 | mm.BaseSentence = BaseSentence{} | |
66 | assert.Equal(t, tt.msg, mm) | |
67 | } | |
68 | }) | |
69 | } | |
70 | } |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypeEVE type of EVE sentence for General Event Message | |
4 | TypeEVE = "EVE" | |
5 | ) | |
6 | ||
7 | // EVE - General Event Message | |
8 | // Source: "Interfacing Voyage Data Recorder Systems, AutroSafe Interactive Fire-Alarm System, 116-P-BSL336/EE, RevA 2007-01-25, | |
9 | // Autronica Fire and Security AS " (page 34 | p.8.1.5) | |
10 | // https://product.autronicafire.com/fileshare/fileupload/14251/bsl336_ee.pdf | |
11 | // | |
12 | // Format: $FREVE,hhmmss,c--c,c--c*hh<CR><LF> | |
13 | // Example: $FREVE,000001,DZ00513,Fire Alarm On: TEST DZ201 Name*0A | |
14 | type EVE struct { | |
15 | BaseSentence | |
16 | Time Time // Event Time | |
17 | TagCode string // Tag code | |
18 | Message string // Event text | |
19 | } | |
20 | ||
21 | // newEVE constructor | |
22 | func newEVE(s BaseSentence) (EVE, error) { | |
23 | p := NewParser(s) | |
24 | p.AssertType(TypeEVE) | |
25 | return EVE{ | |
26 | BaseSentence: s, | |
27 | Time: p.Time(0, "time"), | |
28 | TagCode: p.String(1, "tag code"), | |
29 | Message: p.String(2, "event message text"), | |
30 | }, p.Err() | |
31 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestEVE(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg EVE | |
13 | }{ | |
14 | { | |
15 | name: "good sentence", | |
16 | raw: "$FREVE,000001,DZ00513,Fire Alarm On: TEST DZ201 Name*0A", | |
17 | msg: EVE{ | |
18 | Time: Time{ | |
19 | Valid: true, | |
20 | Hour: 0, | |
21 | Minute: 0, | |
22 | Second: 1, | |
23 | Millisecond: 0, | |
24 | }, | |
25 | TagCode: "DZ00513", | |
26 | Message: "Fire Alarm On: TEST DZ201 Name", | |
27 | }, | |
28 | }, | |
29 | { | |
30 | name: "invalid nmea: Time", | |
31 | raw: "$FREVE,0x0001,DZ00513,Fire Alarm On: TEST DZ201 Name*42", | |
32 | err: "nmea: FREVE invalid time: 0x0001", | |
33 | }, | |
34 | } | |
35 | for _, tt := range tests { | |
36 | t.Run(tt.name, func(t *testing.T) { | |
37 | m, err := Parse(tt.raw) | |
38 | if tt.err != "" { | |
39 | assert.Error(t, err) | |
40 | assert.EqualError(t, err, tt.err) | |
41 | } else { | |
42 | assert.NoError(t, err) | |
43 | eve := m.(EVE) | |
44 | eve.BaseSentence = BaseSentence{} | |
45 | assert.Equal(t, tt.msg, eve) | |
46 | } | |
47 | }) | |
48 | } | |
49 | } |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypeFIR type of FIR sentence for Fire Detection | |
4 | TypeFIR = "FIR" | |
5 | ||
6 | // TypeEventOrAlarmFIR is Event, Fire Alarm type | |
7 | TypeEventOrAlarmFIR = "E" | |
8 | // TypeFaultFIR is type for Fault | |
9 | TypeFaultFIR = "F" | |
10 | // TypeDisablementFIR is type for detector disablement | |
11 | TypeDisablementFIR = "D" | |
12 | ||
13 | // ConditionActivationFIR is activation condition | |
14 | ConditionActivationFIR = "A" | |
15 | // ConditionNonActivationFIR is non-activation condition | |
16 | ConditionNonActivationFIR = "V" | |
17 | // ConditionUnknownFIR is unknown condition | |
18 | ConditionUnknownFIR = "X" | |
19 | ||
20 | // AlarmStateAcknowledgedFIR is value for alarm acknowledgement | |
21 | AlarmStateAcknowledgedFIR = "A" | |
22 | // AlarmStateNotAcknowledgedFIR is value for alarm being not acknowledged | |
23 | AlarmStateNotAcknowledgedFIR = "V" | |
24 | ) | |
25 | ||
26 | // FIR - Fire Detection event with time and location | |
27 | // Source: "Interfacing Voyage Data Recorder Systems, AutroSafe Interactive Fire-Alarm System, 116-P-BSL336/EE, RevA 2007-01-25, | |
28 | // Autronica Fire and Security AS " (page 39 | p.8.1.6) | |
29 | // https://product.autronicafire.com/fileshare/fileupload/14251/bsl336_ee.pdf | |
30 | // | |
31 | // Format: $FRFIR,a,hhmmss,aa,aa,xxx,xxx,a,a,c--c*hh<CR><LF> | |
32 | // Example: $FRFIR,E,103000,FD,PT,000,007,A,V,Fire Alarm : TEST PT7 Name TEST DZ2 Name*7A | |
33 | type FIR struct { | |
34 | BaseSentence | |
35 | ||
36 | // Type is type of the message | |
37 | // * E – Event, Fire Alarm | |
38 | // * F – Fault | |
39 | // * D – Disablement | |
40 | Type string | |
41 | ||
42 | // Time is Event Time | |
43 | Time Time | |
44 | ||
45 | // SystemIndicator is system indicator. Detector system type with 2 char identifier. | |
46 | // * FD Generic fire detector | |
47 | // * FH Heat detector | |
48 | // * FS Smoke detector | |
49 | // * FD Smoke and heat detector | |
50 | // * FM Manual call point | |
51 | // * GD Any gas detector | |
52 | // * GO Oxygen gas detector | |
53 | // * GS Hydrogen sulphide gas detector | |
54 | // * GH Hydro-carbon gas detector | |
55 | // * SF Sprinkler flow switch | |
56 | // * SV Sprinkler manual valve release | |
57 | // * CO CO2 manual release | |
58 | // * OT Other | |
59 | SystemIndicator string | |
60 | ||
61 | // DivisionIndicator1 is first division indicator for locating origin detector for this message | |
62 | DivisionIndicator1 string | |
63 | ||
64 | // DivisionIndicator2 is second division indicator for locating origin detector for this message | |
65 | DivisionIndicator2 int64 | |
66 | ||
67 | // FireDetectorNumberOrCount is Fire detector number or activated detectors count (seems to be field with overloaded meaning) | |
68 | FireDetectorNumberOrCount int64 | |
69 | ||
70 | // Condition describes the condition triggering current message | |
71 | // * A – Activation | |
72 | // * V – Non-activation | |
73 | // * X – State unknown | |
74 | Condition string | |
75 | ||
76 | // AlarmAckState is Alarm's acknowledge state | |
77 | // * A – Acknowledged | |
78 | // * V – Not acknowledged | |
79 | AlarmAckState string | |
80 | ||
81 | // Message's description text (could be cut to fit max packet length) | |
82 | Message string | |
83 | } | |
84 | ||
85 | // newFIR constructor | |
86 | func newFIR(s BaseSentence) (FIR, error) { | |
87 | p := NewParser(s) | |
88 | p.AssertType(TypeFIR) | |
89 | return FIR{ | |
90 | BaseSentence: s, | |
91 | Type: p.EnumString(0, "message type", TypeEventOrAlarmFIR, TypeFaultFIR, TypeDisablementFIR), | |
92 | Time: p.Time(1, "time"), | |
93 | SystemIndicator: p.String(2, "system indicator"), | |
94 | DivisionIndicator1: p.String(3, "division indicator 1"), | |
95 | DivisionIndicator2: p.Int64(4, "division indicator 2"), | |
96 | FireDetectorNumberOrCount: p.Int64(5, "fire detector number or count"), | |
97 | Condition: p.EnumString(6, "condition", ConditionActivationFIR, ConditionNonActivationFIR, ConditionUnknownFIR), | |
98 | AlarmAckState: p.EnumString(7, "alarm acknowledgement state", AlarmStateAcknowledgedFIR, AlarmStateNotAcknowledgedFIR), | |
99 | Message: p.String(8, "message"), | |
100 | }, p.Err() | |
101 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestFIR(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg FIR | |
13 | }{ | |
14 | { | |
15 | name: "good sentence", | |
16 | raw: "$FRFIR,E,103000,FD,PT,000,007,A,V,Fire Alarm : TEST PT7 Name TEST DZ2 Name*7A", | |
17 | msg: FIR{ | |
18 | Type: TypeEventOrAlarmFIR, | |
19 | Time: Time{ | |
20 | Valid: true, | |
21 | Hour: 10, | |
22 | Minute: 30, | |
23 | Second: 0, | |
24 | Millisecond: 0, | |
25 | }, | |
26 | SystemIndicator: "FD", | |
27 | DivisionIndicator1: "PT", | |
28 | DivisionIndicator2: 0, | |
29 | FireDetectorNumberOrCount: 7, | |
30 | Condition: ConditionActivationFIR, | |
31 | AlarmAckState: AlarmStateNotAcknowledgedFIR, | |
32 | Message: "Fire Alarm : TEST PT7 Name TEST DZ2 Name", | |
33 | }, | |
34 | }, | |
35 | { | |
36 | name: "invalid nmea: Type", | |
37 | raw: "$FRFIR,x,103000,FD,PT,000,007,A,V,Fire Alarm : TEST PT7 Name TEST DZ2 Name*47", | |
38 | err: "nmea: FRFIR invalid message type: x", | |
39 | }, | |
40 | { | |
41 | name: "invalid nmea: Time", | |
42 | raw: "$FRFIR,E,1x3000,FD,PT,000,007,A,V,Fire Alarm : TEST PT7 Name TEST DZ2 Name*32", | |
43 | err: "nmea: FRFIR invalid time: 1x3000", | |
44 | }, | |
45 | { | |
46 | name: "invalid nmea: Condition", | |
47 | raw: "$FRFIR,E,103000,FD,PT,000,007,_,V,Fire Alarm : TEST PT7 Name TEST DZ2 Name*64", | |
48 | err: "nmea: FRFIR invalid condition: _", | |
49 | }, | |
50 | { | |
51 | name: "invalid nmea: AlarmAckState", | |
52 | raw: "$FRFIR,E,103000,FD,PT,000,007,A,_,Fire Alarm : TEST PT7 Name TEST DZ2 Name*73", | |
53 | err: "nmea: FRFIR invalid alarm acknowledgement state: _", | |
54 | }, | |
55 | } | |
56 | for _, tt := range tests { | |
57 | t.Run(tt.name, func(t *testing.T) { | |
58 | m, err := Parse(tt.raw) | |
59 | if tt.err != "" { | |
60 | assert.Error(t, err) | |
61 | assert.EqualError(t, err, tt.err) | |
62 | } else { | |
63 | assert.NoError(t, err) | |
64 | fir := m.(FIR) | |
65 | fir.BaseSentence = BaseSentence{} | |
66 | assert.Equal(t, tt.msg, fir) | |
67 | } | |
68 | }) | |
69 | } | |
70 | } |
19 | 19 | ) |
20 | 20 | |
21 | 21 | // GGA is the Time, position, and fix related data of the receiver. |
22 | // http://aprs.gids.nl/nmea/#gga | |
23 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_gga_global_positioning_system_fix_data | |
24 | // | |
25 | // Format: $--GGA,hhmmss.ss,ddmm.mm,a,ddmm.mm,a,x,xx,x.x,x.x,M,x.x,M,x.x,xxxx*hh<CR><LF> | |
26 | // Example: $GNGGA,203415.000,6325.6138,N,01021.4290,E,1,8,2.42,72.5,M,41.5,M,,*7C | |
22 | 27 | type GGA struct { |
23 | 28 | BaseSentence |
24 | 29 | Time Time // Time of fix. |
10 | 10 | |
11 | 11 | // GLL is Geographic Position, Latitude / Longitude and time. |
12 | 12 | // http://aprs.gids.nl/nmea/#gll |
13 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_gll_geographic_position_latitudelongitude | |
14 | // | |
15 | // Format : $--GLL,ddmm.mm,a,dddmm.mm,a,hhmmss.ss,a*hh<CR><LF> | |
16 | // Format (NMEA 2.3+): $--GLL,ddmm.mm,a,dddmm.mm,a,hhmmss.ss,a,m*hh<CR><LF> | |
17 | // Example: $IIGLL,5924.462,N,01030.048,E,062216,A*38 | |
18 | // Example: $GNGLL,4404.14012,N,12118.85993,W,001037.00,A,A*67 | |
13 | 19 | type GLL struct { |
14 | 20 | BaseSentence |
15 | 21 | Latitude float64 // Latitude |
16 | 22 | Longitude float64 // Longitude |
17 | 23 | Time Time // Time Stamp |
18 | Validity string // validity - A-valid | |
24 | Validity string // validity - A=valid, V=invalid | |
25 | FFAMode string // FAA mode indicator (filled in NMEA 2.3 and later) | |
19 | 26 | } |
20 | 27 | |
21 | 28 | // newGLL constructor |
22 | 29 | func newGLL(s BaseSentence) (GLL, error) { |
23 | 30 | p := NewParser(s) |
24 | 31 | p.AssertType(TypeGLL) |
25 | return GLL{ | |
32 | gll := GLL{ | |
26 | 33 | BaseSentence: s, |
27 | 34 | Latitude: p.LatLong(0, 1, "latitude"), |
28 | 35 | Longitude: p.LatLong(2, 3, "longitude"), |
29 | 36 | Time: p.Time(4, "time"), |
30 | 37 | Validity: p.EnumString(5, "validity", ValidGLL, InvalidGLL), |
31 | }, p.Err() | |
38 | } | |
39 | if len(p.Fields) > 6 { | |
40 | gll.FFAMode = p.String(6, "FAA mode") | |
41 | } | |
42 | return gll, p.Err() | |
32 | 43 | } |
25 | 25 | Millisecond: 0, |
26 | 26 | }, |
27 | 27 | Validity: "A", |
28 | FFAMode: FAAModeAutonomous, | |
29 | }, | |
30 | }, | |
31 | { | |
32 | name: "good sentence without FAA mode", | |
33 | raw: "$IIGLL,5924.462,N,01030.048,E,062216,A*38", | |
34 | msg: GLL{ | |
35 | Latitude: MustParseLatLong("5924.462 N"), | |
36 | Longitude: MustParseLatLong("01030.048 E"), | |
37 | Time: Time{ | |
38 | Valid: true, | |
39 | Hour: 6, | |
40 | Minute: 22, | |
41 | Second: 16, | |
42 | Millisecond: 0, | |
43 | }, | |
44 | Validity: "A", | |
45 | FFAMode: "", | |
28 | 46 | }, |
29 | 47 | }, |
30 | 48 | { |
2 | 2 | const ( |
3 | 3 | // TypeGNS type for GNS sentences |
4 | 4 | TypeGNS = "GNS" |
5 | ) | |
6 | ||
7 | // GNS mode values. These are same values ans GLL/RMC FAAMode* values. | |
8 | // Note: there can be other values (proprietary). | |
9 | const ( | |
5 | 10 | // NoFixGNS Character |
6 | 11 | NoFixGNS = "N" |
7 | 12 | // AutonomousGNS Character |
23 | 28 | ) |
24 | 29 | |
25 | 30 | // GNS is standard GNSS sentance that combined multiple constellations |
31 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_gns_fix_data | |
32 | // | |
33 | // Format: $--GNS,hhmmss.ss,ddmm.mm,a,dddmm.mm,a,c--c,xx,x.x,x.x,x.x,x.x,x.x*hh<CR><LF> | |
34 | // Example: $GNGNS,014035.00,4332.69262,S,17235.48549,E,RR,13,0.9,25.63,11.24,,*70 | |
35 | // $GPGNS,224749.00,3333.4268304,N,11153.3538273,W,D,19,0.6,406.110,-26.294,6.0,0138,S*6A | |
26 | 36 | type GNS struct { |
27 | 37 | BaseSentence |
28 | Time Time | |
29 | Latitude float64 | |
30 | Longitude float64 | |
38 | Time Time // UTC of position | |
39 | Latitude float64 | |
40 | Longitude float64 | |
41 | // FAA mode indicator for each satellite navigation system (constellation) supported by device. | |
42 | // | |
43 | // May be up to six characters (according to GPSD). | |
44 | // '1' - GPS | |
45 | // '2' - GLONASS | |
46 | // '3' - Galileo | |
47 | // '4' - BDS | |
48 | // '5' - QZSS | |
49 | // '6' - NavIC (IRNSS) | |
31 | 50 | Mode []string |
32 | SVs int64 | |
33 | HDOP float64 | |
34 | Altitude float64 | |
35 | Separation float64 | |
36 | Age float64 | |
37 | Station int64 | |
51 | SVs int64 // Total number of satellites in use, 00-99 | |
52 | HDOP float64 // Horizontal Dilution of Precision | |
53 | Altitude float64 // Antenna altitude, meters, re:mean-sea-level(geoid). | |
54 | Separation float64 // Geoidal separation meters | |
55 | Age float64 // Age of differential data | |
56 | Station int64 // Differential reference station ID | |
57 | NavStatus string // Navigation status (NMEA 4.1+). See NavStats* (`NavStatusAutonomous` etc) constants for possible values. | |
38 | 58 | } |
39 | 59 | |
40 | 60 | // newGNS Constructor |
54 | 74 | Age: p.Float64(10, "age"), |
55 | 75 | Station: p.Int64(11, "station"), |
56 | 76 | } |
77 | if len(p.Fields) >= 13 { | |
78 | m.NavStatus = p.EnumString( | |
79 | 12, | |
80 | "navigation status", | |
81 | NavStatusAutonomous, | |
82 | NavStatusDifferential, | |
83 | NavStatusEstimated, | |
84 | NavStatusManualInput, | |
85 | NavStatusSimulated, | |
86 | NavStatusDataNotValid, | |
87 | NavStatusDataValid, | |
88 | ) | |
89 | } | |
57 | 90 | return m, p.Err() |
58 | 91 | } |
25 | 25 | Separation: 11.24, |
26 | 26 | Age: 0, |
27 | 27 | Station: 0, |
28 | NavStatus: "", | |
28 | 29 | }, |
29 | 30 | }, |
30 | 31 | { |
41 | 42 | Separation: 48.0, |
42 | 43 | Age: 0, |
43 | 44 | Station: 0, |
45 | NavStatus: "", | |
44 | 46 | }, |
45 | 47 | }, |
46 | 48 | { |
47 | name: "good sentence B", | |
49 | name: "good sentence C", | |
48 | 50 | raw: "$GNGNS,094821.0,4849.931307,N,00216.053323,E,AAN,14,0.6,161.5,48.0,,*23", |
49 | 51 | msg: GNS{ |
50 | 52 | Time: Time{true, 9, 48, 21, 0}, |
57 | 59 | Separation: 48.0, |
58 | 60 | Age: 0, |
59 | 61 | Station: 0, |
62 | NavStatus: "", | |
63 | }, | |
64 | }, | |
65 | { | |
66 | name: "good sentence D with nav status", | |
67 | raw: "$GPGNS,224749.00,3333.4268304,N,11153.3538273,W,D,19,0.6,406.110,-26.294,6.0,0138,S*6A", | |
68 | msg: GNS{ | |
69 | Time: Time{Valid: true, Hour: 22, Minute: 47, Second: 49, Millisecond: 0}, | |
70 | Latitude: 33.55711384000001, | |
71 | Longitude: -111.88923045499999, | |
72 | Mode: []string{"D"}, | |
73 | SVs: 19, | |
74 | HDOP: 0.6, | |
75 | Altitude: 406.11, | |
76 | Separation: -26.294, | |
77 | Age: 6, | |
78 | Station: 138, | |
79 | NavStatus: "S", | |
60 | 80 | }, |
61 | 81 | }, |
62 | 82 | { |
16 | 16 | |
17 | 17 | // GSA represents overview satellite data. |
18 | 18 | // http://aprs.gids.nl/nmea/#gsa |
19 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_gsa_gps_dop_and_active_satellites | |
20 | // | |
21 | // 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<CR><LF> | |
22 | // 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<CR><LF> | |
23 | // Example: $GNGSA,A,3,80,71,73,79,69,,,,,,,,1.83,1.09,1.47*17 | |
24 | // Example (NMEA 4.1+): $GNGSA,A,3,13,12,22,19,08,21,,,,,,,1.05,0.64,0.83,4*0B | |
19 | 25 | type GSA struct { |
20 | 26 | BaseSentence |
21 | 27 | Mode string // The selection mode. |
24 | 30 | PDOP float64 // Dilution of precision. |
25 | 31 | HDOP float64 // Horizontal dilution of precision. |
26 | 32 | VDOP float64 // Vertical dilution of precision. |
33 | // SystemID is (GNSS) System ID (NMEA 4.1+) | |
34 | // 1 - GPS | |
35 | // 2 - GLONASS | |
36 | // 3 - Galileo | |
37 | // 4 - BeiDou | |
38 | // 5 - QZSS | |
39 | // 6 - NavID (IRNSS) | |
40 | SystemID int64 | |
27 | 41 | } |
28 | 42 | |
29 | 43 | // newGSA parses the GSA sentence into this struct. |
45 | 59 | m.PDOP = p.Float64(14, "pdop") |
46 | 60 | m.HDOP = p.Float64(15, "hdop") |
47 | 61 | m.VDOP = p.Float64(16, "vdop") |
62 | ||
63 | if len(p.Fields) > 17 { | |
64 | m.SystemID = p.Int64(17, "system ID") | |
65 | } | |
48 | 66 | return m, p.Err() |
49 | 67 | } |
21 | 21 | PDOP: 3.1, |
22 | 22 | HDOP: 2, |
23 | 23 | VDOP: 2.4, |
24 | }, | |
25 | }, | |
26 | { | |
27 | name: "good sentence with system id", | |
28 | raw: "$GNGSA,A,3,13,12,22,19,08,21,,,,,,,1.05,0.64,0.83,4*0B", | |
29 | msg: GSA{ | |
30 | Mode: "A", | |
31 | FixType: "3", | |
32 | SV: []string{"13", "12", "22", "19", "08", "21"}, | |
33 | PDOP: 1.05, | |
34 | HDOP: 0.64, | |
35 | VDOP: 0.83, | |
36 | SystemID: 4, | |
24 | 37 | }, |
25 | 38 | }, |
26 | 39 | { |
0 | 0 | package nmea |
1 | 1 | |
2 | 2 | const ( |
3 | // TypeGSV type for GSV sentences | |
3 | // TypeGSV type of GSV sentences for satellites in view | |
4 | 4 | TypeGSV = "GSV" |
5 | 5 | ) |
6 | 6 | |
7 | 7 | // GSV represents the GPS Satellites in view |
8 | 8 | // http://aprs.gids.nl/nmea/#glgsv |
9 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_gsv_satellites_in_view | |
10 | // | |
11 | // Format: $--GSV,x,x,x,x,x,x,x,...*hh<CR><LF> | |
12 | // Format (NMEA 4.1+): $--GSV,x,x,x,x,x,x,x,...,x*hh<CR><LF> | |
13 | // Example: $GPGSV,3,1,11,09,76,148,32,05,55,242,29,17,33,054,30,14,27,314,24*71 | |
14 | // Example (NMEA 4.1+): $GAGSV,3,1,09,02,00,179,,04,09,321,,07,11,134,11,11,10,227,,7*7F | |
9 | 15 | type GSV struct { |
10 | 16 | BaseSentence |
11 | 17 | TotalMessages int64 // Total number of messages of this type in this cycle |
12 | 18 | MessageNumber int64 // Message number |
13 | 19 | NumberSVsInView int64 // Total number of SVs in view |
14 | 20 | Info []GSVInfo // visible satellite info (0-4 of these) |
21 | // SystemID is (GNSS) System ID (NMEA 4.1+) | |
22 | // 1 - GPS | |
23 | // 2 - GLONASS | |
24 | // 3 - Galileo | |
25 | // 4 - BeiDou | |
26 | // 5 - QZSS | |
27 | // 6 - NavID (IRNSS) | |
28 | SystemID int64 | |
15 | 29 | } |
16 | 30 | |
17 | 31 | // GSVInfo represents information about a visible satellite |
32 | 46 | MessageNumber: p.Int64(1, "message number"), |
33 | 47 | NumberSVsInView: p.Int64(2, "number of SVs in view"), |
34 | 48 | } |
35 | for i := 0; i < 4; i++ { | |
36 | if 5*i+4 > len(m.Fields) { | |
49 | i := 0 | |
50 | for ; i < 4; i++ { | |
51 | if 6+i*4 >= len(m.Fields) { | |
37 | 52 | break |
38 | 53 | } |
39 | 54 | m.Info = append(m.Info, GSVInfo{ |
43 | 58 | SNR: p.Int64(6+i*4, "SNR"), |
44 | 59 | }) |
45 | 60 | } |
61 | idxSID := (6 + (i-1)*4) + 1 | |
62 | if len(p.Fields) == idxSID+1 { | |
63 | m.SystemID = p.Int64(idxSID, "system ID") | |
64 | } | |
46 | 65 | return m, p.Err() |
47 | 66 | } |
38 | 38 | {SVPRNNumber: 4, Elevation: 15, Azimuth: 270, SNR: 0}, |
39 | 39 | {SVPRNNumber: 6, Elevation: 1, Azimuth: 10, SNR: 12}, |
40 | 40 | }, |
41 | }, | |
42 | }, | |
43 | { | |
44 | name: "sentence with no satellite in view", | |
45 | raw: "$GBGSV,1,1,00,0*77", | |
46 | msg: GSV{ | |
47 | TotalMessages: 1, | |
48 | MessageNumber: 1, | |
49 | NumberSVsInView: 0, | |
50 | Info: nil, | |
51 | }, | |
52 | }, | |
53 | { | |
54 | name: "good sentence with system id", | |
55 | raw: "$GAGSV,3,1,09,02,00,179,,04,09,321,,07,11,134,11,11,10,227,,7*7F", | |
56 | msg: GSV{ | |
57 | TotalMessages: 3, | |
58 | MessageNumber: 1, | |
59 | NumberSVsInView: 9, | |
60 | Info: []GSVInfo{ | |
61 | {SVPRNNumber: 2, Elevation: 0, Azimuth: 179, SNR: 0}, | |
62 | {SVPRNNumber: 4, Elevation: 9, Azimuth: 321, SNR: 0}, | |
63 | {SVPRNNumber: 7, Elevation: 11, Azimuth: 134, SNR: 11}, | |
64 | {SVPRNNumber: 11, Elevation: 10, Azimuth: 227, SNR: 0}, | |
65 | }, | |
66 | SystemID: 7, | |
41 | 67 | }, |
42 | 68 | }, |
43 | 69 | { |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypeHDG type of HDG sentence for vessel heading, deviation and variation with respect to magnetic north. | |
4 | TypeHDG = "HDG" | |
5 | ) | |
6 | ||
7 | // HDG is vessel heading (in degrees), deviation and variation with respect to magnetic north produced by any | |
8 | // device or system producing magnetic reading. | |
9 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_hdg_heading_deviation_variation | |
10 | // | |
11 | // Format: $--HDG,x.x,y.y,a,z.z,a*hr<CR><LF> | |
12 | // Example: $HCHDG,98.3,0.0,E,12.6,W*57 | |
13 | type HDG struct { | |
14 | BaseSentence | |
15 | Heading float64 // Heading in degrees | |
16 | Deviation float64 // Magnetic Deviation in degrees | |
17 | DeviationDirection string // Magnetic Deviation direction, E = Easterly, W = Westerly | |
18 | Variation float64 // Magnetic Variation in degrees | |
19 | VariationDirection string // Magnetic Variation direction, E = Easterly, W = Westerly | |
20 | } | |
21 | ||
22 | // newHDG constructor | |
23 | func newHDG(s BaseSentence) (HDG, error) { | |
24 | p := NewParser(s) | |
25 | p.AssertType(TypeHDG) | |
26 | m := HDG{ | |
27 | BaseSentence: s, | |
28 | Heading: p.Float64(0, "heading"), | |
29 | Deviation: p.Float64(1, "deviation"), | |
30 | DeviationDirection: p.EnumString(2, "deviation direction", East, West), | |
31 | Variation: p.Float64(3, "variation"), | |
32 | VariationDirection: p.EnumString(4, "variation direction", East, West), | |
33 | } | |
34 | return m, p.Err() | |
35 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestHDG(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg HDG | |
13 | }{ | |
14 | { | |
15 | name: "good sentence", | |
16 | raw: "$HCHDG,98.3,0.1,E,12.6,W*56", | |
17 | msg: HDG{ | |
18 | Heading: 98.3, | |
19 | Deviation: 0.1, | |
20 | DeviationDirection: East, | |
21 | Variation: 12.6, | |
22 | VariationDirection: West, | |
23 | }, | |
24 | }, | |
25 | { | |
26 | name: "invalid Heading", | |
27 | raw: "$HCHDG,X,0.1,E,12.6,W*12", | |
28 | err: "nmea: HCHDG invalid heading: X", | |
29 | }, | |
30 | { | |
31 | name: "invalid Deviation", | |
32 | raw: "$HCHDG,98.3,x.1,E,12.6,W*1E", | |
33 | err: "nmea: HCHDG invalid deviation: x.1", | |
34 | }, | |
35 | { | |
36 | name: "invalid DeviationDirection", | |
37 | raw: "$HCHDG,98.3,0.1,X,12.6,W*4B", | |
38 | err: "nmea: HCHDG invalid deviation direction: X", | |
39 | }, | |
40 | { | |
41 | name: "invalid Variation", | |
42 | raw: "$HCHDG,98.3,0.1,E,x.1,W*2A", | |
43 | err: "nmea: HCHDG invalid variation: x.1", | |
44 | }, | |
45 | { | |
46 | name: "invalid VariationDirection", | |
47 | raw: "$HCHDG,98.3,0.1,E,12.6,X*59", | |
48 | err: "nmea: HCHDG invalid variation direction: X", | |
49 | }, | |
50 | } | |
51 | for _, tt := range tests { | |
52 | t.Run(tt.name, func(t *testing.T) { | |
53 | m, err := Parse(tt.raw) | |
54 | if tt.err != "" { | |
55 | assert.Error(t, err) | |
56 | assert.EqualError(t, err, tt.err) | |
57 | } else { | |
58 | assert.NoError(t, err) | |
59 | hdg := m.(HDG) | |
60 | hdg.BaseSentence = BaseSentence{} | |
61 | assert.Equal(t, tt.msg, hdg) | |
62 | } | |
63 | }) | |
64 | } | |
65 | } |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypeHDM type of HDM sentence for vessel heading in degrees with respect to magnetic north | |
4 | TypeHDM = "HDM" | |
5 | // MagneticHDM for valid Magnetic heading | |
6 | MagneticHDM = "M" | |
7 | ) | |
8 | ||
9 | // HDM is vessel heading in degrees with respect to magnetic north produced by any device or system producing magnetic heading. | |
10 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_hdm_heading_magnetic | |
11 | // | |
12 | // Format: $--HDM,xxx.xx,M*hh<CR><LF> | |
13 | // Example: $HCHDM,093.8,M*2B | |
14 | type HDM struct { | |
15 | BaseSentence | |
16 | Heading float64 // Heading in degrees | |
17 | MagneticValid bool // Heading is respect to magnetic north | |
18 | } | |
19 | ||
20 | // newHDM constructor | |
21 | func newHDM(s BaseSentence) (HDM, error) { | |
22 | p := NewParser(s) | |
23 | p.AssertType(TypeHDM) | |
24 | m := HDM{ | |
25 | BaseSentence: s, | |
26 | Heading: p.Float64(0, "heading"), | |
27 | MagneticValid: p.EnumString(1, "magnetic", MagneticHDM) == MagneticHDM, | |
28 | } | |
29 | return m, p.Err() | |
30 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestHDM(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg HDM | |
13 | }{ | |
14 | { | |
15 | name: "good sentence", | |
16 | raw: "$HCHDM,093.8,M*2B", | |
17 | msg: HDM{ | |
18 | Heading: 93.8, | |
19 | MagneticValid: true, | |
20 | }, | |
21 | }, | |
22 | { | |
23 | name: "invalid Magnetic", | |
24 | raw: "$HCHDM,093.8,X*3E", | |
25 | err: "nmea: HCHDM invalid magnetic: X", | |
26 | }, | |
27 | { | |
28 | name: "invalid Heading", | |
29 | raw: "$HCHDM,09X.X,M*20", | |
30 | err: "nmea: HCHDM invalid heading: 09X.X", | |
31 | }, | |
32 | } | |
33 | for _, tt := range tests { | |
34 | t.Run(tt.name, func(t *testing.T) { | |
35 | m, err := Parse(tt.raw) | |
36 | if tt.err != "" { | |
37 | assert.Error(t, err) | |
38 | assert.EqualError(t, err, tt.err) | |
39 | } else { | |
40 | assert.NoError(t, err) | |
41 | hdm := m.(HDM) | |
42 | hdm.BaseSentence = BaseSentence{} | |
43 | assert.Equal(t, tt.msg, hdm) | |
44 | } | |
45 | }) | |
46 | } | |
47 | } |
6 | 6 | |
7 | 7 | // HDT is the Actual vessel heading in degrees True. |
8 | 8 | // http://aprs.gids.nl/nmea/#hdt |
9 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_gsv_satellites_in_view | |
10 | // | |
11 | // Format: $--HDT,x.x,T*hh<CR><LF> | |
12 | // Example: $GPHDT,274.07,T*03 | |
9 | 13 | type HDT struct { |
10 | 14 | BaseSentence |
11 | 15 | Heading float64 // Heading in degrees |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypeHSC type of HSC sentence for Heading steering command | |
4 | TypeHSC = "HSC" | |
5 | ) | |
6 | ||
7 | // HSC - Heading steering command | |
8 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_hsc_heading_steering_command | |
9 | // https://www.tronico.fi/OH6NT/docs/NMEA0183.pdf (page 11) | |
10 | // | |
11 | // Format: $--HSC, x.x, T, x.x, M,a*hh<CR><LF> | |
12 | // Example: $FTHSC,40.12,T,39.11,M*5E | |
13 | type HSC struct { | |
14 | BaseSentence | |
15 | TrueHeading float64 // Heading Degrees, True | |
16 | TrueHeadingType string // T = True | |
17 | MagneticHeading float64 // Heading Degrees, Magnetic | |
18 | MagneticHeadingType string // M = Magnetic | |
19 | } | |
20 | ||
21 | // newHSC constructor | |
22 | func newHSC(s BaseSentence) (HSC, error) { | |
23 | p := NewParser(s) | |
24 | p.AssertType(TypeHSC) | |
25 | return HSC{ | |
26 | BaseSentence: s, | |
27 | TrueHeading: p.Float64(0, "true heading"), | |
28 | TrueHeadingType: p.EnumString(1, "true heading type", HeadingTrue), | |
29 | MagneticHeading: p.Float64(2, "magnetic heading"), | |
30 | MagneticHeadingType: p.EnumString(3, "magnetic heading type", HeadingMagnetic), | |
31 | }, p.Err() | |
32 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestHSC(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg HSC | |
13 | }{ | |
14 | { | |
15 | name: "good sentence", | |
16 | raw: "$FTHSC,40.12,T,39.11,M*5E", | |
17 | msg: HSC{ | |
18 | TrueHeading: 40.12, | |
19 | TrueHeadingType: HeadingTrue, | |
20 | MagneticHeading: 39.11, | |
21 | MagneticHeadingType: HeadingMagnetic, | |
22 | }, | |
23 | }, | |
24 | { | |
25 | name: "invalid nmea: TrueHeading", | |
26 | raw: "$FTHSC,40.1x,T,39.11,M*14", | |
27 | err: "nmea: FTHSC invalid true heading: 40.1x", | |
28 | }, | |
29 | { | |
30 | name: "invalid nmea: TrueHeadingType", | |
31 | raw: "$FTHSC,40.12,x,39.11,M*72", | |
32 | err: "nmea: FTHSC invalid true heading type: x", | |
33 | }, | |
34 | { | |
35 | name: "invalid nmea: MagneticHeading", | |
36 | raw: "$FTHSC,40.12,T,x,M*02", | |
37 | err: "nmea: FTHSC invalid magnetic heading: x", | |
38 | }, | |
39 | { | |
40 | name: "invalid nmea: MagneticHeadingType", | |
41 | raw: "$FTHSC,40.12,T,39.11,x*6b", | |
42 | err: "nmea: FTHSC invalid magnetic heading type: x", | |
43 | }, | |
44 | } | |
45 | for _, tt := range tests { | |
46 | t.Run(tt.name, func(t *testing.T) { | |
47 | m, err := Parse(tt.raw) | |
48 | if tt.err != "" { | |
49 | assert.Error(t, err) | |
50 | assert.EqualError(t, err, tt.err) | |
51 | } else { | |
52 | assert.NoError(t, err) | |
53 | hsc := m.(HSC) | |
54 | hsc.BaseSentence = BaseSentence{} | |
55 | assert.Equal(t, tt.msg, hsc) | |
56 | } | |
57 | }) | |
58 | } | |
59 | } |
53 | 53 | |
54 | 54 | // MDA is the Meteorological Composite |
55 | 55 | // Data of air pressure, air and water temperatures and wind speed and direction |
56 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_mda_meteorological_composite | |
57 | // https://opencpn.org/wiki/dokuwiki/doku.php?id=opencpn:opencpn_user_manual:advanced_features:nmea_sentences#mda | |
58 | // | |
59 | // 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<CR><LF> | |
60 | // 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 | |
56 | 61 | type MDA struct { |
57 | 62 | BaseSentence |
58 | 63 | PressureInch float64 |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypeMTA type of MTA sentence for Air Temperature | |
4 | TypeMTA = "MTA" | |
5 | ) | |
6 | ||
7 | // MTA - Air Temperature (obsolete, use XDR instead) | |
8 | // https://www.nmea.org/Assets/100108_nmea_0183_sentences_not_recommended_for_new_designs.pdf (page 7) | |
9 | // | |
10 | // Format: $--MTA,x.x,C*hh<CR><LF> | |
11 | // Example: $IIMTA,13.3,C*04 | |
12 | type MTA struct { | |
13 | BaseSentence | |
14 | Temperature float64 // temperature | |
15 | Unit string // unit of temperature, should be degrees Celsius | |
16 | } | |
17 | ||
18 | // newMTA constructor | |
19 | func newMTA(s BaseSentence) (MTA, error) { | |
20 | p := NewParser(s) | |
21 | p.AssertType(TypeMTA) | |
22 | return MTA{ | |
23 | BaseSentence: s, | |
24 | Temperature: p.Float64(0, "temperature"), | |
25 | Unit: p.EnumString(1, "temperature unit", TemperatureCelsius), | |
26 | }, p.Err() | |
27 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestMTA(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg MTA | |
13 | }{ | |
14 | { | |
15 | name: "good sentence", | |
16 | raw: "$IIMTA,13.3,C*04", | |
17 | msg: MTA{ | |
18 | Temperature: 13.3, | |
19 | Unit: TemperatureCelsius, | |
20 | }, | |
21 | }, | |
22 | { | |
23 | name: "invalid nmea: Temperature", | |
24 | raw: "$IIMTA,x.x,C*35", | |
25 | err: "nmea: IIMTA invalid temperature: x.x", | |
26 | }, | |
27 | { | |
28 | name: "invalid nmea: Unit", | |
29 | raw: "$IIMTA,13.3,F*01", | |
30 | err: "nmea: IIMTA invalid temperature unit: F", | |
31 | }, | |
32 | } | |
33 | for _, tt := range tests { | |
34 | t.Run(tt.name, func(t *testing.T) { | |
35 | m, err := Parse(tt.raw) | |
36 | if tt.err != "" { | |
37 | assert.Error(t, err) | |
38 | assert.EqualError(t, err, tt.err) | |
39 | } else { | |
40 | assert.NoError(t, err) | |
41 | mta := m.(MTA) | |
42 | mta.BaseSentence = BaseSentence{} | |
43 | assert.Equal(t, tt.msg, mta) | |
44 | } | |
45 | }) | |
46 | } | |
47 | } |
4 | 4 | TypeMTK = "PMTK" |
5 | 5 | ) |
6 | 6 | |
7 | // MTK is the Time, position, and fix related data of the receiver. | |
7 | // MTK is sentence for NMEA embedded command packet protocol. | |
8 | // https://www.rhydolabz.com/documents/25/PMTK_A11.pdf | |
9 | // https://www.sparkfun.com/datasheets/GPS/Modules/PMTK_Protocol.pdf | |
10 | // | |
11 | // The maximum length of each packet is restricted to 255 bytes which is longer than NMEA0183 82 bytes. | |
12 | // | |
13 | // Format: $PMTKxxx,c-c*hh<CR><LF> | |
14 | // Example: $PMTK000*32<CR><LF> | |
15 | // $PMTK001,101,0*33<CR><LF> | |
8 | 16 | type MTK struct { |
9 | 17 | BaseSentence |
10 | Cmd, | |
18 | Cmd, // Three bytes character string. From "000" to "999". An identifier used to tell the decoder how to decode the packet | |
19 | // Flag is flag field in PMTK001 packet. | |
20 | // Note: this field on only relevant for `PMTK001,Cmd,Flag` sentence. | |
21 | // Actual MTK protocol has variable amount of fields (whole sentence can be up to 255 bytes) | |
22 | // | |
23 | // Actual docs say: | |
24 | // DataField: The DataField has variable length depending on the packet type. A comma symbol ‘,’ must be inserted | |
25 | // ahead each data filed to help the decoder process the DataField. | |
11 | 26 | Flag int64 |
12 | 27 | } |
13 | 28 |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypeMTW type of MWT sentence describing mean temperature of water | |
4 | TypeMTW = "MTW" | |
5 | // CelsiusMTW is MTW unit of measurement in celsius | |
6 | CelsiusMTW = "C" | |
7 | ) | |
8 | ||
9 | // MTW is sentence for mean temperature of water. | |
10 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_mtw_mean_temperature_of_water | |
11 | // | |
12 | // Format: $--MTW,TT.T,C*hh<CR><LF> | |
13 | // Example: $INMTW,17.9,C*1B | |
14 | type MTW struct { | |
15 | BaseSentence | |
16 | Temperature float64 // Temperature, degrees | |
17 | CelsiusValid bool // Is unit of measurement Celsius | |
18 | } | |
19 | ||
20 | // newMTW constructor | |
21 | func newMTW(s BaseSentence) (MTW, error) { | |
22 | p := NewParser(s) | |
23 | p.AssertType(TypeMTW) | |
24 | return MTW{ | |
25 | BaseSentence: s, | |
26 | Temperature: p.Float64(0, "temperature"), | |
27 | CelsiusValid: p.EnumString(1, "unit of measurement celsius", CelsiusMTW) == CelsiusMTW, | |
28 | }, p.Err() | |
29 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestMTW(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg MTW | |
13 | }{ | |
14 | { | |
15 | name: "good sentence", | |
16 | raw: "$INMTW,17.9,C*1B", | |
17 | msg: MTW{ | |
18 | Temperature: 17.9, | |
19 | CelsiusValid: true, | |
20 | }, | |
21 | }, | |
22 | { | |
23 | name: "invalid Temperature", | |
24 | raw: "$INMTW,x.9,C*65", | |
25 | err: "nmea: INMTW invalid temperature: x.9", | |
26 | }, | |
27 | { | |
28 | name: "invalid CelsiusValid", | |
29 | raw: "$INMTW,17.9,x*20", | |
30 | err: "nmea: INMTW invalid unit of measurement celsius: x", | |
31 | }, | |
32 | } | |
33 | for _, tt := range tests { | |
34 | t.Run(tt.name, func(t *testing.T) { | |
35 | m, err := Parse(tt.raw) | |
36 | if tt.err != "" { | |
37 | assert.Error(t, err) | |
38 | assert.EqualError(t, err, tt.err) | |
39 | } else { | |
40 | assert.NoError(t, err) | |
41 | mtw := m.(MTW) | |
42 | mtw.BaseSentence = BaseSentence{} | |
43 | assert.Equal(t, tt.msg, mtw) | |
44 | } | |
45 | }) | |
46 | } | |
47 | } |
28 | 28 | return l |
29 | 29 | } |
30 | 30 | |
31 | // ParseDecimal parses a decimal format coordinate and panics on error. | |
32 | func MustParseDecimal(s string) float64 { | |
33 | l, err := ParseDecimal(s) | |
34 | if err != nil { | |
35 | panic(err) | |
36 | } | |
37 | return l | |
38 | } | |
39 | ||
40 | 31 | // MustParseTime parses wall clock and panics on failure |
41 | 32 | func MustParseTime(s string) Time { |
42 | 33 | t, err := ParseTime(s) |
32 | 32 | ) |
33 | 33 | |
34 | 34 | // MWD Wind Direction and Speed, with respect to north. |
35 | // https://www.tronico.fi/OH6NT/docs/NMEA0183.pdf | |
36 | // http://gillinstruments.com/data/manuals/OMC-140_Operator_Manual_v1.04_131117.pdf | |
37 | // | |
38 | // Format: $--MWD,x.x,T,x.x,M,x.x,N,x.x,M*hh<CR><LF> | |
39 | // Example: $WIMWD,10.1,T,10.1,M,12,N,40,M*5D | |
35 | 40 | type MWD struct { |
36 | 41 | BaseSentence |
37 | 42 | WindDirectionTrue float64 |
47 | 47 | ) |
48 | 48 | |
49 | 49 | // MWV is the Wind Speed and Angle, in relation to the vessel’s bow/centerline. |
50 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_mwv_wind_speed_and_angle | |
51 | // | |
52 | // Format: $--MWV,x.x,a,x.x,a*hh<CR><LF> | |
53 | // Example: $WIMWV,12.1,T,10.1,N,A*27 | |
50 | 54 | type MWV struct { |
51 | 55 | BaseSentence |
52 | 56 | WindAngle float64 |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypeOSD type for OSD sentence for Own Ship Data | |
4 | TypeOSD = "OSD" | |
5 | ||
6 | // OSDReferenceBottomTrackingLog is reference for bottom tracking log | |
7 | OSDReferenceBottomTrackingLog = "B" | |
8 | // OSDReferenceManual is reference for manually entered | |
9 | OSDReferenceManual = "M" | |
10 | // OSDReferenceWaterReferenced is reference for water referenced | |
11 | OSDReferenceWaterReferenced = "W" | |
12 | // OSDReferenceRadarTracking is reference for radar tracking of fixed target | |
13 | OSDReferenceRadarTracking = "R" | |
14 | // OSDReferencePositioningSystemGroundReference is reference for positioning system ground reference | |
15 | OSDReferencePositioningSystemGroundReference = "P" | |
16 | ) | |
17 | ||
18 | // OSD - Own Ship Data | |
19 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_osd_own_ship_data | |
20 | // https://github.com/nohal/OpenCPN/wiki/ARPA-targets-tracking-implementation#osd---own-ship-data | |
21 | // | |
22 | // Format: $--OSD,x.x,A,x.x,a,x.x,a,x.x,x.x,a*hh<CR><LF> | |
23 | // Example: $RAOSD,179.0,A,179.0,M,00.0,M,,,N*76 | |
24 | type OSD struct { | |
25 | BaseSentence | |
26 | // Heading is Heading in degrees | |
27 | Heading float64 | |
28 | ||
29 | // HeadingStatus is Heading status | |
30 | // * A - data valid | |
31 | // * V - data invalid | |
32 | HeadingStatus string | |
33 | ||
34 | // VesselTrueCourse is Vessel Course, degrees True | |
35 | VesselTrueCourse float64 | |
36 | ||
37 | // CourseReference is Course Reference, B/M/W/R/P | |
38 | // * B - bottom tracking log | |
39 | // * M - manually entered | |
40 | // * W - water referenced | |
41 | // * R - radar tracking of fixed target | |
42 | // * P - positioning system ground reference | |
43 | CourseReference string | |
44 | ||
45 | // VesselSpeed is Vessel Speed | |
46 | VesselSpeed float64 | |
47 | ||
48 | // SpeedReference is Speed Reference, B/M/W/R/P | |
49 | // * B - bottom tracking log | |
50 | // * M - manually entered | |
51 | // * W - water referenced | |
52 | // * R - radar tracking of fixed target | |
53 | // * P - positioning system ground reference. | |
54 | SpeedReference string | |
55 | ||
56 | // VesselSetTrue is Vessel Set, degrees True - Manually entered | |
57 | VesselSetTrue float64 | |
58 | ||
59 | // VesselDrift is Vessel drift (speed) - Manually entered | |
60 | VesselDrift float64 | |
61 | ||
62 | // SpeedUnits is Speed Units | |
63 | // * K - km/h | |
64 | // * N - Knots | |
65 | // * S - statute miles/h | |
66 | SpeedUnits string | |
67 | } | |
68 | ||
69 | // newOSD constructor | |
70 | func newOSD(s BaseSentence) (OSD, error) { | |
71 | p := NewParser(s) | |
72 | p.AssertType(TypeOSD) | |
73 | m := OSD{ | |
74 | BaseSentence: s, | |
75 | Heading: p.Float64(0, "heading"), | |
76 | HeadingStatus: p.EnumString(1, "heading status", StatusValid, StatusInvalid), | |
77 | VesselTrueCourse: p.Float64(2, "vessel course true"), | |
78 | CourseReference: p.EnumString( | |
79 | 3, | |
80 | "course reference", | |
81 | OSDReferenceBottomTrackingLog, | |
82 | OSDReferenceManual, | |
83 | OSDReferenceWaterReferenced, | |
84 | OSDReferenceRadarTracking, | |
85 | OSDReferencePositioningSystemGroundReference, | |
86 | ), | |
87 | VesselSpeed: p.Float64(4, "vessel speed"), | |
88 | SpeedReference: p.EnumString( | |
89 | 5, | |
90 | "speed reference", | |
91 | OSDReferenceBottomTrackingLog, | |
92 | OSDReferenceManual, | |
93 | OSDReferenceWaterReferenced, | |
94 | OSDReferenceRadarTracking, | |
95 | OSDReferencePositioningSystemGroundReference, | |
96 | ), | |
97 | VesselSetTrue: p.Float64(6, "vessel set"), | |
98 | VesselDrift: p.Float64(7, "vessel drift"), | |
99 | SpeedUnits: p.EnumString(8, "speed units", DistanceUnitKilometre, DistanceUnitNauticalMile, DistanceUnitStatuteMile), | |
100 | } | |
101 | return m, p.Err() | |
102 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestOSD(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg OSD | |
13 | }{ | |
14 | { | |
15 | name: "good sentence", | |
16 | raw: "$RAOSD,179.0,A,179.0,M,00.0,M,,,N*76", | |
17 | msg: OSD{ | |
18 | BaseSentence: BaseSentence{}, | |
19 | Heading: 179, | |
20 | HeadingStatus: "A", | |
21 | VesselTrueCourse: 179, | |
22 | CourseReference: "M", | |
23 | VesselSpeed: 0, | |
24 | SpeedReference: "M", | |
25 | VesselSetTrue: 0, | |
26 | VesselDrift: 0, | |
27 | SpeedUnits: "N", | |
28 | }, | |
29 | }, | |
30 | { | |
31 | name: "invalid nmea: Heading", | |
32 | raw: "$RAOSD,x179.0,A,179.0,M,00.0,M,,,N*0e", | |
33 | err: "nmea: RAOSD invalid heading: x179.0", | |
34 | }, | |
35 | { | |
36 | name: "invalid nmea: HeadingStatus", | |
37 | raw: "$RAOSD,179.0,xA,179.0,M,00.0,M,,,N*0e", | |
38 | err: "nmea: RAOSD invalid heading status: xA", | |
39 | }, | |
40 | { | |
41 | name: "invalid nmea: VesselTrueCourse", | |
42 | raw: "$RAOSD,179.0,A,x179.0,M,00.0,M,,,N*0e", | |
43 | err: "nmea: RAOSD invalid vessel course true: x179.0", | |
44 | }, | |
45 | { | |
46 | name: "invalid nmea: CourseReference", | |
47 | raw: "$RAOSD,179.0,A,179.0,xM,00.0,M,,,N*0e", | |
48 | err: "nmea: RAOSD invalid course reference: xM", | |
49 | }, | |
50 | { | |
51 | name: "invalid nmea: VesselSpeed", | |
52 | raw: "$RAOSD,179.0,A,179.0,M,x00.0,M,,,N*0e", | |
53 | err: "nmea: RAOSD invalid vessel speed: x00.0", | |
54 | }, | |
55 | { | |
56 | name: "invalid nmea: SpeedReference", | |
57 | raw: "$RAOSD,179.0,A,179.0,M,00.0,xM,,,N*0e", | |
58 | err: "nmea: RAOSD invalid speed reference: xM", | |
59 | }, | |
60 | { | |
61 | name: "invalid nmea: VesselSetTrue", | |
62 | raw: "$RAOSD,179.0,A,179.0,M,00.0,M,x,,N*0e", | |
63 | err: "nmea: RAOSD invalid vessel set: x", | |
64 | }, | |
65 | { | |
66 | name: "invalid nmea: VesselDrift", | |
67 | raw: "$RAOSD,179.0,A,179.0,M,00.0,M,,x,N*0e", | |
68 | err: "nmea: RAOSD invalid vessel drift: x", | |
69 | }, | |
70 | { | |
71 | name: "invalid nmea: SpeedUnits", | |
72 | raw: "$RAOSD,179.0,A,179.0,M,00.0,M,,,xN*0e", | |
73 | err: "nmea: RAOSD invalid speed units: xN", | |
74 | }, | |
75 | } | |
76 | for _, tt := range tests { | |
77 | t.Run(tt.name, func(t *testing.T) { | |
78 | m, err := Parse(tt.raw) | |
79 | if tt.err != "" { | |
80 | assert.Error(t, err) | |
81 | assert.EqualError(t, err, tt.err) | |
82 | } else { | |
83 | assert.NoError(t, err) | |
84 | mm := m.(OSD) | |
85 | mm.BaseSentence = BaseSentence{} | |
86 | assert.Equal(t, tt.msg, mm) | |
87 | } | |
88 | }) | |
89 | } | |
90 | } |
8 | 8 | |
9 | 9 | // PGRME is Estimated Position Error (Garmin proprietary sentence) |
10 | 10 | // http://aprs.gids.nl/nmea/#rme |
11 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_pgrme_garmin_estimated_error | |
12 | // | |
13 | // Format: $PGRME,hhh,M,vvv,M,ttt,M*hh<CR><LF> | |
14 | // Example: $PGRME,3.3,M,4.9,M,6.0,M*25 | |
11 | 15 | type PGRME struct { |
12 | 16 | BaseSentence |
13 | 17 | Horizontal float64 // Estimated horizontal position error (HPE) in metres |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypePHTRO type of PHTRO sentence for vessel pitch and roll | |
4 | TypePHTRO = "HTRO" | |
5 | // PHTROBowUP for bow up | |
6 | PHTROBowUP = "M" | |
7 | // PHTROBowDown for bow down | |
8 | PHTROBowDown = "P" | |
9 | // PHTROPortUP for port up | |
10 | PHTROPortUP = "T" | |
11 | // PHTROPortDown for port down | |
12 | PHTROPortDown = "B" | |
13 | ) | |
14 | ||
15 | // PHTRO is proprietary sentence for vessel pitch and roll. | |
16 | // https://www.igp.de/manuals/7-INS-InterfaceLibrary_MU-INSIII-AN-001-O.pdf (page 172) | |
17 | // | |
18 | // Format: $PHTRO,x.xx,a,y.yy,b*hh<CR><LF> | |
19 | // Example: $PHTRO,10.37,P,177.62,T*65 | |
20 | type PHTRO struct { | |
21 | BaseSentence | |
22 | Pitch float64 // Pitch in degrees | |
23 | Bow string // "M" for bow up and "P" for bow down (2 digits after the decimal point) | |
24 | Roll float64 // Roll in degrees | |
25 | Port string // "B" for port down and "T" for port up (2 digits after the decimal point) | |
26 | } | |
27 | ||
28 | // newPHTRO constructor | |
29 | func newPHTRO(s BaseSentence) (PHTRO, error) { | |
30 | p := NewParser(s) | |
31 | p.AssertType(TypePHTRO) | |
32 | m := PHTRO{ | |
33 | BaseSentence: s, | |
34 | Pitch: p.Float64(0, "pitch"), | |
35 | Bow: p.EnumString(1, "bow", PHTROBowUP, PHTROBowDown), | |
36 | Roll: p.Float64(2, "roll"), | |
37 | Port: p.EnumString(3, "port", PHTROPortUP, PHTROPortDown), | |
38 | } | |
39 | return m, p.Err() | |
40 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestPHTRO(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg PHTRO | |
13 | }{ | |
14 | { | |
15 | name: "good sentence", | |
16 | raw: "$PHTRO,10.37,P,177.62,T*65", | |
17 | msg: PHTRO{ | |
18 | Pitch: 10.37, | |
19 | Bow: PHTROBowDown, | |
20 | Roll: 177.62, | |
21 | Port: PHTROPortUP, | |
22 | }, | |
23 | }, | |
24 | { | |
25 | name: "invalid Pitch", | |
26 | raw: "$PHTRO,x,P,177.62,T*36", | |
27 | err: "nmea: PHTRO invalid pitch: x", | |
28 | }, | |
29 | { | |
30 | name: "invalid Bow", | |
31 | raw: "$PHTRO,10.37,x,177.62,T*4D", | |
32 | err: "nmea: PHTRO invalid bow: x", | |
33 | }, | |
34 | { | |
35 | name: "invalid Roll", | |
36 | raw: "$PHTRO,10.37,P,x,T*06", | |
37 | err: "nmea: PHTRO invalid roll: x", | |
38 | }, | |
39 | { | |
40 | name: "invalid Port", | |
41 | raw: "$PHTRO,10.37,P,177.62,x*49", | |
42 | err: "nmea: PHTRO invalid port: x", | |
43 | }, | |
44 | } | |
45 | for _, tt := range tests { | |
46 | t.Run(tt.name, func(t *testing.T) { | |
47 | m, err := Parse(tt.raw) | |
48 | if tt.err != "" { | |
49 | assert.Error(t, err) | |
50 | assert.EqualError(t, err, tt.err) | |
51 | } else { | |
52 | assert.NoError(t, err) | |
53 | phtro := m.(PHTRO) | |
54 | phtro.BaseSentence = BaseSentence{} | |
55 | assert.Equal(t, tt.msg, phtro) | |
56 | } | |
57 | }) | |
58 | } | |
59 | } |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypePRDID type of PRDID sentence for vessel pitch, roll and heading | |
4 | TypePRDID = "RDID" | |
5 | ) | |
6 | ||
7 | // PRDID is proprietary sentence for vessel pitch, roll and heading. | |
8 | // https://www.xsens.com/hubfs/Downloads/Manuals/MT_Low-Level_Documentation.pdf (page 37) | |
9 | // | |
10 | // Format: $PRDID,aPPP.PP,bRRR.RR,HHH.HH*hh<CR><LF> | |
11 | // Example: $PRDID,-10.37,2.34,230.34*AA | |
12 | type PRDID struct { | |
13 | BaseSentence | |
14 | Pitch float64 // Pitch in degrees (positive bow up) | |
15 | Roll float64 // Roll in degrees (positive port up) | |
16 | Heading float64 // True heading in degrees | |
17 | } | |
18 | ||
19 | // newPRDID constructor | |
20 | func newPRDID(s BaseSentence) (PRDID, error) { | |
21 | p := NewParser(s) | |
22 | p.AssertType(TypePRDID) | |
23 | m := PRDID{ | |
24 | BaseSentence: s, | |
25 | Pitch: p.Float64(0, "pitch"), | |
26 | Roll: p.Float64(1, "roll"), | |
27 | Heading: p.Float64(2, "heading"), | |
28 | } | |
29 | return m, p.Err() | |
30 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestPRDID(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg PRDID | |
13 | }{ | |
14 | { | |
15 | name: "good sentence", | |
16 | raw: "$PRDID,-10.37,2.34,230.34*62", | |
17 | msg: PRDID{ | |
18 | Pitch: -10.37, | |
19 | Roll: 2.34, | |
20 | Heading: 230.34, | |
21 | }, | |
22 | }, | |
23 | { | |
24 | name: "invalid Pitch", | |
25 | raw: "$PRDID,x.37,2.34,230.34*36", | |
26 | err: "nmea: PRDID invalid pitch: x.37", | |
27 | }, | |
28 | { | |
29 | name: "invalid Roll", | |
30 | raw: "$PRDID,-10.37,x.34,230.34*28", | |
31 | err: "nmea: PRDID invalid roll: x.34", | |
32 | }, | |
33 | { | |
34 | name: "invalid Heading", | |
35 | raw: "$PRDID,-10.37,2.34,x.34*2B", | |
36 | err: "nmea: PRDID invalid heading: x.34", | |
37 | }, | |
38 | } | |
39 | for _, tt := range tests { | |
40 | t.Run(tt.name, func(t *testing.T) { | |
41 | m, err := Parse(tt.raw) | |
42 | if tt.err != "" { | |
43 | assert.Error(t, err) | |
44 | assert.EqualError(t, err, tt.err) | |
45 | } else { | |
46 | assert.NoError(t, err) | |
47 | prdid := m.(PRDID) | |
48 | prdid.BaseSentence = BaseSentence{} | |
49 | assert.Equal(t, tt.msg, prdid) | |
50 | } | |
51 | }) | |
52 | } | |
53 | } |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypePSONCMS is type of PSONCMS sentence for proprietary Xsens IMU/VRU/AHRS device | |
4 | TypePSONCMS = "SONCMS" | |
5 | ) | |
6 | ||
7 | // PSONCMS is proprietary Xsens IMU/VRU/AHRS device sentence for quaternion, acceleration, rate of turn, | |
8 | // magnetic Field, sensor temperature. | |
9 | // https://www.xsens.com/hubfs/Downloads/Manuals/MT_Low-Level_Documentation.pdf (page 37) | |
10 | // | |
11 | // Format: $PSONCMS,Q.QQQQ,P.PPPP,R.RRRR,S.SSSS,XX.XXXX,YY.YYYY,ZZ.ZZZZ, | |
12 | // FF.FFFF,GG.GGGG,HH.HHHH,NN.NNNN,MM,MMMM,PP.PPPP,TT.T*hh<CR><LF> | |
13 | // 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 | |
14 | type PSONCMS struct { | |
15 | BaseSentence | |
16 | Quaternion0 float64 // q0 from quaternions | |
17 | Quaternion1 float64 // q1 from quaternions | |
18 | Quaternion2 float64 // q2 from quaternions | |
19 | Quaternion3 float64 // q3 from quaternions | |
20 | AccelerationX float64 // acceleration X in m/s2 | |
21 | AccelerationY float64 // acceleration Y in m/s2 | |
22 | AccelerationZ float64 // acceleration Z in m/s2 | |
23 | RateOfTurnX float64 // rate of turn X in rad/s | |
24 | RateOfTurnY float64 // rate of turn Y in rad/s | |
25 | RateOfTurnZ float64 // rate of turn Z in rad/s | |
26 | MagneticFieldX float64 // magnetic field X in a.u. | |
27 | MagneticFieldY float64 // magnetic field Y in a.u. | |
28 | MagneticFieldZ float64 // magnetic field Z in a.u. | |
29 | SensorTemperature float64 // sensor temperature in degrees Celsius | |
30 | } | |
31 | ||
32 | // newPSONCMS constructor | |
33 | func newPSONCMS(s BaseSentence) (PSONCMS, error) { | |
34 | p := NewParser(s) | |
35 | p.AssertType(TypePSONCMS) | |
36 | m := PSONCMS{ | |
37 | BaseSentence: s, | |
38 | Quaternion0: p.Float64(0, "q0 from quaternions"), | |
39 | Quaternion1: p.Float64(1, "q1 from quaternions"), | |
40 | Quaternion2: p.Float64(2, "q2 from quaternions"), | |
41 | Quaternion3: p.Float64(3, "q3 from quaternions"), | |
42 | AccelerationX: p.Float64(4, "acceleration X"), | |
43 | AccelerationY: p.Float64(5, "acceleration Y"), | |
44 | AccelerationZ: p.Float64(6, "acceleration Z"), | |
45 | RateOfTurnX: p.Float64(7, "rate of turn X"), | |
46 | RateOfTurnY: p.Float64(8, "rate of turn Y"), | |
47 | RateOfTurnZ: p.Float64(9, "rate of turn Z"), | |
48 | MagneticFieldX: p.Float64(10, "magnetic field X"), | |
49 | MagneticFieldY: p.Float64(11, "magnetic field Y"), | |
50 | MagneticFieldZ: p.Float64(12, "magnetic field Z"), | |
51 | SensorTemperature: p.Float64(13, "sensor temperature"), | |
52 | } | |
53 | return m, p.Err() | |
54 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestPSONCMS(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg PSONCMS | |
13 | }{ | |
14 | { | |
15 | name: "good sentence", | |
16 | 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", | |
17 | msg: PSONCMS{ | |
18 | BaseSentence: BaseSentence{}, | |
19 | Quaternion0: 0.0905, | |
20 | Quaternion1: 0.4217, | |
21 | Quaternion2: 0.9020, | |
22 | Quaternion3: -0.0196, | |
23 | AccelerationX: -1.7685, | |
24 | AccelerationY: 0.3861, | |
25 | AccelerationZ: -9.6648, | |
26 | RateOfTurnX: -0.0116, | |
27 | RateOfTurnY: 0.0065, | |
28 | RateOfTurnZ: -0.0080, | |
29 | MagneticFieldX: 0.0581, | |
30 | MagneticFieldY: 0.3846, | |
31 | MagneticFieldZ: 0.7421, | |
32 | SensorTemperature: 33.1, | |
33 | }, | |
34 | }, | |
35 | { | |
36 | name: "invalid Quaternion0", | |
37 | 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", | |
38 | err: "nmea: PSONCMS invalid q0 from quaternions: x", | |
39 | }, | |
40 | { | |
41 | name: "invalid Quaternion1", | |
42 | 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", | |
43 | err: "nmea: PSONCMS invalid q1 from quaternions: x", | |
44 | }, | |
45 | } | |
46 | for _, tt := range tests { | |
47 | t.Run(tt.name, func(t *testing.T) { | |
48 | m, err := Parse(tt.raw) | |
49 | if tt.err != "" { | |
50 | assert.Error(t, err) | |
51 | assert.EqualError(t, err, tt.err) | |
52 | } else { | |
53 | assert.NoError(t, err) | |
54 | psoncms := m.(PSONCMS) | |
55 | psoncms.BaseSentence = BaseSentence{} | |
56 | assert.Equal(t, tt.msg, psoncms) | |
57 | } | |
58 | }) | |
59 | } | |
60 | } |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypeRMB type of RMB sentence for recommended minimum navigation information | |
4 | TypeRMB = "RMB" | |
5 | ||
6 | // DataStatusWarningClearRMB means data is OK | |
7 | DataStatusWarningClearRMB = "A" | |
8 | // DataStatusWarningSetRMB means warning flag set | |
9 | DataStatusWarningSetRMB = "V" | |
10 | ) | |
11 | ||
12 | // RMB - Recommended Minimum Navigation Information. To be sent by a navigation receiver when a destination waypoint | |
13 | // is active. Alternative to BOD and BWW sentences. | |
14 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_rmb_recommended_minimum_navigation_information | |
15 | // http://aprs.gids.nl/nmea/#rmb | |
16 | // | |
17 | // Format: $--RMB,A,x.x,a,c--c,c--c,llll.ll,a,yyyyy.yy,a,x.x,x.x,x.x,A*hh<CR><LF> | |
18 | // 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<CR><LF> | |
19 | // Example: $GPRMB,A,0.66,L,003,004,4917.24,N,12309.57,W,001.3,052.5,000.5,V*0B | |
20 | type RMB struct { | |
21 | BaseSentence | |
22 | ||
23 | // DataStatus is status of data, | |
24 | // * A = OK | |
25 | // * V = Navigation receiver warning | |
26 | DataStatus string | |
27 | ||
28 | // Cross Track error (nautical miles, 9.9 max) | |
29 | CrossTrackErrorNauticalMiles float64 | |
30 | ||
31 | // DirectionToSteer is Direction to steer, | |
32 | // * L = left | |
33 | // * R = right | |
34 | DirectionToSteer string | |
35 | ||
36 | // OriginWaypointID is origin (FROM) waypoint ID | |
37 | OriginWaypointID string | |
38 | ||
39 | // DestinationWaypointID is destination (TO) waypoint ID | |
40 | DestinationWaypointID string | |
41 | ||
42 | // DestinationLatitude is destination waypoint latitude | |
43 | DestinationLatitude float64 | |
44 | ||
45 | // DestinationLongitude is destination waypoint longitude | |
46 | DestinationLongitude float64 | |
47 | ||
48 | // RangeToDestinationNauticalMiles is range to destination, nautical miles (999,9 max) | |
49 | RangeToDestinationNauticalMiles float64 | |
50 | ||
51 | // TrueBearingToDestination is true bearing to destination, degrees | |
52 | TrueBearingToDestination float64 | |
53 | ||
54 | // VelocityToDestinationKnots is velocity towards destination, knots | |
55 | VelocityToDestinationKnots float64 | |
56 | ||
57 | // ArrivalStatus is Arrival Status | |
58 | // * A = arrival circle entered | |
59 | // * V = not arrived | |
60 | ArrivalStatus string | |
61 | ||
62 | // FAA mode indicator (filled in NMEA 2.3 and later) | |
63 | FFAMode string | |
64 | } | |
65 | ||
66 | // newRMB constructor | |
67 | func newRMB(s BaseSentence) (RMB, error) { | |
68 | p := NewParser(s) | |
69 | p.AssertType(TypeRMB) | |
70 | rmb := RMB{ | |
71 | BaseSentence: s, | |
72 | DataStatus: p.EnumString(0, "data status", DataStatusWarningClearRMB, DataStatusWarningSetRMB), | |
73 | CrossTrackErrorNauticalMiles: p.Float64(1, "cross track error"), | |
74 | DirectionToSteer: p.EnumString(2, "direction to steer", Left, Right), | |
75 | OriginWaypointID: p.String(3, "origin waypoint ID"), | |
76 | DestinationWaypointID: p.String(4, "destination waypoint ID"), | |
77 | DestinationLatitude: p.LatLong(5, 6, "latitude"), | |
78 | DestinationLongitude: p.LatLong(7, 8, "latitude"), | |
79 | RangeToDestinationNauticalMiles: p.Float64(9, "range to destination"), | |
80 | TrueBearingToDestination: p.Float64(10, "true bearing to destination"), | |
81 | VelocityToDestinationKnots: p.Float64(11, "velocity to destination"), | |
82 | ArrivalStatus: p.EnumString(12, "arrival status", WPStatusArrivalCircleEnteredA, WPStatusArrivalCircleEnteredV), | |
83 | FFAMode: "", | |
84 | } | |
85 | if len(p.Fields) > 13 { | |
86 | rmb.FFAMode = p.String(13, "FAA mode") // not enum because some devices have proprietary "non-nmea" values | |
87 | } | |
88 | return rmb, p.Err() | |
89 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestRMB(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg RMB | |
13 | }{ | |
14 | { | |
15 | name: "good sentence", | |
16 | raw: "$GPRMB,A,0.66,L,003,004,4917.24,N,12309.57,W,001.3,052.5,000.5,V*20", | |
17 | msg: RMB{ | |
18 | DataStatus: DataStatusWarningClearRMB, | |
19 | CrossTrackErrorNauticalMiles: 0.66, | |
20 | DirectionToSteer: Left, | |
21 | OriginWaypointID: "003", | |
22 | DestinationWaypointID: "004", | |
23 | DestinationLatitude: 49.28733333333333, | |
24 | DestinationLongitude: -123.1595, | |
25 | RangeToDestinationNauticalMiles: 1.3, | |
26 | TrueBearingToDestination: 52.5, | |
27 | VelocityToDestinationKnots: 0.5, | |
28 | ArrivalStatus: WPStatusArrivalCircleEnteredV, | |
29 | FFAMode: "", | |
30 | }, | |
31 | }, | |
32 | { | |
33 | name: "good sentence with FAAMode", | |
34 | raw: "$GPRMB,A,0.66,L,003,004,4917.24,N,12309.57,W,001.3,052.5,000.5,V,D*48", | |
35 | msg: RMB{ | |
36 | DataStatus: DataStatusWarningClearRMB, | |
37 | CrossTrackErrorNauticalMiles: 0.66, | |
38 | DirectionToSteer: Left, | |
39 | OriginWaypointID: "003", | |
40 | DestinationWaypointID: "004", | |
41 | DestinationLatitude: 49.28733333333333, | |
42 | DestinationLongitude: -123.1595, | |
43 | RangeToDestinationNauticalMiles: 1.3, | |
44 | TrueBearingToDestination: 52.5, | |
45 | VelocityToDestinationKnots: 0.5, | |
46 | ArrivalStatus: WPStatusArrivalCircleEnteredV, | |
47 | FFAMode: FAAModeDifferential, | |
48 | }, | |
49 | }, | |
50 | { | |
51 | name: "invalid nmea: DataStatus", | |
52 | raw: "$GPRMB,x,0.66,L,003,004,4917.24,N,12309.57,W,001.3,052.5,000.5,V,D*71", | |
53 | err: "nmea: GPRMB invalid data status: x", | |
54 | }, | |
55 | { | |
56 | name: "invalid nmea: CrossTrackErrorNauticalMiles", | |
57 | raw: "$GPRMB,A,x.66,L,003,004,4917.24,N,12309.57,W,001.3,052.5,000.5,V,D*00", | |
58 | err: "nmea: GPRMB invalid cross track error: x.66", | |
59 | }, | |
60 | { | |
61 | name: "invalid nmea: DirectionToSteer", | |
62 | raw: "$GPRMB,A,0.66,x,003,004,4917.24,N,12309.57,W,001.3,052.5,000.5,V,D*7C", | |
63 | err: "nmea: GPRMB invalid direction to steer: x", | |
64 | }, | |
65 | { | |
66 | name: "invalid nmea: DestinationLatitude", | |
67 | raw: "$GPRMB,A,0.66,L,003,004,4x17.24,N,12309.57,W,001.3,052.5,000.5,V,D*09", | |
68 | err: "nmea: GPRMB invalid latitude: cannot parse [4x17.24 N], unknown format", | |
69 | }, | |
70 | { | |
71 | name: "invalid nmea: DestinationLongitude", | |
72 | raw: "$GPRMB,A,0.66,L,003,004,4917.24,N,12x09.57,W,001.3,052.5,000.5,V,D*03", | |
73 | err: "nmea: GPRMB invalid latitude: cannot parse [12x09.57 W], unknown format", | |
74 | }, | |
75 | { | |
76 | name: "invalid nmea: RangeToDestinationNauticalMiles", | |
77 | raw: "$GPRMB,A,0.66,L,003,004,4917.24,N,12309.57,W,x01.3,052.5,000.5,V,D*00", | |
78 | err: "nmea: GPRMB invalid range to destination: x01.3", | |
79 | }, | |
80 | { | |
81 | name: "invalid nmea: TrueBearingToDestination", | |
82 | raw: "$GPRMB,A,0.66,L,003,004,4917.24,N,12309.57,W,001.3,052.x,000.5,V,D*05", | |
83 | err: "nmea: GPRMB invalid true bearing to destination: 052.x", | |
84 | }, | |
85 | { | |
86 | name: "invalid nmea: VelocityToDestinationKnots", | |
87 | raw: "$GPRMB,A,0.66,L,003,004,4917.24,N,12309.57,W,001.3,052.5,000.x,V,D*05", | |
88 | err: "nmea: GPRMB invalid velocity to destination: 000.x", | |
89 | }, | |
90 | { | |
91 | name: "invalid nmea: ArrivalStatus", | |
92 | raw: "$GPRMB,A,0.66,L,003,004,4917.24,N,12309.57,W,001.3,052.5,000.5,x,D*66", | |
93 | err: "nmea: GPRMB invalid arrival status: x", | |
94 | }, | |
95 | } | |
96 | for _, tt := range tests { | |
97 | t.Run(tt.name, func(t *testing.T) { | |
98 | m, err := Parse(tt.raw) | |
99 | if tt.err != "" { | |
100 | assert.Error(t, err) | |
101 | assert.EqualError(t, err, tt.err) | |
102 | } else { | |
103 | assert.NoError(t, err) | |
104 | rmb := m.(RMB) | |
105 | rmb.BaseSentence = BaseSentence{} | |
106 | assert.Equal(t, tt.msg, rmb) | |
107 | } | |
108 | }) | |
109 | } | |
110 | } |
10 | 10 | |
11 | 11 | // RMC is the Recommended Minimum Specific GNSS data. |
12 | 12 | // http://aprs.gids.nl/nmea/#rmc |
13 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_rmc_recommended_minimum_navigation_information | |
14 | // | |
15 | // Format: $--RMC,hhmmss.ss,A,ddmm.mm,a,dddmm.mm,a,x.x,x.x,xxxx,x.x,a*hh<CR><LF> | |
16 | // Format NMEA 2.3: $--RMC,hhmmss.ss,A,ddmm.mm,a,dddmm.mm,a,x.x,x.x,xxxx,x.x,a,m*hh<CR><LF> | |
17 | // 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<CR><LF> | |
18 | // Example: $GNRMC,220516,A,5133.82,N,00042.24,W,173.8,231.8,130694,004.2,W*6E | |
19 | // $GNRMC,142754.0,A,4302.539570,N,07920.379823,W,0.0,,070617,0.0,E,A*21 | |
20 | // $GNRMC,102014.00,A,5550.6082,N,03732.2488,E,000.00000,092.9,300518,,,A,V*3B | |
13 | 21 | type RMC struct { |
14 | 22 | BaseSentence |
15 | 23 | Time Time // Time Stamp |
20 | 28 | Course float64 // True course |
21 | 29 | Date Date // Date |
22 | 30 | Variation float64 // Magnetic variation |
31 | FFAMode string // FAA mode indicator (filled in NMEA 2.3 and later) | |
32 | NavStatus string // Nav Status (NMEA 4.1 and later) | |
23 | 33 | } |
24 | 34 | |
25 | 35 | // newRMC constructor |
40 | 50 | if p.EnumString(10, "direction", West, East) == West { |
41 | 51 | m.Variation = 0 - m.Variation |
42 | 52 | } |
53 | if len(p.Fields) > 11 { | |
54 | m.FFAMode = p.String(11, "FAA mode") // not enum because some devices have proprietary "non-nmea" values | |
55 | } | |
56 | if len(p.Fields) > 12 { | |
57 | m.NavStatus = p.EnumString( | |
58 | 12, | |
59 | "navigation status", | |
60 | NavStatusAutonomous, | |
61 | NavStatusDifferential, | |
62 | NavStatusEstimated, | |
63 | NavStatusManualInput, | |
64 | NavStatusSimulated, | |
65 | NavStatusDataNotValid, | |
66 | NavStatusDataValid, | |
67 | ) | |
68 | } | |
43 | 69 | return m, p.Err() |
44 | 70 | } |
17 | 17 | msg: RMC{ |
18 | 18 | Time: Time{true, 22, 05, 16, 0}, |
19 | 19 | Validity: "A", |
20 | Latitude: MustParseGPS("5133.82 N"), | |
21 | Longitude: MustParseGPS("00042.24 W"), | |
20 | 22 | Speed: 173.8, |
21 | 23 | Course: 231.8, |
22 | 24 | Date: Date{true, 13, 06, 94}, |
23 | 25 | Variation: -4.2, |
24 | Latitude: MustParseGPS("5133.82 N"), | |
25 | Longitude: MustParseGPS("00042.24 W"), | |
26 | FFAMode: "", | |
27 | NavStatus: "", | |
26 | 28 | }, |
27 | 29 | }, |
28 | 30 | { |
31 | 33 | msg: RMC{ |
32 | 34 | Time: Time{true, 14, 27, 54, 0}, |
33 | 35 | Validity: "A", |
36 | Latitude: MustParseGPS("4302.539570 N"), | |
37 | Longitude: MustParseGPS("07920.379823 W"), | |
34 | 38 | Speed: 0, |
35 | 39 | Course: 0, |
36 | 40 | Date: Date{true, 7, 6, 17}, |
37 | 41 | Variation: 0, |
38 | Latitude: MustParseGPS("4302.539570 N"), | |
39 | Longitude: MustParseGPS("07920.379823 W"), | |
42 | FFAMode: FAAModeAutonomous, | |
43 | NavStatus: "", | |
40 | 44 | }, |
41 | 45 | }, |
42 | 46 | { |
45 | 49 | msg: RMC{ |
46 | 50 | Time: Time{true, 10, 5, 38, 0}, |
47 | 51 | Validity: "A", |
52 | Latitude: MustParseGPS("5546.27711 N"), | |
53 | Longitude: MustParseGPS("03736.91144 E"), | |
48 | 54 | Speed: 0.061, |
49 | 55 | Course: 0, |
50 | 56 | Date: Date{true, 26, 3, 18}, |
51 | 57 | Variation: 0, |
52 | Latitude: MustParseGPS("5546.27711 N"), | |
53 | Longitude: MustParseGPS("03736.91144 E"), | |
58 | FFAMode: FAAModeAutonomous, | |
59 | NavStatus: "", | |
54 | 60 | }, |
55 | 61 | }, |
56 | 62 | { |
59 | 65 | err: "nmea: GNRMC invalid validity: D", |
60 | 66 | }, |
61 | 67 | { |
62 | name: "good sentence A", | |
68 | name: "good sentence D", | |
63 | 69 | raw: "$GPRMC,220516,A,5133.82,N,00042.24,W,173.8,231.8,130694,004.2,W*70", |
64 | 70 | msg: RMC{ |
65 | 71 | Time: Time{true, 22, 5, 16, 0}, |
66 | 72 | Validity: "A", |
73 | Latitude: MustParseGPS("5133.82 N"), | |
74 | Longitude: MustParseGPS("00042.24 W"), | |
67 | 75 | Speed: 173.8, |
68 | 76 | Course: 231.8, |
69 | 77 | Date: Date{true, 13, 6, 94}, |
70 | 78 | Variation: -4.2, |
71 | Latitude: MustParseGPS("5133.82 N"), | |
72 | Longitude: MustParseGPS("00042.24 W"), | |
79 | FFAMode: "", | |
80 | NavStatus: "", | |
73 | 81 | }, |
74 | 82 | }, |
75 | 83 | { |
76 | name: "good sentence B", | |
84 | name: "good sentence E", | |
77 | 85 | raw: "$GPRMC,142754.0,A,4302.539570,N,07920.379823,W,0.0,,070617,0.0,E,A*3F", |
78 | 86 | msg: RMC{ |
79 | 87 | Time: Time{true, 14, 27, 54, 0}, |
80 | 88 | Validity: "A", |
89 | Latitude: MustParseGPS("4302.539570 N"), | |
90 | Longitude: MustParseGPS("07920.379823 W"), | |
81 | 91 | Speed: 0, |
82 | 92 | Course: 0, |
83 | 93 | Date: Date{true, 7, 6, 17}, |
84 | 94 | Variation: 0, |
85 | Latitude: MustParseGPS("4302.539570 N"), | |
86 | Longitude: MustParseGPS("07920.379823 W"), | |
95 | FFAMode: FAAModeAutonomous, | |
96 | NavStatus: "", | |
97 | }, | |
98 | }, | |
99 | { | |
100 | name: "good sentence F with nav status", | |
101 | raw: "$GNRMC,102014.00,A,5550.6082,N,03732.2488,E,000.00000,092.9,300518,,,A,V*3B", | |
102 | msg: RMC{ | |
103 | Time: Time{Valid: true, Hour: 10, Minute: 20, Second: 14, Millisecond: 0}, | |
104 | Validity: "A", | |
105 | Latitude: 55.843469999999996, | |
106 | Longitude: 37.537479999999995, | |
107 | Speed: 0, | |
108 | Course: 92.9, | |
109 | Date: Date{Valid: true, DD: 30, MM: 5, YY: 18}, | |
110 | Variation: 0, | |
111 | FFAMode: FAAModeAutonomous, | |
112 | NavStatus: NavStatusDataValid, | |
87 | 113 | }, |
88 | 114 | }, |
89 | 115 | { |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypeROT type of ROT sentence for vessel rate of turn | |
4 | TypeROT = "ROT" | |
5 | // ValidROT data is valid | |
6 | ValidROT = "A" | |
7 | // InvalidROT data is invalid | |
8 | InvalidROT = "V" | |
9 | ) | |
10 | ||
11 | // ROT is sentence for rate of turn. | |
12 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_rot_rate_of_turn | |
13 | // | |
14 | // Format: $HEROT,-xxx.x,A*hh<CR><LF> | |
15 | // Example: $HEROT,-11.23,A*07 | |
16 | type ROT struct { | |
17 | BaseSentence | |
18 | RateOfTurn float64 // rate of turn Z in deg/min (- means bow turns to port) | |
19 | Valid bool // "A" data valid, "V" invalid data | |
20 | } | |
21 | ||
22 | func newROT(s BaseSentence) (ROT, error) { | |
23 | p := NewParser(s) | |
24 | p.AssertType(TypeROT) | |
25 | return ROT{ | |
26 | BaseSentence: s, | |
27 | RateOfTurn: p.Float64(0, "rate of turn"), | |
28 | Valid: p.EnumString(1, "status valid", ValidROT, InvalidROT) == ValidROT, | |
29 | }, p.Err() | |
30 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestROT(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg ROT | |
13 | }{ | |
14 | { | |
15 | name: "good sentence", | |
16 | raw: "$HEROT,-11.23,A*07", | |
17 | msg: ROT{ | |
18 | RateOfTurn: -11.23, | |
19 | Valid: true, | |
20 | }, | |
21 | }, | |
22 | { | |
23 | name: "invalid RateOfTurn", | |
24 | raw: "$HEROT,x,A*7D", | |
25 | err: "nmea: HEROT invalid rate of turn: x", | |
26 | }, | |
27 | { | |
28 | name: "invalid Valid", | |
29 | raw: "$HEROT,-11.23,X*1E", | |
30 | err: "nmea: HEROT invalid status valid: X", | |
31 | }, | |
32 | } | |
33 | for _, tt := range tests { | |
34 | t.Run(tt.name, func(t *testing.T) { | |
35 | m, err := Parse(tt.raw) | |
36 | if tt.err != "" { | |
37 | assert.Error(t, err) | |
38 | assert.EqualError(t, err, tt.err) | |
39 | } else { | |
40 | assert.NoError(t, err) | |
41 | rot := m.(ROT) | |
42 | rot.BaseSentence = BaseSentence{} | |
43 | assert.Equal(t, tt.msg, rot) | |
44 | } | |
45 | }) | |
46 | } | |
47 | } |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypeRPM type of RPM sentence for Engine or Shaft revolutions and pitch | |
4 | TypeRPM = "RPM" | |
5 | ||
6 | // SourceEngineRPM is value for case when source is Engine | |
7 | SourceEngineRPM = "E" | |
8 | // SourceShaftRPM is value for case when source is Shaft | |
9 | SourceShaftRPM = "S" | |
10 | ) | |
11 | ||
12 | // RPM - Engine or Shaft revolutions and pitch | |
13 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_rpm_revolutions | |
14 | // | |
15 | // Format: $--RPM,a,x,x.x,x.x,A*hh<CR><LF> | |
16 | // Example: $RCRPM,S,0,74.6,30.0,A*56 | |
17 | type RPM struct { | |
18 | BaseSentence | |
19 | Source string // Source, S = Shaft, E = Engine | |
20 | EngineNumber int64 // Engine or shaft number | |
21 | SpeedRPM float64 // Speed, Revolutions per minute | |
22 | PitchPercent float64 // Propeller pitch, % of maximum, "-" means astern | |
23 | Status string // Status, A = Valid, V = Invalid | |
24 | } | |
25 | ||
26 | // newRPM constructor | |
27 | func newRPM(s BaseSentence) (RPM, error) { | |
28 | p := NewParser(s) | |
29 | p.AssertType(TypeRPM) | |
30 | return RPM{ | |
31 | BaseSentence: s, | |
32 | Source: p.EnumString(0, "source", SourceEngineRPM, SourceShaftRPM), | |
33 | EngineNumber: p.Int64(1, "engine number"), | |
34 | SpeedRPM: p.Float64(2, "speed"), | |
35 | PitchPercent: p.Float64(3, "pitch"), | |
36 | Status: p.EnumString(4, "status", StatusValid, StatusInvalid), | |
37 | }, p.Err() | |
38 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestRPM(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg RPM | |
13 | }{ | |
14 | { | |
15 | name: "good sentence", | |
16 | raw: "$RCRPM,S,0,74.6,30.0,A*56", | |
17 | msg: RPM{ | |
18 | Source: SourceShaftRPM, | |
19 | EngineNumber: 0, | |
20 | SpeedRPM: 74.6, | |
21 | PitchPercent: 30, | |
22 | Status: StatusValid, | |
23 | }, | |
24 | }, | |
25 | { | |
26 | name: "invalid nmea: Source", | |
27 | raw: "$RCRPM,x,0,74.6,30.0,A*7D", | |
28 | err: "nmea: RCRPM invalid source: x", | |
29 | }, | |
30 | { | |
31 | name: "invalid nmea: Status", | |
32 | raw: "$RCRPM,S,0,74.6,30.0,x*6F", | |
33 | err: "nmea: RCRPM invalid status: x", | |
34 | }, | |
35 | } | |
36 | for _, tt := range tests { | |
37 | t.Run(tt.name, func(t *testing.T) { | |
38 | m, err := Parse(tt.raw) | |
39 | if tt.err != "" { | |
40 | assert.Error(t, err) | |
41 | assert.EqualError(t, err, tt.err) | |
42 | } else { | |
43 | assert.NoError(t, err) | |
44 | rpm := m.(RPM) | |
45 | rpm.BaseSentence = BaseSentence{} | |
46 | assert.Equal(t, tt.msg, rpm) | |
47 | } | |
48 | }) | |
49 | } | |
50 | } |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypeRSA type of RSA sentence for Rudder Sensor Angle | |
4 | TypeRSA = "RSA" | |
5 | ) | |
6 | ||
7 | // RSA - Rudder Sensor Angle | |
8 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_rsa_rudder_sensor_angle | |
9 | // | |
10 | // Format: $--RSA,x.x,A,x.x,A*hh<CR><LF> | |
11 | // Example: $IIRSA,10.5,A,,V*4D | |
12 | type RSA struct { | |
13 | BaseSentence | |
14 | StarboardRudderAngle float64 // Starboard (or single) rudder sensor, "-" means Turn To Port | |
15 | StarboardRudderAngleStatus string // Status, A = valid, V = Invalid | |
16 | PortRudderAngle float64 // Port rudder sensor | |
17 | PortRudderAngleStatus string // Status, A = valid, V = Invalid | |
18 | } | |
19 | ||
20 | // newRSA constructor | |
21 | func newRSA(s BaseSentence) (RSA, error) { | |
22 | p := NewParser(s) | |
23 | p.AssertType(TypeRSA) | |
24 | return RSA{ | |
25 | BaseSentence: s, | |
26 | StarboardRudderAngle: p.Float64(0, "starboard rudder angle"), | |
27 | StarboardRudderAngleStatus: p.EnumString(1, "starboard rudder angle status", StatusValid, StatusInvalid), | |
28 | PortRudderAngle: p.Float64(2, "port rudder angle"), | |
29 | PortRudderAngleStatus: p.EnumString(3, "port rudder angle status", StatusValid, StatusInvalid), | |
30 | }, p.Err() | |
31 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestRSA(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg RSA | |
13 | }{ | |
14 | { | |
15 | name: "good sentence 1", | |
16 | raw: "$IIRSA,10.5,A,0.4,A*70", | |
17 | msg: RSA{ | |
18 | StarboardRudderAngle: 10.5, | |
19 | StarboardRudderAngleStatus: StatusValid, | |
20 | PortRudderAngle: 0.4, | |
21 | PortRudderAngleStatus: StatusValid, | |
22 | }, | |
23 | }, | |
24 | { | |
25 | name: "good sentence 2", | |
26 | raw: "$IIRSA,10.5,A,,V*4D", | |
27 | msg: RSA{ | |
28 | StarboardRudderAngle: 10.5, | |
29 | StarboardRudderAngleStatus: StatusValid, | |
30 | PortRudderAngle: 0, | |
31 | PortRudderAngleStatus: StatusInvalid, | |
32 | }, | |
33 | }, | |
34 | { | |
35 | name: "invalid nmea: StarboardRudderAngleStatus", | |
36 | raw: "$IIRSA,10.5,x,,V*74", | |
37 | err: "nmea: IIRSA invalid starboard rudder angle status: x", | |
38 | }, | |
39 | { | |
40 | name: "invalid nmea: PortRudderAngleStatus", | |
41 | raw: "$IIRSA,10.5,A,,x*63", | |
42 | err: "nmea: IIRSA invalid port rudder angle status: x", | |
43 | }, | |
44 | } | |
45 | for _, tt := range tests { | |
46 | t.Run(tt.name, func(t *testing.T) { | |
47 | m, err := Parse(tt.raw) | |
48 | if tt.err != "" { | |
49 | assert.Error(t, err) | |
50 | assert.EqualError(t, err, tt.err) | |
51 | } else { | |
52 | assert.NoError(t, err) | |
53 | rsa := m.(RSA) | |
54 | rsa.BaseSentence = BaseSentence{} | |
55 | assert.Equal(t, tt.msg, rsa) | |
56 | } | |
57 | }) | |
58 | } | |
59 | } |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypeRSD type of RSD sentence for RADAR System Data | |
4 | TypeRSD = "RSD" | |
5 | ||
6 | // RSDDisplayRotationCourseUp is when display rotation is course up | |
7 | RSDDisplayRotationCourseUp = "C" | |
8 | // RSDDisplayRotationHeadingUp is when display rotation is ship heading up | |
9 | RSDDisplayRotationHeadingUp = "H" | |
10 | // RSDDisplayRotationNorthUp is when display rotation is (true) north up | |
11 | RSDDisplayRotationNorthUp = "N" | |
12 | ) | |
13 | ||
14 | // RSD - RADAR System Data | |
15 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_rsd_radar_system_data | |
16 | // https://github.com/nohal/OpenCPN/wiki/ARPA-targets-tracking-implementation#rsd---radar-system-data | |
17 | // | |
18 | // 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<CR><LF> | |
19 | // Example: $RARSD,0.00,,2.50,005.0,0.00,,4.50,355.0,,,3.0,N,H*51 | |
20 | // Example: $RARSD,,,,,,,,,0.808,326.9,0.750,N,N*58 | |
21 | // Example: $RARSD,0.00,,0.40,,,,,,,,3.0,N,N*53 | |
22 | type RSD struct { | |
23 | BaseSentence | |
24 | Origin1Range float64 // Origin 1 range | |
25 | Origin1Bearing float64 // Origin 1 bearing (degrees from 0°) | |
26 | VariableRangeMarker1 float64 // Variable Range Marker 1 | |
27 | BearingLine1 float64 // Bearing Line 1 | |
28 | ||
29 | Origin2Range float64 // Origin 2 range | |
30 | Origin2Bearing float64 // Origin 2 bearing (degrees from 0°) | |
31 | VariableRangeMarker2 float64 // Variable Range Marker 2 | |
32 | BearingLine2 float64 // Bearing Line 2 | |
33 | ||
34 | CursorRangeFromOwnShip float64 // Cursor Range From Own Ship | |
35 | CursorBearingDegrees float64 // Cursor Bearing (degrees clockwise from 0°) | |
36 | ||
37 | RangeScale float64 // Range scale | |
38 | RangeUnit string // Range units (K = kilometers, N = nautical miles, S = statute miles) | |
39 | DisplayRotation string // Display rotation (C = course up, H = heading up, N - North up) | |
40 | } | |
41 | ||
42 | // newRSD constructor | |
43 | func newRSD(s BaseSentence) (RSD, error) { | |
44 | p := NewParser(s) | |
45 | p.AssertType(TypeRSD) | |
46 | return RSD{ | |
47 | BaseSentence: s, | |
48 | Origin1Range: p.Float64(0, "origin 1 range"), | |
49 | Origin1Bearing: p.Float64(1, "origin 1 bearing"), | |
50 | VariableRangeMarker1: p.Float64(2, "variable range marker 1"), | |
51 | BearingLine1: p.Float64(3, "bearing line 1"), | |
52 | ||
53 | Origin2Range: p.Float64(4, "origin 2 range"), | |
54 | Origin2Bearing: p.Float64(5, "origin 2 bearing"), | |
55 | VariableRangeMarker2: p.Float64(6, "variable range marker 2"), | |
56 | BearingLine2: p.Float64(7, "bearing line 2"), | |
57 | ||
58 | CursorRangeFromOwnShip: p.Float64(8, "cursor range from own ship"), | |
59 | CursorBearingDegrees: p.Float64(9, "cursor bearing"), | |
60 | ||
61 | RangeScale: p.Float64(10, "range scale"), | |
62 | RangeUnit: p.EnumString(11, "range units", DistanceUnitKilometre, DistanceUnitNauticalMile, DistanceUnitStatuteMile), | |
63 | DisplayRotation: p.EnumString(12, "display rotation", RSDDisplayRotationCourseUp, RSDDisplayRotationHeadingUp, RSDDisplayRotationNorthUp), | |
64 | }, p.Err() | |
65 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestRSD(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg RSD | |
13 | }{ | |
14 | { | |
15 | name: "good sentence", | |
16 | raw: "$RARSD,0.00,,2.50,005.0,0.00,,4.50,355.0,,,3.0,N,H*51", | |
17 | msg: RSD{ | |
18 | BaseSentence: BaseSentence{}, | |
19 | Origin1Range: 0, | |
20 | Origin1Bearing: 0, | |
21 | VariableRangeMarker1: 2.5, | |
22 | BearingLine1: 5, | |
23 | Origin2Range: 0, | |
24 | Origin2Bearing: 0, | |
25 | VariableRangeMarker2: 4.5, | |
26 | BearingLine2: 355, | |
27 | CursorRangeFromOwnShip: 0, | |
28 | CursorBearingDegrees: 0, | |
29 | RangeScale: 3, | |
30 | RangeUnit: "N", | |
31 | DisplayRotation: "H", | |
32 | }, | |
33 | }, | |
34 | { | |
35 | name: "good sentence 2", | |
36 | raw: "$RARSD,,,,,,,,,0.808,326.9,0.750,N,N*58", | |
37 | msg: RSD{ | |
38 | BaseSentence: BaseSentence{}, | |
39 | Origin1Range: 0, | |
40 | Origin1Bearing: 0, | |
41 | VariableRangeMarker1: 0, | |
42 | BearingLine1: 0, | |
43 | Origin2Range: 0, | |
44 | Origin2Bearing: 0, | |
45 | VariableRangeMarker2: 0, | |
46 | BearingLine2: 0, | |
47 | CursorRangeFromOwnShip: 0.808, | |
48 | CursorBearingDegrees: 326.9, | |
49 | RangeScale: 0.75, | |
50 | RangeUnit: "N", | |
51 | DisplayRotation: "N", | |
52 | }, | |
53 | }, | |
54 | { | |
55 | name: "invalid nmea: Origin1Range", | |
56 | raw: "$RARSD,x,,2.50,005.0,0.00,,4.50,355.0,,,3.0,N,H*37", | |
57 | err: "nmea: RARSD invalid origin 1 range: x", | |
58 | }, | |
59 | { | |
60 | name: "invalid nmea: Origin1Bearing", | |
61 | raw: "$RARSD,,x,2.50,005.0,0.00,,4.50,355.0,,,3.0,N,H*37", | |
62 | err: "nmea: RARSD invalid origin 1 bearing: x", | |
63 | }, | |
64 | { | |
65 | name: "invalid nmea: VariableRangeMarker1", | |
66 | raw: "$RARSD,,,x2.50,005.0,0.00,,4.50,355.0,,,3.0,N,H*37", | |
67 | err: "nmea: RARSD invalid variable range marker 1: x2.50", | |
68 | }, | |
69 | { | |
70 | name: "invalid nmea: BearingLine1", | |
71 | raw: "$RARSD,,,2.50,x005.0,0.00,,4.50,355.0,,,3.0,N,H*37", | |
72 | err: "nmea: RARSD invalid bearing line 1: x005.0", | |
73 | }, | |
74 | { | |
75 | name: "invalid nmea: Origin2Range", | |
76 | raw: "$RARSD,,,2.50,005.0,x0.00,,4.50,355.0,,,3.0,N,H*37", | |
77 | err: "nmea: RARSD invalid origin 2 range: x0.00", | |
78 | }, | |
79 | { | |
80 | name: "invalid nmea: Origin2Bearing", | |
81 | raw: "$RARSD,,,2.50,005.0,0.00,x,4.50,355.0,,,3.0,N,H*37", | |
82 | err: "nmea: RARSD invalid origin 2 bearing: x", | |
83 | }, | |
84 | { | |
85 | name: "invalid nmea: VariableRangeMarker2", | |
86 | raw: "$RARSD,,,2.50,005.0,0.00,,x4.50,355.0,,,3.0,N,H*37", | |
87 | err: "nmea: RARSD invalid variable range marker 2: x4.50", | |
88 | }, | |
89 | { | |
90 | name: "invalid nmea: BearingLine2", | |
91 | raw: "$RARSD,,,2.50,005.0,0.00,,4.50,x355.0,,,3.0,N,H*37", | |
92 | err: "nmea: RARSD invalid bearing line 2: x355.0", | |
93 | }, | |
94 | { | |
95 | name: "invalid nmea: CursorRangeFromOwnShip", | |
96 | raw: "$RARSD,,,2.50,005.0,0.00,,4.50,355.0,x,,3.0,N,H*37", | |
97 | err: "nmea: RARSD invalid cursor range from own ship: x", | |
98 | }, | |
99 | { | |
100 | name: "invalid nmea: CursorBearingDegrees", | |
101 | raw: "$RARSD,,,2.50,005.0,0.00,,4.50,355.0,,x,3.0,N,H*37", | |
102 | err: "nmea: RARSD invalid cursor bearing: x", | |
103 | }, | |
104 | { | |
105 | name: "invalid nmea: RangeUnit", | |
106 | raw: "$RARSD,,,2.50,005.0,0.00,,4.50,355.0,,,3.0,X,H*59", | |
107 | err: "nmea: RARSD invalid range units: X", | |
108 | }, | |
109 | { | |
110 | name: "invalid nmea: RangeUnit", | |
111 | raw: "$RARSD,,,2.50,005.0,0.00,,4.50,355.0,,,3.0,X,H*59", | |
112 | err: "nmea: RARSD invalid range units: X", | |
113 | }, | |
114 | { | |
115 | name: "invalid nmea: DisplayRotation", | |
116 | raw: "$RARSD,,,2.50,005.0,0.00,,4.50,355.0,,,3.0,N,X*5f", | |
117 | err: "nmea: RARSD invalid display rotation: X", | |
118 | }, | |
119 | } | |
120 | for _, tt := range tests { | |
121 | t.Run(tt.name, func(t *testing.T) { | |
122 | m, err := Parse(tt.raw) | |
123 | if tt.err != "" { | |
124 | assert.Error(t, err) | |
125 | assert.EqualError(t, err, tt.err) | |
126 | } else { | |
127 | assert.NoError(t, err) | |
128 | mm := m.(RSD) | |
129 | mm.BaseSentence = BaseSentence{} | |
130 | assert.Equal(t, tt.msg, mm) | |
131 | } | |
132 | }) | |
133 | } | |
134 | } |
11 | 11 | ) |
12 | 12 | |
13 | 13 | // RTE is a route of waypoints |
14 | // http://aprs.gids.nl/nmea/#rte | |
15 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_rte_routes | |
16 | // | |
17 | // Format: $--RTE,x.x,x.x,a,c--c,c--c, ..... c--c*hh<CR><LF> | |
18 | // Example: $GPRTE,2,1,c,0,PBRCPK,PBRTO,PTELGR,PPLAND,PYAMBU,PPFAIR,PWARRN,PMORTL,PLISMR*73 | |
14 | 19 | type RTE struct { |
15 | 20 | BaseSentence |
16 | 21 | NumberOfSentences int64 // Number of sentences in sequence |
26 | 26 | |
27 | 27 | // ParserFunc callback used to parse specific sentence variants |
28 | 28 | type ParserFunc func(BaseSentence) (Sentence, error) |
29 | ||
30 | // NotSupportedError is returned when parsed sentence is not supported | |
31 | type NotSupportedError struct { | |
32 | Prefix string | |
33 | } | |
34 | ||
35 | // Error returns error message | |
36 | func (p *NotSupportedError) Error() string { | |
37 | return fmt.Sprintf("nmea: sentence prefix '%s' not supported", p.Prefix) | |
38 | } | |
29 | 39 | |
30 | 40 | // Sentence interface for all NMEA sentence |
31 | 41 | type Sentence interface { |
178 | 188 | switch s.Type { |
179 | 189 | case TypeRMC: |
180 | 190 | return newRMC(s) |
191 | case TypeAAM: | |
192 | return newAAM(s) | |
193 | case TypeALA: | |
194 | return newALA(s) | |
195 | case TypeAPB: | |
196 | return newAPB(s) | |
197 | case TypeBEC: | |
198 | return newBEC(s) | |
199 | case TypeBOD: | |
200 | return newBOD(s) | |
201 | case TypeBWC: | |
202 | return newBWC(s) | |
203 | case TypeBWR: | |
204 | return newBWR(s) | |
205 | case TypeBWW: | |
206 | return newBWW(s) | |
207 | case TypeDOR: | |
208 | return newDOR(s) | |
209 | case TypeDSC: | |
210 | return newDSC(s) | |
211 | case TypeDSE: | |
212 | return newDSE(s) | |
213 | case TypeDTM: | |
214 | return newDTM(s) | |
215 | case TypeEVE: | |
216 | return newEVE(s) | |
217 | case TypeFIR: | |
218 | return newFIR(s) | |
181 | 219 | case TypeGGA: |
182 | 220 | return newGGA(s) |
183 | 221 | case TypeGSA: |
190 | 228 | return newZDA(s) |
191 | 229 | case TypePGRME: |
192 | 230 | return newPGRME(s) |
231 | case TypePHTRO: | |
232 | return newPHTRO(s) | |
233 | case TypePRDID: | |
234 | return newPRDID(s) | |
235 | case TypePSONCMS: | |
236 | return newPSONCMS(s) | |
193 | 237 | case TypeGSV: |
194 | 238 | return newGSV(s) |
239 | case TypeHDG: | |
240 | return newHDG(s) | |
195 | 241 | case TypeHDT: |
196 | 242 | return newHDT(s) |
243 | case TypeHDM: | |
244 | return newHDM(s) | |
245 | case TypeHSC: | |
246 | return newHSC(s) | |
197 | 247 | case TypeGNS: |
198 | 248 | return newGNS(s) |
199 | 249 | case TypeTHS: |
200 | 250 | return newTHS(s) |
251 | case TypeTLL: | |
252 | return newTLL(s) | |
253 | case TypeTTM: | |
254 | return newTTM(s) | |
255 | case TypeTXT: | |
256 | return newTXT(s) | |
201 | 257 | case TypeWPL: |
202 | 258 | return newWPL(s) |
259 | case TypeRMB: | |
260 | return newRMB(s) | |
261 | case TypeRPM: | |
262 | return newRPM(s) | |
263 | case TypeRSA: | |
264 | return newRSA(s) | |
265 | case TypeRSD: | |
266 | return newRSD(s) | |
203 | 267 | case TypeRTE: |
204 | 268 | return newRTE(s) |
269 | case TypeROT: | |
270 | return newROT(s) | |
271 | case TypeVBW: | |
272 | return newVBW(s) | |
273 | case TypeVDR: | |
274 | return newVDR(s) | |
205 | 275 | case TypeVHW: |
206 | 276 | return newVHW(s) |
277 | case TypeVPW: | |
278 | return newVPW(s) | |
279 | case TypeVLW: | |
280 | return newVLW(s) | |
281 | case TypeVWR: | |
282 | return newVWR(s) | |
283 | case TypeVWT: | |
284 | return newVWT(s) | |
207 | 285 | case TypeDPT: |
208 | 286 | return newDPT(s) |
209 | 287 | case TypeDBT: |
210 | 288 | return newDBT(s) |
289 | case TypeDBK: | |
290 | return newDBK(s) | |
211 | 291 | case TypeDBS: |
212 | 292 | return newDBS(s) |
213 | 293 | case TypeMDA: |
214 | 294 | return newMDA(s) |
295 | case TypeMTA: | |
296 | return newMTA(s) | |
297 | case TypeMTW: | |
298 | return newMTW(s) | |
215 | 299 | case TypeMWD: |
216 | 300 | return newMWD(s) |
217 | 301 | case TypeMWV: |
218 | 302 | return newMWV(s) |
303 | case TypeOSD: | |
304 | return newOSD(s) | |
305 | case TypeXDR: | |
306 | return newXDR(s) | |
307 | case TypeXTE: | |
308 | return newXTE(s) | |
219 | 309 | } |
220 | 310 | } |
221 | 311 | if strings.HasPrefix(s.Raw, SentenceStartEncapsulated) { |
224 | 314 | return newVDMVDO(s) |
225 | 315 | } |
226 | 316 | } |
227 | return nil, fmt.Errorf("nmea: sentence prefix '%s' not supported", s.Prefix()) | |
228 | } | |
317 | return nil, &NotSupportedError{Prefix: s.Prefix()} | |
318 | } |
0 | 0 | package nmea |
1 | 1 | |
2 | 2 | import ( |
3 | "errors" | |
3 | 4 | "testing" |
4 | 5 | |
5 | 6 | "github.com/stretchr/testify/assert" |
57 | 58 | }, |
58 | 59 | }, |
59 | 60 | { |
60 | name: "valid NMEA 4.10 TAG Block", | |
61 | raw: "\\s:Satelite_1,c:1553390539*62\\!AIVDM,1,1,,A,13M@ah0025QdPDTCOl`K6`nV00Sv,0*52", | |
61 | name: "valid NMEA 4.10 TAG Block", | |
62 | raw: "\\s:Satelite_1,c:1553390539*62\\!AIVDM,1,1,,A,13M@ah0025QdPDTCOl`K6`nV00Sv,0*52", | |
62 | 63 | datatype: "VDM", |
63 | 64 | talkerid: "AI", |
64 | prefix: "AIVDM", | |
65 | prefix: "AIVDM", | |
65 | 66 | sent: BaseSentence{ |
66 | 67 | Talker: "AI", |
67 | 68 | Type: "VDM", |
107 | 108 | { |
108 | 109 | name: "missing TAG Block start delimiter", |
109 | 110 | raw: "s:Satelite_1,c:1553390539*62\\!AIVDM,1,1,,A,13M@ah0025QdPDTCOl`K6`nV00Sv,0*52", |
110 | err: "nmea: sentence does not start with a '$' or '!'", | |
111 | err: "nmea: sentence does not start with a '$' or '!'", | |
111 | 112 | }, |
112 | 113 | { |
113 | 114 | name: "missing TAG Block end delimiter", |
114 | 115 | raw: "\\s:Satelite_1,c:1553390539*62!AIVDM,1,1,,A,13M@ah0025QdPDTCOl`K6`nV00Sv,0*52", |
115 | err: "nmea: sentence does not start with a '$' or '!'", | |
116 | err: "nmea: sentence does not start with a '$' or '!'", | |
116 | 117 | }, |
117 | 118 | { |
118 | 119 | name: "invalid TAG Block contents", |
119 | 120 | raw: "\\\\!AIVDM,1,1,,A,13M@ah0025QdPDTCOl`K6`nV00Sv,0*52", |
120 | err: "nmea: tagblock does not contain checksum separator", | |
121 | err: "nmea: tagblock does not contain checksum separator", | |
121 | 122 | }, |
122 | 123 | } |
123 | 124 | |
190 | 191 | var parsetests = []struct { |
191 | 192 | name string |
192 | 193 | raw string |
193 | err string | |
194 | err error | |
194 | 195 | msg interface{} |
195 | 196 | }{ |
196 | 197 | { |
197 | 198 | name: "bad sentence", |
198 | 199 | raw: "SDFSD,2340dfmswd", |
199 | err: "nmea: sentence does not start with a '$' or '!'", | |
200 | err: errors.New("nmea: sentence does not start with a '$' or '!'"), | |
200 | 201 | }, |
201 | 202 | { |
202 | 203 | name: "bad sentence type", |
203 | 204 | raw: "$INVALID,123,123,*7D", |
204 | err: "nmea: sentence prefix 'INVALID' not supported", | |
205 | err: &NotSupportedError{Prefix: "INVALID"}, | |
205 | 206 | }, |
206 | 207 | { |
207 | 208 | name: "bad encapsulated sentence type", |
208 | 209 | raw: "!INVALID,1,2,*7E", |
209 | err: "nmea: sentence prefix 'INVALID' not supported", | |
210 | err: &NotSupportedError{Prefix: "INVALID"}, | |
210 | 211 | }, |
211 | 212 | } |
212 | 213 | |
214 | 215 | for _, tt := range parsetests { |
215 | 216 | t.Run(tt.name, func(t *testing.T) { |
216 | 217 | m, err := Parse(tt.raw) |
217 | if tt.err != "" { | |
218 | assert.EqualError(t, err, tt.err) | |
218 | if tt.err != nil { | |
219 | assert.Equal(t, err, tt.err) | |
219 | 220 | } else { |
220 | 221 | assert.NoError(t, err) |
221 | 222 | assert.Equal(t, tt.msg, m) |
16 | 16 | |
17 | 17 | // THS is the Actual vessel heading in degrees True with status. |
18 | 18 | // http://www.nuovamarea.net/pytheas_9.html |
19 | // http://manuals.spectracom.com/VSP/Content/VSP/NMEA_THSmess.htm | |
20 | // | |
21 | // Format: $--THS,xxx.xx,c*hh<CR><LF> | |
22 | // Example: $GPTHS,338.01,A*36 | |
19 | 23 | type THS struct { |
20 | 24 | BaseSentence |
21 | 25 | Heading float64 // Heading in degrees |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypeTLL type of TLL sentence for Target latitude and longitude | |
4 | TypeTLL = "TLL" | |
5 | ||
6 | // RadarTargetLost is used when target is lost | |
7 | RadarTargetLost = "L" | |
8 | // RadarTargetAcquisition is used when target is acquired | |
9 | RadarTargetAcquisition = "Q" | |
10 | // RadarTargetTracking is used when tracking target | |
11 | RadarTargetTracking = "T" | |
12 | ) | |
13 | ||
14 | // TLL - Target latitude and longitude | |
15 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_tll_target_latitude_and_longitude | |
16 | // https://github.com/nohal/OpenCPN/wiki/ARPA-targets-tracking-implementation#tll---target-latitude-and-longitude | |
17 | // | |
18 | // Format: $--TLL,xx,llll.ll,a,yyyyy.yy,a,c--c,hhmmss.ss,a,a*hh<CR><LF> | |
19 | // Example: $RATLL,,3647.422,N,01432.592,E,,,,*58 | |
20 | type TLL struct { | |
21 | BaseSentence | |
22 | TargetNumber int64 // Target number 00 – 99 | |
23 | TargetLatitude float64 // Target latitude + N/S | |
24 | TargetLongitude float64 // Target longitude + E/W | |
25 | TargetName string // Target name | |
26 | TimeUTC Time // UTC of data, hh is hours, mm is minutes, ss.ss is seconds. | |
27 | TargetStatus string // Target status (L=lost, Q=acquisition, T=tracking) | |
28 | ReferenceTarget string // Reference target, R= reference target; null (,,)= otherwise | |
29 | } | |
30 | ||
31 | // newTLL constructor | |
32 | func newTLL(s BaseSentence) (TLL, error) { | |
33 | p := NewParser(s) | |
34 | p.AssertType(TypeTLL) | |
35 | return TLL{ | |
36 | BaseSentence: s, | |
37 | TargetNumber: p.Int64(0, "target number"), | |
38 | TargetLatitude: p.LatLong(1, 2, "latitude"), | |
39 | TargetLongitude: p.LatLong(3, 4, "longitude"), | |
40 | TargetName: p.String(5, "target name"), | |
41 | TimeUTC: p.Time(6, "UTC time"), | |
42 | TargetStatus: p.EnumString(7, "target status", RadarTargetLost, RadarTargetAcquisition, RadarTargetTracking), | |
43 | ReferenceTarget: p.EnumString(8, "reference target", "R"), | |
44 | }, p.Err() | |
45 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestTLL(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg TLL | |
13 | }{ | |
14 | { | |
15 | name: "good sentence", | |
16 | raw: "$RATLL,,3647.422,N,01432.592,E,,,,*58", | |
17 | msg: TLL{ | |
18 | BaseSentence: BaseSentence{}, | |
19 | TargetNumber: 0, | |
20 | TargetLatitude: 36.790366666666664, | |
21 | TargetLongitude: 14.543200000000002, | |
22 | TargetName: "", | |
23 | TimeUTC: Time{ | |
24 | Valid: false, | |
25 | Hour: 0, | |
26 | Minute: 0, | |
27 | Second: 0, | |
28 | Millisecond: 0, | |
29 | }, | |
30 | TargetStatus: "", | |
31 | ReferenceTarget: "", | |
32 | }, | |
33 | }, | |
34 | { | |
35 | name: "good sentence 2", | |
36 | raw: "$RATLL,1,3646.54266,N,00235.37778,W,test,020915,L,R*78", | |
37 | msg: TLL{ | |
38 | BaseSentence: BaseSentence{}, | |
39 | TargetNumber: 1, | |
40 | TargetLatitude: 36.775711, | |
41 | TargetLongitude: -2.5896296666666667, | |
42 | TargetName: "test", | |
43 | TimeUTC: Time{Valid: true, Hour: 2, Minute: 9, Second: 15, Millisecond: 0}, | |
44 | TargetStatus: "L", | |
45 | ReferenceTarget: "R", | |
46 | }, | |
47 | }, | |
48 | { | |
49 | name: "invalid nmea: TargetNumber", | |
50 | raw: "$RATLL,x,3647.422,N,01432.592,E,,,,*20", | |
51 | err: "nmea: RATLL invalid target number: x", | |
52 | }, | |
53 | { | |
54 | name: "invalid nmea: TargetLatitude", | |
55 | raw: "$RATLL,1,x3647.422,N,01432.592,E,,,,*11", | |
56 | err: "nmea: RATLL invalid latitude: cannot parse [x3647.422 N], unknown format", | |
57 | }, | |
58 | { | |
59 | name: "invalid nmea: TargetLongitude", | |
60 | raw: "$RATLL,1,3647.422,N,x01432.592,E,,,,*11", | |
61 | err: "nmea: RATLL invalid longitude: cannot parse [x01432.592 E], unknown format", | |
62 | }, | |
63 | { | |
64 | name: "invalid nmea: TimeUTC", | |
65 | raw: "$RATLL,1,3646.54266,N,00235.37778,W,test,x020915,L,R*00", | |
66 | err: "nmea: RATLL invalid UTC time: x020915", | |
67 | }, | |
68 | { | |
69 | name: "invalid nmea: TargetStatus", | |
70 | raw: "$RATLL,1,3646.54266,N,00235.37778,W,test,020915,xL,R*00", | |
71 | err: "nmea: RATLL invalid target status: xL", | |
72 | }, | |
73 | { | |
74 | name: "invalid nmea: ReferenceTarget", | |
75 | raw: "$RATLL,1,3646.54266,N,00235.37778,W,test,020915,L,xR*00", | |
76 | err: "nmea: RATLL invalid reference target: xR", | |
77 | }, | |
78 | } | |
79 | for _, tt := range tests { | |
80 | t.Run(tt.name, func(t *testing.T) { | |
81 | m, err := Parse(tt.raw) | |
82 | if tt.err != "" { | |
83 | assert.Error(t, err) | |
84 | assert.EqualError(t, err, tt.err) | |
85 | } else { | |
86 | assert.NoError(t, err) | |
87 | mm := m.(TLL) | |
88 | mm.BaseSentence = BaseSentence{} | |
89 | assert.Equal(t, tt.msg, mm) | |
90 | } | |
91 | }) | |
92 | } | |
93 | } |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypeTTM type of TTM sentence for Tracked Target Message | |
4 | TypeTTM = "TTM" | |
5 | ) | |
6 | ||
7 | // TTM - Tracked Target Message | |
8 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_ttm_tracked_target_message | |
9 | // https://github.com/nohal/OpenCPN/wiki/ARPA-targets-tracking-implementation#ttm---tracked-target-message | |
10 | // | |
11 | // Format: $--TTM,xx,x.x,x.x,a,x.x,x.x,a,x.x,x.x,a,c--c,a,a*hh<CR><LF> | |
12 | // 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<CR><LF> | |
13 | // Example: $RATTM,02,1.43,170.5,T,0.16,264.4,T,1.42,36.9,N,,T,,,M*2A | |
14 | type TTM struct { | |
15 | BaseSentence | |
16 | TargetNumber int64 // Target number 00 – 99 | |
17 | TargetDistance float64 // Target Distance | |
18 | Bearing float64 // Bearing from own ship, degrees | |
19 | BearingType string // Type of target Bearing, T = True, R = Relative | |
20 | TargetSpeed float64 // Target Speed | |
21 | TargetCourse float64 // Target Course | |
22 | CourseType string // target course type, T = True, R = Relative | |
23 | DistanceCPA float64 // Distance of closest-point-of-approach | |
24 | TimeCPA float64 // Time until closest-point-of-approach "-" means increasing | |
25 | SpeedUnits string // Speed/distance units, K/N/S | |
26 | TargetName string // Target name | |
27 | TargetStatus string // Target status (L=lost, Q=acquisition, T=tracking) | |
28 | ReferenceTarget string // Reference target, R= reference target; null (,,)= otherwise | |
29 | TimeUTC Time // UTC of data, hh is hours, mm is minutes, ss.ss is seconds. | |
30 | TypeOfAcquisition string // Type, A = Auto, M = Manual, R = Reported | |
31 | } | |
32 | ||
33 | // newTTM constructor | |
34 | func newTTM(s BaseSentence) (TTM, error) { | |
35 | p := NewParser(s) | |
36 | p.AssertType(TypeTTM) | |
37 | return TTM{ | |
38 | BaseSentence: s, | |
39 | TargetNumber: p.Int64(0, "target number"), | |
40 | TargetDistance: p.Float64(1, "target Distance"), | |
41 | Bearing: p.Float64(2, "bearing"), | |
42 | BearingType: p.EnumString(3, "bearing type", "T", "R"), | |
43 | TargetSpeed: p.Float64(4, "target speed"), | |
44 | TargetCourse: p.Float64(5, "target course"), | |
45 | CourseType: p.EnumString(6, "course type", "T", "R"), | |
46 | DistanceCPA: p.Float64(7, "distance CPA"), | |
47 | TimeCPA: p.Float64(8, "time of CPA"), | |
48 | SpeedUnits: p.EnumString(9, "speed units", DistanceUnitKilometre, DistanceUnitNauticalMile, DistanceUnitStatuteMile), | |
49 | TargetName: p.String(10, "target name"), | |
50 | TargetStatus: p.EnumString(11, "target status", RadarTargetLost, RadarTargetAcquisition, RadarTargetTracking), | |
51 | ReferenceTarget: p.EnumString(12, "reference target", "R"), | |
52 | TimeUTC: p.Time(13, "UTC time"), | |
53 | TypeOfAcquisition: p.EnumString(14, "type of acquisition", "A", "M", "R"), | |
54 | }, p.Err() | |
55 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestTTM(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg TTM | |
13 | }{ | |
14 | { | |
15 | name: "good sentence", | |
16 | raw: "$RATTM,02,1.43,170.5,T,0.16,264.4,T,1.42,36.9,N,,T,,,M*2A", | |
17 | msg: TTM{ | |
18 | BaseSentence: BaseSentence{}, | |
19 | TargetNumber: 2, | |
20 | TargetDistance: 1.43, | |
21 | Bearing: 170.5, | |
22 | BearingType: "T", | |
23 | TargetSpeed: 0.16, | |
24 | TargetCourse: 264.4, | |
25 | CourseType: "T", | |
26 | DistanceCPA: 1.42, | |
27 | TimeCPA: 36.9, | |
28 | SpeedUnits: "N", | |
29 | TargetName: "", | |
30 | TargetStatus: "T", | |
31 | ReferenceTarget: "", | |
32 | TimeUTC: Time{Valid: false, Hour: 0, Minute: 0, Second: 0, Millisecond: 0}, | |
33 | TypeOfAcquisition: "M", | |
34 | }, | |
35 | }, | |
36 | { | |
37 | name: "invalid nmea: TargetNumber", | |
38 | raw: "$RATTM,x02,1.43,170.5,T,0.16,264.4,T,1.42,36.9,N,,T,,,M*52", | |
39 | err: "nmea: RATTM invalid target number: x02", | |
40 | }, | |
41 | { | |
42 | name: "invalid nmea: TargetDistance", | |
43 | raw: "$RATTM,02,x1.43,170.5,T,0.16,264.4,T,1.42,36.9,N,,T,,,M*52", | |
44 | err: "nmea: RATTM invalid target Distance: x1.43", | |
45 | }, | |
46 | { | |
47 | name: "invalid nmea: Bearing", | |
48 | raw: "$RATTM,02,1.43,x170.5,T,0.16,264.4,T,1.42,36.9,N,,T,,,M*52", | |
49 | err: "nmea: RATTM invalid bearing: x170.5", | |
50 | }, | |
51 | { | |
52 | name: "invalid nmea: BearingType", | |
53 | raw: "$RATTM,02,1.43,170.5,xT,0.16,264.4,T,1.42,36.9,N,,T,,,M*52", | |
54 | err: "nmea: RATTM invalid bearing type: xT", | |
55 | }, | |
56 | { | |
57 | name: "invalid nmea: TargetSpeed", | |
58 | raw: "$RATTM,02,1.43,170.5,T,x0.16,264.4,T,1.42,36.9,N,,T,,,M*52", | |
59 | err: "nmea: RATTM invalid target speed: x0.16", | |
60 | }, | |
61 | { | |
62 | name: "invalid nmea: TargetCourse", | |
63 | raw: "$RATTM,02,1.43,170.5,T,0.16,x264.4,T,1.42,36.9,N,,T,,,M*52", | |
64 | err: "nmea: RATTM invalid target course: x264.4", | |
65 | }, | |
66 | { | |
67 | name: "invalid nmea: CourseType", | |
68 | raw: "$RATTM,02,1.43,170.5,T,0.16,264.4,xT,1.42,36.9,N,,T,,,M*52", | |
69 | err: "nmea: RATTM invalid course type: xT", | |
70 | }, | |
71 | { | |
72 | name: "invalid nmea: DistanceCPA", | |
73 | raw: "$RATTM,02,1.43,170.5,T,0.16,264.4,T,x1.42,36.9,N,,T,,,M*52", | |
74 | err: "nmea: RATTM invalid distance CPA: x1.42", | |
75 | }, | |
76 | { | |
77 | name: "invalid nmea: TimeCPA", | |
78 | raw: "$RATTM,02,1.43,170.5,T,0.16,264.4,T,1.42,x36.9,N,,T,,,M*52", | |
79 | err: "nmea: RATTM invalid time of CPA: x36.9", | |
80 | }, | |
81 | { | |
82 | name: "invalid nmea: SpeedUnits", | |
83 | raw: "$RATTM,02,1.43,170.5,T,0.16,264.4,T,1.42,36.9,xN,,T,,,M*52", | |
84 | err: "nmea: RATTM invalid speed units: xN", | |
85 | }, | |
86 | { | |
87 | name: "invalid nmea: ReferenceTarget", | |
88 | raw: "$RATTM,02,1.43,170.5,T,0.16,264.4,T,1.42,36.9,N,,T,x,,M*52", | |
89 | err: "nmea: RATTM invalid reference target: x", | |
90 | }, | |
91 | { | |
92 | name: "invalid nmea: ReferenceTarget", | |
93 | raw: "$RATTM,02,1.43,170.5,T,0.16,264.4,T,1.42,36.9,N,,T,x,,M*52", | |
94 | err: "nmea: RATTM invalid reference target: x", | |
95 | }, | |
96 | { | |
97 | name: "invalid nmea: TimeUTC", | |
98 | raw: "$RATTM,02,1.43,170.5,T,0.16,264.4,T,1.42,36.9,N,,T,,x,M*52", | |
99 | err: "nmea: RATTM invalid UTC time: x", | |
100 | }, | |
101 | { | |
102 | name: "invalid nmea: TypeOfAcquisition", | |
103 | raw: "$RATTM,02,1.43,170.5,T,0.16,264.4,T,1.42,36.9,N,,T,,x,M*52", | |
104 | err: "nmea: RATTM invalid UTC time: x", | |
105 | }, | |
106 | } | |
107 | for _, tt := range tests { | |
108 | t.Run(tt.name, func(t *testing.T) { | |
109 | m, err := Parse(tt.raw) | |
110 | if tt.err != "" { | |
111 | assert.Error(t, err) | |
112 | assert.EqualError(t, err, tt.err) | |
113 | } else { | |
114 | assert.NoError(t, err) | |
115 | mm := m.(TTM) | |
116 | mm.BaseSentence = BaseSentence{} | |
117 | assert.Equal(t, tt.msg, mm) | |
118 | } | |
119 | }) | |
120 | } | |
121 | } |
0 | package nmea | |
1 | ||
2 | import "strings" | |
3 | ||
4 | const ( | |
5 | // TypeTXT type for TXT sentences for the transmission of text messages | |
6 | TypeTXT = "TXT" | |
7 | ) | |
8 | ||
9 | // TXT is sentence for the transmission of short text messages, longer text messages may be transmitted by using | |
10 | // multiple sentences. This sentence is intended to convey human readable textual information for display purposes. | |
11 | // The TXT sentence shall not be used for sending commands and making device configuration changes. | |
12 | // https://www.nmea.org/Assets/20160520%20txt%20amendment.pdf | |
13 | // | |
14 | // Format: $--TXT,xx,xx,xx,c-c*hh<CR><LF> | |
15 | // Example: $GNTXT,01,01,02,u-blox AG - www.u-blox.com*4E | |
16 | type TXT struct { | |
17 | BaseSentence | |
18 | TotalNumber int64 // total number of sentences, 01 to 99 | |
19 | Number int64 // number of current sentences, 01 to 99 | |
20 | ID int64 // identifier of the text message, 01 to 99 | |
21 | // Message contains ASCII characters, and code delimiters if needed, up to the maximum permitted sentence length | |
22 | // (i.e., up to 61 characters including any code delimiters) | |
23 | Message string | |
24 | } | |
25 | ||
26 | // newTXT constructor | |
27 | func newTXT(s BaseSentence) (TXT, error) { | |
28 | p := NewParser(s) | |
29 | p.AssertType(TypeTXT) | |
30 | m := TXT{ | |
31 | BaseSentence: s, | |
32 | TotalNumber: p.Int64(0, "total number of sentences"), | |
33 | Number: p.Int64(1, "sentence number"), | |
34 | ID: p.Int64(2, "sentence identifier"), | |
35 | Message: strings.Join(p.Fields[3:], FieldSep), | |
36 | } | |
37 | return m, p.Err() | |
38 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestTXT(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg TXT | |
13 | }{ | |
14 | { | |
15 | name: "good sentence", | |
16 | raw: "$GNTXT,01,01,02,u-blox AG - www.u-blox.com*4E", | |
17 | msg: TXT{ | |
18 | TotalNumber: 1, | |
19 | Number: 1, | |
20 | ID: 2, | |
21 | Message: "u-blox AG - www.u-blox.com", | |
22 | }, | |
23 | }, | |
24 | { | |
25 | name: "invalid TotalNumber", | |
26 | raw: "$GNTXT,x,01,02,u-blox AG - www.u-blox.com*37", | |
27 | err: "nmea: GNTXT invalid total number of sentences: x", | |
28 | }, | |
29 | { | |
30 | name: "invalid Number", | |
31 | raw: "$GNTXT,01,X,02,u-blox AG - www.u-blox.com*17", | |
32 | err: "nmea: GNTXT invalid sentence number: X", | |
33 | }, | |
34 | { | |
35 | name: "invalid ID", | |
36 | raw: "$GNTXT,01,01,X,u-blox AG - www.u-blox.com*14", | |
37 | err: "nmea: GNTXT invalid sentence identifier: X", | |
38 | }, | |
39 | } | |
40 | for _, tt := range tests { | |
41 | t.Run(tt.name, func(t *testing.T) { | |
42 | m, err := Parse(tt.raw) | |
43 | if tt.err != "" { | |
44 | assert.Error(t, err) | |
45 | assert.EqualError(t, err, tt.err) | |
46 | } else { | |
47 | assert.NoError(t, err) | |
48 | txt := m.(TXT) | |
49 | txt.BaseSentence = BaseSentence{} | |
50 | assert.Equal(t, tt.msg, txt) | |
51 | } | |
52 | }) | |
53 | } | |
54 | } |
9 | 9 | "strconv" |
10 | 10 | "strings" |
11 | 11 | "unicode" |
12 | ) | |
13 | ||
14 | const ( | |
15 | // StatusValid indicated status having valid value | |
16 | StatusValid = "A" | |
17 | // StatusInvalid indicated status having invalid value | |
18 | StatusInvalid = "V" | |
19 | ) | |
20 | ||
21 | const ( | |
22 | // UnitAmpere is unit for current in Amperes | |
23 | UnitAmpere = "A" | |
24 | // UnitBars is unit for pressure in Bars | |
25 | UnitBars = "B" | |
26 | // UnitBinary is unit for binary data | |
27 | UnitBinary = "B" | |
28 | // UnitCelsius is unit for temperature in Celsius | |
29 | UnitCelsius = TemperatureCelsius | |
30 | // UnitFahrenheit is unit for temperature in Fahrenheit | |
31 | UnitFahrenheit = TemperatureFahrenheit | |
32 | // UnitDegrees is unit for angular displacement in Degrees | |
33 | UnitDegrees = "D" | |
34 | // UnitHertz is unit for frequency in Hertz | |
35 | UnitHertz = "H" | |
36 | // UnitLitresPerSecond is unit for volumetric flow in Litres per second | |
37 | UnitLitresPerSecond = "I" | |
38 | // UnitKelvin is unit of temperature in Kelvin | |
39 | UnitKelvin = TemperatureKelvin | |
40 | // UnitKilogramPerCubicMetre is unit of density in kilogram per cubic metre | |
41 | UnitKilogramPerCubicMetre = "K" | |
42 | // UnitMeters is unit of distance in Meters | |
43 | UnitMeters = DistanceUnitMetre | |
44 | // UnitCubicMeters is unit of volume in cubic meters | |
45 | UnitCubicMeters = "M" | |
46 | // UnitRevolutionsPerMinute is unit of rotational speed or the frequency of rotation around a fixed axis in revolutions per minute (RPM) | |
47 | UnitRevolutionsPerMinute = "R" | |
48 | // UnitPercent is percent of full range | |
49 | UnitPercent = "P" | |
50 | // UnitPascal is unit of pressure in Pascals | |
51 | UnitPascal = "P" | |
52 | // 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. | |
53 | UnitPartsPerThousand = "S" | |
54 | // UnitVolts is unit of voltage in Volts | |
55 | UnitVolts = "V" | |
56 | ) | |
57 | ||
58 | const ( | |
59 | // 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) | |
60 | SpeedKnots = "N" | |
61 | // SpeedMeterPerSecond is unit of speed of 1 meter per second | |
62 | SpeedMeterPerSecond = "M" | |
63 | // SpeedKilometerPerHour is unit of speed of 1 kilometer per hour | |
64 | SpeedKilometerPerHour = "K" | |
65 | ) | |
66 | ||
67 | const ( | |
68 | // TemperatureCelsius is unit of temperature measured in celsius. °C = (°F − 32) / 1,8 | |
69 | TemperatureCelsius = "C" | |
70 | // TemperatureFahrenheit is unit of temperature measured in fahrenheits. °F = °C * 1,8 + 32 | |
71 | TemperatureFahrenheit = "F" | |
72 | // TemperatureKelvin is unit of temperature measured in kelvins. K = °C + 273,15 | |
73 | TemperatureKelvin = "K" | |
74 | ) | |
75 | ||
76 | // In navigation, the heading of a vessel or object is the compass direction in which the craft's bow or nose is pointed. | |
77 | // Note that the heading may not necessarily be the direction that the vehicle actually travels, which is known as | |
78 | // its course or track. | |
79 | // https://en.wikipedia.org/wiki/Heading_(navigation) | |
80 | const ( | |
81 | // HeadingMagnetic - Magnetic heading is your direction relative to magnetic north, read from your magnetic compass. | |
82 | // Magnetic north is the point on the Earth's surface where its magnetic field points directly downwards. | |
83 | HeadingMagnetic = "M" | |
84 | // HeadingTrue - True heading is your direction relative to true north, or the geographic north pole. | |
85 | // True north is the northern axis of rotation of the Earth. It is the point where the lines of longitude converge | |
86 | // on maps. | |
87 | HeadingTrue = "T" | |
88 | ) | |
89 | ||
90 | // In nautical navigation the absolute bearing is the clockwise angle between north and an object observed from the vessel. | |
91 | // https://en.wikipedia.org/wiki/Bearing_(angle) | |
92 | const ( | |
93 | // BearingMagnetic is the clockwise angle between Earth's magnetic north and an object observed from the vessel. | |
94 | BearingMagnetic = "M" | |
95 | // BearingTrue is the clockwise angle between Earth's true (geographical) north and an object observed from the vessel. | |
96 | BearingTrue = "T" | |
97 | ) | |
98 | ||
99 | // FAAMode is type for FAA mode indicator (NMEA 2.3 and later). | |
100 | // In NMEA 2.3, several sentences (APB, BWC, BWR, GLL, RMA, RMB, RMC, VTG, WCV, and XTE) got a new last field carrying | |
101 | // the signal integrity information needed by the FAA. | |
102 | // Source: https://www.xj3.nl/dokuwiki/doku.php?id=nmea | |
103 | // Note: there can be other values (proprietary). | |
104 | const ( | |
105 | // FAAModeAutonomous is Autonomous mode | |
106 | FAAModeAutonomous = "A" | |
107 | // FAAModeDifferential is Differential Mode | |
108 | FAAModeDifferential = "D" | |
109 | // FAAModeEstimated is Estimated (dead-reckoning) mode | |
110 | FAAModeEstimated = "E" | |
111 | // FAAModeRTKFloat is RTK Float mode | |
112 | FAAModeRTKFloat = "F" | |
113 | // FAAModeManualInput is Manual Input Mode | |
114 | FAAModeManualInput = "M" | |
115 | // FAAModeDataNotValid is Data Not Valid | |
116 | FAAModeDataNotValid = "N" | |
117 | // FAAModePrecise is Precise (NMEA4.00+) | |
118 | FAAModePrecise = "P" | |
119 | // FAAModeRTKInteger is RTK Integer mode | |
120 | FAAModeRTKInteger = "R" | |
121 | // FAAModeSimulated is Simulated Mode | |
122 | FAAModeSimulated = "S" | |
123 | ) | |
124 | ||
125 | // Navigation Status (NMEA 4.1 and later) | |
126 | const ( | |
127 | // NavStatusAutonomous is Autonomous mode | |
128 | NavStatusAutonomous = "A" | |
129 | // NavStatusDifferential is Differential Mode | |
130 | NavStatusDifferential = "D" | |
131 | // NavStatusEstimated is Estimated (dead-reckoning) mode | |
132 | NavStatusEstimated = "E" | |
133 | // NavStatusManualInput is Manual Input Mode | |
134 | NavStatusManualInput = "M" | |
135 | // NavStatusSimulated is Simulated Mode | |
136 | NavStatusSimulated = "S" | |
137 | // NavStatusDataNotValid is Data Not Valid | |
138 | NavStatusDataNotValid = "N" | |
139 | // NavStatusDataValid is valid | |
140 | NavStatusDataValid = "V" | |
141 | ) | |
142 | ||
143 | const ( | |
144 | // DistanceUnitKilometre is unit for distance in kilometres (1km = 1000m) | |
145 | DistanceUnitKilometre = "K" | |
146 | // DistanceUnitNauticalMile is unit for distance in nautical miles (1nmi = 1852m) | |
147 | DistanceUnitNauticalMile = "N" | |
148 | // DistanceUnitStatuteMile is unit for distance in statute miles (1smi = 5,280 feet = 1609.344m) | |
149 | DistanceUnitStatuteMile = "S" | |
150 | // DistanceUnitMetre is unit for distance in metres | |
151 | DistanceUnitMetre = "M" | |
152 | // DistanceUnitFeet is unit for distance in feets (1f = 0.3048m) | |
153 | DistanceUnitFeet = "f" | |
154 | // DistanceUnitFathom is unit for distance in fathoms (1fm = 6ft = 1,8288m) | |
155 | DistanceUnitFathom = "F" | |
12 | 156 | ) |
13 | 157 | |
14 | 158 | const ( |
28 | 172 | East = "E" |
29 | 173 | // West value |
30 | 174 | West = "W" |
175 | // Left value | |
176 | Left = "L" | |
177 | // Right value | |
178 | Right = "R" | |
31 | 179 | ) |
32 | 180 | |
33 | 181 | // ParseLatLong parses the supplied string into the LatLong. |
35 | 183 | // Supported formats are: |
36 | 184 | // - DMS (e.g. 33° 23' 22") |
37 | 185 | // - Decimal (e.g. 33.23454) |
38 | // - GPS (e.g 15113.4322S) | |
186 | // - GPS (e.g 15113.4322 S) | |
39 | 187 | // |
40 | 188 | func ParseLatLong(s string) (float64, error) { |
41 | 189 | var l float64 |
52 | 200 | } |
53 | 201 | |
54 | 202 | // ParseGPS parses a GPS/NMEA coordinate. |
55 | // e.g 15113.4322S | |
203 | // e.g `15113.4322 S` | |
56 | 204 | func ParseGPS(s string) (float64, error) { |
57 | 205 | parts := strings.Split(s, " ") |
58 | 206 | if len(parts) != 2 { |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypeVBW type of VBW sentence for Dual Ground/Water Speed | |
4 | TypeVBW = "VBW" | |
5 | ) | |
6 | ||
7 | // VBW - Dual Ground/Water Speed | |
8 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_vbw_dual_groundwater_speed | |
9 | // | |
10 | // Format: $--VBW,x.x,x.x,A,x.x,x.x,A,x.x,A,x.x,A*hh<CR><LF> | |
11 | // Example: $VMVBW,-7.1,0.1,A,,,V,,V,,V*65 | |
12 | type VBW struct { | |
13 | BaseSentence | |
14 | LongitudinalWaterSpeedKnots float64 // longitudinal water speed, "-" means astern, knots | |
15 | TransverseWaterSpeedKnots float64 // transverse water speed, "-" means port, knots | |
16 | WaterSpeedStatusValid bool // A = true | |
17 | WaterSpeedStatus string // A = valid, V = invalid | |
18 | ||
19 | LongitudinalGroundSpeedKnots float64 // longitudinal ground speed, "-" means astern, knots | |
20 | TransverseGroundSpeedKnots float64 // transverse ground speed, "-" means port, knots | |
21 | GroundSpeedStatusValid bool // A = true | |
22 | GroundSpeedStatus string // A = valid, V = invalid | |
23 | ||
24 | SternTraverseWaterSpeedKnots float64 // Stern traverse water speed, knots (NMEA 3 and above) | |
25 | SternTraverseWaterSpeedStatusValid bool // A = true | |
26 | SternTraverseWaterSpeedStatus string // A = valid, V = invalid (NMEA 3 and above) | |
27 | ||
28 | SternTraverseGroundSpeedKnots float64 // Stern traverse ground speed, knots (NMEA 3 and above) | |
29 | SternTraverseGroundSpeedStatusValid bool // A = true | |
30 | SternTraverseGroundSpeedStatus string // A = valid, V = invalid (NMEA 3 and above) | |
31 | } | |
32 | ||
33 | // newVBW constructor | |
34 | func newVBW(s BaseSentence) (VBW, error) { | |
35 | p := NewParser(s) | |
36 | p.AssertType(TypeVBW) | |
37 | ||
38 | m := VBW{ | |
39 | BaseSentence: s, | |
40 | LongitudinalWaterSpeedKnots: p.Float64(0, "longitudinal water speed"), | |
41 | TransverseWaterSpeedKnots: p.Float64(1, "transverse water speed"), | |
42 | WaterSpeedStatusValid: p.String(2, "water speed status valid") == StatusValid, | |
43 | WaterSpeedStatus: p.EnumString(2, "water speed status", StatusValid, StatusInvalid), | |
44 | ||
45 | LongitudinalGroundSpeedKnots: p.Float64(3, "longitudinal ground speed"), | |
46 | TransverseGroundSpeedKnots: p.Float64(4, "transverse ground speed"), | |
47 | GroundSpeedStatusValid: p.String(5, "ground speed status valid") == StatusValid, | |
48 | GroundSpeedStatus: p.EnumString(5, "ground speed status", StatusValid, StatusInvalid), | |
49 | } | |
50 | if len(p.Fields) > 6 { | |
51 | m.SternTraverseWaterSpeedKnots = p.Float64(6, "stern traverse water speed") | |
52 | m.SternTraverseWaterSpeedStatusValid = p.String(7, "stern water speed status valid") == StatusValid | |
53 | m.SternTraverseWaterSpeedStatus = p.EnumString(7, "stern water speed status", StatusValid, StatusInvalid) | |
54 | ||
55 | m.SternTraverseGroundSpeedKnots = p.Float64(8, "stern traverse ground speed") | |
56 | m.SternTraverseGroundSpeedStatusValid = p.String(9, "stern ground speed status valid") == StatusValid | |
57 | m.SternTraverseGroundSpeedStatus = p.EnumString(9, "stern ground speed status", StatusValid, StatusInvalid) | |
58 | } | |
59 | ||
60 | return m, p.Err() | |
61 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestVBW(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg VBW | |
13 | }{ | |
14 | { | |
15 | name: "good sentence", | |
16 | raw: "$VMVBW,-7.1,0.1,A,,,V,,V,,V*65", | |
17 | msg: VBW{ | |
18 | LongitudinalWaterSpeedKnots: -7.1, | |
19 | TransverseWaterSpeedKnots: 0.1, | |
20 | WaterSpeedStatusValid: true, | |
21 | WaterSpeedStatus: "A", | |
22 | LongitudinalGroundSpeedKnots: 0, | |
23 | TransverseGroundSpeedKnots: 0, | |
24 | GroundSpeedStatusValid: false, | |
25 | GroundSpeedStatus: "V", | |
26 | SternTraverseWaterSpeedKnots: 0, | |
27 | SternTraverseWaterSpeedStatusValid: false, | |
28 | SternTraverseWaterSpeedStatus: "V", | |
29 | SternTraverseGroundSpeedKnots: 0, | |
30 | SternTraverseGroundSpeedStatusValid: false, | |
31 | SternTraverseGroundSpeedStatus: "V", | |
32 | }, | |
33 | }, | |
34 | { | |
35 | name: "invalid nmea: LongitudinalWaterSpeedKnots", | |
36 | raw: "$VMVBW,x,0.1,A,,,V,,V,,V*18", | |
37 | err: "nmea: VMVBW invalid longitudinal water speed: x", | |
38 | }, | |
39 | { | |
40 | name: "invalid nmea: TransverseWaterSpeedKnots", | |
41 | raw: "$VMVBW,0.1,x,A,0.3,0.4,A,0.5,A,0.6,A*0b", | |
42 | err: "nmea: VMVBW invalid transverse water speed: x", | |
43 | }, | |
44 | { | |
45 | name: "invalid nmea: WaterSpeedStatusValid", | |
46 | raw: "$VMVBW,0.1,0.2,X,0.3,0.4,A,0.5,A,0.6,A*46", | |
47 | err: "nmea: VMVBW invalid water speed status: X", | |
48 | }, | |
49 | { | |
50 | name: "invalid nmea: LongitudinalGroundSpeedKnots", | |
51 | raw: "$VMVBW,0.1,0.2,A,X,0.4,A,0.5,A,0.6,A*2a", | |
52 | err: "nmea: VMVBW invalid longitudinal ground speed: X", | |
53 | }, | |
54 | { | |
55 | name: "invalid nmea: TransverseGroundSpeedKnots", | |
56 | raw: "$VMVBW,0.1,0.2,A,0.3,X,A,0.5,A,0.6,A*2d", | |
57 | err: "nmea: VMVBW invalid transverse ground speed: X", | |
58 | }, | |
59 | { | |
60 | name: "invalid nmea: GroundSpeedStatusValid", | |
61 | raw: "$VMVBW,0.1,0.2,A,0.3,0.4,X,0.5,A,0.6,A*46", | |
62 | err: "nmea: VMVBW invalid ground speed status: X", | |
63 | }, | |
64 | { | |
65 | name: "invalid nmea: SternTraverseWaterSpeedKnots", | |
66 | raw: "$VMVBW,0.1,0.2,A,0.3,0.4,A,X,A,0.6,A*2c", | |
67 | err: "nmea: VMVBW invalid stern traverse water speed: X", | |
68 | }, | |
69 | { | |
70 | name: "invalid nmea: SternTraverseWaterSpeedStatusValid", | |
71 | raw: "$VMVBW,0.1,0.2,A,0.3,0.4,A,0.5,X,0.6,A*46", | |
72 | err: "nmea: VMVBW invalid stern water speed status: X", | |
73 | }, | |
74 | { | |
75 | name: "invalid nmea: SternTraverseGroundSpeedKnots", | |
76 | raw: "$VMVBW,0.1,0.2,A,0.3,0.4,A,0.5,A,X,A*2f", | |
77 | err: "nmea: VMVBW invalid stern traverse ground speed: X", | |
78 | }, | |
79 | { | |
80 | name: "invalid nmea: SternTraverseGroundSpeedStatusValid", | |
81 | raw: "$VMVBW,0.1,0.2,A,0.3,0.4,A,0.5,A,0.6,X*46", | |
82 | err: "nmea: VMVBW invalid stern ground speed status: X", | |
83 | }, | |
84 | } | |
85 | for _, tt := range tests { | |
86 | t.Run(tt.name, func(t *testing.T) { | |
87 | m, err := Parse(tt.raw) | |
88 | if tt.err != "" { | |
89 | assert.Error(t, err) | |
90 | assert.EqualError(t, err, tt.err) | |
91 | } else { | |
92 | assert.NoError(t, err) | |
93 | mm := m.(VBW) | |
94 | mm.BaseSentence = BaseSentence{} | |
95 | assert.Equal(t, tt.msg, mm) | |
96 | } | |
97 | }) | |
98 | } | |
99 | } |
7 | 7 | TypeVDO = "VDO" |
8 | 8 | ) |
9 | 9 | |
10 | // VDMVDO is a format used to encapsulate generic binary payloads. It is most commonly used | |
11 | // with AIS data. | |
12 | // http://catb.org/gpsd/AIVDM.html | |
10 | // VDMVDO is sentence ($--VDM or $--VDO) used to encapsulate generic binary payloads. It is most commonly used with AIS data. | |
11 | // https://gpsd.gitlab.io/gpsd/AIVDM.html | |
12 | // | |
13 | // Example: !AIVDM,1,1,,B,177KQJ5000G?tO`K>RA1wUbN0TKH,0*5C | |
13 | 14 | type VDMVDO struct { |
14 | 15 | BaseSentence |
15 | 16 | NumFragments int64 |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypeVDR type of VDR sentence for Set and Drift | |
4 | TypeVDR = "VDR" | |
5 | ) | |
6 | ||
7 | // VDR - Set and Drift | |
8 | // In navigation, set and drift are characteristics of the current and velocity of water over the ground in which a ship | |
9 | // is sailing. Set is the bearing the current is flowing. Drift is the magnitude of the current. | |
10 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_vdr_set_and_drift | |
11 | // | |
12 | // Format: $--VDR,x.x,T,x.x,M,x.x,N*hh<CR><LF> | |
13 | // Example: $IIVDR,10.1,T,12.3,M,1.2,N*3A | |
14 | type VDR struct { | |
15 | BaseSentence | |
16 | SetDegreesTrue float64 // Direction degrees, True | |
17 | SetDegreesTrueUnit string // T = True | |
18 | SetDegreesMagnetic float64 // Direction degrees, True | |
19 | SetDegreesMagneticUnit string // M = Magnetic | |
20 | DriftKnots float64 // Current speed, knots | |
21 | DriftUnit string // N = Knots | |
22 | } | |
23 | ||
24 | // newVDR constructor | |
25 | func newVDR(s BaseSentence) (VDR, error) { | |
26 | p := NewParser(s) | |
27 | p.AssertType(TypeVDR) | |
28 | return VDR{ | |
29 | BaseSentence: s, | |
30 | SetDegreesTrue: p.Float64(0, "true set degrees"), | |
31 | SetDegreesTrueUnit: p.EnumString(1, "true set unit", BearingTrue), | |
32 | SetDegreesMagnetic: p.Float64(2, "magnetic set degrees"), | |
33 | SetDegreesMagneticUnit: p.EnumString(3, "magnetic set unit", BearingMagnetic), | |
34 | DriftKnots: p.Float64(4, "drift knots"), | |
35 | DriftUnit: p.EnumString(5, "drift unit", SpeedKnots), | |
36 | }, p.Err() | |
37 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestVDR(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg VDR | |
13 | }{ | |
14 | { | |
15 | name: "good sentence", | |
16 | raw: "$IIVDR,10.1,T,12.3,M,1.2,N*3A", | |
17 | msg: VDR{ | |
18 | SetDegreesTrue: 10.1, | |
19 | SetDegreesTrueUnit: BearingTrue, | |
20 | SetDegreesMagnetic: 12.3, | |
21 | SetDegreesMagneticUnit: BearingMagnetic, | |
22 | DriftKnots: 1.2, | |
23 | DriftUnit: SpeedKnots, | |
24 | }, | |
25 | }, | |
26 | { | |
27 | name: "invalid nmea: SetDegreesTrueUnit", | |
28 | raw: "$IIVDR,10.1,x,12.3,M,1.2,N*16", | |
29 | err: "nmea: IIVDR invalid true set unit: x", | |
30 | }, | |
31 | { | |
32 | name: "invalid nmea: SetDegreesMagneticUnit", | |
33 | raw: "$IIVDR,10.1,T,12.3,x,1.2,N*0f", | |
34 | err: "nmea: IIVDR invalid magnetic set unit: x", | |
35 | }, | |
36 | { | |
37 | name: "invalid nmea: DriftUnit", | |
38 | raw: "$IIVDR,10.1,T,12.3,M,1.2,x*0c", | |
39 | err: "nmea: IIVDR invalid drift unit: x", | |
40 | }, | |
41 | } | |
42 | for _, tt := range tests { | |
43 | t.Run(tt.name, func(t *testing.T) { | |
44 | m, err := Parse(tt.raw) | |
45 | if tt.err != "" { | |
46 | assert.Error(t, err) | |
47 | assert.EqualError(t, err, tt.err) | |
48 | } else { | |
49 | assert.NoError(t, err) | |
50 | vdr := m.(VDR) | |
51 | vdr.BaseSentence = BaseSentence{} | |
52 | assert.Equal(t, tt.msg, vdr) | |
53 | } | |
54 | }) | |
55 | } | |
56 | } |
5 | 5 | ) |
6 | 6 | |
7 | 7 | // VHW contains information about water speed and heading |
8 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_vhw_water_speed_and_heading | |
9 | // | |
10 | // Format: $--VHW,x.x,T,x.x,M,x.x,N,x.x,K*hh<CR><LF> | |
11 | // Example: $VWVHW,45.0,T,43.0,M,3.5,N,6.4,K*56 | |
8 | 12 | type VHW struct { |
9 | 13 | BaseSentence |
10 | 14 | TrueHeading float64 |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypeVLW type of VLW sentence for Distance Traveled through Water | |
4 | TypeVLW = "VLW" | |
5 | ) | |
6 | ||
7 | // VLW - Distance Traveled through Water | |
8 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_vlw_distance_traveled_through_water | |
9 | // | |
10 | // Format: $--VLW,x.x,N,x.x,N*hh<CR><LF> | |
11 | // Format (NMEA 3+): $--VLW,x.x,N,x.x,N,x.x,N,x.x,N*hh<CR><LF> | |
12 | // Example: $IIVLW,10.1,N,3.2,N*7C | |
13 | // Example: $IIVLW,10.1,N,3.2,N,0,N,0,N*7C | |
14 | type VLW struct { | |
15 | BaseSentence | |
16 | TotalInWater float64 // Total cumulative water distance, nm | |
17 | TotalInWaterUnit string // N = Nautical Miles | |
18 | SinceResetInWater float64 // Water distance since Reset, nm | |
19 | SinceResetInWaterUnit string // N = Nautical Miles | |
20 | TotalOnGround float64 // Total cumulative ground distance, nm (NMEA 3 and above) | |
21 | TotalOnGroundUnit string // N = Nautical Miles (NMEA 3 and above) | |
22 | SinceResetOnGround float64 // Ground distance since reset, nm (NMEA 3 and above) | |
23 | SinceResetOnGroundUnit string // N = Nautical Miles (NMEA 3 and above) | |
24 | } | |
25 | ||
26 | // newVLW constructor | |
27 | func newVLW(s BaseSentence) (VLW, error) { | |
28 | p := NewParser(s) | |
29 | p.AssertType(TypeVLW) | |
30 | ||
31 | vlw := VLW{ | |
32 | BaseSentence: s, | |
33 | TotalInWater: p.Float64(0, "total cumulative water distance"), | |
34 | TotalInWaterUnit: p.EnumString(1, "total cumulative water distance unit", DistanceUnitNauticalMile), | |
35 | SinceResetInWater: p.Float64(2, "water distance since reset"), | |
36 | SinceResetInWaterUnit: p.EnumString(3, "water distance since reset unit", DistanceUnitNauticalMile), | |
37 | } | |
38 | if len(p.Fields) > 4 { | |
39 | vlw.TotalOnGround = p.Float64(4, "total cumulative ground distance") | |
40 | vlw.TotalOnGroundUnit = p.EnumString(5, "total cumulative ground distance unit", DistanceUnitNauticalMile) | |
41 | vlw.SinceResetOnGround = p.Float64(6, "ground distance since reset") | |
42 | vlw.SinceResetOnGroundUnit = p.EnumString(7, "ground distance since reset unit", DistanceUnitNauticalMile) | |
43 | } | |
44 | return vlw, p.Err() | |
45 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestVLW(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg VLW | |
13 | }{ | |
14 | { | |
15 | name: "good sentence 1", | |
16 | raw: "$IIVLW,10.1,N,3.2,N*7C", | |
17 | msg: VLW{ | |
18 | TotalInWater: 10.1, | |
19 | TotalInWaterUnit: "N", | |
20 | SinceResetInWater: 3.2, | |
21 | SinceResetInWaterUnit: "N", | |
22 | TotalOnGround: 0, | |
23 | TotalOnGroundUnit: "", | |
24 | SinceResetOnGround: 0, | |
25 | SinceResetOnGroundUnit: "", | |
26 | }, | |
27 | }, | |
28 | { | |
29 | name: "good sentence 2", | |
30 | raw: "$IIVLW,10.1,N,3.2,N,1,N,0.1,N*62", | |
31 | msg: VLW{ | |
32 | TotalInWater: 10.1, | |
33 | TotalInWaterUnit: "N", | |
34 | SinceResetInWater: 3.2, | |
35 | SinceResetInWaterUnit: "N", | |
36 | TotalOnGround: 1, | |
37 | TotalOnGroundUnit: "N", | |
38 | SinceResetOnGround: 0.1, | |
39 | SinceResetOnGroundUnit: "N", | |
40 | }, | |
41 | }, | |
42 | { | |
43 | name: "invalid nmea: TotalInWaterUnit", | |
44 | raw: "$IIVLW,10.1,x,3.2,N,1,N,0.1,N*54", | |
45 | err: "nmea: IIVLW invalid total cumulative water distance unit: x", | |
46 | }, | |
47 | { | |
48 | name: "invalid nmea: SinceResetInWaterUnit", | |
49 | raw: "$IIVLW,10.1,N,3.2,x,1,N,0.1,N*54", | |
50 | err: "nmea: IIVLW invalid water distance since reset unit: x", | |
51 | }, | |
52 | { | |
53 | name: "invalid nmea: TotalOnGroundUnit", | |
54 | raw: "$IIVLW,10.1,N,3.2,N,1,x,0.1,N*54", | |
55 | err: "nmea: IIVLW invalid total cumulative ground distance unit: x", | |
56 | }, | |
57 | { | |
58 | name: "invalid nmea: SinceResetOnGroundUnit", | |
59 | raw: "$IIVLW,10.1,N,3.2,N,1,N,0.1,x*54", | |
60 | err: "nmea: IIVLW invalid ground distance since reset unit: x", | |
61 | }, | |
62 | } | |
63 | for _, tt := range tests { | |
64 | t.Run(tt.name, func(t *testing.T) { | |
65 | m, err := Parse(tt.raw) | |
66 | if tt.err != "" { | |
67 | assert.Error(t, err) | |
68 | assert.EqualError(t, err, tt.err) | |
69 | } else { | |
70 | assert.NoError(t, err) | |
71 | vlw := m.(VLW) | |
72 | vlw.BaseSentence = BaseSentence{} | |
73 | assert.Equal(t, tt.msg, vlw) | |
74 | } | |
75 | }) | |
76 | } | |
77 | } |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypeVPW type of VPW sentence for Speed Measured Parallel to Wind | |
4 | TypeVPW = "VPW" | |
5 | ) | |
6 | ||
7 | // VPW - Speed Measured Parallel to Wind | |
8 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_vpw_speed_measured_parallel_to_wind | |
9 | // | |
10 | // Format: $--VPW,x.x,N,x.x,M*hh<CR><LF> | |
11 | // Example: $IIVPW,4.5,N,6.7,M*52 | |
12 | type VPW struct { | |
13 | BaseSentence | |
14 | SpeedKnots float64 // Speed, "-" means downwind, knots | |
15 | SpeedKnotsUnit string // N = knots | |
16 | SpeedMPS float64 // Speed, "-" means downwind, m/s | |
17 | SpeedMPSUnit string // M = m/s | |
18 | } | |
19 | ||
20 | // newVPW constructor | |
21 | func newVPW(s BaseSentence) (VPW, error) { | |
22 | p := NewParser(s) | |
23 | p.AssertType(TypeVPW) | |
24 | return VPW{ | |
25 | BaseSentence: s, | |
26 | SpeedKnots: p.Float64(0, "wind speed in knots"), | |
27 | SpeedKnotsUnit: p.EnumString(1, "wind speed in knots unit", SpeedKnots), | |
28 | SpeedMPS: p.Float64(2, "wind speed in meters per second"), | |
29 | SpeedMPSUnit: p.EnumString(3, "wind speed in meters per second unit", SpeedMeterPerSecond), | |
30 | }, p.Err() | |
31 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestVPW(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg VPW | |
13 | }{ | |
14 | { | |
15 | name: "good sentence", | |
16 | raw: "$IIVPW,4.5,N,6.7,M*52", | |
17 | msg: VPW{ | |
18 | SpeedKnots: 4.5, | |
19 | SpeedKnotsUnit: SpeedKnots, | |
20 | SpeedMPS: 6.7, | |
21 | SpeedMPSUnit: SpeedMeterPerSecond, | |
22 | }, | |
23 | }, | |
24 | { | |
25 | name: "invalid nmea: SpeedKnotsUnit", | |
26 | raw: "$IIVPW,4.5,x,6.7,M*64", | |
27 | err: "nmea: IIVPW invalid wind speed in knots unit: x", | |
28 | }, | |
29 | { | |
30 | name: "invalid nmea: SpeedMPSUnit", | |
31 | raw: "$IIVPW,4.5,N,6.7,x*67", | |
32 | err: "nmea: IIVPW invalid wind speed in meters per second unit: x", | |
33 | }, | |
34 | } | |
35 | for _, tt := range tests { | |
36 | t.Run(tt.name, func(t *testing.T) { | |
37 | m, err := Parse(tt.raw) | |
38 | if tt.err != "" { | |
39 | assert.Error(t, err) | |
40 | assert.EqualError(t, err, tt.err) | |
41 | } else { | |
42 | assert.NoError(t, err) | |
43 | vpw := m.(VPW) | |
44 | vpw.BaseSentence = BaseSentence{} | |
45 | assert.Equal(t, tt.msg, vpw) | |
46 | } | |
47 | }) | |
48 | } | |
49 | } |
6 | 6 | |
7 | 7 | // VTG represents track & speed data. |
8 | 8 | // http://aprs.gids.nl/nmea/#vtg |
9 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_vtg_track_made_good_and_ground_speed | |
10 | // | |
11 | // Format: $--VTG,x.x,T,x.x,M,x.x,N,x.x,K*hh<CR><LF> | |
12 | // Format (NMEA 2.3+): $--VTG,x.x,T,x.x,M,x.x,N,x.x,K,m*hh<CR><LF> | |
13 | // Example: $GPVTG,45.5,T,67.5,M,30.45,N,56.40,K*4B | |
14 | // $GPVTG,220.86,T,,M,2.550,N,4.724,K,A*34 | |
9 | 15 | type VTG struct { |
10 | 16 | BaseSentence |
11 | 17 | TrueTrack float64 |
12 | 18 | MagneticTrack float64 |
13 | 19 | GroundSpeedKnots float64 |
14 | 20 | GroundSpeedKPH float64 |
21 | FFAMode string // FAA mode indicator (filled in NMEA 2.3 and later) | |
15 | 22 | } |
16 | 23 | |
17 | 24 | // newVTG parses the VTG sentence into this struct. |
19 | 26 | func newVTG(s BaseSentence) (VTG, error) { |
20 | 27 | p := NewParser(s) |
21 | 28 | p.AssertType(TypeVTG) |
22 | return VTG{ | |
29 | vtg := VTG{ | |
23 | 30 | BaseSentence: s, |
24 | 31 | TrueTrack: p.Float64(0, "true track"), |
25 | 32 | MagneticTrack: p.Float64(2, "magnetic track"), |
26 | 33 | GroundSpeedKnots: p.Float64(4, "ground speed (knots)"), |
27 | 34 | GroundSpeedKPH: p.Float64(6, "ground speed (km/h)"), |
28 | }, p.Err() | |
35 | } | |
36 | if len(p.Fields) > 8 { | |
37 | vtg.FFAMode = p.String(8, "FAA mode") // not enum because some devices have proprietary "non-nmea" values | |
38 | } | |
39 | return vtg, p.Err() | |
29 | 40 | } |
19 | 19 | MagneticTrack: 67.5, |
20 | 20 | GroundSpeedKnots: 30.45, |
21 | 21 | GroundSpeedKPH: 56.4, |
22 | FFAMode: "", | |
23 | }, | |
24 | }, | |
25 | { | |
26 | name: "good sentence with FAA mode", | |
27 | raw: "$GPVTG,220.86,T,,M,2.550,N,4.724,K,A*34", | |
28 | msg: VTG{ | |
29 | TrueTrack: 220.86, | |
30 | MagneticTrack: 0, | |
31 | GroundSpeedKnots: 2.55, | |
32 | GroundSpeedKPH: 4.724, | |
33 | FFAMode: "A", | |
22 | 34 | }, |
23 | 35 | }, |
24 | 36 | { |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypeVWR type of VWR sentence for Relative Wind Speed and Angle | |
4 | TypeVWR = "VWR" | |
5 | ) | |
6 | ||
7 | // VWR - Relative Wind Speed and Angle. Speed is measured relative to the moving vessel. | |
8 | // According to NMEA: use of $--MWV is recommended. | |
9 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_vwr_relative_wind_speed_and_angle | |
10 | // https://www.nmea.org/Assets/100108_nmea_0183_sentences_not_recommended_for_new_designs.pdf (page 16) | |
11 | // | |
12 | // Format: $--VWR,x.x,a,x.x,N,x.x,M,x.x,K*hh<CR><LF> | |
13 | // Example: $IIVWR,75,R,1.0,N,0.51,M,1.85,K*6C | |
14 | // $IIVWR,024,L,018,N,,,,*5e | |
15 | // $IIVWR,,,,,,,,*53 | |
16 | type VWR struct { | |
17 | BaseSentence | |
18 | MeasuredAngle float64 // Measured Wind direction magnitude in degrees (0 to 180 deg) | |
19 | MeasuredDirectionBow string // Measured Wind direction Left/Right of bow | |
20 | SpeedKnots float64 // Measured wind Speed, knots | |
21 | SpeedKnotsUnit string // N = knots | |
22 | SpeedMPS float64 // Wind speed, meters/second | |
23 | SpeedMPSUnit string // M = m/s | |
24 | SpeedKPH float64 // Wind speed, km/hour | |
25 | SpeedKPHUnit string // M = km/h | |
26 | } | |
27 | ||
28 | // newVWR constructor | |
29 | func newVWR(s BaseSentence) (VWR, error) { | |
30 | p := NewParser(s) | |
31 | p.AssertType(TypeVWR) | |
32 | return VWR{ | |
33 | BaseSentence: s, | |
34 | MeasuredAngle: p.Float64(0, "measured wind angle"), | |
35 | MeasuredDirectionBow: p.EnumString(1, "measured wind direction to bow", Left, Right), | |
36 | SpeedKnots: p.Float64(2, "wind speed in knots"), | |
37 | SpeedKnotsUnit: p.EnumString(3, "wind speed in knots unit", SpeedKnots), | |
38 | SpeedMPS: p.Float64(4, "wind speed in meters per second"), | |
39 | SpeedMPSUnit: p.EnumString(5, "wind speed in meters per second unit", SpeedMeterPerSecond), | |
40 | SpeedKPH: p.Float64(6, "wind speed in kilometers per hour"), | |
41 | SpeedKPHUnit: p.EnumString(7, "wind speed in kilometers per hour unit", SpeedKilometerPerHour), | |
42 | }, p.Err() | |
43 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestVWR(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg VWR | |
13 | }{ | |
14 | { // these examples are from SignalK | |
15 | name: "good sentence", | |
16 | raw: "$IIVWR,75,R,1.0,N,0.51,M,1.85,K*6C", | |
17 | msg: VWR{ | |
18 | MeasuredAngle: 75, | |
19 | MeasuredDirectionBow: Right, | |
20 | SpeedKnots: 1, | |
21 | SpeedKnotsUnit: SpeedKnots, | |
22 | SpeedMPS: 0.51, | |
23 | SpeedMPSUnit: SpeedMeterPerSecond, | |
24 | SpeedKPH: 1.85, | |
25 | SpeedKPHUnit: SpeedKilometerPerHour, | |
26 | }, | |
27 | }, | |
28 | { | |
29 | name: "good sentence, shorter but still valid", | |
30 | raw: "$IIVWR,024,L,018,N,,,,*5e", | |
31 | msg: VWR{ | |
32 | MeasuredAngle: 24, | |
33 | MeasuredDirectionBow: Left, | |
34 | SpeedKnots: 18, | |
35 | SpeedKnotsUnit: SpeedKnots, | |
36 | SpeedMPS: 0, | |
37 | SpeedMPSUnit: "", | |
38 | SpeedKPH: 0, | |
39 | SpeedKPHUnit: "", | |
40 | }, | |
41 | }, | |
42 | { | |
43 | name: "good sentence, handle empty values", | |
44 | raw: "$IIVWR,,,,,,,,*53", | |
45 | msg: VWR{ | |
46 | MeasuredAngle: 0, | |
47 | MeasuredDirectionBow: "", | |
48 | SpeedKnots: 0, | |
49 | SpeedKnotsUnit: "", | |
50 | SpeedMPS: 0, | |
51 | SpeedMPSUnit: "", | |
52 | SpeedKPH: 0, | |
53 | SpeedKPHUnit: "", | |
54 | }, | |
55 | }, | |
56 | { | |
57 | name: "invalid nmea: DirectionBow", | |
58 | raw: "$IIVWR,75,x,1.0,N,0.51,M,1.85,K*46", | |
59 | err: "nmea: IIVWR invalid measured wind direction to bow: x", | |
60 | }, | |
61 | { | |
62 | name: "invalid nmea: SpeedKnotsUnit", | |
63 | raw: "$IIVWR,75,R,1.0,x,0.51,M,1.85,K*5a", | |
64 | err: "nmea: IIVWR invalid wind speed in knots unit: x", | |
65 | }, | |
66 | { | |
67 | name: "invalid nmea: SpeedMPSUnit", | |
68 | raw: "$IIVWR,75,R,1.0,N,0.51,x,1.85,K*59", | |
69 | err: "nmea: IIVWR invalid wind speed in meters per second unit: x", | |
70 | }, | |
71 | { | |
72 | name: "invalid nmea: SpeedKPHUnit", | |
73 | raw: "$IIVWR,75,R,1.0,N,0.51,M,1.85,x*5f", | |
74 | err: "nmea: IIVWR invalid wind speed in kilometers per hour unit: x", | |
75 | }, | |
76 | } | |
77 | for _, tt := range tests { | |
78 | t.Run(tt.name, func(t *testing.T) { | |
79 | m, err := Parse(tt.raw) | |
80 | if tt.err != "" { | |
81 | assert.Error(t, err) | |
82 | assert.EqualError(t, err, tt.err) | |
83 | } else { | |
84 | assert.NoError(t, err) | |
85 | vwr := m.(VWR) | |
86 | vwr.BaseSentence = BaseSentence{} | |
87 | assert.Equal(t, tt.msg, vwr) | |
88 | } | |
89 | }) | |
90 | } | |
91 | } |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypeVWT type of VWT sentence for True Wind Speed and Angle | |
4 | TypeVWT = "VWT" | |
5 | ) | |
6 | ||
7 | // VWT - True Wind Speed and Angle | |
8 | // https://www.nmea.org/Assets/100108_nmea_0183_sentences_not_recommended_for_new_designs.pdf | |
9 | // https://www.rubydoc.info/gems/nmea_plus/1.0.20/NMEAPlus/Message/NMEA/VWT | |
10 | // https://lists.gnu.org/archive/html/gpsd-dev/2012-04/msg00048.html | |
11 | // | |
12 | // Format: $--VWT,x.x,a,x.x,N,x.x,M,x.x,K*hh<CR><LF> | |
13 | // Example: $IIVWT,75,x,1.0,N,0.51,M,1.85,K*40 | |
14 | type VWT struct { | |
15 | BaseSentence | |
16 | TrueAngle float64 // true Wind direction magnitude in degrees (0 to 180 deg) | |
17 | TrueDirectionBow string // true Wind direction Left/Right of bow | |
18 | SpeedKnots float64 // true wind Speed, knots | |
19 | SpeedKnotsUnit string // N = knots | |
20 | SpeedMPS float64 // Wind speed, meters/second | |
21 | SpeedMPSUnit string // M = m/s | |
22 | SpeedKPH float64 // Wind speed, km/hour | |
23 | SpeedKPHUnit string // M = km/h | |
24 | } | |
25 | ||
26 | // newVWT constructor | |
27 | func newVWT(s BaseSentence) (VWT, error) { | |
28 | p := NewParser(s) | |
29 | p.AssertType(TypeVWT) | |
30 | return VWT{ | |
31 | BaseSentence: s, | |
32 | TrueAngle: p.Float64(0, "true wind angle"), | |
33 | TrueDirectionBow: p.EnumString(1, "true wind direction to bow", Left, Right), | |
34 | SpeedKnots: p.Float64(2, "wind speed in knots"), | |
35 | SpeedKnotsUnit: p.EnumString(3, "wind speed in knots unit", SpeedKnots), | |
36 | SpeedMPS: p.Float64(4, "wind speed in meters per second"), | |
37 | SpeedMPSUnit: p.EnumString(5, "wind speed in meters per second unit", SpeedMeterPerSecond), | |
38 | SpeedKPH: p.Float64(6, "wind speed in kilometers per hour"), | |
39 | SpeedKPHUnit: p.EnumString(7, "wind speed in kilometers per hour unit", SpeedKilometerPerHour), | |
40 | }, p.Err() | |
41 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestVWT(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg VWT | |
13 | }{ | |
14 | { // these examples are from SignalK | |
15 | name: "good sentence", | |
16 | raw: "$IIVWT,75,R,1.0,N,0.51,M,1.85,K*6A", | |
17 | msg: VWT{ | |
18 | TrueAngle: 75, | |
19 | TrueDirectionBow: Right, | |
20 | SpeedKnots: 1, | |
21 | SpeedKnotsUnit: SpeedKnots, | |
22 | SpeedMPS: 0.51, | |
23 | SpeedMPSUnit: SpeedMeterPerSecond, | |
24 | SpeedKPH: 1.85, | |
25 | SpeedKPHUnit: SpeedKilometerPerHour, | |
26 | }, | |
27 | }, | |
28 | { | |
29 | name: "good sentence, shorter but still valid", | |
30 | raw: "$IIVWT,024,L,018,N,,,,*58", | |
31 | msg: VWT{ | |
32 | TrueAngle: 24, | |
33 | TrueDirectionBow: Left, | |
34 | SpeedKnots: 18, | |
35 | SpeedKnotsUnit: SpeedKnots, | |
36 | SpeedMPS: 0, | |
37 | SpeedMPSUnit: "", | |
38 | SpeedKPH: 0, | |
39 | SpeedKPHUnit: "", | |
40 | }, | |
41 | }, | |
42 | { | |
43 | name: "good sentence, handle empty values", | |
44 | raw: "$IIVWT,,,,,,,,*55", | |
45 | msg: VWT{ | |
46 | TrueAngle: 0, | |
47 | TrueDirectionBow: "", | |
48 | SpeedKnots: 0, | |
49 | SpeedKnotsUnit: "", | |
50 | SpeedMPS: 0, | |
51 | SpeedMPSUnit: "", | |
52 | SpeedKPH: 0, | |
53 | SpeedKPHUnit: "", | |
54 | }, | |
55 | }, | |
56 | { | |
57 | name: "invalid nmea: DirectionBow", | |
58 | raw: "$IIVWT,75,x,1.0,N,0.51,M,1.85,K*40", | |
59 | err: "nmea: IIVWT invalid true wind direction to bow: x", | |
60 | }, | |
61 | { | |
62 | name: "invalid nmea: SpeedKnotsUnit", | |
63 | raw: "$IIVWT,75,R,1.0,x,0.51,M,1.85,K*5c", | |
64 | err: "nmea: IIVWT invalid wind speed in knots unit: x", | |
65 | }, | |
66 | { | |
67 | name: "invalid nmea: SpeedMPSUnit", | |
68 | raw: "$IIVWT,75,R,1.0,N,0.51,x,1.85,K*5f", | |
69 | err: "nmea: IIVWT invalid wind speed in meters per second unit: x", | |
70 | }, | |
71 | { | |
72 | name: "invalid nmea: SpeedKPHUnit", | |
73 | raw: "$IIVWT,75,R,1.0,N,0.51,M,1.85,x*59", | |
74 | err: "nmea: IIVWT invalid wind speed in kilometers per hour unit: x", | |
75 | }, | |
76 | } | |
77 | for _, tt := range tests { | |
78 | t.Run(tt.name, func(t *testing.T) { | |
79 | m, err := Parse(tt.raw) | |
80 | if tt.err != "" { | |
81 | assert.Error(t, err) | |
82 | assert.EqualError(t, err, tt.err) | |
83 | } else { | |
84 | assert.NoError(t, err) | |
85 | vwt := m.(VWT) | |
86 | vwt.BaseSentence = BaseSentence{} | |
87 | assert.Equal(t, tt.msg, vwt) | |
88 | } | |
89 | }) | |
90 | } | |
91 | } |
5 | 5 | ) |
6 | 6 | |
7 | 7 | // WPL contains information about a waypoint location |
8 | // http://aprs.gids.nl/nmea/#wpl | |
9 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_wpl_waypoint_location | |
10 | // | |
11 | // Format: $--WPL,llll.ll,a,yyyyy.yy,a,c--c*hh<CR><LF> | |
12 | // Example: $IIWPL,5503.4530,N,01037.2742,E,411*6F | |
8 | 13 | type WPL struct { |
9 | 14 | BaseSentence |
10 | 15 | Latitude float64 // Latitude |
0 | package nmea | |
1 | ||
2 | import "errors" | |
3 | ||
4 | const ( | |
5 | // TypeXDR type of XDR sentence for Transducer Measurement | |
6 | TypeXDR = "XDR" | |
7 | ) | |
8 | ||
9 | const ( | |
10 | // TransducerAngularDisplacementXDR is transducer type for Angular displacement | |
11 | TransducerAngularDisplacementXDR = "A" | |
12 | // TransducerTemperatureXDR is transducer type for Temperature | |
13 | TransducerTemperatureXDR = "C" | |
14 | // TransducerDepthXDR is transducer type for Depth | |
15 | TransducerDepthXDR = "D" | |
16 | // TransducerFrequencyXDR is transducer type for Frequency | |
17 | TransducerFrequencyXDR = "F" | |
18 | // TransducerHumidityXDR is transducer type for Humidity | |
19 | TransducerHumidityXDR = "H" | |
20 | // TransducerForceXDR is transducer type for Force | |
21 | TransducerForceXDR = "N" | |
22 | // TransducerPressureXDR is transducer type for Pressure | |
23 | TransducerPressureXDR = "P" | |
24 | // TransducerFlowXDR is transducer type for Flow | |
25 | TransducerFlowXDR = "R" | |
26 | // TransducerAbsoluteHumidityXDR is transducer type for Absolute humidity | |
27 | TransducerAbsoluteHumidityXDR = "B" | |
28 | // TransducerGenericXDR is transducer type for Generic | |
29 | TransducerGenericXDR = "G" | |
30 | // TransducerCurrentXDR is transducer type for Current | |
31 | TransducerCurrentXDR = "I" | |
32 | // TransducerSalinityXDR is transducer type for Salinity | |
33 | TransducerSalinityXDR = "L" | |
34 | // TransducerSwitchValveXDR is transducer type for Switch, valve | |
35 | TransducerSwitchValveXDR = "S" | |
36 | // TransducerTachometerXDR is transducer type for Tachometer | |
37 | TransducerTachometerXDR = "T" | |
38 | // TransducerVoltageXDR is transducer type for Voltage | |
39 | TransducerVoltageXDR = "U" | |
40 | // TransducerVolumeXDR is transducer type for Volume | |
41 | TransducerVolumeXDR = "V" | |
42 | ) | |
43 | ||
44 | // XDR - Transducer Measurement | |
45 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_xdr_transducer_measurement | |
46 | // https://www.eye4software.com/hydromagic/documentation/articles-and-howtos/handling-nmea0183-xdr/ | |
47 | // | |
48 | // Format: $--XDR,a,x.x,a,c--c, ..... *hh<CR><LF> | |
49 | // Example: $HCXDR,A,171,D,PITCH,A,-37,D,ROLL,G,367,,MAGX,G,2420,,MAGY,G,-8984,,MAGZ*41 | |
50 | // $SDXDR,C,23.15,C,WTHI*70 | |
51 | type XDR struct { | |
52 | BaseSentence | |
53 | Measurements []XDRMeasurement | |
54 | } | |
55 | ||
56 | // XDRMeasurement is measurement recorded by transducer | |
57 | type XDRMeasurement struct { | |
58 | // TransducerType is type of transducer | |
59 | // * A - Angular displacement | |
60 | // * C - Temperature | |
61 | // * D - Depth | |
62 | // * F - Frequency | |
63 | // * H - Humidity | |
64 | // * N - Force | |
65 | // * P - Pressure | |
66 | // * R - Flow | |
67 | // * B - Absolute humidity | |
68 | // * G - Generic | |
69 | // * I - Current | |
70 | // * L - Salinity | |
71 | // * S - Switch, valve | |
72 | // * T - Tachometer | |
73 | // * U - Voltage | |
74 | // * V - Volume | |
75 | // could be more | |
76 | TransducerType string | |
77 | ||
78 | // Value of measurement | |
79 | Value float64 | |
80 | ||
81 | // Unit of measurement | |
82 | // * "" - could be empty! | |
83 | // * A - Amperes | |
84 | // * B - Bars | Binary | |
85 | // * C - Celsius | |
86 | // * D - Degrees | |
87 | // * H - Hertz | |
88 | // * I - liters/second | |
89 | // * K - Kelvin | Density, kg/m3 kilogram per cubic metre | |
90 | // * M - Meters | Cubic Meters (m3) | |
91 | // * N - Newton | |
92 | // * P - percent of full range | Pascal | |
93 | // * R - RPM | |
94 | // * S - Parts per thousand | |
95 | // * V - Volts | |
96 | // could be more | |
97 | Unit string | |
98 | ||
99 | // TransducerName is name of transducer where measurement was recorded | |
100 | TransducerName string | |
101 | } | |
102 | ||
103 | // newXDR constructor | |
104 | func newXDR(s BaseSentence) (XDR, error) { | |
105 | p := NewParser(s) | |
106 | p.AssertType(TypeXDR) | |
107 | ||
108 | xdr := XDR{ | |
109 | BaseSentence: s, | |
110 | Measurements: nil, | |
111 | } | |
112 | ||
113 | if len(p.Fields)%4 != 0 { | |
114 | return xdr, errors.New("XDR field count is not exactly dividable by 4") | |
115 | } | |
116 | ||
117 | xdr.Measurements = make([]XDRMeasurement, 0, len(s.Fields)/4) | |
118 | for i := 0; i < len(s.Fields); { | |
119 | tmp := XDRMeasurement{ | |
120 | TransducerType: p.EnumString( | |
121 | i, | |
122 | "transducer type", | |
123 | TransducerAngularDisplacementXDR, | |
124 | TransducerTemperatureXDR, | |
125 | TransducerDepthXDR, | |
126 | TransducerFrequencyXDR, | |
127 | TransducerHumidityXDR, | |
128 | TransducerForceXDR, | |
129 | TransducerPressureXDR, | |
130 | TransducerFlowXDR, | |
131 | TransducerAbsoluteHumidityXDR, | |
132 | TransducerGenericXDR, | |
133 | TransducerCurrentXDR, | |
134 | TransducerSalinityXDR, | |
135 | TransducerSwitchValveXDR, | |
136 | TransducerTachometerXDR, | |
137 | TransducerVoltageXDR, | |
138 | TransducerVolumeXDR, | |
139 | ), | |
140 | Value: p.Float64(i+1, "measurement value"), | |
141 | Unit: p.EnumString( | |
142 | i+2, | |
143 | "measurement unit", | |
144 | UnitAmpere, | |
145 | UnitBars, | |
146 | UnitBinary, | |
147 | UnitCelsius, | |
148 | UnitDegrees, | |
149 | UnitHertz, | |
150 | UnitLitresPerSecond, | |
151 | UnitKelvin, | |
152 | UnitKilogramPerCubicMetre, | |
153 | UnitMeters, | |
154 | UnitCubicMeters, | |
155 | UnitRevolutionsPerMinute, | |
156 | UnitPercent, | |
157 | UnitPascal, | |
158 | UnitPartsPerThousand, | |
159 | UnitVolts, | |
160 | ), | |
161 | TransducerName: p.String(i+3, "transducer name"), | |
162 | } | |
163 | xdr.Measurements = append(xdr.Measurements, tmp) | |
164 | i += 4 | |
165 | } | |
166 | return xdr, p.Err() | |
167 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestXDR(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg XDR | |
13 | }{ | |
14 | { | |
15 | name: "good sentence with 1 measurement", | |
16 | raw: "$SDXDR,C,23.15,C,WTHI*70", | |
17 | msg: XDR{ | |
18 | Measurements: []XDRMeasurement{ | |
19 | { | |
20 | TransducerType: "C", | |
21 | Value: 23.15, | |
22 | Unit: "C", | |
23 | TransducerName: "WTHI", | |
24 | }, | |
25 | }, | |
26 | }, | |
27 | }, | |
28 | { | |
29 | name: "good sentence with 5 measurements", | |
30 | raw: "$HCXDR,A,171,D,PITCH,A,-37,D,ROLL,G,367,,MAGX,G,2420,,MAGY,G,-8984,,MAGZ*41", | |
31 | msg: XDR{ | |
32 | Measurements: []XDRMeasurement{ | |
33 | {TransducerType: "A", Value: 171, Unit: "D", TransducerName: "PITCH"}, | |
34 | {TransducerType: "A", Value: -37, Unit: "D", TransducerName: "ROLL"}, | |
35 | {TransducerType: "G", Value: 367, Unit: "", TransducerName: "MAGX"}, | |
36 | {TransducerType: "G", Value: 2420, Unit: "", TransducerName: "MAGY"}, | |
37 | {TransducerType: "G", Value: -8984, Unit: "", TransducerName: "MAGZ"}, | |
38 | }, | |
39 | }, | |
40 | }, | |
41 | { | |
42 | name: "invalid nmea: odd number of fields", | |
43 | raw: "$HCXDR,A,171,D,PITCH,A,-37,D,ROLL,G,367,,MAGX,G,2420,MAGY,G,-8984,,MAGZ*6d", | |
44 | err: "XDR field count is not exactly dividable by 4", | |
45 | }, | |
46 | { | |
47 | name: "invalid nmea: TransducerType", | |
48 | raw: "$SDXDR,x,23.15,C,WTHI*4b", | |
49 | err: "nmea: SDXDR invalid transducer type: x", | |
50 | }, | |
51 | { | |
52 | name: "invalid nmea: Value", | |
53 | raw: "$SDXDR,C,23.x,C,WTHI*0C", | |
54 | err: "nmea: SDXDR invalid measurement value: 23.x", | |
55 | }, | |
56 | { | |
57 | name: "invalid nmea: Unit", | |
58 | raw: "$SDXDR,C,23.15,x,WTHI*4b", | |
59 | err: "nmea: SDXDR invalid measurement unit: x", | |
60 | }, | |
61 | } | |
62 | for _, tt := range tests { | |
63 | t.Run(tt.name, func(t *testing.T) { | |
64 | m, err := Parse(tt.raw) | |
65 | if tt.err != "" { | |
66 | assert.Error(t, err) | |
67 | assert.EqualError(t, err, tt.err) | |
68 | } else { | |
69 | assert.NoError(t, err) | |
70 | xdr := m.(XDR) | |
71 | xdr.BaseSentence = BaseSentence{} | |
72 | assert.Equal(t, tt.msg, xdr) | |
73 | } | |
74 | }) | |
75 | } | |
76 | } |
0 | package nmea | |
1 | ||
2 | const ( | |
3 | // TypeXTE type of XTE sentence for Cross-track error, measured | |
4 | TypeXTE = "XTE" | |
5 | ) | |
6 | ||
7 | // XTE - Cross-track error, measured | |
8 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_xte_cross_track_error_measured | |
9 | // | |
10 | // Format: $--XTE,A,A,x.x,a,N*hh<CR><LF> | |
11 | // Format (NMEA 2.3): $--XTE,A,A,x.x,a,N,m*hh<CR><LF> | |
12 | // Example: $GPXTE,V,V,,,N,S*43 | |
13 | type XTE struct { | |
14 | BaseSentence | |
15 | ||
16 | // StatusGeneralWarning is used for warnings | |
17 | // * V = LORAN-C Blink or SNR warning | |
18 | // * A = general warning flag or other navigation systems when a reliable fix is not available | |
19 | StatusGeneralWarning string | |
20 | ||
21 | // StatusLockWarning is used for lock warning | |
22 | // * V = Loran-C Cycle Lock warning flag | |
23 | // * A = OK or not used | |
24 | StatusLockWarning string | |
25 | ||
26 | // CrossTrackErrorMagnitude is Cross Track Error Magnitude | |
27 | CrossTrackErrorMagnitude float64 | |
28 | ||
29 | // DirectionToSteer is Direction to steer, | |
30 | // * L = left | |
31 | // * R = right | |
32 | DirectionToSteer string | |
33 | ||
34 | // CrossTrackUnits is cross track units | |
35 | // * N = nautical miles | |
36 | // * K = for kilometers | |
37 | CrossTrackUnits string | |
38 | ||
39 | // FAA mode indicator (filled in NMEA 2.3 and later) | |
40 | FFAMode string | |
41 | } | |
42 | ||
43 | // newXTE constructor | |
44 | func newXTE(s BaseSentence) (XTE, error) { | |
45 | p := NewParser(s) | |
46 | p.AssertType(TypeXTE) | |
47 | xte := XTE{ | |
48 | BaseSentence: s, | |
49 | StatusGeneralWarning: p.EnumString(0, "general warning", StatusWarningAClearORNotUsedAPB, StatusWarningASetAPB), | |
50 | StatusLockWarning: p.EnumString(1, "lock warning", StatusWarningBSetAPB, StatusWarningBClearAPB), | |
51 | CrossTrackErrorMagnitude: p.Float64(2, "cross track error magnitude"), | |
52 | DirectionToSteer: p.EnumString(3, "direction to steer", Left, Right), | |
53 | CrossTrackUnits: p.EnumString(4, "cross track units", DistanceUnitKilometre, DistanceUnitNauticalMile, DistanceUnitStatuteMile, DistanceUnitMetre), | |
54 | } | |
55 | if len(p.Fields) > 5 { | |
56 | xte.FFAMode = p.String(5, "FAA mode") // not enum because some devices have proprietary "non-nmea" values | |
57 | } | |
58 | return xte, p.Err() | |
59 | } |
0 | package nmea | |
1 | ||
2 | import ( | |
3 | "github.com/stretchr/testify/assert" | |
4 | "testing" | |
5 | ) | |
6 | ||
7 | func TestXTE(t *testing.T) { | |
8 | var tests = []struct { | |
9 | name string | |
10 | raw string | |
11 | err string | |
12 | msg XTE | |
13 | }{ | |
14 | { | |
15 | name: "good sentence", | |
16 | raw: "$GPXTE,V,V,10.1,L,N*6E", | |
17 | msg: XTE{ | |
18 | StatusGeneralWarning: "V", | |
19 | StatusLockWarning: "V", | |
20 | CrossTrackErrorMagnitude: 10.1, | |
21 | DirectionToSteer: "L", | |
22 | CrossTrackUnits: "N", | |
23 | FFAMode: "", | |
24 | }, | |
25 | }, | |
26 | { | |
27 | name: "good sentence with FAAMode", | |
28 | raw: "$GPXTE,V,V,,,N,S*43", | |
29 | msg: XTE{ | |
30 | StatusGeneralWarning: "V", | |
31 | StatusLockWarning: "V", | |
32 | CrossTrackErrorMagnitude: 0, | |
33 | DirectionToSteer: "", | |
34 | CrossTrackUnits: "N", | |
35 | FFAMode: "S", | |
36 | }, | |
37 | }, | |
38 | { | |
39 | name: "invalid nmea: StatusGeneralWarning", | |
40 | raw: "$GPXTE,x,V,,,N,S*6d", | |
41 | err: "nmea: GPXTE invalid general warning: x", | |
42 | }, | |
43 | { | |
44 | name: "invalid nmea: StatusLockWarning", | |
45 | raw: "$GPXTE,V,x,,,N,S*6d", | |
46 | err: "nmea: GPXTE invalid lock warning: x", | |
47 | }, | |
48 | { | |
49 | name: "invalid nmea: DirectionToSteer", | |
50 | raw: "$GPXTE,V,V,,x,N,S*3b", | |
51 | err: "nmea: GPXTE invalid direction to steer: x", | |
52 | }, | |
53 | { | |
54 | name: "invalid nmea: CrossTrackUnits", | |
55 | raw: "$GPXTE,V,V,,,x,S*75", | |
56 | err: "nmea: GPXTE invalid cross track units: x", | |
57 | }, | |
58 | } | |
59 | for _, tt := range tests { | |
60 | t.Run(tt.name, func(t *testing.T) { | |
61 | m, err := Parse(tt.raw) | |
62 | if tt.err != "" { | |
63 | assert.Error(t, err) | |
64 | assert.EqualError(t, err, tt.err) | |
65 | } else { | |
66 | assert.NoError(t, err) | |
67 | xte := m.(XTE) | |
68 | xte.BaseSentence = BaseSentence{} | |
69 | assert.Equal(t, tt.msg, xte) | |
70 | } | |
71 | }) | |
72 | } | |
73 | } |
6 | 6 | |
7 | 7 | // ZDA represents date & time data. |
8 | 8 | // http://aprs.gids.nl/nmea/#zda |
9 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_zda_time_date_utc_day_month_year_and_local_time_zone | |
10 | // | |
11 | // Format: $--ZDA,hhmmss.ss,xx,xx,xxxx,xx,xx*hh<CR><LF> | |
12 | // Example: $GPZDA,172809.456,12,07,1996,00,00*57 | |
9 | 13 | type ZDA struct { |
10 | 14 | BaseSentence |
11 | 15 | Time Time |