diff --git a/cmd/ingestor/decoder.go b/cmd/ingestor/decoder.go index 56ab95b4..8821d828 100644 --- a/cmd/ingestor/decoder.go +++ b/cmd/ingestor/decoder.go @@ -131,6 +131,7 @@ type Payload struct { SenderTimestamp uint32 `json:"sender_timestamp,omitempty"` EphemeralPubKey string `json:"ephemeralPubKey,omitempty"` PathData string `json:"pathData,omitempty"` + SNRValues []float64 `json:"snrValues,omitempty"` Tag uint32 `json:"tag,omitempty"` AuthCode uint32 `json:"authCode,omitempty"` TraceFlags *int `json:"traceFlags,omitempty"` @@ -609,6 +610,21 @@ func DecodePacket(hexString string, channelKeys map[string]string, validateSigna } // The header path hops count represents SNR entries = completed hops hopsCompleted := path.HashCount + // Extract per-hop SNR from header path bytes (int8, quarter-dB encoding). + // Mirrors cmd/server/decoder.go — must be done at ingest time so SNR + // values are persisted in decoded_json (server endpoint serves DB as-is). + if hopsCompleted > 0 && len(path.Hops) >= hopsCompleted { + snrVals := make([]float64, 0, hopsCompleted) + for i := 0; i < hopsCompleted; i++ { + b, err := hex.DecodeString(path.Hops[i]) + if err == nil && len(b) == 1 { + snrVals = append(snrVals, float64(int8(b[0]))/4.0) + } + } + if len(snrVals) > 0 { + payload.SNRValues = snrVals + } + } pathBytes, err := hex.DecodeString(payload.PathData) if err == nil && payload.TraceFlags != nil { // path_sz from flags byte is a power-of-two exponent per firmware: diff --git a/cmd/ingestor/decoder_test.go b/cmd/ingestor/decoder_test.go index c781b4aa..92e1c5db 100644 --- a/cmd/ingestor/decoder_test.go +++ b/cmd/ingestor/decoder_test.go @@ -1947,3 +1947,32 @@ func TestDecodeTracePayloadFailSetsAnomaly(t *testing.T) { t.Error("expected Anomaly to be set when TRACE payload decode fails but observation is stored") } } + +// TestDecodeTraceExtractsSNRValues verifies that for TRACE packets, the header +// path bytes are interpreted as int8 SNR values (quarter-dB) and exposed via +// payload.SNRValues. Mirrors logic in cmd/server/decoder.go (issue: SNR values +// extracted by server but never written into decoded_json by ingestor). +// +// Packet 26022FF8116A23A80000000001C0DE1000DEDE: +// header 0x26 → TRACE (pt=9), DIRECT (rt=2) +// pathByte 0x02 → hash_size=1, hash_count=2 +// header path: 2F F8 → SNR = [int8(0x2F)/4, int8(0xF8)/4] = [11.75, -2.0] +// payload (15B): tag=116A23A8 auth=00000000 flags=0x01 pathData=C0DE1000DEDE +func TestDecodeTraceExtractsSNRValues(t *testing.T) { + pkt, err := DecodePacket("26022FF8116A23A80000000001C0DE1000DEDE", nil, false) + if err != nil { + t.Fatalf("DecodePacket error: %v", err) + } + if pkt.Payload.Type != "TRACE" { + t.Fatalf("payload type=%s, want TRACE", pkt.Payload.Type) + } + if len(pkt.Payload.SNRValues) != 2 { + t.Fatalf("len(SNRValues)=%d, want 2 (got %v)", len(pkt.Payload.SNRValues), pkt.Payload.SNRValues) + } + if pkt.Payload.SNRValues[0] != 11.75 { + t.Errorf("SNRValues[0]=%v, want 11.75", pkt.Payload.SNRValues[0]) + } + if pkt.Payload.SNRValues[1] != -2.0 { + t.Errorf("SNRValues[1]=%v, want -2.0", pkt.Payload.SNRValues[1]) + } +}