Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for TagBlocks #59

Closed
wants to merge 10 commits into from
9 changes: 9 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,12 @@ 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)
var tagBlock TagBlock
klyve marked this conversation as resolved.
Show resolved Hide resolved
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 +84,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
111 changes: 111 additions & 0 deletions tagblock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package nmea

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

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
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+)\\(.*)`)
klyve marked this conversation as resolved.
Show resolved Hide resolved
)

// TagBlock struct
type TagBlock struct {
Head string // *
Time uint32 // -c
klyve marked this conversation as resolved.
Show resolved Hide resolved
RelativeTime uint32 // -r
Destination string // -d 15 char max
Grouping string // -g nummeric string
LineCount uint32 // -n int
Source string // -s 15 char max
Text string // -t Variable length text
}

func parseUint(raw string) (uint32, error) {
i, err := strconv.ParseUint(raw[2:], 10, 32)
if err != nil {
return 0, fmt.Errorf("nmea: tagblock unable to parse uint32 [%s]", raw)
}
return uint32(i), 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)
var tagBlock TagBlock
if matches != nil {
klyve marked this conversation as resolved.
Show resolved Hide resolved
raw = matches[3]
tags := matches[2]
tagBlock.Head = matches[1]

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

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

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

data := strings.Split(tags[:sumSepIndex], ",")
for _, item := range data {
switch item[0:1] {
case TypeUnixTime:
tagBlock.Time, err = parseUint(item)
if err != nil {
return tagBlock, raw, err
}
case TypeDestinationID:
tagBlock.Destination = item[2:]
case TypeGrouping:
tagBlock.Grouping = item[2:]
case TypeLineCount:
tagBlock.LineCount, err = parseUint(item)
if err != nil {
return tagBlock, raw, err
}
case TypeRelativeTime:
tagBlock.RelativeTime, err = parseUint(item)
if err != nil {
return tagBlock, raw, err
}
case TypeSourceID:
tagBlock.Source = item[2:]
case TypeTextString:
tagBlock.Text = item[2:]
}
}

}

return tagBlock, raw, nil
}
107 changes: 107 additions & 0 deletions tagblock_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package nmea

import (
"testing"

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

var tagblocktests = []struct {
//name: "Tagblock ok",
//raw: "",
klyve marked this conversation as resolved.
Show resolved Hide resolved
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 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 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)
}
})
}
}