From db1122ef4b397b9e9d7a71abff5e9f10c8b490d4 Mon Sep 17 00:00:00 2001 From: you Date: Sun, 29 Mar 2026 14:14:50 +0000 Subject: [PATCH] fix: align Go packet decoder with MeshCore firmware spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Match the C++ firmware wire format (Packet::writeTo/readFrom): 1. Field order: transport codes are parsed BEFORE path_length byte, matching firmware's header → transport_codes → path_len → path → payload 2. ACK payload: just 4-byte CRC checksum, not dest+src+ackHash. Firmware createAck() writes only ack_crc (4 bytes). 3. TRACE payload: tag(4) + authCode(4) + flags(1) + pathData, matching firmware createTrace() and onRecvPacket() TRACE handler. 4. ADVERT features: parse feat1 (0x20) and feat2 (0x40) optional 2-byte fields between location and name, matching AdvertDataBuilder and AdvertDataParser in the firmware. 5. Transport code naming: code1/code2 instead of nextHop/lastHop, matching firmware's transport_codes[0]/transport_codes[1] naming. Fixes applied to both cmd/ingestor/decoder.go and cmd/server/decoder.go. Tests updated to match new behavior. --- cmd/ingestor/decoder.go | 83 ++++++++++++++++++++++++++---------- cmd/ingestor/decoder_test.go | 61 +++++++++++++++----------- cmd/server/decoder.go | 77 +++++++++++++++++++++++---------- 3 files changed, 152 insertions(+), 69 deletions(-) diff --git a/cmd/ingestor/decoder.go b/cmd/ingestor/decoder.go index 147ec88..708d5b4 100644 --- a/cmd/ingestor/decoder.go +++ b/cmd/ingestor/decoder.go @@ -72,8 +72,8 @@ type Header struct { // TransportCodes are present on TRANSPORT_FLOOD and TRANSPORT_DIRECT routes. type TransportCodes struct { - NextHop string `json:"nextHop"` - LastHop string `json:"lastHop"` + Code1 string `json:"code1"` + Code2 string `json:"code2"` } // Path holds decoded path/hop information. @@ -92,6 +92,8 @@ type AdvertFlags struct { Room bool `json:"room"` Sensor bool `json:"sensor"` HasLocation bool `json:"hasLocation"` + HasFeat1 bool `json:"hasFeat1"` + HasFeat2 bool `json:"hasFeat2"` HasName bool `json:"hasName"` } @@ -111,6 +113,8 @@ type Payload struct { Lat *float64 `json:"lat,omitempty"` Lon *float64 `json:"lon,omitempty"` Name string `json:"name,omitempty"` + Feat1 *int `json:"feat1,omitempty"` + Feat2 *int `json:"feat2,omitempty"` BatteryMv *int `json:"battery_mv,omitempty"` TemperatureC *float64 `json:"temperature_c,omitempty"` ChannelHash int `json:"channelHash,omitempty"` @@ -123,6 +127,8 @@ type Payload struct { EphemeralPubKey string `json:"ephemeralPubKey,omitempty"` PathData string `json:"pathData,omitempty"` Tag uint32 `json:"tag,omitempty"` + AuthCode uint32 `json:"authCode,omitempty"` + TraceFlags *int `json:"traceFlags,omitempty"` RawHex string `json:"raw,omitempty"` Error string `json:"error,omitempty"` } @@ -199,14 +205,13 @@ func decodeEncryptedPayload(typeName string, buf []byte) Payload { } func decodeAck(buf []byte) Payload { - if len(buf) < 6 { + if len(buf) < 4 { return Payload{Type: "ACK", Error: "too short", RawHex: hex.EncodeToString(buf)} } + checksum := binary.LittleEndian.Uint32(buf[0:4]) return Payload{ Type: "ACK", - DestHash: hex.EncodeToString(buf[0:1]), - SrcHash: hex.EncodeToString(buf[1:2]), - ExtraHash: hex.EncodeToString(buf[2:6]), + ExtraHash: fmt.Sprintf("%08x", checksum), } } @@ -231,6 +236,8 @@ func decodeAdvert(buf []byte) Payload { if len(appdata) > 0 { flags := appdata[0] advType := int(flags & 0x0F) + hasFeat1 := flags&0x20 != 0 + hasFeat2 := flags&0x40 != 0 p.Flags = &AdvertFlags{ Raw: int(flags), Type: advType, @@ -239,6 +246,8 @@ func decodeAdvert(buf []byte) Payload { Room: advType == 3, Sensor: advType == 4, HasLocation: flags&0x10 != 0, + HasFeat1: hasFeat1, + HasFeat2: hasFeat2, HasName: flags&0x80 != 0, } @@ -252,6 +261,16 @@ func decodeAdvert(buf []byte) Payload { p.Lon = &lon off += 8 } + if hasFeat1 && len(appdata) >= off+2 { + feat1 := int(binary.LittleEndian.Uint16(appdata[off : off+2])) + p.Feat1 = &feat1 + off += 2 + } + if hasFeat2 && len(appdata) >= off+2 { + feat2 := int(binary.LittleEndian.Uint16(appdata[off : off+2])) + p.Feat2 = &feat2 + off += 2 + } if p.Flags.HasName { // Find null terminator to separate name from trailing telemetry bytes nameEnd := len(appdata) @@ -469,15 +488,22 @@ func decodePathPayload(buf []byte) Payload { } func decodeTrace(buf []byte) Payload { - if len(buf) < 12 { + if len(buf) < 9 { return Payload{Type: "TRACE", Error: "too short", RawHex: hex.EncodeToString(buf)} } - return Payload{ - Type: "TRACE", - DestHash: hex.EncodeToString(buf[5:11]), - SrcHash: hex.EncodeToString(buf[11:12]), - Tag: binary.LittleEndian.Uint32(buf[1:5]), + tag := binary.LittleEndian.Uint32(buf[0:4]) + authCode := binary.LittleEndian.Uint32(buf[4:8]) + flags := int(buf[8]) + p := Payload{ + Type: "TRACE", + Tag: tag, + AuthCode: authCode, + TraceFlags: &flags, } + if len(buf) > 9 { + p.PathData = hex.EncodeToString(buf[9:]) + } + return p } func decodePayload(payloadType int, buf []byte, channelKeys map[string]string) Payload { @@ -520,8 +546,7 @@ func DecodePacket(hexString string, channelKeys map[string]string) (*DecodedPack } header := decodeHeader(buf[0]) - pathByte := buf[1] - offset := 2 + offset := 1 var tc *TransportCodes if isTransportRoute(header.RouteType) { @@ -529,12 +554,18 @@ func DecodePacket(hexString string, channelKeys map[string]string) (*DecodedPack return nil, fmt.Errorf("packet too short for transport codes") } tc = &TransportCodes{ - NextHop: strings.ToUpper(hex.EncodeToString(buf[offset : offset+2])), - LastHop: strings.ToUpper(hex.EncodeToString(buf[offset+2 : offset+4])), + Code1: strings.ToUpper(hex.EncodeToString(buf[offset : offset+2])), + Code2: strings.ToUpper(hex.EncodeToString(buf[offset+2 : offset+4])), } offset += 4 } + if offset >= len(buf) { + return nil, fmt.Errorf("packet too short (no path byte)") + } + pathByte := buf[offset] + offset++ + path, bytesConsumed := decodePath(pathByte, buf, offset) offset += bytesConsumed @@ -562,16 +593,24 @@ func ComputeContentHash(rawHex string) string { return rawHex } - pathByte := buf[1] + headerByte := buf[0] + offset := 1 + if isTransportRoute(int(headerByte & 0x03)) { + offset += 4 + } + if offset >= len(buf) { + if len(rawHex) >= 16 { + return rawHex[:16] + } + return rawHex + } + pathByte := buf[offset] + offset++ hashSize := int((pathByte>>6)&0x3) + 1 hashCount := int(pathByte & 0x3F) pathBytes := hashSize * hashCount - headerByte := buf[0] - payloadStart := 2 + pathBytes - if isTransportRoute(int(headerByte & 0x03)) { - payloadStart += 4 - } + payloadStart := offset + pathBytes if payloadStart > len(buf) { if len(rawHex) >= 16 { return rawHex[:16] diff --git a/cmd/ingestor/decoder_test.go b/cmd/ingestor/decoder_test.go index 8c219dd..51ae989 100644 --- a/cmd/ingestor/decoder_test.go +++ b/cmd/ingestor/decoder_test.go @@ -129,7 +129,8 @@ func TestDecodePath3ByteHashes(t *testing.T) { func TestTransportCodes(t *testing.T) { // Route type 0 (TRANSPORT_FLOOD) should have transport codes - hex := "1400" + "AABB" + "CCDD" + "1A" + strings.Repeat("00", 10) + // Firmware order: header + transport_codes(4) + path_len + path + payload + hex := "14" + "AABB" + "CCDD" + "00" + strings.Repeat("00", 10) pkt, err := DecodePacket(hex, nil) if err != nil { t.Fatal(err) @@ -140,11 +141,11 @@ func TestTransportCodes(t *testing.T) { if pkt.TransportCodes == nil { t.Fatal("transportCodes should not be nil for TRANSPORT_FLOOD") } - if pkt.TransportCodes.NextHop != "AABB" { - t.Errorf("nextHop=%s, want AABB", pkt.TransportCodes.NextHop) + if pkt.TransportCodes.Code1 != "AABB" { + t.Errorf("code1=%s, want AABB", pkt.TransportCodes.Code1) } - if pkt.TransportCodes.LastHop != "CCDD" { - t.Errorf("lastHop=%s, want CCDD", pkt.TransportCodes.LastHop) + if pkt.TransportCodes.Code2 != "CCDD" { + t.Errorf("code2=%s, want CCDD", pkt.TransportCodes.Code2) } // Route type 1 (FLOOD) should NOT have transport codes @@ -537,10 +538,11 @@ func TestDecodeTraceShort(t *testing.T) { func TestDecodeTraceValid(t *testing.T) { buf := make([]byte, 16) - buf[0] = 0x00 - buf[1] = 0x01 // tag LE uint32 = 1 - buf[5] = 0xAA // destHash start - buf[11] = 0xBB + // tag(4) + authCode(4) + flags(1) + pathData + binary.LittleEndian.PutUint32(buf[0:4], 1) // tag = 1 + binary.LittleEndian.PutUint32(buf[4:8], 0xDEADBEEF) // authCode + buf[8] = 0x02 // flags + buf[9] = 0xAA // path data p := decodeTrace(buf) if p.Error != "" { t.Errorf("unexpected error: %s", p.Error) @@ -548,9 +550,18 @@ func TestDecodeTraceValid(t *testing.T) { if p.Tag != 1 { t.Errorf("tag=%d, want 1", p.Tag) } + if p.AuthCode != 0xDEADBEEF { + t.Errorf("authCode=%d, want 0xDEADBEEF", p.AuthCode) + } + if p.TraceFlags == nil || *p.TraceFlags != 2 { + t.Errorf("traceFlags=%v, want 2", p.TraceFlags) + } if p.Type != "TRACE" { t.Errorf("type=%s, want TRACE", p.Type) } + if p.PathData == "" { + t.Error("pathData should not be empty") + } } func TestDecodeAdvertShort(t *testing.T) { @@ -833,10 +844,9 @@ func TestComputeContentHashShortHex(t *testing.T) { } func TestComputeContentHashTransportRoute(t *testing.T) { - // Route type 0 (TRANSPORT_FLOOD) with no path hops + 4 transport code bytes - // header=0x14 (TRANSPORT_FLOOD, ADVERT), path=0x00 (0 hops) - // transport codes = 4 bytes, then payload - hex := "1400" + "AABBCCDD" + strings.Repeat("EE", 10) + // Route type 0 (TRANSPORT_FLOOD) with transport codes then path=0x00 (0 hops) + // header=0x14 (TRANSPORT_FLOOD, ADVERT), transport(4), path=0x00 + hex := "14" + "AABBCCDD" + "00" + strings.Repeat("EE", 10) hash := ComputeContentHash(hex) if len(hash) != 16 { t.Errorf("hash length=%d, want 16", len(hash)) @@ -870,12 +880,10 @@ func TestComputeContentHashPayloadBeyondBufferLongHex(t *testing.T) { func TestComputeContentHashTransportBeyondBuffer(t *testing.T) { // Transport route (0x00 = TRANSPORT_FLOOD) with path claiming some bytes - // total buffer too short for transport codes + path - // header=0x00, pathByte=0x02 (2 hops, 1-byte hash), then only 2 more bytes - // payloadStart = 2 + 2 + 4(transport) = 8, but buffer only 6 bytes - hex := "0002" + "AABB" + strings.Repeat("CC", 6) // 20 chars = 10 bytes + // header=0x00, transport(4), pathByte=0x02 (2 hops, 1-byte hash) + // offset=1+4+1+2=8, buffer needs to be >= 8 + hex := "00" + "AABB" + "CCDD" + "02" + strings.Repeat("CC", 6) // 20 chars = 10 bytes hash := ComputeContentHash(hex) - // payloadStart = 2 + 2 + 4 = 8, buffer is 10 bytes → should work if len(hash) != 16 { t.Errorf("hash length=%d, want 16", len(hash)) } @@ -913,8 +921,8 @@ func TestDecodePacketWithNewlines(t *testing.T) { } func TestDecodePacketTransportRouteTooShort(t *testing.T) { - // TRANSPORT_FLOOD (route=0) but only 3 bytes total → too short for transport codes - _, err := DecodePacket("140011", nil) + // TRANSPORT_FLOOD (route=0) but only 2 bytes total → too short for transport codes + _, err := DecodePacket("1400", nil) if err == nil { t.Error("expected error for transport route with too-short buffer") } @@ -931,16 +939,19 @@ func TestDecodeAckShort(t *testing.T) { } func TestDecodeAckValid(t *testing.T) { - buf := []byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF} + buf := []byte{0xAA, 0xBB, 0xCC, 0xDD} p := decodeAck(buf) if p.Error != "" { t.Errorf("unexpected error: %s", p.Error) } - if p.DestHash != "aa" { - t.Errorf("destHash=%s, want aa", p.DestHash) + if p.ExtraHash != "ddccbbaa" { + t.Errorf("extraHash=%s, want ddccbbaa", p.ExtraHash) } - if p.ExtraHash != "ccddeeff" { - t.Errorf("extraHash=%s, want ccddeeff", p.ExtraHash) + if p.DestHash != "" { + t.Errorf("destHash should be empty, got %s", p.DestHash) + } + if p.SrcHash != "" { + t.Errorf("srcHash should be empty, got %s", p.SrcHash) } } diff --git a/cmd/server/decoder.go b/cmd/server/decoder.go index fd51a7e..8058e02 100644 --- a/cmd/server/decoder.go +++ b/cmd/server/decoder.go @@ -54,8 +54,8 @@ type Header struct { // TransportCodes are present on TRANSPORT_FLOOD and TRANSPORT_DIRECT routes. type TransportCodes struct { - NextHop string `json:"nextHop"` - LastHop string `json:"lastHop"` + Code1 string `json:"code1"` + Code2 string `json:"code2"` } // Path holds decoded path/hop information. @@ -74,6 +74,8 @@ type AdvertFlags struct { Room bool `json:"room"` Sensor bool `json:"sensor"` HasLocation bool `json:"hasLocation"` + HasFeat1 bool `json:"hasFeat1"` + HasFeat2 bool `json:"hasFeat2"` HasName bool `json:"hasName"` } @@ -97,6 +99,8 @@ type Payload struct { EphemeralPubKey string `json:"ephemeralPubKey,omitempty"` PathData string `json:"pathData,omitempty"` Tag uint32 `json:"tag,omitempty"` + AuthCode uint32 `json:"authCode,omitempty"` + TraceFlags *int `json:"traceFlags,omitempty"` RawHex string `json:"raw,omitempty"` Error string `json:"error,omitempty"` } @@ -173,14 +177,13 @@ func decodeEncryptedPayload(typeName string, buf []byte) Payload { } func decodeAck(buf []byte) Payload { - if len(buf) < 6 { + if len(buf) < 4 { return Payload{Type: "ACK", Error: "too short", RawHex: hex.EncodeToString(buf)} } + checksum := binary.LittleEndian.Uint32(buf[0:4]) return Payload{ Type: "ACK", - DestHash: hex.EncodeToString(buf[0:1]), - SrcHash: hex.EncodeToString(buf[1:2]), - ExtraHash: hex.EncodeToString(buf[2:6]), + ExtraHash: fmt.Sprintf("%08x", checksum), } } @@ -205,6 +208,8 @@ func decodeAdvert(buf []byte) Payload { if len(appdata) > 0 { flags := appdata[0] advType := int(flags & 0x0F) + hasFeat1 := flags&0x20 != 0 + hasFeat2 := flags&0x40 != 0 p.Flags = &AdvertFlags{ Raw: int(flags), Type: advType, @@ -213,6 +218,8 @@ func decodeAdvert(buf []byte) Payload { Room: advType == 3, Sensor: advType == 4, HasLocation: flags&0x10 != 0, + HasFeat1: hasFeat1, + HasFeat2: hasFeat2, HasName: flags&0x80 != 0, } @@ -226,6 +233,12 @@ func decodeAdvert(buf []byte) Payload { p.Lon = &lon off += 8 } + if hasFeat1 && len(appdata) >= off+2 { + off += 2 // skip feat1 bytes (reserved for future use) + } + if hasFeat2 && len(appdata) >= off+2 { + off += 2 // skip feat2 bytes (reserved for future use) + } if p.Flags.HasName { name := string(appdata[off:]) name = strings.TrimRight(name, "\x00") @@ -276,15 +289,22 @@ func decodePathPayload(buf []byte) Payload { } func decodeTrace(buf []byte) Payload { - if len(buf) < 12 { + if len(buf) < 9 { return Payload{Type: "TRACE", Error: "too short", RawHex: hex.EncodeToString(buf)} } - return Payload{ - Type: "TRACE", - DestHash: hex.EncodeToString(buf[5:11]), - SrcHash: hex.EncodeToString(buf[11:12]), - Tag: binary.LittleEndian.Uint32(buf[1:5]), + tag := binary.LittleEndian.Uint32(buf[0:4]) + authCode := binary.LittleEndian.Uint32(buf[4:8]) + flags := int(buf[8]) + p := Payload{ + Type: "TRACE", + Tag: tag, + AuthCode: authCode, + TraceFlags: &flags, } + if len(buf) > 9 { + p.PathData = hex.EncodeToString(buf[9:]) + } + return p } func decodePayload(payloadType int, buf []byte) Payload { @@ -327,8 +347,7 @@ func DecodePacket(hexString string) (*DecodedPacket, error) { } header := decodeHeader(buf[0]) - pathByte := buf[1] - offset := 2 + offset := 1 var tc *TransportCodes if isTransportRoute(header.RouteType) { @@ -336,12 +355,18 @@ func DecodePacket(hexString string) (*DecodedPacket, error) { return nil, fmt.Errorf("packet too short for transport codes") } tc = &TransportCodes{ - NextHop: strings.ToUpper(hex.EncodeToString(buf[offset : offset+2])), - LastHop: strings.ToUpper(hex.EncodeToString(buf[offset+2 : offset+4])), + Code1: strings.ToUpper(hex.EncodeToString(buf[offset : offset+2])), + Code2: strings.ToUpper(hex.EncodeToString(buf[offset+2 : offset+4])), } offset += 4 } + if offset >= len(buf) { + return nil, fmt.Errorf("packet too short (no path byte)") + } + pathByte := buf[offset] + offset++ + path, bytesConsumed := decodePath(pathByte, buf, offset) offset += bytesConsumed @@ -367,16 +392,24 @@ func ComputeContentHash(rawHex string) string { return rawHex } - pathByte := buf[1] + headerByte := buf[0] + offset := 1 + if isTransportRoute(int(headerByte & 0x03)) { + offset += 4 + } + if offset >= len(buf) { + if len(rawHex) >= 16 { + return rawHex[:16] + } + return rawHex + } + pathByte := buf[offset] + offset++ hashSize := int((pathByte>>6)&0x3) + 1 hashCount := int(pathByte & 0x3F) pathBytes := hashSize * hashCount - headerByte := buf[0] - payloadStart := 2 + pathBytes - if isTransportRoute(int(headerByte & 0x03)) { - payloadStart += 4 - } + payloadStart := offset + pathBytes if payloadStart > len(buf) { if len(rawHex) >= 16 { return rawHex[:16]