├── .github └── workflows │ └── ci.yml ├── LICENSE ├── Makefile ├── README.md ├── aam.go ├── aam_test.go ├── abm.go ├── abm_test.go ├── ack.go ├── ack_test.go ├── acn.go ├── acn_test.go ├── ala.go ├── ala_test.go ├── alc.go ├── alc_test.go ├── alf.go ├── alf_test.go ├── alr.go ├── alr_test.go ├── apb.go ├── apb_test.go ├── arc.go ├── arc_test.go ├── bbm.go ├── bbm_test.go ├── bec.go ├── bec_test.go ├── bod.go ├── bod_test.go ├── bwc.go ├── bwc_test.go ├── bwr.go ├── bwr_test.go ├── bww.go ├── bww_test.go ├── dbk.go ├── dbk_test.go ├── dbs.go ├── dbs_test.go ├── dbt.go ├── dbt_test.go ├── deprecated.go ├── deprecated_test.go ├── dor.go ├── dor_test.go ├── dpt.go ├── dpt_test.go ├── dsc.go ├── dsc_test.go ├── dse.go ├── dse_test.go ├── dtm.go ├── dtm_test.go ├── eve.go ├── eve_test.go ├── fir.go ├── fir_test.go ├── gga.go ├── gga_test.go ├── gll.go ├── gll_test.go ├── gns.go ├── gns_test.go ├── go.mod ├── go.sum ├── gsa.go ├── gsa_test.go ├── gsv.go ├── gsv_test.go ├── hbt.go ├── hbt_test.go ├── hdg.go ├── hdg_test.go ├── hdm.go ├── hdm_test.go ├── hdt.go ├── hdt_test.go ├── hsc.go ├── hsc_test.go ├── mda.go ├── mda_test.go ├── mta.go ├── mta_test.go ├── mtk.go ├── mtk_test.go ├── mtw.go ├── mtw_test.go ├── must_test.go ├── mwd.go ├── mwd_test.go ├── mwv.go ├── mwv_test.go ├── osd.go ├── osd_test.go ├── parser.go ├── parser_test.go ├── pcdin.go ├── pcdin_test.go ├── pgn.go ├── pgn_test.go ├── pgrme.go ├── pgrme_test.go ├── pgrmt.go ├── pgrmt_test.go ├── phtro.go ├── phtro_test.go ├── pklds.go ├── pklds_test.go ├── pklid.go ├── pklid_test.go ├── pklsh.go ├── pklsh_test.go ├── pknds.go ├── pknds_test.go ├── pknid.go ├── pknid_test.go ├── pknsh.go ├── pknsh_test.go ├── pkwdwpl.go ├── pkwdwpl_test.go ├── pmtk.go ├── pmtk_test.go ├── prdid.go ├── prdid_test.go ├── pskpdpt.go ├── pskpdpt_test.go ├── psoncms.go ├── psoncms_test.go ├── query.go ├── query_test.go ├── rmb.go ├── rmb_test.go ├── rmc.go ├── rmc_test.go ├── rot.go ├── rot_test.go ├── rpm.go ├── rpm_test.go ├── rsa.go ├── rsa_test.go ├── rsd.go ├── rsd_test.go ├── rte.go ├── rte_test.go ├── sentence.go ├── sentence_customparser_test.go ├── sentence_test.go ├── tagblock.go ├── tagblock_test.go ├── ths.go ├── ths_test.go ├── tlb.go ├── tlb_test.go ├── tll.go ├── tll_test.go ├── ttd.go ├── ttd_test.go ├── ttm.go ├── ttm_test.go ├── txt.go ├── txt_test.go ├── types.go ├── types_test.go ├── vbw.go ├── vbw_test.go ├── vdmvdo.go ├── vdmvdo_test.go ├── vdr.go ├── vdr_test.go ├── vhw.go ├── vhw_test.go ├── vlw.go ├── vlw_test.go ├── vpw.go ├── vpw_test.go ├── vsd.go ├── vsd_test.go ├── vtg.go ├── vtg_test.go ├── vwr.go ├── vwr_test.go ├── vwt.go ├── vwt_test.go ├── wpl.go ├── wpl_test.go ├── xdr.go ├── xdr_test.go ├── xte.go ├── xte_test.go ├── zda.go └── zda_test.go /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | go: ["1.20", "1.19", "1.18", "1.17"] 19 | steps: 20 | - uses: actions/checkout@v3 21 | 22 | - name: Set up Go 23 | uses: actions/setup-go@v3 24 | with: 25 | go-version: ${{ matrix.go }} 26 | 27 | - name: Install dependencies 28 | run: | 29 | go install golang.org/x/lint/golint@latest 30 | go install github.com/mattn/goveralls@v0.0.11 31 | 32 | - name: Lint 33 | run: | 34 | go vet ./... 35 | golint -set_exit_status ./... 36 | 37 | - name: Test 38 | run: | 39 | go test -v -race -covermode=atomic -coverprofile=profile.cov ./... 40 | 41 | - name: Coverage 42 | run: | 43 | goveralls -coverprofile=profile.cov -service=github -parallel -flagname="go-${{ matrix.go }}" 44 | env: 45 | COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Adrian Moreno 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := check 2 | check: lint vet test ## Check project 3 | 4 | lint: ## Lint the files 5 | @golint -set_exit_status ./... 6 | 7 | vet: ## Vet the files 8 | @go vet ./... 9 | 10 | test: ## Run tests with data race detector 11 | @go test -race ./... 12 | 13 | init: 14 | @go install golang.org/x/lint/golint@latest 15 | 16 | goversion ?= "1.19" 17 | test_version: ## Run tests inside Docker with given version (defaults to 1.19). Example for Go1.15: make test_version goversion=1.15 18 | @docker run --rm -it -v $(shell pwd):/project golang:$(goversion) /bin/sh -c "cd /project && make test" 19 | -------------------------------------------------------------------------------- /aam.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeAAM type of AAM sentence for Waypoint Arrival Alarm 5 | TypeAAM = "AAM" 6 | ) 7 | 8 | // AAM - Waypoint Arrival Alarm 9 | // This sentence is generated by some units to indicate the status of arrival (entering the arrival circle, or passing 10 | // the perpendicular of the course line) at the destination waypoint (source: GPSD). 11 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_aam_waypoint_arrival_alarm 12 | // 13 | // Format: $--AAM,A,A,x.x,N,c--c*hh 14 | // Example: $GPAAM,A,A,0.10,N,WPTNME*43 15 | type AAM struct { 16 | BaseSentence 17 | // StatusArrivalCircleEntered is warning of arrival to waypoint circle 18 | // * A = Arrival Circle Entered 19 | // * V = not entered 20 | StatusArrivalCircleEntered string 21 | 22 | // StatusPerpendicularPassed is warning for perpendicular passing of waypoint 23 | // * A = Perpendicular passed at waypoint 24 | // * V = not passed 25 | StatusPerpendicularPassed string 26 | 27 | // ArrivalCircleRadius is radius for arrival circle 28 | ArrivalCircleRadius float64 29 | 30 | // ArrivalCircleRadiusUnit is unit for arrival circle radius 31 | ArrivalCircleRadiusUnit string 32 | 33 | // DestinationWaypointID is destination waypoint ID 34 | DestinationWaypointID string 35 | } 36 | 37 | // newAAM constructor 38 | func newAAM(s BaseSentence) (Sentence, error) { 39 | p := NewParser(s) 40 | p.AssertType(TypeAAM) 41 | return AAM{ 42 | BaseSentence: s, 43 | StatusArrivalCircleEntered: p.EnumString(0, "arrival circle entered status", WPStatusArrivalCircleEnteredA, WPStatusArrivalCircleEnteredV), 44 | StatusPerpendicularPassed: p.EnumString(1, "perpendicularly passed status", WPStatusPerpendicularPassedA, WPStatusPerpendicularPassedV), 45 | ArrivalCircleRadius: p.Float64(2, "arrival circle radius"), 46 | ArrivalCircleRadiusUnit: p.EnumString(3, "arrival circle radius units", DistanceUnitKilometre, DistanceUnitNauticalMile, DistanceUnitStatuteMile, DistanceUnitMetre), 47 | DestinationWaypointID: p.String(4, "destination waypoint ID"), 48 | }, p.Err() 49 | } 50 | -------------------------------------------------------------------------------- /aam_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestAAM(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg AAM 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$GPAAM,A,A,0.10,N,WPTNME*32", 18 | msg: AAM{ 19 | StatusArrivalCircleEntered: WPStatusArrivalCircleEnteredA, 20 | StatusPerpendicularPassed: WPStatusPerpendicularPassedA, 21 | ArrivalCircleRadius: 0.1, 22 | ArrivalCircleRadiusUnit: DistanceUnitNauticalMile, 23 | DestinationWaypointID: "WPTNME", 24 | }, 25 | }, 26 | { 27 | name: "invalid nmea: StatusArrivalCircleEntered", 28 | raw: "$GPAAM,x,A,0.10,N,WPTNME*0B", 29 | err: "nmea: GPAAM invalid arrival circle entered status: x", 30 | }, 31 | { 32 | name: "invalid nmea: StatusPerpendicularPassed", 33 | raw: "$GPAAM,A,x,0.10,N,WPTNME*0B", 34 | err: "nmea: GPAAM invalid perpendicularly passed status: x", 35 | }, 36 | { 37 | name: "invalid nmea: DistanceUnitNauticalMile", 38 | raw: "$GPAAM,A,A,0.10,x,WPTNME*04", 39 | err: "nmea: GPAAM invalid arrival circle radius units: x", 40 | }, 41 | } 42 | for _, tt := range tests { 43 | t.Run(tt.name, func(t *testing.T) { 44 | m, err := Parse(tt.raw) 45 | if tt.err != "" { 46 | assert.Error(t, err) 47 | assert.EqualError(t, err, tt.err) 48 | } else { 49 | assert.NoError(t, err) 50 | aam := m.(AAM) 51 | aam.BaseSentence = BaseSentence{} 52 | assert.Equal(t, tt.msg, aam) 53 | } 54 | }) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /abm.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeABM type of ABM sentence for AIS addressed binary and safety related message 5 | TypeABM = "ABM" 6 | ) 7 | 8 | // ABM - AIS addressed binary and safety related message 9 | // https://fcc.report/FCC-ID/ADB9ZWRTR100/2768717.pdf (page 6) FURUNO MARINE RADAR, model FAR-15XX manual 10 | // 11 | // Format: !--ABM,x,x,x,xxxxxxxxx,x,xx,s--s,x,*hh 12 | // Example: !AIABM,26,2,1,3381581370,3,8,177KQJ5000G?tO`K>RA1wUbN0TKH,0*02 13 | type ABM struct { 14 | BaseSentence 15 | 16 | // NumFragments is total number of fragments/sentences need to transfer the message (1 - 9) 17 | NumFragments int64 // 0 18 | 19 | // FragmentNumber is current fragment/sentence number (1 - 9) 20 | FragmentNumber int64 // 1 21 | 22 | // MessageID is sequential message identifier (0 - 3) 23 | MessageID int64 // 2 24 | 25 | // MMSI is The MMSI of destination AIS unit for the ITU-R M.1371 message (10 digits or empty) 26 | MMSI string // 3 27 | 28 | // Channel is AIS channel for broadcast of the radio message (0 - 3) 29 | // 0 - no broadcast 30 | // 1 - on AIS channel A 31 | // 2 - on AIS channel B 32 | // 3 - broadcast on both AIS channels 33 | Channel string // 4 34 | 35 | // VDLMessageNumber is VDL message number (6/12), see ITU-R M.1371 36 | VDLMessageNumber int64 // 5 37 | 38 | // Payload is encapsulated data (6 bit binary-converted data) (1 - 63 bytes) 39 | Payload []byte // 6 40 | // 7 - Number of fill bits (0 - 5) 41 | } 42 | 43 | // newABM constructor 44 | func newABM(s BaseSentence) (Sentence, error) { 45 | p := NewParser(s) 46 | p.AssertType(TypeABM) 47 | return ABM{ 48 | BaseSentence: s, 49 | NumFragments: p.Int64(0, "number of fragments"), 50 | FragmentNumber: p.Int64(1, "fragment number"), 51 | MessageID: p.Int64(2, "message ID"), 52 | MMSI: p.String(3, "MMSI"), 53 | Channel: p.String(4, "channel"), 54 | VDLMessageNumber: p.Int64(5, "VDL message number"), 55 | Payload: p.SixBitASCIIArmour(6, int(p.Int64(7, "number of padding bits")), "payload"), 56 | }, p.Err() 57 | } 58 | -------------------------------------------------------------------------------- /ack.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeACK type of ACK sentence for alert acknowledge 5 | TypeACK = "ACK" 6 | ) 7 | 8 | // ACK - Acknowledge. This sentence is used to acknowledge an alarm condition reported by a device. 9 | // http://www.nmea.de/nmea0183datensaetze.html#ack 10 | // https://www.furuno.it/docs/INSTALLATION%20MANUALgp170_installation_manual.pdf GPS NAVIGATOR Model GP-170 (page 42) 11 | // https://www.manualslib.com/manual/2226813/Jrc-Jln-900.html?page=239#manual (JRC JLN-900: Installation And Instruction Manual) 12 | // 13 | // Format: $--ACK,xxx*hh 14 | // Example: $VRACK,001*50 15 | type ACK struct { 16 | BaseSentence 17 | 18 | // AlertIdentifier is alert identifier (001 to 99999) 19 | AlertIdentifier int64 // 0 20 | } 21 | 22 | // newACKN constructor 23 | func newACK(s BaseSentence) (Sentence, error) { 24 | p := NewParser(s) 25 | p.AssertType(TypeACK) 26 | return ACK{ 27 | BaseSentence: s, 28 | AlertIdentifier: p.Int64(0, "alert identifier"), 29 | }, p.Err() 30 | } 31 | -------------------------------------------------------------------------------- /ack_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestACK(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg ACK 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$VRACK,001*50", 18 | msg: ACK{ 19 | AlertIdentifier: 1, 20 | }, 21 | }, 22 | { 23 | name: "invalid nmea: AlertIdentifier", 24 | raw: "$VRACK,x*19", 25 | err: "nmea: VRACK invalid alert identifier: x", 26 | }, 27 | } 28 | for _, tt := range tests { 29 | t.Run(tt.name, func(t *testing.T) { 30 | m, err := Parse(tt.raw) 31 | if tt.err != "" { 32 | assert.Error(t, err) 33 | assert.EqualError(t, err, tt.err) 34 | } else { 35 | assert.NoError(t, err) 36 | ack := m.(ACK) 37 | ack.BaseSentence = BaseSentence{} 38 | assert.Equal(t, tt.msg, ack) 39 | } 40 | }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /acn.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeACN type of ACN sentence for alert command 5 | TypeACN = "ACN" 6 | ) 7 | 8 | // ACN - Alert command. Used for acknowledge, silence, responsibility transfer and to request repeat of alert. 9 | // https://www.furuno.it/docs/INSTALLATION%20MANUALIME44900D_FA170.pdf Furuno CLASS A AIS Model FA-170 (page 49) 10 | // https://www.furuno.it/docs/INSTALLATION%20MANUALgp170_installation_manual.pdf GPS NAVIGATOR Model GP-170 (page 42) 11 | // 12 | // Format: $--ACN,hhmmss.ss,AAA,x.x,x.x,A,A*hh 13 | // Example: $VRACN,220516,BPMP1,A,A,Bilge pump alarm1*43 14 | type ACN struct { 15 | BaseSentence 16 | 17 | // Time is time of alarm condition change, UTC (000000.00 - 240001.00) 18 | Time Time // 0 19 | 20 | // ManufacturerMnemonicCode is manufacturer mnemonic code 21 | ManufacturerMnemonicCode string // 1 22 | 23 | // AlertIdentifier is alert identifier (001 to 99999) 24 | AlertIdentifier int64 // 2 25 | 26 | // AlertInstance is alert instance 27 | AlertInstance int64 // 3 28 | 29 | // Command is Alert command 30 | // * A - acknowledge, 31 | // * Q - request/repeat information 32 | // * O - responsibility transfer 33 | // * S - silence 34 | Command string // 4 35 | 36 | // State is alarm state 37 | // * C - command 38 | // * possible more classifier values but these are not mentioned in manual 39 | State string // 5 40 | } 41 | 42 | // newACN constructor 43 | func newACN(s BaseSentence) (Sentence, error) { 44 | p := NewParser(s) 45 | p.AssertType(TypeACN) 46 | return ACN{ 47 | BaseSentence: s, 48 | Time: p.Time(0, "time"), 49 | ManufacturerMnemonicCode: p.String(1, "manufacturer mnemonic code"), 50 | AlertIdentifier: p.Int64(2, "alert identifier"), 51 | AlertInstance: p.Int64(3, "alert instance"), 52 | Command: p.EnumString(4, "alert command", AlertCommandAcknowledge, AlertCommandRequestRepeatInformation, AlertCommandResponsibilityTransfer, AlertCommandSilence), 53 | State: p.String(5, "alarm state"), 54 | }, p.Err() 55 | } 56 | -------------------------------------------------------------------------------- /acn_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestACN(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg ACN 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$RAACN,220516,TCK,002,1,A,C*00", 18 | msg: ACN{ 19 | Time: Time{ 20 | Valid: true, 21 | Hour: 22, 22 | Minute: 05, 23 | Second: 16, 24 | Millisecond: 0, 25 | }, 26 | ManufacturerMnemonicCode: "TCK", 27 | AlertIdentifier: 2, 28 | AlertInstance: 1, 29 | Command: AlertCommandAcknowledge, 30 | State: "C", 31 | }, 32 | }, 33 | { 34 | name: "invalid nmea: Time", 35 | raw: "$RAACN,2x0516,TCK,002,1,A,C*4a", 36 | err: "nmea: RAACN invalid time: 2x0516", 37 | }, 38 | { 39 | name: "invalid nmea: AlertIdentifier", 40 | raw: "$RAACN,220516,TCK,x02,1,A,C*48", 41 | err: "nmea: RAACN invalid alert identifier: x02", 42 | }, 43 | { 44 | name: "invalid nmea: AlertInstance", 45 | raw: "$RAACN,220516,TCK,002,x,A,C*49", 46 | err: "nmea: RAACN invalid alert instance: x", 47 | }, 48 | { 49 | name: "invalid nmea: Command", 50 | raw: "$RAACN,220516,TCK,002,1,x,C*39", 51 | err: "nmea: RAACN invalid alert command: x", 52 | }, 53 | } 54 | for _, tt := range tests { 55 | t.Run(tt.name, func(t *testing.T) { 56 | m, err := Parse(tt.raw) 57 | if tt.err != "" { 58 | assert.Error(t, err) 59 | assert.EqualError(t, err, tt.err) 60 | } else { 61 | assert.NoError(t, err) 62 | acn := m.(ACN) 63 | acn.BaseSentence = BaseSentence{} 64 | assert.Equal(t, tt.msg, acn) 65 | } 66 | }) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /ala.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeALA type of ALA sentence for System Faults and alarms 5 | TypeALA = "ALA" 6 | ) 7 | 8 | // ALA - System Faults and alarms 9 | // Source: "Interfacing Voyage Data Recorder Systems, AutroSafe Interactive Fire-Alarm System, 116-P-BSL336/EE, RevA 2007-01-25, 10 | // Autronica Fire and Security AS " (page 31 | p.8.1.3) 11 | // https://product.autronicafire.com/fileshare/fileupload/14251/bsl336_ee.pdf 12 | // 13 | // Format: $FRALA,hhmmss,aa,aa,xx,xxx,a,a,c-cc*hh 14 | // Example: $FRALA,143955,FR,OT,00,901,N,V,Syst Fault : AutroSafe comm. OK*4F 15 | type ALA struct { 16 | BaseSentence 17 | 18 | // Time is Event Time 19 | Time Time 20 | 21 | // SystemIndicator is system indicator of original alarm source. Detector system type with 2 char identifier. 22 | // Values not known 23 | // https://www.nmea.org/Assets/20190303%20nmea%200183%20talker%20identifier%20mnemonics.pdf 24 | SystemIndicator string 25 | 26 | // SubSystemIndicator is sub system equipment indicator of original alarm source 27 | SubSystemIndicator string 28 | 29 | // InstanceNumber is instance number of equipment/unit/item (00-99) 30 | InstanceNumber int64 31 | 32 | // Type is alarm type (000-999) 33 | Type int64 34 | 35 | // Condition describes the condition triggering current message 36 | // * N – Normal state (OK) 37 | // * H - Alarm state (fault); 38 | // could be more 39 | Condition string 40 | 41 | // AlarmAckState is Alarm's acknowledge state 42 | // * A – Acknowledged 43 | // * H - Harbour mode 44 | // * V – Not acknowledged 45 | // * O - Override 46 | // could be more 47 | AlarmAckState string 48 | 49 | // Message's description text (could be cut to fit max packet length) 50 | Message string 51 | } 52 | 53 | // newALA constructor 54 | func newALA(s BaseSentence) (Sentence, error) { 55 | p := NewParser(s) 56 | p.AssertType(TypeALA) 57 | return ALA{ 58 | BaseSentence: s, 59 | Time: p.Time(0, "time"), 60 | SystemIndicator: p.String(1, "system indicator"), 61 | SubSystemIndicator: p.String(2, "subsystem indicator"), 62 | InstanceNumber: p.Int64(3, "instance number"), 63 | Type: p.Int64(4, "type"), 64 | Condition: p.String(5, "condition"), // string as there could be more 65 | AlarmAckState: p.String(6, "alarm acknowledgement state"), // string as there could be more 66 | Message: p.String(7, "message"), 67 | }, p.Err() 68 | } 69 | -------------------------------------------------------------------------------- /ala_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestALA(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg ALA 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$FRALA,143955,FR,OT,00,901,N,V,Syst Fault : AutroSafe comm. OK*4F", 18 | msg: ALA{ 19 | Time: Time{ 20 | Valid: true, 21 | Hour: 14, 22 | Minute: 39, 23 | Second: 55, 24 | Millisecond: 0, 25 | }, 26 | SystemIndicator: "FR", 27 | SubSystemIndicator: "OT", 28 | InstanceNumber: 0, 29 | Type: 901, 30 | Condition: "N", 31 | AlarmAckState: "V", 32 | Message: "Syst Fault : AutroSafe comm. OK", 33 | }, 34 | }, 35 | { 36 | name: "invalid nmea: Time", 37 | raw: "$FRALA,1x3955,FR,OT,00,901,N,V,Syst Fault : AutroSafe comm. OK*03", 38 | err: "nmea: FRALA invalid time: 1x3955", 39 | }, 40 | { 41 | name: "invalid nmea: InstanceNumber", 42 | raw: "$FRALA,143955,FR,OT,x0,901,N,V,Syst Fault : AutroSafe comm. OK*07", 43 | err: "nmea: FRALA invalid instance number: x0", 44 | }, 45 | { 46 | name: "invalid nmea: Type", 47 | raw: "$FRALA,143955,FR,OT,00,9x1,N,V,Syst Fault : AutroSafe comm. OK*07", 48 | err: "nmea: FRALA invalid type: 9x1", 49 | }, 50 | } 51 | for _, tt := range tests { 52 | t.Run(tt.name, func(t *testing.T) { 53 | m, err := Parse(tt.raw) 54 | if tt.err != "" { 55 | assert.Error(t, err) 56 | assert.EqualError(t, err, tt.err) 57 | } else { 58 | assert.NoError(t, err) 59 | ala := m.(ALA) 60 | ala.BaseSentence = BaseSentence{} 61 | assert.Equal(t, tt.msg, ala) 62 | } 63 | }) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /alr.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeALR type of ALR sentence for alert command refused 5 | TypeALR = "ALR" 6 | ) 7 | 8 | // ALR - Set alarm state 9 | // https://fcc.report/FCC-ID/ADB9ZWRTR100/2768717.pdf (page 7) FURUNO MARINE RADAR, model FAR-15XX manual 10 | // 11 | // Format: $--ALR,hhmmss.ss,xxx,A,A,c--c,*hh 12 | // Example: $RAALR,220516,BPMP1,A,A,Bilge pump alarm1*43 13 | type ALR struct { 14 | BaseSentence 15 | 16 | // Time is time of alarm condition change, UTC (000000.00 - 240001.00) 17 | Time Time // 0 18 | 19 | // AlarmIdentifier is unique alarm number (identifier) at alarm source 20 | AlarmIdentifier int64 // 1 21 | 22 | // AlarmCondition is alarm condition (A/V) 23 | // A - threshold exceeded 24 | // V - not exceeded 25 | Condition string // 2 26 | 27 | // State is alarm state (A/V) 28 | // A - acknowledged 29 | // V - not acknowledged 30 | State string // 3 31 | 32 | // Description is alarm description text 33 | Description string // 4 34 | } 35 | 36 | // newALR constructor 37 | func newALR(s BaseSentence) (Sentence, error) { 38 | p := NewParser(s) 39 | p.AssertType(TypeALR) 40 | return ALR{ 41 | BaseSentence: s, 42 | Time: p.Time(0, "time"), 43 | AlarmIdentifier: p.Int64(1, "unique alarm number"), 44 | Condition: p.EnumString(2, "alarm condition", StatusValid, StatusInvalid), 45 | State: p.EnumString(3, "alarm state", StatusValid, StatusInvalid), 46 | Description: p.String(4, "description"), 47 | }, p.Err() 48 | } 49 | -------------------------------------------------------------------------------- /alr_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestALR(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg ALR 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$RAALR,220516,001,A,A,Bilge pump alarm1*4c", 18 | msg: ALR{ 19 | Time: Time{ 20 | Valid: true, 21 | Hour: 22, 22 | Minute: 05, 23 | Second: 16, 24 | Millisecond: 0, 25 | }, 26 | AlarmIdentifier: 1, 27 | Condition: StatusValid, 28 | State: StatusValid, 29 | Description: "Bilge pump alarm1", 30 | }, 31 | }, 32 | { 33 | name: "nmea: Description empty", 34 | raw: "$RAALR,220516,001,A,A,*53", 35 | msg: ALR{ 36 | Time: Time{ 37 | Valid: true, 38 | Hour: 22, 39 | Minute: 05, 40 | Second: 16, 41 | Millisecond: 0, 42 | }, 43 | AlarmIdentifier: 1, 44 | Condition: StatusValid, 45 | State: StatusValid, 46 | Description: "", 47 | }, 48 | }, 49 | { 50 | name: "invalid nmea: Time", 51 | raw: "$RAALR,2x0516,001,A,A,Bilge pump alarm1*06", 52 | err: "nmea: RAALR invalid time: 2x0516", 53 | }, 54 | { 55 | name: "invalid nmea: Condition", 56 | raw: "$RAALR,220516,001,x,A,Bilge pump alarm1*75", 57 | err: "nmea: RAALR invalid alarm condition: x", 58 | }, 59 | { 60 | name: "invalid nmea: State", 61 | raw: "$RAALR,220516,001,A,x,Bilge pump alarm1*75", 62 | err: "nmea: RAALR invalid alarm state: x", 63 | }, 64 | } 65 | for _, tt := range tests { 66 | t.Run(tt.name, func(t *testing.T) { 67 | m, err := Parse(tt.raw) 68 | if tt.err != "" { 69 | assert.Error(t, err) 70 | assert.EqualError(t, err, tt.err) 71 | } else { 72 | assert.NoError(t, err) 73 | alr := m.(ALR) 74 | alr.BaseSentence = BaseSentence{} 75 | assert.Equal(t, tt.msg, alr) 76 | } 77 | }) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /arc.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeARC type of ARC sentence for alert command refused 5 | TypeARC = "ARC" 6 | ) 7 | 8 | const ( 9 | // AlertCommandAcknowledge means acknowledge 10 | AlertCommandAcknowledge = "A" 11 | // AlertCommandRequestRepeatInformation means request/repeat information 12 | AlertCommandRequestRepeatInformation = "Q" 13 | // AlertCommandResponsibilityTransfer means responsibility transfer 14 | AlertCommandResponsibilityTransfer = "O" 15 | // AlertCommandSilence means silence 16 | AlertCommandSilence = "S" 17 | ) 18 | 19 | // ARC - Alert command refused 20 | // https://fcc.report/FCC-ID/ADB9ZWRTR100/2768717.pdf (page 7) FURUNO MARINE RADAR, model FAR-15XX manual 21 | // 22 | // Format: $--ARC,hhmmss.ss,aaa,x.x,x.x,c*hh 23 | // Example: $RAARC,220516,TCK,002,1,A*73 24 | type ARC struct { 25 | BaseSentence 26 | 27 | // Time is UTC Time 28 | Time Time // 0 29 | 30 | // ManufacturerMnemonicCode is manufacturer mnemonic code 31 | ManufacturerMnemonicCode string // 1 32 | 33 | // AlertIdentifier is alert identifier (001 to 99999) 34 | AlertIdentifier int64 // 2 35 | 36 | // AlertInstance is alert instance 37 | AlertInstance int64 // 3 38 | 39 | // Command is Refused alert command 40 | // A - acknowledge 41 | // Q - request/repeat information 42 | // O - responsibility transfer 43 | // S - silence 44 | Command string // 4 45 | } 46 | 47 | // newARC constructor 48 | func newARC(s BaseSentence) (Sentence, error) { 49 | p := NewParser(s) 50 | p.AssertType(TypeARC) 51 | return ARC{ 52 | BaseSentence: s, 53 | Time: p.Time(0, "time"), 54 | ManufacturerMnemonicCode: p.String(1, "manufacturer mnemonic code"), 55 | AlertIdentifier: p.Int64(2, "alert identifier"), 56 | AlertInstance: p.Int64(3, "alert instance"), 57 | Command: p.EnumString(4, "refused alert command", AlertCommandAcknowledge, AlertCommandRequestRepeatInformation, AlertCommandResponsibilityTransfer, AlertCommandSilence), 58 | }, p.Err() 59 | } 60 | -------------------------------------------------------------------------------- /arc_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestARC(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg ARC 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$RAARC,220516,TCK,002,1,A*73", 18 | msg: ARC{ 19 | Time: Time{ 20 | Valid: true, 21 | Hour: 22, 22 | Minute: 05, 23 | Second: 16, 24 | Millisecond: 0, 25 | }, 26 | ManufacturerMnemonicCode: "TCK", 27 | AlertIdentifier: 2, 28 | AlertInstance: 1, 29 | Command: AlertCommandAcknowledge, 30 | }, 31 | }, 32 | { 33 | name: "invalid nmea: Time", 34 | raw: "$RAARC,2x0516,TCK,002,1,A*39", 35 | err: "nmea: RAARC invalid time: 2x0516", 36 | }, 37 | { 38 | name: "invalid nmea: AlertIdentifier", 39 | raw: "$RAARC,220516,TCK,x02,1,A*3b", 40 | err: "nmea: RAARC invalid alert identifier: x02", 41 | }, 42 | { 43 | name: "invalid nmea: AlertInstance", 44 | raw: "$RAARC,220516,TCK,002,x,A*3a", 45 | err: "nmea: RAARC invalid alert instance: x", 46 | }, 47 | { 48 | name: "invalid nmea: Command", 49 | raw: "$RAARC,220516,TCK,002,1,x*4a", 50 | err: "nmea: RAARC invalid refused alert command: x", 51 | }, 52 | } 53 | for _, tt := range tests { 54 | t.Run(tt.name, func(t *testing.T) { 55 | m, err := Parse(tt.raw) 56 | if tt.err != "" { 57 | assert.Error(t, err) 58 | assert.EqualError(t, err, tt.err) 59 | } else { 60 | assert.NoError(t, err) 61 | arc := m.(ARC) 62 | arc.BaseSentence = BaseSentence{} 63 | assert.Equal(t, tt.msg, arc) 64 | } 65 | }) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /bbm.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeBBM type of BBM sentence for AIS broadcast binary message 5 | TypeBBM = "BBM" 6 | ) 7 | 8 | // BBM - AIS broadcast binary message 9 | // https://fcc.report/FCC-ID/ADB9ZWRTR100/2768717.pdf (page 7) FURUNO MARINE RADAR, model FAR-15XX manual 10 | // 11 | // Format: !--BBM,x,x,x,x,xx,s—s,x*hh 12 | // Example: !AIBBM,26,2,1,3,8,177KQJ5000G?tO`K>RA1wUbN0TKH,0*2C 13 | type BBM struct { 14 | BaseSentence 15 | 16 | // NumFragments is total number of fragments/sentences need to transfer the message (1 - 9) 17 | NumFragments int64 // 0 18 | 19 | // FragmentNumber is current fragment/sentence number (1 - 9) 20 | FragmentNumber int64 // 1 21 | 22 | // MessageID is sequential message identifier (0 - 9) 23 | MessageID int64 // 2 24 | 25 | // Channel is AIS channel for broadcast of the radio message (0 - 3) 26 | // 0 - no broadcast 27 | // 1 - on AIS channel A 28 | // 2 - on AIS channel B 29 | // 3 - broadcast on both AIS channels 30 | Channel string // 3 31 | 32 | // VDLMessageNumber is ITU-r M.1371 message number (8/14) 33 | VDLMessageNumber int64 // 4 34 | 35 | // Payload is encapsulated data (6 bit binary-converted data) (1 - 63 bytes) 36 | Payload []byte // 5 37 | // 6 - Number of fill bits (0 - 5) 38 | } 39 | 40 | // newBBM constructor 41 | func newBBM(s BaseSentence) (Sentence, error) { 42 | p := NewParser(s) 43 | p.AssertType(TypeBBM) 44 | m := BBM{ 45 | BaseSentence: s, 46 | NumFragments: p.Int64(0, "number of fragments"), 47 | FragmentNumber: p.Int64(1, "fragment number"), 48 | MessageID: p.Int64(2, "message ID"), 49 | Channel: p.String(3, "channel"), 50 | VDLMessageNumber: p.Int64(4, "VDL message number"), 51 | Payload: p.SixBitASCIIArmour(5, int(p.Int64(6, "number of padding bits")), "payload"), 52 | } 53 | return m, p.Err() 54 | } 55 | -------------------------------------------------------------------------------- /bec.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeBEC type of BEC sentence for bearing and distance to waypoint (dead reckoning) 5 | TypeBEC = "BEC" 6 | ) 7 | 8 | // BEC - bearing and distance to waypoint (dead reckoning) 9 | // http://www.nmea.de/nmea0183datensaetze.html#bec 10 | // https://www.eye4software.com/hydromagic/documentation/nmea0183/ 11 | // 12 | // Format: $--BEC,hhmmss.ss,llll.ll,a,yyyyy.yy,a,x.x,T,x.x,M,x.x,N,c--c*hh 13 | // Example: $GPBEC,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*33 14 | type BEC struct { 15 | BaseSentence 16 | Time Time // UTC Time 17 | Latitude float64 // latitude of waypoint 18 | Longitude float64 // longitude of waypoint 19 | BearingTrue float64 // true bearing in degrees 20 | BearingTrueValid bool // is unit of true bearing valid 21 | BearingMagnetic float64 // magnetic bearing in degrees 22 | BearingMagneticValid bool // is unit of magnetic bearing valid 23 | DistanceNauticalMiles float64 // distance to waypoint in nautical miles 24 | DistanceNauticalMilesValid bool // is unit of distance to waypoint nautical miles valid 25 | DestinationWaypointID string // destination waypoint ID 26 | } 27 | 28 | // newBEC constructor 29 | func newBEC(s BaseSentence) (Sentence, error) { 30 | p := NewParser(s) 31 | p.AssertType(TypeBEC) 32 | return BEC{ 33 | BaseSentence: s, 34 | Time: p.Time(0, "time"), 35 | Latitude: p.LatLong(1, 2, "latitude"), 36 | Longitude: p.LatLong(3, 4, "longitude"), 37 | BearingTrue: p.Float64(5, "true bearing"), 38 | BearingTrueValid: p.EnumString(6, "true bearing unit valid", BearingTrue) == BearingTrue, 39 | BearingMagnetic: p.Float64(7, "magnetic bearing"), 40 | BearingMagneticValid: p.EnumString(8, "magnetic bearing unit valid", BearingMagnetic) == BearingMagnetic, 41 | DistanceNauticalMiles: p.Float64(9, "distance to waypoint is nautical miles"), 42 | DistanceNauticalMilesValid: p.EnumString(10, "is distance to waypoint nautical miles valid", DistanceUnitNauticalMile) == DistanceUnitNauticalMile, 43 | DestinationWaypointID: p.String(11, "destination waypoint ID"), 44 | }, p.Err() 45 | } 46 | -------------------------------------------------------------------------------- /bec_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestBEC(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg BEC 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$GPBEC,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*33", 18 | msg: BEC{ 19 | Time: Time{ 20 | Valid: true, 21 | Hour: 22, 22 | Minute: 5, 23 | Second: 16, 24 | Millisecond: 0, 25 | }, 26 | Latitude: 51.50033333333334, 27 | Longitude: -0.7723333333333334, 28 | BearingTrue: 213.8, 29 | BearingTrueValid: true, 30 | BearingMagnetic: 218, 31 | BearingMagneticValid: true, 32 | DistanceNauticalMiles: 4.6, 33 | DistanceNauticalMilesValid: true, 34 | DestinationWaypointID: "EGLM", 35 | }, 36 | }, 37 | { 38 | name: "invalid nmea: Time", 39 | raw: "$GPBEC,2x0516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*79", 40 | err: "nmea: GPBEC invalid time: 2x0516", 41 | }, 42 | { 43 | name: "invalid nmea: BearingTrueValid", 44 | raw: "$GPBEC,220516,5130.02,N,00046.34,W,213.8,M,218.0,M,0004.6,N,EGLM*2A", 45 | err: "nmea: GPBEC invalid true bearing unit valid: M", 46 | }, 47 | { 48 | name: "invalid nmea: BearingMagneticValid", 49 | raw: "$GPBEC,220516,5130.02,N,00046.34,W,213.8,T,218.0,T,0004.6,N,EGLM*2A", 50 | err: "nmea: GPBEC invalid magnetic bearing unit valid: T", 51 | }, 52 | { 53 | name: "invalid nmea: DistanceNauticalMilesValid", 54 | raw: "$GPBEC,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,K,EGLM*36", 55 | err: "nmea: GPBEC invalid is distance to waypoint nautical miles valid: K", 56 | }, 57 | } 58 | for _, tt := range tests { 59 | t.Run(tt.name, func(t *testing.T) { 60 | m, err := Parse(tt.raw) 61 | if tt.err != "" { 62 | assert.Error(t, err) 63 | assert.EqualError(t, err, tt.err) 64 | } else { 65 | assert.NoError(t, err) 66 | bec := m.(BEC) 67 | bec.BaseSentence = BaseSentence{} 68 | assert.Equal(t, tt.msg, bec) 69 | } 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /bod.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeBOD type of BOD sentence for bearing waypoint to waypoint 5 | TypeBOD = "BOD" 6 | ) 7 | 8 | // BOD - bearing waypoint to waypoint (origin to destination). 9 | // Replaced by BWW in NMEA4+ (according to GPSD docs) 10 | // If your system supports RMB it is better to use RMB as it is more common (according to OpenCPN docs) 11 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_bod_bearing_waypoint_to_waypoint 12 | // 13 | // Format: $--BOD,x.x,T,x.x,M,c--c,c--c*hh 14 | // Example: $GPBOD,099.3,T,105.6,M,POINTB*64 15 | // $GPBOD,097.0,T,103.2,M,POINTB,POINTA*4A 16 | type BOD struct { 17 | BaseSentence 18 | BearingTrue float64 // true bearing in degrees 19 | BearingTrueType string // is type of true bearing 20 | BearingMagnetic float64 // magnetic bearing in degrees 21 | BearingMagneticType string // is type of magnetic bearing 22 | DestinationWaypointID string // destination waypoint ID 23 | OriginWaypointID string // origin waypoint ID 24 | } 25 | 26 | // newBOD constructor 27 | func newBOD(s BaseSentence) (Sentence, error) { 28 | p := NewParser(s) 29 | p.AssertType(TypeBOD) 30 | bod := BOD{ 31 | BaseSentence: s, 32 | BearingTrue: p.Float64(0, "true bearing"), 33 | BearingTrueType: p.EnumString(1, "true bearing type", BearingTrue), 34 | BearingMagnetic: p.Float64(2, "magnetic bearing"), 35 | BearingMagneticType: p.EnumString(3, "magnetic bearing type", BearingMagnetic), 36 | DestinationWaypointID: p.String(4, "destination waypoint ID"), 37 | OriginWaypointID: "", 38 | } 39 | // According to GSPD docs: OriginWaypointID is not transmitted in the GOTO mode, without an active route on your GPS. 40 | // in that case you have only DestinationWaypointID 41 | if len(p.Fields) > 5 { 42 | bod.OriginWaypointID = p.String(5, "origin waypoint ID") 43 | } 44 | return bod, p.Err() 45 | } 46 | -------------------------------------------------------------------------------- /bod_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestBOD(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg BOD 14 | }{ 15 | { 16 | name: "good sentence with both WPs", 17 | raw: "$GPBOD,097.0,T,103.2,M,POINTB,POINTA*4A", 18 | msg: BOD{ 19 | BearingTrue: 97.0, 20 | BearingTrueType: BearingTrue, 21 | BearingMagnetic: 103.2, 22 | BearingMagneticType: BearingMagnetic, 23 | DestinationWaypointID: "POINTB", 24 | OriginWaypointID: "POINTA", 25 | }, 26 | }, 27 | { 28 | name: "good sentence onyl destination", 29 | raw: "$GPBOD,099.3,T,105.6,M,POINTB*64", 30 | msg: BOD{ 31 | BearingTrue: 99.3, 32 | BearingTrueType: BearingTrue, 33 | BearingMagnetic: 105.6, 34 | BearingMagneticType: BearingMagnetic, 35 | DestinationWaypointID: "POINTB", 36 | OriginWaypointID: "", 37 | }, 38 | }, 39 | { 40 | name: "invalid nmea: BearingTrueValid", 41 | raw: "$GPBOD,097.0,M,103.2,M,POINTB,POINTA*53", 42 | err: "nmea: GPBOD invalid true bearing type: M", 43 | }, 44 | { 45 | name: "invalid nmea: BearingMagneticValid", 46 | raw: "$GPBOD,097.0,T,103.2,T,POINTB,POINTA*53", 47 | err: "nmea: GPBOD invalid magnetic bearing type: T", 48 | }, 49 | } 50 | for _, tt := range tests { 51 | t.Run(tt.name, func(t *testing.T) { 52 | m, err := Parse(tt.raw) 53 | if tt.err != "" { 54 | assert.Error(t, err) 55 | assert.EqualError(t, err, tt.err) 56 | } else { 57 | assert.NoError(t, err) 58 | bod := m.(BOD) 59 | bod.BaseSentence = BaseSentence{} 60 | assert.Equal(t, tt.msg, bod) 61 | } 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /bwc.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeBWC type of BWC sentence for bearing and distance to waypoint, great circle 5 | TypeBWC = "BWC" 6 | ) 7 | 8 | // BWC - bearing and distance to waypoint, great circle 9 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_bwc_bearing_distance_to_waypoint_great_circle 10 | // http://aprs.gids.nl/nmea/#bwc 11 | // 12 | // Format: $--BWC,hhmmss.ss,llll.ll,a,yyyyy.yy,a,x.x,T,x.x,M,x.x,N,c--c*hh 13 | // 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 14 | // Example: $GPBWC,081837,,,,,,T,,M,,N,*13 15 | // $GPBWC,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*21 16 | type BWC struct { 17 | BaseSentence 18 | Time Time // UTC Time 19 | Latitude float64 // latitude of waypoint 20 | Longitude float64 // longitude of waypoint 21 | BearingTrue float64 // true bearing in degrees 22 | BearingTrueType string // is type of true bearing 23 | BearingMagnetic float64 // magnetic bearing in degrees 24 | BearingMagneticType string // is type of magnetic bearing 25 | DistanceNauticalMiles float64 // distance to waypoint in nautical miles 26 | DistanceNauticalMilesUnit string // is unit of distance to waypoint nautical miles 27 | DestinationWaypointID string // destination waypoint ID 28 | FFAMode string // FAA mode indicator (filled in NMEA 2.3 and later) 29 | } 30 | 31 | // newBWC constructor 32 | func newBWC(s BaseSentence) (Sentence, error) { 33 | p := NewParser(s) 34 | p.AssertType(TypeBWC) 35 | bwc := BWC{ 36 | BaseSentence: s, 37 | Time: p.Time(0, "time"), 38 | Latitude: p.LatLong(1, 2, "latitude"), 39 | Longitude: p.LatLong(3, 4, "longitude"), 40 | BearingTrue: p.Float64(5, "true bearing"), 41 | BearingTrueType: p.EnumString(6, "true bearing type", BearingTrue), 42 | BearingMagnetic: p.Float64(7, "magnetic bearing"), 43 | BearingMagneticType: p.EnumString(8, "magnetic bearing type", BearingMagnetic), 44 | DistanceNauticalMiles: p.Float64(9, "distance to waypoint is nautical miles"), 45 | DistanceNauticalMilesUnit: p.EnumString(10, "is distance to waypoint nautical miles unit", DistanceUnitNauticalMile), 46 | DestinationWaypointID: p.String(11, "destination waypoint ID"), 47 | } 48 | if len(p.Fields) > 12 { 49 | bwc.FFAMode = p.String(12, "FAA mode") // not enum because some devices have proprietary "non-nmea" values 50 | } 51 | return bwc, p.Err() 52 | } 53 | -------------------------------------------------------------------------------- /bww.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeBWW type of BWW sentence for bearing (from destination) destination waypoint to origin waypoint 5 | TypeBWW = "BWW" 6 | ) 7 | 8 | // BWW - bearing (from destination) destination waypoint to origin waypoint 9 | // Replaces by BOD in NMEA4+ (according to GPSD docs) 10 | // If your system supports RMB it is better to use RMB as it is more common (according to OpenCPN docs) 11 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_bww_bearing_waypoint_to_waypoint 12 | // http://www.nmea.de/nmea0183datensaetze.html#bww 13 | // 14 | // Format: $--BWW,x.x,T,x.x,M,c--c,c--c*hh 15 | // Example: $GPBWW,097.0,T,103.2,M,POINTB,POINTA*41 16 | type BWW struct { 17 | BaseSentence 18 | BearingTrue float64 // true bearing in degrees 19 | BearingTrueType string // is type of true bearing 20 | BearingMagnetic float64 // magnetic bearing in degrees 21 | BearingMagneticType string // is type of magnetic bearing 22 | DestinationWaypointID string // destination waypoint ID 23 | OriginWaypointID string // origin waypoint ID 24 | } 25 | 26 | // newBWW constructor 27 | func newBWW(s BaseSentence) (Sentence, error) { 28 | p := NewParser(s) 29 | p.AssertType(TypeBWW) 30 | bod := BWW{ 31 | BaseSentence: s, 32 | BearingTrue: p.Float64(0, "true bearing"), 33 | BearingTrueType: p.EnumString(1, "true bearing type", BearingTrue), 34 | BearingMagnetic: p.Float64(2, "magnetic bearing"), 35 | BearingMagneticType: p.EnumString(3, "magnetic bearing type", BearingMagnetic), 36 | DestinationWaypointID: p.String(4, "destination waypoint ID"), 37 | OriginWaypointID: p.String(5, "origin waypoint ID"), 38 | } 39 | return bod, p.Err() 40 | } 41 | -------------------------------------------------------------------------------- /bww_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestBWW(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg BWW 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$GPBWW,097.0,T,103.2,M,POINTB,POINTA*41", 18 | msg: BWW{ 19 | BearingTrue: 97.0, 20 | BearingTrueType: BearingTrue, 21 | BearingMagnetic: 103.2, 22 | BearingMagneticType: BearingMagnetic, 23 | DestinationWaypointID: "POINTB", 24 | OriginWaypointID: "POINTA", 25 | }, 26 | }, 27 | { 28 | name: "invalid nmea: BearingTrueValid", 29 | raw: "$GPBWW,097.0,M,103.2,M,POINTB,POINTA*58", 30 | err: "nmea: GPBWW invalid true bearing type: M", 31 | }, 32 | { 33 | name: "invalid nmea: BearingMagneticValid", 34 | raw: "$GPBWW,097.0,T,103.2,T,POINTB,POINTA*58", 35 | err: "nmea: GPBWW invalid magnetic bearing type: T", 36 | }, 37 | } 38 | for _, tt := range tests { 39 | t.Run(tt.name, func(t *testing.T) { 40 | m, err := Parse(tt.raw) 41 | if tt.err != "" { 42 | assert.Error(t, err) 43 | assert.EqualError(t, err, tt.err) 44 | } else { 45 | assert.NoError(t, err) 46 | bww := m.(BWW) 47 | bww.BaseSentence = BaseSentence{} 48 | assert.Equal(t, tt.msg, bww) 49 | } 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /dbk.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeDBK type of DBK sentence for Depth Below Keel 5 | TypeDBK = "DBK" 6 | ) 7 | 8 | // DBK - Depth Below Keel (obsolete, use DPT instead) 9 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_dbk_depth_below_keel 10 | // https://wiki.openseamap.org/wiki/OpenSeaMap-dev:NMEA#DBK_-_Depth_below_keel 11 | // 12 | // Format: $--DBK,x.x,f,x.x,M,x.x,F*hh 13 | // Example: $SDDBK,12.3,f,3.7,M,2.0,F*2F 14 | type DBK struct { 15 | BaseSentence 16 | DepthFeet float64 // Depth, feet 17 | DepthFeetUnit string // f = feet 18 | DepthMeters float64 // Depth, meters 19 | DepthMetersUnit string // M = meters 20 | DepthFathoms float64 // Depth, Fathoms 21 | DepthFathomsUnit string // F = Fathoms 22 | } 23 | 24 | // newDBK constructor 25 | func newDBK(s BaseSentence) (Sentence, error) { 26 | p := NewParser(s) 27 | p.AssertType(TypeDBK) 28 | return DBK{ 29 | BaseSentence: s, 30 | DepthFeet: p.Float64(0, "depth feet"), 31 | DepthFeetUnit: p.EnumString(1, "depth feet unit", DistanceUnitFeet), 32 | DepthMeters: p.Float64(2, "depth meters"), 33 | DepthMetersUnit: p.EnumString(3, "depth meters unit", DistanceUnitMetre), 34 | DepthFathoms: p.Float64(4, "depth fathom"), 35 | DepthFathomsUnit: p.EnumString(5, "depth fathom unit", DistanceUnitFathom), 36 | }, p.Err() 37 | } 38 | -------------------------------------------------------------------------------- /dbk_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestDBK(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg DBK 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$SDDBK,12.3,f,3.7,M,2.0,F*2F", 18 | msg: DBK{ 19 | DepthFeet: 12.3, 20 | DepthFeetUnit: DistanceUnitFeet, 21 | DepthMeters: 3.7, 22 | DepthMetersUnit: DistanceUnitMetre, 23 | DepthFathoms: 2, 24 | DepthFathomsUnit: DistanceUnitFathom, 25 | }, 26 | }, 27 | { 28 | name: "invalid nmea: DepthFeetUnit", 29 | raw: "$SDDBK,12.3,x,3.7,M,2.0,F*31", 30 | err: "nmea: SDDBK invalid depth feet unit: x", 31 | }, 32 | { 33 | name: "invalid nmea: DepthMeterUnit", 34 | raw: "$SDDBK,12.3,f,3.7,x,2.0,F*1A", 35 | err: "nmea: SDDBK invalid depth meters unit: x", 36 | }, 37 | { 38 | name: "invalid nmea: DepthFathomUnit", 39 | raw: "$SDDBK,12.3,f,3.7,M,2.0,x*11", 40 | err: "nmea: SDDBK invalid depth fathom unit: x", 41 | }, 42 | } 43 | for _, tt := range tests { 44 | t.Run(tt.name, func(t *testing.T) { 45 | m, err := Parse(tt.raw) 46 | if tt.err != "" { 47 | assert.Error(t, err) 48 | assert.EqualError(t, err, tt.err) 49 | } else { 50 | assert.NoError(t, err) 51 | dbk := m.(DBK) 52 | dbk.BaseSentence = BaseSentence{} 53 | assert.Equal(t, tt.msg, dbk) 54 | } 55 | }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /dbs.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeDBS is type of DBS sentence for Depth Below Surface 5 | TypeDBS = "DBS" 6 | ) 7 | 8 | // DBS - Depth Below Surface (obsolete, use DPT instead) 9 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_dbs_depth_below_surface 10 | // https://wiki.openseamap.org/wiki/OpenSeaMap-dev:NMEA#DBS_-_Depth_below_surface 11 | // 12 | // Format: $--DBS,x.x,f,x.x,M,x.x,F*hh 13 | // Example: $23DBS,01.9,f,0.58,M,00.3,F*21 14 | type DBS struct { 15 | BaseSentence 16 | DepthFeet float64 // Depth, feet 17 | DepthFeetUnit string // f = feet 18 | DepthMeters float64 // Depth, meters 19 | DepthMeterUnit string // M = meters 20 | DepthFathoms float64 // Depth, Fathoms 21 | DepthFathomUnit string // F = Fathoms 22 | } 23 | 24 | // newDBS constructor 25 | func newDBS(s BaseSentence) (Sentence, error) { 26 | p := NewParser(s) 27 | p.AssertType(TypeDBS) 28 | return DBS{ 29 | BaseSentence: s, 30 | DepthFeet: p.Float64(0, "depth feet"), 31 | DepthFeetUnit: p.EnumString(1, "depth feet unit", DistanceUnitFeet), 32 | DepthMeters: p.Float64(2, "depth meters"), 33 | DepthMeterUnit: p.EnumString(3, "depth feet unit", DistanceUnitMetre), 34 | DepthFathoms: p.Float64(4, "depth fathoms"), 35 | DepthFathomUnit: p.EnumString(5, "depth fathom unit", DistanceUnitFathom), 36 | }, p.Err() 37 | } 38 | -------------------------------------------------------------------------------- /dbs_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestDBS(t *testing.T) { 10 | var dbstests = []struct { 11 | name string 12 | raw string 13 | err string 14 | msg DBS 15 | }{ 16 | { 17 | name: "good sentence", 18 | raw: "$23DBS,01.9,f,0.58,M,00.3,F*21", 19 | msg: DBS{ 20 | DepthFeet: 1.9, 21 | DepthFeetUnit: DistanceUnitFeet, 22 | DepthMeters: 0.58, 23 | DepthMeterUnit: DistanceUnitMetre, 24 | DepthFathoms: 0.3, 25 | DepthFathomUnit: DistanceUnitFathom, 26 | }, 27 | }, 28 | { 29 | name: "good sentence 2", 30 | raw: "$SDDBS,,,0187.5,M,,*1A", // Simrad ITI Trawl System 31 | msg: DBS{ 32 | DepthFeet: 0, 33 | DepthFeetUnit: "", 34 | DepthMeters: 187.5, 35 | DepthMeterUnit: DistanceUnitMetre, 36 | DepthFathoms: 0, 37 | DepthFathomUnit: "", 38 | }, 39 | }, 40 | { 41 | name: "bad validity", 42 | raw: "$23DBS,01.9,f,0.58,M,00.3,F*25", 43 | err: "nmea: sentence checksum mismatch [21 != 25]", 44 | }, 45 | } 46 | 47 | for _, tt := range dbstests { 48 | t.Run(tt.name, func(t *testing.T) { 49 | m, err := Parse(tt.raw) 50 | if tt.err != "" { 51 | assert.Error(t, err) 52 | assert.EqualError(t, err, tt.err) 53 | } else { 54 | assert.NoError(t, err) 55 | dbs := m.(DBS) 56 | dbs.BaseSentence = BaseSentence{} 57 | assert.Equal(t, tt.msg, dbs) 58 | } 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /dbt.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeDBT type for DBT sentences 5 | TypeDBT = "DBT" 6 | ) 7 | 8 | // DBT - Depth below transducer 9 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_dbt_depth_below_transducer 10 | // 11 | // Format: $--DBT,x.x,f,x.x,M,x.x,F*hh 12 | // Example: $IIDBT,032.93,f,010.04,M,005.42,F*2C 13 | type DBT struct { 14 | BaseSentence 15 | DepthFeet float64 16 | DepthMeters float64 17 | DepthFathoms float64 18 | } 19 | 20 | // newDBT constructor 21 | func newDBT(s BaseSentence) (Sentence, error) { 22 | p := NewParser(s) 23 | p.AssertType(TypeDBT) 24 | return DBT{ 25 | BaseSentence: s, 26 | DepthFeet: p.Float64(0, "depth_feet"), 27 | DepthMeters: p.Float64(2, "depth_meters"), 28 | DepthFathoms: p.Float64(4, "depth_fathoms"), 29 | }, p.Err() 30 | } 31 | -------------------------------------------------------------------------------- /dbt_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | var dbttests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg DBT 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$IIDBT,032.93,f,010.04,M,005.42,F*2C", 18 | msg: DBT{ 19 | DepthFeet: 32.93, 20 | DepthMeters: 10.04, 21 | DepthFathoms: 5.42, 22 | }, 23 | }, 24 | { 25 | name: "bad validity", 26 | raw: "$IIDBT,032.93,f,010.04,M,005.42,F*22", 27 | err: "nmea: sentence checksum mismatch [2C != 22]", 28 | }, 29 | } 30 | 31 | func TestDBT(t *testing.T) { 32 | for _, tt := range dbttests { 33 | t.Run(tt.name, func(t *testing.T) { 34 | m, err := Parse(tt.raw) 35 | if tt.err != "" { 36 | assert.Error(t, err) 37 | assert.EqualError(t, err, tt.err) 38 | } else { 39 | assert.NoError(t, err) 40 | dbt := m.(DBT) 41 | dbt.BaseSentence = BaseSentence{} 42 | assert.Equal(t, tt.msg, dbt) 43 | } 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /dor_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestDOR(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg DOR 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$FRDOR,E,233042,FD,FP,000,010,C,C,Door Closed : TEST FPA Name*4D", 18 | msg: DOR{ 19 | Type: TypeSingleDoorDOR, 20 | Time: Time{ 21 | Valid: true, 22 | Hour: 23, 23 | Minute: 30, 24 | Second: 42, 25 | Millisecond: 0, 26 | }, 27 | SystemIndicator: "FD", 28 | DivisionIndicator1: "FP", 29 | DivisionIndicator2: 0, 30 | DoorNumberOrCount: 10, 31 | DoorStatus: DoorStatusClosedDOR, 32 | SwitchSetting: SwitchSettingSeaModeDOR, 33 | Message: "Door Closed : TEST FPA Name", 34 | }, 35 | }, 36 | { 37 | name: "invalid nmea: Type", 38 | raw: "$FRDOR,x,233042,FD,FP,000,010,C,C,Door Closed : TEST FPA Name*70", 39 | err: "nmea: FRDOR invalid message type: x", 40 | }, 41 | { 42 | name: "invalid nmea: Time", 43 | raw: "$FRDOR,E,2x3042,FD,FP,000,010,C,C,Door Closed : TEST FPA Name*06", 44 | err: "nmea: FRDOR invalid time: 2x3042", 45 | }, 46 | { 47 | name: "invalid nmea: DoorStatus", 48 | raw: "$FRDOR,E,233042,FD,FP,000,010,_,C,Door Closed : TEST FPA Name*51", 49 | err: "nmea: FRDOR invalid door state: _", 50 | }, 51 | { 52 | name: "invalid nmea: SwitchSetting", 53 | raw: "$FRDOR,E,233042,FD,FP,000,010,C,_,Door Closed : TEST FPA Name*51", 54 | err: "nmea: FRDOR invalid switch setting mode: _", 55 | }, 56 | } 57 | for _, tt := range tests { 58 | t.Run(tt.name, func(t *testing.T) { 59 | m, err := Parse(tt.raw) 60 | if tt.err != "" { 61 | assert.Error(t, err) 62 | assert.EqualError(t, err, tt.err) 63 | } else { 64 | assert.NoError(t, err) 65 | dor := m.(DOR) 66 | dor.BaseSentence = BaseSentence{} 67 | assert.Equal(t, tt.msg, dor) 68 | } 69 | }) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /dpt.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeDPT type for DPT sentences 5 | TypeDPT = "DPT" 6 | ) 7 | 8 | // DPT - Depth of Water 9 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_dpt_depth_of_water 10 | // 11 | // Format: $--DPT,x.x,x.x,x.x*hh 12 | // Example: $SDDPT,0.5,0.5,*7B 13 | // $INDPT,2.3,0.0*46 14 | type DPT struct { 15 | BaseSentence 16 | Depth float64 // Water depth relative to transducer, meters 17 | Offset float64 // offset from transducer 18 | RangeScale float64 // OPTIONAL, Maximum range scale in use (NMEA 3.0 and above) 19 | } 20 | 21 | // newDPT constructor 22 | func newDPT(s BaseSentence) (Sentence, error) { 23 | p := NewParser(s) 24 | p.AssertType(TypeDPT) 25 | dpt := DPT{ 26 | BaseSentence: s, 27 | Depth: p.Float64(0, "depth"), 28 | Offset: p.Float64(1, "offset"), 29 | } 30 | if len(p.Fields) > 2 { 31 | dpt.RangeScale = p.Float64(2, "range scale") 32 | } 33 | return dpt, p.Err() 34 | } 35 | -------------------------------------------------------------------------------- /dpt_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | var dpttests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg DPT 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$SDDPT,0.5,0.5,*7B", 18 | msg: DPT{ 19 | Depth: 0.5, 20 | Offset: 0.5, 21 | RangeScale: 0, 22 | }, 23 | }, 24 | { 25 | name: "good sentence with scale", 26 | raw: "$SDDPT,0.5,0.5,0.1*54", 27 | msg: DPT{ 28 | Depth: 0.5, 29 | Offset: 0.5, 30 | RangeScale: 0.1, 31 | }, 32 | }, 33 | { 34 | name: "good sentence with 2 fields", 35 | raw: "$INDPT,2.3,0.0*46", 36 | msg: DPT{ 37 | Depth: 2.3, 38 | Offset: 0, 39 | RangeScale: 0, 40 | }, 41 | }, 42 | { 43 | name: "bad validity", 44 | raw: "$SDDPT,0.5,0.5,*AA", 45 | err: "nmea: sentence checksum mismatch [7B != AA]", 46 | }, 47 | } 48 | 49 | func TestDPT(t *testing.T) { 50 | for _, tt := range dpttests { 51 | t.Run(tt.name, func(t *testing.T) { 52 | m, err := Parse(tt.raw) 53 | if tt.err != "" { 54 | assert.Error(t, err) 55 | assert.EqualError(t, err, tt.err) 56 | } else { 57 | assert.NoError(t, err) 58 | dpt := m.(DPT) 59 | dpt.BaseSentence = BaseSentence{} 60 | assert.Equal(t, tt.msg, dpt) 61 | } 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /dse_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestDSE(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg DSE 14 | }{ 15 | { 16 | name: "good sentence, single dataset", 17 | raw: "$CDDSE,1,1,A,3380400790,00,46504437*15", 18 | msg: DSE{ 19 | TotalNumber: 1, 20 | Number: 1, 21 | Acknowledgement: AcknowledgementAutomaticDSE, 22 | MMSI: "3380400790", 23 | DataSets: []DSEDataSet{ 24 | {Code: "00", Data: "46504437"}, 25 | }, 26 | }, 27 | }, 28 | { 29 | name: "good sentence, single dataset", 30 | raw: "$CDDSE,1,1,A,3380400790,00,46504437,01,16501437*17", 31 | msg: DSE{ 32 | TotalNumber: 1, 33 | Number: 1, 34 | Acknowledgement: AcknowledgementAutomaticDSE, 35 | MMSI: "3380400790", 36 | DataSets: []DSEDataSet{ 37 | {Code: "00", Data: "46504437"}, 38 | {Code: "01", Data: "16501437"}, 39 | }, 40 | }, 41 | }, 42 | { 43 | name: "invalid nmea: field count", 44 | raw: "$CDDSE,1,1,x,3380400790,46504437*00", 45 | err: "DSE is missing fields for parsing data sets", 46 | }, 47 | { 48 | name: "invalid nmea: data set field count", 49 | raw: "$CDDSE,1,1,A,3380400790,00,46504437,01*38", 50 | err: "DSE data set field count is not exactly dividable by 2", 51 | }, 52 | { 53 | name: "invalid nmea: Acknowledgement", 54 | raw: "$CDDSE,1,1,x,3380400790,00,46504437*2c", 55 | err: "nmea: CDDSE invalid acknowledgement: x", 56 | }, 57 | } 58 | for _, tt := range tests { 59 | t.Run(tt.name, func(t *testing.T) { 60 | m, err := Parse(tt.raw) 61 | if tt.err != "" { 62 | assert.Error(t, err) 63 | assert.EqualError(t, err, tt.err) 64 | } else { 65 | assert.NoError(t, err) 66 | dse := m.(DSE) 67 | dse.BaseSentence = BaseSentence{} 68 | assert.Equal(t, tt.msg, dse) 69 | } 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /dtm.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeDTM type of DTM sentence for Datum Reference 5 | TypeDTM = "DTM" 6 | ) 7 | 8 | // DTM - Datum Reference 9 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_dtm_datum_reference 10 | // 11 | // Format: $--DTM,ref,x,llll,c,llll,c,aaa,ref*hh 12 | // Example: $GPDTM,W84,,0.0,N,0.0,E,0.0,W84*6F 13 | // Example: $GPDTM,W84,,00.0000,N,00.0000,W,,W84*53 14 | type DTM struct { 15 | BaseSentence 16 | LocalDatumCode string // Local datum code (W84,W72,S85,P90,999) 17 | LocalDatumSubcode string // Local datum subcode. May be blank. 18 | 19 | LatitudeOffsetMinute float64 // Latitude offset (minutes) (negative if south) 20 | LongitudeOffsetMinute float64 // Longitude offset (minutes) (negative if west) 21 | 22 | AltitudeOffsetMeters float64 // Altitude offset in meters 23 | DatumName string // Reference datum name. What’s usually seen here is "W84", the standard WGS84 datum used by GPS. 24 | } 25 | 26 | // newDTM constructor 27 | func newDTM(s BaseSentence) (Sentence, error) { 28 | p := NewParser(s) 29 | p.AssertType(TypeDTM) 30 | m := DTM{ 31 | BaseSentence: s, 32 | LocalDatumCode: p.String(0, "local datum code"), 33 | LocalDatumSubcode: p.String(1, "local datum subcode"), 34 | 35 | LatitudeOffsetMinute: p.Float64(2, "latitude offset minutes"), 36 | LongitudeOffsetMinute: p.Float64(4, "longitude offset minutes"), 37 | 38 | AltitudeOffsetMeters: p.Float64(6, "altitude offset offset"), 39 | DatumName: p.String(7, "datum name"), 40 | } 41 | if p.String(3, "latitude offset direction") == South { 42 | m.LatitudeOffsetMinute *= -1 43 | } 44 | if p.String(5, "longitude offset direction") == West { 45 | m.LongitudeOffsetMinute *= -1 46 | } 47 | return m, p.Err() 48 | } 49 | -------------------------------------------------------------------------------- /dtm_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestDTM(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg DTM 14 | }{ 15 | { 16 | name: "good sentence 1", 17 | raw: "$GPDTM,W84,,0.0,N,0.0,E,0.0,W84*6F", 18 | msg: DTM{ 19 | BaseSentence: BaseSentence{}, 20 | LocalDatumCode: "W84", 21 | LocalDatumSubcode: "", 22 | LatitudeOffsetMinute: 0, 23 | LongitudeOffsetMinute: 0, 24 | AltitudeOffsetMeters: 0, 25 | DatumName: "W84", 26 | }, 27 | }, 28 | { 29 | name: "good sentence 2", 30 | raw: "$GPDTM,W84,X,00.1200,S,12.0000,W,100,W84*27", 31 | msg: DTM{ 32 | BaseSentence: BaseSentence{}, 33 | LocalDatumCode: "W84", 34 | LocalDatumSubcode: "X", 35 | LatitudeOffsetMinute: -0.12, 36 | LongitudeOffsetMinute: -12, 37 | AltitudeOffsetMeters: 100, 38 | DatumName: "W84", 39 | }, 40 | }, 41 | { 42 | name: "invalid nmea: LatitudeOffsetMinute", 43 | raw: "$GPDTM,W84,,x,N,0.0,E,0.0,W84*39", 44 | err: "nmea: GPDTM invalid latitude offset minutes: x", 45 | }, 46 | { 47 | name: "invalid nmea: LongitudeOffsetMinute", 48 | raw: "$GPDTM,W84,,0.0,N,x,E,0.0,W84*39", 49 | err: "nmea: GPDTM invalid longitude offset minutes: x", 50 | }, 51 | { 52 | name: "invalid nmea: AltitudeOffsetMeters", 53 | raw: "$GPDTM,W84,,0.0,N,0.0,E,x,W84*39", 54 | err: "nmea: GPDTM invalid altitude offset offset: x", 55 | }, 56 | } 57 | for _, tt := range tests { 58 | t.Run(tt.name, func(t *testing.T) { 59 | m, err := Parse(tt.raw) 60 | if tt.err != "" { 61 | assert.Error(t, err) 62 | assert.EqualError(t, err, tt.err) 63 | } else { 64 | assert.NoError(t, err) 65 | mm := m.(DTM) 66 | mm.BaseSentence = BaseSentence{} 67 | assert.Equal(t, tt.msg, mm) 68 | } 69 | }) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /eve.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeEVE type of EVE sentence for General Event Message 5 | TypeEVE = "EVE" 6 | ) 7 | 8 | // EVE - General Event Message 9 | // Source: "Interfacing Voyage Data Recorder Systems, AutroSafe Interactive Fire-Alarm System, 116-P-BSL336/EE, RevA 2007-01-25, 10 | // Autronica Fire and Security AS " (page 34 | p.8.1.5) 11 | // https://product.autronicafire.com/fileshare/fileupload/14251/bsl336_ee.pdf 12 | // 13 | // Format: $FREVE,hhmmss,c--c,c--c*hh 14 | // Example: $FREVE,000001,DZ00513,Fire Alarm On: TEST DZ201 Name*0A 15 | type EVE struct { 16 | BaseSentence 17 | Time Time // Event Time 18 | TagCode string // Tag code 19 | Message string // Event text 20 | } 21 | 22 | // newEVE constructor 23 | func newEVE(s BaseSentence) (Sentence, error) { 24 | p := NewParser(s) 25 | p.AssertType(TypeEVE) 26 | return EVE{ 27 | BaseSentence: s, 28 | Time: p.Time(0, "time"), 29 | TagCode: p.String(1, "tag code"), 30 | Message: p.String(2, "event message text"), 31 | }, p.Err() 32 | } 33 | -------------------------------------------------------------------------------- /eve_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestEVE(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg EVE 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$FREVE,000001,DZ00513,Fire Alarm On: TEST DZ201 Name*0A", 18 | msg: EVE{ 19 | Time: Time{ 20 | Valid: true, 21 | Hour: 0, 22 | Minute: 0, 23 | Second: 1, 24 | Millisecond: 0, 25 | }, 26 | TagCode: "DZ00513", 27 | Message: "Fire Alarm On: TEST DZ201 Name", 28 | }, 29 | }, 30 | { 31 | name: "invalid nmea: Time", 32 | raw: "$FREVE,0x0001,DZ00513,Fire Alarm On: TEST DZ201 Name*42", 33 | err: "nmea: FREVE invalid time: 0x0001", 34 | }, 35 | } 36 | for _, tt := range tests { 37 | t.Run(tt.name, func(t *testing.T) { 38 | m, err := Parse(tt.raw) 39 | if tt.err != "" { 40 | assert.Error(t, err) 41 | assert.EqualError(t, err, tt.err) 42 | } else { 43 | assert.NoError(t, err) 44 | eve := m.(EVE) 45 | eve.BaseSentence = BaseSentence{} 46 | assert.Equal(t, tt.msg, eve) 47 | } 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /fir_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestFIR(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg FIR 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$FRFIR,E,103000,FD,PT,000,007,A,V,Fire Alarm : TEST PT7 Name TEST DZ2 Name*7A", 18 | msg: FIR{ 19 | Type: TypeEventOrAlarmFIR, 20 | Time: Time{ 21 | Valid: true, 22 | Hour: 10, 23 | Minute: 30, 24 | Second: 0, 25 | Millisecond: 0, 26 | }, 27 | SystemIndicator: "FD", 28 | DivisionIndicator1: "PT", 29 | DivisionIndicator2: 0, 30 | FireDetectorNumberOrCount: 7, 31 | Condition: ConditionActivationFIR, 32 | AlarmAckState: AlarmStateNotAcknowledgedFIR, 33 | Message: "Fire Alarm : TEST PT7 Name TEST DZ2 Name", 34 | }, 35 | }, 36 | { 37 | name: "invalid nmea: Type", 38 | raw: "$FRFIR,x,103000,FD,PT,000,007,A,V,Fire Alarm : TEST PT7 Name TEST DZ2 Name*47", 39 | err: "nmea: FRFIR invalid message type: x", 40 | }, 41 | { 42 | name: "invalid nmea: Time", 43 | raw: "$FRFIR,E,1x3000,FD,PT,000,007,A,V,Fire Alarm : TEST PT7 Name TEST DZ2 Name*32", 44 | err: "nmea: FRFIR invalid time: 1x3000", 45 | }, 46 | { 47 | name: "invalid nmea: Condition", 48 | raw: "$FRFIR,E,103000,FD,PT,000,007,_,V,Fire Alarm : TEST PT7 Name TEST DZ2 Name*64", 49 | err: "nmea: FRFIR invalid condition: _", 50 | }, 51 | { 52 | name: "invalid nmea: AlarmAckState", 53 | raw: "$FRFIR,E,103000,FD,PT,000,007,A,_,Fire Alarm : TEST PT7 Name TEST DZ2 Name*73", 54 | err: "nmea: FRFIR invalid alarm acknowledgement state: _", 55 | }, 56 | } 57 | for _, tt := range tests { 58 | t.Run(tt.name, func(t *testing.T) { 59 | m, err := Parse(tt.raw) 60 | if tt.err != "" { 61 | assert.Error(t, err) 62 | assert.EqualError(t, err, tt.err) 63 | } else { 64 | assert.NoError(t, err) 65 | fir := m.(FIR) 66 | fir.BaseSentence = BaseSentence{} 67 | assert.Equal(t, tt.msg, fir) 68 | } 69 | }) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /gga.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeGGA type for GGA sentences 5 | TypeGGA = "GGA" 6 | // Invalid fix quality. 7 | Invalid = "0" 8 | // GPS fix quality 9 | GPS = "1" 10 | // DGPS fix quality 11 | DGPS = "2" 12 | // PPS fix 13 | PPS = "3" 14 | // RTK real time kinematic fix 15 | RTK = "4" 16 | // FRTK float RTK fix 17 | FRTK = "5" 18 | // EST estimated fix. 19 | EST = "6" 20 | ) 21 | 22 | // GGA is the Time, position, and fix related data of the receiver. 23 | // http://aprs.gids.nl/nmea/#gga 24 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_gga_global_positioning_system_fix_data 25 | // 26 | // Format: $--GGA,hhmmss.ss,ddmm.mm,a,ddmm.mm,a,x,xx,x.x,x.x,M,x.x,M,x.x,xxxx*hh 27 | // Example: $GNGGA,203415.000,6325.6138,N,01021.4290,E,1,8,2.42,72.5,M,41.5,M,,*7C 28 | type GGA struct { 29 | BaseSentence 30 | Time Time // Time of fix. 31 | Latitude float64 // Latitude. 32 | Longitude float64 // Longitude. 33 | FixQuality string // Quality of fix. 34 | NumSatellites int64 // Number of satellites in use. 35 | HDOP float64 // Horizontal dilution of precision. 36 | Altitude float64 // Altitude. 37 | Separation float64 // Geoidal separation 38 | DGPSAge string // Age of differential GPD data. 39 | DGPSId string // DGPS reference station ID. 40 | } 41 | 42 | // newGGA constructor 43 | func newGGA(s BaseSentence) (Sentence, error) { 44 | p := NewParser(s) 45 | p.AssertType(TypeGGA) 46 | return GGA{ 47 | BaseSentence: s, 48 | Time: p.Time(0, "time"), 49 | Latitude: p.LatLong(1, 2, "latitude"), 50 | Longitude: p.LatLong(3, 4, "longitude"), 51 | FixQuality: p.EnumString(5, "fix quality", Invalid, GPS, DGPS, PPS, RTK, FRTK, EST), 52 | NumSatellites: p.Int64(6, "number of satellites"), 53 | HDOP: p.Float64(7, "hdop"), 54 | Altitude: p.Float64(8, "altitude"), 55 | Separation: p.Float64(10, "separation"), 56 | DGPSAge: p.String(12, "dgps age"), 57 | DGPSId: p.String(13, "dgps id"), 58 | }, p.Err() 59 | } 60 | -------------------------------------------------------------------------------- /gll.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeGLL type for GLL sentences 5 | TypeGLL = "GLL" 6 | // ValidGLL character 7 | ValidGLL = "A" 8 | // InvalidGLL character 9 | InvalidGLL = "V" 10 | ) 11 | 12 | // GLL is Geographic Position, Latitude / Longitude and time. 13 | // http://aprs.gids.nl/nmea/#gll 14 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_gll_geographic_position_latitudelongitude 15 | // 16 | // Format : $--GLL,ddmm.mm,a,dddmm.mm,a,hhmmss.ss,a*hh 17 | // Format (NMEA 2.3+): $--GLL,ddmm.mm,a,dddmm.mm,a,hhmmss.ss,a,m*hh 18 | // Example: $IIGLL,5924.462,N,01030.048,E,062216,A*38 19 | // Example: $GNGLL,4404.14012,N,12118.85993,W,001037.00,A,A*67 20 | type GLL struct { 21 | BaseSentence 22 | Latitude float64 // Latitude 23 | Longitude float64 // Longitude 24 | Time Time // Time Stamp 25 | Validity string // validity - A=valid, V=invalid 26 | FFAMode string // FAA mode indicator (filled in NMEA 2.3 and later) 27 | } 28 | 29 | // newGLL constructor 30 | func newGLL(s BaseSentence) (Sentence, error) { 31 | p := NewParser(s) 32 | p.AssertType(TypeGLL) 33 | gll := GLL{ 34 | BaseSentence: s, 35 | Latitude: p.LatLong(0, 1, "latitude"), 36 | Longitude: p.LatLong(2, 3, "longitude"), 37 | Time: p.Time(4, "time"), 38 | Validity: p.EnumString(5, "validity", ValidGLL, InvalidGLL), 39 | } 40 | if len(p.Fields) > 6 { 41 | gll.FFAMode = p.String(6, "FAA mode") 42 | } 43 | return gll, p.Err() 44 | } 45 | -------------------------------------------------------------------------------- /gll_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | var glltests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg GLL 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$GPGLL,3926.7952,N,12000.5947,W,022732,A,A*58", 18 | msg: GLL{ 19 | Latitude: MustParseLatLong("3926.7952 N"), 20 | Longitude: MustParseLatLong("12000.5947 W"), 21 | Time: Time{ 22 | Valid: true, 23 | Hour: 2, 24 | Minute: 27, 25 | Second: 32, 26 | Millisecond: 0, 27 | }, 28 | Validity: "A", 29 | FFAMode: FAAModeAutonomous, 30 | }, 31 | }, 32 | { 33 | name: "good sentence without FAA mode", 34 | raw: "$IIGLL,5924.462,N,01030.048,E,062216,A*38", 35 | msg: GLL{ 36 | Latitude: MustParseLatLong("5924.462 N"), 37 | Longitude: MustParseLatLong("01030.048 E"), 38 | Time: Time{ 39 | Valid: true, 40 | Hour: 6, 41 | Minute: 22, 42 | Second: 16, 43 | Millisecond: 0, 44 | }, 45 | Validity: "A", 46 | FFAMode: "", 47 | }, 48 | }, 49 | { 50 | name: "bad validity", 51 | raw: "$GPGLL,3926.7952,N,12000.5947,W,022732,D,A*5D", 52 | err: "nmea: GPGLL invalid validity: D", 53 | }, 54 | } 55 | 56 | func TestGLL(t *testing.T) { 57 | for _, tt := range glltests { 58 | t.Run(tt.name, func(t *testing.T) { 59 | m, err := Parse(tt.raw) 60 | if tt.err != "" { 61 | assert.Error(t, err) 62 | assert.EqualError(t, err, tt.err) 63 | } else { 64 | assert.NoError(t, err) 65 | gll := m.(GLL) 66 | gll.BaseSentence = BaseSentence{} 67 | assert.Equal(t, tt.msg, gll) 68 | } 69 | }) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/adrianmo/go-nmea 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/davecgh/go-spew v1.1.1 // indirect 7 | github.com/stretchr/testify v1.5.1 8 | ) 9 | -------------------------------------------------------------------------------- /gsa.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeGSA type for GSA sentences 5 | TypeGSA = "GSA" 6 | // Auto - Field 1, auto or manual fix. 7 | Auto = "A" 8 | // Manual - Field 1, auto or manual fix. 9 | Manual = "M" 10 | // FixNone - Field 2, fix type. 11 | FixNone = "1" 12 | // Fix2D - Field 2, fix type. 13 | Fix2D = "2" 14 | // Fix3D - Field 2, fix type. 15 | Fix3D = "3" 16 | ) 17 | 18 | // GSA represents overview satellite data. 19 | // http://aprs.gids.nl/nmea/#gsa 20 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_gsa_gps_dop_and_active_satellites 21 | // 22 | // Format: $--GSA,a,a,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x.x,x.x,x.x*hh 23 | // Format (NMEA 4.1+): $--GSA,a,a,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x.x,x.x,x.x,x*hh 24 | // Example: $GNGSA,A,3,80,71,73,79,69,,,,,,,,1.83,1.09,1.47*17 25 | // Example (NMEA 4.1+): $GNGSA,A,3,13,12,22,19,08,21,,,,,,,1.05,0.64,0.83,4*0B 26 | type GSA struct { 27 | BaseSentence 28 | Mode string // The selection mode. 29 | FixType string // The fix type. 30 | SV []string // List of satellite PRNs used for this fix. 31 | PDOP float64 // Dilution of precision. 32 | HDOP float64 // Horizontal dilution of precision. 33 | VDOP float64 // Vertical dilution of precision. 34 | // SystemID is (GNSS) System ID (NMEA 4.1+) 35 | // 1 - GPS 36 | // 2 - GLONASS 37 | // 3 - Galileo 38 | // 4 - BeiDou 39 | // 5 - QZSS 40 | // 6 - NavID (IRNSS) 41 | SystemID int64 42 | } 43 | 44 | // newGSA parses the GSA sentence into this struct. 45 | func newGSA(s BaseSentence) (Sentence, error) { 46 | p := NewParser(s) 47 | p.AssertType(TypeGSA) 48 | m := GSA{ 49 | BaseSentence: s, 50 | Mode: p.EnumString(0, "selection mode", Auto, Manual), 51 | FixType: p.EnumString(1, "fix type", FixNone, Fix2D, Fix3D), 52 | } 53 | // Satellites in view. 54 | for i := 2; i < 14; i++ { 55 | if v := p.String(i, "satellite in view"); v != "" { 56 | m.SV = append(m.SV, v) 57 | } 58 | } 59 | // Dilution of precision. 60 | m.PDOP = p.Float64(14, "pdop") 61 | m.HDOP = p.Float64(15, "hdop") 62 | m.VDOP = p.Float64(16, "vdop") 63 | 64 | if len(p.Fields) > 17 { 65 | m.SystemID = p.Int64(17, "system ID") 66 | } 67 | return m, p.Err() 68 | } 69 | -------------------------------------------------------------------------------- /gsa_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | var gsatests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg GSA 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$GPGSA,A,3,22,19,18,27,14,03,,,,,,,3.1,2.0,2.4*36", 18 | msg: GSA{ 19 | Mode: "A", 20 | FixType: "3", 21 | SV: []string{"22", "19", "18", "27", "14", "03"}, 22 | PDOP: 3.1, 23 | HDOP: 2, 24 | VDOP: 2.4, 25 | }, 26 | }, 27 | { 28 | name: "good sentence with system id", 29 | raw: "$GNGSA,A,3,13,12,22,19,08,21,,,,,,,1.05,0.64,0.83,4*0B", 30 | msg: GSA{ 31 | Mode: "A", 32 | FixType: "3", 33 | SV: []string{"13", "12", "22", "19", "08", "21"}, 34 | PDOP: 1.05, 35 | HDOP: 0.64, 36 | VDOP: 0.83, 37 | SystemID: 4, 38 | }, 39 | }, 40 | { 41 | name: "bad mode", 42 | raw: "$GPGSA,F,3,22,19,18,27,14,03,,,,,,,3.1,2.0,2.4*31", 43 | err: "nmea: GPGSA invalid selection mode: F", 44 | }, 45 | { 46 | name: "bad fix", 47 | raw: "$GPGSA,A,6,22,19,18,27,14,03,,,,,,,3.1,2.0,2.4*33", 48 | err: "nmea: GPGSA invalid fix type: 6", 49 | }, 50 | } 51 | 52 | func TestGSA(t *testing.T) { 53 | for _, tt := range gsatests { 54 | t.Run(tt.name, func(t *testing.T) { 55 | m, err := Parse(tt.raw) 56 | if tt.err != "" { 57 | assert.Error(t, err) 58 | assert.EqualError(t, err, tt.err) 59 | } else { 60 | assert.NoError(t, err) 61 | gsa := m.(GSA) 62 | gsa.BaseSentence = BaseSentence{} 63 | assert.Equal(t, tt.msg, gsa) 64 | } 65 | }) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /gsv.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeGSV type of GSV sentences for satellites in view 5 | TypeGSV = "GSV" 6 | ) 7 | 8 | // GSV represents the GPS Satellites in view 9 | // http://aprs.gids.nl/nmea/#glgsv 10 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_gsv_satellites_in_view 11 | // 12 | // Format: $--GSV,x,x,x,x,x,x,x,...*hh 13 | // Format (NMEA 4.1+): $--GSV,x,x,x,x,x,x,x,...,x*hh 14 | // Example: $GPGSV,3,1,11,09,76,148,32,05,55,242,29,17,33,054,30,14,27,314,24*71 15 | // Example (NMEA 4.1+): $GAGSV,3,1,09,02,00,179,,04,09,321,,07,11,134,11,11,10,227,,7*7F 16 | type GSV struct { 17 | BaseSentence 18 | TotalMessages int64 // Total number of messages of this type in this cycle 19 | MessageNumber int64 // Message number 20 | NumberSVsInView int64 // Total number of SVs in view 21 | Info []GSVInfo // visible satellite info (0-4 of these) 22 | // SystemID is (GNSS) System ID (NMEA 4.1+) 23 | // 1 - GPS 24 | // 2 - GLONASS 25 | // 3 - Galileo 26 | // 4 - BeiDou 27 | // 5 - QZSS 28 | // 6 - NavID (IRNSS) 29 | SystemID int64 30 | } 31 | 32 | // GSVInfo represents information about a visible satellite 33 | type GSVInfo struct { 34 | SVPRNNumber int64 // SV PRN number, pseudo-random noise or gold code 35 | Elevation int64 // Elevation in degrees, 90 maximum 36 | Azimuth int64 // Azimuth, degrees from true north, 000 to 359 37 | SNR int64 // SNR, 00-99 dB (null when not tracking) 38 | } 39 | 40 | // newGSV constructor 41 | func newGSV(s BaseSentence) (Sentence, error) { 42 | p := NewParser(s) 43 | p.AssertType(TypeGSV) 44 | m := GSV{ 45 | BaseSentence: s, 46 | TotalMessages: p.Int64(0, "total number of messages"), 47 | MessageNumber: p.Int64(1, "message number"), 48 | NumberSVsInView: p.Int64(2, "number of SVs in view"), 49 | } 50 | i := 0 51 | for ; i < 4; i++ { 52 | if 6+i*4 >= len(m.Fields) { 53 | break 54 | } 55 | m.Info = append(m.Info, GSVInfo{ 56 | SVPRNNumber: p.Int64(3+i*4, "SV prn number"), 57 | Elevation: p.Int64(4+i*4, "elevation"), 58 | Azimuth: p.Int64(5+i*4, "azimuth"), 59 | SNR: p.Int64(6+i*4, "SNR"), 60 | }) 61 | } 62 | idxSID := (6 + (i-1)*4) + 1 63 | if len(p.Fields) == idxSID+1 { 64 | m.SystemID = p.Int64(idxSID, "system ID") 65 | } 66 | return m, p.Err() 67 | } 68 | -------------------------------------------------------------------------------- /hbt.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeHBT type of HBT sentence for heartbeat supervision sentence. 5 | TypeHBT = "HBT" 6 | ) 7 | 8 | // HBT is heartbeat supervision sentence to indicate if equipment is operating normally. 9 | // https://fcc.report/FCC-ID/ADB9ZWRTR100/2768717.pdf (page 1) FURUNO MARINE RADAR, model FAR-15XX manual 10 | // 11 | // Format: $--HBT,x.x,A,x*hh 12 | // Example: $HCHBT,98.3,0.0,E,12.6,W*57 13 | type HBT struct { 14 | BaseSentence 15 | // Interval is configured repeat interval in seconds (1 - 999, null) 16 | Interval float64 17 | // OperationStatus is equipment operation status: A = ok, V = not ok 18 | OperationStatus string 19 | // MessageID is sequential message identifier (0 - 9). Counts to 9 and resets to 0. 20 | MessageID int64 21 | } 22 | 23 | // newHBT constructor 24 | func newHBT(s BaseSentence) (Sentence, error) { 25 | p := NewParser(s) 26 | p.AssertType(TypeHBT) 27 | m := HBT{ 28 | BaseSentence: s, 29 | Interval: p.Float64(0, "interval"), 30 | OperationStatus: p.EnumString(1, "operation status", StatusValid, StatusInvalid), 31 | MessageID: p.Int64(2, "message ID"), 32 | } 33 | return m, p.Err() 34 | } 35 | -------------------------------------------------------------------------------- /hbt_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestHBT(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg HBT 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$HCHBT,1.5,A,1*23", 18 | msg: HBT{ 19 | Interval: 1.5, 20 | OperationStatus: StatusValid, 21 | MessageID: 1, 22 | }, 23 | }, 24 | { 25 | name: "invalid interval and status", 26 | raw: "$HCHBT,,V,1*1E", 27 | msg: HBT{ 28 | Interval: 0, 29 | OperationStatus: StatusInvalid, 30 | MessageID: 1, 31 | }, 32 | }, 33 | { 34 | name: "invalid interval", 35 | raw: "$HCHBT,x.5,A,1*6A", 36 | err: "nmea: HCHBT invalid interval: x.5", 37 | }, 38 | { 39 | name: "invalid operation status", 40 | raw: "$HCHBT,1.5,X,1*3A", 41 | err: "nmea: HCHBT invalid operation status: X", 42 | }, 43 | { 44 | name: "invalid sequence identification", 45 | raw: "$HCHBT,1.5,A,x*6A", 46 | err: "nmea: HCHBT invalid message ID: x", 47 | }, 48 | } 49 | for _, tt := range tests { 50 | t.Run(tt.name, func(t *testing.T) { 51 | m, err := Parse(tt.raw) 52 | if tt.err != "" { 53 | assert.Error(t, err) 54 | assert.EqualError(t, err, tt.err) 55 | } else { 56 | assert.NoError(t, err) 57 | hbt := m.(HBT) 58 | hbt.BaseSentence = BaseSentence{} 59 | assert.Equal(t, tt.msg, hbt) 60 | } 61 | }) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /hdg.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeHDG type of HDG sentence for vessel heading, deviation and variation with respect to magnetic north. 5 | TypeHDG = "HDG" 6 | ) 7 | 8 | // HDG is vessel heading (in degrees), deviation and variation with respect to magnetic north produced by any 9 | // device or system producing magnetic reading. 10 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_hdg_heading_deviation_variation 11 | // 12 | // Format: $--HDG,x.x,y.y,a,z.z,a*hr 13 | // Example: $HCHDG,98.3,0.0,E,12.6,W*57 14 | type HDG struct { 15 | BaseSentence 16 | Heading float64 // Heading in degrees 17 | Deviation float64 // Magnetic Deviation in degrees 18 | DeviationDirection string // Magnetic Deviation direction, E = Easterly, W = Westerly 19 | Variation float64 // Magnetic Variation in degrees 20 | VariationDirection string // Magnetic Variation direction, E = Easterly, W = Westerly 21 | } 22 | 23 | // newHDG constructor 24 | func newHDG(s BaseSentence) (Sentence, error) { 25 | p := NewParser(s) 26 | p.AssertType(TypeHDG) 27 | m := HDG{ 28 | BaseSentence: s, 29 | Heading: p.Float64(0, "heading"), 30 | Deviation: p.Float64(1, "deviation"), 31 | DeviationDirection: p.EnumString(2, "deviation direction", East, West), 32 | Variation: p.Float64(3, "variation"), 33 | VariationDirection: p.EnumString(4, "variation direction", East, West), 34 | } 35 | return m, p.Err() 36 | } 37 | -------------------------------------------------------------------------------- /hdg_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestHDG(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg HDG 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$HCHDG,98.3,0.1,E,12.6,W*56", 18 | msg: HDG{ 19 | Heading: 98.3, 20 | Deviation: 0.1, 21 | DeviationDirection: East, 22 | Variation: 12.6, 23 | VariationDirection: West, 24 | }, 25 | }, 26 | { 27 | name: "invalid Heading", 28 | raw: "$HCHDG,X,0.1,E,12.6,W*12", 29 | err: "nmea: HCHDG invalid heading: X", 30 | }, 31 | { 32 | name: "invalid Deviation", 33 | raw: "$HCHDG,98.3,x.1,E,12.6,W*1E", 34 | err: "nmea: HCHDG invalid deviation: x.1", 35 | }, 36 | { 37 | name: "invalid DeviationDirection", 38 | raw: "$HCHDG,98.3,0.1,X,12.6,W*4B", 39 | err: "nmea: HCHDG invalid deviation direction: X", 40 | }, 41 | { 42 | name: "invalid Variation", 43 | raw: "$HCHDG,98.3,0.1,E,x.1,W*2A", 44 | err: "nmea: HCHDG invalid variation: x.1", 45 | }, 46 | { 47 | name: "invalid VariationDirection", 48 | raw: "$HCHDG,98.3,0.1,E,12.6,X*59", 49 | err: "nmea: HCHDG invalid variation direction: X", 50 | }, 51 | } 52 | for _, tt := range tests { 53 | t.Run(tt.name, func(t *testing.T) { 54 | m, err := Parse(tt.raw) 55 | if tt.err != "" { 56 | assert.Error(t, err) 57 | assert.EqualError(t, err, tt.err) 58 | } else { 59 | assert.NoError(t, err) 60 | hdg := m.(HDG) 61 | hdg.BaseSentence = BaseSentence{} 62 | assert.Equal(t, tt.msg, hdg) 63 | } 64 | }) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /hdm.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeHDM type of HDM sentence for vessel heading in degrees with respect to magnetic north 5 | TypeHDM = "HDM" 6 | // MagneticHDM for valid Magnetic heading 7 | MagneticHDM = "M" 8 | ) 9 | 10 | // HDM is vessel heading in degrees with respect to magnetic north produced by any device or system producing magnetic heading. 11 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_hdm_heading_magnetic 12 | // 13 | // Format: $--HDM,xxx.xx,M*hh 14 | // Example: $HCHDM,093.8,M*2B 15 | type HDM struct { 16 | BaseSentence 17 | Heading float64 // Heading in degrees 18 | MagneticValid bool // Heading is respect to magnetic north 19 | } 20 | 21 | // newHDM constructor 22 | func newHDM(s BaseSentence) (Sentence, error) { 23 | p := NewParser(s) 24 | p.AssertType(TypeHDM) 25 | m := HDM{ 26 | BaseSentence: s, 27 | Heading: p.Float64(0, "heading"), 28 | MagneticValid: p.EnumString(1, "magnetic", MagneticHDM) == MagneticHDM, 29 | } 30 | return m, p.Err() 31 | } 32 | -------------------------------------------------------------------------------- /hdm_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestHDM(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg HDM 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$HCHDM,093.8,M*2B", 18 | msg: HDM{ 19 | Heading: 93.8, 20 | MagneticValid: true, 21 | }, 22 | }, 23 | { 24 | name: "invalid Magnetic", 25 | raw: "$HCHDM,093.8,X*3E", 26 | err: "nmea: HCHDM invalid magnetic: X", 27 | }, 28 | { 29 | name: "invalid Heading", 30 | raw: "$HCHDM,09X.X,M*20", 31 | err: "nmea: HCHDM invalid heading: 09X.X", 32 | }, 33 | } 34 | for _, tt := range tests { 35 | t.Run(tt.name, func(t *testing.T) { 36 | m, err := Parse(tt.raw) 37 | if tt.err != "" { 38 | assert.Error(t, err) 39 | assert.EqualError(t, err, tt.err) 40 | } else { 41 | assert.NoError(t, err) 42 | hdm := m.(HDM) 43 | hdm.BaseSentence = BaseSentence{} 44 | assert.Equal(t, tt.msg, hdm) 45 | } 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /hdt.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeHDT type for HDT sentences 5 | TypeHDT = "HDT" 6 | ) 7 | 8 | // HDT is the Actual vessel heading in degrees True. 9 | // http://aprs.gids.nl/nmea/#hdt 10 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_gsv_satellites_in_view 11 | // 12 | // Format: $--HDT,x.x,T*hh 13 | // Example: $GPHDT,274.07,T*03 14 | type HDT struct { 15 | BaseSentence 16 | Heading float64 // Heading in degrees 17 | True bool // Heading is relative to true north 18 | } 19 | 20 | // newHDT constructor 21 | func newHDT(s BaseSentence) (Sentence, error) { 22 | p := NewParser(s) 23 | p.AssertType(TypeHDT) 24 | m := HDT{ 25 | BaseSentence: s, 26 | Heading: p.Float64(0, "heading"), 27 | True: p.EnumString(1, "true", "T") == "T", 28 | } 29 | return m, p.Err() 30 | } 31 | -------------------------------------------------------------------------------- /hdt_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | var hdttests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg HDT 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$GPHDT,123.456,T*32", 18 | msg: HDT{ 19 | Heading: 123.456, 20 | True: true, 21 | }, 22 | }, 23 | { 24 | name: "invalid True", 25 | raw: "$GPHDT,123.456,X*3E", 26 | err: "nmea: GPHDT invalid true: X", 27 | }, 28 | { 29 | name: "invalid Heading", 30 | raw: "$GPHDT,XXX,T*43", 31 | err: "nmea: GPHDT invalid heading: XXX", 32 | }, 33 | } 34 | 35 | func TestHDT(t *testing.T) { 36 | for _, tt := range hdttests { 37 | t.Run(tt.name, func(t *testing.T) { 38 | m, err := Parse(tt.raw) 39 | if tt.err != "" { 40 | assert.Error(t, err) 41 | assert.EqualError(t, err, tt.err) 42 | } else { 43 | assert.NoError(t, err) 44 | hdt := m.(HDT) 45 | hdt.BaseSentence = BaseSentence{} 46 | assert.Equal(t, tt.msg, hdt) 47 | } 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /hsc.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeHSC type of HSC sentence for Heading steering command 5 | TypeHSC = "HSC" 6 | ) 7 | 8 | // HSC - Heading steering command 9 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_hsc_heading_steering_command 10 | // https://www.tronico.fi/OH6NT/docs/NMEA0183.pdf (page 11) 11 | // 12 | // Format: $--HSC, x.x, T, x.x, M,a*hh 13 | // Example: $FTHSC,40.12,T,39.11,M*5E 14 | type HSC struct { 15 | BaseSentence 16 | TrueHeading float64 // Heading Degrees, True 17 | TrueHeadingType string // T = True 18 | MagneticHeading float64 // Heading Degrees, Magnetic 19 | MagneticHeadingType string // M = Magnetic 20 | } 21 | 22 | // newHSC constructor 23 | func newHSC(s BaseSentence) (Sentence, error) { 24 | p := NewParser(s) 25 | p.AssertType(TypeHSC) 26 | return HSC{ 27 | BaseSentence: s, 28 | TrueHeading: p.Float64(0, "true heading"), 29 | TrueHeadingType: p.EnumString(1, "true heading type", HeadingTrue), 30 | MagneticHeading: p.Float64(2, "magnetic heading"), 31 | MagneticHeadingType: p.EnumString(3, "magnetic heading type", HeadingMagnetic), 32 | }, p.Err() 33 | } 34 | -------------------------------------------------------------------------------- /hsc_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestHSC(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg HSC 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$FTHSC,40.12,T,39.11,M*5E", 18 | msg: HSC{ 19 | TrueHeading: 40.12, 20 | TrueHeadingType: HeadingTrue, 21 | MagneticHeading: 39.11, 22 | MagneticHeadingType: HeadingMagnetic, 23 | }, 24 | }, 25 | { 26 | name: "invalid nmea: TrueHeading", 27 | raw: "$FTHSC,40.1x,T,39.11,M*14", 28 | err: "nmea: FTHSC invalid true heading: 40.1x", 29 | }, 30 | { 31 | name: "invalid nmea: TrueHeadingType", 32 | raw: "$FTHSC,40.12,x,39.11,M*72", 33 | err: "nmea: FTHSC invalid true heading type: x", 34 | }, 35 | { 36 | name: "invalid nmea: MagneticHeading", 37 | raw: "$FTHSC,40.12,T,x,M*02", 38 | err: "nmea: FTHSC invalid magnetic heading: x", 39 | }, 40 | { 41 | name: "invalid nmea: MagneticHeadingType", 42 | raw: "$FTHSC,40.12,T,39.11,x*6b", 43 | err: "nmea: FTHSC invalid magnetic heading type: x", 44 | }, 45 | } 46 | for _, tt := range tests { 47 | t.Run(tt.name, func(t *testing.T) { 48 | m, err := Parse(tt.raw) 49 | if tt.err != "" { 50 | assert.Error(t, err) 51 | assert.EqualError(t, err, tt.err) 52 | } else { 53 | assert.NoError(t, err) 54 | hsc := m.(HSC) 55 | hsc.BaseSentence = BaseSentence{} 56 | assert.Equal(t, tt.msg, hsc) 57 | } 58 | }) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /mda_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | var mdatests = []struct { 9 | name string 10 | raw string 11 | err string 12 | msg MDA 13 | }{ 14 | { 15 | name: "good sentence", 16 | raw: "$WIMDA,3.02,I,1.01,B,23.4,C,,,40.2,,12.1,C,19.3,T,20.1,M,13.1,N,1.1,M*62", 17 | msg: MDA{ 18 | PressureInch: 3.02, 19 | InchesValid: true, 20 | PressureBar: 1.01, 21 | BarsValid: true, 22 | AirTemp: 23.4, 23 | AirTempValid: true, 24 | WaterTemp: 0, 25 | WaterTempValid: false, 26 | RelativeHum: 40.2, 27 | AbsoluteHum: 0, 28 | DewPoint: 12.1, 29 | DewPointValid: true, 30 | WindDirectionTrue: 19.3, 31 | TrueValid: true, 32 | WindDirectionMagnetic: 20.1, 33 | MagneticValid: true, 34 | WindSpeedKnots: 13.1, 35 | KnotsValid: true, 36 | WindSpeedMeters: 1.1, 37 | MetersValid: true, 38 | }, 39 | }, 40 | } 41 | 42 | func TestMDA(t *testing.T) { 43 | for _, tt := range mdatests { 44 | t.Run(tt.name, func(t *testing.T) { 45 | m, err := Parse(tt.raw) 46 | if tt.err != "" { 47 | assert.Error(t, err) 48 | assert.EqualError(t, err, tt.err) 49 | } else { 50 | assert.NoError(t, err) 51 | mda := m.(MDA) 52 | mda.BaseSentence = BaseSentence{} 53 | assert.Equal(t, tt.msg, mda) 54 | } 55 | }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /mta.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeMTA type of MTA sentence for Air Temperature 5 | TypeMTA = "MTA" 6 | ) 7 | 8 | // MTA - Air Temperature (obsolete, use XDR instead) 9 | // https://www.nmea.org/Assets/100108_nmea_0183_sentences_not_recommended_for_new_designs.pdf (page 7) 10 | // 11 | // Format: $--MTA,x.x,C*hh 12 | // Example: $IIMTA,13.3,C*04 13 | type MTA struct { 14 | BaseSentence 15 | Temperature float64 // temperature 16 | Unit string // unit of temperature, should be degrees Celsius 17 | } 18 | 19 | // newMTA constructor 20 | func newMTA(s BaseSentence) (Sentence, error) { 21 | p := NewParser(s) 22 | p.AssertType(TypeMTA) 23 | return MTA{ 24 | BaseSentence: s, 25 | Temperature: p.Float64(0, "temperature"), 26 | Unit: p.EnumString(1, "temperature unit", TemperatureCelsius), 27 | }, p.Err() 28 | } 29 | -------------------------------------------------------------------------------- /mta_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestMTA(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg MTA 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$IIMTA,13.3,C*04", 18 | msg: MTA{ 19 | Temperature: 13.3, 20 | Unit: TemperatureCelsius, 21 | }, 22 | }, 23 | { 24 | name: "invalid nmea: Temperature", 25 | raw: "$IIMTA,x.x,C*35", 26 | err: "nmea: IIMTA invalid temperature: x.x", 27 | }, 28 | { 29 | name: "invalid nmea: Unit", 30 | raw: "$IIMTA,13.3,F*01", 31 | err: "nmea: IIMTA invalid temperature unit: F", 32 | }, 33 | } 34 | for _, tt := range tests { 35 | t.Run(tt.name, func(t *testing.T) { 36 | m, err := Parse(tt.raw) 37 | if tt.err != "" { 38 | assert.Error(t, err) 39 | assert.EqualError(t, err, tt.err) 40 | } else { 41 | assert.NoError(t, err) 42 | mta := m.(MTA) 43 | mta.BaseSentence = BaseSentence{} 44 | assert.Equal(t, tt.msg, mta) 45 | } 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /mtk.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeMTK type for PMTK sentences 5 | // Deprecated: use PMTK001 instead. PMTK protocol contains actually many commands. This struct is for MTK 001 ACK command. 6 | TypeMTK = "MTK001" 7 | ) 8 | 9 | // MTK is sentence for NMEA embedded command packet protocol, command type 001 - ACK. 10 | // https://www.rhydolabz.com/documents/25/PMTK_A11.pdf 11 | // https://www.sparkfun.com/datasheets/GPS/Modules/PMTK_Protocol.pdf 12 | // 13 | // The maximum length of each packet is restricted to 255 bytes which is longer than NMEA0183 82 bytes. 14 | // 15 | // Format: $PMTKxxx,c-c*hh 16 | // Example: $PMTK000*32 17 | // 18 | // $PMTK001,101,0*33 19 | // 20 | // Deprecated: use PMTK001 instead. PMTK protocol contains actually many commands. This struct is for MTK 001 ACK command. 21 | type MTK struct { 22 | BaseSentence 23 | Cmd, // Three bytes character string. From "000" to "999". An identifier used to tell the decoder how to decode the packet 24 | // Flag is flag field in PMTK001 packet. 25 | // Note: this field on only relevant for `PMTK001,Cmd,Flag` sentence. 26 | // Actual MTK protocol has variable amount of fields (whole sentence can be up to 255 bytes) 27 | // 28 | // Actual docs say: 29 | // DataField: The DataField has variable length depending on the packet type. A comma symbol ‘,’ must be inserted 30 | // ahead each data filed to help the decoder process the DataField. 31 | Flag int64 32 | } 33 | 34 | // newMTK constructor 35 | // Deprecated: use newPMTK001 instead 36 | func newMTK(s BaseSentence) (Sentence, error) { 37 | p := NewParser(s) 38 | p.AssertType(TypeMTK) 39 | cmd := p.Int64(0, "command") 40 | flag := p.Int64(1, "flag") 41 | return MTK{ 42 | BaseSentence: s, 43 | Cmd: cmd, 44 | Flag: flag, 45 | }, p.Err() 46 | } 47 | -------------------------------------------------------------------------------- /mtk_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestMTK(t *testing.T) { 10 | var tests = []struct { 11 | name string 12 | raw string 13 | err string 14 | msg MTK 15 | }{ 16 | { 17 | name: "good: Packet Type: 001 PMTK_ACK", 18 | raw: "$PMTK001,604,3*32", 19 | msg: MTK{ 20 | Cmd: 604, 21 | Flag: 3, 22 | }, 23 | }, 24 | { 25 | name: "missing flag", 26 | raw: "$PMTK001,604*2d", 27 | err: "nmea: PMTK001 invalid flag: index out of range", 28 | }, 29 | { 30 | name: "missing cmd", 31 | raw: "$PMTK001*33", 32 | err: "nmea: PMTK001 invalid command: index out of range", 33 | }, 34 | } 35 | for _, tt := range tests { 36 | t.Run(tt.name, func(t *testing.T) { 37 | m, err := Parse(tt.raw) 38 | if tt.err != "" { 39 | assert.Error(t, err) 40 | assert.EqualError(t, err, tt.err) 41 | } else { 42 | assert.NoError(t, err) 43 | mtk := m.(MTK) 44 | mtk.BaseSentence = BaseSentence{} 45 | assert.Equal(t, tt.msg, mtk) 46 | } 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /mtw.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeMTW type of MWT sentence describing mean temperature of water 5 | TypeMTW = "MTW" 6 | // CelsiusMTW is MTW unit of measurement in celsius 7 | CelsiusMTW = "C" 8 | ) 9 | 10 | // MTW is sentence for mean temperature of water. 11 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_mtw_mean_temperature_of_water 12 | // 13 | // Format: $--MTW,TT.T,C*hh 14 | // Example: $INMTW,17.9,C*1B 15 | type MTW struct { 16 | BaseSentence 17 | Temperature float64 // Temperature, degrees 18 | CelsiusValid bool // Is unit of measurement Celsius 19 | } 20 | 21 | // newMTW constructor 22 | func newMTW(s BaseSentence) (Sentence, error) { 23 | p := NewParser(s) 24 | p.AssertType(TypeMTW) 25 | return MTW{ 26 | BaseSentence: s, 27 | Temperature: p.Float64(0, "temperature"), 28 | CelsiusValid: p.EnumString(1, "unit of measurement celsius", CelsiusMTW) == CelsiusMTW, 29 | }, p.Err() 30 | } 31 | -------------------------------------------------------------------------------- /mtw_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestMTW(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg MTW 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$INMTW,17.9,C*1B", 18 | msg: MTW{ 19 | Temperature: 17.9, 20 | CelsiusValid: true, 21 | }, 22 | }, 23 | { 24 | name: "invalid Temperature", 25 | raw: "$INMTW,x.9,C*65", 26 | err: "nmea: INMTW invalid temperature: x.9", 27 | }, 28 | { 29 | name: "invalid CelsiusValid", 30 | raw: "$INMTW,17.9,x*20", 31 | err: "nmea: INMTW invalid unit of measurement celsius: x", 32 | }, 33 | } 34 | for _, tt := range tests { 35 | t.Run(tt.name, func(t *testing.T) { 36 | m, err := Parse(tt.raw) 37 | if tt.err != "" { 38 | assert.Error(t, err) 39 | assert.EqualError(t, err, tt.err) 40 | } else { 41 | assert.NoError(t, err) 42 | mtw := m.(MTW) 43 | mtw.BaseSentence = BaseSentence{} 44 | assert.Equal(t, tt.msg, mtw) 45 | } 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /must_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | // MustParseLatLong parses the supplied string into the LatLong. 4 | // It panics if an error is encountered 5 | func MustParseLatLong(s string) float64 { 6 | l, err := ParseLatLong(s) 7 | if err != nil { 8 | panic(err) 9 | } 10 | return l 11 | } 12 | 13 | // MustParseGPS parses a GPS/NMEA coordinate or panics if it fails. 14 | func MustParseGPS(s string) float64 { 15 | l, err := ParseGPS(s) 16 | if err != nil { 17 | panic(err) 18 | } 19 | return l 20 | } 21 | 22 | // MustParseDMS parses a coordinate in degrees, minutes, seconds and 23 | // panics on failure 24 | func MustParseDMS(s string) float64 { 25 | l, err := ParseDMS(s) 26 | if err != nil { 27 | panic(err) 28 | } 29 | return l 30 | } 31 | 32 | // MustParseTime parses wall clock and panics on failure 33 | func MustParseTime(s string) Time { 34 | t, err := ParseTime(s) 35 | if err != nil { 36 | panic(err) 37 | } 38 | return t 39 | } 40 | 41 | // MustParseDate parses a date and panics on failure 42 | func MustParseDate(s string) Date { 43 | d, err := ParseDate(s) 44 | if err != nil { 45 | panic(err) 46 | } 47 | return d 48 | } 49 | -------------------------------------------------------------------------------- /mwd.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | /** 4 | $WIMWD 5 | 6 | NMEA 0183 standard Wind Direction and Speed, with respect to north. 7 | 8 | Syntax 9 | $WIMWD,<1>,<2>,<3>,<4>,<5>,<6>,<7>,<8>*hh 10 | 11 | Fields 12 | <1> Wind direction, 0.0 to 359.9 degrees True, to the nearest 0.1 degree 13 | <2> T = True 14 | <3> Wind direction, 0.0 to 359.9 degrees Magnetic, to the nearest 0.1 degree 15 | <4> M = Magnetic 16 | <5> Wind speed, knots, to the nearest 0.1 knot. 17 | <6> N = Knots 18 | <7> Wind speed, meters/second, to the nearest 0.1 m/s. 19 | <8> M = Meters/second 20 | */ 21 | 22 | const ( 23 | // TypeMWD type for MWD sentences 24 | TypeMWD = "MWD" 25 | // TrueMWD for valid True Direction 26 | TrueMWD = "T" 27 | // MagneticMWD for valid Magnetic direction 28 | MagneticMWD = "M" 29 | // KnotsMWD for valid Knots 30 | KnotsMWD = "N" 31 | // MetersSecondMWD for valid Meters per Second 32 | MetersSecondMWD = "M" 33 | ) 34 | 35 | // MWD Wind Direction and Speed, with respect to north. 36 | // https://www.tronico.fi/OH6NT/docs/NMEA0183.pdf 37 | // http://gillinstruments.com/data/manuals/OMC-140_Operator_Manual_v1.04_131117.pdf 38 | // 39 | // Format: $--MWD,x.x,T,x.x,M,x.x,N,x.x,M*hh 40 | // Example: $WIMWD,10.1,T,10.1,M,12,N,40,M*5D 41 | type MWD struct { 42 | BaseSentence 43 | WindDirectionTrue float64 44 | TrueValid bool 45 | WindDirectionMagnetic float64 46 | MagneticValid bool 47 | WindSpeedKnots float64 48 | KnotsValid bool 49 | WindSpeedMeters float64 50 | MetersValid bool 51 | } 52 | 53 | func newMWD(s BaseSentence) (Sentence, error) { 54 | p := NewParser(s) 55 | p.AssertType(TypeMWD) 56 | return MWD{ 57 | BaseSentence: s, 58 | WindDirectionTrue: p.Float64(0, "true wind direction"), 59 | TrueValid: p.EnumString(1, "true wind valid", TrueMWD) == TrueMWD, 60 | WindDirectionMagnetic: p.Float64(2, "magnetic wind direction"), 61 | MagneticValid: p.EnumString(3, "magnetic direction valid", MagneticMWD) == MagneticMWD, 62 | WindSpeedKnots: p.Float64(4, "windspeed knots"), 63 | KnotsValid: p.EnumString(5, "windspeed knots valid", KnotsMWD) == KnotsMWD, 64 | WindSpeedMeters: p.Float64(6, "windspeed m/s"), 65 | MetersValid: p.EnumString(7, "windspeed m/s valid", MetersSecondMWD) == MetersSecondMWD, 66 | }, p.Err() 67 | } 68 | -------------------------------------------------------------------------------- /mwd_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | var mwdtests = []struct { 9 | name string 10 | raw string 11 | err string 12 | msg MWD 13 | }{ 14 | { 15 | name: "good sentence", 16 | raw: "$WIMWD,10.1,T,10.1,M,12,N,40,M*5D", 17 | msg: MWD{ 18 | WindDirectionTrue: 10.1, 19 | TrueValid: true, 20 | WindDirectionMagnetic: 10.1, 21 | MagneticValid: true, 22 | WindSpeedKnots: 12, 23 | KnotsValid: true, 24 | WindSpeedMeters: 40, 25 | MetersValid: true, 26 | }, 27 | }, 28 | { 29 | name: "empty data", 30 | raw: "$WIMWD,,,,,,,,*40", 31 | msg: MWD{ 32 | WindDirectionTrue: 0, 33 | TrueValid: false, 34 | WindDirectionMagnetic: 0, 35 | MagneticValid: false, 36 | WindSpeedKnots: 0, 37 | KnotsValid: false, 38 | WindSpeedMeters: 0, 39 | MetersValid: false, 40 | }, 41 | }, 42 | } 43 | 44 | func TestMWD(t *testing.T) { 45 | for _, tt := range mwdtests { 46 | t.Run(tt.name, func(t *testing.T) { 47 | m, err := Parse(tt.raw) 48 | if tt.err != "" { 49 | assert.Error(t, err) 50 | assert.EqualError(t, err, tt.err) 51 | } else { 52 | assert.NoError(t, err) 53 | mwd := m.(MWD) 54 | mwd.BaseSentence = BaseSentence{} 55 | assert.Equal(t, tt.msg, mwd) 56 | } 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /mwv_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | var mwvtests = []struct { 9 | name string 10 | raw string 11 | err string 12 | msg MWV 13 | }{ 14 | { 15 | name: "good sentence", 16 | raw: "$WIMWV,12.1,T,10.1,N,A*27", 17 | msg: MWV{ 18 | WindAngle: 12.1, 19 | Reference: "T", 20 | WindSpeed: 10.1, 21 | WindSpeedUnit: "N", 22 | StatusValid: true, 23 | }, 24 | }, 25 | { 26 | name: "invalid data", 27 | raw: "$WIMWV,,T,,N,V*32", 28 | msg: MWV{ 29 | WindAngle: 0, 30 | Reference: "T", 31 | WindSpeed: 0, 32 | WindSpeedUnit: "N", 33 | StatusValid: false, 34 | }, 35 | }, 36 | } 37 | 38 | func TestMWV(t *testing.T) { 39 | for _, tt := range mwvtests { 40 | t.Run(tt.name, func(t *testing.T) { 41 | m, err := Parse(tt.raw) 42 | if tt.err != "" { 43 | assert.Error(t, err) 44 | assert.EqualError(t, err, tt.err) 45 | } else { 46 | assert.NoError(t, err) 47 | mwv := m.(MWV) 48 | mwv.BaseSentence = BaseSentence{} 49 | assert.Equal(t, tt.msg, mwv) 50 | } 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /osd_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestOSD(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg OSD 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$RAOSD,179.0,A,179.0,M,00.0,M,,,N*76", 18 | msg: OSD{ 19 | BaseSentence: BaseSentence{}, 20 | Heading: 179, 21 | HeadingStatus: "A", 22 | VesselTrueCourse: 179, 23 | CourseReference: "M", 24 | VesselSpeed: 0, 25 | SpeedReference: "M", 26 | VesselSetTrue: 0, 27 | VesselDrift: 0, 28 | SpeedUnits: "N", 29 | }, 30 | }, 31 | { 32 | name: "invalid nmea: Heading", 33 | raw: "$RAOSD,x179.0,A,179.0,M,00.0,M,,,N*0e", 34 | err: "nmea: RAOSD invalid heading: x179.0", 35 | }, 36 | { 37 | name: "invalid nmea: HeadingStatus", 38 | raw: "$RAOSD,179.0,xA,179.0,M,00.0,M,,,N*0e", 39 | err: "nmea: RAOSD invalid heading status: xA", 40 | }, 41 | { 42 | name: "invalid nmea: VesselTrueCourse", 43 | raw: "$RAOSD,179.0,A,x179.0,M,00.0,M,,,N*0e", 44 | err: "nmea: RAOSD invalid vessel course true: x179.0", 45 | }, 46 | { 47 | name: "invalid nmea: CourseReference", 48 | raw: "$RAOSD,179.0,A,179.0,xM,00.0,M,,,N*0e", 49 | err: "nmea: RAOSD invalid course reference: xM", 50 | }, 51 | { 52 | name: "invalid nmea: VesselSpeed", 53 | raw: "$RAOSD,179.0,A,179.0,M,x00.0,M,,,N*0e", 54 | err: "nmea: RAOSD invalid vessel speed: x00.0", 55 | }, 56 | { 57 | name: "invalid nmea: SpeedReference", 58 | raw: "$RAOSD,179.0,A,179.0,M,00.0,xM,,,N*0e", 59 | err: "nmea: RAOSD invalid speed reference: xM", 60 | }, 61 | { 62 | name: "invalid nmea: VesselSetTrue", 63 | raw: "$RAOSD,179.0,A,179.0,M,00.0,M,x,,N*0e", 64 | err: "nmea: RAOSD invalid vessel set: x", 65 | }, 66 | { 67 | name: "invalid nmea: VesselDrift", 68 | raw: "$RAOSD,179.0,A,179.0,M,00.0,M,,x,N*0e", 69 | err: "nmea: RAOSD invalid vessel drift: x", 70 | }, 71 | { 72 | name: "invalid nmea: SpeedUnits", 73 | raw: "$RAOSD,179.0,A,179.0,M,00.0,M,,,xN*0e", 74 | err: "nmea: RAOSD invalid speed units: xN", 75 | }, 76 | } 77 | for _, tt := range tests { 78 | t.Run(tt.name, func(t *testing.T) { 79 | m, err := Parse(tt.raw) 80 | if tt.err != "" { 81 | assert.Error(t, err) 82 | assert.EqualError(t, err, tt.err) 83 | } else { 84 | assert.NoError(t, err) 85 | mm := m.(OSD) 86 | mm.BaseSentence = BaseSentence{} 87 | assert.Equal(t, tt.msg, mm) 88 | } 89 | }) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /pcdin.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "strconv" 7 | ) 8 | 9 | const ( 10 | // TypePCDIN is type of PCDIN sentence for SeaSmart.Net Protocol 11 | TypePCDIN = "CDIN" 12 | ) 13 | 14 | // PCDIN - SeaSmart.Net Protocol transfers NMEA2000 message as NMEA0183 sentence 15 | // http://www.seasmart.net/pdf/SeaSmart_HTTP_Protocol_RevG_043012.pdf (SeaSmart.Net Protocol Specification Version 1.7) 16 | // 17 | // Note: older SeaSmart.Net Protocol versions have different amount of fields 18 | // 19 | // Format: $PCDIN,hhhhhh,hhhhhhhh,hh,h--h*hh 20 | // Example: $PCDIN,01F112,000C72EA,09,28C36A0000B40AFD*56 21 | type PCDIN struct { 22 | BaseSentence 23 | PGN uint32 // PGN of NMEA2000 packet 24 | Timestamp uint32 // ticks since something 25 | Source uint8 // 0-255 26 | Data []byte // can be more than 8 bytes i.e can contain assembled fast packets 27 | } 28 | 29 | // newPCDIN constructor 30 | func newPCDIN(s BaseSentence) (Sentence, error) { 31 | p := NewParser(s) 32 | p.AssertType(TypePCDIN) 33 | 34 | if len(p.Fields) != 4 { 35 | p.SetErr("fields", "invalid number of fields in sentence") 36 | return nil, p.Err() 37 | } 38 | pgn, err := strconv.ParseUint(p.Fields[0], 16, 24) 39 | if err != nil { 40 | p.err = fmt.Errorf("nmea: %s failed to parse PGN field: %w", p.Prefix(), err) 41 | return nil, p.Err() 42 | } 43 | timestamp, err := strconv.ParseUint(p.Fields[1], 16, 32) 44 | if err != nil { 45 | p.err = fmt.Errorf("nmea: %s failed to parse timestamp field: %w", p.Prefix(), err) 46 | return nil, p.Err() 47 | } 48 | source, err := strconv.ParseUint(p.Fields[2], 16, 8) 49 | if err != nil { 50 | p.err = fmt.Errorf("nmea: %s failed to parse source field: %w", p.Prefix(), err) 51 | return nil, p.Err() 52 | } 53 | data, err := hex.DecodeString(p.Fields[3]) 54 | if err != nil { 55 | p.err = fmt.Errorf("nmea: %s failed to decode data: %w", p.Prefix(), err) 56 | return nil, p.Err() 57 | } 58 | 59 | return PCDIN{ 60 | BaseSentence: s, 61 | PGN: uint32(pgn), 62 | Timestamp: uint32(timestamp), 63 | Source: uint8(source), 64 | Data: data, 65 | }, p.Err() 66 | } 67 | -------------------------------------------------------------------------------- /pcdin_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestPCDIN(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg PCDIN 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$PCDIN,01F112,000C72EA,09,28C36A0000B40AFD*56", 18 | msg: PCDIN{ 19 | PGN: 127250, // 0x1F112 Vessel Heading 20 | Timestamp: 815850, 21 | Source: 9, 22 | Data: []byte{0x28, 0xC3, 0x6A, 0x00, 0x00, 0xB4, 0x0A, 0xFD}, 23 | }, 24 | }, 25 | { 26 | name: "invalid number of fields", 27 | raw: "$PCDIN,01F112,000C72EA,28C36A0000B40AFD*73", 28 | err: "nmea: PCDIN invalid fields: invalid number of fields in sentence", 29 | }, 30 | { 31 | name: "invalid PGN field", 32 | raw: "$PCDIN,x1F112,000C72EA,09,28C36A0000B40AFD*1e", 33 | err: "nmea: PCDIN failed to parse PGN field: strconv.ParseUint: parsing \"x1F112\": invalid syntax", 34 | }, 35 | { 36 | name: "invalid timestamp field", 37 | raw: "$PCDIN,01F112,x00C72EA,09,28C36A0000B40AFD*1e", 38 | err: "nmea: PCDIN failed to parse timestamp field: strconv.ParseUint: parsing \"x00C72EA\": invalid syntax", 39 | }, 40 | { 41 | name: "invalid source field", 42 | raw: "$PCDIN,01F112,000C72EA,x9,28C36A0000B40AFD*1e", 43 | err: "nmea: PCDIN failed to parse source field: strconv.ParseUint: parsing \"x9\": invalid syntax", 44 | }, 45 | { 46 | name: "invalid hex data", 47 | raw: "$PCDIN,01F112,000C72EA,09,x8C36A0000B40AFD*1c", 48 | err: "nmea: PCDIN failed to decode data: encoding/hex: invalid byte: U+0078 'x'", 49 | }, 50 | } 51 | 52 | for _, tt := range tests { 53 | t.Run(tt.name, func(t *testing.T) { 54 | m, err := Parse(tt.raw) 55 | if tt.err != "" { 56 | assert.Error(t, err) 57 | assert.EqualError(t, err, tt.err) 58 | } else { 59 | assert.NoError(t, err) 60 | pgrme := m.(PCDIN) 61 | pgrme.BaseSentence = BaseSentence{} 62 | assert.Equal(t, tt.msg, pgrme) 63 | } 64 | }) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /pgn.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "strconv" 7 | ) 8 | 9 | const ( 10 | // TypePGN is type of PGN sentence for transferring single NMEA2000 frame as NMEA0183 sentence 11 | TypePGN = "PGN" 12 | ) 13 | 14 | // PGN - transferring single NMEA2000 frame as NMEA0183 sentence 15 | // https://opencpn.org/wiki/dokuwiki/lib/exe/fetch.php?media=opencpn:software:mxpgn_sentence.pdf 16 | // 17 | // Format: $--PGN,pppppp,aaaa,c--c*hh 18 | // Example: $MXPGN,01F112,2807,FC7FFF7FFF168012*11 19 | type PGN struct { 20 | BaseSentence 21 | PGN uint32 // PGN of NMEA2000 packet 22 | IsSend bool // is this sentence received or for sending 23 | Priority uint8 // 0-7 24 | Address uint8 // depending on the IsSend field this is Source Address of received packet or Destination for send packet 25 | Data []byte // 1-8 bytes. This is single N2K frame. N2K Fast-packets should be assembled from individual frames 26 | } 27 | 28 | // newPGN constructor 29 | func newPGN(s BaseSentence) (Sentence, error) { 30 | p := NewParser(s) 31 | p.AssertType(TypePGN) 32 | 33 | if len(p.Fields) != 3 { 34 | p.SetErr("fields", "invalid number of fields in sentence") 35 | return nil, p.Err() 36 | } 37 | pgn, err := strconv.ParseUint(p.Fields[0], 16, 24) 38 | if err != nil { 39 | p.err = fmt.Errorf("nmea: %s failed to parse PGN field: %w", p.Prefix(), err) 40 | return nil, p.Err() 41 | } 42 | attributes, err := strconv.ParseUint(p.Fields[1], 16, 16) 43 | if err != nil { 44 | p.err = fmt.Errorf("nmea: %s failed to parse attributes field: %w", p.Prefix(), err) 45 | return nil, p.Err() 46 | } 47 | dataLength := int((attributes >> 8) & 0b1111) // bits 8-11 48 | if dataLength*2 != (len(p.Fields[2])) { 49 | p.SetErr("dlc", "data length does not match actual data length") 50 | return nil, p.Err() 51 | } 52 | data, err := hex.DecodeString(p.Fields[2]) 53 | if err != nil { 54 | p.err = fmt.Errorf("nmea: %s failed to decode data: %w", p.Prefix(), err) 55 | return nil, p.Err() 56 | } 57 | 58 | return PGN{ 59 | BaseSentence: s, 60 | PGN: uint32(pgn), 61 | IsSend: attributes>>15 == 1, // bit 15 62 | Priority: uint8((attributes >> 12) & 0b111), // bits 12,13,14 63 | Address: uint8(attributes), // bits 0-7 64 | Data: data, 65 | }, p.Err() 66 | } 67 | -------------------------------------------------------------------------------- /pgn_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestPGN(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg PGN 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$MXPGN,01F112,2807,FC7FFF7FFF168012*11", 18 | msg: PGN{ 19 | PGN: 127250, // 0x1F112 Vessel Heading 20 | IsSend: false, 21 | Priority: 2, 22 | Address: 7, 23 | Data: []byte{0xFC, 0x7f, 0xFF, 0x7f, 0xFF, 0x16, 0x80, 0x12}, 24 | }, 25 | }, 26 | { 27 | name: "invalid number of fields", 28 | raw: "$MXPGN,01F112,FC7FFF7FFF168012*30", 29 | err: "nmea: MXPGN invalid fields: invalid number of fields in sentence", 30 | }, 31 | { 32 | name: "invalid PGN field", 33 | raw: "$MXPGN,0xF112,2807,FC7FFF7FFF168012*58", 34 | err: "nmea: MXPGN failed to parse PGN field: strconv.ParseUint: parsing \"0xF112\": invalid syntax", 35 | }, 36 | { 37 | name: "invalid attributes field", 38 | raw: "$MXPGN,01F112,x807,FC7FFF7FFF168012*5b", 39 | err: "nmea: MXPGN failed to parse attributes field: strconv.ParseUint: parsing \"x807\": invalid syntax", 40 | }, 41 | { 42 | name: "invalid data length field", 43 | raw: "$MXPGN,01F112,2207,FC7FFF7FFF168012*1b", 44 | err: "nmea: MXPGN invalid dlc: data length does not match actual data length", 45 | }, 46 | { 47 | name: "invalid hex data", 48 | raw: "$MXPGN,01F112,2807,xC7FFF7FFF168012*2f", 49 | err: "nmea: MXPGN failed to decode data: encoding/hex: invalid byte: U+0078 'x'", 50 | }, 51 | } 52 | 53 | for _, tt := range tests { 54 | t.Run(tt.name, func(t *testing.T) { 55 | m, err := Parse(tt.raw) 56 | if tt.err != "" { 57 | assert.Error(t, err) 58 | assert.EqualError(t, err, tt.err) 59 | } else { 60 | assert.NoError(t, err) 61 | pgrme := m.(PGN) 62 | pgrme.BaseSentence = BaseSentence{} 63 | assert.Equal(t, tt.msg, pgrme) 64 | } 65 | }) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /pgrme.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypePGRME type for PGRME sentences 5 | TypePGRME = "GRME" 6 | // ErrorUnit must be meters (M) 7 | ErrorUnit = "M" 8 | ) 9 | 10 | // PGRME is Estimated Position Error (Garmin proprietary sentence) 11 | // http://aprs.gids.nl/nmea/#rme 12 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_pgrme_garmin_estimated_error 13 | // 14 | // Format: $PGRME,hhh,M,vvv,M,ttt,M*hh 15 | // Example: $PGRME,3.3,M,4.9,M,6.0,M*25 16 | type PGRME struct { 17 | BaseSentence 18 | Horizontal float64 // Estimated horizontal position error (HPE) in metres 19 | Vertical float64 // Estimated vertical position error (VPE) in metres 20 | Spherical float64 // Overall spherical equivalent position error in meters 21 | } 22 | 23 | // newPGRME constructor 24 | func newPGRME(s BaseSentence) (Sentence, error) { 25 | p := NewParser(s) 26 | p.AssertType(TypePGRME) 27 | 28 | horizontal := p.Float64(0, "horizontal error") 29 | _ = p.EnumString(1, "horizontal error unit", ErrorUnit) 30 | 31 | vertial := p.Float64(2, "vertical error") 32 | _ = p.EnumString(3, "vertical error unit", ErrorUnit) 33 | 34 | spherical := p.Float64(4, "spherical error") 35 | _ = p.EnumString(5, "spherical error unit", ErrorUnit) 36 | 37 | return PGRME{ 38 | BaseSentence: s, 39 | Horizontal: horizontal, 40 | Vertical: vertial, 41 | Spherical: spherical, 42 | }, p.Err() 43 | } 44 | -------------------------------------------------------------------------------- /pgrme_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | var pgrmetests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg PGRME 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$PGRME,3.3,M,4.9,M,6.0,M*25", 18 | msg: PGRME{ 19 | Horizontal: 3.3, 20 | Vertical: 4.9, 21 | Spherical: 6, 22 | }, 23 | }, 24 | { 25 | name: "invalid horizontal error", 26 | raw: "$PGRME,A,M,4.9,M,6.0,M*4A", 27 | err: "nmea: PGRME invalid horizontal error: A", 28 | }, 29 | { 30 | name: "invalid vertical error", 31 | raw: "$PGRME,3.3,M,A,M,6.0,M*47", 32 | err: "nmea: PGRME invalid vertical error: A", 33 | }, 34 | { 35 | name: "invalid vertical error unit", 36 | raw: "$PGRME,3.3,M,4.9,A,6.0,M*29", 37 | err: "nmea: PGRME invalid vertical error unit: A", 38 | }, 39 | { 40 | name: "invalid spherical error", 41 | raw: "$PGRME,3.3,M,4.9,M,A,M*4C", 42 | err: "nmea: PGRME invalid spherical error: A", 43 | }, 44 | { 45 | name: "invalid spherical error unit", 46 | raw: "$PGRME,3.3,M,4.9,M,6.0,A*29", 47 | err: "nmea: PGRME invalid spherical error unit: A", 48 | }, 49 | } 50 | 51 | func TestPGRME(t *testing.T) { 52 | for _, tt := range pgrmetests { 53 | t.Run(tt.name, func(t *testing.T) { 54 | m, err := Parse(tt.raw) 55 | if tt.err != "" { 56 | assert.Error(t, err) 57 | assert.EqualError(t, err, tt.err) 58 | } else { 59 | assert.NoError(t, err) 60 | pgrme := m.(PGRME) 61 | pgrme.BaseSentence = BaseSentence{} 62 | assert.Equal(t, tt.msg, pgrme) 63 | } 64 | }) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /pgrmt.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypePGRMT type for PGRMT sentences 5 | TypePGRMT = "GRMT" 6 | // PassPGRMT Self-Test Passed 7 | PassPGRMT = "P" 8 | // FailPGRMT Self-Test Failed 9 | FailPGRMT = "F" 10 | // DataRetainedPGRMT Data Retained 11 | DataRetainedPGRMT = "R" 12 | // DataLostPGRMT Data Lost 13 | DataLostPGRMT = "L" 14 | // DataCollectingPGRMT Data Collecting 15 | DataCollectingPGRMT = "C" 16 | ) 17 | 18 | // PGRMT is Sensor Status Information (Garmin proprietary sentence) 19 | // https://developer.garmin.com/downloads/legacy/uploads/2015/08/190-00684-00.pdf 20 | // $PGRMT,<0>,<1>,<2>,<3>,<4>,<5>,<6>,<7>,<8>*hh 21 | // Format: $PGRMT,xxxxxxxxxx,A,A,A,A,A,A,N,A*hh 22 | // Example: $PGRMT,GPS24xd-HVS VER 2.30,,,,,,,,*10 23 | type PGRMT struct { 24 | BaseSentence 25 | ModelAndFirmwareVersion string 26 | ROMChecksumTest string // "P" = pass, "F" = fail 27 | ReceiverFailureDiscrete string // "P" = pass, "F" = fail 28 | StoredDataLost string // "R" = retained, "L" = lost 29 | RealtimeClockLost string // "R" = retained, "L" = lost 30 | OscillatorDriftDiscrete string // "P" = pass, "F" = fail 31 | DataCollectionDiscrete string // "C" = collecting, "" = not collecting 32 | SensorTemperature float64 // Degrees C 33 | SensorConfigurationData string // "R" = retained, "L" = lost 34 | } 35 | 36 | // newPGRMT constructor 37 | func newPGRMT(s BaseSentence) (Sentence, error) { 38 | p := NewParser(s) 39 | p.AssertType(TypePGRMT) 40 | 41 | return PGRMT{ 42 | BaseSentence: s, 43 | ModelAndFirmwareVersion: p.String(0, "product, model and software version"), 44 | ROMChecksumTest: p.EnumString(1, "rom checksum test", PassPGRMT, FailPGRMT), 45 | ReceiverFailureDiscrete: p.EnumString(2, "receiver failure discrete", PassPGRMT, FailPGRMT), 46 | StoredDataLost: p.EnumString(3, "stored data lost", DataRetainedPGRMT, DataLostPGRMT), 47 | RealtimeClockLost: p.EnumString(4, "realtime clock lost", DataRetainedPGRMT, DataLostPGRMT), 48 | OscillatorDriftDiscrete: p.EnumString(5, "oscillator drift discrete", PassPGRMT, FailPGRMT), 49 | DataCollectionDiscrete: p.EnumString(6, "data collection discrete", DataCollectingPGRMT), 50 | SensorTemperature: p.Float64(7, "sensor temperature in degrees celsius"), 51 | SensorConfigurationData: p.EnumString(8, "sensor configuration data", DataRetainedPGRMT, DataLostPGRMT), 52 | }, p.Err() 53 | } 54 | -------------------------------------------------------------------------------- /pgrmt_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | var pgrmttests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg PGRMT 14 | }{ 15 | { 16 | name: "typical sentence", 17 | raw: "$PGRMT,GPS24xd-HVS VER 2.30,,,,,,,,*10", 18 | msg: PGRMT{ 19 | ModelAndFirmwareVersion: "GPS24xd-HVS VER 2.30", 20 | }, 21 | }, 22 | { 23 | name: "all good", 24 | raw: "$PGRMT,GOOD GPS VER 1.0,P,P,R,R,P,C,32,R*39", 25 | msg: PGRMT{ 26 | ModelAndFirmwareVersion: "GOOD GPS VER 1.0", 27 | ROMChecksumTest: PassPGRMT, 28 | ReceiverFailureDiscrete: PassPGRMT, 29 | StoredDataLost: DataRetainedPGRMT, 30 | RealtimeClockLost: DataRetainedPGRMT, 31 | OscillatorDriftDiscrete: PassPGRMT, 32 | DataCollectionDiscrete: DataCollectingPGRMT, 33 | SensorTemperature: 32, 34 | SensorConfigurationData: DataRetainedPGRMT, 35 | }, 36 | }, 37 | { 38 | name: "all bad", 39 | raw: "$PGRMT,BAD GPS VER 1.0,F,F,L,L,F,,-64,L*18", 40 | msg: PGRMT{ 41 | ModelAndFirmwareVersion: "BAD GPS VER 1.0", 42 | ROMChecksumTest: FailPGRMT, 43 | ReceiverFailureDiscrete: FailPGRMT, 44 | StoredDataLost: DataLostPGRMT, 45 | RealtimeClockLost: DataLostPGRMT, 46 | OscillatorDriftDiscrete: FailPGRMT, 47 | SensorTemperature: -64, 48 | SensorConfigurationData: DataLostPGRMT, 49 | }, 50 | }, 51 | } 52 | 53 | func TestPGRMT(t *testing.T) { 54 | for _, tt := range pgrmttests { 55 | t.Run(tt.name, func(t *testing.T) { 56 | m, err := Parse(tt.raw) 57 | if tt.err != "" { 58 | assert.Error(t, err) 59 | assert.EqualError(t, err, tt.err) 60 | } else { 61 | assert.NoError(t, err) 62 | pgrmt := m.(PGRMT) 63 | pgrmt.BaseSentence = BaseSentence{} 64 | assert.Equal(t, tt.msg, pgrmt) 65 | } 66 | }) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /phtro.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypePHTRO type of PHTRO sentence for vessel pitch and roll 5 | TypePHTRO = "HTRO" 6 | // PHTROBowUP for bow up 7 | PHTROBowUP = "M" 8 | // PHTROBowDown for bow down 9 | PHTROBowDown = "P" 10 | // PHTROPortUP for port up 11 | PHTROPortUP = "T" 12 | // PHTROPortDown for port down 13 | PHTROPortDown = "B" 14 | ) 15 | 16 | // PHTRO is proprietary sentence for vessel pitch and roll. 17 | // https://www.igp.de/manuals/7-INS-InterfaceLibrary_MU-INSIII-AN-001-O.pdf (page 172) 18 | // 19 | // Format: $PHTRO,x.xx,a,y.yy,b*hh 20 | // Example: $PHTRO,10.37,P,177.62,T*65 21 | type PHTRO struct { 22 | BaseSentence 23 | Pitch float64 // Pitch in degrees 24 | Bow string // "M" for bow up and "P" for bow down (2 digits after the decimal point) 25 | Roll float64 // Roll in degrees 26 | Port string // "B" for port down and "T" for port up (2 digits after the decimal point) 27 | } 28 | 29 | // newPHTRO constructor 30 | func newPHTRO(s BaseSentence) (Sentence, error) { 31 | p := NewParser(s) 32 | p.AssertType(TypePHTRO) 33 | m := PHTRO{ 34 | BaseSentence: s, 35 | Pitch: p.Float64(0, "pitch"), 36 | Bow: p.EnumString(1, "bow", PHTROBowUP, PHTROBowDown), 37 | Roll: p.Float64(2, "roll"), 38 | Port: p.EnumString(3, "port", PHTROPortUP, PHTROPortDown), 39 | } 40 | return m, p.Err() 41 | } 42 | -------------------------------------------------------------------------------- /phtro_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestPHTRO(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg PHTRO 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$PHTRO,10.37,P,177.62,T*65", 18 | msg: PHTRO{ 19 | Pitch: 10.37, 20 | Bow: PHTROBowDown, 21 | Roll: 177.62, 22 | Port: PHTROPortUP, 23 | }, 24 | }, 25 | { 26 | name: "invalid Pitch", 27 | raw: "$PHTRO,x,P,177.62,T*36", 28 | err: "nmea: PHTRO invalid pitch: x", 29 | }, 30 | { 31 | name: "invalid Bow", 32 | raw: "$PHTRO,10.37,x,177.62,T*4D", 33 | err: "nmea: PHTRO invalid bow: x", 34 | }, 35 | { 36 | name: "invalid Roll", 37 | raw: "$PHTRO,10.37,P,x,T*06", 38 | err: "nmea: PHTRO invalid roll: x", 39 | }, 40 | { 41 | name: "invalid Port", 42 | raw: "$PHTRO,10.37,P,177.62,x*49", 43 | err: "nmea: PHTRO invalid port: x", 44 | }, 45 | } 46 | for _, tt := range tests { 47 | t.Run(tt.name, func(t *testing.T) { 48 | m, err := Parse(tt.raw) 49 | if tt.err != "" { 50 | assert.Error(t, err) 51 | assert.EqualError(t, err, tt.err) 52 | } else { 53 | assert.NoError(t, err) 54 | phtro := m.(PHTRO) 55 | phtro.BaseSentence = BaseSentence{} 56 | assert.Equal(t, tt.msg, phtro) 57 | } 58 | }) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /pklds.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | const ( 8 | // TypePKLDS type for PKLDS sentences 9 | TypePKLDS = "KLDS" 10 | ) 11 | 12 | // PKLDS is Kenwood propirtary sentance it is RMC with the addition of Fleetsync ID and status information. 13 | // http://aprs.gids.nl/nmea/#rmc 14 | // 15 | // Format: $PKLDS,hhmmss.ss,A,ddmm.mm,a,dddmm.mm,a,x.x,x.x,xxxx,x.x,a*hh 16 | // Example: $PKLDS,220516,A,5133.82,N,00042.24,W,173.8,231.8,130694,004.2,W00,100,2000,25,00*6E 17 | type PKLDS struct { 18 | BaseSentence 19 | Time Time // Time Stamp 20 | Validity string // validity - A-ok, V-invalid 21 | Latitude float64 // Latitude 22 | Longitude float64 // Longitude 23 | Speed float64 // Speed in knots 24 | Course float64 // True course 25 | Date Date // Date 26 | Variation float64 // Magnetic variation 27 | SentanceVersion string // 00 to 15 28 | Fleet string // 100 to 349 29 | UnitID string // 1000 to 4999 30 | Status string // 10 to 99 31 | Extension string // 00 to 99 32 | } 33 | 34 | // newPKLDS constructor 35 | func newPKLDS(s BaseSentence) (Sentence, error) { 36 | p := NewParser(s) 37 | p.AssertType(TypePKLDS) 38 | m := PKLDS{ 39 | BaseSentence: s, 40 | Time: p.Time(0, "time"), 41 | Validity: p.EnumString(1, "validity", ValidRMC, InvalidRMC), 42 | Latitude: p.LatLong(2, 3, "latitude"), 43 | Longitude: p.LatLong(4, 5, "longitude"), 44 | Speed: p.Float64(6, "speed"), 45 | Course: p.Float64(7, "course"), 46 | Date: p.Date(8, "date"), 47 | Variation: p.Float64(9, "variation"), 48 | SentanceVersion: p.String(10, "sentance version, range of 00 to 15"), 49 | Fleet: p.String(11, "fleet, range of 100 to 349"), 50 | UnitID: p.String(12, "subscriber unit id, range of 1000 to 4999"), 51 | Status: p.String(13, "subscriber unit status id, range of 10 to 99"), 52 | Extension: p.String(14, "reserved for future use, range of 00 to 99"), 53 | } 54 | if strings.HasPrefix(m.SentanceVersion, "W") == true { 55 | m.Variation = 0 - m.Variation 56 | } 57 | m.SentanceVersion = strings.TrimPrefix(strings.TrimPrefix(m.SentanceVersion, "W"), "E") 58 | return m, p.Err() 59 | } 60 | -------------------------------------------------------------------------------- /pklds_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | var pkldstests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg PKLDS 14 | }{ 15 | { 16 | name: "good sentence West", 17 | raw: "$PKLDS,220516,A,5133.82,N,00042.24,W,173.8,231.8,130694,004.2,W00,100,2000,15,00,*60", 18 | msg: PKLDS{ 19 | Time: Time{true, 22, 05, 16, 0}, 20 | Validity: "A", 21 | Latitude: MustParseGPS("5133.82 N"), 22 | Longitude: MustParseGPS("00042.24 W"), 23 | Speed: 173.8, 24 | Course: 231.8, 25 | Date: Date{true, 13, 06, 94}, 26 | Variation: -4.2, 27 | SentanceVersion: "00", 28 | Fleet: "100", 29 | UnitID: "2000", 30 | Status: "15", 31 | Extension: "00", 32 | }, 33 | }, 34 | { 35 | name: "good sentence East", 36 | raw: "$PKLDS,220516,A,5133.82,N,00042.24,W,173.8,231.8,130694,004.2,E00,100,2000,15,00,*72", 37 | msg: PKLDS{ 38 | Time: Time{true, 22, 05, 16, 0}, 39 | Validity: "A", 40 | Latitude: MustParseGPS("5133.82 N"), 41 | Longitude: MustParseGPS("00042.24 W"), 42 | Speed: 173.8, 43 | Course: 231.8, 44 | Date: Date{true, 13, 06, 94}, 45 | Variation: 4.2, 46 | SentanceVersion: "00", 47 | Fleet: "100", 48 | UnitID: "2000", 49 | Status: "15", 50 | Extension: "00", 51 | }, 52 | }, 53 | 54 | { 55 | name: "bad sentence", 56 | raw: "$PKLDS,220516,D,5133.82,N,00042.24,W,173.8,231.8,130694,004.2,W00,100,2000,15,00,*65", 57 | err: "nmea: PKLDS invalid validity: D", 58 | }, 59 | } 60 | 61 | func TestPKLDS(t *testing.T) { 62 | for _, tt := range pkldstests { 63 | t.Run(tt.name, func(t *testing.T) { 64 | m, err := Parse(tt.raw) 65 | if tt.err != "" { 66 | assert.Error(t, err) 67 | assert.EqualError(t, err, tt.err) 68 | } else { 69 | assert.NoError(t, err) 70 | pklds := m.(PKLDS) 71 | pklds.BaseSentence = BaseSentence{} 72 | assert.Equal(t, tt.msg, pklds) 73 | } 74 | }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /pklid.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypePKLID type for PKLID sentances 5 | TypePKLID = "KLID" 6 | ) 7 | 8 | // PKLID is a Kenwood Propritary sentance used for GPS data communications in FleetSync. 9 | // $PKLID,<0>,<1>,<2>,<3>,<4>*hh 10 | // Format: $PKLID,xx,xxx,xxxx,xx,xx,*xx 11 | // Example: $PKLID,00,100,2000,15,00,*?? 12 | type PKLID struct { 13 | BaseSentence 14 | SentanceVersion string // 00 to 15 15 | Fleet string // 100 to 349 16 | UnitID string // 1000 to 4999 17 | Status string // 10 to 99 18 | Extension string // 00 to 99 19 | } 20 | 21 | // newPKLID constructor 22 | func newPKLID(s BaseSentence) (Sentence, error) { 23 | p := NewParser(s) 24 | p.AssertType(TypePKLID) 25 | 26 | return PKLID{ 27 | BaseSentence: s, 28 | SentanceVersion: p.String(0, "sentance version, range of 00 to 15"), 29 | Fleet: p.String(1, "fleet, range of 100 to 349"), 30 | UnitID: p.String(2, "subscriber unit id, range of 1000 to 4999"), 31 | Status: p.String(3, "subscriber unit status id, range of 10 to 99"), 32 | Extension: p.String(4, "reserved for future use, range of 00 to 99"), 33 | }, p.Err() 34 | } 35 | -------------------------------------------------------------------------------- /pklid_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | var pklidtests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg PKLID 14 | }{ 15 | { 16 | name: "typical sentance", 17 | raw: "$PKLID,00,100,2000,15,00,*6D", 18 | msg: PKLID{ 19 | SentanceVersion: "00", 20 | Fleet: "100", 21 | UnitID: "2000", 22 | Status: "15", 23 | Extension: "00", 24 | }, 25 | }, 26 | } 27 | 28 | func TestPKLID(t *testing.T) { 29 | for _, tt := range pklidtests { 30 | t.Run(tt.name, func(t *testing.T) { 31 | m, err := Parse(tt.raw) 32 | if tt.err != "" { 33 | assert.Error(t, err) 34 | assert.EqualError(t, err, tt.err) 35 | } else { 36 | assert.NoError(t, err) 37 | pklid := m.(PKLID) 38 | pklid.BaseSentence = BaseSentence{} 39 | assert.Equal(t, tt.msg, pklid) 40 | } 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pklsh.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypePKLSH type for PKLSH sentances 5 | TypePKLSH = "KLSH" 6 | ) 7 | 8 | // PKLSH is a Kenwood Propritary sentance used for GPS data communications in FleetSync. 9 | // 10 | // adds UnitID and Fleet to $GPGLL sentance 11 | // 12 | // $PKLSH,<0>,<1>,<2>,<3>,<4>,<5>,<6>,<7>*hh 13 | // Format: $PKLSH,xxxx.xxxx,x,xxxxx.xxxx,x,xxxxxx,x,xxx,xxxx,*xx 14 | // Example: $PKLSH,4000.0000,N,13500.0000,E,021720,A,100,2000,*?? 15 | type PKLSH struct { 16 | BaseSentence 17 | Latitude float64 // Latitude 18 | Longitude float64 // Longitude 19 | Time Time // Time Stamp 20 | Validity string // validity - A=valid, V=invalid 21 | Fleet string // 100 to 349 22 | UnitID string // 1000 to 4999 23 | } 24 | 25 | // newPKLSH constructor 26 | func newPKLSH(s BaseSentence) (Sentence, error) { 27 | p := NewParser(s) 28 | p.AssertType(TypePKLSH) 29 | 30 | return PKLSH{ 31 | BaseSentence: s, 32 | Latitude: p.LatLong(0, 1, "latitude"), 33 | Longitude: p.LatLong(2, 3, "longitude"), 34 | Time: p.Time(4, "time"), 35 | Validity: p.EnumString(5, "validity", ValidGLL, InvalidGLL), 36 | Fleet: p.String(6, "fleet, range of 100 to 349"), 37 | UnitID: p.String(7, "subscriber unit id, range of 1000 to 4999"), 38 | }, p.Err() 39 | } 40 | -------------------------------------------------------------------------------- /pklsh_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | var pklshtests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg PKLSH 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$PKLSH,3926.7952,N,12000.5947,W,022732,A,100,2000*1A", 18 | msg: PKLSH{ 19 | Latitude: MustParseLatLong("3926.7952 N"), 20 | Longitude: MustParseLatLong("12000.5947 W"), 21 | Time: Time{ 22 | Valid: true, 23 | Hour: 2, 24 | Minute: 27, 25 | Second: 32, 26 | Millisecond: 0, 27 | }, 28 | Validity: "A", 29 | Fleet: "100", 30 | UnitID: "2000", 31 | }, 32 | }, 33 | { 34 | name: "bad validity", 35 | raw: "$PKLSH,3926.7952,N,12000.5947,W,022732,D,100,2000*1F", 36 | err: "nmea: PKLSH invalid validity: D", 37 | }, 38 | } 39 | 40 | func TestPKLSH(t *testing.T) { 41 | for _, tt := range pklshtests { 42 | t.Run(tt.name, func(t *testing.T) { 43 | m, err := Parse(tt.raw) 44 | if tt.err != "" { 45 | assert.Error(t, err) 46 | assert.EqualError(t, err, tt.err) 47 | } else { 48 | assert.NoError(t, err) 49 | pklsh := m.(PKLSH) 50 | pklsh.BaseSentence = BaseSentence{} 51 | assert.Equal(t, tt.msg, pklsh) 52 | } 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /pknds.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | const ( 8 | // TypePKNDS type for PKLDS sentences 9 | TypePKNDS = "KNDS" 10 | ) 11 | 12 | // PKNDS is Kenwood propirtary sentance it is RMC with the addition of NEXTEDGE and status information. 13 | // http://aprs.gids.nl/nmea/#rmc 14 | // 15 | // Format: $PKNDS,hhmmss.ss,A,ddmm.mm,a,dddmm.mm,a,xxx.x,x,x.x,xxx,Uxxxx,xxx.xx,*hh 16 | // Example: $PKNDS,220516,A,5133.82,N,00042.24,W,173.8,231.8,130694,004.2,W00,U00001,207,00,*6E 17 | type PKNDS struct { 18 | BaseSentence 19 | Time Time // Time Stamp 20 | Validity string // validity - A-ok, V-invalid 21 | Latitude float64 // Latitude 22 | Longitude float64 // Longitude 23 | Speed float64 // Speed in knots 24 | Course float64 // True course 25 | Date Date // Date 26 | Variation float64 // Magnetic variation 27 | SentanceVersion string // 00 to 15 28 | UnitID string // U00001 to U65519 or U00000001 to U16776415 (U is FIXED) 29 | Status string // 001 to 255 30 | Extension string // 00 to 99 31 | } 32 | 33 | // newPKNDS constructor 34 | func newPKNDS(s BaseSentence) (Sentence, error) { 35 | p := NewParser(s) 36 | p.AssertType(TypePKNDS) 37 | m := PKNDS{ 38 | BaseSentence: s, 39 | Time: p.Time(0, "time"), 40 | Validity: p.EnumString(1, "validity", ValidRMC, InvalidRMC), 41 | Latitude: p.LatLong(2, 3, "latitude"), 42 | Longitude: p.LatLong(4, 5, "longitude"), 43 | Speed: p.Float64(6, "speed"), 44 | Course: p.Float64(7, "course"), 45 | Date: p.Date(8, "date"), 46 | Variation: p.Float64(9, "variation"), 47 | SentanceVersion: p.String(10, "sentance version, range of 00 to 15"), 48 | UnitID: p.String(11, "unit ID, NXDN range U00001 to U65519, DMR range of U00000001 to U16776415"), 49 | Status: p.String(12, "subscriber unit status id, range of 001 to 255"), 50 | Extension: p.String(13, "reserved for future use, range of 00 to 99"), 51 | } 52 | if strings.HasPrefix(m.SentanceVersion, "W") == true { 53 | m.Variation = 0 - m.Variation 54 | } 55 | m.SentanceVersion = strings.TrimPrefix(strings.TrimPrefix(m.SentanceVersion, "W"), "E") 56 | return m, p.Err() 57 | } 58 | -------------------------------------------------------------------------------- /pknds_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | var pkndstests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg PKNDS 14 | }{ 15 | { 16 | name: "good sentence West", 17 | raw: "$PKNDS,220516,A,5133.82,N,00042.24,W,173.8,231.8,130694,004.2,W00,U00001,207,00,*28", 18 | msg: PKNDS{ 19 | Time: Time{true, 22, 05, 16, 0}, 20 | Validity: "A", 21 | Latitude: MustParseGPS("5133.82 N"), 22 | Longitude: MustParseGPS("00042.24 W"), 23 | Speed: 173.8, 24 | Course: 231.8, 25 | Date: Date{true, 13, 06, 94}, 26 | Variation: -4.2, 27 | SentanceVersion: "00", 28 | UnitID: "U00001", 29 | Status: "207", 30 | Extension: "00", 31 | }, 32 | }, 33 | { 34 | name: "good sentence East", 35 | raw: "$PKNDS,220516,A,5133.82,N,00042.24,W,173.8,231.8,130694,004.2,E00,U00001,207,00,*3A", 36 | msg: PKNDS{ 37 | Time: Time{true, 22, 05, 16, 0}, 38 | Validity: "A", 39 | Latitude: MustParseGPS("5133.82 N"), 40 | Longitude: MustParseGPS("00042.24 W"), 41 | Speed: 173.8, 42 | Course: 231.8, 43 | Date: Date{true, 13, 06, 94}, 44 | Variation: 4.2, 45 | SentanceVersion: "00", 46 | UnitID: "U00001", 47 | Status: "207", 48 | Extension: "00", 49 | }, 50 | }, 51 | 52 | { 53 | name: "bad sentence", 54 | raw: "$PKNDS,220516,D,5133.82,N,00042.24,W,173.8,231.8,130694,004.2,E00,U00001,207,00,*3F", 55 | err: "nmea: PKNDS invalid validity: D", 56 | }, 57 | } 58 | 59 | func TestPKNDS(t *testing.T) { 60 | for _, tt := range pkndstests { 61 | t.Run(tt.name, func(t *testing.T) { 62 | m, err := Parse(tt.raw) 63 | if tt.err != "" { 64 | assert.Error(t, err) 65 | assert.EqualError(t, err, tt.err) 66 | } else { 67 | assert.NoError(t, err) 68 | pknds := m.(PKNDS) 69 | pknds.BaseSentence = BaseSentence{} 70 | assert.Equal(t, tt.msg, pknds) 71 | } 72 | }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /pknid.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypePKNID type for PKNID sentances 5 | TypePKNID = "KNID" 6 | ) 7 | 8 | // PKNID is a Kenwood Propritary sentance used for GPS data communications in NEXTEDGE Digital. 9 | // $PKNID,<0>,<1>,<2>,<3>,*hh 10 | // Format: $PKNID,xx,Uxxxx,xxx,xx,*xx 11 | // Example: $PKNID,00,U00065519,207,00,*?? 12 | type PKNID struct { 13 | BaseSentence 14 | SentanceVersion string // 00 to 15 15 | UnitID string // U00001 to U65519 or U00000001 to U16776415 (U is FIXED) 16 | Status string // 001 to 255 17 | Extension string // 00 to 99 18 | } 19 | 20 | // newPKNID constructor 21 | func newPKNID(s BaseSentence) (Sentence, error) { 22 | p := NewParser(s) 23 | p.AssertType(TypePKNID) 24 | 25 | return PKNID{ 26 | BaseSentence: s, 27 | SentanceVersion: p.String(0, "sentance version, range of 00 to 15"), 28 | UnitID: p.String(1, "unit ID, NXDN range U00001 to U65519, DMR range of U00000001 to U16776415"), 29 | Status: p.String(2, "status NXDN, range of 001 to 255"), 30 | Extension: p.String(3, "reserved for future use, range of 00 to 99"), 31 | }, p.Err() 32 | } 33 | -------------------------------------------------------------------------------- /pknid_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | var pknidtests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg PKNID 14 | }{ 15 | { 16 | name: "typical sentance", 17 | raw: "$PKNID,00,U00001,015,00,*24", 18 | msg: PKNID{ 19 | SentanceVersion: "00", 20 | UnitID: "U00001", 21 | Status: "015", 22 | Extension: "00", 23 | }, 24 | }, 25 | } 26 | 27 | func TestPKNID(t *testing.T) { 28 | for _, tt := range pknidtests { 29 | t.Run(tt.name, func(t *testing.T) { 30 | m, err := Parse(tt.raw) 31 | if tt.err != "" { 32 | assert.Error(t, err) 33 | assert.EqualError(t, err, tt.err) 34 | } else { 35 | assert.NoError(t, err) 36 | pknid := m.(PKNID) 37 | pknid.BaseSentence = BaseSentence{} 38 | assert.Equal(t, tt.msg, pknid) 39 | } 40 | }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pknsh.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypePKNSH type for PKLSH sentances 5 | TypePKNSH = "KNSH" 6 | ) 7 | 8 | // PKNSH is a Kenwood Propritary sentance used for GPS data communications in NEXTEDGE Digital. 9 | // 10 | // adds UnitID and Fleet to $GPGLL sentance 11 | // 12 | // $PKNSH,<0>,<1>,<2>,<3>,<4>,<5>,<6>*hh 13 | // Format: $PKNSH,xxxx.xxxx,x,xxxxx.xxxx,x,xxxxxx,x,Uxxxxx,*xx 14 | // Example: $PKNSH,4000.0000,N,13500.0000,E,021720,A,U00001,*?? 15 | type PKNSH struct { 16 | BaseSentence 17 | Latitude float64 // Latitude 18 | Longitude float64 // Longitude 19 | Time Time // Time Stamp 20 | Validity string // validity - A=valid, V=invalid 21 | UnitID string // U00001 to U65519 or U00000001 to U16776415 (U is FIXED) 22 | } 23 | 24 | // newPKNSH constructor 25 | func newPKNSH(s BaseSentence) (Sentence, error) { 26 | p := NewParser(s) 27 | p.AssertType(TypePKNSH) 28 | 29 | return PKNSH{ 30 | BaseSentence: s, 31 | Latitude: p.LatLong(0, 1, "latitude"), 32 | Longitude: p.LatLong(2, 3, "longitude"), 33 | Time: p.Time(4, "time"), 34 | Validity: p.EnumString(5, "validity", ValidGLL, InvalidGLL), 35 | UnitID: p.String(6, "unit ID, NXDN range U00001 to U65519, DMR range of U00000001 to U16776415"), 36 | }, p.Err() 37 | } 38 | -------------------------------------------------------------------------------- /pknsh_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | var pknshtests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg PKNSH 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$PKNSH,3926.7952,N,12000.5947,W,022732,A,U00001*63", 18 | msg: PKNSH{ 19 | Latitude: MustParseLatLong("3926.7952 N"), 20 | Longitude: MustParseLatLong("12000.5947 W"), 21 | Time: Time{ 22 | Valid: true, 23 | Hour: 2, 24 | Minute: 27, 25 | Second: 32, 26 | Millisecond: 0, 27 | }, 28 | Validity: "A", 29 | UnitID: "U00001", 30 | }, 31 | }, 32 | { 33 | name: "bad validity", 34 | raw: "$PKNSH,3926.7952,N,12000.5947,W,022732,D,U00001*66", 35 | err: "nmea: PKNSH invalid validity: D", 36 | }, 37 | } 38 | 39 | func TestPKNSH(t *testing.T) { 40 | for _, tt := range pknshtests { 41 | t.Run(tt.name, func(t *testing.T) { 42 | m, err := Parse(tt.raw) 43 | if tt.err != "" { 44 | assert.Error(t, err) 45 | assert.EqualError(t, err, tt.err) 46 | } else { 47 | assert.NoError(t, err) 48 | pknsh := m.(PKNSH) 49 | pknsh.BaseSentence = BaseSentence{} 50 | assert.Equal(t, tt.msg, pknsh) 51 | } 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /pkwdwpl.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypePKWDWPL type for PKLDS sentences 5 | TypePKWDWPL = "KWDWPL" 6 | ) 7 | 8 | // PKWDWPL is Kenwood Waypoint Location 9 | // https://github.com/wb2osz/direwolf/blob/master/waypoint.c 10 | // 11 | // Format: $PKWDWPL,hhmmss,v,ddmm.mm,ns,dddmm.mm,ew,speed,course,ddmmyy,alt,wname,ts*hh 12 | // Example: $PKWDWPL,204714,V,4237.1400,N,07120.8300,W,,,200316,,test|5,/'*61 13 | type PKWDWPL struct { 14 | BaseSentence 15 | Time Time // Time Stamp 16 | Validity string // validity - A-ok, V-invalid 17 | Latitude float64 // Latitude 18 | Longitude float64 // Longitude 19 | Speed float64 // Speed in knots 20 | Course float64 // True course 21 | Date Date // Date 22 | Altitude float64 // Magnetic variation 23 | WaypointName string // 00 to 15 24 | TableSymbol string // U00001 to U65519 or U00000001 to U16776415 (U is FIXED) 25 | } 26 | 27 | // newPKWDWPL constructor 28 | func newPKWDWPL(s BaseSentence) (Sentence, error) { 29 | p := NewParser(s) 30 | p.AssertType(TypePKWDWPL) 31 | m := PKWDWPL{ 32 | BaseSentence: s, 33 | Time: p.Time(0, "time"), 34 | Validity: p.EnumString(1, "validity", ValidRMC, InvalidRMC), 35 | Latitude: p.LatLong(2, 3, "latitude"), 36 | Longitude: p.LatLong(4, 5, "longitude"), 37 | Speed: p.Float64(6, "speed"), 38 | Course: p.Float64(7, "course"), 39 | Date: p.Date(8, "date"), 40 | Altitude: p.Float64(9, "altitude"), 41 | WaypointName: p.String(10, "waypoint name, Object name/Sendin Station"), 42 | TableSymbol: p.String(11, "table and symbol as per APRS spec"), 43 | } 44 | return m, p.Err() 45 | } 46 | -------------------------------------------------------------------------------- /pkwdwpl_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | var pkwdkpltests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg PKWDWPL 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$PKWDWPL,150803,A,4237.14,N,07120.83,W,173.8,231.8,190316,1120,test,/'*39", 18 | msg: PKWDWPL{ 19 | Time: Time{true, 15, 8, 3, 0}, 20 | Validity: "A", 21 | Latitude: MustParseGPS("4237.14 N"), 22 | Longitude: MustParseGPS("07120.83 W"), 23 | Speed: 173.8, 24 | Course: 231.8, 25 | Date: Date{true, 19, 3, 16}, 26 | Altitude: 1120, 27 | WaypointName: "test", 28 | TableSymbol: "/'", 29 | }, 30 | }, 31 | } 32 | 33 | func TestPKWDWPL(t *testing.T) { 34 | for _, tt := range pkwdkpltests { 35 | t.Run(tt.name, func(t *testing.T) { 36 | m, err := Parse(tt.raw) 37 | if tt.err != "" { 38 | assert.Error(t, err) 39 | assert.EqualError(t, err, tt.err) 40 | } else { 41 | assert.NoError(t, err) 42 | pkwdwpl := m.(PKWDWPL) 43 | pkwdwpl.BaseSentence = BaseSentence{} 44 | assert.Equal(t, tt.msg, pkwdwpl) 45 | } 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /pmtk.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypePMTK001 type of acknowledgement sentence for MTK protocol 5 | TypePMTK001 = "MTK001" 6 | ) 7 | 8 | // PMTK001 is sentence for acknowledgement of previously sent command/packet 9 | // https://www.rhydolabz.com/documents/25/PMTK_A11.pdf 10 | // https://www.sparkfun.com/datasheets/GPS/Modules/PMTK_Protocol.pdf 11 | // 12 | // The maximum length of each packet is restricted to 255 bytes which is longer than NMEA0183 82 bytes. 13 | // 14 | // Format: $PMTK001,c-c,d*hh 15 | // Example: $PMTK001,101,0*33 16 | type PMTK001 struct { 17 | BaseSentence 18 | 19 | // Cmd is command/packet acknowledgement is sent for. 20 | // Three bytes character string. From "000" to "999". 21 | Cmd int64 22 | 23 | // Flag is acknowledgement status for previously sent command/packet 24 | // 0 = invalid command/packet type 25 | // 1 = unsupported command packet type 26 | // 2 = valid command/packet, but action failed 27 | // 3 = valid command/packet and action succeeded 28 | Flag int64 29 | } 30 | 31 | // newPMTK001 constructor 32 | func newPMTK001(s BaseSentence) (Sentence, error) { 33 | p := NewParser(s) 34 | 35 | cmd := p.Int64(0, "command") 36 | flag := p.Int64(1, "flag") 37 | return PMTK001{ 38 | BaseSentence: s, 39 | Cmd: cmd, 40 | Flag: flag, 41 | }, p.Err() 42 | } 43 | -------------------------------------------------------------------------------- /pmtk_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPMTK001(t *testing.T) { 10 | var tests = []struct { 11 | name string 12 | raw string 13 | err string 14 | msg PMTK001 15 | }{ 16 | { 17 | name: "good: Packet Type: 001 PMTK_ACK", 18 | raw: "$PMTK001,604,3*32", 19 | msg: PMTK001{ 20 | Cmd: 604, 21 | Flag: 3, 22 | }, 23 | }, 24 | { 25 | name: "missing flag", 26 | raw: "$PMTK001,604*2d", 27 | err: "nmea: PMTK001 invalid flag: index out of range", 28 | }, 29 | { 30 | name: "missing cmd", 31 | raw: "$PMTK001*33", 32 | err: "nmea: PMTK001 invalid command: index out of range", 33 | }, 34 | } 35 | p := SentenceParser{} 36 | for _, tt := range tests { 37 | t.Run(tt.name, func(t *testing.T) { 38 | m, err := p.Parse(tt.raw) 39 | if tt.err != "" { 40 | assert.Error(t, err) 41 | assert.EqualError(t, err, tt.err) 42 | } else { 43 | assert.NoError(t, err) 44 | mtk := m.(PMTK001) // is used by non-global SentenceParser instance 45 | mtk.BaseSentence = BaseSentence{} 46 | assert.Equal(t, tt.msg, mtk) 47 | } 48 | }) 49 | } 50 | } 51 | 52 | func TestDefaultParserUsesDeprecatedMTK(t *testing.T) { 53 | m, err := Parse("$PMTK001,604,3*32") 54 | assert.NoError(t, err) 55 | assert.IsType(t, MTK{}, m) 56 | } 57 | -------------------------------------------------------------------------------- /prdid.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypePRDID type of PRDID sentence for vessel pitch, roll and heading 5 | TypePRDID = "RDID" 6 | ) 7 | 8 | // PRDID is proprietary sentence for vessel pitch, roll and heading. 9 | // https://www.xsens.com/hubfs/Downloads/Manuals/MT_Low-Level_Documentation.pdf (page 37) 10 | // 11 | // Format: $PRDID,aPPP.PP,bRRR.RR,HHH.HH*hh 12 | // Example: $PRDID,-10.37,2.34,230.34*AA 13 | type PRDID struct { 14 | BaseSentence 15 | Pitch float64 // Pitch in degrees (positive bow up) 16 | Roll float64 // Roll in degrees (positive port up) 17 | Heading float64 // True heading in degrees 18 | } 19 | 20 | // newPRDID constructor 21 | func newPRDID(s BaseSentence) (Sentence, error) { 22 | p := NewParser(s) 23 | p.AssertType(TypePRDID) 24 | m := PRDID{ 25 | BaseSentence: s, 26 | Pitch: p.Float64(0, "pitch"), 27 | Roll: p.Float64(1, "roll"), 28 | Heading: p.Float64(2, "heading"), 29 | } 30 | return m, p.Err() 31 | } 32 | -------------------------------------------------------------------------------- /prdid_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestPRDID(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg PRDID 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$PRDID,-10.37,2.34,230.34*62", 18 | msg: PRDID{ 19 | Pitch: -10.37, 20 | Roll: 2.34, 21 | Heading: 230.34, 22 | }, 23 | }, 24 | { 25 | name: "invalid Pitch", 26 | raw: "$PRDID,x.37,2.34,230.34*36", 27 | err: "nmea: PRDID invalid pitch: x.37", 28 | }, 29 | { 30 | name: "invalid Roll", 31 | raw: "$PRDID,-10.37,x.34,230.34*28", 32 | err: "nmea: PRDID invalid roll: x.34", 33 | }, 34 | { 35 | name: "invalid Heading", 36 | raw: "$PRDID,-10.37,2.34,x.34*2B", 37 | err: "nmea: PRDID invalid heading: x.34", 38 | }, 39 | } 40 | for _, tt := range tests { 41 | t.Run(tt.name, func(t *testing.T) { 42 | m, err := Parse(tt.raw) 43 | if tt.err != "" { 44 | assert.Error(t, err) 45 | assert.EqualError(t, err, tt.err) 46 | } else { 47 | assert.NoError(t, err) 48 | prdid := m.(PRDID) 49 | prdid.BaseSentence = BaseSentence{} 50 | assert.Equal(t, tt.msg, prdid) 51 | } 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /pskpdpt.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypePSKPDPT type for proprietary Skipper PSKPDPT sentences 5 | TypePSKPDPT = "SKPDPT" 6 | ) 7 | 8 | // PSKPDPT - Depth of Water for multiple transducer installation 9 | // https://www.alphatronmarine.com/files/products/120-echonav-skipper-gds101-instoper-manual-12-6-2017_1556099135_47f5f8d1.pdf (page 56, Edition: 2017.06.12) 10 | // https://www.kongsberg.com/globalassets/maritime/km-products/product-documents/164821aa_rd301_instruction_manual_lr.pdf (page 2, 857-164821aa) 11 | // 12 | // Format: $PSKPDPT,x.x,x.x,x.x,xx,xx,c--c*hh 13 | // Example: $PSKPDPT,0002.5,+00.0,0010,10,03,*77 14 | type PSKPDPT struct { 15 | BaseSentence 16 | // Depth is water depth relative to transducer, meters 17 | Depth float64 18 | // Offset from transducer, meters 19 | Offset float64 20 | // RangeScale is Maximum range scale in use, meters 21 | RangeScale float64 22 | // BottomEchoStrength is Bottom echo strength (0,9) 23 | BottomEchoStrength int64 24 | // ChannelNumber is Echo sounder channel number (0-99) (1 = 38 kHz. 2 = 50 kHz. 3 = 200 kHz) 25 | ChannelNumber int64 26 | // TransducerLocation is Transducer location. Text string, indicating transducer position: FWD/AFT/PORT/STB. 27 | // If position is not preset by operator, empty field is provided. 28 | TransducerLocation string 29 | } 30 | 31 | // newPSKPDPT constructor 32 | func newPSKPDPT(s BaseSentence) (Sentence, error) { 33 | p := NewParser(s) 34 | p.AssertType(TypePSKPDPT) 35 | sentence := PSKPDPT{ 36 | BaseSentence: s, 37 | Depth: p.Float64(0, "depth"), 38 | Offset: p.Float64(1, "offset"), 39 | RangeScale: p.Float64(2, "range scale"), 40 | BottomEchoStrength: p.Int64(3, "bottom echo strength"), 41 | ChannelNumber: p.Int64(4, "channel number"), 42 | TransducerLocation: p.String(5, "transducer location"), 43 | } 44 | return sentence, p.Err() 45 | } 46 | -------------------------------------------------------------------------------- /pskpdpt_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPSKPDPT(t *testing.T) { 10 | var testcases = []struct { 11 | name string 12 | raw string 13 | err string 14 | msg PSKPDPT 15 | }{ 16 | { 17 | name: "good sentence, empty location", 18 | raw: "$PSKPDPT,0002.5,+00.0,0010,10,03,*77", 19 | msg: PSKPDPT{ 20 | Depth: 2.5, 21 | Offset: 0, 22 | RangeScale: 10, 23 | BottomEchoStrength: 10, 24 | ChannelNumber: 03, 25 | TransducerLocation: "", 26 | }, 27 | }, 28 | { 29 | name: "good sentence", 30 | raw: "$PSKPDPT,0002.5,-01.1,0010,10,03,AFT*22", 31 | msg: PSKPDPT{ 32 | Depth: 2.5, 33 | Offset: -1.1, 34 | RangeScale: 10, 35 | BottomEchoStrength: 10, 36 | ChannelNumber: 03, 37 | TransducerLocation: "AFT", 38 | }, 39 | }, 40 | { 41 | name: "invalid nmea: Depth", 42 | raw: "$PSKPDPT,x0002.5,+00.0,0010,10,03,*0f", 43 | err: "nmea: PSKPDPT invalid depth: x0002.5", 44 | }, 45 | { 46 | name: "invalid nmea: Offset", 47 | raw: "$PSKPDPT,0002.5,+x00.0,0010,10,03,*0f", 48 | err: "nmea: PSKPDPT invalid offset: +x00.0", 49 | }, 50 | { 51 | name: "invalid nmea: RangeScale", 52 | raw: "$PSKPDPT,0002.5,+00.0,x0010,10,03,*0f", 53 | err: "nmea: PSKPDPT invalid range scale: x0010", 54 | }, 55 | { 56 | name: "invalid nmea: BottomEchoStrength", 57 | raw: "$PSKPDPT,0002.5,+00.0,0010,10x,03,*0f", 58 | err: "nmea: PSKPDPT invalid bottom echo strength: 10x", 59 | }, 60 | { 61 | name: "invalid nmea: ChannelNumber", 62 | raw: "$PSKPDPT,0002.5,+00.0,0010,10,0x3,*0f", 63 | err: "nmea: PSKPDPT invalid channel number: 0x3", 64 | }, 65 | } 66 | 67 | for _, tt := range testcases { 68 | t.Run(tt.name, func(t *testing.T) { 69 | m, err := Parse(tt.raw) 70 | if tt.err != "" { 71 | assert.Error(t, err) 72 | assert.EqualError(t, err, tt.err) 73 | } else { 74 | assert.NoError(t, err) 75 | sentence := m.(PSKPDPT) 76 | sentence.BaseSentence = BaseSentence{} 77 | assert.Equal(t, tt.msg, sentence) 78 | } 79 | }) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /psoncms.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypePSONCMS is type of PSONCMS sentence for proprietary Xsens IMU/VRU/AHRS device 5 | TypePSONCMS = "SONCMS" 6 | ) 7 | 8 | // PSONCMS is proprietary Xsens IMU/VRU/AHRS device sentence for quaternion, acceleration, rate of turn, 9 | // magnetic Field, sensor temperature. 10 | // https://www.xsens.com/hubfs/Downloads/Manuals/MT_Low-Level_Documentation.pdf (page 37) 11 | // 12 | // Format: $PSONCMS,Q.QQQQ,P.PPPP,R.RRRR,S.SSSS,XX.XXXX,YY.YYYY,ZZ.ZZZZ, 13 | // FF.FFFF,GG.GGGG,HH.HHHH,NN.NNNN,MM,MMMM,PP.PPPP,TT.T*hh 14 | // Example: $PSONCMS,0.0905,0.4217,0.9020,-0.0196,-1.7685,0.3861,-9.6648,-0.0116,0.0065,-0.0080,0.0581,0.3846,0.7421,33.1*76 15 | type PSONCMS struct { 16 | BaseSentence 17 | Quaternion0 float64 // q0 from quaternions 18 | Quaternion1 float64 // q1 from quaternions 19 | Quaternion2 float64 // q2 from quaternions 20 | Quaternion3 float64 // q3 from quaternions 21 | AccelerationX float64 // acceleration X in m/s2 22 | AccelerationY float64 // acceleration Y in m/s2 23 | AccelerationZ float64 // acceleration Z in m/s2 24 | RateOfTurnX float64 // rate of turn X in rad/s 25 | RateOfTurnY float64 // rate of turn Y in rad/s 26 | RateOfTurnZ float64 // rate of turn Z in rad/s 27 | MagneticFieldX float64 // magnetic field X in a.u. 28 | MagneticFieldY float64 // magnetic field Y in a.u. 29 | MagneticFieldZ float64 // magnetic field Z in a.u. 30 | SensorTemperature float64 // sensor temperature in degrees Celsius 31 | } 32 | 33 | // newPSONCMS constructor 34 | func newPSONCMS(s BaseSentence) (Sentence, error) { 35 | p := NewParser(s) 36 | p.AssertType(TypePSONCMS) 37 | m := PSONCMS{ 38 | BaseSentence: s, 39 | Quaternion0: p.Float64(0, "q0 from quaternions"), 40 | Quaternion1: p.Float64(1, "q1 from quaternions"), 41 | Quaternion2: p.Float64(2, "q2 from quaternions"), 42 | Quaternion3: p.Float64(3, "q3 from quaternions"), 43 | AccelerationX: p.Float64(4, "acceleration X"), 44 | AccelerationY: p.Float64(5, "acceleration Y"), 45 | AccelerationZ: p.Float64(6, "acceleration Z"), 46 | RateOfTurnX: p.Float64(7, "rate of turn X"), 47 | RateOfTurnY: p.Float64(8, "rate of turn Y"), 48 | RateOfTurnZ: p.Float64(9, "rate of turn Z"), 49 | MagneticFieldX: p.Float64(10, "magnetic field X"), 50 | MagneticFieldY: p.Float64(11, "magnetic field Y"), 51 | MagneticFieldZ: p.Float64(12, "magnetic field Z"), 52 | SensorTemperature: p.Float64(13, "sensor temperature"), 53 | } 54 | return m, p.Err() 55 | } 56 | -------------------------------------------------------------------------------- /psoncms_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestPSONCMS(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg PSONCMS 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$PSONCMS,0.0905,0.4217,0.9020,-0.0196,-1.7685,0.3861,-9.6648,-0.0116,0.0065,-0.0080,0.0581,0.3846,0.7421,33.1*76", 18 | msg: PSONCMS{ 19 | BaseSentence: BaseSentence{}, 20 | Quaternion0: 0.0905, 21 | Quaternion1: 0.4217, 22 | Quaternion2: 0.9020, 23 | Quaternion3: -0.0196, 24 | AccelerationX: -1.7685, 25 | AccelerationY: 0.3861, 26 | AccelerationZ: -9.6648, 27 | RateOfTurnX: -0.0116, 28 | RateOfTurnY: 0.0065, 29 | RateOfTurnZ: -0.0080, 30 | MagneticFieldX: 0.0581, 31 | MagneticFieldY: 0.3846, 32 | MagneticFieldZ: 0.7421, 33 | SensorTemperature: 33.1, 34 | }, 35 | }, 36 | { 37 | name: "invalid Quaternion0", 38 | raw: "$PSONCMS,x,0.4217,0.9020,-0.0196,-1.7685,0.3861,-9.6648,-0.0116,0.0065,-0.0080,0.0581,0.3846,0.7421,33.1*1C", 39 | err: "nmea: PSONCMS invalid q0 from quaternions: x", 40 | }, 41 | { 42 | name: "invalid Quaternion1", 43 | raw: "$PSONCMS,0.0905,x,0.9020,-0.0196,-1.7685,0.3861,-9.6648,-0.0116,0.0065,-0.0080,0.0581,0.3846,0.7421,33.1*10", 44 | err: "nmea: PSONCMS invalid q1 from quaternions: x", 45 | }, 46 | } 47 | for _, tt := range tests { 48 | t.Run(tt.name, func(t *testing.T) { 49 | m, err := Parse(tt.raw) 50 | if tt.err != "" { 51 | assert.Error(t, err) 52 | assert.EqualError(t, err, tt.err) 53 | } else { 54 | assert.NoError(t, err) 55 | psoncms := m.(PSONCMS) 56 | psoncms.BaseSentence = BaseSentence{} 57 | assert.Equal(t, tt.msg, psoncms) 58 | } 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /query.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeQuery type of Query sentence for a listener to request a particular sentence from a talker 5 | TypeQuery = "Q" 6 | ) 7 | 8 | // Query sentences is special type of sentence for a listener to request a particular sentence from a talker. 9 | // https://www.tronico.fi/OH6NT/docs/NMEA0183.pdf (page 3) 10 | // 11 | // Format: $ttllQ,sss*hh 12 | // Example: $CCGPQ,GGA*2B 13 | type Query struct { 14 | BaseSentence 15 | DestinationTalkerID string 16 | RequestedSentence string 17 | } 18 | 19 | // newQuery constructor 20 | func newQuery(s BaseSentence) (Sentence, error) { 21 | p := NewParser(s) 22 | p.AssertType(TypeQuery) 23 | 24 | return Query{ 25 | BaseSentence: s, 26 | DestinationTalkerID: s.Raw[3:5], 27 | RequestedSentence: p.String(0, "requested sentence"), 28 | }, p.Err() 29 | } 30 | -------------------------------------------------------------------------------- /query_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestQuery(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg Query 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$CCGPQ,GGA*2B", 18 | msg: Query{ 19 | BaseSentence: BaseSentence{}, 20 | DestinationTalkerID: "GP", 21 | RequestedSentence: "GGA", 22 | }, 23 | }, 24 | { 25 | name: "invalid nmea: RequestedSentence", 26 | raw: "$CCGPQ*46", 27 | err: "nmea: CCQ invalid requested sentence: index out of range", 28 | }, 29 | } 30 | for _, tt := range tests { 31 | t.Run(tt.name, func(t *testing.T) { 32 | m, err := Parse(tt.raw) 33 | if tt.err != "" { 34 | assert.Error(t, err) 35 | assert.EqualError(t, err, tt.err) 36 | } else { 37 | assert.NoError(t, err) 38 | rmb := m.(Query) 39 | rmb.BaseSentence = BaseSentence{} 40 | assert.Equal(t, tt.msg, rmb) 41 | } 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /rmc.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeRMC type for RMC sentences 5 | TypeRMC = "RMC" 6 | // ValidRMC character 7 | ValidRMC = "A" 8 | // InvalidRMC character 9 | InvalidRMC = "V" 10 | ) 11 | 12 | // RMC is the Recommended Minimum Specific GNSS data. 13 | // http://aprs.gids.nl/nmea/#rmc 14 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_rmc_recommended_minimum_navigation_information 15 | // 16 | // Format: $--RMC,hhmmss.ss,A,ddmm.mm,a,dddmm.mm,a,x.x,x.x,xxxx,x.x,a*hh 17 | // Format NMEA 2.3: $--RMC,hhmmss.ss,A,ddmm.mm,a,dddmm.mm,a,x.x,x.x,xxxx,x.x,a,m*hh 18 | // Format NMEA 4.1: $--RMC,hhmmss.ss,A,ddmm.mm,a,dddmm.mm,a,x.x,x.x,xxxx,x.x,a,m,s*hh 19 | // Example: $GNRMC,220516,A,5133.82,N,00042.24,W,173.8,231.8,130694,004.2,W*6E 20 | // $GNRMC,142754.0,A,4302.539570,N,07920.379823,W,0.0,,070617,0.0,E,A*21 21 | // $GNRMC,102014.00,A,5550.6082,N,03732.2488,E,000.00000,092.9,300518,,,A,V*3B 22 | type RMC struct { 23 | BaseSentence 24 | Time Time // Time Stamp 25 | Validity string // validity - A-ok, V-invalid 26 | Latitude float64 // Latitude 27 | Longitude float64 // Longitude 28 | Speed float64 // Speed in knots 29 | Course float64 // True course 30 | Date Date // Date 31 | Variation float64 // Magnetic variation 32 | FFAMode string // FAA mode indicator (filled in NMEA 2.3 and later) 33 | NavStatus string // Nav Status (NMEA 4.1 and later) 34 | } 35 | 36 | // newRMC constructor 37 | func newRMC(s BaseSentence) (Sentence, error) { 38 | p := NewParser(s) 39 | p.AssertType(TypeRMC) 40 | m := RMC{ 41 | BaseSentence: s, 42 | Time: p.Time(0, "time"), 43 | Validity: p.EnumString(1, "validity", ValidRMC, InvalidRMC), 44 | Latitude: p.LatLong(2, 3, "latitude"), 45 | Longitude: p.LatLong(4, 5, "longitude"), 46 | Speed: p.Float64(6, "speed"), 47 | Course: p.Float64(7, "course"), 48 | Date: p.Date(8, "date"), 49 | Variation: p.Float64(9, "variation"), 50 | } 51 | if p.EnumString(10, "direction", West, East) == West { 52 | m.Variation = 0 - m.Variation 53 | } 54 | if len(p.Fields) > 11 { 55 | m.FFAMode = p.String(11, "FAA mode") // not enum because some devices have proprietary "non-nmea" values 56 | } 57 | if len(p.Fields) > 12 { 58 | m.NavStatus = p.EnumString( 59 | 12, 60 | "navigation status", 61 | NavStatusSafe, 62 | NavStatusCaution, 63 | NavStatusUnsafe, 64 | NavStatusNotValid, 65 | ) 66 | } 67 | return m, p.Err() 68 | } 69 | -------------------------------------------------------------------------------- /rot.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeROT type of ROT sentence for vessel rate of turn 5 | TypeROT = "ROT" 6 | // ValidROT data is valid 7 | ValidROT = "A" 8 | // InvalidROT data is invalid 9 | InvalidROT = "V" 10 | ) 11 | 12 | // ROT is sentence for rate of turn. 13 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_rot_rate_of_turn 14 | // 15 | // Format: $HEROT,-xxx.x,A*hh 16 | // Example: $HEROT,-11.23,A*07 17 | type ROT struct { 18 | BaseSentence 19 | RateOfTurn float64 // rate of turn Z in deg/min (- means bow turns to port) 20 | Valid bool // "A" data valid, "V" invalid data 21 | } 22 | 23 | func newROT(s BaseSentence) (Sentence, error) { 24 | p := NewParser(s) 25 | p.AssertType(TypeROT) 26 | return ROT{ 27 | BaseSentence: s, 28 | RateOfTurn: p.Float64(0, "rate of turn"), 29 | Valid: p.EnumString(1, "status valid", ValidROT, InvalidROT) == ValidROT, 30 | }, p.Err() 31 | } 32 | -------------------------------------------------------------------------------- /rot_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestROT(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg ROT 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$HEROT,-11.23,A*07", 18 | msg: ROT{ 19 | RateOfTurn: -11.23, 20 | Valid: true, 21 | }, 22 | }, 23 | { 24 | name: "invalid RateOfTurn", 25 | raw: "$HEROT,x,A*7D", 26 | err: "nmea: HEROT invalid rate of turn: x", 27 | }, 28 | { 29 | name: "invalid Valid", 30 | raw: "$HEROT,-11.23,X*1E", 31 | err: "nmea: HEROT invalid status valid: X", 32 | }, 33 | } 34 | for _, tt := range tests { 35 | t.Run(tt.name, func(t *testing.T) { 36 | m, err := Parse(tt.raw) 37 | if tt.err != "" { 38 | assert.Error(t, err) 39 | assert.EqualError(t, err, tt.err) 40 | } else { 41 | assert.NoError(t, err) 42 | rot := m.(ROT) 43 | rot.BaseSentence = BaseSentence{} 44 | assert.Equal(t, tt.msg, rot) 45 | } 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /rpm.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeRPM type of RPM sentence for Engine or Shaft revolutions and pitch 5 | TypeRPM = "RPM" 6 | 7 | // SourceEngineRPM is value for case when source is Engine 8 | SourceEngineRPM = "E" 9 | // SourceShaftRPM is value for case when source is Shaft 10 | SourceShaftRPM = "S" 11 | ) 12 | 13 | // RPM - Engine or Shaft revolutions and pitch 14 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_rpm_revolutions 15 | // 16 | // Format: $--RPM,a,x,x.x,x.x,A*hh 17 | // Example: $RCRPM,S,0,74.6,30.0,A*56 18 | type RPM struct { 19 | BaseSentence 20 | Source string // Source, S = Shaft, E = Engine 21 | EngineNumber int64 // Engine or shaft number 22 | SpeedRPM float64 // Speed, Revolutions per minute 23 | PitchPercent float64 // Propeller pitch, % of maximum, "-" means astern 24 | Status string // Status, A = Valid, V = Invalid 25 | } 26 | 27 | // newRPM constructor 28 | func newRPM(s BaseSentence) (Sentence, error) { 29 | p := NewParser(s) 30 | p.AssertType(TypeRPM) 31 | return RPM{ 32 | BaseSentence: s, 33 | Source: p.EnumString(0, "source", SourceEngineRPM, SourceShaftRPM), 34 | EngineNumber: p.Int64(1, "engine number"), 35 | SpeedRPM: p.Float64(2, "speed"), 36 | PitchPercent: p.Float64(3, "pitch"), 37 | Status: p.EnumString(4, "status", StatusValid, StatusInvalid), 38 | }, p.Err() 39 | } 40 | -------------------------------------------------------------------------------- /rpm_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestRPM(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg RPM 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$RCRPM,S,0,74.6,30.0,A*56", 18 | msg: RPM{ 19 | Source: SourceShaftRPM, 20 | EngineNumber: 0, 21 | SpeedRPM: 74.6, 22 | PitchPercent: 30, 23 | Status: StatusValid, 24 | }, 25 | }, 26 | { 27 | name: "invalid nmea: Source", 28 | raw: "$RCRPM,x,0,74.6,30.0,A*7D", 29 | err: "nmea: RCRPM invalid source: x", 30 | }, 31 | { 32 | name: "invalid nmea: Status", 33 | raw: "$RCRPM,S,0,74.6,30.0,x*6F", 34 | err: "nmea: RCRPM invalid status: x", 35 | }, 36 | } 37 | for _, tt := range tests { 38 | t.Run(tt.name, func(t *testing.T) { 39 | m, err := Parse(tt.raw) 40 | if tt.err != "" { 41 | assert.Error(t, err) 42 | assert.EqualError(t, err, tt.err) 43 | } else { 44 | assert.NoError(t, err) 45 | rpm := m.(RPM) 46 | rpm.BaseSentence = BaseSentence{} 47 | assert.Equal(t, tt.msg, rpm) 48 | } 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /rsa.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeRSA type of RSA sentence for Rudder Sensor Angle 5 | TypeRSA = "RSA" 6 | ) 7 | 8 | // RSA - Rudder Sensor Angle 9 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_rsa_rudder_sensor_angle 10 | // 11 | // Format: $--RSA,x.x,A,x.x,A*hh 12 | // Example: $IIRSA,10.5,A,,V*4D 13 | type RSA struct { 14 | BaseSentence 15 | StarboardRudderAngle float64 // Starboard (or single) rudder sensor, "-" means Turn To Port 16 | StarboardRudderAngleStatus string // Status, A = valid, V = Invalid 17 | PortRudderAngle float64 // Port rudder sensor 18 | PortRudderAngleStatus string // Status, A = valid, V = Invalid 19 | } 20 | 21 | // newRSA constructor 22 | func newRSA(s BaseSentence) (Sentence, error) { 23 | p := NewParser(s) 24 | p.AssertType(TypeRSA) 25 | return RSA{ 26 | BaseSentence: s, 27 | StarboardRudderAngle: p.Float64(0, "starboard rudder angle"), 28 | StarboardRudderAngleStatus: p.EnumString(1, "starboard rudder angle status", StatusValid, StatusInvalid), 29 | PortRudderAngle: p.Float64(2, "port rudder angle"), 30 | PortRudderAngleStatus: p.EnumString(3, "port rudder angle status", StatusValid, StatusInvalid), 31 | }, p.Err() 32 | } 33 | -------------------------------------------------------------------------------- /rsa_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestRSA(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg RSA 14 | }{ 15 | { 16 | name: "good sentence 1", 17 | raw: "$IIRSA,10.5,A,0.4,A*70", 18 | msg: RSA{ 19 | StarboardRudderAngle: 10.5, 20 | StarboardRudderAngleStatus: StatusValid, 21 | PortRudderAngle: 0.4, 22 | PortRudderAngleStatus: StatusValid, 23 | }, 24 | }, 25 | { 26 | name: "good sentence 2", 27 | raw: "$IIRSA,10.5,A,,V*4D", 28 | msg: RSA{ 29 | StarboardRudderAngle: 10.5, 30 | StarboardRudderAngleStatus: StatusValid, 31 | PortRudderAngle: 0, 32 | PortRudderAngleStatus: StatusInvalid, 33 | }, 34 | }, 35 | { 36 | name: "invalid nmea: StarboardRudderAngleStatus", 37 | raw: "$IIRSA,10.5,x,,V*74", 38 | err: "nmea: IIRSA invalid starboard rudder angle status: x", 39 | }, 40 | { 41 | name: "invalid nmea: PortRudderAngleStatus", 42 | raw: "$IIRSA,10.5,A,,x*63", 43 | err: "nmea: IIRSA invalid port rudder angle status: x", 44 | }, 45 | } 46 | for _, tt := range tests { 47 | t.Run(tt.name, func(t *testing.T) { 48 | m, err := Parse(tt.raw) 49 | if tt.err != "" { 50 | assert.Error(t, err) 51 | assert.EqualError(t, err, tt.err) 52 | } else { 53 | assert.NoError(t, err) 54 | rsa := m.(RSA) 55 | rsa.BaseSentence = BaseSentence{} 56 | assert.Equal(t, tt.msg, rsa) 57 | } 58 | }) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /rte.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeRTE type for RTE sentences 5 | TypeRTE = "RTE" 6 | 7 | // ActiveRoute active route 8 | ActiveRoute = "c" 9 | 10 | // WaypointList list containing waypoints 11 | WaypointList = "w" 12 | ) 13 | 14 | // RTE is a route of waypoints 15 | // http://aprs.gids.nl/nmea/#rte 16 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_rte_routes 17 | // 18 | // Format: $--RTE,x.x,x.x,a,c--c,c--c, ..... c--c*hh 19 | // Example: $GPRTE,2,1,c,0,PBRCPK,PBRTO,PTELGR,PPLAND,PYAMBU,PPFAIR,PWARRN,PMORTL,PLISMR*73 20 | type RTE struct { 21 | BaseSentence 22 | NumberOfSentences int64 // Number of sentences in sequence 23 | SentenceNumber int64 // Sentence number 24 | ActiveRouteOrWaypointList string // Current active route or waypoint list 25 | Name string // Name or number of active route 26 | Idents []string // List of ident of waypoints 27 | } 28 | 29 | // newRTE constructor 30 | func newRTE(s BaseSentence) (Sentence, error) { 31 | p := NewParser(s) 32 | p.AssertType(TypeRTE) 33 | return RTE{ 34 | BaseSentence: s, 35 | NumberOfSentences: p.Int64(0, "number of sentences"), 36 | SentenceNumber: p.Int64(1, "sentence number"), 37 | ActiveRouteOrWaypointList: p.EnumString(2, "active route or waypoint list", ActiveRoute, WaypointList), 38 | Name: p.String(3, "name or number"), 39 | Idents: p.ListString(4, "ident of waypoints"), 40 | }, p.Err() 41 | } 42 | -------------------------------------------------------------------------------- /rte_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | var rtetests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg RTE 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$IIRTE,4,1,c,Rte 1,411,412,413,414,415*6F", 18 | msg: RTE{ 19 | NumberOfSentences: 4, 20 | SentenceNumber: 1, 21 | ActiveRouteOrWaypointList: ActiveRoute, 22 | Name: "Rte 1", 23 | Idents: []string{"411", "412", "413", "414", "415"}, 24 | }, 25 | }, 26 | { 27 | name: "index out if range", 28 | raw: "$IIRTE,4,1,c,Rte 1*77", 29 | err: "nmea: IIRTE invalid ident of waypoints: index out of range", 30 | }, 31 | { 32 | name: "invalid number of sentences", 33 | raw: "$IIRTE,X,1,c,Rte 1,411,412,413,414,415*03", 34 | err: "nmea: IIRTE invalid number of sentences: X", 35 | }, 36 | { 37 | name: "invalid sentence number", 38 | raw: "$IIRTE,4,X,c,Rte 1,411,412,413,414,415*06", 39 | err: "nmea: IIRTE invalid sentence number: X", 40 | }, 41 | { 42 | name: "invalid active route or waypoint list", 43 | raw: "$IIRTE,4,1,X,Rte 1,411,412,413,414,415*54", 44 | err: "nmea: IIRTE invalid active route or waypoint list: X", 45 | }, 46 | } 47 | 48 | func TestRTE(t *testing.T) { 49 | for _, tt := range rtetests { 50 | t.Run(tt.name, func(t *testing.T) { 51 | m, err := Parse(tt.raw) 52 | if tt.err != "" { 53 | assert.Error(t, err) 54 | assert.EqualError(t, err, tt.err) 55 | } else { 56 | assert.NoError(t, err) 57 | rte := m.(RTE) 58 | rte.BaseSentence = BaseSentence{} 59 | assert.Equal(t, tt.msg, rte) 60 | } 61 | }) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /ths.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeTHS type for THS sentences 5 | TypeTHS = "THS" 6 | // AutonomousTHS autonomous ths heading 7 | AutonomousTHS = "A" 8 | // EstimatedTHS estimated (dead reckoning) THS heading 9 | EstimatedTHS = "E" 10 | // ManualTHS manual input THS heading 11 | ManualTHS = "M" 12 | // SimulatorTHS simulated THS heading 13 | SimulatorTHS = "S" 14 | // InvalidTHS not valid THS heading (or standby) 15 | InvalidTHS = "V" 16 | ) 17 | 18 | // THS is the Actual vessel heading in degrees True with status. 19 | // http://www.nuovamarea.net/pytheas_9.html 20 | // http://manuals.spectracom.com/VSP/Content/VSP/NMEA_THSmess.htm 21 | // 22 | // Format: $--THS,xxx.xx,c*hh 23 | // Example: $GPTHS,338.01,A*36 24 | type THS struct { 25 | BaseSentence 26 | Heading float64 // Heading in degrees 27 | Status string // Heading status 28 | } 29 | 30 | // newTHS constructor 31 | func newTHS(s BaseSentence) (Sentence, error) { 32 | p := NewParser(s) 33 | p.AssertType(TypeTHS) 34 | m := THS{ 35 | BaseSentence: s, 36 | Heading: p.Float64(0, "heading"), 37 | Status: p.EnumString(1, "status", AutonomousTHS, EstimatedTHS, ManualTHS, SimulatorTHS, InvalidTHS), 38 | } 39 | return m, p.Err() 40 | } 41 | -------------------------------------------------------------------------------- /ths_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | var thstests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg THS 14 | }{ 15 | { 16 | name: "good sentence AutonomousTHS", 17 | raw: "$INTHS,123.456,A*20", 18 | msg: THS{ 19 | Heading: 123.456, 20 | Status: AutonomousTHS, 21 | }, 22 | }, 23 | { 24 | name: "good sentence EstimatedTHS", 25 | raw: "$INTHS,123.456,E*24", 26 | msg: THS{ 27 | Heading: 123.456, 28 | Status: EstimatedTHS, 29 | }, 30 | }, 31 | { 32 | name: "good sentence ManualTHS", 33 | raw: "$INTHS,123.456,M*2C", 34 | msg: THS{ 35 | Heading: 123.456, 36 | Status: ManualTHS, 37 | }, 38 | }, 39 | { 40 | name: "good sentence SimulatorTHS", 41 | raw: "$INTHS,123.456,S*32", 42 | msg: THS{ 43 | Heading: 123.456, 44 | Status: SimulatorTHS, 45 | }, 46 | }, 47 | { 48 | name: "good sentence InvalidTHS", 49 | raw: "$INTHS,,V*1E", 50 | msg: THS{ 51 | Heading: 0.0, 52 | Status: InvalidTHS, 53 | }, 54 | }, 55 | { 56 | name: "invalid Status", 57 | raw: "$INTHS,123.456,B*23", 58 | err: "nmea: INTHS invalid status: B", 59 | }, 60 | { 61 | name: "invalid Heading", 62 | raw: "$INTHS,XXX,A*51", 63 | err: "nmea: INTHS invalid heading: XXX", 64 | }, 65 | } 66 | 67 | func TestTHS(t *testing.T) { 68 | for _, tt := range thstests { 69 | t.Run(tt.name, func(t *testing.T) { 70 | m, err := Parse(tt.raw) 71 | if tt.err != "" { 72 | assert.Error(t, err) 73 | assert.EqualError(t, err, tt.err) 74 | } else { 75 | assert.NoError(t, err) 76 | ths := m.(THS) 77 | ths.BaseSentence = BaseSentence{} 78 | assert.Equal(t, tt.msg, ths) 79 | } 80 | }) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /tlb.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import "errors" 4 | 5 | const ( 6 | // TypeTLB type of TLB target label. 7 | TypeTLB = "TLB" 8 | ) 9 | 10 | // TLB is sentence for target label. 11 | // https://fcc.report/FCC-ID/ADB9ZWRTR100/2768717.pdf (page 8) FURUNO MARINE RADAR, model FAR-15XX manual 12 | // 13 | // Format: $--TLB,x.x,c--c,x.x,c--c,...x.x,c--c*hh 14 | // Example: $CDTLB,1,XXX,2.0,YYY*41 15 | type TLB struct { 16 | BaseSentence 17 | Targets []TLBTarget 18 | } 19 | 20 | // TLBTarget is instance of target for TLB sentence 21 | type TLBTarget struct { 22 | // TargetNumber is target number “n” reported by the device (1 - 1023) 23 | TargetNumber float64 24 | // TargetLabel is label assigned to target “n” (TT=000 - 999, AIS=000000000 - 999999999). Could be empty. 25 | TargetLabel string 26 | } 27 | 28 | // newTLB constructor 29 | func newTLB(s BaseSentence) (Sentence, error) { 30 | p := NewParser(s) 31 | p.AssertType(TypeTLB) 32 | tlb := TLB{ 33 | BaseSentence: s, 34 | Targets: make([]TLBTarget, 0), 35 | } 36 | fieldCount := len(p.Fields) 37 | if fieldCount < 2 { 38 | return tlb, errors.New("TLB is missing fields for parsing target pairs") 39 | } 40 | if fieldCount%2 != 0 { 41 | return tlb, errors.New("TLB data set field count is not exactly dividable by 2") 42 | } 43 | tlb.Targets = make([]TLBTarget, 0, fieldCount/2) 44 | for i := 0; i < fieldCount; i = i + 2 { 45 | tmp := TLBTarget{ 46 | TargetNumber: p.Float64(0+i, "target number"), 47 | TargetLabel: p.String(1+i, "target label"), 48 | } 49 | tlb.Targets = append(tlb.Targets, tmp) 50 | } 51 | return tlb, p.Err() 52 | } 53 | -------------------------------------------------------------------------------- /tlb_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestTLB(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg TLB 14 | }{ 15 | { 16 | name: "good sentence, single target", 17 | raw: "$RATLB,1,XXX*20", 18 | msg: TLB{ 19 | Targets: []TLBTarget{ 20 | {TargetNumber: 1, TargetLabel: "XXX"}, 21 | }, 22 | }, 23 | }, 24 | { 25 | name: "good sentence, multiple targets", 26 | raw: "$RATLB,1,XXX,2.0,YYY*55", 27 | msg: TLB{ 28 | Targets: []TLBTarget{ 29 | {TargetNumber: 1, TargetLabel: "XXX"}, 30 | {TargetNumber: 2, TargetLabel: "YYY"}, 31 | }, 32 | }, 33 | }, 34 | { 35 | name: "invalid nmea: field count", 36 | raw: "$RATLB,1*54", 37 | err: "TLB is missing fields for parsing target pairs", 38 | }, 39 | { 40 | name: "invalid nmea: data set field count", 41 | raw: "$RATLB,1,XXX,2.0*20", 42 | err: "TLB data set field count is not exactly dividable by 2", 43 | }, 44 | { 45 | name: "invalid nmea: target number", 46 | raw: "$RATLB,x,XXX*69", 47 | err: "nmea: RATLB invalid target number: x", 48 | }, 49 | } 50 | for _, tt := range tests { 51 | t.Run(tt.name, func(t *testing.T) { 52 | m, err := Parse(tt.raw) 53 | if tt.err != "" { 54 | assert.Error(t, err) 55 | assert.EqualError(t, err, tt.err) 56 | } else { 57 | assert.NoError(t, err) 58 | tlb := m.(TLB) 59 | tlb.BaseSentence = BaseSentence{} 60 | assert.Equal(t, tt.msg, tlb) 61 | } 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tll.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeTLL type of TLL sentence for Target latitude and longitude 5 | TypeTLL = "TLL" 6 | 7 | // RadarTargetLost is used when target is lost 8 | RadarTargetLost = "L" 9 | // RadarTargetAcquisition is used when target is acquired 10 | RadarTargetAcquisition = "Q" 11 | // RadarTargetTracking is used when tracking target 12 | RadarTargetTracking = "T" 13 | ) 14 | 15 | // TLL - Target latitude and longitude 16 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_tll_target_latitude_and_longitude 17 | // https://github.com/nohal/OpenCPN/wiki/ARPA-targets-tracking-implementation#tll---target-latitude-and-longitude 18 | // 19 | // Format: $--TLL,xx,llll.ll,a,yyyyy.yy,a,c--c,hhmmss.ss,a,a*hh 20 | // Example: $RATLL,,3647.422,N,01432.592,E,,,,*58 21 | type TLL struct { 22 | BaseSentence 23 | TargetNumber int64 // Target number 00 – 99 24 | TargetLatitude float64 // Target latitude + N/S 25 | TargetLongitude float64 // Target longitude + E/W 26 | TargetName string // Target name 27 | TimeUTC Time // UTC of data, hh is hours, mm is minutes, ss.ss is seconds. 28 | TargetStatus string // Target status (L=lost, Q=acquisition, T=tracking) 29 | ReferenceTarget string // Reference target, R= reference target; null (,,)= otherwise 30 | } 31 | 32 | // newTLL constructor 33 | func newTLL(s BaseSentence) (Sentence, error) { 34 | p := NewParser(s) 35 | p.AssertType(TypeTLL) 36 | return TLL{ 37 | BaseSentence: s, 38 | TargetNumber: p.Int64(0, "target number"), 39 | TargetLatitude: p.LatLong(1, 2, "latitude"), 40 | TargetLongitude: p.LatLong(3, 4, "longitude"), 41 | TargetName: p.String(5, "target name"), 42 | TimeUTC: p.Time(6, "UTC time"), 43 | TargetStatus: p.EnumString(7, "target status", RadarTargetLost, RadarTargetAcquisition, RadarTargetTracking), 44 | ReferenceTarget: p.EnumString(8, "reference target", "R"), 45 | }, p.Err() 46 | } 47 | -------------------------------------------------------------------------------- /ttd.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeTTD type of TTD sentence for tracked target data. 5 | TypeTTD = "TTD" 6 | ) 7 | 8 | // TTD is sentence used by radars to transmit tracked targets data. 9 | // https://fcc.report/FCC-ID/ADB9ZWRTR100/2768717.pdf (page 1) FURUNO MARINE RADAR, model FAR-15XX manual 10 | // 11 | // Format: !--TTD,hh,hh,x,s--s,x*hh 12 | // Example: !RATTD,1A,01,1,177KQJ5000G?tO`K>RA1wUbN0TKH,0*5C 13 | type TTD struct { 14 | BaseSentence 15 | // NumFragments is total hex number of fragments/sentences need to transfer the message (1 - FF) 16 | NumFragments int64 // 0 17 | // FragmentNumber is current fragment/sentence number (1 - FF) 18 | FragmentNumber int64 // 1 19 | // MessageID is sequential message identifier (0 - 9, null) 20 | MessageID int64 // 2 21 | // Payload is encapsulated tracked target data (6 bit binary-converted data) 22 | Payload []byte // 3 23 | // 4 - Number of fill bits (0 - 5) 24 | } 25 | 26 | // newTTD constructor 27 | func newTTD(s BaseSentence) (Sentence, error) { 28 | p := NewParser(s) 29 | p.AssertType(TypeTTD) 30 | m := TTD{ 31 | BaseSentence: s, 32 | NumFragments: p.HexInt64(0, "number of fragments"), 33 | FragmentNumber: p.HexInt64(1, "fragment number"), 34 | MessageID: p.Int64(2, "sequence number"), 35 | Payload: p.SixBitASCIIArmour(3, int(p.Int64(4, "number of padding bits")), "payload"), 36 | } 37 | return m, p.Err() 38 | } 39 | -------------------------------------------------------------------------------- /txt.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import "strings" 4 | 5 | const ( 6 | // TypeTXT type for TXT sentences for the transmission of text messages 7 | TypeTXT = "TXT" 8 | ) 9 | 10 | // TXT is sentence for the transmission of short text messages, longer text messages may be transmitted by using 11 | // multiple sentences. This sentence is intended to convey human readable textual information for display purposes. 12 | // The TXT sentence shall not be used for sending commands and making device configuration changes. 13 | // https://www.nmea.org/Assets/20160520%20txt%20amendment.pdf 14 | // 15 | // Format: $--TXT,xx,xx,xx,c-c*hh 16 | // Example: $GNTXT,01,01,02,u-blox AG - www.u-blox.com*4E 17 | type TXT struct { 18 | BaseSentence 19 | TotalNumber int64 // total number of sentences, 01 to 99 20 | Number int64 // number of current sentences, 01 to 99 21 | ID int64 // identifier of the text message, 01 to 99 22 | // Message contains ASCII characters, and code delimiters if needed, up to the maximum permitted sentence length 23 | // (i.e., up to 61 characters including any code delimiters) 24 | Message string 25 | } 26 | 27 | // newTXT constructor 28 | func newTXT(s BaseSentence) (Sentence, error) { 29 | p := NewParser(s) 30 | p.AssertType(TypeTXT) 31 | m := TXT{ 32 | BaseSentence: s, 33 | TotalNumber: p.Int64(0, "total number of sentences"), 34 | Number: p.Int64(1, "sentence number"), 35 | ID: p.Int64(2, "sentence identifier"), 36 | Message: strings.Join(p.Fields[3:], FieldSep), 37 | } 38 | return m, p.Err() 39 | } 40 | -------------------------------------------------------------------------------- /txt_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestTXT(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg TXT 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$GNTXT,01,01,02,u-blox AG - www.u-blox.com*4E", 18 | msg: TXT{ 19 | TotalNumber: 1, 20 | Number: 1, 21 | ID: 2, 22 | Message: "u-blox AG - www.u-blox.com", 23 | }, 24 | }, 25 | { 26 | name: "invalid TotalNumber", 27 | raw: "$GNTXT,x,01,02,u-blox AG - www.u-blox.com*37", 28 | err: "nmea: GNTXT invalid total number of sentences: x", 29 | }, 30 | { 31 | name: "invalid Number", 32 | raw: "$GNTXT,01,X,02,u-blox AG - www.u-blox.com*17", 33 | err: "nmea: GNTXT invalid sentence number: X", 34 | }, 35 | { 36 | name: "invalid ID", 37 | raw: "$GNTXT,01,01,X,u-blox AG - www.u-blox.com*14", 38 | err: "nmea: GNTXT invalid sentence identifier: X", 39 | }, 40 | } 41 | for _, tt := range tests { 42 | t.Run(tt.name, func(t *testing.T) { 43 | m, err := Parse(tt.raw) 44 | if tt.err != "" { 45 | assert.Error(t, err) 46 | assert.EqualError(t, err, tt.err) 47 | } else { 48 | assert.NoError(t, err) 49 | txt := m.(TXT) 50 | txt.BaseSentence = BaseSentence{} 51 | assert.Equal(t, tt.msg, txt) 52 | } 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /vdmvdo.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeVDM type for VDM sentences 5 | TypeVDM = "VDM" 6 | 7 | // TypeVDO type for VDO sentences 8 | TypeVDO = "VDO" 9 | ) 10 | 11 | // VDMVDO is sentence ($--VDM or $--VDO) used to encapsulate generic binary payloads. It is most commonly used with AIS data. 12 | // https://gpsd.gitlab.io/gpsd/AIVDM.html 13 | // 14 | // Format: !--VDO,x,x,x,a,s--s,x*hh 15 | // Format: !--VDM,x,x,x,a,s--s,x*hh 16 | // Example: !AIVDM,1,1,,B,177KQJ5000G?tO`K>RA1wUbN0TKH,0*5C 17 | type VDMVDO struct { 18 | BaseSentence 19 | NumFragments int64 20 | FragmentNumber int64 21 | MessageID int64 22 | Channel string 23 | Payload []byte 24 | } 25 | 26 | // newVDMVDO constructor 27 | func newVDMVDO(s BaseSentence) (Sentence, error) { 28 | p := NewParser(s) 29 | m := VDMVDO{ 30 | BaseSentence: s, 31 | NumFragments: p.Int64(0, "number of fragments"), 32 | FragmentNumber: p.Int64(1, "fragment number"), 33 | MessageID: p.Int64(2, "sequence number"), 34 | Channel: p.String(3, "channel ID"), 35 | Payload: p.SixBitASCIIArmour(4, int(p.Int64(5, "number of padding bits")), "payload"), 36 | } 37 | return m, p.Err() 38 | } 39 | -------------------------------------------------------------------------------- /vdr.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeVDR type of VDR sentence for Set and Drift 5 | TypeVDR = "VDR" 6 | ) 7 | 8 | // VDR - Set and Drift 9 | // In navigation, set and drift are characteristics of the current and velocity of water over the ground in which a ship 10 | // is sailing. Set is the bearing the current is flowing. Drift is the magnitude of the current. 11 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_vdr_set_and_drift 12 | // 13 | // Format: $--VDR,x.x,T,x.x,M,x.x,N*hh 14 | // Example: $IIVDR,10.1,T,12.3,M,1.2,N*3A 15 | type VDR struct { 16 | BaseSentence 17 | SetDegreesTrue float64 // Direction degrees, True 18 | SetDegreesTrueUnit string // T = True 19 | SetDegreesMagnetic float64 // Direction degrees, True 20 | SetDegreesMagneticUnit string // M = Magnetic 21 | DriftKnots float64 // Current speed, knots 22 | DriftUnit string // N = Knots 23 | } 24 | 25 | // newVDR constructor 26 | func newVDR(s BaseSentence) (Sentence, error) { 27 | p := NewParser(s) 28 | p.AssertType(TypeVDR) 29 | return VDR{ 30 | BaseSentence: s, 31 | SetDegreesTrue: p.Float64(0, "true set degrees"), 32 | SetDegreesTrueUnit: p.EnumString(1, "true set unit", BearingTrue), 33 | SetDegreesMagnetic: p.Float64(2, "magnetic set degrees"), 34 | SetDegreesMagneticUnit: p.EnumString(3, "magnetic set unit", BearingMagnetic), 35 | DriftKnots: p.Float64(4, "drift knots"), 36 | DriftUnit: p.EnumString(5, "drift unit", SpeedKnots), 37 | }, p.Err() 38 | } 39 | -------------------------------------------------------------------------------- /vdr_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestVDR(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg VDR 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$IIVDR,10.1,T,12.3,M,1.2,N*3A", 18 | msg: VDR{ 19 | SetDegreesTrue: 10.1, 20 | SetDegreesTrueUnit: BearingTrue, 21 | SetDegreesMagnetic: 12.3, 22 | SetDegreesMagneticUnit: BearingMagnetic, 23 | DriftKnots: 1.2, 24 | DriftUnit: SpeedKnots, 25 | }, 26 | }, 27 | { 28 | name: "invalid nmea: SetDegreesTrueUnit", 29 | raw: "$IIVDR,10.1,x,12.3,M,1.2,N*16", 30 | err: "nmea: IIVDR invalid true set unit: x", 31 | }, 32 | { 33 | name: "invalid nmea: SetDegreesMagneticUnit", 34 | raw: "$IIVDR,10.1,T,12.3,x,1.2,N*0f", 35 | err: "nmea: IIVDR invalid magnetic set unit: x", 36 | }, 37 | { 38 | name: "invalid nmea: DriftUnit", 39 | raw: "$IIVDR,10.1,T,12.3,M,1.2,x*0c", 40 | err: "nmea: IIVDR invalid drift unit: x", 41 | }, 42 | } 43 | for _, tt := range tests { 44 | t.Run(tt.name, func(t *testing.T) { 45 | m, err := Parse(tt.raw) 46 | if tt.err != "" { 47 | assert.Error(t, err) 48 | assert.EqualError(t, err, tt.err) 49 | } else { 50 | assert.NoError(t, err) 51 | vdr := m.(VDR) 52 | vdr.BaseSentence = BaseSentence{} 53 | assert.Equal(t, tt.msg, vdr) 54 | } 55 | }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /vhw.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeVHW type for VHW sentences 5 | TypeVHW = "VHW" 6 | ) 7 | 8 | // VHW contains information about water speed and heading 9 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_vhw_water_speed_and_heading 10 | // 11 | // Format: $--VHW,x.x,T,x.x,M,x.x,N,x.x,K*hh 12 | // Example: $VWVHW,45.0,T,43.0,M,3.5,N,6.4,K*56 13 | type VHW struct { 14 | BaseSentence 15 | TrueHeading float64 16 | MagneticHeading float64 17 | SpeedThroughWaterKnots float64 18 | SpeedThroughWaterKPH float64 19 | } 20 | 21 | // newVHW constructor 22 | func newVHW(s BaseSentence) (Sentence, error) { 23 | p := NewParser(s) 24 | p.AssertType(TypeVHW) 25 | return VHW{ 26 | BaseSentence: s, 27 | TrueHeading: p.Float64(0, "true heading"), 28 | MagneticHeading: p.Float64(2, "magnetic heading"), 29 | SpeedThroughWaterKnots: p.Float64(4, "speed through water in knots"), 30 | SpeedThroughWaterKPH: p.Float64(6, "speed through water in kilometers per hour"), 31 | }, p.Err() 32 | } 33 | -------------------------------------------------------------------------------- /vhw_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | var vhw = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg VHW 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$VWVHW,45.0,T,43.0,M,3.5,N,6.4,K*56", 18 | msg: VHW{ 19 | TrueHeading: 45.0, 20 | MagneticHeading: 43.0, 21 | SpeedThroughWaterKnots: 3.5, 22 | SpeedThroughWaterKPH: 6.4, 23 | }, 24 | }, 25 | { 26 | name: "bad sentence", 27 | raw: "$VWVHW,T,45.0,43.0,M,3.5,N,6.4,K*56", 28 | err: "nmea: VWVHW invalid true heading: T", 29 | }, 30 | } 31 | 32 | func TestVHW(t *testing.T) { 33 | for _, tt := range vhw { 34 | t.Run(tt.name, func(t *testing.T) { 35 | m, err := Parse(tt.raw) 36 | if tt.err != "" { 37 | assert.Error(t, err) 38 | assert.EqualError(t, err, tt.err) 39 | } else { 40 | assert.NoError(t, err) 41 | vhw := m.(VHW) 42 | vhw.BaseSentence = BaseSentence{} 43 | assert.Equal(t, tt.msg, vhw) 44 | } 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /vlw.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeVLW type of VLW sentence for Distance Traveled through Water 5 | TypeVLW = "VLW" 6 | ) 7 | 8 | // VLW - Distance Traveled through Water 9 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_vlw_distance_traveled_through_water 10 | // 11 | // Format: $--VLW,x.x,N,x.x,N*hh 12 | // Format (NMEA 3+): $--VLW,x.x,N,x.x,N,x.x,N,x.x,N*hh 13 | // Example: $IIVLW,10.1,N,3.2,N*7C 14 | // Example: $IIVLW,10.1,N,3.2,N,0,N,0,N*7C 15 | type VLW struct { 16 | BaseSentence 17 | TotalInWater float64 // Total cumulative water distance, nm 18 | TotalInWaterUnit string // N = Nautical Miles 19 | SinceResetInWater float64 // Water distance since Reset, nm 20 | SinceResetInWaterUnit string // N = Nautical Miles 21 | TotalOnGround float64 // Total cumulative ground distance, nm (NMEA 3 and above) 22 | TotalOnGroundUnit string // N = Nautical Miles (NMEA 3 and above) 23 | SinceResetOnGround float64 // Ground distance since reset, nm (NMEA 3 and above) 24 | SinceResetOnGroundUnit string // N = Nautical Miles (NMEA 3 and above) 25 | } 26 | 27 | // newVLW constructor 28 | func newVLW(s BaseSentence) (Sentence, error) { 29 | p := NewParser(s) 30 | p.AssertType(TypeVLW) 31 | 32 | vlw := VLW{ 33 | BaseSentence: s, 34 | TotalInWater: p.Float64(0, "total cumulative water distance"), 35 | TotalInWaterUnit: p.EnumString(1, "total cumulative water distance unit", DistanceUnitNauticalMile), 36 | SinceResetInWater: p.Float64(2, "water distance since reset"), 37 | SinceResetInWaterUnit: p.EnumString(3, "water distance since reset unit", DistanceUnitNauticalMile), 38 | } 39 | if len(p.Fields) > 4 { 40 | vlw.TotalOnGround = p.Float64(4, "total cumulative ground distance") 41 | vlw.TotalOnGroundUnit = p.EnumString(5, "total cumulative ground distance unit", DistanceUnitNauticalMile) 42 | vlw.SinceResetOnGround = p.Float64(6, "ground distance since reset") 43 | vlw.SinceResetOnGroundUnit = p.EnumString(7, "ground distance since reset unit", DistanceUnitNauticalMile) 44 | } 45 | return vlw, p.Err() 46 | } 47 | -------------------------------------------------------------------------------- /vlw_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestVLW(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg VLW 14 | }{ 15 | { 16 | name: "good sentence 1", 17 | raw: "$IIVLW,10.1,N,3.2,N*7C", 18 | msg: VLW{ 19 | TotalInWater: 10.1, 20 | TotalInWaterUnit: "N", 21 | SinceResetInWater: 3.2, 22 | SinceResetInWaterUnit: "N", 23 | TotalOnGround: 0, 24 | TotalOnGroundUnit: "", 25 | SinceResetOnGround: 0, 26 | SinceResetOnGroundUnit: "", 27 | }, 28 | }, 29 | { 30 | name: "good sentence 2", 31 | raw: "$IIVLW,10.1,N,3.2,N,1,N,0.1,N*62", 32 | msg: VLW{ 33 | TotalInWater: 10.1, 34 | TotalInWaterUnit: "N", 35 | SinceResetInWater: 3.2, 36 | SinceResetInWaterUnit: "N", 37 | TotalOnGround: 1, 38 | TotalOnGroundUnit: "N", 39 | SinceResetOnGround: 0.1, 40 | SinceResetOnGroundUnit: "N", 41 | }, 42 | }, 43 | { 44 | name: "invalid nmea: TotalInWaterUnit", 45 | raw: "$IIVLW,10.1,x,3.2,N,1,N,0.1,N*54", 46 | err: "nmea: IIVLW invalid total cumulative water distance unit: x", 47 | }, 48 | { 49 | name: "invalid nmea: SinceResetInWaterUnit", 50 | raw: "$IIVLW,10.1,N,3.2,x,1,N,0.1,N*54", 51 | err: "nmea: IIVLW invalid water distance since reset unit: x", 52 | }, 53 | { 54 | name: "invalid nmea: TotalOnGroundUnit", 55 | raw: "$IIVLW,10.1,N,3.2,N,1,x,0.1,N*54", 56 | err: "nmea: IIVLW invalid total cumulative ground distance unit: x", 57 | }, 58 | { 59 | name: "invalid nmea: SinceResetOnGroundUnit", 60 | raw: "$IIVLW,10.1,N,3.2,N,1,N,0.1,x*54", 61 | err: "nmea: IIVLW invalid ground distance since reset unit: x", 62 | }, 63 | } 64 | for _, tt := range tests { 65 | t.Run(tt.name, func(t *testing.T) { 66 | m, err := Parse(tt.raw) 67 | if tt.err != "" { 68 | assert.Error(t, err) 69 | assert.EqualError(t, err, tt.err) 70 | } else { 71 | assert.NoError(t, err) 72 | vlw := m.(VLW) 73 | vlw.BaseSentence = BaseSentence{} 74 | assert.Equal(t, tt.msg, vlw) 75 | } 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /vpw.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeVPW type of VPW sentence for Speed Measured Parallel to Wind 5 | TypeVPW = "VPW" 6 | ) 7 | 8 | // VPW - Speed Measured Parallel to Wind 9 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_vpw_speed_measured_parallel_to_wind 10 | // 11 | // Format: $--VPW,x.x,N,x.x,M*hh 12 | // Example: $IIVPW,4.5,N,6.7,M*52 13 | type VPW struct { 14 | BaseSentence 15 | SpeedKnots float64 // Speed, "-" means downwind, knots 16 | SpeedKnotsUnit string // N = knots 17 | SpeedMPS float64 // Speed, "-" means downwind, m/s 18 | SpeedMPSUnit string // M = m/s 19 | } 20 | 21 | // newVPW constructor 22 | func newVPW(s BaseSentence) (Sentence, error) { 23 | p := NewParser(s) 24 | p.AssertType(TypeVPW) 25 | return VPW{ 26 | BaseSentence: s, 27 | SpeedKnots: p.Float64(0, "wind speed in knots"), 28 | SpeedKnotsUnit: p.EnumString(1, "wind speed in knots unit", SpeedKnots), 29 | SpeedMPS: p.Float64(2, "wind speed in meters per second"), 30 | SpeedMPSUnit: p.EnumString(3, "wind speed in meters per second unit", SpeedMeterPerSecond), 31 | }, p.Err() 32 | } 33 | -------------------------------------------------------------------------------- /vpw_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestVPW(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg VPW 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$IIVPW,4.5,N,6.7,M*52", 18 | msg: VPW{ 19 | SpeedKnots: 4.5, 20 | SpeedKnotsUnit: SpeedKnots, 21 | SpeedMPS: 6.7, 22 | SpeedMPSUnit: SpeedMeterPerSecond, 23 | }, 24 | }, 25 | { 26 | name: "invalid nmea: SpeedKnotsUnit", 27 | raw: "$IIVPW,4.5,x,6.7,M*64", 28 | err: "nmea: IIVPW invalid wind speed in knots unit: x", 29 | }, 30 | { 31 | name: "invalid nmea: SpeedMPSUnit", 32 | raw: "$IIVPW,4.5,N,6.7,x*67", 33 | err: "nmea: IIVPW invalid wind speed in meters per second unit: x", 34 | }, 35 | } 36 | for _, tt := range tests { 37 | t.Run(tt.name, func(t *testing.T) { 38 | m, err := Parse(tt.raw) 39 | if tt.err != "" { 40 | assert.Error(t, err) 41 | assert.EqualError(t, err, tt.err) 42 | } else { 43 | assert.NoError(t, err) 44 | vpw := m.(VPW) 45 | vpw.BaseSentence = BaseSentence{} 46 | assert.Equal(t, tt.msg, vpw) 47 | } 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /vtg.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeVTG type for VTG sentences 5 | TypeVTG = "VTG" 6 | ) 7 | 8 | // VTG represents track & speed data. 9 | // http://aprs.gids.nl/nmea/#vtg 10 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_vtg_track_made_good_and_ground_speed 11 | // 12 | // Format: $--VTG,x.x,T,x.x,M,x.x,N,x.x,K*hh 13 | // Format (NMEA 2.3+): $--VTG,x.x,T,x.x,M,x.x,N,x.x,K,m*hh 14 | // Example: $GPVTG,45.5,T,67.5,M,30.45,N,56.40,K*4B 15 | // $GPVTG,220.86,T,,M,2.550,N,4.724,K,A*34 16 | type VTG struct { 17 | BaseSentence 18 | TrueTrack float64 19 | MagneticTrack float64 20 | GroundSpeedKnots float64 21 | GroundSpeedKPH float64 22 | FFAMode string // FAA mode indicator (filled in NMEA 2.3 and later) 23 | } 24 | 25 | // newVTG parses the VTG sentence into this struct. 26 | // e.g: $GPVTG,360.0,T,348.7,M,000.0,N,000.0,K*43 27 | func newVTG(s BaseSentence) (Sentence, error) { 28 | p := NewParser(s) 29 | p.AssertType(TypeVTG) 30 | vtg := VTG{ 31 | BaseSentence: s, 32 | TrueTrack: p.Float64(0, "true track"), 33 | MagneticTrack: p.Float64(2, "magnetic track"), 34 | GroundSpeedKnots: p.Float64(4, "ground speed (knots)"), 35 | GroundSpeedKPH: p.Float64(6, "ground speed (km/h)"), 36 | } 37 | if len(p.Fields) > 8 { 38 | vtg.FFAMode = p.String(8, "FAA mode") // not enum because some devices have proprietary "non-nmea" values 39 | } 40 | return vtg, p.Err() 41 | } 42 | -------------------------------------------------------------------------------- /vtg_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | var vtgtests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg VTG 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$GPVTG,45.5,T,67.5,M,30.45,N,56.40,K*4B", 18 | msg: VTG{ 19 | TrueTrack: 45.5, 20 | MagneticTrack: 67.5, 21 | GroundSpeedKnots: 30.45, 22 | GroundSpeedKPH: 56.4, 23 | FFAMode: "", 24 | }, 25 | }, 26 | { 27 | name: "good sentence with FAA mode", 28 | raw: "$GPVTG,220.86,T,,M,2.550,N,4.724,K,A*34", 29 | msg: VTG{ 30 | TrueTrack: 220.86, 31 | MagneticTrack: 0, 32 | GroundSpeedKnots: 2.55, 33 | GroundSpeedKPH: 4.724, 34 | FFAMode: "A", 35 | }, 36 | }, 37 | { 38 | name: "bad true track", 39 | raw: "$GPVTG,T,45.5,67.5,M,30.45,N,56.40,K*4B", 40 | err: "nmea: GPVTG invalid true track: T", 41 | }, 42 | } 43 | 44 | func TestVTG(t *testing.T) { 45 | for _, tt := range vtgtests { 46 | t.Run(tt.name, func(t *testing.T) { 47 | m, err := Parse(tt.raw) 48 | if tt.err != "" { 49 | assert.Error(t, err) 50 | assert.EqualError(t, err, tt.err) 51 | } else { 52 | assert.NoError(t, err) 53 | vtg := m.(VTG) 54 | vtg.BaseSentence = BaseSentence{} 55 | assert.Equal(t, tt.msg, vtg) 56 | } 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /vwr.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeVWR type of VWR sentence for Relative Wind Speed and Angle 5 | TypeVWR = "VWR" 6 | ) 7 | 8 | // VWR - Relative Wind Speed and Angle. Speed is measured relative to the moving vessel. 9 | // According to NMEA: use of $--MWV is recommended. 10 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_vwr_relative_wind_speed_and_angle 11 | // https://www.nmea.org/Assets/100108_nmea_0183_sentences_not_recommended_for_new_designs.pdf (page 16) 12 | // 13 | // Format: $--VWR,x.x,a,x.x,N,x.x,M,x.x,K*hh 14 | // Example: $IIVWR,75,R,1.0,N,0.51,M,1.85,K*6C 15 | // $IIVWR,024,L,018,N,,,,*5e 16 | // $IIVWR,,,,,,,,*53 17 | type VWR struct { 18 | BaseSentence 19 | MeasuredAngle float64 // Measured Wind direction magnitude in degrees (0 to 180 deg) 20 | MeasuredDirectionBow string // Measured Wind direction Left/Right of bow 21 | SpeedKnots float64 // Measured wind Speed, knots 22 | SpeedKnotsUnit string // N = knots 23 | SpeedMPS float64 // Wind speed, meters/second 24 | SpeedMPSUnit string // M = m/s 25 | SpeedKPH float64 // Wind speed, km/hour 26 | SpeedKPHUnit string // M = km/h 27 | } 28 | 29 | // newVWR constructor 30 | func newVWR(s BaseSentence) (Sentence, error) { 31 | p := NewParser(s) 32 | p.AssertType(TypeVWR) 33 | return VWR{ 34 | BaseSentence: s, 35 | MeasuredAngle: p.Float64(0, "measured wind angle"), 36 | MeasuredDirectionBow: p.EnumString(1, "measured wind direction to bow", Left, Right), 37 | SpeedKnots: p.Float64(2, "wind speed in knots"), 38 | SpeedKnotsUnit: p.EnumString(3, "wind speed in knots unit", SpeedKnots), 39 | SpeedMPS: p.Float64(4, "wind speed in meters per second"), 40 | SpeedMPSUnit: p.EnumString(5, "wind speed in meters per second unit", SpeedMeterPerSecond), 41 | SpeedKPH: p.Float64(6, "wind speed in kilometers per hour"), 42 | SpeedKPHUnit: p.EnumString(7, "wind speed in kilometers per hour unit", SpeedKilometerPerHour), 43 | }, p.Err() 44 | } 45 | -------------------------------------------------------------------------------- /vwt.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeVWT type of VWT sentence for True Wind Speed and Angle 5 | TypeVWT = "VWT" 6 | ) 7 | 8 | // VWT - True Wind Speed and Angle 9 | // https://www.nmea.org/Assets/100108_nmea_0183_sentences_not_recommended_for_new_designs.pdf 10 | // https://www.rubydoc.info/gems/nmea_plus/1.0.20/NMEAPlus/Message/NMEA/VWT 11 | // https://lists.gnu.org/archive/html/gpsd-dev/2012-04/msg00048.html 12 | // 13 | // Format: $--VWT,x.x,a,x.x,N,x.x,M,x.x,K*hh 14 | // Example: $IIVWT,75,x,1.0,N,0.51,M,1.85,K*40 15 | type VWT struct { 16 | BaseSentence 17 | TrueAngle float64 // true Wind direction magnitude in degrees (0 to 180 deg) 18 | TrueDirectionBow string // true Wind direction Left/Right of bow 19 | SpeedKnots float64 // true wind Speed, knots 20 | SpeedKnotsUnit string // N = knots 21 | SpeedMPS float64 // Wind speed, meters/second 22 | SpeedMPSUnit string // M = m/s 23 | SpeedKPH float64 // Wind speed, km/hour 24 | SpeedKPHUnit string // M = km/h 25 | } 26 | 27 | // newVWT constructor 28 | func newVWT(s BaseSentence) (Sentence, error) { 29 | p := NewParser(s) 30 | p.AssertType(TypeVWT) 31 | return VWT{ 32 | BaseSentence: s, 33 | TrueAngle: p.Float64(0, "true wind angle"), 34 | TrueDirectionBow: p.EnumString(1, "true wind direction to bow", Left, Right), 35 | SpeedKnots: p.Float64(2, "wind speed in knots"), 36 | SpeedKnotsUnit: p.EnumString(3, "wind speed in knots unit", SpeedKnots), 37 | SpeedMPS: p.Float64(4, "wind speed in meters per second"), 38 | SpeedMPSUnit: p.EnumString(5, "wind speed in meters per second unit", SpeedMeterPerSecond), 39 | SpeedKPH: p.Float64(6, "wind speed in kilometers per hour"), 40 | SpeedKPHUnit: p.EnumString(7, "wind speed in kilometers per hour unit", SpeedKilometerPerHour), 41 | }, p.Err() 42 | } 43 | -------------------------------------------------------------------------------- /vwt_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestVWT(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg VWT 14 | }{ 15 | { // these examples are from SignalK 16 | name: "good sentence", 17 | raw: "$IIVWT,75,R,1.0,N,0.51,M,1.85,K*6A", 18 | msg: VWT{ 19 | TrueAngle: 75, 20 | TrueDirectionBow: Right, 21 | SpeedKnots: 1, 22 | SpeedKnotsUnit: SpeedKnots, 23 | SpeedMPS: 0.51, 24 | SpeedMPSUnit: SpeedMeterPerSecond, 25 | SpeedKPH: 1.85, 26 | SpeedKPHUnit: SpeedKilometerPerHour, 27 | }, 28 | }, 29 | { 30 | name: "good sentence, shorter but still valid", 31 | raw: "$IIVWT,024,L,018,N,,,,*58", 32 | msg: VWT{ 33 | TrueAngle: 24, 34 | TrueDirectionBow: Left, 35 | SpeedKnots: 18, 36 | SpeedKnotsUnit: SpeedKnots, 37 | SpeedMPS: 0, 38 | SpeedMPSUnit: "", 39 | SpeedKPH: 0, 40 | SpeedKPHUnit: "", 41 | }, 42 | }, 43 | { 44 | name: "good sentence, handle empty values", 45 | raw: "$IIVWT,,,,,,,,*55", 46 | msg: VWT{ 47 | TrueAngle: 0, 48 | TrueDirectionBow: "", 49 | SpeedKnots: 0, 50 | SpeedKnotsUnit: "", 51 | SpeedMPS: 0, 52 | SpeedMPSUnit: "", 53 | SpeedKPH: 0, 54 | SpeedKPHUnit: "", 55 | }, 56 | }, 57 | { 58 | name: "invalid nmea: DirectionBow", 59 | raw: "$IIVWT,75,x,1.0,N,0.51,M,1.85,K*40", 60 | err: "nmea: IIVWT invalid true wind direction to bow: x", 61 | }, 62 | { 63 | name: "invalid nmea: SpeedKnotsUnit", 64 | raw: "$IIVWT,75,R,1.0,x,0.51,M,1.85,K*5c", 65 | err: "nmea: IIVWT invalid wind speed in knots unit: x", 66 | }, 67 | { 68 | name: "invalid nmea: SpeedMPSUnit", 69 | raw: "$IIVWT,75,R,1.0,N,0.51,x,1.85,K*5f", 70 | err: "nmea: IIVWT invalid wind speed in meters per second unit: x", 71 | }, 72 | { 73 | name: "invalid nmea: SpeedKPHUnit", 74 | raw: "$IIVWT,75,R,1.0,N,0.51,M,1.85,x*59", 75 | err: "nmea: IIVWT invalid wind speed in kilometers per hour unit: x", 76 | }, 77 | } 78 | for _, tt := range tests { 79 | t.Run(tt.name, func(t *testing.T) { 80 | m, err := Parse(tt.raw) 81 | if tt.err != "" { 82 | assert.Error(t, err) 83 | assert.EqualError(t, err, tt.err) 84 | } else { 85 | assert.NoError(t, err) 86 | vwt := m.(VWT) 87 | vwt.BaseSentence = BaseSentence{} 88 | assert.Equal(t, tt.msg, vwt) 89 | } 90 | }) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /wpl.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeWPL type for WPL sentences 5 | TypeWPL = "WPL" 6 | ) 7 | 8 | // WPL contains information about a waypoint location 9 | // http://aprs.gids.nl/nmea/#wpl 10 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_wpl_waypoint_location 11 | // 12 | // Format: $--WPL,llll.ll,a,yyyyy.yy,a,c--c*hh 13 | // Example: $IIWPL,5503.4530,N,01037.2742,E,411*6F 14 | type WPL struct { 15 | BaseSentence 16 | Latitude float64 // Latitude 17 | Longitude float64 // Longitude 18 | Ident string // Ident of nth waypoint 19 | } 20 | 21 | // newWPL constructor 22 | func newWPL(s BaseSentence) (Sentence, error) { 23 | p := NewParser(s) 24 | p.AssertType(TypeWPL) 25 | return WPL{ 26 | BaseSentence: s, 27 | Latitude: p.LatLong(0, 1, "latitude"), 28 | Longitude: p.LatLong(2, 3, "longitude"), 29 | Ident: p.String(4, "ident of nth waypoint"), 30 | }, p.Err() 31 | } 32 | -------------------------------------------------------------------------------- /wpl_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | var wpltests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg WPL 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$IIWPL,5503.4530,N,01037.2742,E,411*6F", 18 | msg: WPL{ 19 | Latitude: MustParseLatLong("5503.4530 N"), 20 | Longitude: MustParseLatLong("01037.2742 E"), 21 | Ident: "411", 22 | }, 23 | }, 24 | { 25 | name: "bad latitude", 26 | raw: "$IIWPL,A,N,01037.2742,E,411*01", 27 | err: "nmea: IIWPL invalid latitude: cannot parse [A N], unknown format", 28 | }, 29 | { 30 | name: "bad longitude", 31 | raw: "$IIWPL,5503.4530,N,A,E,411*36", 32 | err: "nmea: IIWPL invalid longitude: cannot parse [A E], unknown format", 33 | }, 34 | { 35 | name: "good sentence", 36 | raw: "$IIWPL,3356.4650,S,15124.5567,E,411*73", 37 | msg: WPL{ 38 | Latitude: MustParseLatLong("3356.4650 S"), 39 | Longitude: MustParseLatLong("15124.5567 E"), 40 | Ident: "411", 41 | }, 42 | }, 43 | { 44 | name: "bad latitude", 45 | raw: "$IIWPL,A,S,15124.5567,E,411*18", 46 | err: "nmea: IIWPL invalid latitude: cannot parse [A S], unknown format", 47 | }, 48 | { 49 | name: "bad longitude", 50 | raw: "$IIWPL,3356.4650,S,A,E,411*2E", 51 | err: "nmea: IIWPL invalid longitude: cannot parse [A E], unknown format", 52 | }, 53 | } 54 | 55 | func TestWPL(t *testing.T) { 56 | for _, tt := range wpltests { 57 | t.Run(tt.name, func(t *testing.T) { 58 | m, err := Parse(tt.raw) 59 | if tt.err != "" { 60 | assert.Error(t, err) 61 | assert.EqualError(t, err, tt.err) 62 | } else { 63 | assert.NoError(t, err) 64 | wpl := m.(WPL) 65 | wpl.BaseSentence = BaseSentence{} 66 | assert.Equal(t, tt.msg, wpl) 67 | } 68 | }) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /xte.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeXTE type of XTE sentence for Cross-track error, measured 5 | TypeXTE = "XTE" 6 | ) 7 | 8 | // XTE - Cross-track error, measured 9 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_xte_cross_track_error_measured 10 | // 11 | // Format: $--XTE,A,A,x.x,a,N*hh 12 | // Format (NMEA 2.3): $--XTE,A,A,x.x,a,N,m*hh 13 | // Example: $GPXTE,V,V,,,N,S*43 14 | type XTE struct { 15 | BaseSentence 16 | 17 | // StatusGeneralWarning is used for warnings 18 | // * V = LORAN-C Blink or SNR warning 19 | // * A = general warning flag or other navigation systems when a reliable fix is not available 20 | StatusGeneralWarning string 21 | 22 | // StatusLockWarning is used for lock warning 23 | // * V = Loran-C Cycle Lock warning flag 24 | // * A = OK or not used 25 | StatusLockWarning string 26 | 27 | // CrossTrackErrorMagnitude is Cross Track Error Magnitude 28 | CrossTrackErrorMagnitude float64 29 | 30 | // DirectionToSteer is Direction to steer, 31 | // * L = left 32 | // * R = right 33 | DirectionToSteer string 34 | 35 | // CrossTrackUnits is cross track units 36 | // * N = nautical miles 37 | // * K = for kilometers 38 | CrossTrackUnits string 39 | 40 | // FAA mode indicator (filled in NMEA 2.3 and later) 41 | FFAMode string 42 | } 43 | 44 | // newXTE constructor 45 | func newXTE(s BaseSentence) (Sentence, error) { 46 | p := NewParser(s) 47 | p.AssertType(TypeXTE) 48 | xte := XTE{ 49 | BaseSentence: s, 50 | StatusGeneralWarning: p.EnumString(0, "general warning", StatusWarningAClearORNotUsedAPB, StatusWarningASetAPB), 51 | StatusLockWarning: p.EnumString(1, "lock warning", StatusWarningBSetAPB, StatusWarningBClearAPB), 52 | CrossTrackErrorMagnitude: p.Float64(2, "cross track error magnitude"), 53 | DirectionToSteer: p.EnumString(3, "direction to steer", Left, Right), 54 | CrossTrackUnits: p.EnumString(4, "cross track units", DistanceUnitKilometre, DistanceUnitNauticalMile, DistanceUnitStatuteMile, DistanceUnitMetre), 55 | } 56 | if len(p.Fields) > 5 { 57 | xte.FFAMode = p.String(5, "FAA mode") // not enum because some devices have proprietary "non-nmea" values 58 | } 59 | return xte, p.Err() 60 | } 61 | -------------------------------------------------------------------------------- /xte_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestXTE(t *testing.T) { 9 | var tests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg XTE 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$GPXTE,V,V,10.1,L,N*6E", 18 | msg: XTE{ 19 | StatusGeneralWarning: "V", 20 | StatusLockWarning: "V", 21 | CrossTrackErrorMagnitude: 10.1, 22 | DirectionToSteer: "L", 23 | CrossTrackUnits: "N", 24 | FFAMode: "", 25 | }, 26 | }, 27 | { 28 | name: "good sentence with FAAMode", 29 | raw: "$GPXTE,V,V,,,N,S*43", 30 | msg: XTE{ 31 | StatusGeneralWarning: "V", 32 | StatusLockWarning: "V", 33 | CrossTrackErrorMagnitude: 0, 34 | DirectionToSteer: "", 35 | CrossTrackUnits: "N", 36 | FFAMode: "S", 37 | }, 38 | }, 39 | { 40 | name: "invalid nmea: StatusGeneralWarning", 41 | raw: "$GPXTE,x,V,,,N,S*6d", 42 | err: "nmea: GPXTE invalid general warning: x", 43 | }, 44 | { 45 | name: "invalid nmea: StatusLockWarning", 46 | raw: "$GPXTE,V,x,,,N,S*6d", 47 | err: "nmea: GPXTE invalid lock warning: x", 48 | }, 49 | { 50 | name: "invalid nmea: DirectionToSteer", 51 | raw: "$GPXTE,V,V,,x,N,S*3b", 52 | err: "nmea: GPXTE invalid direction to steer: x", 53 | }, 54 | { 55 | name: "invalid nmea: CrossTrackUnits", 56 | raw: "$GPXTE,V,V,,,x,S*75", 57 | err: "nmea: GPXTE invalid cross track units: x", 58 | }, 59 | } 60 | for _, tt := range tests { 61 | t.Run(tt.name, func(t *testing.T) { 62 | m, err := Parse(tt.raw) 63 | if tt.err != "" { 64 | assert.Error(t, err) 65 | assert.EqualError(t, err, tt.err) 66 | } else { 67 | assert.NoError(t, err) 68 | xte := m.(XTE) 69 | xte.BaseSentence = BaseSentence{} 70 | assert.Equal(t, tt.msg, xte) 71 | } 72 | }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /zda.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | const ( 4 | // TypeZDA type for ZDA sentences 5 | TypeZDA = "ZDA" 6 | ) 7 | 8 | // ZDA represents date & time data. 9 | // http://aprs.gids.nl/nmea/#zda 10 | // https://gpsd.gitlab.io/gpsd/NMEA.html#_zda_time_date_utc_day_month_year_and_local_time_zone 11 | // 12 | // Format: $--ZDA,hhmmss.ss,xx,xx,xxxx,xx,xx*hh 13 | // Example: $GPZDA,172809.456,12,07,1996,00,00*57 14 | type ZDA struct { 15 | BaseSentence 16 | Time Time 17 | Day int64 18 | Month int64 19 | Year int64 20 | OffsetHours int64 // Local time zone offset from GMT, hours 21 | OffsetMinutes int64 // Local time zone offset from GMT, minutes 22 | } 23 | 24 | // newZDA constructor 25 | func newZDA(s BaseSentence) (Sentence, error) { 26 | p := NewParser(s) 27 | p.AssertType(TypeZDA) 28 | return ZDA{ 29 | BaseSentence: s, 30 | Time: p.Time(0, "time"), 31 | Day: p.Int64(1, "day"), 32 | Month: p.Int64(2, "month"), 33 | Year: p.Int64(3, "year"), 34 | OffsetHours: p.Int64(4, "offset (hours)"), 35 | OffsetMinutes: p.Int64(5, "offset (minutes)"), 36 | }, p.Err() 37 | } 38 | -------------------------------------------------------------------------------- /zda_test.go: -------------------------------------------------------------------------------- 1 | package nmea 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | var zdatests = []struct { 10 | name string 11 | raw string 12 | err string 13 | msg ZDA 14 | }{ 15 | { 16 | name: "good sentence", 17 | raw: "$GPZDA,172809.456,12,07,1996,00,00*57", 18 | msg: ZDA{ 19 | Time: Time{ 20 | Valid: true, 21 | Hour: 17, 22 | Minute: 28, 23 | Second: 9, 24 | Millisecond: 456, 25 | }, 26 | Day: 12, 27 | Month: 7, 28 | Year: 1996, 29 | OffsetHours: 0, 30 | OffsetMinutes: 0, 31 | }, 32 | }, 33 | { 34 | name: "invalid day", 35 | raw: "$GPZDA,220516,D,5133.82,N,00042.24,W,173.8,231.8,130694,004.2,W*76", 36 | err: "nmea: GPZDA invalid day: D", 37 | }, 38 | } 39 | 40 | func TestZDA(t *testing.T) { 41 | for _, tt := range zdatests { 42 | t.Run(tt.name, func(t *testing.T) { 43 | m, err := Parse(tt.raw) 44 | if tt.err != "" { 45 | assert.Error(t, err) 46 | assert.EqualError(t, err, tt.err) 47 | } else { 48 | assert.NoError(t, err) 49 | zda := m.(ZDA) 50 | zda.BaseSentence = BaseSentence{} 51 | assert.Equal(t, tt.msg, zda) 52 | } 53 | }) 54 | } 55 | } 56 | --------------------------------------------------------------------------------