Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module github.com/adrianmo/go-nmea
module github.com/klyve/go-nmea

require (
github.com/davecgh/go-spew v1.1.1 // indirect
Expand Down
8 changes: 8 additions & 0 deletions sentence.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type BaseSentence struct {
Fields []string // Array of fields
Checksum string // The Checksum
Raw string // The raw NMEA sentence received
TagBlock TagBlock // NMEA tagblock
}

// Prefix returns the talker and type of message
Expand All @@ -57,6 +58,11 @@ func (s BaseSentence) String() string { return s.Raw }
// parseSentence parses a raw message into it's fields
func parseSentence(raw string) (BaseSentence, error) {
raw = strings.TrimSpace(raw)
tagBlock, raw, err := parseTagBlock(raw)
if err != nil {
return BaseSentence{}, err
}

startIndex := strings.IndexAny(raw, SentenceStart+SentenceStartEncapsulated)
if startIndex != 0 {
return BaseSentence{}, fmt.Errorf("nmea: sentence does not start with a '$' or '!'")
Expand All @@ -77,12 +83,14 @@ func parseSentence(raw string) (BaseSentence, error) {
"nmea: sentence checksum mismatch [%s != %s]", checksum, checksumRaw)
}
talker, typ := parsePrefix(fields[0])

return BaseSentence{
Talker: talker,
Type: typ,
Fields: fields[1:],
Checksum: checksumRaw,
Raw: raw,
TagBlock: tagBlock,
}, nil
}

Expand Down
137 changes: 137 additions & 0 deletions tagblock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package nmea

import (
"errors"
"fmt"
"regexp"
"strconv"
"strings"
"time"
)

const (
// TypeUnixTime unix timestamp, parameter: -c
TypeUnixTime = "c"
// TypeDestinationID destination identification 15char max, parameter: -d
TypeDestinationID = "d"
// TypeGrouping sentence grouping, parameter: -g
TypeGrouping = "g"
// TypeLineCount linecount, parameter: -n
TypeLineCount = "n"
// TypeRelativeTime relative time time, paremeter: -r
TypeRelativeTime = "r"
// TypeSourceID source identification 15char max, paremter: -s
TypeSourceID = "s"
// TypeTextString valid character string, parameter -t
TypeTextString = "t"
)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need these constants in the public API. Since they're only used in the one spot, I think it would be better to just inline them into the switch.

Move the comments from these types to the TagBlock struct fields.


var (
// tagBlockRegexp matches nmea tag blocks
tagBlockRegexp = regexp.MustCompile(`^(.*)\\(\S+)\\(.*)`)
)

// TagBlock struct
type TagBlock struct {
Head string // *
Time int64 // -c
RelativeTime int64 // -r
Destination string // -d 15 char max
Grouping string // -g nummeric string
LineCount int64 // -n int
Source string // -s 15 char max
Text string // -t Variable length text
}

func parseInt64(raw string) (int64, error) {
i, err := strconv.ParseInt(raw[2:], 10, 64)
if err != nil {
return 0, fmt.Errorf("nmea: tagblock unable to parse uint32 [%s]", raw)
}
return i, nil
}

// Timestamp can come as milliseconds or seconds
func validUnixTimestamp(timestamp int64) (int64, error) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems hacky. I think we should leave it up to the user of the package to know what units their device is emitting. I'd prefer to just pass along the value we receive instead of trying to be clever with it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was some debate about how to handle this on the gpsd mailing list. ESR's document (https://gpsd.gitlab.io/gpsd/AIVDM.html) says:

As of May 2014 no NMEA 4.10 relative time fields have been observed in the wild. It is unknown whether the unit is seconds or milliseconds.

I've worked with tagblock timestamps from AISHub.net, as well as a commercial satellite AIS provider, and so far have only seen seconds, never milliseconds. In light of this, beyond agreeing with @icholy about just passing the value along as an int64, I'd suggest documenting that the value should be seconds, not milliseconds (but warn the user that YMMV).

if timestamp < 0 {
return 0, errors.New("nmea: Tagblock timestamp is not valid must be between 0 and now + 24h")
}
now := time.Now()
unix := now.Unix() + 24*3600
if timestamp > unix {
if timestamp > unix*1000 {
return 0, errors.New("nmea: Tagblock timestamp is not valid")
}
return timestamp / 1000, nil
}

return timestamp, nil
}

// parseTagBlock adds support for tagblocks
// https://rietman.wordpress.com/2016/09/17/nemastudio-now-supports-the-nmea-0183-tag-block/
func parseTagBlock(raw string) (TagBlock, string, error) {
matches := tagBlockRegexp.FindStringSubmatch(raw)
if matches == nil {
return TagBlock{}, raw, nil
}

tagBlock := TagBlock{}
raw = matches[3]
tags := matches[2]
tagBlock.Head = matches[1]

sumSepIndex := strings.Index(tags, ChecksumSep)
if sumSepIndex == -1 {
return tagBlock, "", fmt.Errorf("nmea: tagblock does not contain checksum separator")
}

var (
fieldsRaw = tags[0:sumSepIndex]
checksumRaw = strings.ToUpper(tags[sumSepIndex+1:])
checksum = Checksum(fieldsRaw)
err error
)

// Validate the checksum
if checksum != checksumRaw {
return tagBlock, "", fmt.Errorf("nmea: tagblock checksum mismatch [%s != %s]", checksum, checksumRaw)
}

items := strings.Split(tags[:sumSepIndex], ",")
for _, item := range items {
if len(item) == 0 {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be if len(item) < 2 { ?

Actually, I think a cleaner way would be to just use strings.Split on it.

parts := strings.SplitN(item, ":", 2)
if len(parts) != 2 {
	continue
}
key, value := parts[0], parts[1]

continue
}
switch item[:1] {
case TypeUnixTime:
tagBlock.Time, err = parseInt64(item)
if err != nil {
return tagBlock, raw, err
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return "" instead of raw and TagBlock{} instead of the partially constructed tagBlock variable. This makes it explicit that these aren't being used by the caller. Do this at all the other error returns too.

}
tagBlock.Time, err = validUnixTimestamp(tagBlock.Time)
if err != nil {
return tagBlock, raw, err
}
case TypeDestinationID:
tagBlock.Destination = item[2:]
case TypeGrouping:
tagBlock.Grouping = item[2:]
case TypeLineCount:
tagBlock.LineCount, err = parseInt64(item)
if err != nil {
return tagBlock, raw, err
}
case TypeRelativeTime:
tagBlock.RelativeTime, err = parseInt64(item)
if err != nil {
return tagBlock, raw, err
}
case TypeSourceID:
tagBlock.Source = item[2:]
case TypeTextString:
tagBlock.Text = item[2:]
}
}
return tagBlock, raw, nil
}
152 changes: 152 additions & 0 deletions tagblock_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package nmea

import (
"testing"

"github.com/stretchr/testify/assert"
)

var tagblocktests = []struct {
name string
raw string
err string
msg TagBlock
}{
{

name: "Test NMEA tag block",
raw: "\\s:Satelite_1,c:1553390539*62\\!AIVDM,1,1,,A,13M@ah0025QdPDTCOl`K6`nV00Sv,0*52",
msg: TagBlock{
Time: 1553390539,
Source: "Satelite_1",
},
},
{

name: "Test NMEA tag block with head",
raw: "UdPbC?\\s:satelite,c:1564827317*25\\!AIVDM,1,1,,A,19NSRaP02A0fo91kwnaMKbjR08:J,0*15",
msg: TagBlock{
Time: 1564827317,
Source: "satelite",
Head: "UdPbC?",
},
},
{

name: "Test unknown tag",
raw: "UdPbC?\\x:NorSat_1,c:1564827317*42\\!AIVDM,1,1,,A,19NSRaP02A0fo91kwnaMKbjR08:J,0*15",
msg: TagBlock{
Time: 1564827317,
Source: "",
Head: "UdPbC?",
},
},
{
name: "Test unix timestamp",
raw: "UdPbC?\\x:NorSat_1,c:1564827317*42\\!AIVDM,1,1,,A,19NSRaP02A0fo91kwnaMKbjR08:J,0*15",
msg: TagBlock{
Time: 1564827317,
Source: "",
Head: "UdPbC?",
},
},
{

name: "Test milliseconds timestamp",
raw: "UdPbC?\\x:NorSat_1,c:1564827317000*72\\!AIVDM,1,1,,A,19NSRaP02A0fo91kwnaMKbjR08:J,0*15",
msg: TagBlock{
Time: 1564827317,
Source: "",
Head: "UdPbC?",
},
},
{

name: "Test invalid high timestamp",
raw: "UdPbC?\\x:NorSat_1,c:25648273170000000*71\\!AIVDM,1,1,,A,19NSRaP02A0fo91kwnaMKbjR08:J,0*15",
err: "nmea: Tagblock timestamp is not valid",
},
{

name: "Test invalid low timestamp",
raw: "UdPbC?\\x:NorSat_1,c:-10*60\\!AIVDM,1,1,,A,19NSRaP02A0fo91kwnaMKbjR08:J,0*15",
err: "nmea: Tagblock timestamp is not valid must be between 0 and now + 24h",
},
{

name: "Test all input types",
raw: "UdPbC?\\s:satelite,c:1564827317,r:1553390539,d:ara,g:bulk,n:13,t:helloworld*3F\\!AIVDM,1,1,,A,19NSRaP02A0fo91kwnaMKbjR08:J,0*15",
msg: TagBlock{
Time: 1564827317,
RelativeTime: 1553390539,
Destination: "ara",
Grouping: "bulk",
Source: "satelite",
Head: "UdPbC?",
Text: "helloworld",
LineCount: 13,
},
},
{

name: "Test empty tag in tagblock",
raw: "UdPbC?\\s:satelite,,r:1553390539,d:ara,g:bulk,n:13,t:helloworld*68\\!AIVDM,1,1,,A,19NSRaP02A0fo91kwnaMKbjR08:J,0*15",
msg: TagBlock{
Time: 0,
RelativeTime: 1553390539,
Destination: "ara",
Grouping: "bulk",
Source: "satelite",
Head: "UdPbC?",
Text: "helloworld",
LineCount: 13,
},
//err: "nmea: tagblock checksum mismatch [25 != 49]",
Copy link
Copy Markdown
Collaborator

@icholy icholy Aug 8, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't commit commented-out code

},
{

name: "Test Invalid checksum",
raw: "UdPbC?\\s:satelite,c:1564827317*49\\!AIVDM,1,1,,A,19NSRaP02A0fo91kwnaMKbjR08:J,0*15",
err: "nmea: tagblock checksum mismatch [25 != 49]",
},
{

name: "Test no checksum",
raw: "UdPbC?\\s:satelite,c:156482731749\\!AIVDM,1,1,,A,19NSRaP02A0fo91kwnaMKbjR08:J,0*15",
err: "nmea: tagblock does not contain checksum separator",
},
{

name: "Test invalid timestamp",
raw: "UdPbC?\\s:satelite,c:gjadslkg*30\\!AIVDM,1,1,,A,19NSRaP02A0fo91kwnaMKbjR08:J,0*15",
err: "nmea: tagblock unable to parse uint32 [c:gjadslkg]",
},
{

name: "Test invalid linecount",
raw: "UdPbC?\\s:satelite,n:gjadslkg*3D\\!AIVDM,1,1,,A,19NSRaP02A0fo91kwnaMKbjR08:J,0*15",
err: "nmea: tagblock unable to parse uint32 [n:gjadslkg]",
},
{

name: "Test invalid relative time",
raw: "UdPbC?\\s:satelite,r:gjadslkg*21\\!AIVDM,1,1,,A,19NSRaP02A0fo91kwnaMKbjR08:J,0*15",
err: "nmea: tagblock unable to parse uint32 [r:gjadslkg]",
},
}

func TestTagBlock(t *testing.T) {
for _, tt := range tagblocktests {
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)
vdm := m.(VDMVDO)
assert.Equal(t, tt.msg, vdm.BaseSentence.TagBlock)
}
})
}
}