Skip to content

Commit

Permalink
sentences for NMEA2000 over NMEA0183 (#106)
Browse files Browse the repository at this point in the history
* sentences for NMEA2000 over NMEA0183
  • Loading branch information
aldas committed Jun 13, 2023
1 parent 0203fa1 commit 852d88f
Show file tree
Hide file tree
Showing 6 changed files with 286 additions and 11 deletions.
28 changes: 17 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
|--------------------|---------------------------------------------------------------------|------------------------------------------------------------------------------------------------|
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:

Expand Down
66 changes: 66 additions & 0 deletions pcdin.go
Original file line number Diff line number Diff line change
@@ -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<CR><LF>
// 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()
}
66 changes: 66 additions & 0 deletions pcdin_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
66 changes: 66 additions & 0 deletions pgn.go
Original file line number Diff line number Diff line change
@@ -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<CR><LF>
// 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()
}
67 changes: 67 additions & 0 deletions pgn_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
4 changes: 4 additions & 0 deletions sentence.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit 852d88f

Please sign in to comment.