From 97b08a2825e46c165ba563c031744643f43dd26a Mon Sep 17 00:00:00 2001 From: toimtoimtoim Date: Sat, 1 Jan 2022 18:11:34 +0200 Subject: [PATCH 1/3] a lot of new sentences --- README.md | 85 ++++++++++++++++---------- aam.go | 49 +++++++++++++++ aam_test.go | 56 ++++++++++++++++++ ala.go | 68 +++++++++++++++++++++ ala_test.go | 65 ++++++++++++++++++++ apb.go | 131 ++++++++++++++++++++++++++++++++++++++++ apb_test.go | 92 ++++++++++++++++++++++++++++ bec.go | 45 ++++++++++++++ bec_test.go | 72 ++++++++++++++++++++++ bod.go | 45 ++++++++++++++ bod_test.go | 64 ++++++++++++++++++++ bwc.go | 52 ++++++++++++++++ bwc_test.go | 113 +++++++++++++++++++++++++++++++++++ bwr.go | 51 ++++++++++++++++ bwr_test.go | 113 +++++++++++++++++++++++++++++++++++ bww.go | 40 +++++++++++++ bww_test.go | 52 ++++++++++++++++ dbk.go | 37 ++++++++++++ dbk_test.go | 57 ++++++++++++++++++ dbs.go | 25 +++++--- dbs_test.go | 64 +++++++++++--------- dor.go | 97 ++++++++++++++++++++++++++++++ dor_test.go | 71 ++++++++++++++++++++++ dsc.go | 134 +++++++++++++++++++++++++++++++++++++++++ dsc_test.go | 86 +++++++++++++++++++++++++++ dse.go | 78 ++++++++++++++++++++++++ dse_test.go | 72 ++++++++++++++++++++++ eve.go | 32 ++++++++++ eve_test.go | 50 ++++++++++++++++ fir.go | 102 +++++++++++++++++++++++++++++++ fir_test.go | 71 ++++++++++++++++++++++ hsc.go | 33 +++++++++++ hsc_test.go | 60 +++++++++++++++++++ mta.go | 28 +++++++++ mta_test.go | 48 +++++++++++++++ rmb.go | 90 ++++++++++++++++++++++++++++ rmb_test.go | 111 ++++++++++++++++++++++++++++++++++ rpm.go | 39 ++++++++++++ rpm_test.go | 51 ++++++++++++++++ rsa.go | 32 ++++++++++ rsa_test.go | 60 +++++++++++++++++++ sentence.go | 52 ++++++++++++++++ types.go | 104 ++++++++++++++++++++++++++++++++ vdr.go | 38 ++++++++++++ vdr_test.go | 57 ++++++++++++++++++ vlw.go | 46 ++++++++++++++ vlw_test.go | 78 ++++++++++++++++++++++++ vpw.go | 32 ++++++++++ vpw_test.go | 50 ++++++++++++++++ vwr.go | 44 ++++++++++++++ vwr_test.go | 92 ++++++++++++++++++++++++++++ vwt.go | 42 +++++++++++++ vwt_test.go | 92 ++++++++++++++++++++++++++++ xdr.go | 168 ++++++++++++++++++++++++++++++++++++++++++++++++++++ xdr_test.go | 77 ++++++++++++++++++++++++ xte.go | 60 +++++++++++++++++++ xte_test.go | 74 +++++++++++++++++++++++ 57 files changed, 3759 insertions(+), 68 deletions(-) create mode 100644 aam.go create mode 100644 aam_test.go create mode 100644 ala.go create mode 100644 ala_test.go create mode 100644 apb.go create mode 100644 apb_test.go create mode 100644 bec.go create mode 100644 bec_test.go create mode 100644 bod.go create mode 100644 bod_test.go create mode 100644 bwc.go create mode 100644 bwc_test.go create mode 100644 bwr.go create mode 100644 bwr_test.go create mode 100644 bww.go create mode 100644 bww_test.go create mode 100644 dbk.go create mode 100644 dbk_test.go create mode 100644 dor.go create mode 100644 dor_test.go create mode 100644 dsc.go create mode 100644 dsc_test.go create mode 100644 dse.go create mode 100644 dse_test.go create mode 100644 eve.go create mode 100644 eve_test.go create mode 100644 fir.go create mode 100644 fir_test.go create mode 100644 hsc.go create mode 100644 hsc_test.go create mode 100644 mta.go create mode 100644 mta_test.go create mode 100644 rmb.go create mode 100644 rmb_test.go create mode 100644 rpm.go create mode 100644 rpm_test.go create mode 100644 rsa.go create mode 100644 rsa_test.go create mode 100644 vdr.go create mode 100644 vdr_test.go create mode 100644 vlw.go create mode 100644 vlw_test.go create mode 100644 vpw.go create mode 100644 vpw_test.go create mode 100644 vwr.go create mode 100644 vwr_test.go create mode 100644 vwt.go create mode 100644 vwt_test.go create mode 100644 xdr.go create mode 100644 xdr_test.go create mode 100644 xte.go create mode 100644 xte_test.go diff --git a/README.md b/README.md index 1bdb1bd..012853f 100644 --- a/README.md +++ b/README.md @@ -29,41 +29,66 @@ To update go-nmea to the latest version, use `go get -u github.com/adrianmo/go-n At this moment, this library supports the following sentence types: -| Sentence type | Description | -|-------------------------------------------------------------------------------|-----------------------------------------------------------| -| [RMC](http://aprs.gids.nl/nmea/#rmc) | Recommended Minimum Specific GPS/Transit data | -| [GGA](http://aprs.gids.nl/nmea/#gga) | GPS Positioning System Fix Data | -| [GSA](http://aprs.gids.nl/nmea/#gsa) | GPS DOP and active satellites | -| [GSV](http://aprs.gids.nl/nmea/#gsv) | GPS Satellites in view | -| [GLL](http://aprs.gids.nl/nmea/#gll) | Geographic Position, Latitude / Longitude and time | -| [VTG](http://aprs.gids.nl/nmea/#vtg) | Track Made Good and Ground Speed | -| [ZDA](http://aprs.gids.nl/nmea/#zda) | Date & time data | -| [HDT](http://aprs.gids.nl/nmea/#hdt) | Actual vessel heading in degrees True | -| [HDG](https://gpsd.gitlab.io/gpsd/NMEA.html#_hdg_heading_deviation_variation) | Heading, Deviation & Variation | -| [HDM](https://gpsd.gitlab.io/gpsd/NMEA.html#_hdm_heading_magnetic) | Heading - Magnetic | -| [GNS](https://gpsd.gitlab.io/gpsd/NMEA.html#_gns_fix_data) | Combined GPS fix for GPS, Glonass, Galileo, and BeiDou | -| [VDM/VDO](https://gpsd.gitlab.io/gpsd/AIVDM.html) | Encapsulated binary payload (commonly used with AIS data) | -| [WPL](http://aprs.gids.nl/nmea/#wpl) | Waypoint location | -| [RTE](http://aprs.gids.nl/nmea/#rte) | Route | -| [ROT](https://gpsd.gitlab.io/gpsd/NMEA.html#_rot_rate_of_turn) | Rate of turn | -| [VHW](https://www.tronico.fi/OH6NT/docs/NMEA0183.pdf) | Water Speed and Heading | -| [DPT](https://gpsd.gitlab.io/gpsd/NMEA.html#_dpt_depth_of_water) | Depth of Water | -| [DBS](https://gpsd.gitlab.io/gpsd/NMEA.html#_dbs_depth_below_surface) | Depth Below Surface | -| [DBT](https://gpsd.gitlab.io/gpsd/NMEA.html#_dbt_depth_below_transducer) | Depth below transducer | -| [MDA](https://gpsd.gitlab.io/gpsd/NMEA.html#_mda_meteorological_composite) | Meteorological Composite | -| [MWD](https://www.tronico.fi/OH6NT/docs/NMEA0183.pdf) | Wind Direction and Speed | -| [MWV](https://gpsd.gitlab.io/gpsd/NMEA.html#_mwv_wind_speed_and_angle) | Wind Speed and Angle | -| [MTW](https://gpsd.gitlab.io/gpsd/NMEA.html#_mtw_mean_temperature_of_water) | Mean Temperature of Water | -| [THS](http://www.nuovamarea.net/pytheas_9.html) | Actual vessel heading in degrees True and status | -| [TXT](https://www.nmea.org/Assets/20160520%20txt%20amendment.pdf) | Sentence is for the transmission of text messages | +| Sentence type | Description | +|-----------------------------------------------------------------------------------------------|-----------------------------------------------------------| +| [AAM](https://gpsd.gitlab.io/gpsd/NMEA.html#_aam_waypoint_arrival_alarm) | Waypoint Arrival Alarm | +| [ALA](./ala.go) | System Faults and Alarms | +| [APB](https://gpsd.gitlab.io/gpsd/NMEA.html#_apb_autopilot_sentence_b) | Autopilot Sentence "B" | +| [BEC](http://www.nmea.de/nmea0183datensaetze.html#bec) | Bearing and distance to waypoint (dead reckoning) | +| [BOD](https://gpsd.gitlab.io/gpsd/NMEA.html#_bod_bearing_waypoint_to_waypoint) | Bearing waypoint to waypoint (origin to destination) | +| [BWC](https://gpsd.gitlab.io/gpsd/NMEA.html#_bwc_bearing_distance_to_waypoint_great_circle) | Bearing and distance to waypoint (great circle) | +| [BWR](https://gpsd.gitlab.io/gpsd/NMEA.html#_bwr_bearing_and_distance_to_waypoint_rhumb_line) | Bearing and distance to waypoint (Rhumb Line) | +| [BWW](https://gpsd.gitlab.io/gpsd/NMEA.html#_bww_bearing_waypoint_to_waypoint) | Bearing from destination waypoint to origin waypoint | +| [DBK](https://gpsd.gitlab.io/gpsd/NMEA.html#_dbk_depth_below_keel) | Depth Below Keel (obsolete, use DPT instead) | +| [DBS](https://gpsd.gitlab.io/gpsd/NMEA.html#_dbs_depth_below_surface) | Depth Below Surface (obsolete, use DPT instead) | +| [DBT](https://gpsd.gitlab.io/gpsd/NMEA.html#_dbt_depth_below_transducer) | Depth below transducer | +| [DOR](./dor.go) | Door Status Detection | +| [DPT](https://gpsd.gitlab.io/gpsd/NMEA.html#_dpt_depth_of_water) | Depth of Water | +| [DSC](./dsc.go) | Digital Selective Calling Information | +| [DSE](./dse.go) | Expanded digital selective calling | +| [EVE](./eve.go) | General Event Message | +| [FIR](./fir.go) | Fire Detection event with time and location | +| [GGA](http://aprs.gids.nl/nmea/#gga) | GPS Positioning System Fix Data | +| [GLL](http://aprs.gids.nl/nmea/#gll) | Geographic Position, Latitude / Longitude and time | +| [GNS](https://gpsd.gitlab.io/gpsd/NMEA.html#_gns_fix_data) | Combined GPS fix for GPS, Glonass, Galileo, and BeiDou | +| [GSA](http://aprs.gids.nl/nmea/#gsa) | GPS DOP and active satellites | +| [GSV](http://aprs.gids.nl/nmea/#gsv) | GPS Satellites in view | +| [HDG](https://gpsd.gitlab.io/gpsd/NMEA.html#_hdg_heading_deviation_variation) | Heading, Deviation & Variation | +| [HDM](https://gpsd.gitlab.io/gpsd/NMEA.html#_hdm_heading_magnetic) | Heading - Magnetic | +| [HDT](http://aprs.gids.nl/nmea/#hdt) | Actual vessel heading in degrees True | +| [HSC](https://gpsd.gitlab.io/gpsd/NMEA.html#_hsc_heading_steering_command) | Heading steering command | +| [MDA](https://gpsd.gitlab.io/gpsd/NMEA.html#_mda_meteorological_composite) | Meteorological Composite | +| [MTA](./mta.go) | Air Temperature (obsolete, use XDR instead) | +| [MTW](https://gpsd.gitlab.io/gpsd/NMEA.html#_mtw_mean_temperature_of_water) | Mean Temperature of Water | +| [MWD](https://www.tronico.fi/OH6NT/docs/NMEA0183.pdf) | Wind Direction and Speed | +| [MWV](https://gpsd.gitlab.io/gpsd/NMEA.html#_mwv_wind_speed_and_angle) | Wind Speed and Angle | +| [RMB](https://gpsd.gitlab.io/gpsd/NMEA.html#_rmb_recommended_minimum_navigation_information) | Recommended Minimum Navigation Information | +| [RMC](http://aprs.gids.nl/nmea/#rmc) | Recommended Minimum Specific GPS/Transit data | +| [ROT](https://gpsd.gitlab.io/gpsd/NMEA.html#_rot_rate_of_turn) | Rate of turn | +| [RPM](https://gpsd.gitlab.io/gpsd/NMEA.html#_rpm_revolutions) | Engine or Shaft revolutions and pitch | +| [RSA](https://gpsd.gitlab.io/gpsd/NMEA.html#_rsa_rudder_sensor_angle) | Rudder Sensor Angle | +| [RTE](http://aprs.gids.nl/nmea/#rte) | Route | +| [THS](http://www.nuovamarea.net/pytheas_9.html) | Actual vessel heading in degrees True and status | +| [TXT](https://www.nmea.org/Assets/20160520%20txt%20amendment.pdf) | Sentence is for the transmission of text messages | +| [VDM/VDO](https://gpsd.gitlab.io/gpsd/AIVDM.html) | Encapsulated binary payload (commonly used with AIS data) | +| [VDR](https://gpsd.gitlab.io/gpsd/NMEA.html#_vdr_set_and_drift) | Set and Drift | +| [VHW](https://www.tronico.fi/OH6NT/docs/NMEA0183.pdf) | Water Speed and Heading | +| [VLW](https://gpsd.gitlab.io/gpsd/NMEA.html#_vlw_distance_traveled_through_water) | Distance Traveled through Water | +| [VPW](https://gpsd.gitlab.io/gpsd/NMEA.html#_vpw_speed_measured_parallel_to_wind) | Speed Measured Parallel to Wind | +| [VTG](http://aprs.gids.nl/nmea/#vtg) | Track Made Good and Ground Speed | +| [VWR](https://gpsd.gitlab.io/gpsd/NMEA.html#_vwr_relative_wind_speed_and_angle) | Relative Wind Speed and Angle | +| [VWT](./vwt.go) | True Wind Speed and Angle | +| [WPL](http://aprs.gids.nl/nmea/#wpl) | Waypoint location | +| [XDR](https://gpsd.gitlab.io/gpsd/NMEA.html#_xdr_transducer_measurement) | Transducer Measurement | +| [ZDA](http://aprs.gids.nl/nmea/#zda) | Date & time data | | Proprietary sentence type | Description | |-------------------------------------------------------------|-------------------------------------------------------------------------------------------------| -| [PMTK](https://www.rhydolabz.com/documents/25/PMTK_A11.pdf) | Messages for setting and reading commands for MediaTek gps modules. | | [PGRME](http://aprs.gids.nl/nmea/#rme) | Estimated Position Error (Garmin proprietary sentence) | -| [PSONCMS](#) | Quaternion, acceleration, rate of turn, magnetic field, sensor temperature (Xsens IMU/VRU/AHRS) | -| [PRDID](#) | Vessel pitch, roll and heading (Xsens IMU/VRU/AHRS) | | [PHTRO](#) | Vessel pitch and roll (Xsens IMU/VRU/AHRS) | +| [PMTK](https://www.rhydolabz.com/documents/25/PMTK_A11.pdf) | Messages for setting and reading commands for MediaTek gps modules. | +| [PRDID](#) | Vessel pitch, roll and heading (Xsens IMU/VRU/AHRS) | +| [PSONCMS](#) | Quaternion, acceleration, rate of turn, magnetic field, sensor temperature (Xsens IMU/VRU/AHRS) | If you need to parse a message that contains an unsupported sentence type you can implement and register your own message parser and get yourself unblocked immediately. Check the example below to know how diff --git a/aam.go b/aam.go new file mode 100644 index 0000000..f5b27c8 --- /dev/null +++ b/aam.go @@ -0,0 +1,49 @@ +package nmea + +const ( + // TypeAAM type of AAM sentence for Waypoint Arrival Alarm + TypeAAM = "AAM" +) + +// AAM - Waypoint Arrival Alarm +// This sentence is generated by some units to indicate the status of arrival (entering the arrival circle, or passing +// the perpendicular of the course line) at the destination waypoint (source: GPSD). +// https://gpsd.gitlab.io/gpsd/NMEA.html#_aam_waypoint_arrival_alarm +// +// Format: $--AAM,A,A,x.x,N,c--c*hh +// Example: $GPAAM,A,A,0.10,N,WPTNME*43 +type AAM struct { + BaseSentence + // StatusArrivalCircleEntered is warning of arrival to waypoint circle + // * A = Arrival Circle Entered + // * V = not entered + StatusArrivalCircleEntered string + + // StatusPerpendicularPassed is warning for perpendicular passing of waypoint + // * A = Perpendicular passed at waypoint + // * V = not passed + StatusPerpendicularPassed string + + // ArrivalCircleRadius is radius for arrival circle + ArrivalCircleRadius float64 + + // ArrivalCircleRadiusUnit is unit for arrival circle radius + ArrivalCircleRadiusUnit string + + // DestinationWaypointID is destination waypoint ID + DestinationWaypointID string +} + +// newAAM constructor +func newAAM(s BaseSentence) (AAM, error) { + p := NewParser(s) + p.AssertType(TypeAAM) + return AAM{ + BaseSentence: s, + StatusArrivalCircleEntered: p.EnumString(0, "arrival circle entered status", WPStatusArrivalCircleEnteredA, WPStatusArrivalCircleEnteredV), + StatusPerpendicularPassed: p.EnumString(1, "perpendicularly passed status", WPStatusPerpendicularPassedA, WPStatusPerpendicularPassedV), + ArrivalCircleRadius: p.Float64(2, "arrival circle radius"), + ArrivalCircleRadiusUnit: p.EnumString(3, "arrival circle radius units", DistanceUnitKilometre, DistanceUnitNauticalMile, DistanceUnitStatuteMile, DistanceUnitMetre), + DestinationWaypointID: p.String(4, "destination waypoint ID"), + }, p.Err() +} diff --git a/aam_test.go b/aam_test.go new file mode 100644 index 0000000..1aa167a --- /dev/null +++ b/aam_test.go @@ -0,0 +1,56 @@ +package nmea + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestAAM(t *testing.T) { + var tests = []struct { + name string + raw string + err string + msg AAM + }{ + { + name: "good sentence", + raw: "$GPAAM,A,A,0.10,N,WPTNME*32", + msg: AAM{ + StatusArrivalCircleEntered: WPStatusArrivalCircleEnteredA, + StatusPerpendicularPassed: WPStatusPerpendicularPassedA, + ArrivalCircleRadius: 0.1, + ArrivalCircleRadiusUnit: DistanceUnitNauticalMile, + DestinationWaypointID: "WPTNME", + }, + }, + { + name: "invalid nmea: StatusArrivalCircleEntered", + raw: "$GPAAM,x,A,0.10,N,WPTNME*0B", + err: "nmea: GPAAM invalid arrival circle entered status: x", + }, + { + name: "invalid nmea: StatusPerpendicularPassed", + raw: "$GPAAM,A,x,0.10,N,WPTNME*0B", + err: "nmea: GPAAM invalid perpendicularly passed status: x", + }, + { + name: "invalid nmea: DistanceUnitNauticalMile", + raw: "$GPAAM,A,A,0.10,x,WPTNME*04", + err: "nmea: GPAAM invalid arrival circle radius units: x", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m, err := Parse(tt.raw) + if tt.err != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.err) + } else { + assert.NoError(t, err) + hdt := m.(AAM) + hdt.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, hdt) + } + }) + } +} diff --git a/ala.go b/ala.go new file mode 100644 index 0000000..1ee8110 --- /dev/null +++ b/ala.go @@ -0,0 +1,68 @@ +package nmea + +const ( + // TypeALA type of ALA sentence for System Faults and alarms + TypeALA = "ALA" +) + +// ALA - System Faults and alarms +// Source: "Interfacing Voyage Data Recorder Systems, AutroSafe Interactive Fire-Alarm System, 116-P-BSL336/EE, RevA 2007-01-25, +// Autronica Fire and Security AS " (page 31 | p.8.1.3) +// https://product.autronicafire.com/fileshare/fileupload/14251/bsl336_ee.pdf +// +// Format: $FRALA,hhmmss,aa,aa,xx,xxx,a,a,c-cc*hh +// Example: $FRALA,143955,FR,OT,00,901,N,V,Syst Fault : AutroSafe comm. OK*4F +type ALA struct { + BaseSentence + + // Time is Event Time + Time Time + + // SystemIndicator is system indicator of original alarm source. Detector system type with 2 char identifier. + // Values not known + // https://www.nmea.org/Assets/20190303%20nmea%200183%20talker%20identifier%20mnemonics.pdf + SystemIndicator string + + // SubSystemIndicator is sub system equipment indicator of original alarm source + SubSystemIndicator string + + // InstanceNumber is instance number of equipment/unit/item (00-99) + InstanceNumber int64 + + // Type is alarm type (000-999) + Type int64 + + // Condition describes the condition triggering current message + // * N – Normal state (OK) + // * H - Alarm state (fault); + // could be more + Condition string + + // AlarmAckState is Alarm's acknowledge state + // * A – Acknowledged + // * H - Harbour mode + // * V – Not acknowledged + // * O - Override + // could be more + AlarmAckState string + + // Message's description text (could be cut to fit max packet length) + Message string +} + +// newALA constructor +func newALA(s BaseSentence) (ALA, error) { + p := NewParser(s) + p.AssertType(TypeALA) + return ALA{ + BaseSentence: s, + Time: p.Time(0, "time"), + SystemIndicator: p.String(1, "system indicator"), + SubSystemIndicator: p.String(2, "subsystem indicator"), + InstanceNumber: p.Int64(3, "instance number"), + Type: p.Int64(4, "type"), + Condition: p.String(5, "condition"), // string as there could be more + AlarmAckState: p.String(6, "alarm acknowledgement state"), // string as there could be more + Message: p.String(7, "message"), + }, p.Err() +} diff --git a/ala_test.go b/ala_test.go new file mode 100644 index 0000000..f23a906 --- /dev/null +++ b/ala_test.go @@ -0,0 +1,65 @@ +package nmea + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestALA(t *testing.T) { + var tests = []struct { + name string + raw string + err string + msg ALA + }{ + { + name: "good sentence", + raw: "$FRALA,143955,FR,OT,00,901,N,V,Syst Fault : AutroSafe comm. OK*4F", + msg: ALA{ + Time: Time{ + Valid: true, + Hour: 14, + Minute: 39, + Second: 55, + Millisecond: 0, + }, + SystemIndicator: "FR", + SubSystemIndicator: "OT", + InstanceNumber: 0, + Type: 901, + Condition: "N", + AlarmAckState: "V", + Message: "Syst Fault : AutroSafe comm. OK", + }, + }, + { + name: "invalid nmea: Time", + raw: "$FRALA,1x3955,FR,OT,00,901,N,V,Syst Fault : AutroSafe comm. OK*03", + err: "nmea: FRALA invalid time: 1x3955", + }, + { + name: "invalid nmea: InstanceNumber", + raw: "$FRALA,143955,FR,OT,x0,901,N,V,Syst Fault : AutroSafe comm. OK*07", + err: "nmea: FRALA invalid instance number: x0", + }, + { + name: "invalid nmea: Type", + raw: "$FRALA,143955,FR,OT,00,9x1,N,V,Syst Fault : AutroSafe comm. OK*07", + err: "nmea: FRALA invalid type: 9x1", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m, err := Parse(tt.raw) + if tt.err != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.err) + } else { + assert.NoError(t, err) + hdt := m.(ALA) + hdt.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, hdt) + } + }) + } +} diff --git a/apb.go b/apb.go new file mode 100644 index 0000000..8f326c3 --- /dev/null +++ b/apb.go @@ -0,0 +1,131 @@ +package nmea + +const ( + // TypeAPB type of APB sentence for Autopilot Sentence "B" + TypeAPB = "APB" + + // StatusWarningASetAPB indicates LORAN-C Blink or SNR warning + StatusWarningASetAPB = "V" + // StatusWarningAClearORNotUsedAPB general warning flag or other navigation systems when a reliable fix is not available + StatusWarningAClearORNotUsedAPB = "A" + + // StatusWarningBSetAPB means Loran-C Cycle Lock warning OK or not used + StatusWarningBSetAPB = "A" + // StatusWarningBClearAPB means Loran-C Cycle Lock warning flag + StatusWarningBClearAPB = "V" +) + +// Autopilot related constants (used in APB, APA, AAM) +const ( + // WPStatusPerpendicularPassedA is warning for passing the perpendicular of the course line of waypoint + WPStatusPerpendicularPassedA = "A" + // WPStatusPerpendicularPassedV indicates for not passing of the perpendicular of the course line of waypoint + WPStatusPerpendicularPassedV = "V" + + // WPStatusArrivalCircleEnteredA is warning of entering to waypoint circle + WPStatusArrivalCircleEnteredA = "A" + // WPStatusArrivalCircleEnteredV indicates of not yet entered into waypoint circle + WPStatusArrivalCircleEnteredV = "V" +) + +// APB - Autopilot Sentence "B" for heading/tracking +// https://gpsd.gitlab.io/gpsd/NMEA.html#_apb_autopilot_sentence_b +// https://www.tronico.fi/OH6NT/docs/NMEA0183.pdf (page 5) +// +// Format: $--APB,A,A,x.x,a,N,A,A,x.x,a,c--c,x.x,a,x.x,a*hh +// Format NMEA 2.3+: $--APB,A,A,x.x,a,N,A,A,x.x,a,c--c,x.x,a,x.x,a,a*hh +// Example: $GPAPB,A,A,0.10,R,N,V,V,011,M,DEST,011,M,011,M*82 +// $ECAPB,A,A,0.0,L,M,V,V,175.2,T,Antechamber_Bay,175.2,T,175.2,T*48 +type APB struct { + BaseSentence + + // StatusGeneralWarning is used for warnings + // * V = LORAN-C Blink or SNR warning + // * A = general warning flag or other navigation systems when a reliable fix is not available + StatusGeneralWarning string + + // StatusLockWarning is used for lock warning + // * V = Loran-C Cycle Lock warning flag + // * A = OK or not used + StatusLockWarning string + + // CrossTrackErrorMagnitude is Cross Track Error Magnitude + CrossTrackErrorMagnitude float64 + + // DirectionToSteer is Direction to steer, + // * L = left + // * R = right + DirectionToSteer string + + // CrossTrackUnits is cross track units + // * N = nautical miles + // * K = for kilometers + CrossTrackUnits string + + // StatusArrivalCircleEntered is warning of arrival to waypoint circle + // * A = Arrival Circle Entered + // * V = not entered + StatusArrivalCircleEntered string + + // StatusPerpendicularPassed is warning for perpendicular passing of waypoint + // * A = Perpendicular passed at waypoint + // * V = not passed + StatusPerpendicularPassed string + + // BearingOriginToDest is Bearing origin to destination + BearingOriginToDest float64 + + // BearingOriginToDestType is Bearing origin to dest type + // * M = Magnetic + // * T = True + BearingOriginToDestType string + + // DestinationWaypointID is Destination waypoint ID + DestinationWaypointID string + + // BearingPresentToDest is Bearing, present position to Destination + BearingPresentToDest float64 + + // BearingPresentToDestType is Bearing present to dest type + // * M = Magnetic + // * T = True + BearingPresentToDestType string + + // Heading is heading to steer to destination waypoint + Heading float64 + + // HeadingType is Heading type + // * M = Magnetic + // * T = True + HeadingType string + + // FAA mode indicator (filled in NMEA 2.3 and later) + FFAMode string +} + +// newAPB constructor +func newAPB(s BaseSentence) (APB, error) { + p := NewParser(s) + p.AssertType(TypeAPB) + apb := APB{ + BaseSentence: s, + StatusGeneralWarning: p.EnumString(0, "general warning", StatusWarningAClearORNotUsedAPB, StatusWarningASetAPB), + StatusLockWarning: p.EnumString(1, "lock warning", StatusWarningBSetAPB, StatusWarningBClearAPB), + CrossTrackErrorMagnitude: p.Float64(2, "cross track error magnitude"), + DirectionToSteer: p.EnumString(3, "direction to steer", Left, Right), + CrossTrackUnits: p.EnumString(4, "cross track units", DistanceUnitKilometre, DistanceUnitNauticalMile, DistanceUnitStatuteMile, DistanceUnitMetre), + StatusArrivalCircleEntered: p.EnumString(5, "arrival circle entered status", WPStatusArrivalCircleEnteredA, WPStatusArrivalCircleEnteredV), + StatusPerpendicularPassed: p.EnumString(6, "perpendicularly passed status", WPStatusPerpendicularPassedA, WPStatusPerpendicularPassedV), + BearingOriginToDest: p.Float64(7, "origin bearing to destination"), + BearingOriginToDestType: p.EnumString(8, "origin bearing to destination type", HeadingMagnetic, HeadingTrue), + DestinationWaypointID: p.String(9, "destination waypoint ID"), + BearingPresentToDest: p.Float64(10, "present bearing to destination"), + BearingPresentToDestType: p.EnumString(11, "present bearing to destination type", HeadingMagnetic, HeadingTrue), + Heading: p.Float64(12, "heading"), + HeadingType: p.EnumString(13, "heading type", HeadingMagnetic, HeadingTrue), + } + if len(p.Fields) > 14 { + apb.FFAMode = p.String(14, "FAA mode") // not enum because some devices have proprietary "non-nmea" values + } + return apb, p.Err() +} diff --git a/apb_test.go b/apb_test.go new file mode 100644 index 0000000..ede569b --- /dev/null +++ b/apb_test.go @@ -0,0 +1,92 @@ +package nmea + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestAPB(t *testing.T) { + var tests = []struct { + name string + raw string + err string + msg APB + }{ + { + name: "good sentence", + raw: "$GPAPB,A,A,0.10,R,N,V,V,011,M,DEST,011,M,011,M*3C", + msg: APB{ + StatusGeneralWarning: "A", + StatusLockWarning: "A", + CrossTrackErrorMagnitude: 0.1, + DirectionToSteer: "R", + CrossTrackUnits: "N", + StatusArrivalCircleEntered: "V", + StatusPerpendicularPassed: "V", + BearingOriginToDest: 11, + BearingOriginToDestType: "M", + DestinationWaypointID: "DEST", + BearingPresentToDest: 11, + BearingPresentToDestType: "M", + Heading: 11, + HeadingType: "M", + FFAMode: "", + }, + }, + { + name: "good sentence b with FAA mode", + raw: "$ECAPB,A,A,0.0,L,M,V,V,175.2,T,Antechamber_Bay,175.2,T,175.2,T,V*32", + msg: APB{ + StatusGeneralWarning: "A", + StatusLockWarning: "A", + CrossTrackErrorMagnitude: 0, + DirectionToSteer: "L", + CrossTrackUnits: "M", + StatusArrivalCircleEntered: "V", + StatusPerpendicularPassed: "V", + BearingOriginToDest: 175.2, + BearingOriginToDestType: "T", + DestinationWaypointID: "Antechamber_Bay", + BearingPresentToDest: 175.2, + BearingPresentToDestType: "T", + Heading: 175.2, + HeadingType: "T", + FFAMode: "V", + }, + }, + { + name: "invalid nmea: CrossTrackErrorMagnitude", + raw: "$ECAPB,A,A,x.0,L,M,V,V,175.2,T,Antechamber_Bay,175.2,T,175.2,T,V*7A", + err: "nmea: ECAPB invalid cross track error magnitude: x.0", + }, + { + name: "invalid nmea: BearingOriginToDest", + raw: "$ECAPB,A,A,0.0,L,M,V,V,175.x,T,Antechamber_Bay,175.2,T,175.2,T,V*78", + err: "nmea: ECAPB invalid origin bearing to destination: 175.x", + }, + { + name: "invalid nmea: BearingPresentToDest", + raw: "$ECAPB,A,A,0.0,L,M,V,V,175.2,T,Antechamber_Bay,175.x,T,175.2,T,V*78", + err: "nmea: ECAPB invalid present bearing to destination: 175.x", + }, + { + name: "invalid nmea: Heading", + raw: "$ECAPB,A,A,0.0,L,M,V,V,175.2,T,Antechamber_Bay,175.2,T,175.x,T,V*78", + err: "nmea: ECAPB invalid heading: 175.x", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m, err := Parse(tt.raw) + if tt.err != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.err) + } else { + assert.NoError(t, err) + hdt := m.(APB) + hdt.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, hdt) + } + }) + } +} diff --git a/bec.go b/bec.go new file mode 100644 index 0000000..c6bba6c --- /dev/null +++ b/bec.go @@ -0,0 +1,45 @@ +package nmea + +const ( + // TypeBEC type of BEC sentence for bearing and distance to waypoint (dead reckoning) + TypeBEC = "BEC" +) + +// BEC - bearing and distance to waypoint (dead reckoning) +// http://www.nmea.de/nmea0183datensaetze.html#bec +// https://www.eye4software.com/hydromagic/documentation/nmea0183/ +// +// Format: $--BEC,hhmmss.ss,llll.ll,a,yyyyy.yy,a,x.x,T,x.x,M,x.x,N,c--c*hh +// Example: $GPBEC,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*33 +type BEC struct { + BaseSentence + Time Time // UTC Time + Latitude float64 // latitude of waypoint + Longitude float64 // longitude of waypoint + BearingTrue float64 // true bearing in degrees + BearingTrueValid bool // is unit of true bearing valid + BearingMagnetic float64 // magnetic bearing in degrees + BearingMagneticValid bool // is unit of magnetic bearing valid + DistanceNauticalMiles float64 // distance to waypoint in nautical miles + DistanceNauticalMilesValid bool // is unit of distance to waypoint nautical miles valid + DestinationWaypointID string // destination waypoint ID +} + +// newBEC constructor +func newBEC(s BaseSentence) (BEC, error) { + p := NewParser(s) + p.AssertType(TypeBEC) + return BEC{ + BaseSentence: s, + Time: p.Time(0, "time"), + Latitude: p.LatLong(1, 2, "latitude"), + Longitude: p.LatLong(3, 4, "longitude"), + BearingTrue: p.Float64(5, "true bearing"), + BearingTrueValid: p.EnumString(6, "true bearing unit valid", BearingTrue) == BearingTrue, + BearingMagnetic: p.Float64(7, "magnetic bearing"), + BearingMagneticValid: p.EnumString(8, "magnetic bearing unit valid", BearingMagnetic) == BearingMagnetic, + DistanceNauticalMiles: p.Float64(9, "distance to waypoint is nautical miles"), + DistanceNauticalMilesValid: p.EnumString(10, "is distance to waypoint nautical miles valid", DistanceUnitNauticalMile) == DistanceUnitNauticalMile, + DestinationWaypointID: p.String(11, "destination waypoint ID"), + }, p.Err() +} diff --git a/bec_test.go b/bec_test.go new file mode 100644 index 0000000..31fb92a --- /dev/null +++ b/bec_test.go @@ -0,0 +1,72 @@ +package nmea + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestBEC(t *testing.T) { + var tests = []struct { + name string + raw string + err string + msg BEC + }{ + { + name: "good sentence", + raw: "$GPBEC,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*33", + msg: BEC{ + Time: Time{ + Valid: true, + Hour: 22, + Minute: 5, + Second: 16, + Millisecond: 0, + }, + Latitude: 51.50033333333334, + Longitude: -0.7723333333333334, + BearingTrue: 213.8, + BearingTrueValid: true, + BearingMagnetic: 218, + BearingMagneticValid: true, + DistanceNauticalMiles: 4.6, + DistanceNauticalMilesValid: true, + DestinationWaypointID: "EGLM", + }, + }, + { + name: "invalid nmea: Time", + raw: "$GPBEC,2x0516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*79", + err: "nmea: GPBEC invalid time: 2x0516", + }, + { + name: "invalid nmea: BearingTrueValid", + raw: "$GPBEC,220516,5130.02,N,00046.34,W,213.8,M,218.0,M,0004.6,N,EGLM*2A", + err: "nmea: GPBEC invalid true bearing unit valid: M", + }, + { + name: "invalid nmea: BearingMagneticValid", + raw: "$GPBEC,220516,5130.02,N,00046.34,W,213.8,T,218.0,T,0004.6,N,EGLM*2A", + err: "nmea: GPBEC invalid magnetic bearing unit valid: T", + }, + { + name: "invalid nmea: DistanceNauticalMilesValid", + raw: "$GPBEC,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,K,EGLM*36", + err: "nmea: GPBEC invalid is distance to waypoint nautical miles valid: K", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m, err := Parse(tt.raw) + if tt.err != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.err) + } else { + assert.NoError(t, err) + hdt := m.(BEC) + hdt.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, hdt) + } + }) + } +} diff --git a/bod.go b/bod.go new file mode 100644 index 0000000..1caa2ec --- /dev/null +++ b/bod.go @@ -0,0 +1,45 @@ +package nmea + +const ( + // TypeBOD type of BOD sentence for bearing waypoint to waypoint + TypeBOD = "BOD" +) + +// BOD - bearing waypoint to waypoint (origin to destination). +// Replaced by BWW in NMEA4+ (according to GPSD docs) +// If your system supports RMB it is better to use RMB as it is more common (according to OpenCPN docs) +// https://gpsd.gitlab.io/gpsd/NMEA.html#_bod_bearing_waypoint_to_waypoint +// +// Format: $--BOD,x.x,T,x.x,M,c--c,c--c*hh +// Example: $GPBOD,099.3,T,105.6,M,POINTB*64 +// $GPBOD,097.0,T,103.2,M,POINTB,POINTA*4A +type BOD struct { + BaseSentence + BearingTrue float64 // true bearing in degrees + BearingTrueType string // is type of true bearing + BearingMagnetic float64 // magnetic bearing in degrees + BearingMagneticType string // is type of magnetic bearing + DestinationWaypointID string // destination waypoint ID + OriginWaypointID string // origin waypoint ID +} + +// newBOD constructor +func newBOD(s BaseSentence) (BOD, error) { + p := NewParser(s) + p.AssertType(TypeBOD) + bod := BOD{ + BaseSentence: s, + BearingTrue: p.Float64(0, "true bearing"), + BearingTrueType: p.EnumString(1, "true bearing type", BearingTrue), + BearingMagnetic: p.Float64(2, "magnetic bearing"), + BearingMagneticType: p.EnumString(3, "magnetic bearing type", BearingMagnetic), + DestinationWaypointID: p.String(4, "destination waypoint ID"), + OriginWaypointID: "", + } + // According to GSPD docs: OriginWaypointID is not transmitted in the GOTO mode, without an active route on your GPS. + // in that case you have only DestinationWaypointID + if len(p.Fields) > 5 { + bod.OriginWaypointID = p.String(5, "origin waypoint ID") + } + return bod, p.Err() +} diff --git a/bod_test.go b/bod_test.go new file mode 100644 index 0000000..aef9bfc --- /dev/null +++ b/bod_test.go @@ -0,0 +1,64 @@ +package nmea + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestBOD(t *testing.T) { + var tests = []struct { + name string + raw string + err string + msg BOD + }{ + { + name: "good sentence with both WPs", + raw: "$GPBOD,097.0,T,103.2,M,POINTB,POINTA*4A", + msg: BOD{ + BearingTrue: 97.0, + BearingTrueType: BearingTrue, + BearingMagnetic: 103.2, + BearingMagneticType: BearingMagnetic, + DestinationWaypointID: "POINTB", + OriginWaypointID: "POINTA", + }, + }, + { + name: "good sentence onyl destination", + raw: "$GPBOD,099.3,T,105.6,M,POINTB*64", + msg: BOD{ + BearingTrue: 99.3, + BearingTrueType: BearingTrue, + BearingMagnetic: 105.6, + BearingMagneticType: BearingMagnetic, + DestinationWaypointID: "POINTB", + OriginWaypointID: "", + }, + }, + { + name: "invalid nmea: BearingTrueValid", + raw: "$GPBOD,097.0,M,103.2,M,POINTB,POINTA*53", + err: "nmea: GPBOD invalid true bearing type: M", + }, + { + name: "invalid nmea: BearingMagneticValid", + raw: "$GPBOD,097.0,T,103.2,T,POINTB,POINTA*53", + err: "nmea: GPBOD invalid magnetic bearing type: T", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m, err := Parse(tt.raw) + if tt.err != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.err) + } else { + assert.NoError(t, err) + hdt := m.(BOD) + hdt.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, hdt) + } + }) + } +} diff --git a/bwc.go b/bwc.go new file mode 100644 index 0000000..5cd53b3 --- /dev/null +++ b/bwc.go @@ -0,0 +1,52 @@ +package nmea + +const ( + // TypeBWC type of BWC sentence for bearing and distance to waypoint, great circle + TypeBWC = "BWC" +) + +// BWC - bearing and distance to waypoint, great circle +// https://gpsd.gitlab.io/gpsd/NMEA.html#_bwc_bearing_distance_to_waypoint_great_circle +// http://aprs.gids.nl/nmea/#bwc +// +// Format: $--BWC,hhmmss.ss,llll.ll,a,yyyyy.yy,a,x.x,T,x.x,M,x.x,N,c--c*hh +// Format (NMEA 2.3+): $--BWC,hhmmss.ss,llll.ll,a,yyyyy.yy,a,x.x,T,x.x,M,x.x,N,c--c,m*hh +// Example: $GPBWC,081837,,,,,,T,,M,,N,*13 +// $GPBWC,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*21 +type BWC struct { + BaseSentence + Time Time // UTC Time + Latitude float64 // latitude of waypoint + Longitude float64 // longitude of waypoint + BearingTrue float64 // true bearing in degrees + BearingTrueType string // is type of true bearing + BearingMagnetic float64 // magnetic bearing in degrees + BearingMagneticType string // is type of magnetic bearing + DistanceNauticalMiles float64 // distance to waypoint in nautical miles + DistanceNauticalMilesUnit string // is unit of distance to waypoint nautical miles + DestinationWaypointID string // destination waypoint ID + FFAMode string // FAA mode indicator (filled in NMEA 2.3 and later) +} + +// newBWC constructor +func newBWC(s BaseSentence) (BWC, error) { + p := NewParser(s) + p.AssertType(TypeBWC) + bwc := BWC{ + BaseSentence: s, + Time: p.Time(0, "time"), + Latitude: p.LatLong(1, 2, "latitude"), + Longitude: p.LatLong(3, 4, "longitude"), + BearingTrue: p.Float64(5, "true bearing"), + BearingTrueType: p.EnumString(6, "true bearing type", BearingTrue), + BearingMagnetic: p.Float64(7, "magnetic bearing"), + BearingMagneticType: p.EnumString(8, "magnetic bearing type", BearingMagnetic), + DistanceNauticalMiles: p.Float64(9, "distance to waypoint is nautical miles"), + DistanceNauticalMilesUnit: p.EnumString(10, "is distance to waypoint nautical miles unit", DistanceUnitNauticalMile), + DestinationWaypointID: p.String(11, "destination waypoint ID"), + } + if len(p.Fields) > 12 { + bwc.FFAMode = p.String(12, "FAA mode") // not enum because some devices have proprietary "non-nmea" values + } + return bwc, p.Err() +} diff --git a/bwc_test.go b/bwc_test.go new file mode 100644 index 0000000..e25fa2f --- /dev/null +++ b/bwc_test.go @@ -0,0 +1,113 @@ +package nmea + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestBWC(t *testing.T) { + var tests = []struct { + name string + raw string + err string + msg BWC + }{ + { + name: "good sentence", + raw: "$GPBWC,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*21", + msg: BWC{ + Time: Time{ + Valid: true, + Hour: 22, + Minute: 5, + Second: 16, + Millisecond: 0, + }, + Latitude: 51.50033333333334, + Longitude: -0.7723333333333334, + BearingTrue: 213.8, + BearingTrueType: BearingTrue, + BearingMagnetic: 218, + BearingMagneticType: BearingMagnetic, + DistanceNauticalMiles: 4.6, + DistanceNauticalMilesUnit: DistanceUnitNauticalMile, + DestinationWaypointID: "EGLM", + FFAMode: "", + }, + }, + { + name: "good sentence no waypoint", + raw: "$GPBWC,081837,,,,,,T,,M,,N,*13", + msg: BWC{ + Time: Time{Valid: true, Hour: 8, Minute: 18, Second: 37, Millisecond: 0}, + Latitude: 0, + Longitude: 0, + BearingTrue: 0, + BearingTrueType: BearingTrue, + BearingMagnetic: 0, + BearingMagneticType: BearingMagnetic, + DistanceNauticalMiles: 0, + DistanceNauticalMilesUnit: DistanceUnitNauticalMile, + DestinationWaypointID: "", + FFAMode: "", + }, + }, + { + name: "good sentence with FAAMode", + raw: "$GPBWC,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM,D*49", + msg: BWC{ + Time: Time{ + Valid: true, + Hour: 22, + Minute: 5, + Second: 16, + Millisecond: 0, + }, + Latitude: 51.50033333333334, + Longitude: -0.7723333333333334, + BearingTrue: 213.8, + BearingTrueType: BearingTrue, + BearingMagnetic: 218, + BearingMagneticType: BearingMagnetic, + DistanceNauticalMiles: 4.6, + DistanceNauticalMilesUnit: DistanceUnitNauticalMile, + DestinationWaypointID: "EGLM", + FFAMode: FAAModeDifferential, + }, + }, + { + name: "invalid nmea: Time", + raw: "$GPBWC,2x0516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*6B", + err: "nmea: GPBWC invalid time: 2x0516", + }, + { + name: "invalid nmea: BearingTrueValid", + raw: "$GPBWC,220516,5130.02,N,00046.34,W,213.8,M,218.0,M,0004.6,N,EGLM*38", + err: "nmea: GPBWC invalid true bearing type: M", + }, + { + name: "invalid nmea: BearingMagneticValid", + raw: "$GPBWC,220516,5130.02,N,00046.34,W,213.8,T,218.0,T,0004.6,N,EGLM*38", + err: "nmea: GPBWC invalid magnetic bearing type: T", + }, + { + name: "invalid nmea: DistanceNauticalMilesValid", + raw: "$GPBWC,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,K,EGLM*24", + err: "nmea: GPBWC invalid is distance to waypoint nautical miles unit: K", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m, err := Parse(tt.raw) + if tt.err != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.err) + } else { + assert.NoError(t, err) + hdt := m.(BWC) + hdt.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, hdt) + } + }) + } +} diff --git a/bwr.go b/bwr.go new file mode 100644 index 0000000..65a7563 --- /dev/null +++ b/bwr.go @@ -0,0 +1,51 @@ +package nmea + +const ( + // TypeBWR type of BWR sentence for bearing and distance to waypoint (Rhumb Line) + TypeBWR = "BWR" +) + +// BWR - bearing and distance to waypoint (Rhumb Line). This is calculated along rumb line instead of along the great circle. +// https://gpsd.gitlab.io/gpsd/NMEA.html#_bwr_bearing_and_distance_to_waypoint_rhumb_line +// +// Format: $--BWR,hhmmss.ss,llll.ll,a,yyyyy.yy,a,x.x,T,x.x,M,x.x,N,c--c*hh +// Format (NMEA 2.3+): $--BWR,hhmmss.ss,llll.ll,a,yyyyy.yy,a,x.x,T,x.x,M,x.x,N,c--c,m*hh +// Example: $GPBWR,081837,,,,,,T,,M,,N,*02 +// $GPBWR,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*30 +type BWR struct { + BaseSentence + Time Time // UTC Time + Latitude float64 // latitude of waypoint + Longitude float64 // longitude of waypoint + BearingTrue float64 // true bearing in degrees + BearingTrueType string // is type of true bearing + BearingMagnetic float64 // magnetic bearing in degrees + BearingMagneticType string // is type of magnetic bearing + DistanceNauticalMiles float64 // distance to waypoint in nautical miles + DistanceNauticalMilesUnit string // is unit of distance to waypoint nautical miles + DestinationWaypointID string // destination waypoint ID + FFAMode string // FAA mode indicator (filled in NMEA 2.3 and later) +} + +// newBWR constructor +func newBWR(s BaseSentence) (BWR, error) { + p := NewParser(s) + p.AssertType(TypeBWR) + bwc := BWR{ + BaseSentence: s, + Time: p.Time(0, "time"), + Latitude: p.LatLong(1, 2, "latitude"), + Longitude: p.LatLong(3, 4, "longitude"), + BearingTrue: p.Float64(5, "true bearing"), + BearingTrueType: p.EnumString(6, "true bearing type", BearingTrue), + BearingMagnetic: p.Float64(7, "magnetic bearing"), + BearingMagneticType: p.EnumString(8, "magnetic bearing type", BearingMagnetic), + DistanceNauticalMiles: p.Float64(9, "distance to waypoint is nautical miles"), + DistanceNauticalMilesUnit: p.EnumString(10, "is distance to waypoint nautical miles unit", DistanceUnitNauticalMile), + DestinationWaypointID: p.String(11, "destination waypoint ID"), + } + if len(p.Fields) > 12 { + bwc.FFAMode = p.String(12, "FAA mode") // not enum because some devices have proprietary "non-nmea" values + } + return bwc, p.Err() +} diff --git a/bwr_test.go b/bwr_test.go new file mode 100644 index 0000000..4e3ab54 --- /dev/null +++ b/bwr_test.go @@ -0,0 +1,113 @@ +package nmea + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestBWR(t *testing.T) { + var tests = []struct { + name string + raw string + err string + msg BWR + }{ + { + name: "good sentence", + raw: "$GPBWR,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*30", + msg: BWR{ + Time: Time{ + Valid: true, + Hour: 22, + Minute: 5, + Second: 16, + Millisecond: 0, + }, + Latitude: 51.50033333333334, + Longitude: -0.7723333333333334, + BearingTrue: 213.8, + BearingTrueType: BearingTrue, + BearingMagnetic: 218, + BearingMagneticType: BearingMagnetic, + DistanceNauticalMiles: 4.6, + DistanceNauticalMilesUnit: DistanceUnitNauticalMile, + DestinationWaypointID: "EGLM", + FFAMode: "", + }, + }, + { + name: "good sentence no waypoint", + raw: "$GPBWR,081837,,,,,,T,,M,,N,*02", + msg: BWR{ + Time: Time{Valid: true, Hour: 8, Minute: 18, Second: 37, Millisecond: 0}, + Latitude: 0, + Longitude: 0, + BearingTrue: 0, + BearingTrueType: BearingTrue, + BearingMagnetic: 0, + BearingMagneticType: BearingMagnetic, + DistanceNauticalMiles: 0, + DistanceNauticalMilesUnit: DistanceUnitNauticalMile, + DestinationWaypointID: "", + FFAMode: "", + }, + }, + { + name: "good sentence with FAAMode", + raw: "$GPBWR,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM,D*58", + msg: BWR{ + Time: Time{ + Valid: true, + Hour: 22, + Minute: 5, + Second: 16, + Millisecond: 0, + }, + Latitude: 51.50033333333334, + Longitude: -0.7723333333333334, + BearingTrue: 213.8, + BearingTrueType: BearingTrue, + BearingMagnetic: 218, + BearingMagneticType: BearingMagnetic, + DistanceNauticalMiles: 4.6, + DistanceNauticalMilesUnit: DistanceUnitNauticalMile, + DestinationWaypointID: "EGLM", + FFAMode: FAAModeDifferential, + }, + }, + { + name: "invalid nmea: Time", + raw: "$GPBWR,2x0516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*7A", + err: "nmea: GPBWR invalid time: 2x0516", + }, + { + name: "invalid nmea: BearingTrueType", + raw: "$GPBWR,220516,5130.02,N,00046.34,W,213.8,M,218.0,M,0004.6,N,EGLM*29", + err: "nmea: GPBWR invalid true bearing type: M", + }, + { + name: "invalid nmea: BearingMagneticType", + raw: "$GPBWR,220516,5130.02,N,00046.34,W,213.8,T,218.0,T,0004.6,N,EGLM*29", + err: "nmea: GPBWR invalid magnetic bearing type: T", + }, + { + name: "invalid nmea: DistanceNauticalMilesUnit", + raw: "$GPBWR,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,K,EGLM*35", + err: "nmea: GPBWR invalid is distance to waypoint nautical miles unit: K", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m, err := Parse(tt.raw) + if tt.err != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.err) + } else { + assert.NoError(t, err) + hdt := m.(BWR) + hdt.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, hdt) + } + }) + } +} diff --git a/bww.go b/bww.go new file mode 100644 index 0000000..f29d340 --- /dev/null +++ b/bww.go @@ -0,0 +1,40 @@ +package nmea + +const ( + // TypeBWW type of BWW sentence for bearing (from destination) destination waypoint to origin waypoint + TypeBWW = "BWW" +) + +// BWW - bearing (from destination) destination waypoint to origin waypoint +// Replaces by BOD in NMEA4+ (according to GPSD docs) +// If your system supports RMB it is better to use RMB as it is more common (according to OpenCPN docs) +// https://gpsd.gitlab.io/gpsd/NMEA.html#_bww_bearing_waypoint_to_waypoint +// http://www.nmea.de/nmea0183datensaetze.html#bww +// +// Format: $--BWW,x.x,T,x.x,M,c--c,c--c*hh +// Example: $GPBWW,097.0,T,103.2,M,POINTB,POINTA*41 +type BWW struct { + BaseSentence + BearingTrue float64 // true bearing in degrees + BearingTrueType string // is type of true bearing + BearingMagnetic float64 // magnetic bearing in degrees + BearingMagneticType string // is type of magnetic bearing + DestinationWaypointID string // destination waypoint ID + OriginWaypointID string // origin waypoint ID +} + +// newBWW constructor +func newBWW(s BaseSentence) (BWW, error) { + p := NewParser(s) + p.AssertType(TypeBWW) + bod := BWW{ + BaseSentence: s, + BearingTrue: p.Float64(0, "true bearing"), + BearingTrueType: p.EnumString(1, "true bearing type", BearingTrue), + BearingMagnetic: p.Float64(2, "magnetic bearing"), + BearingMagneticType: p.EnumString(3, "magnetic bearing type", BearingMagnetic), + DestinationWaypointID: p.String(4, "destination waypoint ID"), + OriginWaypointID: p.String(5, "origin waypoint ID"), + } + return bod, p.Err() +} diff --git a/bww_test.go b/bww_test.go new file mode 100644 index 0000000..e9bd948 --- /dev/null +++ b/bww_test.go @@ -0,0 +1,52 @@ +package nmea + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestBWW(t *testing.T) { + var tests = []struct { + name string + raw string + err string + msg BWW + }{ + { + name: "good sentence", + raw: "$GPBWW,097.0,T,103.2,M,POINTB,POINTA*41", + msg: BWW{ + BearingTrue: 97.0, + BearingTrueType: BearingTrue, + BearingMagnetic: 103.2, + BearingMagneticType: BearingMagnetic, + DestinationWaypointID: "POINTB", + OriginWaypointID: "POINTA", + }, + }, + { + name: "invalid nmea: BearingTrueValid", + raw: "$GPBWW,097.0,M,103.2,M,POINTB,POINTA*58", + err: "nmea: GPBWW invalid true bearing type: M", + }, + { + name: "invalid nmea: BearingMagneticValid", + raw: "$GPBWW,097.0,T,103.2,T,POINTB,POINTA*58", + err: "nmea: GPBWW invalid magnetic bearing type: T", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m, err := Parse(tt.raw) + if tt.err != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.err) + } else { + assert.NoError(t, err) + hdt := m.(BWW) + hdt.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, hdt) + } + }) + } +} diff --git a/dbk.go b/dbk.go new file mode 100644 index 0000000..33d8a36 --- /dev/null +++ b/dbk.go @@ -0,0 +1,37 @@ +package nmea + +const ( + // TypeDBK type of DBK sentence for Depth Below Keel + TypeDBK = "DBK" +) + +// DBK - Depth Below Keel (obsolete, use DPT instead) +// https://gpsd.gitlab.io/gpsd/NMEA.html#_dbk_depth_below_keel +// https://wiki.openseamap.org/wiki/OpenSeaMap-dev:NMEA#DBK_-_Depth_below_keel +// +// Format: $--DBK,x.x,f,x.x,M,x.x,F*hh +// Example: $SDDBK,12.3,f,3.7,M,2.0,F*2F +type DBK struct { + BaseSentence + DepthFeet float64 // Depth, feet + DepthFeetUnit string // f = feet + DepthMeters float64 // Depth, meters + DepthMetersUnit string // M = meters + DepthFathoms float64 // Depth, Fathoms + DepthFathomsUnit string // F = Fathoms +} + +// newDBK constructor +func newDBK(s BaseSentence) (DBK, error) { + p := NewParser(s) + p.AssertType(TypeDBK) + return DBK{ + BaseSentence: s, + DepthFeet: p.Float64(0, "depth feet"), + DepthFeetUnit: p.EnumString(1, "depth feet unit", DistanceUnitFeet), + DepthMeters: p.Float64(2, "depth meters"), + DepthMetersUnit: p.EnumString(3, "depth meters unit", DistanceUnitMetre), + DepthFathoms: p.Float64(4, "depth fathom"), + DepthFathomsUnit: p.EnumString(5, "depth fathom unit", DistanceUnitFathom), + }, p.Err() +} diff --git a/dbk_test.go b/dbk_test.go new file mode 100644 index 0000000..f480b96 --- /dev/null +++ b/dbk_test.go @@ -0,0 +1,57 @@ +package nmea + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestDBK(t *testing.T) { + var tests = []struct { + name string + raw string + err string + msg DBK + }{ + { + name: "good sentence", + raw: "$SDDBK,12.3,f,3.7,M,2.0,F*2F", + msg: DBK{ + DepthFeet: 12.3, + DepthFeetUnit: DistanceUnitFeet, + DepthMeters: 3.7, + DepthMetersUnit: DistanceUnitMetre, + DepthFathoms: 2, + DepthFathomsUnit: DistanceUnitFathom, + }, + }, + { + name: "invalid nmea: DepthFeetUnit", + raw: "$SDDBK,12.3,x,3.7,M,2.0,F*31", + err: "nmea: SDDBK invalid depth feet unit: x", + }, + { + name: "invalid nmea: DepthMeterUnit", + raw: "$SDDBK,12.3,f,3.7,x,2.0,F*1A", + err: "nmea: SDDBK invalid depth meters unit: x", + }, + { + name: "invalid nmea: DepthFathomUnit", + raw: "$SDDBK,12.3,f,3.7,M,2.0,x*11", + err: "nmea: SDDBK invalid depth fathom unit: x", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m, err := Parse(tt.raw) + if tt.err != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.err) + } else { + assert.NoError(t, err) + hdt := m.(DBK) + hdt.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, hdt) + } + }) + } +} diff --git a/dbs.go b/dbs.go index feecd14..b2540bc 100644 --- a/dbs.go +++ b/dbs.go @@ -1,20 +1,24 @@ package nmea const ( - // TypeDBS type for DBS sentences + // TypeDBS is type of DBS sentence for Depth Below Surface TypeDBS = "DBS" ) -// DBS - Depth Below Surface +// DBS - Depth Below Surface (obsolete, use DPT instead) // https://gpsd.gitlab.io/gpsd/NMEA.html#_dbs_depth_below_surface +// https://wiki.openseamap.org/wiki/OpenSeaMap-dev:NMEA#DBS_-_Depth_below_surface // // Format: $--DBS,x.x,f,x.x,M,x.x,F*hh // Example: $23DBS,01.9,f,0.58,M,00.3,F*21 type DBS struct { BaseSentence - DepthFeet float64 - DepthMeters float64 - DepthFathoms float64 + DepthFeet float64 // Depth, feet + DepthFeetUnit string // f = feet + DepthMeters float64 // Depth, meters + DepthMeterUnit string // M = meters + DepthFathoms float64 // Depth, Fathoms + DepthFathomUnit string // F = Fathoms } // newDBS constructor @@ -22,9 +26,12 @@ func newDBS(s BaseSentence) (DBS, error) { p := NewParser(s) p.AssertType(TypeDBS) return DBS{ - BaseSentence: s, - DepthFeet: p.Float64(0, "depth_feet"), - DepthMeters: p.Float64(2, "depth_meters"), - DepthFathoms: p.Float64(4, "depth_fathoms"), + BaseSentence: s, + DepthFeet: p.Float64(0, "depth feet"), + DepthFeetUnit: p.EnumString(1, "depth feet unit", DistanceUnitFeet), + DepthMeters: p.Float64(2, "depth meters"), + DepthMeterUnit: p.EnumString(3, "depth feet unit", DistanceUnitMetre), + DepthFathoms: p.Float64(4, "depth fathoms"), + DepthFathomUnit: p.EnumString(5, "depth fathom unit", DistanceUnitFathom), }, p.Err() } diff --git a/dbs_test.go b/dbs_test.go index 80e6cf9..0f28b71 100644 --- a/dbs_test.go +++ b/dbs_test.go @@ -6,38 +6,44 @@ import ( "github.com/stretchr/testify/assert" ) -var dbstests = []struct { - name string - raw string - err string - msg DBS -}{ - { - name: "good sentence", - raw: "$23DBS,01.9,f,0.58,M,00.3,F*21", - msg: DBS{ - DepthFeet: 1.9, - DepthMeters: 0.58, - DepthFathoms: 0.3, +func TestDBS(t *testing.T) { + var dbstests = []struct { + name string + raw string + err string + msg DBS + }{ + { + name: "good sentence", + raw: "$23DBS,01.9,f,0.58,M,00.3,F*21", + msg: DBS{ + DepthFeet: 1.9, + DepthFeetUnit: DistanceUnitFeet, + DepthMeters: 0.58, + DepthMeterUnit: DistanceUnitMetre, + DepthFathoms: 0.3, + DepthFathomUnit: DistanceUnitFathom, + }, }, - }, - { - name: "good sentence 2", - raw: "$SDDBS,,,0187.5,M,,*1A", // Simrad ITI Trawl System - msg: DBS{ - DepthFeet: 0, - DepthMeters: 187.5, - DepthFathoms: 0, + { + name: "good sentence 2", + raw: "$SDDBS,,,0187.5,M,,*1A", // Simrad ITI Trawl System + msg: DBS{ + DepthFeet: 0, + DepthFeetUnit: "", + DepthMeters: 187.5, + DepthMeterUnit: DistanceUnitMetre, + DepthFathoms: 0, + DepthFathomUnit: "", + }, }, - }, - { - name: "bad validity", - raw: "$23DBS,01.9,f,0.58,M,00.3,F*25", - err: "nmea: sentence checksum mismatch [21 != 25]", - }, -} + { + name: "bad validity", + raw: "$23DBS,01.9,f,0.58,M,00.3,F*25", + err: "nmea: sentence checksum mismatch [21 != 25]", + }, + } -func TestDBS(t *testing.T) { for _, tt := range dbstests { t.Run(tt.name, func(t *testing.T) { m, err := Parse(tt.raw) diff --git a/dor.go b/dor.go new file mode 100644 index 0000000..40fbe75 --- /dev/null +++ b/dor.go @@ -0,0 +1,97 @@ +package nmea + +const ( + // TypeDOR type of DOR sentence for Door Status Detection + TypeDOR = "DOR" + + // TypeSingleDoorDOR is type for single door related event + TypeSingleDoorDOR = "E" + // TypeFaultDOR is type for fault with door + TypeFaultDOR = "F" + // TypeSectionDOR is type for section of doors related event + TypeSectionDOR = "S" + + // DoorStatusOpenDOR is status for open door + DoorStatusOpenDOR = "O" + // DoorStatusClosedDOR is status for closed door + DoorStatusClosedDOR = "C" + // DoorStatusFaultDOR is status for fault with door + DoorStatusFaultDOR = "X" + + // SwitchSettingHarbourModeDOR is setting for Harbour mode (allowed open) + SwitchSettingHarbourModeDOR = "O" + // SwitchSettingSeaModeDOR is setting for Sea mode (ordered closed) + SwitchSettingSeaModeDOR = "C" +) + +// DOR - Door Status Detection +// Source: "Interfacing Voyage Data Recorder Systems, AutroSafe Interactive Fire-Alarm System, 116-P-BSL336/EE, RevA 2007-01-25, +// Autronica Fire and Security AS " (page 32 | p.8.1.4) +// https://product.autronicafire.com/fileshare/fileupload/14251/bsl336_ee.pdf +// +// Format: $FRDOR,a,hhmmss,aa,aa,xxx,xxx,a,a,c--c*hh +// Example: $FRDOR,E,233042,FD,FP,000,010,C,C,Door Closed : TEST FPA Name*4D +type DOR struct { + BaseSentence + + // Type is type of the message + // * E – Single door + // * F – Fault + // * S – Section (whole or part of section) + Type string + + // Time is Event Time + Time Time + + // SystemIndicator is system indicator. Detector system type with 2 char identifier. + // * WT - watertight + // * WS - semi watertight + // * FD - fire door + // * HD - hull door + // * OT - other + // could be more + // https://www.nmea.org/Assets/20190303%20nmea%200183%20talker%20identifier%20mnemonics.pdf + SystemIndicator string + + // DivisionIndicator1 is first division indicator for locating origin detector for this message + DivisionIndicator1 string + + // DivisionIndicator2 is second division indicator for locating origin detector for this message + DivisionIndicator2 int64 + + // DoorNumberOrCount is Door number or activated door count (seems to be field with overloaded meaning) + DoorNumberOrCount int64 + + // DoorStatus is Door status + // * O – Open + // * C – Closed + // * X – Fault + // could be more + DoorStatus string + + // SwitchSetting is Mode switch setting + // * O – Harbour mode (allowed open) + // * C – Sea mode (ordered closed) + SwitchSetting string + + // Message's description text (could be cut to fit max packet length) + Message string +} + +// newDOR constructor +func newDOR(s BaseSentence) (DOR, error) { + p := NewParser(s) + p.AssertType(TypeDOR) + return DOR{ + BaseSentence: s, + Type: p.EnumString(0, "message type", TypeSingleDoorDOR, TypeFaultDOR, TypeSectionDOR), + Time: p.Time(1, "time"), + SystemIndicator: p.String(2, "system indicator"), + DivisionIndicator1: p.String(3, "division indicator 1"), + DivisionIndicator2: p.Int64(4, "division indicator 2"), + DoorNumberOrCount: p.Int64(5, "door number or count"), + DoorStatus: p.EnumString(6, "door state", DoorStatusOpenDOR, DoorStatusClosedDOR, DoorStatusFaultDOR), + SwitchSetting: p.EnumString(7, "switch setting mode", SwitchSettingHarbourModeDOR, SwitchSettingSeaModeDOR), + Message: p.String(8, "message"), + }, p.Err() +} diff --git a/dor_test.go b/dor_test.go new file mode 100644 index 0000000..304841d --- /dev/null +++ b/dor_test.go @@ -0,0 +1,71 @@ +package nmea + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestDOR(t *testing.T) { + var tests = []struct { + name string + raw string + err string + msg DOR + }{ + { + name: "good sentence", + raw: "$FRDOR,E,233042,FD,FP,000,010,C,C,Door Closed : TEST FPA Name*4D", + msg: DOR{ + Type: TypeSingleDoorDOR, + Time: Time{ + Valid: true, + Hour: 23, + Minute: 30, + Second: 42, + Millisecond: 0, + }, + SystemIndicator: "FD", + DivisionIndicator1: "FP", + DivisionIndicator2: 0, + DoorNumberOrCount: 10, + DoorStatus: DoorStatusClosedDOR, + SwitchSetting: SwitchSettingSeaModeDOR, + Message: "Door Closed : TEST FPA Name", + }, + }, + { + name: "invalid nmea: Type", + raw: "$FRDOR,x,233042,FD,FP,000,010,C,C,Door Closed : TEST FPA Name*70", + err: "nmea: FRDOR invalid message type: x", + }, + { + name: "invalid nmea: Time", + raw: "$FRDOR,E,2x3042,FD,FP,000,010,C,C,Door Closed : TEST FPA Name*06", + err: "nmea: FRDOR invalid time: 2x3042", + }, + { + name: "invalid nmea: DoorStatus", + raw: "$FRDOR,E,233042,FD,FP,000,010,_,C,Door Closed : TEST FPA Name*51", + err: "nmea: FRDOR invalid door state: _", + }, + { + name: "invalid nmea: SwitchSetting", + raw: "$FRDOR,E,233042,FD,FP,000,010,C,_,Door Closed : TEST FPA Name*51", + err: "nmea: FRDOR invalid switch setting mode: _", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m, err := Parse(tt.raw) + if tt.err != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.err) + } else { + assert.NoError(t, err) + hdt := m.(DOR) + hdt.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, hdt) + } + }) + } +} diff --git a/dsc.go b/dsc.go new file mode 100644 index 0000000..5f919f5 --- /dev/null +++ b/dsc.go @@ -0,0 +1,134 @@ +package nmea + +import "strings" + +const ( + // TypeDSC type of DSC sentence for Digital Selective Calling Information + TypeDSC = "DSC" + + // AcknowledgementRequestDSC is type for Acknowledge request + AcknowledgementRequestDSC = "R" + // AcknowledgementDSC is type for Acknowledgement + AcknowledgementDSC = "B" + // AcknowledgementNeitherDSC is type for Neither (end of sequence) + AcknowledgementNeitherDSC = "S" +) + +// DSC – Digital Selective Calling Information +// https://opencpn.org/wiki/dokuwiki/doku.php?id=opencpn:opencpn_user_manual:advanced_features:nmea_sentences +// https://web.archive.org/web/20190303170916/http://continuouswave.com/whaler/reference/DSC_Datagrams.html +// http://www.busse-yachtshop.de/pdf/icom-GM600-handbuch.pdf +// https://github.com/mariokonrad/marnav/blob/master/src/marnav/nmea/dsc.cpp (marnav has interesting enums worth checking) +// +// Note: many fields of DSC are conditional with double meaning and we only map raw sentence to fields without any +// logic/checking of those conditions. We could have specific fields if we only knew the rules to populate them. +// +// Format: $--DSC,xx,xxxxxxxxxx,xx,xx,xx,x.x, x.x,xxxxxxxxxx,xx, a,a*hh +// Example: $CDDSC,20,3380400790,00,21,26,1423108312,2021,,,B, E*73 +type DSC struct { + BaseSentence + // Note: all fields are strings even if specified as digits as int can not express "00" and would be 0 which is different + // Source of quotes: https://web.archive.org/web/20190303170916/http://continuouswave.com/whaler/reference/DSC_Datagrams.html + + // FormatSpecifier is Format specifier (2 digits) + // > The call content is first described by a "format specifier" element. The format specifier is explained in + // > ITU-Rec. M.493-13 Section 4, with various symbol codes in the "service command" range of symbols representing + // > various message formats, as shown in Table 3 (by symbol number, then meaning of symbol) as follows: + // > * 102 = selective call to a group of ships in particular geographic area + // > * 112 = distress alert call + // > * 114 = selective call to a group of ships having common interest + // > * 116 = all ships call + // > * 120 = selective call to particular individual station + // > * 123 = selective call to a particular individual using automatic service + FormatSpecifier string + + // Address (10 digits) + Address string + + // Category (2 digits or empty) + // > The call content is next described by a "category element" in Section 6. Again, various symbol codes in the + // > "service command" range of symbols represent various categories, as follows from Table 3 (by symbol number, + // > then meaning of symbol): + // > * 100 = routine + // > * 108 = safety + // > * 110 = urgency + // > * 112 = distress + Category string + + // DistressCauseOrTeleCommand1 is The cause of the distress or first telecommand (2 digits or empty) + // > Nature of Distress is to be encoded, again using Table 3, as follows + // > * 100 = Fire, explosion + // > * 101 = Flooding + // > * 102 = Collision + // > * 103 = Grounding + // > * 104 = Listing, in danger of capsize + // > * 105 = Sinking + // > * 106 = Disabled and adrift + // > * 107 = Undesignated distres + // > * 108 = Abandoning ship + // > * 109 = Piracy/armed robbery attack + // > * 110 = Man overboard + // > * 111 = unassigned symbol; take no action + // > * 112 = EPRIB emission + // > * 113 through 27 = unassigned symbol; take no action + DistressCauseOrTeleCommand1 string + + // CommandTypeOrTeleCommand2 is Type of communication or second telecommand (2 digits) + CommandTypeOrTeleCommand2 string + + // PositionOrCanal is Position (lat+lon) or Canal/frequency (Maximum 16 digits) + // > Distress coordinates are to be encoded five parts, sent as a string of ten digits. The first digit indicates + // > the direction of the latitude and longitude, with "0" for North and East, "1" for North and West, + // > "2" for South and East, and "3" for South and West. The next two digits are the latitude in degrees. + // > The next two digits are the latitude in whole minutes. The next three digits are the longitude in degrees. + // > The next two digits are longitude in whole minutes. + PositionOrCanal string // Position (lat+lon) or Canal/frequency (Maximum 16 digits) + + // TimeOrTelephoneNumber is Time or Telephone Number (Maximum 16 digits) + // > The time in universal coordinated time is to be sent in 24-hour format in two parts, a total of four digits. + // > The first two digits are the hours. The next two are the minutes. + TimeOrTelephoneNumber string + + // MMSI of ship in distress (10 digits or empty) + // > The call content is next described as having a "self-identification" element. This is simply the sending + // > station's MMSI, encoded like the address element. This identifies who sent the message. + MSSI string + + // DistressCause is The cause of the distress (2 digits or empty) + DistressCause string + + // Acknowledgement (R=Acknowledge request, B=Acknowledgement, S=Neither (end of sequence)) + Acknowledgement string + + // Expansion indicator (E or empty) + ExpansionIndicator string +} + +// newDSC constructor +func newDSC(s BaseSentence) (DSC, error) { + p := NewParser(s) + p.AssertType(TypeDSC) + return DSC{ + BaseSentence: s, + FormatSpecifier: p.String(0, "format specifier"), + Address: p.String(1, "address"), + Category: p.String(2, "category"), + DistressCauseOrTeleCommand1: p.String(3, "cause of the distress or first telecommand"), + CommandTypeOrTeleCommand2: p.String(4, "type of communication or second telecommand"), + PositionOrCanal: p.String(5, "position or canal"), + TimeOrTelephoneNumber: p.String(6, "time or telephone"), + MSSI: p.String(7, "MSSI"), + DistressCause: p.String(8, "distress cause"), + Acknowledgement: strings.TrimSpace(p.EnumString( + 9, + "acknowledgement", + AcknowledgementRequestDSC, + " "+AcknowledgementRequestDSC, + AcknowledgementDSC, + " "+AcknowledgementDSC, + AcknowledgementNeitherDSC, + " "+AcknowledgementNeitherDSC, + )), + ExpansionIndicator: p.String(10, "expansion indicator"), + }, p.Err() +} diff --git a/dsc_test.go b/dsc_test.go new file mode 100644 index 0000000..af4a655 --- /dev/null +++ b/dsc_test.go @@ -0,0 +1,86 @@ +package nmea + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestDSC(t *testing.T) { + var tests = []struct { + name string + raw string + err string + msg DSC + }{ + { + name: "good sentence", + raw: "$CDDSC,12,3380400790,12,06,00,1423108312,2019, , , S, E *4a", + msg: DSC{ + FormatSpecifier: "12", + Address: "3380400790", + Category: "12", + DistressCauseOrTeleCommand1: "06", + CommandTypeOrTeleCommand2: "00", + PositionOrCanal: "1423108312", + TimeOrTelephoneNumber: "2019", + MSSI: " ", + DistressCause: " ", + Acknowledgement: "S", + ExpansionIndicator: " E ", + }, + }, + { + name: "good sentence Distress Alert Cancel", + raw: "$CDDSC,12,3381581370,12,06,00,1423108312,0236,3381581370, , S, *20", + msg: DSC{ + FormatSpecifier: "12", + Address: "3381581370", + Category: "12", + DistressCauseOrTeleCommand1: "06", + CommandTypeOrTeleCommand2: "00", + PositionOrCanal: "1423108312", + TimeOrTelephoneNumber: "0236", + MSSI: "3381581370", + DistressCause: " ", + Acknowledgement: "S", + ExpansionIndicator: " ", + }, + }, + { + name: "good sentence Non-Distress Call - Reply to Position Request\n", + raw: "$CDDSC,20,3381581370,00,21,26,1423108312,1902, , , B, E *7B", + msg: DSC{ + FormatSpecifier: "20", + Address: "3381581370", + Category: "00", + DistressCauseOrTeleCommand1: "21", + CommandTypeOrTeleCommand2: "26", + PositionOrCanal: "1423108312", + TimeOrTelephoneNumber: "1902", + MSSI: " ", + DistressCause: " ", + Acknowledgement: "B", + ExpansionIndicator: " E ", + }, + }, + { + name: "invalid nmea: Acknowledgement", + raw: "$CDDSC,20,3380400790,00,21,26,1423108312,2021,,,x, E*69", + err: "nmea: CDDSC invalid acknowledgement: x", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m, err := Parse(tt.raw) + if tt.err != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.err) + } else { + assert.NoError(t, err) + hdt := m.(DSC) + hdt.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, hdt) + } + }) + } +} diff --git a/dse.go b/dse.go new file mode 100644 index 0000000..14b447e --- /dev/null +++ b/dse.go @@ -0,0 +1,78 @@ +package nmea + +import "errors" + +const ( + // TypeDSE type of DSE sentence for Expanded digital selective calling + TypeDSE = "DSE" + + // AcknowledgementAutomaticDSE is type for automatic + AcknowledgementAutomaticDSE = "A" + // AcknowledgementRequestDSE is type for request + AcknowledgementRequestDSE = "R" + // AcknowledgementQueryDSE is type for query + AcknowledgementQueryDSE = "Q" +) + +// DSE – Expanded digital selective calling. Is sentence that follows DSC sentence to provide additional (extended) data. +// https://opencpn.org/wiki/dokuwiki/doku.php?id=opencpn:opencpn_user_manual:advanced_features:nmea_sentences +// http://www.busse-yachtshop.de/pdf/icom-GM600-handbuch.pdf +// +// Format: $CDDSE, x, x, a, xxxxxxxxxx, xx, c--c, .........., xx, c--c*hh +// Example: $CDDSE,1,1,A,3380400790,00,46504437*15 +type DSE struct { + BaseSentence + TotalNumber int64 // total number of sentences, 01 to 99 + Number int64 // number of current sentence, 01 to 99 + Acknowledgement string // Acknowledgement (R=Acknowledge request, B=Acknowledgement, S=Neither (end of sequence)) + MSSI string // MMSI of vessel (10 digits) + DataSets []DSEDataSet +} + +// DSEDataSet is pair of DSE sets of data containing code + its data +type DSEDataSet struct { + // Code is code field, 2 digits + // From OpenCPN wiki: + // > 00–this field of two-digits appears to be the expansion data specifier described in Table 1 of ITU-Rec.M821-1, + // > but with the symbol representation in two-digits instead of three-digits. The leading “1” seems to not be used. + // > (See modified table, above.) This field identifies the data that will follow in the next field. In this message, + // > the data will be “enhanced position resolution.” + Code string + // Data is data field, Enhanced position resolution, Maximum 8 characters, could be empty + // From OpenCPN wiki: + // > 45894494–the data payload, which is eight digits. The first four are the decimal portion of the latitude + // > minutes; the last four are the decimal portion of the longitude minutes. The latitude and longitude whole + // > minutes were sent in the immediately preceding datagram. This is as specified in the ITU-Rec. M.821-1 in + // > section 2.1.2.1 + Data string +} + +// newDSE constructor +func newDSE(s BaseSentence) (DSE, error) { + p := NewParser(s) + p.AssertType(TypeDSE) + dse := DSE{ + BaseSentence: s, + TotalNumber: p.Int64(0, "total number of sentences"), + Number: p.Int64(1, "sentence number"), + Acknowledgement: p.EnumString(2, "acknowledgement", AcknowledgementAutomaticDSE, AcknowledgementRequestDSE, AcknowledgementQueryDSE), + MSSI: p.String(3, "MSSI"), + DataSets: nil, + } + datasetFieldCount := len(p.Fields) - 4 + if datasetFieldCount < 2 { + return dse, errors.New("DSE is missing fields for parsing data sets") + } + if datasetFieldCount%2 != 0 { + return dse, errors.New("DSE data set field count is not exactly dividable by 2") + } + dse.DataSets = make([]DSEDataSet, 0, datasetFieldCount/2) + for i := 0; i < datasetFieldCount; i = i + 2 { + tmp := DSEDataSet{ + Code: p.String(4+i, "data set code"), + Data: p.String(5+i, "data set data"), + } + dse.DataSets = append(dse.DataSets, tmp) + } + return dse, p.Err() +} diff --git a/dse_test.go b/dse_test.go new file mode 100644 index 0000000..71321c0 --- /dev/null +++ b/dse_test.go @@ -0,0 +1,72 @@ +package nmea + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestDSE(t *testing.T) { + var tests = []struct { + name string + raw string + err string + msg DSE + }{ + { + name: "good sentence, single dataset", + raw: "$CDDSE,1,1,A,3380400790,00,46504437*15", + msg: DSE{ + TotalNumber: 1, + Number: 1, + Acknowledgement: AcknowledgementAutomaticDSE, + MSSI: "3380400790", + DataSets: []DSEDataSet{ + {Code: "00", Data: "46504437"}, + }, + }, + }, + { + name: "good sentence, single dataset", + raw: "$CDDSE,1,1,A,3380400790,00,46504437,01,16501437*17", + msg: DSE{ + TotalNumber: 1, + Number: 1, + Acknowledgement: AcknowledgementAutomaticDSE, + MSSI: "3380400790", + DataSets: []DSEDataSet{ + {Code: "00", Data: "46504437"}, + {Code: "01", Data: "16501437"}, + }, + }, + }, + { + name: "invalid nmea: field count", + raw: "$CDDSE,1,1,x,3380400790,46504437*00", + err: "DSE is missing fields for parsing data sets", + }, + { + name: "invalid nmea: data set field count", + raw: "$CDDSE,1,1,A,3380400790,00,46504437,01*38", + err: "DSE data set field count is not exactly dividable by 2", + }, + { + name: "invalid nmea: Acknowledgement", + raw: "$CDDSE,1,1,x,3380400790,00,46504437*2c", + err: "nmea: CDDSE invalid acknowledgement: x", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m, err := Parse(tt.raw) + if tt.err != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.err) + } else { + assert.NoError(t, err) + hdt := m.(DSE) + hdt.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, hdt) + } + }) + } +} diff --git a/eve.go b/eve.go new file mode 100644 index 0000000..0ee4fe0 --- /dev/null +++ b/eve.go @@ -0,0 +1,32 @@ +package nmea + +const ( + // TypeEVE type of EVE sentence for General Event Message + TypeEVE = "EVE" +) + +// EVE - General Event Message +// Source: "Interfacing Voyage Data Recorder Systems, AutroSafe Interactive Fire-Alarm System, 116-P-BSL336/EE, RevA 2007-01-25, +// Autronica Fire and Security AS " (page 34 | p.8.1.5) +// https://product.autronicafire.com/fileshare/fileupload/14251/bsl336_ee.pdf +// +// Format: $FREVE,hhmmss,c--c,c--c*hh +// Example: $FREVE,000001,DZ00513,Fire Alarm On: TEST DZ201 Name*0A +type EVE struct { + BaseSentence + Time Time // Event Time + TagCode string // Tag code + Message string // Event text +} + +// newEVE constructor +func newEVE(s BaseSentence) (EVE, error) { + p := NewParser(s) + p.AssertType(TypeEVE) + return EVE{ + BaseSentence: s, + Time: p.Time(0, "time"), + TagCode: p.String(1, "tag code"), + Message: p.String(2, "event message text"), + }, p.Err() +} diff --git a/eve_test.go b/eve_test.go new file mode 100644 index 0000000..eb130aa --- /dev/null +++ b/eve_test.go @@ -0,0 +1,50 @@ +package nmea + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestEVE(t *testing.T) { + var tests = []struct { + name string + raw string + err string + msg EVE + }{ + { + name: "good sentence", + raw: "$FREVE,000001,DZ00513,Fire Alarm On: TEST DZ201 Name*0A", + msg: EVE{ + Time: Time{ + Valid: true, + Hour: 0, + Minute: 0, + Second: 1, + Millisecond: 0, + }, + TagCode: "DZ00513", + Message: "Fire Alarm On: TEST DZ201 Name", + }, + }, + { + name: "invalid nmea: Time", + raw: "$FREVE,0x0001,DZ00513,Fire Alarm On: TEST DZ201 Name*42", + err: "nmea: FREVE invalid time: 0x0001", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m, err := Parse(tt.raw) + if tt.err != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.err) + } else { + assert.NoError(t, err) + hdt := m.(EVE) + hdt.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, hdt) + } + }) + } +} diff --git a/fir.go b/fir.go new file mode 100644 index 0000000..b5a506c --- /dev/null +++ b/fir.go @@ -0,0 +1,102 @@ +package nmea + +const ( + // TypeFIR type of FIR sentence for Fire Detection + TypeFIR = "FIR" + + // TypeEventOrAlarmFIR is Event, Fire Alarm type + TypeEventOrAlarmFIR = "E" + // TypeFaultFIR is type for Fault + TypeFaultFIR = "F" + // TypeDisablementFIR is type for detector disablement + TypeDisablementFIR = "D" + + // ConditionActivationFIR is activation condition + ConditionActivationFIR = "A" + // ConditionNonActivationFIR is non-activation condition + ConditionNonActivationFIR = "V" + // ConditionUnknownFIR is unknown condition + ConditionUnknownFIR = "X" + + // AlarmStateAcknowledgedFIR is value for alarm acknowledgement + AlarmStateAcknowledgedFIR = "A" + // AlarmStateNotAcknowledgedFIR is value for alarm being not acknowledged + AlarmStateNotAcknowledgedFIR = "V" +) + +// FIR - Fire Detection event with time and location +// Source: "Interfacing Voyage Data Recorder Systems, AutroSafe Interactive Fire-Alarm System, 116-P-BSL336/EE, RevA 2007-01-25, +// Autronica Fire and Security AS " (page 39 | p.8.1.6) +// https://product.autronicafire.com/fileshare/fileupload/14251/bsl336_ee.pdf +// +// Format: $FRFIR,a,hhmmss,aa,aa,xxx,xxx,a,a,c--c*hh +// Example: $FRFIR,E,103000,FD,PT,000,007,A,V,Fire Alarm : TEST PT7 Name TEST DZ2 Name*7A +type FIR struct { + BaseSentence + + // Type is type of the message + // * E – Event, Fire Alarm + // * F – Fault + // * D – Disablement + Type string + + // Time is Event Time + Time Time + + // SystemIndicator is system indicator. Detector system type with 2 char identifier. + // * FD Generic fire detector + // * FH Heat detector + // * FS Smoke detector + // * FD Smoke and heat detector + // * FM Manual call point + // * GD Any gas detector + // * GO Oxygen gas detector + // * GS Hydrogen sulphide gas detector + // * GH Hydro-carbon gas detector + // * SF Sprinkler flow switch + // * SV Sprinkler manual valve release + // * CO CO2 manual release + // * OT Other + SystemIndicator string + + // DivisionIndicator1 is first division indicator for locating origin detector for this message + DivisionIndicator1 string + + // DivisionIndicator2 is second division indicator for locating origin detector for this message + DivisionIndicator2 int64 + + // FireDetectorNumberOrCount is Fire detector number or activated detectors count (seems to be field with overloaded meaning) + FireDetectorNumberOrCount int64 + + // Condition describes the condition triggering current message + // * A – Activation + // * V – Non-activation + // * X – State unknown + Condition string + + // AlarmAckState is Alarm's acknowledge state + // * A – Acknowledged + // * V – Not acknowledged + AlarmAckState string + + // Message's description text (could be cut to fit max packet length) + Message string +} + +// newFIR constructor +func newFIR(s BaseSentence) (FIR, error) { + p := NewParser(s) + p.AssertType(TypeFIR) + return FIR{ + BaseSentence: s, + Type: p.EnumString(0, "message type", TypeEventOrAlarmFIR, TypeFaultFIR, TypeDisablementFIR), + Time: p.Time(1, "time"), + SystemIndicator: p.String(2, "system indicator"), + DivisionIndicator1: p.String(3, "division indicator 1"), + DivisionIndicator2: p.Int64(4, "division indicator 2"), + FireDetectorNumberOrCount: p.Int64(5, "fire detector number or count"), + Condition: p.EnumString(6, "condition", ConditionActivationFIR, ConditionNonActivationFIR, ConditionUnknownFIR), + AlarmAckState: p.EnumString(7, "alarm acknowledgement state", AlarmStateAcknowledgedFIR, AlarmStateNotAcknowledgedFIR), + Message: p.String(8, "message"), + }, p.Err() +} diff --git a/fir_test.go b/fir_test.go new file mode 100644 index 0000000..eb93980 --- /dev/null +++ b/fir_test.go @@ -0,0 +1,71 @@ +package nmea + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestFIR(t *testing.T) { + var tests = []struct { + name string + raw string + err string + msg FIR + }{ + { + name: "good sentence", + raw: "$FRFIR,E,103000,FD,PT,000,007,A,V,Fire Alarm : TEST PT7 Name TEST DZ2 Name*7A", + msg: FIR{ + Type: TypeEventOrAlarmFIR, + Time: Time{ + Valid: true, + Hour: 10, + Minute: 30, + Second: 0, + Millisecond: 0, + }, + SystemIndicator: "FD", + DivisionIndicator1: "PT", + DivisionIndicator2: 0, + FireDetectorNumberOrCount: 7, + Condition: ConditionActivationFIR, + AlarmAckState: AlarmStateNotAcknowledgedFIR, + Message: "Fire Alarm : TEST PT7 Name TEST DZ2 Name", + }, + }, + { + name: "invalid nmea: Type", + raw: "$FRFIR,x,103000,FD,PT,000,007,A,V,Fire Alarm : TEST PT7 Name TEST DZ2 Name*47", + err: "nmea: FRFIR invalid message type: x", + }, + { + name: "invalid nmea: Time", + raw: "$FRFIR,E,1x3000,FD,PT,000,007,A,V,Fire Alarm : TEST PT7 Name TEST DZ2 Name*32", + err: "nmea: FRFIR invalid time: 1x3000", + }, + { + name: "invalid nmea: Condition", + raw: "$FRFIR,E,103000,FD,PT,000,007,_,V,Fire Alarm : TEST PT7 Name TEST DZ2 Name*64", + err: "nmea: FRFIR invalid condition: _", + }, + { + name: "invalid nmea: AlarmAckState", + raw: "$FRFIR,E,103000,FD,PT,000,007,A,_,Fire Alarm : TEST PT7 Name TEST DZ2 Name*73", + err: "nmea: FRFIR invalid alarm acknowledgement state: _", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m, err := Parse(tt.raw) + if tt.err != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.err) + } else { + assert.NoError(t, err) + hdt := m.(FIR) + hdt.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, hdt) + } + }) + } +} diff --git a/hsc.go b/hsc.go new file mode 100644 index 0000000..2f4965b --- /dev/null +++ b/hsc.go @@ -0,0 +1,33 @@ +package nmea + +const ( + // TypeHSC type of HSC sentence for Heading steering command + TypeHSC = "HSC" +) + +// HSC - Heading steering command +// https://gpsd.gitlab.io/gpsd/NMEA.html#_hsc_heading_steering_command +// https://www.tronico.fi/OH6NT/docs/NMEA0183.pdf (page 11) +// +// Format: $--HSC, x.x, T, x.x, M,a*hh +// Example: $FTHSC,40.12,T,39.11,M*5E +type HSC struct { + BaseSentence + TrueHeading float64 // Heading Degrees, True + TrueHeadingType string // T = True + MagneticHeading float64 // Heading Degrees, Magnetic + MagneticHeadingType string // M = Magnetic +} + +// newHSC constructor +func newHSC(s BaseSentence) (HSC, error) { + p := NewParser(s) + p.AssertType(TypeHSC) + return HSC{ + BaseSentence: s, + TrueHeading: p.Float64(0, "true heading"), + TrueHeadingType: p.EnumString(1, "true heading type", HeadingTrue), + MagneticHeading: p.Float64(2, "magnetic heading"), + MagneticHeadingType: p.EnumString(3, "magnetic heading type", HeadingMagnetic), + }, p.Err() +} diff --git a/hsc_test.go b/hsc_test.go new file mode 100644 index 0000000..f6ec2bd --- /dev/null +++ b/hsc_test.go @@ -0,0 +1,60 @@ +package nmea + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestHSC(t *testing.T) { + var tests = []struct { + name string + raw string + err string + msg HSC + }{ + { + name: "good sentence", + raw: "$FTHSC,40.12,T,39.11,M*5E", + msg: HSC{ + TrueHeading: 40.12, + TrueHeadingType: HeadingTrue, + MagneticHeading: 39.11, + MagneticHeadingType: HeadingMagnetic, + }, + }, + { + name: "invalid nmea: TrueHeading", + raw: "$FTHSC,40.1x,T,39.11,M*14", + err: "nmea: FTHSC invalid true heading: 40.1x", + }, + { + name: "invalid nmea: TrueHeadingType", + raw: "$FTHSC,40.12,x,39.11,M*72", + err: "nmea: FTHSC invalid true heading type: x", + }, + { + name: "invalid nmea: MagneticHeading", + raw: "$FTHSC,40.12,T,x,M*02", + err: "nmea: FTHSC invalid magnetic heading: x", + }, + { + name: "invalid nmea: MagneticHeadingType", + raw: "$FTHSC,40.12,T,39.11,x*6b", + err: "nmea: FTHSC invalid magnetic heading type: x", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m, err := Parse(tt.raw) + if tt.err != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.err) + } else { + assert.NoError(t, err) + hdt := m.(HSC) + hdt.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, hdt) + } + }) + } +} diff --git a/mta.go b/mta.go new file mode 100644 index 0000000..9a49793 --- /dev/null +++ b/mta.go @@ -0,0 +1,28 @@ +package nmea + +const ( + // TypeMTA type of MTA sentence for Air Temperature + TypeMTA = "MTA" +) + +// MTA - Air Temperature (obsolete, use XDR instead) +// https://www.nmea.org/Assets/100108_nmea_0183_sentences_not_recommended_for_new_designs.pdf (page 7) +// +// Format: $--MTA,x.x,C*hh +// Example: $IIMTA,13.3,C*04 +type MTA struct { + BaseSentence + Temperature float64 // temperature + Unit string // unit of temperature, should be degrees Celsius +} + +// newMTA constructor +func newMTA(s BaseSentence) (MTA, error) { + p := NewParser(s) + p.AssertType(TypeMTA) + return MTA{ + BaseSentence: s, + Temperature: p.Float64(0, "temperature"), + Unit: p.EnumString(1, "temperature unit", TemperatureCelsius), + }, p.Err() +} diff --git a/mta_test.go b/mta_test.go new file mode 100644 index 0000000..5d202ce --- /dev/null +++ b/mta_test.go @@ -0,0 +1,48 @@ +package nmea + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestMTA(t *testing.T) { + var tests = []struct { + name string + raw string + err string + msg MTA + }{ + { + name: "good sentence", + raw: "$IIMTA,13.3,C*04", + msg: MTA{ + Temperature: 13.3, + Unit: TemperatureCelsius, + }, + }, + { + name: "invalid nmea: Temperature", + raw: "$IIMTA,x.x,C*35", + err: "nmea: IIMTA invalid temperature: x.x", + }, + { + name: "invalid nmea: Unit", + raw: "$IIMTA,13.3,F*01", + err: "nmea: IIMTA invalid temperature unit: F", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m, err := Parse(tt.raw) + if tt.err != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.err) + } else { + assert.NoError(t, err) + hdt := m.(MTA) + hdt.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, hdt) + } + }) + } +} diff --git a/rmb.go b/rmb.go new file mode 100644 index 0000000..562153d --- /dev/null +++ b/rmb.go @@ -0,0 +1,90 @@ +package nmea + +const ( + // TypeRMB type of RMB sentence for recommended minimum navigation information + TypeRMB = "RMB" + + // DataStatusWarningClearRMB means data is OK + DataStatusWarningClearRMB = "A" + // DataStatusWarningSetRMB means warning flag set + DataStatusWarningSetRMB = "V" +) + +// RMB - Recommended Minimum Navigation Information. To be sent by a navigation receiver when a destination waypoint +// is active. Alternative to BOD and BWW sentences. +// https://gpsd.gitlab.io/gpsd/NMEA.html#_rmb_recommended_minimum_navigation_information +// http://aprs.gids.nl/nmea/#rmb +// +// Format: $--RMB,A,x.x,a,c--c,c--c,llll.ll,a,yyyyy.yy,a,x.x,x.x,x.x,A*hh +// Format (NMEA2.3+): $--RMB,A,x.x,a,c--c,c--c,llll.ll,a,yyyyy.yy,a,x.x,x.x,x.x,A,m*hh +// Example: $GPRMB,A,0.66,L,003,004,4917.24,N,12309.57,W,001.3,052.5,000.5,V*0B +type RMB struct { + BaseSentence + + // DataStatus is status of data, + // * A = OK + // * V = Navigation receiver warning + DataStatus string + + // Cross Track error (nautical miles, 9.9 max) + CrossTrackErrorNauticalMiles float64 + + // DirectionToSteer is Direction to steer, + // * L = left + // * R = right + DirectionToSteer string + + // OriginWaypointID is origin (FROM) waypoint ID + OriginWaypointID string + + // DestinationWaypointID is destination (TO) waypoint ID + DestinationWaypointID string + + // DestinationLatitude is destination waypoint latitude + DestinationLatitude float64 + + // DestinationLongitude is destination waypoint longitude + DestinationLongitude float64 + + // RangeToDestinationNauticalMiles is range to destination, nautical miles (999,9 max) + RangeToDestinationNauticalMiles float64 + + // TrueBearingToDestination is true bearing to destination, degrees + TrueBearingToDestination float64 + + // VelocityToDestinationKnots is velocity towards destination, knots + VelocityToDestinationKnots float64 + + // ArrivalStatus is Arrival Status + // * A = arrival circle entered + // * V = not arrived + ArrivalStatus string + + // FAA mode indicator (filled in NMEA 2.3 and later) + FFAMode string +} + +// newRMB constructor +func newRMB(s BaseSentence) (RMB, error) { + p := NewParser(s) + p.AssertType(TypeRMB) + rmb := RMB{ + BaseSentence: s, + DataStatus: p.EnumString(0, "data status", DataStatusWarningClearRMB, DataStatusWarningSetRMB), + CrossTrackErrorNauticalMiles: p.Float64(1, "cross track error"), + DirectionToSteer: p.EnumString(2, "direction to steer", Left, Right), + OriginWaypointID: p.String(3, "origin waypoint ID"), + DestinationWaypointID: p.String(4, "destination waypoint ID"), + DestinationLatitude: p.LatLong(5, 6, "latitude"), + DestinationLongitude: p.LatLong(7, 8, "latitude"), + RangeToDestinationNauticalMiles: p.Float64(9, "range to destination"), + TrueBearingToDestination: p.Float64(10, "true bearing to destination"), + VelocityToDestinationKnots: p.Float64(11, "velocity to destination"), + ArrivalStatus: p.EnumString(12, "arrival status", WPStatusArrivalCircleEnteredA, WPStatusArrivalCircleEnteredV), + FFAMode: "", + } + if len(p.Fields) > 13 { + rmb.FFAMode = p.String(13, "FAA mode") // not enum because some devices have proprietary "non-nmea" values + } + return rmb, p.Err() +} diff --git a/rmb_test.go b/rmb_test.go new file mode 100644 index 0000000..21a7b62 --- /dev/null +++ b/rmb_test.go @@ -0,0 +1,111 @@ +package nmea + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestRMB(t *testing.T) { + var tests = []struct { + name string + raw string + err string + msg RMB + }{ + { + name: "good sentence", + raw: "$GPRMB,A,0.66,L,003,004,4917.24,N,12309.57,W,001.3,052.5,000.5,V*20", + msg: RMB{ + DataStatus: DataStatusWarningClearRMB, + CrossTrackErrorNauticalMiles: 0.66, + DirectionToSteer: Left, + OriginWaypointID: "003", + DestinationWaypointID: "004", + DestinationLatitude: 49.28733333333333, + DestinationLongitude: -123.1595, + RangeToDestinationNauticalMiles: 1.3, + TrueBearingToDestination: 52.5, + VelocityToDestinationKnots: 0.5, + ArrivalStatus: WPStatusArrivalCircleEnteredV, + FFAMode: "", + }, + }, + { + name: "good sentence with FAAMode", + raw: "$GPRMB,A,0.66,L,003,004,4917.24,N,12309.57,W,001.3,052.5,000.5,V,D*48", + msg: RMB{ + DataStatus: DataStatusWarningClearRMB, + CrossTrackErrorNauticalMiles: 0.66, + DirectionToSteer: Left, + OriginWaypointID: "003", + DestinationWaypointID: "004", + DestinationLatitude: 49.28733333333333, + DestinationLongitude: -123.1595, + RangeToDestinationNauticalMiles: 1.3, + TrueBearingToDestination: 52.5, + VelocityToDestinationKnots: 0.5, + ArrivalStatus: WPStatusArrivalCircleEnteredV, + FFAMode: FAAModeDifferential, + }, + }, + { + name: "invalid nmea: DataStatus", + raw: "$GPRMB,x,0.66,L,003,004,4917.24,N,12309.57,W,001.3,052.5,000.5,V,D*71", + err: "nmea: GPRMB invalid data status: x", + }, + { + name: "invalid nmea: CrossTrackErrorNauticalMiles", + raw: "$GPRMB,A,x.66,L,003,004,4917.24,N,12309.57,W,001.3,052.5,000.5,V,D*00", + err: "nmea: GPRMB invalid cross track error: x.66", + }, + { + name: "invalid nmea: DirectionToSteer", + raw: "$GPRMB,A,0.66,x,003,004,4917.24,N,12309.57,W,001.3,052.5,000.5,V,D*7C", + err: "nmea: GPRMB invalid direction to steer: x", + }, + { + name: "invalid nmea: DestinationLatitude", + raw: "$GPRMB,A,0.66,L,003,004,4x17.24,N,12309.57,W,001.3,052.5,000.5,V,D*09", + err: "nmea: GPRMB invalid latitude: cannot parse [4x17.24 N], unknown format", + }, + { + name: "invalid nmea: DestinationLongitude", + raw: "$GPRMB,A,0.66,L,003,004,4917.24,N,12x09.57,W,001.3,052.5,000.5,V,D*03", + err: "nmea: GPRMB invalid latitude: cannot parse [12x09.57 W], unknown format", + }, + { + name: "invalid nmea: RangeToDestinationNauticalMiles", + raw: "$GPRMB,A,0.66,L,003,004,4917.24,N,12309.57,W,x01.3,052.5,000.5,V,D*00", + err: "nmea: GPRMB invalid range to destination: x01.3", + }, + { + name: "invalid nmea: TrueBearingToDestination", + raw: "$GPRMB,A,0.66,L,003,004,4917.24,N,12309.57,W,001.3,052.x,000.5,V,D*05", + err: "nmea: GPRMB invalid true bearing to destination: 052.x", + }, + { + name: "invalid nmea: VelocityToDestinationKnots", + raw: "$GPRMB,A,0.66,L,003,004,4917.24,N,12309.57,W,001.3,052.5,000.x,V,D*05", + err: "nmea: GPRMB invalid velocity to destination: 000.x", + }, + { + name: "invalid nmea: ArrivalStatus", + raw: "$GPRMB,A,0.66,L,003,004,4917.24,N,12309.57,W,001.3,052.5,000.5,x,D*66", + err: "nmea: GPRMB invalid arrival status: x", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m, err := Parse(tt.raw) + if tt.err != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.err) + } else { + assert.NoError(t, err) + hdt := m.(RMB) + hdt.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, hdt) + } + }) + } +} diff --git a/rpm.go b/rpm.go new file mode 100644 index 0000000..1c1bc33 --- /dev/null +++ b/rpm.go @@ -0,0 +1,39 @@ +package nmea + +const ( + // TypeRPM type of RPM sentence for Engine or Shaft revolutions and pitch + TypeRPM = "RPM" + + // SourceEngineRPM is value for case when source is Engine + SourceEngineRPM = "E" + // SourceShaftRPM is value for case when source is Shaft + SourceShaftRPM = "S" +) + +// RPM - Engine or Shaft revolutions and pitch +// https://gpsd.gitlab.io/gpsd/NMEA.html#_rpm_revolutions +// +// Format: $--RPM,a,x,x.x,x.x,A*hh +// Example: $RCRPM,S,0,74.6,30.0,A*56 +type RPM struct { + BaseSentence + Source string // Source, S = Shaft, E = Engine + EngineNumber int64 // Engine or shaft number + SpeedRPM float64 // Speed, Revolutions per minute + PitchPercent float64 // Propeller pitch, % of maximum, "-" means astern + Status string // Status, A = Valid, V = Invalid +} + +// newRPM constructor +func newRPM(s BaseSentence) (RPM, error) { + p := NewParser(s) + p.AssertType(TypeRPM) + return RPM{ + BaseSentence: s, + Source: p.EnumString(0, "source", SourceEngineRPM, SourceShaftRPM), + EngineNumber: p.Int64(1, "engine number"), + SpeedRPM: p.Float64(2, "speed"), + PitchPercent: p.Float64(3, "pitch"), + Status: p.EnumString(4, "status", StatusValid, StatusInvalid), + }, p.Err() +} diff --git a/rpm_test.go b/rpm_test.go new file mode 100644 index 0000000..65dde8d --- /dev/null +++ b/rpm_test.go @@ -0,0 +1,51 @@ +package nmea + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestRPM(t *testing.T) { + var tests = []struct { + name string + raw string + err string + msg RPM + }{ + { + name: "good sentence", + raw: "$RCRPM,S,0,74.6,30.0,A*56", + msg: RPM{ + Source: SourceShaftRPM, + EngineNumber: 0, + SpeedRPM: 74.6, + PitchPercent: 30, + Status: StatusValid, + }, + }, + { + name: "invalid nmea: Source", + raw: "$RCRPM,x,0,74.6,30.0,A*7D", + err: "nmea: RCRPM invalid source: x", + }, + { + name: "invalid nmea: Status", + raw: "$RCRPM,S,0,74.6,30.0,x*6F", + err: "nmea: RCRPM invalid status: x", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m, err := Parse(tt.raw) + if tt.err != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.err) + } else { + assert.NoError(t, err) + hdt := m.(RPM) + hdt.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, hdt) + } + }) + } +} diff --git a/rsa.go b/rsa.go new file mode 100644 index 0000000..eb75ea4 --- /dev/null +++ b/rsa.go @@ -0,0 +1,32 @@ +package nmea + +const ( + // TypeRSA type of RSA sentence for Rudder Sensor Angle + TypeRSA = "RSA" +) + +// RSA - Rudder Sensor Angle +// https://gpsd.gitlab.io/gpsd/NMEA.html#_rsa_rudder_sensor_angle +// +// Format: $--RSA,x.x,A,x.x,A*hh +// Example: $IIRSA,10.5,A,,V*4D +type RSA struct { + BaseSentence + StarboardRudderAngle float64 // Starboard (or single) rudder sensor, "-" means Turn To Port + StarboardRudderAngleStatus string // Status, A = valid, V = Invalid + PortRudderAngle float64 // Port rudder sensor + PortRudderAngleStatus string // Status, A = valid, V = Invalid +} + +// newRSA constructor +func newRSA(s BaseSentence) (RSA, error) { + p := NewParser(s) + p.AssertType(TypeRSA) + return RSA{ + BaseSentence: s, + StarboardRudderAngle: p.Float64(0, "starboard rudder angle"), + StarboardRudderAngleStatus: p.EnumString(1, "starboard rudder angle status", StatusValid, StatusInvalid), + PortRudderAngle: p.Float64(2, "port rudder angle"), + PortRudderAngleStatus: p.EnumString(3, "port rudder angle status", StatusValid, StatusInvalid), + }, p.Err() +} diff --git a/rsa_test.go b/rsa_test.go new file mode 100644 index 0000000..8fa5ee7 --- /dev/null +++ b/rsa_test.go @@ -0,0 +1,60 @@ +package nmea + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestRSA(t *testing.T) { + var tests = []struct { + name string + raw string + err string + msg RSA + }{ + { + name: "good sentence 1", + raw: "$IIRSA,10.5,A,0.4,A*70", + msg: RSA{ + StarboardRudderAngle: 10.5, + StarboardRudderAngleStatus: StatusValid, + PortRudderAngle: 0.4, + PortRudderAngleStatus: StatusValid, + }, + }, + { + name: "good sentence 2", + raw: "$IIRSA,10.5,A,,V*4D", + msg: RSA{ + StarboardRudderAngle: 10.5, + StarboardRudderAngleStatus: StatusValid, + PortRudderAngle: 0, + PortRudderAngleStatus: StatusInvalid, + }, + }, + { + name: "invalid nmea: StarboardRudderAngleStatus", + raw: "$IIRSA,10.5,x,,V*74", + err: "nmea: IIRSA invalid starboard rudder angle status: x", + }, + { + name: "invalid nmea: PortRudderAngleStatus", + raw: "$IIRSA,10.5,A,,x*63", + err: "nmea: IIRSA invalid port rudder angle status: x", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m, err := Parse(tt.raw) + if tt.err != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.err) + } else { + assert.NoError(t, err) + hdt := m.(RSA) + hdt.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, hdt) + } + }) + } +} diff --git a/sentence.go b/sentence.go index 503f279..5f29bb2 100644 --- a/sentence.go +++ b/sentence.go @@ -189,6 +189,32 @@ func Parse(raw string) (Sentence, error) { switch s.Type { case TypeRMC: return newRMC(s) + case TypeAAM: + return newAAM(s) + case TypeALA: + return newALA(s) + case TypeAPB: + return newAPB(s) + case TypeBEC: + return newBEC(s) + case TypeBOD: + return newBOD(s) + case TypeBWC: + return newBWC(s) + case TypeBWR: + return newBWR(s) + case TypeBWW: + return newBWW(s) + case TypeDOR: + return newDOR(s) + case TypeDSE: + return newDSE(s) + case TypeDSC: + return newDSC(s) + case TypeEVE: + return newEVE(s) + case TypeFIR: + return newFIR(s) case TypeGGA: return newGGA(s) case TypeGSA: @@ -215,6 +241,8 @@ func Parse(raw string) (Sentence, error) { return newHDT(s) case TypeHDM: return newHDM(s) + case TypeHSC: + return newHSC(s) case TypeGNS: return newGNS(s) case TypeTHS: @@ -223,26 +251,50 @@ func Parse(raw string) (Sentence, error) { return newTXT(s) case TypeWPL: return newWPL(s) + case TypeRMB: + return newRMB(s) + case TypeRPM: + return newRPM(s) + case TypeRSA: + return newRSA(s) case TypeRTE: return newRTE(s) case TypeROT: return newROT(s) + case TypeVDR: + return newVDR(s) case TypeVHW: return newVHW(s) + case TypeVPW: + return newVPW(s) + case TypeVLW: + return newVLW(s) + case TypeVWR: + return newVWR(s) + case TypeVWT: + return newVWT(s) case TypeDPT: return newDPT(s) case TypeDBT: return newDBT(s) + case TypeDBK: + return newDBK(s) case TypeDBS: return newDBS(s) case TypeMDA: return newMDA(s) + case TypeMTA: + return newMTA(s) case TypeMTW: return newMTW(s) case TypeMWD: return newMWD(s) case TypeMWV: return newMWV(s) + case TypeXDR: + return newXDR(s) + case TypeXTE: + return newXTE(s) } } if strings.HasPrefix(s.Raw, SentenceStartEncapsulated) { diff --git a/types.go b/types.go index 8898f8f..eef691c 100644 --- a/types.go +++ b/types.go @@ -12,6 +12,91 @@ import ( "unicode" ) +const ( + // StatusValid indicated status having valid value + StatusValid = "A" + // StatusInvalid indicated status having invalid value + StatusInvalid = "V" +) + +const ( + // UnitAmpere is unit for current in Amperes + UnitAmpere = "A" + // UnitBars is unit for pressure in Bars + UnitBars = "B" + // UnitBinary is unit for binary data + UnitBinary = "B" + // UnitCelsius is unit for temperature in Celsius + UnitCelsius = TemperatureCelsius + // UnitFahrenheit is unit for temperature in Fahrenheit + UnitFahrenheit = TemperatureFahrenheit + // UnitDegrees is unit for angular displacement in Degrees + UnitDegrees = "D" + // UnitHertz is unit for frequency in Hertz + UnitHertz = "H" + // UnitLitresPerSecond is unit for volumetric flow in Litres per second + UnitLitresPerSecond = "I" + // UnitKelvin is unit of temperature in Kelvin + UnitKelvin = TemperatureKelvin + // UnitKilogramPerCubicMetre is unit of density in kilogram per cubic metre + UnitKilogramPerCubicMetre = "K" + // UnitMeters is unit of distance in Meters + UnitMeters = DistanceUnitMetre + // UnitCubicMeters is unit of volume in cubic meters + UnitCubicMeters = "M" + // UnitRevolutionsPerMinute is unit of rotational speed or the frequency of rotation around a fixed axis in revolutions per minute (RPM) + UnitRevolutionsPerMinute = "R" + // UnitPercent is percent of full range + UnitPercent = "P" + // UnitPascal is unit of pressure in Pascals + UnitPascal = "P" + // UnitPartsPerThousand is in parts-per notation set of pseudo-unit to describe small values of miscellaneous dimensionless quantities, e.g. mole fraction or mass fraction. + UnitPartsPerThousand = "S" + // UnitVolts is unit of voltage in Volts + UnitVolts = "V" +) + +const ( + // SpeedKnots is a unit of speed equal to one nautical mile per hour, exactly 1.852 km/h (approximately 1.151 mph or 0.514 m/s) + SpeedKnots = "N" + // SpeedMeterPerSecond is unit of speed of 1 meter per second + SpeedMeterPerSecond = "M" + // SpeedKilometerPerHour is unit of speed of 1 kilometer per hour + SpeedKilometerPerHour = "K" +) + +const ( + // TemperatureCelsius is unit of temperature measured in celsius. °C = (°F − 32) / 1,8 + TemperatureCelsius = "C" + // TemperatureFahrenheit is unit of temperature measured in fahrenheits. °F = °C * 1,8 + 32 + TemperatureFahrenheit = "F" + // TemperatureKelvin is unit of temperature measured in kelvins. K = °C + 273,15 + TemperatureKelvin = "K" +) + +// In navigation, the heading of a vessel or object is the compass direction in which the craft's bow or nose is pointed. +// Note that the heading may not necessarily be the direction that the vehicle actually travels, which is known as +// its course or track. +// https://en.wikipedia.org/wiki/Heading_(navigation) +const ( + // HeadingMagnetic - Magnetic heading is your direction relative to magnetic north, read from your magnetic compass. + // Magnetic north is the point on the Earth's surface where its magnetic field points directly downwards. + HeadingMagnetic = "M" + // HeadingTrue - True heading is your direction relative to true north, or the geographic north pole. + // True north is the northern axis of rotation of the Earth. It is the point where the lines of longitude converge + // on maps. + HeadingTrue = "T" +) + +// In nautical navigation the absolute bearing is the clockwise angle between north and an object observed from the vessel. +// https://en.wikipedia.org/wiki/Bearing_(angle) +const ( + // BearingMagnetic is the clockwise angle between Earth's magnetic north and an object observed from the vessel. + BearingMagnetic = "M" + // BearingTrue is the clockwise angle between Earth's true (geographical) north and an object observed from the vessel. + BearingTrue = "T" +) + // FAAMode is type for FAA mode indicator (NMEA 2.3 and later). // In NMEA 2.3, several sentences (APB, BWC, BWR, GLL, RMA, RMB, RMC, VTG, WCV, and XTE) got a new last field carrying // the signal integrity information needed by the FAA. @@ -56,6 +141,21 @@ const ( NavStatusDataValid = "V" ) +const ( + // DistanceUnitKilometre is unit for distance in kilometres (1km = 1000m) + DistanceUnitKilometre = "K" + // DistanceUnitNauticalMile is unit for distance in nautical miles (1nmi = 1852m) + DistanceUnitNauticalMile = "N" + // DistanceUnitStatuteMile is unit for distance in statute miles (1smi = 5,280 feet = 1609.344m) + DistanceUnitStatuteMile = "S" + // DistanceUnitMetre is unit for distance in metres + DistanceUnitMetre = "M" + // DistanceUnitFeet is unit for distance in feets (1f = 0.3048m) + DistanceUnitFeet = "f" + // DistanceUnitFathom is unit for distance in fathoms (1fm = 6ft = 1,8288m) + DistanceUnitFathom = "F" +) + const ( // Degrees value Degrees = '\u00B0' @@ -73,6 +173,10 @@ const ( East = "E" // West value West = "W" + // Left value + Left = "L" + // Right value + Right = "R" ) // ParseLatLong parses the supplied string into the LatLong. diff --git a/vdr.go b/vdr.go new file mode 100644 index 0000000..0c7bbe9 --- /dev/null +++ b/vdr.go @@ -0,0 +1,38 @@ +package nmea + +const ( + // TypeVDR type of VDR sentence for Set and Drift + TypeVDR = "VDR" +) + +// VDR - Set and Drift +// In navigation, set and drift are characteristics of the current and velocity of water over the ground in which a ship +// is sailing. Set is the bearing the current is flowing. Drift is the magnitude of the current. +// https://gpsd.gitlab.io/gpsd/NMEA.html#_vdr_set_and_drift +// +// Format: $--VDR,x.x,T,x.x,M,x.x,N*hh +// Example: $IIVDR,10.1,T,12.3,M,1.2,N*3A +type VDR struct { + BaseSentence + SetDegreesTrue float64 // Direction degrees, True + SetDegreesTrueUnit string // T = True + SetDegreesMagnetic float64 // Direction degrees, True + SetDegreesMagneticUnit string // M = Magnetic + DriftKnots float64 // Current speed, knots + DriftUnit string // N = Knots +} + +// newVDR constructor +func newVDR(s BaseSentence) (VDR, error) { + p := NewParser(s) + p.AssertType(TypeVDR) + return VDR{ + BaseSentence: s, + SetDegreesTrue: p.Float64(0, "true set degrees"), + SetDegreesTrueUnit: p.EnumString(1, "true set unit", BearingTrue), + SetDegreesMagnetic: p.Float64(2, "magnetic set degrees"), + SetDegreesMagneticUnit: p.EnumString(3, "magnetic set unit", BearingMagnetic), + DriftKnots: p.Float64(4, "drift knots"), + DriftUnit: p.EnumString(5, "drift unit", SpeedKnots), + }, p.Err() +} diff --git a/vdr_test.go b/vdr_test.go new file mode 100644 index 0000000..849a6f1 --- /dev/null +++ b/vdr_test.go @@ -0,0 +1,57 @@ +package nmea + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestVDR(t *testing.T) { + var tests = []struct { + name string + raw string + err string + msg VDR + }{ + { + name: "good sentence", + raw: "$IIVDR,10.1,T,12.3,M,1.2,N*3A", + msg: VDR{ + SetDegreesTrue: 10.1, + SetDegreesTrueUnit: BearingTrue, + SetDegreesMagnetic: 12.3, + SetDegreesMagneticUnit: BearingMagnetic, + DriftKnots: 1.2, + DriftUnit: SpeedKnots, + }, + }, + { + name: "invalid nmea: SetDegreesTrueUnit", + raw: "$IIVDR,10.1,x,12.3,M,1.2,N*16", + err: "nmea: IIVDR invalid true set unit: x", + }, + { + name: "invalid nmea: SetDegreesMagneticUnit", + raw: "$IIVDR,10.1,T,12.3,x,1.2,N*0f", + err: "nmea: IIVDR invalid magnetic set unit: x", + }, + { + name: "invalid nmea: DriftUnit", + raw: "$IIVDR,10.1,T,12.3,M,1.2,x*0c", + err: "nmea: IIVDR invalid drift unit: x", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m, err := Parse(tt.raw) + if tt.err != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.err) + } else { + assert.NoError(t, err) + hdt := m.(VDR) + hdt.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, hdt) + } + }) + } +} diff --git a/vlw.go b/vlw.go new file mode 100644 index 0000000..4e04e2b --- /dev/null +++ b/vlw.go @@ -0,0 +1,46 @@ +package nmea + +const ( + // TypeVLW type of VLW sentence for Distance Traveled through Water + TypeVLW = "VLW" +) + +// VLW - Distance Traveled through Water +// https://gpsd.gitlab.io/gpsd/NMEA.html#_vlw_distance_traveled_through_water +// +// Format: $--VLW,x.x,N,x.x,N*hh +// Format (NMEA 3+): $--VLW,x.x,N,x.x,N,x.x,N,x.x,N*hh +// Example: $IIVLW,10.1,N,3.2,N*7C +// Example: $IIVLW,10.1,N,3.2,N,0,N,0,N*7C +type VLW struct { + BaseSentence + TotalInWater float64 // Total cumulative water distance, nm + TotalInWaterUnit string // N = Nautical Miles + SinceResetInWater float64 // Water distance since Reset, nm + SinceResetInWaterUnit string // N = Nautical Miles + TotalOnGround float64 // Total cumulative ground distance, nm (NMEA 3 and above) + TotalOnGroundUnit string // N = Nautical Miles (NMEA 3 and above) + SinceResetOnGround float64 // Ground distance since reset, nm (NMEA 3 and above) + SinceResetOnGroundUnit string // N = Nautical Miles (NMEA 3 and above) +} + +// newVLW constructor +func newVLW(s BaseSentence) (VLW, error) { + p := NewParser(s) + p.AssertType(TypeVLW) + + vlw := VLW{ + BaseSentence: s, + TotalInWater: p.Float64(0, "total cumulative water distance"), + TotalInWaterUnit: p.EnumString(1, "total cumulative water distance unit", DistanceUnitNauticalMile), + SinceResetInWater: p.Float64(2, "water distance since reset"), + SinceResetInWaterUnit: p.EnumString(3, "water distance since reset unit", DistanceUnitNauticalMile), + } + if len(p.Fields) > 4 { + vlw.TotalOnGround = p.Float64(4, "total cumulative ground distance") + vlw.TotalOnGroundUnit = p.EnumString(5, "total cumulative ground distance unit", DistanceUnitNauticalMile) + vlw.SinceResetOnGround = p.Float64(6, "ground distance since reset") + vlw.SinceResetOnGroundUnit = p.EnumString(7, "ground distance since reset unit", DistanceUnitNauticalMile) + } + return vlw, p.Err() +} diff --git a/vlw_test.go b/vlw_test.go new file mode 100644 index 0000000..3105431 --- /dev/null +++ b/vlw_test.go @@ -0,0 +1,78 @@ +package nmea + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestVLW(t *testing.T) { + var tests = []struct { + name string + raw string + err string + msg VLW + }{ + { + name: "good sentence 1", + raw: "$IIVLW,10.1,N,3.2,N*7C", + msg: VLW{ + TotalInWater: 10.1, + TotalInWaterUnit: "N", + SinceResetInWater: 3.2, + SinceResetInWaterUnit: "N", + TotalOnGround: 0, + TotalOnGroundUnit: "", + SinceResetOnGround: 0, + SinceResetOnGroundUnit: "", + }, + }, + { + name: "good sentence 2", + raw: "$IIVLW,10.1,N,3.2,N,1,N,0.1,N*62", + msg: VLW{ + TotalInWater: 10.1, + TotalInWaterUnit: "N", + SinceResetInWater: 3.2, + SinceResetInWaterUnit: "N", + TotalOnGround: 1, + TotalOnGroundUnit: "N", + SinceResetOnGround: 0.1, + SinceResetOnGroundUnit: "N", + }, + }, + { + name: "invalid nmea: TotalInWaterUnit", + raw: "$IIVLW,10.1,x,3.2,N,1,N,0.1,N*54", + err: "nmea: IIVLW invalid total cumulative water distance unit: x", + }, + { + name: "invalid nmea: SinceResetInWaterUnit", + raw: "$IIVLW,10.1,N,3.2,x,1,N,0.1,N*54", + err: "nmea: IIVLW invalid water distance since reset unit: x", + }, + { + name: "invalid nmea: TotalOnGroundUnit", + raw: "$IIVLW,10.1,N,3.2,N,1,x,0.1,N*54", + err: "nmea: IIVLW invalid total cumulative ground distance unit: x", + }, + { + name: "invalid nmea: SinceResetOnGroundUnit", + raw: "$IIVLW,10.1,N,3.2,N,1,N,0.1,x*54", + err: "nmea: IIVLW invalid ground distance since reset unit: x", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m, err := Parse(tt.raw) + if tt.err != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.err) + } else { + assert.NoError(t, err) + hdt := m.(VLW) + hdt.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, hdt) + } + }) + } +} diff --git a/vpw.go b/vpw.go new file mode 100644 index 0000000..8150b49 --- /dev/null +++ b/vpw.go @@ -0,0 +1,32 @@ +package nmea + +const ( + // TypeVPW type of VPW sentence for Speed Measured Parallel to Wind + TypeVPW = "VPW" +) + +// VPW - Speed Measured Parallel to Wind +// https://gpsd.gitlab.io/gpsd/NMEA.html#_vpw_speed_measured_parallel_to_wind +// +// Format: $--VPW,x.x,N,x.x,M*hh +// Example: $IIVPW,4.5,N,6.7,M*52 +type VPW struct { + BaseSentence + SpeedKnots float64 // Speed, "-" means downwind, knots + SpeedKnotsUnit string // N = knots + SpeedMPS float64 // Speed, "-" means downwind, m/s + SpeedMPSUnit string // M = m/s +} + +// newVPW constructor +func newVPW(s BaseSentence) (VPW, error) { + p := NewParser(s) + p.AssertType(TypeVPW) + return VPW{ + BaseSentence: s, + SpeedKnots: p.Float64(0, "wind speed in knots"), + SpeedKnotsUnit: p.EnumString(1, "wind speed in knots unit", SpeedKnots), + SpeedMPS: p.Float64(2, "wind speed in meters per second"), + SpeedMPSUnit: p.EnumString(3, "wind speed in meters per second unit", SpeedMeterPerSecond), + }, p.Err() +} diff --git a/vpw_test.go b/vpw_test.go new file mode 100644 index 0000000..d4802c8 --- /dev/null +++ b/vpw_test.go @@ -0,0 +1,50 @@ +package nmea + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestVPW(t *testing.T) { + var tests = []struct { + name string + raw string + err string + msg VPW + }{ + { + name: "good sentence", + raw: "$IIVPW,4.5,N,6.7,M*52", + msg: VPW{ + SpeedKnots: 4.5, + SpeedKnotsUnit: SpeedKnots, + SpeedMPS: 6.7, + SpeedMPSUnit: SpeedMeterPerSecond, + }, + }, + { + name: "invalid nmea: SpeedKnotsUnit", + raw: "$IIVPW,4.5,x,6.7,M*64", + err: "nmea: IIVPW invalid wind speed in knots unit: x", + }, + { + name: "invalid nmea: SpeedMPSUnit", + raw: "$IIVPW,4.5,N,6.7,x*67", + err: "nmea: IIVPW invalid wind speed in meters per second unit: x", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m, err := Parse(tt.raw) + if tt.err != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.err) + } else { + assert.NoError(t, err) + hdt := m.(VPW) + hdt.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, hdt) + } + }) + } +} diff --git a/vwr.go b/vwr.go new file mode 100644 index 0000000..4a7993c --- /dev/null +++ b/vwr.go @@ -0,0 +1,44 @@ +package nmea + +const ( + // TypeVWR type of VWR sentence for Relative Wind Speed and Angle + TypeVWR = "VWR" +) + +// VWR - Relative Wind Speed and Angle. Speed is measured relative to the moving vessel. +// According to NMEA: use of $--MWV is recommended. +// https://gpsd.gitlab.io/gpsd/NMEA.html#_vwr_relative_wind_speed_and_angle +// https://www.nmea.org/Assets/100108_nmea_0183_sentences_not_recommended_for_new_designs.pdf (page 16) +// +// Format: $--VWR,x.x,a,x.x,N,x.x,M,x.x,K*hh +// Example: $IIVWR,75,R,1.0,N,0.51,M,1.85,K*6C +// $IIVWR,024,L,018,N,,,,*5e +// $IIVWR,,,,,,,,*53 +type VWR struct { + BaseSentence + MeasuredAngle float64 // Measured Wind direction magnitude in degrees (0 to 180 deg) + MeasuredDirectionBow string // Measured Wind direction Left/Right of bow + SpeedKnots float64 // Measured wind Speed, knots + SpeedKnotsUnit string // N = knots + SpeedMPS float64 // Wind speed, meters/second + SpeedMPSUnit string // M = m/s + SpeedKPH float64 // Wind speed, km/hour + SpeedKPHUnit string // M = km/h +} + +// newVWR constructor +func newVWR(s BaseSentence) (VWR, error) { + p := NewParser(s) + p.AssertType(TypeVWR) + return VWR{ + BaseSentence: s, + MeasuredAngle: p.Float64(0, "measured wind angle"), + MeasuredDirectionBow: p.EnumString(1, "measured wind direction to bow", Left, Right), + SpeedKnots: p.Float64(2, "wind speed in knots"), + SpeedKnotsUnit: p.EnumString(3, "wind speed in knots unit", SpeedKnots), + SpeedMPS: p.Float64(4, "wind speed in meters per second"), + SpeedMPSUnit: p.EnumString(5, "wind speed in meters per second unit", SpeedMeterPerSecond), + SpeedKPH: p.Float64(6, "wind speed in kilometers per hour"), + SpeedKPHUnit: p.EnumString(7, "wind speed in kilometers per hour unit", SpeedKilometerPerHour), + }, p.Err() +} diff --git a/vwr_test.go b/vwr_test.go new file mode 100644 index 0000000..034aa32 --- /dev/null +++ b/vwr_test.go @@ -0,0 +1,92 @@ +package nmea + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestVWR(t *testing.T) { + var tests = []struct { + name string + raw string + err string + msg VWR + }{ + { // these examples are from SignalK + name: "good sentence", + raw: "$IIVWR,75,R,1.0,N,0.51,M,1.85,K*6C", + msg: VWR{ + MeasuredAngle: 75, + MeasuredDirectionBow: Right, + SpeedKnots: 1, + SpeedKnotsUnit: SpeedKnots, + SpeedMPS: 0.51, + SpeedMPSUnit: SpeedMeterPerSecond, + SpeedKPH: 1.85, + SpeedKPHUnit: SpeedKilometerPerHour, + }, + }, + { + name: "good sentence, shorter but still valid", + raw: "$IIVWR,024,L,018,N,,,,*5e", + msg: VWR{ + MeasuredAngle: 24, + MeasuredDirectionBow: Left, + SpeedKnots: 18, + SpeedKnotsUnit: SpeedKnots, + SpeedMPS: 0, + SpeedMPSUnit: "", + SpeedKPH: 0, + SpeedKPHUnit: "", + }, + }, + { + name: "good sentence, handle empty values", + raw: "$IIVWR,,,,,,,,*53", + msg: VWR{ + MeasuredAngle: 0, + MeasuredDirectionBow: "", + SpeedKnots: 0, + SpeedKnotsUnit: "", + SpeedMPS: 0, + SpeedMPSUnit: "", + SpeedKPH: 0, + SpeedKPHUnit: "", + }, + }, + { + name: "invalid nmea: DirectionBow", + raw: "$IIVWR,75,x,1.0,N,0.51,M,1.85,K*46", + err: "nmea: IIVWR invalid measured wind direction to bow: x", + }, + { + name: "invalid nmea: SpeedKnotsUnit", + raw: "$IIVWR,75,R,1.0,x,0.51,M,1.85,K*5a", + err: "nmea: IIVWR invalid wind speed in knots unit: x", + }, + { + name: "invalid nmea: SpeedMPSUnit", + raw: "$IIVWR,75,R,1.0,N,0.51,x,1.85,K*59", + err: "nmea: IIVWR invalid wind speed in meters per second unit: x", + }, + { + name: "invalid nmea: SpeedKPHUnit", + raw: "$IIVWR,75,R,1.0,N,0.51,M,1.85,x*5f", + err: "nmea: IIVWR invalid wind speed in kilometers per hour unit: x", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m, err := Parse(tt.raw) + if tt.err != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.err) + } else { + assert.NoError(t, err) + hdt := m.(VWR) + hdt.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, hdt) + } + }) + } +} diff --git a/vwt.go b/vwt.go new file mode 100644 index 0000000..df8371e --- /dev/null +++ b/vwt.go @@ -0,0 +1,42 @@ +package nmea + +const ( + // TypeVWT type of VWT sentence for True Wind Speed and Angle + TypeVWT = "VWT" +) + +// VWT - True Wind Speed and Angle +// https://www.nmea.org/Assets/100108_nmea_0183_sentences_not_recommended_for_new_designs.pdf +// https://www.rubydoc.info/gems/nmea_plus/1.0.20/NMEAPlus/Message/NMEA/VWT +// https://lists.gnu.org/archive/html/gpsd-dev/2012-04/msg00048.html +// +// Format: $--VWT,x.x,a,x.x,N,x.x,M,x.x,K*hh +// Example: $IIVWT,75,x,1.0,N,0.51,M,1.85,K*40 +type VWT struct { + BaseSentence + TrueAngle float64 // true Wind direction magnitude in degrees (0 to 180 deg) + TrueDirectionBow string // true Wind direction Left/Right of bow + SpeedKnots float64 // true wind Speed, knots + SpeedKnotsUnit string // N = knots + SpeedMPS float64 // Wind speed, meters/second + SpeedMPSUnit string // M = m/s + SpeedKPH float64 // Wind speed, km/hour + SpeedKPHUnit string // M = km/h +} + +// newVWT constructor +func newVWT(s BaseSentence) (VWT, error) { + p := NewParser(s) + p.AssertType(TypeVWT) + return VWT{ + BaseSentence: s, + TrueAngle: p.Float64(0, "true wind angle"), + TrueDirectionBow: p.EnumString(1, "true wind direction to bow", Left, Right), + SpeedKnots: p.Float64(2, "wind speed in knots"), + SpeedKnotsUnit: p.EnumString(3, "wind speed in knots unit", SpeedKnots), + SpeedMPS: p.Float64(4, "wind speed in meters per second"), + SpeedMPSUnit: p.EnumString(5, "wind speed in meters per second unit", SpeedMeterPerSecond), + SpeedKPH: p.Float64(6, "wind speed in kilometers per hour"), + SpeedKPHUnit: p.EnumString(7, "wind speed in kilometers per hour unit", SpeedKilometerPerHour), + }, p.Err() +} diff --git a/vwt_test.go b/vwt_test.go new file mode 100644 index 0000000..3509b1a --- /dev/null +++ b/vwt_test.go @@ -0,0 +1,92 @@ +package nmea + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestVWT(t *testing.T) { + var tests = []struct { + name string + raw string + err string + msg VWT + }{ + { // these examples are from SignalK + name: "good sentence", + raw: "$IIVWT,75,R,1.0,N,0.51,M,1.85,K*6A", + msg: VWT{ + TrueAngle: 75, + TrueDirectionBow: Right, + SpeedKnots: 1, + SpeedKnotsUnit: SpeedKnots, + SpeedMPS: 0.51, + SpeedMPSUnit: SpeedMeterPerSecond, + SpeedKPH: 1.85, + SpeedKPHUnit: SpeedKilometerPerHour, + }, + }, + { + name: "good sentence, shorter but still valid", + raw: "$IIVWT,024,L,018,N,,,,*58", + msg: VWT{ + TrueAngle: 24, + TrueDirectionBow: Left, + SpeedKnots: 18, + SpeedKnotsUnit: SpeedKnots, + SpeedMPS: 0, + SpeedMPSUnit: "", + SpeedKPH: 0, + SpeedKPHUnit: "", + }, + }, + { + name: "good sentence, handle empty values", + raw: "$IIVWT,,,,,,,,*55", + msg: VWT{ + TrueAngle: 0, + TrueDirectionBow: "", + SpeedKnots: 0, + SpeedKnotsUnit: "", + SpeedMPS: 0, + SpeedMPSUnit: "", + SpeedKPH: 0, + SpeedKPHUnit: "", + }, + }, + { + name: "invalid nmea: DirectionBow", + raw: "$IIVWT,75,x,1.0,N,0.51,M,1.85,K*40", + err: "nmea: IIVWT invalid true wind direction to bow: x", + }, + { + name: "invalid nmea: SpeedKnotsUnit", + raw: "$IIVWT,75,R,1.0,x,0.51,M,1.85,K*5c", + err: "nmea: IIVWT invalid wind speed in knots unit: x", + }, + { + name: "invalid nmea: SpeedMPSUnit", + raw: "$IIVWT,75,R,1.0,N,0.51,x,1.85,K*5f", + err: "nmea: IIVWT invalid wind speed in meters per second unit: x", + }, + { + name: "invalid nmea: SpeedKPHUnit", + raw: "$IIVWT,75,R,1.0,N,0.51,M,1.85,x*59", + err: "nmea: IIVWT invalid wind speed in kilometers per hour unit: x", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m, err := Parse(tt.raw) + if tt.err != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.err) + } else { + assert.NoError(t, err) + hdt := m.(VWT) + hdt.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, hdt) + } + }) + } +} diff --git a/xdr.go b/xdr.go new file mode 100644 index 0000000..8f0813e --- /dev/null +++ b/xdr.go @@ -0,0 +1,168 @@ +package nmea + +import "errors" + +const ( + // TypeXDR type of XDR sentence for Transducer Measurement + TypeXDR = "XDR" +) + +const ( + // TransducerAngularDisplacementXDR is transducer type for Angular displacement + TransducerAngularDisplacementXDR = "A" + // TransducerTemperatureXDR is transducer type for Temperature + TransducerTemperatureXDR = "C" + // TransducerDepthXDR is transducer type for Depth + TransducerDepthXDR = "D" + // TransducerFrequencyXDR is transducer type for Frequency + TransducerFrequencyXDR = "F" + // TransducerHumidityXDR is transducer type for Humidity + TransducerHumidityXDR = "H" + // TransducerForceXDR is transducer type for Force + TransducerForceXDR = "N" + // TransducerPressureXDR is transducer type for Pressure + TransducerPressureXDR = "P" + // TransducerFlowXDR is transducer type for Flow + TransducerFlowXDR = "R" + // TransducerAbsoluteHumidityXDR is transducer type for Absolute humidity + TransducerAbsoluteHumidityXDR = "B" + // TransducerGenericXDR is transducer type for Generic + TransducerGenericXDR = "G" + // TransducerCurrentXDR is transducer type for Current + TransducerCurrentXDR = "I" + // TransducerSalinityXDR is transducer type for Salinity + TransducerSalinityXDR = "L" + // TransducerSwitchValveXDR is transducer type for Switch, valve + TransducerSwitchValveXDR = "S" + // TransducerTachometerXDR is transducer type for Tachometer + TransducerTachometerXDR = "T" + // TransducerVoltageXDR is transducer type for Voltage + TransducerVoltageXDR = "U" + // TransducerVolumeXDR is transducer type for Volume + TransducerVolumeXDR = "V" +) + +// XDR - Transducer Measurement +// https://gpsd.gitlab.io/gpsd/NMEA.html#_xdr_transducer_measurement +// https://www.eye4software.com/hydromagic/documentation/articles-and-howtos/handling-nmea0183-xdr/ +// +// Format: $--XDR,a,x.x,a,c--c, ..... *hh +// Example: $HCXDR,A,171,D,PITCH,A,-37,D,ROLL,G,367,,MAGX,G,2420,,MAGY,G,-8984,,MAGZ*41 +// $SDXDR,C,23.15,C,WTHI*70 +type XDR struct { + BaseSentence + Measurements []XDRMeasurement +} + +// XDRMeasurement is measurement recorded by transducer +type XDRMeasurement struct { + // TransducerType is type of transducer + // * A - Angular displacement + // * C - Temperature + // * D - Depth + // * F - Frequency + // * H - Humidity + // * N - Force + // * P - Pressure + // * R - Flow + // * B - Absolute humidity + // * G - Generic + // * I - Current + // * L - Salinity + // * S - Switch, valve + // * T - Tachometer + // * U - Voltage + // * V - Volume + // could be more + TransducerType string + + // Value of measurement + Value float64 + + // Unit of measurement + // * "" - could be empty! + // * A - Amperes + // * B - Bars | Binary + // * C - Celsius + // * D - Degrees + // * H - Hertz + // * I - liters/second + // * K - Kelvin | Density, kg/m3 kilogram per cubic metre + // * M - Meters | Cubic Meters (m3) + // * N - Newton + // * P - percent of full range | Pascal + // * R - RPM + // * S - Parts per thousand + // * V - Volts + // could be more + Unit string + + // TransducerName is name of transducer where measurement was recorded + TransducerName string +} + +// newXDR constructor +func newXDR(s BaseSentence) (XDR, error) { + p := NewParser(s) + p.AssertType(TypeXDR) + + xdr := XDR{ + BaseSentence: s, + Measurements: nil, + } + + if len(p.Fields)%4 != 0 { + return xdr, errors.New("XDR field count is not exactly dividable by 4") + } + + xdr.Measurements = make([]XDRMeasurement, 0, len(s.Fields)/4) + for i := 0; i < len(s.Fields); { + tmp := XDRMeasurement{ + TransducerType: p.EnumString( + i, + "transducer type", + TransducerAngularDisplacementXDR, + TransducerTemperatureXDR, + TransducerDepthXDR, + TransducerFrequencyXDR, + TransducerHumidityXDR, + TransducerForceXDR, + TransducerPressureXDR, + TransducerFlowXDR, + TransducerAbsoluteHumidityXDR, + TransducerGenericXDR, + TransducerCurrentXDR, + TransducerSalinityXDR, + TransducerSwitchValveXDR, + TransducerTachometerXDR, + TransducerVoltageXDR, + TransducerVolumeXDR, + ), + Value: p.Float64(i+1, "measurement value"), + Unit: p.EnumString( + i+2, + "measurement unit", + UnitAmpere, + UnitBars, + UnitBinary, + UnitCelsius, + UnitDegrees, + UnitHertz, + UnitLitresPerSecond, + UnitKelvin, + UnitKilogramPerCubicMetre, + UnitMeters, + UnitCubicMeters, + UnitRevolutionsPerMinute, + UnitPercent, + UnitPascal, + UnitPartsPerThousand, + UnitVolts, + ), + TransducerName: p.String(i+3, "transducer name"), + } + xdr.Measurements = append(xdr.Measurements, tmp) + i += 4 + } + return xdr, p.Err() +} diff --git a/xdr_test.go b/xdr_test.go new file mode 100644 index 0000000..7e37741 --- /dev/null +++ b/xdr_test.go @@ -0,0 +1,77 @@ +package nmea + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestXDR(t *testing.T) { + var tests = []struct { + name string + raw string + err string + msg XDR + }{ + { + name: "good sentence with 1 measurement", + raw: "$SDXDR,C,23.15,C,WTHI*70", + msg: XDR{ + Measurements: []XDRMeasurement{ + { + TransducerType: "C", + Value: 23.15, + Unit: "C", + TransducerName: "WTHI", + }, + }, + }, + }, + { + name: "good sentence with 5 measurements", + raw: "$HCXDR,A,171,D,PITCH,A,-37,D,ROLL,G,367,,MAGX,G,2420,,MAGY,G,-8984,,MAGZ*41", + msg: XDR{ + Measurements: []XDRMeasurement{ + {TransducerType: "A", Value: 171, Unit: "D", TransducerName: "PITCH"}, + {TransducerType: "A", Value: -37, Unit: "D", TransducerName: "ROLL"}, + {TransducerType: "G", Value: 367, Unit: "", TransducerName: "MAGX"}, + {TransducerType: "G", Value: 2420, Unit: "", TransducerName: "MAGY"}, + {TransducerType: "G", Value: -8984, Unit: "", TransducerName: "MAGZ"}, + }, + }, + }, + { + name: "invalid nmea: odd number of fields", + raw: "$HCXDR,A,171,D,PITCH,A,-37,D,ROLL,G,367,,MAGX,G,2420,MAGY,G,-8984,,MAGZ*6d", + err: "XDR field count is not exactly dividable by 4", + }, + { + name: "invalid nmea: TransducerType", + raw: "$SDXDR,x,23.15,C,WTHI*4b", + err: "nmea: SDXDR invalid transducer type: x", + }, + { + name: "invalid nmea: Value", + raw: "$SDXDR,C,23.x,C,WTHI*0C", + err: "nmea: SDXDR invalid measurement value: 23.x", + }, + { + name: "invalid nmea: Unit", + raw: "$SDXDR,C,23.15,x,WTHI*4b", + err: "nmea: SDXDR invalid measurement unit: x", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m, err := Parse(tt.raw) + if tt.err != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.err) + } else { + assert.NoError(t, err) + hdt := m.(XDR) + hdt.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, hdt) + } + }) + } +} diff --git a/xte.go b/xte.go new file mode 100644 index 0000000..e4d494d --- /dev/null +++ b/xte.go @@ -0,0 +1,60 @@ +package nmea + +const ( + // TypeXTE type of XTE sentence for Cross-track error, measured + TypeXTE = "XTE" +) + +// XTE - Cross-track error, measured +// https://gpsd.gitlab.io/gpsd/NMEA.html#_xte_cross_track_error_measured +// +// Format: $--XTE,A,A,x.x,a,N*hh +// Format (NMEA 2.3): $--XTE,A,A,x.x,a,N,m*hh +// Example: $GPXTE,V,V,,,N,S*43 +type XTE struct { + BaseSentence + + // StatusGeneralWarning is used for warnings + // * V = LORAN-C Blink or SNR warning + // * A = general warning flag or other navigation systems when a reliable fix is not available + StatusGeneralWarning string + + // StatusLockWarning is used for lock warning + // * V = Loran-C Cycle Lock warning flag + // * A = OK or not used + StatusLockWarning string + + // CrossTrackErrorMagnitude is Cross Track Error Magnitude + CrossTrackErrorMagnitude float64 + + // DirectionToSteer is Direction to steer, + // * L = left + // * R = right + DirectionToSteer string + + // CrossTrackUnits is cross track units + // * N = nautical miles + // * K = for kilometers + CrossTrackUnits string + + // FAA mode indicator (filled in NMEA 2.3 and later) + FFAMode string +} + +// newXTE constructor +func newXTE(s BaseSentence) (XTE, error) { + p := NewParser(s) + p.AssertType(TypeXTE) + xte := XTE{ + BaseSentence: s, + StatusGeneralWarning: p.EnumString(0, "general warning", StatusWarningAClearORNotUsedAPB, StatusWarningASetAPB), + StatusLockWarning: p.EnumString(1, "lock warning", StatusWarningBSetAPB, StatusWarningBClearAPB), + CrossTrackErrorMagnitude: p.Float64(2, "cross track error magnitude"), + DirectionToSteer: p.EnumString(3, "direction to steer", Left, Right), + CrossTrackUnits: p.EnumString(4, "cross track units", DistanceUnitKilometre, DistanceUnitNauticalMile, DistanceUnitStatuteMile, DistanceUnitMetre), + } + if len(p.Fields) > 5 { + xte.FFAMode = p.String(5, "FAA mode") // not enum because some devices have proprietary "non-nmea" values + } + return xte, p.Err() +} diff --git a/xte_test.go b/xte_test.go new file mode 100644 index 0000000..e0c86d0 --- /dev/null +++ b/xte_test.go @@ -0,0 +1,74 @@ +package nmea + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestXTE(t *testing.T) { + var tests = []struct { + name string + raw string + err string + msg XTE + }{ + { + name: "good sentence", + raw: "$GPXTE,V,V,10.1,L,N*6E", + msg: XTE{ + StatusGeneralWarning: "V", + StatusLockWarning: "V", + CrossTrackErrorMagnitude: 10.1, + DirectionToSteer: "L", + CrossTrackUnits: "N", + FFAMode: "", + }, + }, + { + name: "good sentence with FAAMode", + raw: "$GPXTE,V,V,,,N,S*43", + msg: XTE{ + StatusGeneralWarning: "V", + StatusLockWarning: "V", + CrossTrackErrorMagnitude: 0, + DirectionToSteer: "", + CrossTrackUnits: "N", + FFAMode: "S", + }, + }, + { + name: "invalid nmea: StatusGeneralWarning", + raw: "$GPXTE,x,V,,,N,S*6d", + err: "nmea: GPXTE invalid general warning: x", + }, + { + name: "invalid nmea: StatusLockWarning", + raw: "$GPXTE,V,x,,,N,S*6d", + err: "nmea: GPXTE invalid lock warning: x", + }, + { + name: "invalid nmea: DirectionToSteer", + raw: "$GPXTE,V,V,,x,N,S*3b", + err: "nmea: GPXTE invalid direction to steer: x", + }, + { + name: "invalid nmea: CrossTrackUnits", + raw: "$GPXTE,V,V,,,x,S*75", + err: "nmea: GPXTE invalid cross track units: x", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m, err := Parse(tt.raw) + if tt.err != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.err) + } else { + assert.NoError(t, err) + hdt := m.(XTE) + hdt.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, hdt) + } + }) + } +} From 03882e13ed6cefff3d82973548823254a37ad900 Mon Sep 17 00:00:00 2001 From: toimtoimtoim Date: Mon, 3 Jan 2022 07:32:34 +0200 Subject: [PATCH 2/3] rename variable name in tests --- aam_test.go | 6 +++--- ala_test.go | 6 +++--- apb_test.go | 6 +++--- bec_test.go | 6 +++--- bod_test.go | 6 +++--- bwc_test.go | 6 +++--- bwr_test.go | 6 +++--- bww_test.go | 6 +++--- dbk_test.go | 6 +++--- dor_test.go | 6 +++--- dsc_test.go | 6 +++--- dse_test.go | 6 +++--- eve_test.go | 6 +++--- fir_test.go | 6 +++--- hdg_test.go | 6 +++--- hdm_test.go | 6 +++--- hsc_test.go | 6 +++--- mta_test.go | 6 +++--- mtw_test.go | 6 +++--- phtro_test.go | 6 +++--- prdid_test.go | 6 +++--- psoncms_test.go | 6 +++--- rmb_test.go | 6 +++--- rot_test.go | 6 +++--- rpm_test.go | 6 +++--- rsa_test.go | 6 +++--- txt_test.go | 6 +++--- vdr_test.go | 6 +++--- vlw_test.go | 6 +++--- vpw_test.go | 6 +++--- vwr_test.go | 6 +++--- vwt_test.go | 6 +++--- xdr_test.go | 6 +++--- xte_test.go | 6 +++--- 34 files changed, 102 insertions(+), 102 deletions(-) diff --git a/aam_test.go b/aam_test.go index 1aa167a..ff657ea 100644 --- a/aam_test.go +++ b/aam_test.go @@ -47,9 +47,9 @@ func TestAAM(t *testing.T) { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - hdt := m.(AAM) - hdt.BaseSentence = BaseSentence{} - assert.Equal(t, tt.msg, hdt) + aam := m.(AAM) + aam.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, aam) } }) } diff --git a/ala_test.go b/ala_test.go index f23a906..7a234f5 100644 --- a/ala_test.go +++ b/ala_test.go @@ -56,9 +56,9 @@ func TestALA(t *testing.T) { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - hdt := m.(ALA) - hdt.BaseSentence = BaseSentence{} - assert.Equal(t, tt.msg, hdt) + ala := m.(ALA) + ala.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, ala) } }) } diff --git a/apb_test.go b/apb_test.go index ede569b..943efbb 100644 --- a/apb_test.go +++ b/apb_test.go @@ -83,9 +83,9 @@ func TestAPB(t *testing.T) { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - hdt := m.(APB) - hdt.BaseSentence = BaseSentence{} - assert.Equal(t, tt.msg, hdt) + apb := m.(APB) + apb.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, apb) } }) } diff --git a/bec_test.go b/bec_test.go index 31fb92a..618d81e 100644 --- a/bec_test.go +++ b/bec_test.go @@ -63,9 +63,9 @@ func TestBEC(t *testing.T) { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - hdt := m.(BEC) - hdt.BaseSentence = BaseSentence{} - assert.Equal(t, tt.msg, hdt) + bec := m.(BEC) + bec.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, bec) } }) } diff --git a/bod_test.go b/bod_test.go index aef9bfc..02760ae 100644 --- a/bod_test.go +++ b/bod_test.go @@ -55,9 +55,9 @@ func TestBOD(t *testing.T) { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - hdt := m.(BOD) - hdt.BaseSentence = BaseSentence{} - assert.Equal(t, tt.msg, hdt) + bod := m.(BOD) + bod.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, bod) } }) } diff --git a/bwc_test.go b/bwc_test.go index e25fa2f..6653df8 100644 --- a/bwc_test.go +++ b/bwc_test.go @@ -104,9 +104,9 @@ func TestBWC(t *testing.T) { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - hdt := m.(BWC) - hdt.BaseSentence = BaseSentence{} - assert.Equal(t, tt.msg, hdt) + bwc := m.(BWC) + bwc.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, bwc) } }) } diff --git a/bwr_test.go b/bwr_test.go index 4e3ab54..dacc1a9 100644 --- a/bwr_test.go +++ b/bwr_test.go @@ -104,9 +104,9 @@ func TestBWR(t *testing.T) { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - hdt := m.(BWR) - hdt.BaseSentence = BaseSentence{} - assert.Equal(t, tt.msg, hdt) + bwr := m.(BWR) + bwr.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, bwr) } }) } diff --git a/bww_test.go b/bww_test.go index e9bd948..9c804a0 100644 --- a/bww_test.go +++ b/bww_test.go @@ -43,9 +43,9 @@ func TestBWW(t *testing.T) { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - hdt := m.(BWW) - hdt.BaseSentence = BaseSentence{} - assert.Equal(t, tt.msg, hdt) + bww := m.(BWW) + bww.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, bww) } }) } diff --git a/dbk_test.go b/dbk_test.go index f480b96..da82ade 100644 --- a/dbk_test.go +++ b/dbk_test.go @@ -48,9 +48,9 @@ func TestDBK(t *testing.T) { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - hdt := m.(DBK) - hdt.BaseSentence = BaseSentence{} - assert.Equal(t, tt.msg, hdt) + dbk := m.(DBK) + dbk.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, dbk) } }) } diff --git a/dor_test.go b/dor_test.go index 304841d..678c681 100644 --- a/dor_test.go +++ b/dor_test.go @@ -62,9 +62,9 @@ func TestDOR(t *testing.T) { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - hdt := m.(DOR) - hdt.BaseSentence = BaseSentence{} - assert.Equal(t, tt.msg, hdt) + dor := m.(DOR) + dor.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, dor) } }) } diff --git a/dsc_test.go b/dsc_test.go index af4a655..52bd16b 100644 --- a/dsc_test.go +++ b/dsc_test.go @@ -77,9 +77,9 @@ func TestDSC(t *testing.T) { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - hdt := m.(DSC) - hdt.BaseSentence = BaseSentence{} - assert.Equal(t, tt.msg, hdt) + dsc := m.(DSC) + dsc.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, dsc) } }) } diff --git a/dse_test.go b/dse_test.go index 71321c0..1d7be57 100644 --- a/dse_test.go +++ b/dse_test.go @@ -63,9 +63,9 @@ func TestDSE(t *testing.T) { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - hdt := m.(DSE) - hdt.BaseSentence = BaseSentence{} - assert.Equal(t, tt.msg, hdt) + dse := m.(DSE) + dse.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, dse) } }) } diff --git a/eve_test.go b/eve_test.go index eb130aa..b4db9d6 100644 --- a/eve_test.go +++ b/eve_test.go @@ -41,9 +41,9 @@ func TestEVE(t *testing.T) { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - hdt := m.(EVE) - hdt.BaseSentence = BaseSentence{} - assert.Equal(t, tt.msg, hdt) + eve := m.(EVE) + eve.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, eve) } }) } diff --git a/fir_test.go b/fir_test.go index eb93980..dd4e76a 100644 --- a/fir_test.go +++ b/fir_test.go @@ -62,9 +62,9 @@ func TestFIR(t *testing.T) { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - hdt := m.(FIR) - hdt.BaseSentence = BaseSentence{} - assert.Equal(t, tt.msg, hdt) + fir := m.(FIR) + fir.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, fir) } }) } diff --git a/hdg_test.go b/hdg_test.go index 9820d41..fb9626f 100644 --- a/hdg_test.go +++ b/hdg_test.go @@ -57,9 +57,9 @@ func TestHDG(t *testing.T) { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - hdt := m.(HDG) - hdt.BaseSentence = BaseSentence{} - assert.Equal(t, tt.msg, hdt) + hdg := m.(HDG) + hdg.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, hdg) } }) } diff --git a/hdm_test.go b/hdm_test.go index 8023f42..fb6626f 100644 --- a/hdm_test.go +++ b/hdm_test.go @@ -39,9 +39,9 @@ func TestHDM(t *testing.T) { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - hdt := m.(HDM) - hdt.BaseSentence = BaseSentence{} - assert.Equal(t, tt.msg, hdt) + hdm := m.(HDM) + hdm.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, hdm) } }) } diff --git a/hsc_test.go b/hsc_test.go index f6ec2bd..0e69b11 100644 --- a/hsc_test.go +++ b/hsc_test.go @@ -51,9 +51,9 @@ func TestHSC(t *testing.T) { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - hdt := m.(HSC) - hdt.BaseSentence = BaseSentence{} - assert.Equal(t, tt.msg, hdt) + hsc := m.(HSC) + hsc.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, hsc) } }) } diff --git a/mta_test.go b/mta_test.go index 5d202ce..8ba3611 100644 --- a/mta_test.go +++ b/mta_test.go @@ -39,9 +39,9 @@ func TestMTA(t *testing.T) { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - hdt := m.(MTA) - hdt.BaseSentence = BaseSentence{} - assert.Equal(t, tt.msg, hdt) + mta := m.(MTA) + mta.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, mta) } }) } diff --git a/mtw_test.go b/mtw_test.go index 301364e..781980b 100644 --- a/mtw_test.go +++ b/mtw_test.go @@ -39,9 +39,9 @@ func TestMTW(t *testing.T) { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - hdt := m.(MTW) - hdt.BaseSentence = BaseSentence{} - assert.Equal(t, tt.msg, hdt) + mtw := m.(MTW) + mtw.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, mtw) } }) } diff --git a/phtro_test.go b/phtro_test.go index 3e920f8..875355f 100644 --- a/phtro_test.go +++ b/phtro_test.go @@ -51,9 +51,9 @@ func TestPHTRO(t *testing.T) { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - hdt := m.(PHTRO) - hdt.BaseSentence = BaseSentence{} - assert.Equal(t, tt.msg, hdt) + phtro := m.(PHTRO) + phtro.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, phtro) } }) } diff --git a/prdid_test.go b/prdid_test.go index af95a59..b446aed 100644 --- a/prdid_test.go +++ b/prdid_test.go @@ -45,9 +45,9 @@ func TestPRDID(t *testing.T) { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - hdt := m.(PRDID) - hdt.BaseSentence = BaseSentence{} - assert.Equal(t, tt.msg, hdt) + prdid := m.(PRDID) + prdid.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, prdid) } }) } diff --git a/psoncms_test.go b/psoncms_test.go index fa028b9..3435293 100644 --- a/psoncms_test.go +++ b/psoncms_test.go @@ -52,9 +52,9 @@ func TestPSONCMS(t *testing.T) { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - hdt := m.(PSONCMS) - hdt.BaseSentence = BaseSentence{} - assert.Equal(t, tt.msg, hdt) + psoncms := m.(PSONCMS) + psoncms.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, psoncms) } }) } diff --git a/rmb_test.go b/rmb_test.go index 21a7b62..a360846 100644 --- a/rmb_test.go +++ b/rmb_test.go @@ -102,9 +102,9 @@ func TestRMB(t *testing.T) { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - hdt := m.(RMB) - hdt.BaseSentence = BaseSentence{} - assert.Equal(t, tt.msg, hdt) + rmb := m.(RMB) + rmb.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, rmb) } }) } diff --git a/rot_test.go b/rot_test.go index 3bc419d..739646f 100644 --- a/rot_test.go +++ b/rot_test.go @@ -39,9 +39,9 @@ func TestROT(t *testing.T) { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - hdt := m.(ROT) - hdt.BaseSentence = BaseSentence{} - assert.Equal(t, tt.msg, hdt) + rot := m.(ROT) + rot.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, rot) } }) } diff --git a/rpm_test.go b/rpm_test.go index 65dde8d..b692134 100644 --- a/rpm_test.go +++ b/rpm_test.go @@ -42,9 +42,9 @@ func TestRPM(t *testing.T) { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - hdt := m.(RPM) - hdt.BaseSentence = BaseSentence{} - assert.Equal(t, tt.msg, hdt) + rpm := m.(RPM) + rpm.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, rpm) } }) } diff --git a/rsa_test.go b/rsa_test.go index 8fa5ee7..58254f7 100644 --- a/rsa_test.go +++ b/rsa_test.go @@ -51,9 +51,9 @@ func TestRSA(t *testing.T) { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - hdt := m.(RSA) - hdt.BaseSentence = BaseSentence{} - assert.Equal(t, tt.msg, hdt) + rsa := m.(RSA) + rsa.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, rsa) } }) } diff --git a/txt_test.go b/txt_test.go index 511b837..7db7b84 100644 --- a/txt_test.go +++ b/txt_test.go @@ -46,9 +46,9 @@ func TestTXT(t *testing.T) { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - hdt := m.(TXT) - hdt.BaseSentence = BaseSentence{} - assert.Equal(t, tt.msg, hdt) + txt := m.(TXT) + txt.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, txt) } }) } diff --git a/vdr_test.go b/vdr_test.go index 849a6f1..15d8479 100644 --- a/vdr_test.go +++ b/vdr_test.go @@ -48,9 +48,9 @@ func TestVDR(t *testing.T) { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - hdt := m.(VDR) - hdt.BaseSentence = BaseSentence{} - assert.Equal(t, tt.msg, hdt) + vdr := m.(VDR) + vdr.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, vdr) } }) } diff --git a/vlw_test.go b/vlw_test.go index 3105431..db9ec29 100644 --- a/vlw_test.go +++ b/vlw_test.go @@ -69,9 +69,9 @@ func TestVLW(t *testing.T) { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - hdt := m.(VLW) - hdt.BaseSentence = BaseSentence{} - assert.Equal(t, tt.msg, hdt) + vlw := m.(VLW) + vlw.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, vlw) } }) } diff --git a/vpw_test.go b/vpw_test.go index d4802c8..112a2d0 100644 --- a/vpw_test.go +++ b/vpw_test.go @@ -41,9 +41,9 @@ func TestVPW(t *testing.T) { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - hdt := m.(VPW) - hdt.BaseSentence = BaseSentence{} - assert.Equal(t, tt.msg, hdt) + vpw := m.(VPW) + vpw.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, vpw) } }) } diff --git a/vwr_test.go b/vwr_test.go index 034aa32..40ba368 100644 --- a/vwr_test.go +++ b/vwr_test.go @@ -83,9 +83,9 @@ func TestVWR(t *testing.T) { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - hdt := m.(VWR) - hdt.BaseSentence = BaseSentence{} - assert.Equal(t, tt.msg, hdt) + vwr := m.(VWR) + vwr.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, vwr) } }) } diff --git a/vwt_test.go b/vwt_test.go index 3509b1a..b99e866 100644 --- a/vwt_test.go +++ b/vwt_test.go @@ -83,9 +83,9 @@ func TestVWT(t *testing.T) { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - hdt := m.(VWT) - hdt.BaseSentence = BaseSentence{} - assert.Equal(t, tt.msg, hdt) + vwt := m.(VWT) + vwt.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, vwt) } }) } diff --git a/xdr_test.go b/xdr_test.go index 7e37741..1417cd6 100644 --- a/xdr_test.go +++ b/xdr_test.go @@ -68,9 +68,9 @@ func TestXDR(t *testing.T) { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - hdt := m.(XDR) - hdt.BaseSentence = BaseSentence{} - assert.Equal(t, tt.msg, hdt) + xdr := m.(XDR) + xdr.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, xdr) } }) } diff --git a/xte_test.go b/xte_test.go index e0c86d0..769542a 100644 --- a/xte_test.go +++ b/xte_test.go @@ -65,9 +65,9 @@ func TestXTE(t *testing.T) { assert.EqualError(t, err, tt.err) } else { assert.NoError(t, err) - hdt := m.(XTE) - hdt.BaseSentence = BaseSentence{} - assert.Equal(t, tt.msg, hdt) + xte := m.(XTE) + xte.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, xte) } }) } From f4d3e5112f8720ccb24779e0d03d4b1b616e9c9e Mon Sep 17 00:00:00 2001 From: toimtoimtoim Date: Mon, 3 Jan 2022 15:59:00 +0200 Subject: [PATCH 3/3] fix typo on MMSI variable name --- dsc.go | 4 ++-- dsc_test.go | 6 +++--- dse.go | 4 ++-- dse_test.go | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/dsc.go b/dsc.go index 5f919f5..0edf769 100644 --- a/dsc.go +++ b/dsc.go @@ -92,7 +92,7 @@ type DSC struct { // MMSI of ship in distress (10 digits or empty) // > The call content is next described as having a "self-identification" element. This is simply the sending // > station's MMSI, encoded like the address element. This identifies who sent the message. - MSSI string + MMSI string // DistressCause is The cause of the distress (2 digits or empty) DistressCause string @@ -117,7 +117,7 @@ func newDSC(s BaseSentence) (DSC, error) { CommandTypeOrTeleCommand2: p.String(4, "type of communication or second telecommand"), PositionOrCanal: p.String(5, "position or canal"), TimeOrTelephoneNumber: p.String(6, "time or telephone"), - MSSI: p.String(7, "MSSI"), + MMSI: p.String(7, "MMSI"), DistressCause: p.String(8, "distress cause"), Acknowledgement: strings.TrimSpace(p.EnumString( 9, diff --git a/dsc_test.go b/dsc_test.go index 52bd16b..d1514b1 100644 --- a/dsc_test.go +++ b/dsc_test.go @@ -23,7 +23,7 @@ func TestDSC(t *testing.T) { CommandTypeOrTeleCommand2: "00", PositionOrCanal: "1423108312", TimeOrTelephoneNumber: "2019", - MSSI: " ", + MMSI: " ", DistressCause: " ", Acknowledgement: "S", ExpansionIndicator: " E ", @@ -40,7 +40,7 @@ func TestDSC(t *testing.T) { CommandTypeOrTeleCommand2: "00", PositionOrCanal: "1423108312", TimeOrTelephoneNumber: "0236", - MSSI: "3381581370", + MMSI: "3381581370", DistressCause: " ", Acknowledgement: "S", ExpansionIndicator: " ", @@ -57,7 +57,7 @@ func TestDSC(t *testing.T) { CommandTypeOrTeleCommand2: "26", PositionOrCanal: "1423108312", TimeOrTelephoneNumber: "1902", - MSSI: " ", + MMSI: " ", DistressCause: " ", Acknowledgement: "B", ExpansionIndicator: " E ", diff --git a/dse.go b/dse.go index 14b447e..a4d0d02 100644 --- a/dse.go +++ b/dse.go @@ -25,7 +25,7 @@ type DSE struct { TotalNumber int64 // total number of sentences, 01 to 99 Number int64 // number of current sentence, 01 to 99 Acknowledgement string // Acknowledgement (R=Acknowledge request, B=Acknowledgement, S=Neither (end of sequence)) - MSSI string // MMSI of vessel (10 digits) + MMSI string // MMSI of vessel (10 digits) DataSets []DSEDataSet } @@ -56,7 +56,7 @@ func newDSE(s BaseSentence) (DSE, error) { TotalNumber: p.Int64(0, "total number of sentences"), Number: p.Int64(1, "sentence number"), Acknowledgement: p.EnumString(2, "acknowledgement", AcknowledgementAutomaticDSE, AcknowledgementRequestDSE, AcknowledgementQueryDSE), - MSSI: p.String(3, "MSSI"), + MMSI: p.String(3, "MMSI"), DataSets: nil, } datasetFieldCount := len(p.Fields) - 4 diff --git a/dse_test.go b/dse_test.go index 1d7be57..fcc2952 100644 --- a/dse_test.go +++ b/dse_test.go @@ -19,7 +19,7 @@ func TestDSE(t *testing.T) { TotalNumber: 1, Number: 1, Acknowledgement: AcknowledgementAutomaticDSE, - MSSI: "3380400790", + MMSI: "3380400790", DataSets: []DSEDataSet{ {Code: "00", Data: "46504437"}, }, @@ -32,7 +32,7 @@ func TestDSE(t *testing.T) { TotalNumber: 1, Number: 1, Acknowledgement: AcknowledgementAutomaticDSE, - MSSI: "3380400790", + MMSI: "3380400790", DataSets: []DSEDataSet{ {Code: "00", Data: "46504437"}, {Code: "01", Data: "16501437"},