Codebase list golang-github-adrianmo-go-nmea / ab90b32
New upstream version 1.7.0 Francisco Vilmar Cardoso Ruviaro 2 years ago
118 changed file(s) with 6027 addition(s) and 154 deletion(s). Raw diff Collapse all Expand all
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"
2828
2929 At this moment, this library supports the following sentence types:
3030
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.
57103
58104 ## Examples
59105
146192
147193 ### Custom message parsing
148194
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.
151197
152198 ```go
153199 package main
192238 panic(err)
193239 }
194240
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 }
205251 }
206252 ```
207253
219265
220266 ## Contributing
221267
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.
223270
224271 ## License
225272
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 }
00 package nmea
11
22 const (
3 // TypeDBS type for DBS sentences
3 // TypeDBS is type of DBS sentence for Depth Below Surface
44 TypeDBS = "DBS"
55 )
66
7 // DBS - Depth Below Surface
7 // DBS - Depth Below Surface (obsolete, use DPT instead)
88 // 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
913 type DBS struct {
1014 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
1421 }
1522
1623 // newDBS constructor
1825 p := NewParser(s)
1926 p.AssertType(TypeDBS)
2027 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),
2535 }, p.Err()
2636 }
55 "github.com/stretchr/testify/assert"
66 )
77
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 },
2126 },
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 }
2945
30 func TestDBS(t *testing.T) {
3146 for _, tt := range dbstests {
3247 t.Run(tt.name, func(t *testing.T) {
3348 m, err := Parse(tt.raw)
66
77 // DBT - Depth below transducer
88 // 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
912 type DBT struct {
1013 BaseSentence
1114 DepthFeet float64
1515 name: "good sentence",
1616 raw: "$IIDBT,032.93,f,010.04,M,005.42,F*2C",
1717 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,
2121 },
2222 },
2323 {
246246 msg: GNRMC{
247247 Time: Time{true, 22, 05, 16, 0},
248248 Validity: "A",
249 Latitude: MustParseGPS("5133.82 N"),
250 Longitude: MustParseGPS("00042.24 W"),
249251 Speed: 173.8,
250252 Course: 231.8,
251253 Date: Date{true, 13, 06, 94},
252254 Variation: -4.2,
253 Latitude: MustParseGPS("5133.82 N"),
254 Longitude: MustParseGPS("00042.24 W"),
255 FFAMode: "",
256 NavStatus: "",
255257 },
256258 },
257259 {
260262 msg: GNRMC{
261263 Time: Time{true, 14, 27, 54, 0},
262264 Validity: "A",
265 Latitude: MustParseGPS("4302.539570 N"),
266 Longitude: MustParseGPS("07920.379823 W"),
263267 Speed: 0,
264268 Course: 0,
265269 Date: Date{true, 7, 6, 17},
266270 Variation: 0,
267 Latitude: MustParseGPS("4302.539570 N"),
268 Longitude: MustParseGPS("07920.379823 W"),
271 FFAMode: FAAModeAutonomous,
272 NavStatus: "",
269273 },
270274 },
271275 {
274278 msg: GNRMC{
275279 Time: Time{true, 10, 5, 38, 0},
276280 Validity: "A",
281 Latitude: MustParseGPS("5546.27711 N"),
282 Longitude: MustParseGPS("03736.91144 E"),
277283 Speed: 0.061,
278284 Course: 0,
279285 Date: Date{true, 26, 3, 18},
280286 Variation: 0,
281 Latitude: MustParseGPS("5546.27711 N"),
282 Longitude: MustParseGPS("03736.91144 E"),
287 FFAMode: FAAModeAutonomous,
288 NavStatus: "",
283289 },
284290 },
285291 {
382388 Millisecond: 0,
383389 },
384390 Validity: "A",
391 FFAMode: FAAModeAutonomous,
385392 },
386393 },
387394 {
599606 msg: GPRMC{
600607 Time: Time{true, 22, 5, 16, 0},
601608 Validity: "A",
609 Latitude: MustParseGPS("5133.82 N"),
610 Longitude: MustParseGPS("00042.24 W"),
602611 Speed: 173.8,
603612 Course: 231.8,
604613 Date: Date{true, 13, 6, 94},
605614 Variation: -4.2,
606 Latitude: MustParseGPS("5133.82 N"),
607 Longitude: MustParseGPS("00042.24 W"),
615 FFAMode: "",
616 NavStatus: "",
608617 },
609618 },
610619 {
613622 msg: GPRMC{
614623 Time: Time{true, 14, 27, 54, 0},
615624 Validity: "A",
625 Latitude: MustParseGPS("4302.539570 N"),
626 Longitude: MustParseGPS("07920.379823 W"),
616627 Speed: 0,
617628 Course: 0,
618629 Date: Date{true, 7, 6, 17},
619630 Variation: 0,
620 Latitude: MustParseGPS("4302.539570 N"),
621 Longitude: MustParseGPS("07920.379823 W"),
631 FFAMode: FAAModeAutonomous,
632 NavStatus: "",
622633 },
623634 },
624635 {
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 }
66
77 // DPT - Depth of Water
88 // 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
913 type DPT struct {
1014 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)
1418 }
1519
1620 // newDPT constructor
1721 func newDPT(s BaseSentence) (DPT, error) {
1822 p := NewParser(s)
1923 p.AssertType(TypeDPT)
20 return DPT{
24 dpt := DPT{
2125 BaseSentence: s,
2226 Depth: p.Float64(0, "depth"),
2327 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()
2633 }
1515 name: "good sentence",
1616 raw: "$SDDPT,0.5,0.5,*7B",
1717 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,
2121 },
2222 },
2323 {
2424 name: "good sentence with scale",
2525 raw: "$SDDPT,0.5,0.5,0.1*54",
2626 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,
3039 },
3140 },
3241 {
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 }
1919 )
2020
2121 // 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
2227 type GGA struct {
2328 BaseSentence
2429 Time Time // Time of fix.
1010
1111 // GLL is Geographic Position, Latitude / Longitude and time.
1212 // 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
1319 type GLL struct {
1420 BaseSentence
1521 Latitude float64 // Latitude
1622 Longitude float64 // Longitude
1723 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)
1926 }
2027
2128 // newGLL constructor
2229 func newGLL(s BaseSentence) (GLL, error) {
2330 p := NewParser(s)
2431 p.AssertType(TypeGLL)
25 return GLL{
32 gll := GLL{
2633 BaseSentence: s,
2734 Latitude: p.LatLong(0, 1, "latitude"),
2835 Longitude: p.LatLong(2, 3, "longitude"),
2936 Time: p.Time(4, "time"),
3037 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()
3243 }
2525 Millisecond: 0,
2626 },
2727 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: "",
2846 },
2947 },
3048 {
22 const (
33 // TypeGNS type for GNS sentences
44 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 (
510 // NoFixGNS Character
611 NoFixGNS = "N"
712 // AutonomousGNS Character
2328 )
2429
2530 // 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
2636 type GNS struct {
2737 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)
3150 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.
3858 }
3959
4060 // newGNS Constructor
5474 Age: p.Float64(10, "age"),
5575 Station: p.Int64(11, "station"),
5676 }
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 }
5790 return m, p.Err()
5891 }
2525 Separation: 11.24,
2626 Age: 0,
2727 Station: 0,
28 NavStatus: "",
2829 },
2930 },
3031 {
4142 Separation: 48.0,
4243 Age: 0,
4344 Station: 0,
45 NavStatus: "",
4446 },
4547 },
4648 {
47 name: "good sentence B",
49 name: "good sentence C",
4850 raw: "$GNGNS,094821.0,4849.931307,N,00216.053323,E,AAN,14,0.6,161.5,48.0,,*23",
4951 msg: GNS{
5052 Time: Time{true, 9, 48, 21, 0},
5759 Separation: 48.0,
5860 Age: 0,
5961 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",
6080 },
6181 },
6282 {
1616
1717 // GSA represents overview satellite data.
1818 // 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
1925 type GSA struct {
2026 BaseSentence
2127 Mode string // The selection mode.
2430 PDOP float64 // Dilution of precision.
2531 HDOP float64 // Horizontal dilution of precision.
2632 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
2741 }
2842
2943 // newGSA parses the GSA sentence into this struct.
4559 m.PDOP = p.Float64(14, "pdop")
4660 m.HDOP = p.Float64(15, "hdop")
4761 m.VDOP = p.Float64(16, "vdop")
62
63 if len(p.Fields) > 17 {
64 m.SystemID = p.Int64(17, "system ID")
65 }
4866 return m, p.Err()
4967 }
2121 PDOP: 3.1,
2222 HDOP: 2,
2323 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,
2437 },
2538 },
2639 {
00 package nmea
11
22 const (
3 // TypeGSV type for GSV sentences
3 // TypeGSV type of GSV sentences for satellites in view
44 TypeGSV = "GSV"
55 )
66
77 // GSV represents the GPS Satellites in view
88 // 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
915 type GSV struct {
1016 BaseSentence
1117 TotalMessages int64 // Total number of messages of this type in this cycle
1218 MessageNumber int64 // Message number
1319 NumberSVsInView int64 // Total number of SVs in view
1420 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
1529 }
1630
1731 // GSVInfo represents information about a visible satellite
3246 MessageNumber: p.Int64(1, "message number"),
3347 NumberSVsInView: p.Int64(2, "number of SVs in view"),
3448 }
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) {
3752 break
3853 }
3954 m.Info = append(m.Info, GSVInfo{
4358 SNR: p.Int64(6+i*4, "SNR"),
4459 })
4560 }
61 idxSID := (6 + (i-1)*4) + 1
62 if len(p.Fields) == idxSID+1 {
63 m.SystemID = p.Int64(idxSID, "system ID")
64 }
4665 return m, p.Err()
4766 }
3838 {SVPRNNumber: 4, Elevation: 15, Azimuth: 270, SNR: 0},
3939 {SVPRNNumber: 6, Elevation: 1, Azimuth: 10, SNR: 12},
4040 },
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,
4167 },
4268 },
4369 {
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 }
66
77 // HDT is the Actual vessel heading in degrees True.
88 // 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
913 type HDT struct {
1014 BaseSentence
1115 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 }
5353
5454 // MDA is the Meteorological Composite
5555 // 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
5661 type MDA struct {
5762 BaseSentence
5863 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 }
44 TypeMTK = "PMTK"
55 )
66
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>
816 type MTK struct {
917 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.
1126 Flag int64
1227 }
1328
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 }
2828 return l
2929 }
3030
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
4031 // MustParseTime parses wall clock and panics on failure
4132 func MustParseTime(s string) Time {
4233 t, err := ParseTime(s)
3232 )
3333
3434 // 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
3540 type MWD struct {
3641 BaseSentence
3742 WindDirectionTrue float64
4747 )
4848
4949 // 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
5054 type MWV struct {
5155 BaseSentence
5256 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 }
88
99 // PGRME is Estimated Position Error (Garmin proprietary sentence)
1010 // 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
1115 type PGRME struct {
1216 BaseSentence
1317 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 }
1010
1111 // RMC is the Recommended Minimum Specific GNSS data.
1212 // 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
1321 type RMC struct {
1422 BaseSentence
1523 Time Time // Time Stamp
2028 Course float64 // True course
2129 Date Date // Date
2230 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)
2333 }
2434
2535 // newRMC constructor
4050 if p.EnumString(10, "direction", West, East) == West {
4151 m.Variation = 0 - m.Variation
4252 }
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 }
4369 return m, p.Err()
4470 }
1717 msg: RMC{
1818 Time: Time{true, 22, 05, 16, 0},
1919 Validity: "A",
20 Latitude: MustParseGPS("5133.82 N"),
21 Longitude: MustParseGPS("00042.24 W"),
2022 Speed: 173.8,
2123 Course: 231.8,
2224 Date: Date{true, 13, 06, 94},
2325 Variation: -4.2,
24 Latitude: MustParseGPS("5133.82 N"),
25 Longitude: MustParseGPS("00042.24 W"),
26 FFAMode: "",
27 NavStatus: "",
2628 },
2729 },
2830 {
3133 msg: RMC{
3234 Time: Time{true, 14, 27, 54, 0},
3335 Validity: "A",
36 Latitude: MustParseGPS("4302.539570 N"),
37 Longitude: MustParseGPS("07920.379823 W"),
3438 Speed: 0,
3539 Course: 0,
3640 Date: Date{true, 7, 6, 17},
3741 Variation: 0,
38 Latitude: MustParseGPS("4302.539570 N"),
39 Longitude: MustParseGPS("07920.379823 W"),
42 FFAMode: FAAModeAutonomous,
43 NavStatus: "",
4044 },
4145 },
4246 {
4549 msg: RMC{
4650 Time: Time{true, 10, 5, 38, 0},
4751 Validity: "A",
52 Latitude: MustParseGPS("5546.27711 N"),
53 Longitude: MustParseGPS("03736.91144 E"),
4854 Speed: 0.061,
4955 Course: 0,
5056 Date: Date{true, 26, 3, 18},
5157 Variation: 0,
52 Latitude: MustParseGPS("5546.27711 N"),
53 Longitude: MustParseGPS("03736.91144 E"),
58 FFAMode: FAAModeAutonomous,
59 NavStatus: "",
5460 },
5561 },
5662 {
5965 err: "nmea: GNRMC invalid validity: D",
6066 },
6167 {
62 name: "good sentence A",
68 name: "good sentence D",
6369 raw: "$GPRMC,220516,A,5133.82,N,00042.24,W,173.8,231.8,130694,004.2,W*70",
6470 msg: RMC{
6571 Time: Time{true, 22, 5, 16, 0},
6672 Validity: "A",
73 Latitude: MustParseGPS("5133.82 N"),
74 Longitude: MustParseGPS("00042.24 W"),
6775 Speed: 173.8,
6876 Course: 231.8,
6977 Date: Date{true, 13, 6, 94},
7078 Variation: -4.2,
71 Latitude: MustParseGPS("5133.82 N"),
72 Longitude: MustParseGPS("00042.24 W"),
79 FFAMode: "",
80 NavStatus: "",
7381 },
7482 },
7583 {
76 name: "good sentence B",
84 name: "good sentence E",
7785 raw: "$GPRMC,142754.0,A,4302.539570,N,07920.379823,W,0.0,,070617,0.0,E,A*3F",
7886 msg: RMC{
7987 Time: Time{true, 14, 27, 54, 0},
8088 Validity: "A",
89 Latitude: MustParseGPS("4302.539570 N"),
90 Longitude: MustParseGPS("07920.379823 W"),
8191 Speed: 0,
8292 Course: 0,
8393 Date: Date{true, 7, 6, 17},
8494 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,
87113 },
88114 },
89115 {
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 }
1111 )
1212
1313 // 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
1419 type RTE struct {
1520 BaseSentence
1621 NumberOfSentences int64 // Number of sentences in sequence
2626
2727 // ParserFunc callback used to parse specific sentence variants
2828 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 }
2939
3040 // Sentence interface for all NMEA sentence
3141 type Sentence interface {
178188 switch s.Type {
179189 case TypeRMC:
180190 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)
181219 case TypeGGA:
182220 return newGGA(s)
183221 case TypeGSA:
190228 return newZDA(s)
191229 case TypePGRME:
192230 return newPGRME(s)
231 case TypePHTRO:
232 return newPHTRO(s)
233 case TypePRDID:
234 return newPRDID(s)
235 case TypePSONCMS:
236 return newPSONCMS(s)
193237 case TypeGSV:
194238 return newGSV(s)
239 case TypeHDG:
240 return newHDG(s)
195241 case TypeHDT:
196242 return newHDT(s)
243 case TypeHDM:
244 return newHDM(s)
245 case TypeHSC:
246 return newHSC(s)
197247 case TypeGNS:
198248 return newGNS(s)
199249 case TypeTHS:
200250 return newTHS(s)
251 case TypeTLL:
252 return newTLL(s)
253 case TypeTTM:
254 return newTTM(s)
255 case TypeTXT:
256 return newTXT(s)
201257 case TypeWPL:
202258 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)
203267 case TypeRTE:
204268 return newRTE(s)
269 case TypeROT:
270 return newROT(s)
271 case TypeVBW:
272 return newVBW(s)
273 case TypeVDR:
274 return newVDR(s)
205275 case TypeVHW:
206276 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)
207285 case TypeDPT:
208286 return newDPT(s)
209287 case TypeDBT:
210288 return newDBT(s)
289 case TypeDBK:
290 return newDBK(s)
211291 case TypeDBS:
212292 return newDBS(s)
213293 case TypeMDA:
214294 return newMDA(s)
295 case TypeMTA:
296 return newMTA(s)
297 case TypeMTW:
298 return newMTW(s)
215299 case TypeMWD:
216300 return newMWD(s)
217301 case TypeMWV:
218302 return newMWV(s)
303 case TypeOSD:
304 return newOSD(s)
305 case TypeXDR:
306 return newXDR(s)
307 case TypeXTE:
308 return newXTE(s)
219309 }
220310 }
221311 if strings.HasPrefix(s.Raw, SentenceStartEncapsulated) {
224314 return newVDMVDO(s)
225315 }
226316 }
227 return nil, fmt.Errorf("nmea: sentence prefix '%s' not supported", s.Prefix())
228 }
317 return nil, &NotSupportedError{Prefix: s.Prefix()}
318 }
00 package nmea
11
22 import (
3 "errors"
34 "testing"
45
56 "github.com/stretchr/testify/assert"
5758 },
5859 },
5960 {
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",
6263 datatype: "VDM",
6364 talkerid: "AI",
64 prefix: "AIVDM",
65 prefix: "AIVDM",
6566 sent: BaseSentence{
6667 Talker: "AI",
6768 Type: "VDM",
107108 {
108109 name: "missing TAG Block start delimiter",
109110 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 '!'",
111112 },
112113 {
113114 name: "missing TAG Block end delimiter",
114115 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 '!'",
116117 },
117118 {
118119 name: "invalid TAG Block contents",
119120 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",
121122 },
122123 }
123124
190191 var parsetests = []struct {
191192 name string
192193 raw string
193 err string
194 err error
194195 msg interface{}
195196 }{
196197 {
197198 name: "bad sentence",
198199 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 '!'"),
200201 },
201202 {
202203 name: "bad sentence type",
203204 raw: "$INVALID,123,123,*7D",
204 err: "nmea: sentence prefix 'INVALID' not supported",
205 err: &NotSupportedError{Prefix: "INVALID"},
205206 },
206207 {
207208 name: "bad encapsulated sentence type",
208209 raw: "!INVALID,1,2,*7E",
209 err: "nmea: sentence prefix 'INVALID' not supported",
210 err: &NotSupportedError{Prefix: "INVALID"},
210211 },
211212 }
212213
214215 for _, tt := range parsetests {
215216 t.Run(tt.name, func(t *testing.T) {
216217 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)
219220 } else {
220221 assert.NoError(t, err)
221222 assert.Equal(t, tt.msg, m)
1616
1717 // THS is the Actual vessel heading in degrees True with status.
1818 // 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
1923 type THS struct {
2024 BaseSentence
2125 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 }
99 "strconv"
1010 "strings"
1111 "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"
12156 )
13157
14158 const (
28172 East = "E"
29173 // West value
30174 West = "W"
175 // Left value
176 Left = "L"
177 // Right value
178 Right = "R"
31179 )
32180
33181 // ParseLatLong parses the supplied string into the LatLong.
35183 // Supported formats are:
36184 // - DMS (e.g. 33° 23' 22")
37185 // - Decimal (e.g. 33.23454)
38 // - GPS (e.g 15113.4322S)
186 // - GPS (e.g 15113.4322 S)
39187 //
40188 func ParseLatLong(s string) (float64, error) {
41189 var l float64
52200 }
53201
54202 // ParseGPS parses a GPS/NMEA coordinate.
55 // e.g 15113.4322S
203 // e.g `15113.4322 S`
56204 func ParseGPS(s string) (float64, error) {
57205 parts := strings.Split(s, " ")
58206 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 }
77 TypeVDO = "VDO"
88 )
99
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
1314 type VDMVDO struct {
1415 BaseSentence
1516 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 }
55 )
66
77 // 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
812 type VHW struct {
913 BaseSentence
1014 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 }
66
77 // VTG represents track & speed data.
88 // 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
915 type VTG struct {
1016 BaseSentence
1117 TrueTrack float64
1218 MagneticTrack float64
1319 GroundSpeedKnots float64
1420 GroundSpeedKPH float64
21 FFAMode string // FAA mode indicator (filled in NMEA 2.3 and later)
1522 }
1623
1724 // newVTG parses the VTG sentence into this struct.
1926 func newVTG(s BaseSentence) (VTG, error) {
2027 p := NewParser(s)
2128 p.AssertType(TypeVTG)
22 return VTG{
29 vtg := VTG{
2330 BaseSentence: s,
2431 TrueTrack: p.Float64(0, "true track"),
2532 MagneticTrack: p.Float64(2, "magnetic track"),
2633 GroundSpeedKnots: p.Float64(4, "ground speed (knots)"),
2734 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()
2940 }
1919 MagneticTrack: 67.5,
2020 GroundSpeedKnots: 30.45,
2121 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",
2234 },
2335 },
2436 {
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 }
55 )
66
77 // 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
813 type WPL struct {
914 BaseSentence
1015 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 }
66
77 // ZDA represents date & time data.
88 // 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
913 type ZDA struct {
1014 BaseSentence
1115 Time Time