From 852d88f0f468f9a60524af214435bf72c703be87 Mon Sep 17 00:00:00 2001 From: Martti T Date: Tue, 13 Jun 2023 08:19:52 +0300 Subject: [PATCH] sentences for NMEA2000 over NMEA0183 (#106) * sentences for NMEA2000 over NMEA0183 --- README.md | 28 ++++++++++++--------- pcdin.go | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++ pcdin_test.go | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++ pgn.go | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++ pgn_test.go | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++ sentence.go | 4 +++ 6 files changed, 286 insertions(+), 11 deletions(-) create mode 100644 pcdin.go create mode 100644 pcdin_test.go create mode 100644 pgn.go create mode 100644 pgn_test.go diff --git a/README.md b/README.md index d35866e..a922ede 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,8 @@ To update go-nmea to the latest version, use `go get -u github.com/adrianmo/go-n ## Supported sentences -Sentence with link is supported by this library. NMEA0183 sentences list is based on [IEC 61162-1:2016 (Edition 5.0 2016-08)](https://webstore.iec.ch/publication/25754) table of contents. +Sentence with link is supported by this library. NMEA0183 sentences list is based +on [IEC 61162-1:2016 (Edition 5.0 2016-08)](https://webstore.iec.ch/publication/25754) table of contents. | Sentence | Description | References | |--------------------|---------------------------------------------------------------------|------------------------------------------------------------------------------------------------| @@ -164,15 +165,16 @@ Sentence with link is supported by this library. NMEA0183 sentences list is base | ZFO | UTC and time from origin waypoint | | | ZTG | UTC and time to destination waypoint | | - -| Proprietary sentence type | Description | References | -|---------------------------|-------------------------------------------------------------------------------------------------|----------------------------------------------------------| -| [PGRME](./pgrme.go) | Estimated Position Error (Garmin proprietary sentence) | [1](http://aprs.gids.nl/nmea/#rme) | -| [PHTRO](./phtro.go) | Vessel pitch and roll (Xsens IMU/VRU/AHRS) | | -| [PMTK001](./pmtk.go) | Acknowledgement of previously sent command/packet | [1](https://www.rhydolabz.com/documents/25/PMTK_A11.pdf) | -| [PRDID](./prdid.go) | Vessel pitch, roll and heading (Xsens IMU/VRU/AHRS) | | -| [PSKPDPT](./pskpdpt.go) | Depth of Water for multiple transducer installation | | -| [PSONCMS](./psoncms.go) | Quaternion, acceleration, rate of turn, magnetic field, sensor temperature (Xsens IMU/VRU/AHRS) | | +| Proprietary sentence type | Description | References | +|---------------------------|-------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------| +| [PNG](./pgn.go) | Transfer NMEA2000 frame as NMEA0183 sentence (ShipModul MiniPlex-3) | [1](https://opencpn.org/wiki/dokuwiki/lib/exe/fetch.php?media=opencpn:software:mxpgn_sentence.pdf) | +| [PCDIN](./pcdin.go) | Transfer NMEA2000 frame as NMEA0183 sentence (SeaSmart.Net Protocol) | [1](http://www.seasmart.net/pdf/SeaSmart_HTTP_Protocol_RevG_043012.pdf) | +| [PGRME](./pgrme.go) | Estimated Position Error (Garmin proprietary sentence) | [1](http://aprs.gids.nl/nmea/#rme) | +| [PHTRO](./phtro.go) | Vessel pitch and roll (Xsens IMU/VRU/AHRS) | | +| [PMTK001](./pmtk.go) | Acknowledgement of previously sent command/packet | [1](https://www.rhydolabz.com/documents/25/PMTK_A11.pdf) | +| [PRDID](./prdid.go) | Vessel pitch, roll and heading (Xsens IMU/VRU/AHRS) | | +| [PSKPDPT](./pskpdpt.go) | Depth of Water for multiple transducer installation | | +| [PSONCMS](./psoncms.go) | Quaternion, acceleration, rate of turn, magnetic field, sensor temperature (Xsens IMU/VRU/AHRS) | | If you need to parse a message that contains an unsupported sentence type you can implement and register your own message parser and get yourself unblocked immediately. Check the example below to know how @@ -358,7 +360,11 @@ Value: 5133.820000 ### Message parsing with optional values -Some messages have optional fields. By default, omitted numeric values are set to 0. In situations where you need finer control to distinguish between an undefined value and an actual 0, you can register types overriding existing sentences, using `nmea.Int64` and `nmea.Float64` instead of `int64` and `float64`. The matching parsing methods are `(*Parser).NullInt64` and `(*Parser).NullFloat64`. Both `nmea.Int64` and `nmea.Float64` contains a numeric field `Value` which is defined only if the field `Valid` is `true`. +Some messages have optional fields. By default, omitted numeric values are set to 0. In situations where you need finer +control to distinguish between an undefined value and an actual 0, you can register types overriding existing sentences, +using `nmea.Int64` and `nmea.Float64` instead of `int64` and `float64`. The matching parsing methods +are `(*Parser).NullInt64` and `(*Parser).NullFloat64`. Both `nmea.Int64` and `nmea.Float64` contains a numeric +field `Value` which is defined only if the field `Valid` is `true`. See below example for a modified VTG sentence parser: diff --git a/pcdin.go b/pcdin.go new file mode 100644 index 0000000..d9b632f --- /dev/null +++ b/pcdin.go @@ -0,0 +1,66 @@ +package nmea + +import ( + "encoding/hex" + "fmt" + "strconv" +) + +const ( + // TypePCDIN is type of PCDIN sentence for SeaSmart.Net Protocol + TypePCDIN = "CDIN" +) + +// PCDIN - SeaSmart.Net Protocol transfers NMEA2000 message as NMEA0183 sentence +// http://www.seasmart.net/pdf/SeaSmart_HTTP_Protocol_RevG_043012.pdf (SeaSmart.Net Protocol Specification Version 1.7) +// +// Note: older SeaSmart.Net Protocol versions have different amount of fields +// +// Format: $PCDIN,hhhhhh,hhhhhhhh,hh,h--h*hh +// Example: $PCDIN,01F112,000C72EA,09,28C36A0000B40AFD*56 +type PCDIN struct { + BaseSentence + PGN uint32 // PGN of NMEA2000 packet + Timestamp uint32 // ticks since something + Source uint8 // 0-255 + Data []byte // can be more than 8 bytes i.e can contain assembled fast packets +} + +// newPCDIN constructor +func newPCDIN(s BaseSentence) (Sentence, error) { + p := NewParser(s) + p.AssertType(TypePCDIN) + + if len(p.Fields) != 4 { + p.SetErr("fields", "invalid number of fields in sentence") + return nil, p.Err() + } + pgn, err := strconv.ParseUint(p.Fields[0], 16, 24) + if err != nil { + p.err = fmt.Errorf("nmea: %s failed to parse PGN field: %w", p.Prefix(), err) + return nil, p.Err() + } + timestamp, err := strconv.ParseUint(p.Fields[1], 16, 32) + if err != nil { + p.err = fmt.Errorf("nmea: %s failed to parse timestamp field: %w", p.Prefix(), err) + return nil, p.Err() + } + source, err := strconv.ParseUint(p.Fields[2], 16, 8) + if err != nil { + p.err = fmt.Errorf("nmea: %s failed to parse source field: %w", p.Prefix(), err) + return nil, p.Err() + } + data, err := hex.DecodeString(p.Fields[3]) + if err != nil { + p.err = fmt.Errorf("nmea: %s failed to decode data: %w", p.Prefix(), err) + return nil, p.Err() + } + + return PCDIN{ + BaseSentence: s, + PGN: uint32(pgn), + Timestamp: uint32(timestamp), + Source: uint8(source), + Data: data, + }, p.Err() +} diff --git a/pcdin_test.go b/pcdin_test.go new file mode 100644 index 0000000..e27e347 --- /dev/null +++ b/pcdin_test.go @@ -0,0 +1,66 @@ +package nmea + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestPCDIN(t *testing.T) { + var tests = []struct { + name string + raw string + err string + msg PCDIN + }{ + { + name: "good sentence", + raw: "$PCDIN,01F112,000C72EA,09,28C36A0000B40AFD*56", + msg: PCDIN{ + PGN: 127250, // 0x1F112 Vessel Heading + Timestamp: 815850, + Source: 9, + Data: []byte{0x28, 0xC3, 0x6A, 0x00, 0x00, 0xB4, 0x0A, 0xFD}, + }, + }, + { + name: "invalid number of fields", + raw: "$PCDIN,01F112,000C72EA,28C36A0000B40AFD*73", + err: "nmea: PCDIN invalid fields: invalid number of fields in sentence", + }, + { + name: "invalid PGN field", + raw: "$PCDIN,x1F112,000C72EA,09,28C36A0000B40AFD*1e", + err: "nmea: PCDIN failed to parse PGN field: strconv.ParseUint: parsing \"x1F112\": invalid syntax", + }, + { + name: "invalid timestamp field", + raw: "$PCDIN,01F112,x00C72EA,09,28C36A0000B40AFD*1e", + err: "nmea: PCDIN failed to parse timestamp field: strconv.ParseUint: parsing \"x00C72EA\": invalid syntax", + }, + { + name: "invalid source field", + raw: "$PCDIN,01F112,000C72EA,x9,28C36A0000B40AFD*1e", + err: "nmea: PCDIN failed to parse source field: strconv.ParseUint: parsing \"x9\": invalid syntax", + }, + { + name: "invalid hex data", + raw: "$PCDIN,01F112,000C72EA,09,x8C36A0000B40AFD*1c", + err: "nmea: PCDIN failed to decode data: encoding/hex: invalid byte: U+0078 'x'", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m, err := Parse(tt.raw) + if tt.err != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.err) + } else { + assert.NoError(t, err) + pgrme := m.(PCDIN) + pgrme.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, pgrme) + } + }) + } +} diff --git a/pgn.go b/pgn.go new file mode 100644 index 0000000..92c41cd --- /dev/null +++ b/pgn.go @@ -0,0 +1,66 @@ +package nmea + +import ( + "encoding/hex" + "fmt" + "strconv" +) + +const ( + // TypePGN is type of PGN sentence for transferring single NMEA2000 frame as NMEA0183 sentence + TypePGN = "PGN" +) + +// PGN - transferring single NMEA2000 frame as NMEA0183 sentence +// https://opencpn.org/wiki/dokuwiki/lib/exe/fetch.php?media=opencpn:software:mxpgn_sentence.pdf +// +// Format: $--PGN,pppppp,aaaa,c--c*hh +// Example: $MXPGN,01F112,2807,FC7FFF7FFF168012*11 +type PGN struct { + BaseSentence + PGN uint32 // PGN of NMEA2000 packet + IsSend bool // is this sentence received or for sending + Priority uint8 // 0-7 + Address uint8 // depending on the IsSend field this is Source Address of received packet or Destination for send packet + Data []byte // 1-8 bytes. This is single N2K frame. N2K Fast-packets should be assembled from individual frames +} + +// newPGN constructor +func newPGN(s BaseSentence) (Sentence, error) { + p := NewParser(s) + p.AssertType(TypePGN) + + if len(p.Fields) != 3 { + p.SetErr("fields", "invalid number of fields in sentence") + return nil, p.Err() + } + pgn, err := strconv.ParseUint(p.Fields[0], 16, 24) + if err != nil { + p.err = fmt.Errorf("nmea: %s failed to parse PGN field: %w", p.Prefix(), err) + return nil, p.Err() + } + attributes, err := strconv.ParseUint(p.Fields[1], 16, 16) + if err != nil { + p.err = fmt.Errorf("nmea: %s failed to parse attributes field: %w", p.Prefix(), err) + return nil, p.Err() + } + dataLength := int((attributes >> 8) & 0b1111) // bits 8-11 + if dataLength*2 != (len(p.Fields[2])) { + p.SetErr("dlc", "data length does not match actual data length") + return nil, p.Err() + } + data, err := hex.DecodeString(p.Fields[2]) + if err != nil { + p.err = fmt.Errorf("nmea: %s failed to decode data: %w", p.Prefix(), err) + return nil, p.Err() + } + + return PGN{ + BaseSentence: s, + PGN: uint32(pgn), + IsSend: attributes>>15 == 1, // bit 15 + Priority: uint8((attributes >> 12) & 0b111), // bits 12,13,14 + Address: uint8(attributes), // bits 0-7 + Data: data, + }, p.Err() +} diff --git a/pgn_test.go b/pgn_test.go new file mode 100644 index 0000000..893b9cd --- /dev/null +++ b/pgn_test.go @@ -0,0 +1,67 @@ +package nmea + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestPGN(t *testing.T) { + var tests = []struct { + name string + raw string + err string + msg PGN + }{ + { + name: "good sentence", + raw: "$MXPGN,01F112,2807,FC7FFF7FFF168012*11", + msg: PGN{ + PGN: 127250, // 0x1F112 Vessel Heading + IsSend: false, + Priority: 2, + Address: 7, + Data: []byte{0xFC, 0x7f, 0xFF, 0x7f, 0xFF, 0x16, 0x80, 0x12}, + }, + }, + { + name: "invalid number of fields", + raw: "$MXPGN,01F112,FC7FFF7FFF168012*30", + err: "nmea: MXPGN invalid fields: invalid number of fields in sentence", + }, + { + name: "invalid PGN field", + raw: "$MXPGN,0xF112,2807,FC7FFF7FFF168012*58", + err: "nmea: MXPGN failed to parse PGN field: strconv.ParseUint: parsing \"0xF112\": invalid syntax", + }, + { + name: "invalid attributes field", + raw: "$MXPGN,01F112,x807,FC7FFF7FFF168012*5b", + err: "nmea: MXPGN failed to parse attributes field: strconv.ParseUint: parsing \"x807\": invalid syntax", + }, + { + name: "invalid data length field", + raw: "$MXPGN,01F112,2207,FC7FFF7FFF168012*1b", + err: "nmea: MXPGN invalid dlc: data length does not match actual data length", + }, + { + name: "invalid hex data", + raw: "$MXPGN,01F112,2807,xC7FFF7FFF168012*2f", + err: "nmea: MXPGN failed to decode data: encoding/hex: invalid byte: U+0078 'x'", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m, err := Parse(tt.raw) + if tt.err != "" { + assert.Error(t, err) + assert.EqualError(t, err, tt.err) + } else { + assert.NoError(t, err) + pgrme := m.(PGN) + pgrme.BaseSentence = BaseSentence{} + assert.Equal(t, tt.msg, pgrme) + } + }) + } +} diff --git a/sentence.go b/sentence.go index 154d72f..2619275 100644 --- a/sentence.go +++ b/sentence.go @@ -339,6 +339,10 @@ func (p *SentenceParser) Parse(raw string) (Sentence, error) { return newVTG(s) case TypeZDA: return newZDA(s) + case TypePGN: + return newPGN(s) + case TypePCDIN: + return newPCDIN(s) case TypePGRME: return newPGRME(s) case TypePHTRO: