From 956f06e612ee8d87482a0c88059f92c8c9144cf1 Mon Sep 17 00:00:00 2001 From: Jan Svabenik Date: Tue, 17 Mar 2026 19:51:50 +0100 Subject: [PATCH] Auto-restart on IQ timeout and report last-sample age --- cmd/sdrd/main.go | 36 ++++++++++++++++++++++++++++++++++++ internal/sdr/source.go | 7 ++++--- internal/sdrplay/sdrplay.go | 8 +++++++- web/app.js | 3 ++- 4 files changed, 49 insertions(+), 5 deletions(-) diff --git a/cmd/sdrd/main.go b/cmd/sdrd/main.go index e8b4a18..4386e04 100644 --- a/cmd/sdrd/main.go +++ b/cmd/sdrd/main.go @@ -10,6 +10,7 @@ import ( "os/signal" "path/filepath" "strconv" + "strings" "sync" "syscall" "time" @@ -97,6 +98,36 @@ type sourceManager struct { newSource func(cfg config.Config) (sdr.Source, error) } +func (m *sourceManager) Restart(cfg config.Config) error { + m.mu.Lock() + defer m.mu.Unlock() + old := m.src + _ = old.Stop() + next, err := m.newSource(cfg) + if err != nil { + _ = old.Start() + m.src = old + return err + } + if err := next.Start(); err != nil { + _ = next.Stop() + _ = old.Start() + m.src = old + return err + } + m.src = next + return nil +} + +func (m *sourceManager) Stats() sdr.SourceStats { + m.mu.RLock() + defer m.mu.RUnlock() + if sp, ok := m.src.(sdr.StatsProvider); ok { + return sp.Stats() + } + return sdr.SourceStats{} +} + func newSourceManager(src sdr.Source, newSource func(cfg config.Config) (sdr.Source, error)) *sourceManager { return &sourceManager{src: src, newSource: newSource} } @@ -463,6 +494,11 @@ func runDSP(ctx context.Context, src sdr.Source, cfg config.Config, det *detecto iq, err := src.ReadIQ(cfg.FFTSize) if err != nil { log.Printf("read IQ: %v", err) + if strings.Contains(err.Error(), "timeout") { + if err := src.Restart(cfg); err != nil { + log.Printf("restart failed: %v", err) + } + } continue } if !gotSamples { diff --git a/internal/sdr/source.go b/internal/sdr/source.go index 9ed74d8..25043e8 100644 --- a/internal/sdr/source.go +++ b/internal/sdr/source.go @@ -13,9 +13,10 @@ type ConfigurableSource interface { } type SourceStats struct { - BufferSamples int `json:"buffer_samples"` - Dropped uint64 `json:"dropped"` - Resets uint64 `json:"resets"` + BufferSamples int `json:"buffer_samples"` + Dropped uint64 `json:"dropped"` + Resets uint64 `json:"resets"` + LastSampleAgoMs int64 `json:"last_sample_ago_ms"` } type StatsProvider interface { diff --git a/internal/sdrplay/sdrplay.go b/internal/sdrplay/sdrplay.go index 72d37a8..9ea439f 100644 --- a/internal/sdrplay/sdrplay.go +++ b/internal/sdrplay/sdrplay.go @@ -97,6 +97,7 @@ type Source struct { bwKHz int dropped uint64 resets uint64 + lastSample time.Time cond *sync.Cond } @@ -284,7 +285,11 @@ func (s *Source) appendRing(samples []complex64) { func (s *Source) Stats() sdr.SourceStats { s.mu.Lock() defer s.mu.Unlock() - return sdr.SourceStats{BufferSamples: s.size, Dropped: s.dropped, Resets: s.resets} + ago := int64(-1) + if !s.lastSample.IsZero() { + ago = time.Since(s.lastSample).Milliseconds() + } + return sdr.SourceStats{BufferSamples: s.size, Dropped: s.dropped, Resets: s.resets, LastSampleAgoMs: ago} } func (s *Source) Flush() { @@ -383,6 +388,7 @@ func goStreamCallback(xi *C.short, xq *C.short, numSamples C.uint, reset C.uint, iq[i] = complex(re, im) } src.mu.Lock() + src.lastSample = time.Now() src.appendRing(iq) src.mu.Unlock() } diff --git a/web/app.js b/web/app.js index 242c9b0..a40136a 100644 --- a/web/app.js +++ b/web/app.js @@ -353,7 +353,8 @@ function renderSpectrum() { const binHz = sample_rate / n; const gpuState = gpuInfo.active ? 'GPU:ON' : (gpuInfo.available ? 'GPU:OFF' : 'GPU:N/A'); - metaEl.textContent = `Center ${(center_hz/1e6).toFixed(3)} MHz | Span ${(span/1e6).toFixed(3)} MHz | Res ${binHz.toFixed(1)} Hz/bin | Buf ${stats.buffer_samples} Drop ${stats.dropped} Reset ${stats.resets} | ${gpuState}`; + const lastAge = stats.last_sample_ago_ms >= 0 ? `${stats.last_sample_ago_ms}ms` : 'n/a'; + metaEl.textContent = `Center ${(center_hz/1e6).toFixed(3)} MHz | Span ${(span/1e6).toFixed(3)} MHz | Res ${binHz.toFixed(1)} Hz/bin | Buf ${stats.buffer_samples} Drop ${stats.dropped} Reset ${stats.resets} Last ${lastAge} | ${gpuState}`; } function renderWaterfall() {