From 2333468f92933771c8e4faebce89a9f4feb6d849 Mon Sep 17 00:00:00 2001 From: "Enot (ded) Skelly" Date: Tue, 9 Jun 2026 08:27:51 -0700 Subject: [PATCH] feat(traces): include raw path hashes as well as resolved closes #43 --- db/packets.go | 5 ++- db/store.go | 76 ----------------------------------- db/traces.go | 90 +++++++++++++++++++++++++++++++++++++++++- internal/api/traces.go | 6 +++ 4 files changed, 99 insertions(+), 78 deletions(-) diff --git a/db/packets.go b/db/packets.go index ad067b1..22013b6 100644 --- a/db/packets.go +++ b/db/packets.go @@ -335,7 +335,10 @@ func (s *Store) GetPacket(ctx context.Context, packetHash []byte) (*api.Packet, iatas = append(iatas, v.Iata) } } - p.ResolvedRoute = s.resolveTraceRoute(ctx, row.ParsedPayload, iatas) + var parsed tracePayload + if err := json.Unmarshal(row.ParsedPayload, &parsed); err == nil { + p.ResolvedRoute = s.resolveTraceRoute(ctx, &parsed, iatas) + } } return p, nil } diff --git a/db/store.go b/db/store.go index fa05e81..c4ace41 100644 --- a/db/store.go +++ b/db/store.go @@ -9,7 +9,6 @@ package db import ( "context" "encoding/hex" - "encoding/json" sqlc "github.com/MeshCore-Beacon/beacon-server/db/sqlc" "github.com/MeshCore-Beacon/beacon-server/internal/api" @@ -94,78 +93,3 @@ func toChannelMessage(id int64, packetHashHex string, channelHash []byte, sender ObservationCount: observationCount, } } - -// resolveTraceRoute resolves the path hashes from a trace parsedPayload across -// all provided IATAs, merging results per hop with best-confidence-wins semantics. -// Returns nil if the payload cannot be parsed or contains no path hashes. -func (s *Store) resolveTraceRoute(ctx context.Context, parsedPayload []byte, iatas []string) []api.ResolvedHop { - var tracePayload struct { - PathHashes []string `json:"pathHashes"` - Flags byte `json:"flags"` - SNRValues []float32 `json:"snrValues"` - } - if err := json.Unmarshal(parsedPayload, &tracePayload); err != nil || len(tracePayload.PathHashes) == 0 { - return nil - } - hashSize := int(1 << (tracePayload.Flags & 0x03)) - hashes := make([][]byte, 0, len(tracePayload.PathHashes)) - for _, h := range tracePayload.PathHashes { - b, err := hex.DecodeString(h) - if err == nil { - hashes = append(hashes, b) - } - } - confidenceRank := map[string]int{"none": 0, "ambiguous": 1, "high": 2} - type hopResult struct { - confidence string - entries []api.ResolvedPathEntry - } - merged := make([]hopResult, len(hashes)) - for i := range merged { - merged[i] = hopResult{confidence: "none"} - } - for _, iata := range iatas { - resolved, err := s.ResolvePathHashes(ctx, iata, hashes) - if err != nil { - continue - } - for i, hash := range hashes { - key := hex.EncodeToString(hash[:hashSize]) - entries := resolved[key] - var confidence string - switch len(entries) { - case 0: - confidence = "none" - case 1: - confidence = "high" - default: - confidence = "ambiguous" - } - if confidenceRank[confidence] > confidenceRank[merged[i].confidence] { - merged[i] = hopResult{confidence: confidence, entries: entries} - } - } - } - route := make([]api.ResolvedHop, 0, len(hashes)) - for i, hr := range merged { - hop := api.ResolvedHop{ - Confidence: hr.confidence, - Nodes: make([]api.ResolvedNode, 0, len(hr.entries)), - } - if i < len(tracePayload.SNRValues) { - snr := tracePayload.SNRValues[i] - hop.SNR = &snr - } - for _, e := range hr.entries { - hop.Nodes = append(hop.Nodes, api.ResolvedNode{ - ID: e.NodeID, - Name: e.Name, - Latitude: e.Latitude, - Longitude: e.Longitude, - PublicKey: hex.EncodeToString(e.PublicKey), - }) - } - route = append(route, hop) - } - return route -} diff --git a/db/traces.go b/db/traces.go index 30a6b11..3072a3c 100644 --- a/db/traces.go +++ b/db/traces.go @@ -6,6 +6,7 @@ package db import ( "context" "encoding/hex" + "encoding/json" "strings" "time" @@ -14,6 +15,12 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +type tracePayload struct { + PathHashes []string `json:"pathHashes"` + Flags byte `json:"flags"` + SNRValues []float32 `json:"snrValues"` +} + func (s *Store) ListTraceTags(ctx context.Context, iatas []string, scope string, since, until time.Time, cursor time.Time, limit int32) ([]api.TraceTagSummary, error) { iataFilter := strings.Join(iatas, ",") var sinceTS, untilTS, cursorTS pgtype.Timestamptz @@ -71,6 +78,20 @@ func (s *Store) GetTraceByTag(ctx context.Context, tag string) (*api.TraceDetail FirstHeardAt: r.FirstHeardAt.Time.UnixMilli(), LastHeardAt: r.LastHeardAt.Time.UnixMilli(), } + var parsed tracePayload + if err := json.Unmarshal(r.ParsedPayload, &parsed); err == nil { + // build raw path + rawPath := make([]api.RawHop, 0, len(parsed.PathHashes)) + for i, h := range parsed.PathHashes { + hop := api.RawHop{Hash: h} + if i < len(parsed.SNRValues) { + snr := parsed.SNRValues[i] + hop.SNR = &snr + } + rawPath = append(rawPath, hop) + } + packet.RawPath = rawPath + } // fetch observations to get IATAs for route resolution packetHashBytes, err := hex.DecodeString(r.PacketHashHex) if err == nil { @@ -84,10 +105,77 @@ func (s *Store) GetTraceByTag(ctx context.Context, tag string) (*api.TraceDetail iatas = append(iatas, v.Iata) } } - packet.ResolvedRoute = s.resolveTraceRoute(ctx, r.ParsedPayload, iatas) + packet.ResolvedRoute = s.resolveTraceRoute(ctx, &parsed, iatas) } } detail.Packets = append(detail.Packets, packet) } return detail, nil } + +func (s *Store) resolveTraceRoute(ctx context.Context, payload *tracePayload, iatas []string) []api.ResolvedHop { + if payload == nil || len(payload.PathHashes) == 0 { + return nil + } + hashSize := int(1 << (payload.Flags & 0x03)) + hashes := make([][]byte, 0, len(payload.PathHashes)) + for _, h := range payload.PathHashes { + b, err := hex.DecodeString(h) + if err == nil { + hashes = append(hashes, b) + } + } + confidenceRank := map[string]int{"none": 0, "ambiguous": 1, "high": 2} + type hopResult struct { + confidence string + entries []api.ResolvedPathEntry + } + merged := make([]hopResult, len(hashes)) + for i := range merged { + merged[i] = hopResult{confidence: "none"} + } + for _, iata := range iatas { + resolved, err := s.ResolvePathHashes(ctx, iata, hashes) + if err != nil { + continue + } + for i, hash := range hashes { + key := hex.EncodeToString(hash[:hashSize]) + entries := resolved[key] + var confidence string + switch len(entries) { + case 0: + confidence = "none" + case 1: + confidence = "high" + default: + confidence = "ambiguous" + } + if confidenceRank[confidence] > confidenceRank[merged[i].confidence] { + merged[i] = hopResult{confidence: confidence, entries: entries} + } + } + } + route := make([]api.ResolvedHop, 0, len(hashes)) + for i, hr := range merged { + hop := api.ResolvedHop{ + Confidence: hr.confidence, + Nodes: make([]api.ResolvedNode, 0, len(hr.entries)), + } + if i < len(payload.SNRValues) { + snr := payload.SNRValues[i] + hop.SNR = &snr + } + for _, e := range hr.entries { + hop.Nodes = append(hop.Nodes, api.ResolvedNode{ + ID: e.NodeID, + Name: e.Name, + Latitude: e.Latitude, + Longitude: e.Longitude, + PublicKey: hex.EncodeToString(e.PublicKey), + }) + } + route = append(route, hop) + } + return route +} diff --git a/internal/api/traces.go b/internal/api/traces.go index b444f9e..e7db15c 100644 --- a/internal/api/traces.go +++ b/internal/api/traces.go @@ -21,9 +21,15 @@ type TracePacket struct { Scope *string `json:"scope,omitempty"` FirstHeardAt int64 `json:"firstHeardAt"` // epoch ms LastHeardAt int64 `json:"lastHeardAt"` // epoch ms + RawPath []RawHop `json:"rawPath"` ResolvedRoute []ResolvedHop `json:"resolvedRoute"` } +type RawHop struct { + Hash string `json:"hash"` // hex-encoded path hash + SNR *float32 `json:"snr,omitempty"` +} + // TraceDetail is the full trace series for a given trace tag. type TraceDetail struct { TraceTag string `json:"traceTag"` // hex-encoded 4-byte tag