Просмотр исходного кода

Add detector stability and gap tolerance controls

master
Jan Svabenik 3 дней назад
Родитель
Сommit
4332cc09aa
8 измененных файлов: 108 добавлений и 53 удалений
  1. +10
    -2
      cmd/sdrd/main.go
  2. +2
    -0
      config.yaml
  3. +14
    -6
      internal/config/config.go
  4. +52
    -39
      internal/detector/detector.go
  5. +1
    -1
      internal/detector/detector_test.go
  6. +13
    -5
      internal/runtime/runtime.go
  7. +10
    -0
      web/app.js
  8. +6
    -0
      web/index.html

+ 10
- 2
cmd/sdrd/main.go Просмотреть файл

@@ -316,7 +316,9 @@ func main() {
time.Duration(cfg.Detector.MinDurationMs)*time.Millisecond, time.Duration(cfg.Detector.MinDurationMs)*time.Millisecond,
time.Duration(cfg.Detector.HoldMs)*time.Millisecond, time.Duration(cfg.Detector.HoldMs)*time.Millisecond,
cfg.Detector.EmaAlpha, cfg.Detector.EmaAlpha,
cfg.Detector.HysteresisDb)
cfg.Detector.HysteresisDb,
cfg.Detector.MinStableFrames,
time.Duration(cfg.Detector.GapToleranceMs)*time.Millisecond)


window := fftutil.Hann(cfg.FFTSize) window := fftutil.Hann(cfg.FFTSize)
h := newHub() h := newHub()
@@ -429,6 +431,10 @@ func main() {
detChanged := prev.Detector.ThresholdDb != next.Detector.ThresholdDb || detChanged := prev.Detector.ThresholdDb != next.Detector.ThresholdDb ||
prev.Detector.MinDurationMs != next.Detector.MinDurationMs || prev.Detector.MinDurationMs != next.Detector.MinDurationMs ||
prev.Detector.HoldMs != next.Detector.HoldMs || 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.SampleRate != next.SampleRate ||
prev.FFTSize != next.FFTSize prev.FFTSize != next.FFTSize
windowChanged := 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.MinDurationMs)*time.Millisecond,
time.Duration(next.Detector.HoldMs)*time.Millisecond, time.Duration(next.Detector.HoldMs)*time.Millisecond,
next.Detector.EmaAlpha, next.Detector.EmaAlpha,
next.Detector.HysteresisDb)
next.Detector.HysteresisDb,
next.Detector.MinStableFrames,
time.Duration(next.Detector.GapToleranceMs)*time.Millisecond)
} }
if windowChanged { if windowChanged {
newWindow = fftutil.Hann(next.FFTSize) newWindow = fftutil.Hann(next.FFTSize)


+ 2
- 0
config.yaml Просмотреть файл

@@ -17,6 +17,8 @@ detector:
hold_ms: 500 hold_ms: 500
ema_alpha: 0.2 ema_alpha: 0.2
hysteresis_db: 3 hysteresis_db: 3
min_stable_frames: 3
gap_tolerance_ms: 500
recorder: recorder:
enabled: true enabled: true
min_snr_db: 10 min_snr_db: 10


+ 14
- 6
internal/config/config.go Просмотреть файл

@@ -14,11 +14,13 @@ type Band struct {
} }


type DetectorConfig 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 { type RecorderConfig struct {
@@ -81,7 +83,7 @@ func Default() Config {
AGC: false, AGC: false,
DCBlock: false, DCBlock: false,
IQBalance: 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{ Recorder: RecorderConfig{
Enabled: false, Enabled: false,
MinSNRDb: 10, MinSNRDb: 10,
@@ -120,6 +122,12 @@ func Load(path string) (Config, error) {
if cfg.Detector.HoldMs <= 0 { if cfg.Detector.HoldMs <= 0 {
cfg.Detector.HoldMs = 500 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 { if cfg.FrameRate <= 0 {
cfg.FrameRate = 15 cfg.FrameRate = 15
} }


+ 52
- 39
internal/detector/detector.go Просмотреть файл

@@ -22,11 +22,13 @@ type Event struct {
} }


type Detector 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 binWidth float64
nbins int nbins int
@@ -38,16 +40,17 @@ type Detector struct {
} }


type activeEvent 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 { type Signal struct {
@@ -60,7 +63,7 @@ type Signal struct {
Class *classifier.Classification `json:"class,omitempty"` 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 { if minDur <= 0 {
minDur = 250 * time.Millisecond minDur = 250 * time.Millisecond
} }
@@ -73,18 +76,26 @@ func New(thresholdDb float64, sampleRate int, fftSize int, minDur, hold time.Dur
if hysteresis <= 0 { if hysteresis <= 0 {
hysteresis = 3 hysteresis = 3
} }
if minStable <= 0 {
minStable = 3
}
if gapTolerance <= 0 {
gapTolerance = hold
}
return &Detector{ 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 id := d.nextID
d.nextID++ d.nextID++
d.active[id] = &activeEvent{ 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 continue
} }
used[best.id] = true used[best.id] = true
best.lastSeen = now best.lastSeen = now
best.stableHits++
best.centerHz = (best.centerHz + s.CenterHz) / 2.0 best.centerHz = (best.centerHz + s.CenterHz) / 2.0
if s.BWHz > best.bwHz { if s.BWHz > best.bwHz {
best.bwHz = s.BWHz best.bwHz = s.BWHz
@@ -242,11 +255,11 @@ func (d *Detector) matchSignals(now time.Time, signals []Signal) []Event {
if used[id] { if used[id] {
continue continue
} }
if now.Sub(ev.lastSeen) < d.Hold {
if now.Sub(ev.lastSeen) < d.GapTolerance {
continue continue
} }
duration := ev.lastSeen.Sub(ev.start) duration := ev.lastSeen.Sub(ev.start)
if duration < d.MinDuration {
if duration < d.MinDuration || ev.stableHits < d.MinStableFrames {
delete(d.active, id) delete(d.active, id)
continue continue
} }


+ 1
- 1
internal/detector/detector_test.go Просмотреть файл

@@ -6,7 +6,7 @@ import (
) )


func TestDetectorCreatesEvent(t *testing.T) { 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 center := 0.0
spectrum := []float64{-30, -30, -30, -5, -5, -30, -30, -30, -30, -30} spectrum := []float64{-30, -30, -30, -5, -5, -30, -30, -30, -30, -30}
now := time.Now() now := time.Now()


+ 13
- 5
internal/runtime/runtime.go Просмотреть файл

@@ -19,11 +19,13 @@ type ConfigUpdate struct {
} }


type DetectorUpdate 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 { type SettingsUpdate struct {
@@ -129,6 +131,12 @@ func (m *Manager) ApplyConfig(update ConfigUpdate) (config.Config, error) {
if update.Detector.HysteresisDb != nil { if update.Detector.HysteresisDb != nil {
next.Detector.HysteresisDb = *update.Detector.HysteresisDb 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 != nil {
if update.Recorder.Enabled != nil { if update.Recorder.Enabled != nil {


+ 10
- 0
web/app.js Просмотреть файл

@@ -29,6 +29,10 @@ const gainRange = qs('gainRange');
const gainInput = qs('gainInput'); const gainInput = qs('gainInput');
const thresholdRange = qs('thresholdRange'); const thresholdRange = qs('thresholdRange');
const thresholdInput = qs('thresholdInput'); 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 agcToggle = qs('agcToggle');
const dcToggle = qs('dcToggle'); const dcToggle = qs('dcToggle');
const iqToggle = qs('iqToggle'); const iqToggle = qs('iqToggle');
@@ -278,6 +282,12 @@ function applyConfigToUI(cfg) {
gainInput.value = uiGain; gainInput.value = uiGain;
thresholdRange.value = cfg.detector.threshold_db; thresholdRange.value = cfg.detector.threshold_db;
thresholdInput.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; agcToggle.checked = !!cfg.agc;
dcToggle.checked = !!cfg.dc_block; dcToggle.checked = !!cfg.dc_block;
iqToggle.checked = !!cfg.iq_balance; iqToggle.checked = !!cfg.iq_balance;


+ 6
- 0
web/index.html Просмотреть файл

@@ -173,12 +173,18 @@
<span>Threshold</span> <span>Threshold</span>
<div class="slider-row"><input id="thresholdRange" type="range" min="-120" max="0" step="1" class="range--warn" /><input id="thresholdInput" type="number" min="-120" max="0" step="1" class="slider-num" /><em>dB</em></div> <div class="slider-row"><input id="thresholdRange" type="range" min="-120" max="0" step="1" class="range--warn" /><input id="thresholdInput" type="number" min="-120" max="0" step="1" class="slider-num" /><em>dB</em></div>
</div> </div>
<label class="field"><span>Min Duration (ms)</span><input id="minDurationInput" type="number" step="50" min="50" /></label>
<label class="field"><span>Hold (ms)</span><input id="holdInput" type="number" step="50" min="50" /></label>
<label class="field"><span>Averaging</span> <label class="field"><span>Averaging</span>
<select id="avgSelect"> <select id="avgSelect">
<option value="0">Off</option><option value="0.4">Fast</option> <option value="0">Off</option><option value="0.4">Fast</option>
<option value="0.2">Medium</option><option value="0.1">Slow</option> <option value="0.2">Medium</option><option value="0.1">Slow</option>
</select> </select>
</label> </label>
<label class="field"><span>EMA Alpha</span><input id="emaAlphaInput" type="number" step="0.05" min="0" max="1" /></label>
<label class="field"><span>Hysteresis (dB)</span><input id="hysteresisInput" type="number" step="1" min="0" /></label>
<label class="field"><span>Min Stable Frames</span><input id="stableFramesInput" type="number" step="1" min="1" /></label>
<label class="field"><span>Gap Tolerance (ms)</span><input id="gapToleranceInput" type="number" step="50" min="0" /></label>
<div class="toggle-grid"> <div class="toggle-grid">
<label class="pill-toggle"><input id="agcToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">AGC</span></label> <label class="pill-toggle"><input id="agcToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">AGC</span></label>
<label class="pill-toggle"><input id="dcToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">DC Block</span></label> <label class="pill-toggle"><input id="dcToggle" type="checkbox" /><span class="pt"><span class="pk"></span></span><span class="pl">DC Block</span></label>


Загрузка…
Отмена
Сохранить