-
Notifications
You must be signed in to change notification settings - Fork 86
Added support for TagBlocks #59
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
Changes from 6 commits
2ffc4bc
83a54df
30fd183
e7a1392
d2c4cad
60b3520
1dea5fa
7ae4adf
75d6121
1270711
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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" | ||
| ) | ||
|
|
||
| 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) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
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 |
||
| 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 = xorChecksum(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 { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't this be Actually, I think a cleaner way would be to just use 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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. return |
||
| } | ||
| 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 | ||
| } | ||
| 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]", | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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
TagBlockstruct fields.