Skip to content

Commit

Permalink
Merge acffae7 into 9e4de63
Browse files Browse the repository at this point in the history
  • Loading branch information
simeonmiteff committed Aug 24, 2020
2 parents 9e4de63 + acffae7 commit e9ca15f
Show file tree
Hide file tree
Showing 5 changed files with 294 additions and 0 deletions.
36 changes: 36 additions & 0 deletions README.md
Expand Up @@ -7,6 +7,7 @@ This is a NMEA library for the Go programming language (Golang).
## Features

- Parse individual NMEA 0183 sentences
- Support for sentences with NMEA 4.10 "TAG Blocks"
- Register custom parser for unsupported sentence types
- User-friendly MIT license

Expand Down Expand Up @@ -106,6 +107,41 @@ Date: 13/06/94
Variation: -4.200000
```

### TAG Blocks

NMEA 4.10 TAG Block values can be accessed via the message's `TagBlock` struct:

```go
package main

import (
"fmt"
"log"
"time"
"github.com/adrianmo/go-nmea"
)

func main() {
sentence := "\\s:Satelite_1,c:1553390539*62\\!AIVDM,1,1,,A,13M@ah0025QdPDTCOl`K6`nV00Sv,0*52"
s, err := nmea.Parse(sentence)
if err != nil {
log.Fatal(err)
}
parsed := s.(nmea.VDMVDO)
fmt.Printf("TAG Block timestamp: %v\n", time.Unix(parsed.TagBlock.Time, 0))
fmt.Printf("TAG Block source: %v\n", parsed.TagBlock.Source)
}
```

Output (locale/time zone dependent):

```
$ go run main/main.go
TAG Block timestamp: 2019-03-24 14:22:19 +1300 NZDT
TAG Block source: Satelite_1
```

### Custom message parsing

If you need to parse a message not supported by the library you can implement your own message parsing.
Expand Down
17 changes: 17 additions & 0 deletions sentence.go
Expand Up @@ -43,6 +43,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 @@ -66,6 +67,21 @@ 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)
tagBlockParts := strings.SplitN(raw, `\`, 3)

var (
tagBlock TagBlock
err error
)
if len(tagBlockParts) == 3 {
tags := tagBlockParts[1]
raw = tagBlockParts[2]
tagBlock, err = parseTagBlock(tags)
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 @@ -92,6 +108,7 @@ func parseSentence(raw string) (BaseSentence, error) {
Fields: fields[1:],
Checksum: checksumRaw,
Raw: raw,
TagBlock: tagBlock,
}, nil
}

Expand Down
33 changes: 33 additions & 0 deletions sentence_test.go
Expand Up @@ -57,6 +57,24 @@ var sentencetests = []struct {
Raw: "$GPRMC,235236,A,3925.9479,N,11945.9211,W,44.7,153.6,250905,15.2,E,A*0C",
},
},
{
name: "valid NMEA 4.10 TAG Block",
raw: "\\s:Satelite_1,c:1553390539*62\\!AIVDM,1,1,,A,13M@ah0025QdPDTCOl`K6`nV00Sv,0*52",
datatype: "VDM",
talkerid: "AI",
prefix: "AIVDM",
sent: BaseSentence{
Talker: "AI",
Type: "VDM",
Fields: []string{"1", "1", "", "A", "13M@ah0025QdPDTCOl`K6`nV00Sv", "0"},
Checksum: "52",
Raw: "!AIVDM,1,1,,A,13M@ah0025QdPDTCOl`K6`nV00Sv,0*52",
TagBlock: TagBlock{
Time: 1553390539,
Source: "Satelite_1",
},
},
},
{
name: "checksum bad",
raw: "$GPFOO,1,2,3.4,x,y,zz,*51",
Expand Down Expand Up @@ -87,6 +105,21 @@ var sentencetests = []struct {
raw: "$GPRMC,235236,A,3925.9479,N,11945.9211,W,44.7,153.6,250905,15.2,E,A*0A",
err: "nmea: sentence checksum mismatch [0C != 0A]",
},
{
name: "missing TAG Block start delimiter",
raw: "s:Satelite_1,c:1553390539*62\\!AIVDM,1,1,,A,13M@ah0025QdPDTCOl`K6`nV00Sv,0*52",
err: "nmea: sentence does not start with a '$' or '!'",
},
{
name: "missing TAG Block end delimiter",
raw: "\\s:Satelite_1,c:1553390539*62!AIVDM,1,1,,A,13M@ah0025QdPDTCOl`K6`nV00Sv,0*52",
err: "nmea: sentence does not start with a '$' or '!'",
},
{
name: "invalid TAG Block contents",
raw: "\\\\!AIVDM,1,1,,A,13M@ah0025QdPDTCOl`K6`nV00Sv,0*52",
err: "nmea: tagblock does not contain checksum separator",
},
}

func TestSentences(t *testing.T) {
Expand Down
84 changes: 84 additions & 0 deletions tagblock.go
@@ -0,0 +1,84 @@
package nmea

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

// TagBlock struct
type TagBlock struct {
Time int64 // TypeUnixTime unix timestamp (unit is likely to be s, but might be ms, YMMV), parameter: -c
RelativeTime int64 // TypeRelativeTime relative time, parameter: -r
Destination string // TypeDestinationID destination identification 15 char max, parameter: -d
Grouping string // TypeGrouping sentence grouping, parameter: -g
LineCount int64 // TypeLineCount line count, parameter: -n
Source string // TypeSourceID source identification 15 char max, parameter: -s
Text string // TypeTextString valid character string, parameter -t
}

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

// parseTagBlock adds support for tagblocks
// https://gpsd.gitlab.io/gpsd/AIVDM.html#_nmea_tag_blocks
func parseTagBlock(tags string) (TagBlock, error) {
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)
tagBlock TagBlock
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 {
parts := strings.SplitN(item, ":", 2)
if len(parts) != 2 {
return TagBlock{},
fmt.Errorf("nmea: tagblock field is malformed (should be <key>:<value>) [%s]", item)
}
key, value := parts[0], parts[1]
switch key {
case "c": // UNIX timestamp
tagBlock.Time, err = parseInt64(value)
if err != nil {
return TagBlock{}, err
}
case "d": // Destination ID
tagBlock.Destination = value
case "g": // Grouping
tagBlock.Grouping = value
case "n": // Line count
tagBlock.LineCount, err = parseInt64(value)
if err != nil {
return TagBlock{}, err
}
case "r": // Relative time
tagBlock.RelativeTime, err = parseInt64(value)
if err != nil {
return TagBlock{}, err
}
case "s": // Source ID
tagBlock.Source = value
case "t": // Text string
tagBlock.Text = value
}
}
return tagBlock, nil
}
124 changes: 124 additions & 0 deletions tagblock_test.go
@@ -0,0 +1,124 @@
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",
msg: TagBlock{
Time: 1553390539,
Source: "Satelite_1",
},
},
{

name: "Test NMEA tag block with head",
raw: "s:satelite,c:1564827317*25",
msg: TagBlock{
Time: 1564827317,
Source: "satelite",
},
},
{

name: "Test unknown tag",
raw: "x:NorSat_1,c:1564827317*42",
msg: TagBlock{
Time: 1564827317,
Source: "",
},
},
{
name: "Test unix timestamp",
raw: "x:NorSat_1,c:1564827317*42",
msg: TagBlock{
Time: 1564827317,
Source: "",
},
},
{

name: "Test milliseconds timestamp",
raw: "x:NorSat_1,c:1564827317000*72",
msg: TagBlock{
Time: 1564827317000,
Source: "",
},
},
{

name: "Test all input types",
raw: "s:satelite,c:1564827317,r:1553390539,d:ara,g:bulk,n:13,t:helloworld*3F",
msg: TagBlock{
Time: 1564827317,
RelativeTime: 1553390539,
Destination: "ara",
Grouping: "bulk",
Source: "satelite",
Text: "helloworld",
LineCount: 13,
},
},
{

name: "Test empty tag in tagblock",
raw: "s:satelite,,r:1553390539,d:ara,g:bulk,n:13,t:helloworld*68",
err: "nmea: tagblock field is malformed (should be <key>:<value>) []",
},
{

name: "Test Invalid checksum",
raw: "s:satelite,c:1564827317*49",
err: "nmea: tagblock checksum mismatch [25 != 49]",
},
{

name: "Test no checksum",
raw: "s:satelite,c:156482731749",
err: "nmea: tagblock does not contain checksum separator",
},
{

name: "Test invalid timestamp",
raw: "s:satelite,c:gjadslkg*30",
err: "nmea: tagblock unable to parse uint64 [gjadslkg]",
},
{

name: "Test invalid linecount",
raw: "s:satelite,n:gjadslkg*3D",
err: "nmea: tagblock unable to parse uint64 [gjadslkg]",
},
{

name: "Test invalid relative time",
raw: "s:satelite,r:gjadslkg*21",
err: "nmea: tagblock unable to parse uint64 [gjadslkg]",
},
}

func TestTagBlock(t *testing.T) {
for _, tt := range tagblocktests {
t.Run(tt.name, func(t *testing.T) {
m, err := parseTagBlock(tt.raw)
if tt.err != "" {
assert.Error(t, err)
assert.EqualError(t, err, tt.err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.msg, m)
}
})
}
}

0 comments on commit e9ca15f

Please sign in to comment.