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
5 changes: 4 additions & 1 deletion cmd/ntripper/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -413,13 +413,16 @@ func streamFrames(
var rawxCount uint64

// Inject RTCM 1033 on connect and every 30s.
msg1033 := rtcm.Encode1033(rtcm.AntennaDescriptor{
msg1033, err := rtcm.Encode1033(rtcm.AntennaDescriptor{
StationID: stationID,
AntennaType: "ADVNULLANTENNA",
AntennaSetupID: 0,
ReceiverType: "u-blox F9T",
ReceiverFW: "2.20",
})
if err != nil {
return fmt.Errorf("encoding 1033: %w", err)
}
if _, err := client.Write(msg1033); err != nil {
return casterWrite("initial 1033", err)
}
Expand Down
6 changes: 4 additions & 2 deletions cmd/ntripper/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,10 @@ func TestSetupLogger(t *testing.T) {

func TestParseStreamCleanFile(t *testing.T) {
// A file of back-to-back valid RTCM3 frames parses without error.
a := rtcm.Encode1033(rtcm.AntennaDescriptor{StationID: 1, AntennaType: "A"})
b := rtcm.Encode1033(rtcm.AntennaDescriptor{StationID: 2, AntennaType: "BB"})
a, err := rtcm.Encode1033(rtcm.AntennaDescriptor{StationID: 1, AntennaType: "A"})
require.NoError(t, err)
b, err := rtcm.Encode1033(rtcm.AntennaDescriptor{StationID: 2, AntennaType: "BB"})
require.NoError(t, err)
path := filepath.Join(t.TempDir(), "stream.rtcm3")
require.NoError(t, os.WriteFile(path, append(append([]byte{}, a...), b...), 0o644))
require.NoError(t, parseStream(path))
Expand Down
38 changes: 34 additions & 4 deletions rtcm/encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ limitations under the License.

package rtcm

import "encoding/binary"
import "fmt"

// maxDescriptorFieldLen is the width of the DF227/DF230/DF232/DF234/DF236
// counters that prefix each descriptor string.
const maxDescriptorFieldLen = 255

// AntennaDescriptor holds parameters for generating RTCM 1033
// (Receiver and Antenna Descriptor) messages.
Expand All @@ -31,7 +35,10 @@ type AntennaDescriptor struct {
}

// Encode1033 generates a complete RTCM 1033 frame (header + payload + CRC).
func Encode1033(desc AntennaDescriptor) []byte {
// It reports ErrDescriptorTooLong for a string the 8-bit counters cannot carry,
// and ErrPayloadTooLarge for a frame whose length does not fit the 10-bit header
// field, rather than emitting a frame the parser and scanner both reject.
func Encode1033(desc AntennaDescriptor) ([]byte, error) {
// Build the bit-packed payload.
// RTCM 1033 fields:
// DF002: Message Number (12 bits) = 1033
Expand All @@ -48,6 +55,23 @@ func Encode1033(desc AntennaDescriptor) []byte {
// DF236: Serial Counter K (8 bits)
// DF237: Receiver Serial (K bytes)

fields := []struct {
name string
value string
}{
{"AntennaType", desc.AntennaType},
{"AntennaSerial", desc.AntennaSerial},
{"ReceiverType", desc.ReceiverType},
{"ReceiverFW", desc.ReceiverFW},
{"ReceiverSerial", desc.ReceiverSerial},
}
for _, f := range fields {
if len(f.value) > maxDescriptorFieldLen {
return nil, fmt.Errorf("%w: %s is %d bytes, max %d",
ErrDescriptorTooLong, f.name, len(f.value), maxDescriptorFieldLen)
}
}

payloadSize := 3 + // 12+12 bits = 24 bits = 3 bytes for msg number + station ID
1 + len(desc.AntennaType) +
1 + // setup ID
Expand All @@ -56,6 +80,11 @@ func Encode1033(desc AntennaDescriptor) []byte {
1 + len(desc.ReceiverFW) +
1 + len(desc.ReceiverSerial)

if payloadSize > MaxPayloadLen {
return nil, fmt.Errorf("%w: %d > %d for 1033",
ErrPayloadTooLarge, payloadSize, MaxPayloadLen)
}

payload := make([]byte, payloadSize)
offset := 0

Expand Down Expand Up @@ -107,11 +136,12 @@ func Encode1033(desc AntennaDescriptor) []byte {

// Header: preamble + 6 reserved bits (0) + 10-bit length
frame[0] = Preamble
binary.BigEndian.PutUint16(frame[1:3], uint16(len(payload)&0xFFFF))
frame[1] = byte((len(payload) >> 8) & 0x03)
frame[2] = byte(len(payload) & 0xFF)

// Payload
copy(frame[HeaderSize:], payload)

putCRC(frame)
return frame
return frame, nil
}
64 changes: 62 additions & 2 deletions rtcm/encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ limitations under the License.
package rtcm

import (
"bytes"
"strings"
"testing"

"github.com/stretchr/testify/require"
Expand All @@ -27,14 +29,21 @@ func stationIDOf(payload []byte) uint16 {
return uint16(payload[1]&0x0F)<<8 | uint16(payload[2])
}

func mustEncode1033(t *testing.T, desc AntennaDescriptor) []byte {
t.Helper()
frame, err := Encode1033(desc)
require.NoError(t, err)
return frame
}

func TestEncode1033RoundTrip(t *testing.T) {
desc := AntennaDescriptor{
StationID: 1234,
AntennaType: "ADVNULLANTENNA",
ReceiverType: "u-blox F9T",
ReceiverFW: "2.20",
}
frame := Encode1033(desc)
frame := mustEncode1033(t, desc)

f, err := ParseFrame(frame) // also validates CRC-24Q
require.NoError(t, err)
Expand All @@ -45,8 +54,59 @@ func TestEncode1033RoundTrip(t *testing.T) {
require.Contains(t, string(f.Payload), "2.20")
}

func TestEncode1033RejectsUnrepresentableFrames(t *testing.T) {
t.Run("total exceeds the 10-bit header length", func(t *testing.T) {
// Every field is individually legal under its 8-bit counter, but the
// payload they add up to does not fit the header's length field.
long := strings.Repeat("A", 250)
frame, err := Encode1033(AntennaDescriptor{
StationID: 7,
AntennaType: long,
AntennaSerial: long,
ReceiverType: long,
ReceiverFW: long,
ReceiverSerial: long,
})
require.Nil(t, frame)
require.ErrorIs(t, err, ErrPayloadTooLarge)
})

t.Run("field exceeds its 8-bit counter", func(t *testing.T) {
frame, err := Encode1033(AntennaDescriptor{
StationID: 7,
AntennaType: strings.Repeat("A", maxDescriptorFieldLen+1),
})
require.Nil(t, frame)
require.ErrorIs(t, err, ErrDescriptorTooLong)
})
}

func TestEncode1033LargestRepresentableFrame(t *testing.T) {
// 9 bytes of fixed overhead plus 255+255+255+249 descriptor bytes is exactly
// MaxPayloadLen, the longest frame the header can still describe.
frame, err := Encode1033(AntennaDescriptor{
StationID: 7,
AntennaType: strings.Repeat("A", maxDescriptorFieldLen),
AntennaSerial: strings.Repeat("B", maxDescriptorFieldLen),
ReceiverType: strings.Repeat("C", maxDescriptorFieldLen),
ReceiverFW: strings.Repeat("D", maxDescriptorFieldLen-6),
})
require.NoError(t, err)
require.Len(t, frame, HeaderSize+MaxPayloadLen+CRCSize)
require.Zero(t, frame[1]&0xFC, "reserved bits must stay zero")

f, err := ParseFrame(frame)
require.NoError(t, err)
require.Equal(t, uint16(1033), f.MessageType)

s := NewScanner(bytes.NewReader(frame))
require.True(t, s.Scan(), "the scanner must sync on what the encoder emitted")
require.Equal(t, uint16(1033), s.Frame().MessageType)
require.NoError(t, s.Err())
}

func TestPatchStationID(t *testing.T) {
orig := Encode1033(AntennaDescriptor{StationID: 1, AntennaType: "X"})
orig := mustEncode1033(t, AntennaDescriptor{StationID: 1, AntennaType: "X"})
patched := PatchStationID(orig, 4090)

f, err := ParseFrame(patched) // CRC must still validate after the patch
Expand Down
8 changes: 4 additions & 4 deletions rtcm/mixed_scanner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ func TestScannerUBXFrame(t *testing.T) {
}

func TestScannerMixedUBXAndRTCM(t *testing.T) {
rtcmA := Encode1033(AntennaDescriptor{StationID: 1, AntennaType: "A"})
rtcmA := mustEncode1033(t, AntennaDescriptor{StationID: 1, AntennaType: "A"})
ubx := buildUBXFrame(UBXMsgRAWX, []byte{1, 2, 3, 4, 5})
rtcmB := Encode1033(AntennaDescriptor{StationID: 2, AntennaType: "BB"})
rtcmB := mustEncode1033(t, AntennaDescriptor{StationID: 2, AntennaType: "BB"})

var buf bytes.Buffer
buf.Write(rtcmA)
Expand Down Expand Up @@ -73,7 +73,7 @@ func TestScannerMixedUBXAndRTCM(t *testing.T) {
func TestScannerUBXBadChecksumResyncs(t *testing.T) {
bad := buildUBXFrame(UBXMsgRAWX, []byte{0x11, 0x22, 0x33})
bad[len(bad)-1] ^= 0xFF // corrupt the checksum
good := Encode1033(AntennaDescriptor{StationID: 3, AntennaType: "CCC"})
good := mustEncode1033(t, AntennaDescriptor{StationID: 3, AntennaType: "CCC"})

var buf bytes.Buffer
buf.Write(bad)
Expand Down Expand Up @@ -145,7 +145,7 @@ func TestScannerRecordsNonEOFReadError(t *testing.T) {
})

t.Run("RTCM", func(t *testing.T) {
frame := Encode1033(AntennaDescriptor{StationID: 1, AntennaType: "A"})
frame := mustEncode1033(t, AntennaDescriptor{StationID: 1, AntennaType: "A"})
s := NewMixedScanner(&shortThenErrReader{data: frame[:4], err: readErr})
require.False(t, s.Scan())
require.ErrorIs(t, s.Err(), readErr)
Expand Down
9 changes: 5 additions & 4 deletions rtcm/rtcm.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,11 @@ const (
)

var (
ErrInvalidPreamble = errors.New("invalid preamble")
ErrPayloadTooLarge = errors.New("payload length exceeds maximum")
ErrFrameTooShort = errors.New("frame data too short")
ErrCRCMismatch = errors.New("CRC-24Q mismatch")
ErrInvalidPreamble = errors.New("invalid preamble")
ErrPayloadTooLarge = errors.New("payload length exceeds maximum")
ErrDescriptorTooLong = errors.New("descriptor length exceeds 8-bit counter")
ErrFrameTooShort = errors.New("frame data too short")
ErrCRCMismatch = errors.New("CRC-24Q mismatch")
)

// Frame represents a parsed RTCM3 frame.
Expand Down
Loading