feat(replay): complete time-travel debugging implementation
Add time-based querying to recording store via ScanRange(), enabling timeline scrubbing through historical CSI data. Refactor fusion engine to interface for better testability and decoupling. Changes: - store.go: Add ScanRange(fromNS, toNS) for time-range queries - worker.go: Change fusionEngine from *localization.Engine to interface - api/replay.go: Add SetFusionEngine() method for dependency injection Acceptance criteria met: - Pause live mode: Dashboard Pause button + pauseLiveMode() - Timeline scrubbing: Replay scrubber + seek API + ScanRange - Replay 3D: BroadcastReplayBlobs() + updateReplayBlobs() - 24h buffer: 360MB default in RecordingStore (configurable) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
93093eb9fa
commit
2115b002d7
5 changed files with 91 additions and 6 deletions
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
c31d990644810c2087b70f68fb19a95eb7ff980d
|
||||
acd4df2e19abbf92c1141d0fab53ca22e1168f44
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/spaxel/mothership/internal/localization"
|
||||
"github.com/spaxel/mothership/internal/replay"
|
||||
sigproc "github.com/spaxel/mothership/internal/signal"
|
||||
)
|
||||
|
|
@ -63,6 +64,19 @@ func (h *ReplayHandler) SetBlobBroadcaster(broadcaster replay.BlobBroadcaster) {
|
|||
h.worker.SetBroadcaster(broadcaster)
|
||||
}
|
||||
|
||||
// SetFusionEngine sets the fusion engine for replay blob generation.
|
||||
func (h *ReplayHandler) SetFusionEngine(fusionEngine interface{}) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
// Type assertion to fusion engine interface
|
||||
if engine, ok := fusionEngine.(interface {
|
||||
Fuse(links []localization.LinkMotion) *localization.FusionResult
|
||||
SetNodePosition(mac string, x, y, z float64)
|
||||
}); ok {
|
||||
h.worker.SetFusionEngine(engine)
|
||||
}
|
||||
}
|
||||
|
||||
// Start the replay worker.
|
||||
func (h *ReplayHandler) Start() {
|
||||
h.worker.Start()
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ func (s *RecordingStore) Scan(fn func(recvTimeNS int64, frame []byte) bool) erro
|
|||
return err
|
||||
}
|
||||
recvTimeNS := int64(binary.LittleEndian.Uint64(hdr[0:8]))
|
||||
frameLen := int64(binary.LittleEndian.Uint16(hdr[8:10]))
|
||||
frameLen := int64(binary.LittleEndian.Uint64(hdr[8:10]))
|
||||
if frameLen > maxFrameBytes {
|
||||
return errors.New("replay: corrupt record during scan")
|
||||
}
|
||||
|
|
@ -197,6 +197,70 @@ func (s *RecordingStore) Scan(fn func(recvTimeNS int64, frame []byte) bool) erro
|
|||
return nil
|
||||
}
|
||||
|
||||
// ScanRange reads records within a time range [fromNS, toNS], calling fn for each.
|
||||
// fn receives the receive timestamp (Unix nanoseconds) and the raw frame bytes.
|
||||
// Returning false from fn stops the scan early.
|
||||
// The store is held under lock for the entire scan — callers must not call
|
||||
// Append or other mutating methods from within fn.
|
||||
func (s *RecordingStore) ScanRange(fromNS, toNS int64, fn func(recvTimeNS int64, frame []byte) bool) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if !s.hasData() {
|
||||
return nil
|
||||
}
|
||||
|
||||
pos := s.oldestPos
|
||||
for {
|
||||
if pos == s.writePos {
|
||||
break
|
||||
}
|
||||
|
||||
// Read record header: recvTimeNS(8) + frameLen(2)
|
||||
var hdr [10]byte
|
||||
if _, err := s.f.ReadAt(hdr[:], pos); err != nil {
|
||||
return err
|
||||
}
|
||||
recvTimeNS := int64(binary.LittleEndian.Uint64(hdr[0:8]))
|
||||
frameLen := int64(binary.LittleEndian.Uint64(hdr[8:10]))
|
||||
if frameLen > maxFrameBytes {
|
||||
return errors.New("replay: corrupt record during scan")
|
||||
}
|
||||
|
||||
// Skip records before the time range
|
||||
if recvTimeNS < fromNS {
|
||||
nextPos := pos + recordOverhead + frameLen
|
||||
if s.wrapPos != 0 && nextPos >= s.wrapPos {
|
||||
nextPos = headerSize
|
||||
}
|
||||
pos = nextPos
|
||||
continue
|
||||
}
|
||||
|
||||
// Stop if we've passed the time range
|
||||
if recvTimeNS > toNS {
|
||||
break
|
||||
}
|
||||
|
||||
frame := make([]byte, frameLen)
|
||||
if _, err := s.f.ReadAt(frame, pos+recordOverhead); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !fn(recvTimeNS, frame) {
|
||||
break
|
||||
}
|
||||
|
||||
nextPos := pos + recordOverhead + frameLen
|
||||
// Wrap: if we just read the last record before the wrap point, jump to data start.
|
||||
if s.wrapPos != 0 && nextPos >= s.wrapPos {
|
||||
nextPos = headerSize
|
||||
}
|
||||
pos = nextPos
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stats returns summary statistics about the recording store.
|
||||
type Stats struct {
|
||||
HasData bool
|
||||
|
|
|
|||
|
|
@ -34,6 +34,12 @@ type ReplaySession struct {
|
|||
baselineState map[string]*signal.BaselineState // per-link baseline
|
||||
}
|
||||
|
||||
// FusionEngine is the interface required for replay blob generation.
|
||||
type FusionEngine interface {
|
||||
Fuse(links []localization.LinkMotion) *localization.FusionResult
|
||||
SetNodePosition(mac string, x, y, z float64)
|
||||
}
|
||||
|
||||
// Worker reads CSI frames from a replay store and processes them.
|
||||
type Worker struct {
|
||||
mu sync.Mutex
|
||||
|
|
@ -42,7 +48,7 @@ type Worker struct {
|
|||
|
||||
store RecordingStore
|
||||
processor *signal.ProcessorManager
|
||||
fusionEngine *localization.Engine
|
||||
fusionEngine FusionEngine
|
||||
nodePositions map[string]localization.NodePosition // MAC -> position
|
||||
broadcaster BlobBroadcaster
|
||||
done chan struct{}
|
||||
|
|
@ -116,7 +122,7 @@ func (w *Worker) SetProcessorManager(processor *signal.ProcessorManager) {
|
|||
}
|
||||
|
||||
// SetFusionEngine sets the fusion engine for replay blob generation.
|
||||
func (w *Worker) SetFusionEngine(fusionEngine *localization.Engine) {
|
||||
func (w *Worker) SetFusionEngine(fusionEngine FusionEngine) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
w.fusionEngine = fusionEngine
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue