diff --git a/cmd/sdrd/main.go b/cmd/sdrd/main.go index 0290924..df1418f 100644 --- a/cmd/sdrd/main.go +++ b/cmd/sdrd/main.go @@ -316,7 +316,9 @@ func main() { time.Duration(cfg.Detector.MinDurationMs)*time.Millisecond, time.Duration(cfg.Detector.HoldMs)*time.Millisecond, cfg.Detector.EmaAlpha, - cfg.Detector.HysteresisDb) + cfg.Detector.HysteresisDb, + cfg.Detector.MinStableFrames, + time.Duration(cfg.Detector.GapToleranceMs)*time.Millisecond) window := fftutil.Hann(cfg.FFTSize) h := newHub() @@ -429,6 +431,10 @@ func main() { detChanged := prev.Detector.ThresholdDb != next.Detector.ThresholdDb || prev.Detector.MinDurationMs != next.Detector.MinDurationMs || prev.Detector.HoldMs != next.Detector.HoldMs || + prev.Detector.EmaAlpha != next.Detector.EmaAlpha || + prev.Detector.HysteresisDb != next.Detector.HysteresisDb || + prev.Detector.MinStableFrames != next.Detector.MinStableFrames || + prev.Detector.GapToleranceMs != next.Detector.GapToleranceMs || prev.SampleRate != next.SampleRate || prev.FFTSize != next.FFTSize windowChanged := prev.FFTSize != next.FFTSize @@ -439,7 +445,9 @@ func main() { time.Duration(next.Detector.MinDurationMs)*time.Millisecond, time.Duration(next.Detector.HoldMs)*time.Millisecond, next.Detector.EmaAlpha, - next.Detector.HysteresisDb) + next.Detector.HysteresisDb, + next.Detector.MinStableFrames, + time.Duration(next.Detector.GapToleranceMs)*time.Millisecond) } if windowChanged { newWindow = fftutil.Hann(next.FFTSize) diff --git a/config.yaml b/config.yaml index 76ebef3..ee504cb 100644 --- a/config.yaml +++ b/config.yaml @@ -17,6 +17,8 @@ detector: hold_ms: 500 ema_alpha: 0.2 hysteresis_db: 3 + min_stable_frames: 3 + gap_tolerance_ms: 500 recorder: enabled: true min_snr_db: 10 diff --git a/internal/config/config.go b/internal/config/config.go index d51ad23..eb0ac21 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -14,11 +14,13 @@ type Band struct { } type DetectorConfig struct { - ThresholdDb float64 `yaml:"threshold_db" json:"threshold_db"` - MinDurationMs int `yaml:"min_duration_ms" json:"min_duration_ms"` - HoldMs int `yaml:"hold_ms" json:"hold_ms"` - EmaAlpha float64 `yaml:"ema_alpha" json:"ema_alpha"` - HysteresisDb float64 `yaml:"hysteresis_db" json:"hysteresis_db"` + ThresholdDb float64 `yaml:"threshold_db" json:"threshold_db"` + MinDurationMs int `yaml:"min_duration_ms" json:"min_duration_ms"` + HoldMs int `yaml:"hold_ms" json:"hold_ms"` + EmaAlpha float64 `yaml:"ema_alpha" json:"ema_alpha"` + HysteresisDb float64 `yaml:"hysteresis_db" json:"hysteresis_db"` + MinStableFrames int `yaml:"min_stable_frames" json:"min_stable_frames"` + GapToleranceMs int `yaml:"gap_tolerance_ms" json:"gap_tolerance_ms"` } type RecorderConfig struct { @@ -81,7 +83,7 @@ func Default() Config { AGC: false, DCBlock: false, IQBalance: false, - Detector: DetectorConfig{ThresholdDb: -20, MinDurationMs: 250, HoldMs: 500, EmaAlpha: 0.2, HysteresisDb: 3}, + Detector: DetectorConfig{ThresholdDb: -20, MinDurationMs: 250, HoldMs: 500, EmaAlpha: 0.2, HysteresisDb: 3, MinStableFrames: 3, GapToleranceMs: 500}, Recorder: RecorderConfig{ Enabled: false, MinSNRDb: 10, @@ -120,6 +122,12 @@ func Load(path string) (Config, error) { if cfg.Detector.HoldMs <= 0 { cfg.Detector.HoldMs = 500 } + if cfg.Detector.MinStableFrames <= 0 { + cfg.Detector.MinStableFrames = 3 + } + if cfg.Detector.GapToleranceMs <= 0 { + cfg.Detector.GapToleranceMs = cfg.Detector.HoldMs + } if cfg.FrameRate <= 0 { cfg.FrameRate = 15 } diff --git a/internal/detector/detector.go b/internal/detector/detector.go index cbca7b5..bc74f56 100644 --- a/internal/detector/detector.go +++ b/internal/detector/detector.go @@ -22,11 +22,13 @@ type Event struct { } type Detector struct { - ThresholdDb float64 - MinDuration time.Duration - Hold time.Duration - EmaAlpha float64 - HysteresisDb float64 + ThresholdDb float64 + MinDuration time.Duration + Hold time.Duration + EmaAlpha float64 + HysteresisDb float64 + MinStableFrames int + GapTolerance time.Duration binWidth float64 nbins int @@ -38,16 +40,17 @@ type Detector struct { } type activeEvent struct { - id int64 - start time.Time - lastSeen time.Time - centerHz float64 - bwHz float64 - peakDb float64 - snrDb float64 - firstBin int - lastBin int - class *classifier.Classification + id int64 + start time.Time + lastSeen time.Time + centerHz float64 + bwHz float64 + peakDb float64 + snrDb float64 + firstBin int + lastBin int + class *classifier.Classification + stableHits int } type Signal struct { @@ -60,7 +63,7 @@ type Signal struct { Class *classifier.Classification `json:"class,omitempty"` } -func New(thresholdDb float64, sampleRate int, fftSize int, minDur, hold time.Duration, emaAlpha, hysteresis float64) *Detector { +func New(thresholdDb float64, sampleRate int, fftSize int, minDur, hold time.Duration, emaAlpha, hysteresis float64, minStable int, gapTolerance time.Duration) *Detector { if minDur <= 0 { minDur = 250 * time.Millisecond } @@ -73,18 +76,26 @@ func New(thresholdDb float64, sampleRate int, fftSize int, minDur, hold time.Dur if hysteresis <= 0 { hysteresis = 3 } + if minStable <= 0 { + minStable = 3 + } + if gapTolerance <= 0 { + gapTolerance = hold + } return &Detector{ - ThresholdDb: thresholdDb, - MinDuration: minDur, - Hold: hold, - EmaAlpha: emaAlpha, - HysteresisDb: hysteresis, - binWidth: float64(sampleRate) / float64(fftSize), - nbins: fftSize, - sampleRate: sampleRate, - ema: make([]float64, fftSize), - active: map[int64]*activeEvent{}, - nextID: 1, + ThresholdDb: thresholdDb, + MinDuration: minDur, + Hold: hold, + EmaAlpha: emaAlpha, + HysteresisDb: hysteresis, + MinStableFrames: minStable, + GapTolerance: gapTolerance, + binWidth: float64(sampleRate) / float64(fftSize), + nbins: fftSize, + sampleRate: sampleRate, + ema: make([]float64, fftSize), + active: map[int64]*activeEvent{}, + nextID: 1, } } @@ -199,21 +210,23 @@ func (d *Detector) matchSignals(now time.Time, signals []Signal) []Event { id := d.nextID d.nextID++ d.active[id] = &activeEvent{ - id: id, - start: now, - lastSeen: now, - centerHz: s.CenterHz, - bwHz: s.BWHz, - peakDb: s.PeakDb, - snrDb: s.SNRDb, - firstBin: s.FirstBin, - lastBin: s.LastBin, - class: s.Class, + id: id, + start: now, + lastSeen: now, + centerHz: s.CenterHz, + bwHz: s.BWHz, + peakDb: s.PeakDb, + snrDb: s.SNRDb, + firstBin: s.FirstBin, + lastBin: s.LastBin, + class: s.Class, + stableHits: 1, } continue } used[best.id] = true best.lastSeen = now + best.stableHits++ best.centerHz = (best.centerHz + s.CenterHz) / 2.0 if s.BWHz > best.bwHz { best.bwHz = s.BWHz @@ -242,11 +255,11 @@ func (d *Detector) matchSignals(now time.Time, signals []Signal) []Event { if used[id] { continue } - if now.Sub(ev.lastSeen) < d.Hold { + if now.Sub(ev.lastSeen) < d.GapTolerance { continue } duration := ev.lastSeen.Sub(ev.start) - if duration < d.MinDuration { + if duration < d.MinDuration || ev.stableHits < d.MinStableFrames { delete(d.active, id) continue } diff --git a/internal/detector/detector_test.go b/internal/detector/detector_test.go index 563d2da..6fae075 100644 --- a/internal/detector/detector_test.go +++ b/internal/detector/detector_test.go @@ -6,7 +6,7 @@ import ( ) func TestDetectorCreatesEvent(t *testing.T) { - d := New(-10, 1000, 10, 1*time.Millisecond, 10*time.Millisecond, 0.2, 3) + d := New(-10, 1000, 10, 1*time.Millisecond, 10*time.Millisecond, 0.2, 3, 1, 10*time.Millisecond) center := 0.0 spectrum := []float64{-30, -30, -30, -5, -5, -30, -30, -30, -30, -30} now := time.Now() diff --git a/internal/runtime/runtime.go b/internal/runtime/runtime.go index 9d3fa84..93b1844 100644 --- a/internal/runtime/runtime.go +++ b/internal/runtime/runtime.go @@ -19,11 +19,13 @@ type ConfigUpdate struct { } type DetectorUpdate struct { - ThresholdDb *float64 `json:"threshold_db"` - MinDuration *int `json:"min_duration_ms"` - HoldMs *int `json:"hold_ms"` - EmaAlpha *float64 `json:"ema_alpha"` - HysteresisDb *float64 `json:"hysteresis_db"` + ThresholdDb *float64 `json:"threshold_db"` + MinDuration *int `json:"min_duration_ms"` + HoldMs *int `json:"hold_ms"` + EmaAlpha *float64 `json:"ema_alpha"` + HysteresisDb *float64 `json:"hysteresis_db"` + MinStableFrames *int `json:"min_stable_frames"` + GapToleranceMs *int `json:"gap_tolerance_ms"` } type SettingsUpdate struct { @@ -129,6 +131,12 @@ func (m *Manager) ApplyConfig(update ConfigUpdate) (config.Config, error) { if update.Detector.HysteresisDb != nil { next.Detector.HysteresisDb = *update.Detector.HysteresisDb } + if update.Detector.MinStableFrames != nil { + next.Detector.MinStableFrames = *update.Detector.MinStableFrames + } + if update.Detector.GapToleranceMs != nil { + next.Detector.GapToleranceMs = *update.Detector.GapToleranceMs + } } if update.Recorder != nil { if update.Recorder.Enabled != nil { diff --git a/web/app.js b/web/app.js index 28f8447..39f416d 100644 --- a/web/app.js +++ b/web/app.js @@ -29,6 +29,10 @@ const gainRange = qs('gainRange'); const gainInput = qs('gainInput'); const thresholdRange = qs('thresholdRange'); const thresholdInput = qs('thresholdInput'); +const emaAlphaInput = qs('emaAlphaInput'); +const hysteresisInput = qs('hysteresisInput'); +const stableFramesInput = qs('stableFramesInput'); +const gapToleranceInput = qs('gapToleranceInput'); const agcToggle = qs('agcToggle'); const dcToggle = qs('dcToggle'); const iqToggle = qs('iqToggle'); @@ -278,6 +282,12 @@ function applyConfigToUI(cfg) { gainInput.value = uiGain; thresholdRange.value = cfg.detector.threshold_db; thresholdInput.value = cfg.detector.threshold_db; + if (minDurationInput) minDurationInput.value = cfg.detector.min_duration_ms; + if (holdInput) holdInput.value = cfg.detector.hold_ms; + if (emaAlphaInput) emaAlphaInput.value = cfg.detector.ema_alpha ?? 0.2; + if (hysteresisInput) hysteresisInput.value = cfg.detector.hysteresis_db ?? 3; + if (stableFramesInput) stableFramesInput.value = cfg.detector.min_stable_frames ?? 3; + if (gapToleranceInput) gapToleranceInput.value = cfg.detector.gap_tolerance_ms ?? cfg.detector.hold_ms; agcToggle.checked = !!cfg.agc; dcToggle.checked = !!cfg.dc_block; iqToggle.checked = !!cfg.iq_balance; diff --git a/web/index.html b/web/index.html index 65ce04a..d723ac0 100644 --- a/web/index.html +++ b/web/index.html @@ -173,12 +173,18 @@ Threshold
dB
+ + + + + +