Quellcode durchsuchen

Add detector stability and gap tolerance controls

master
Jan Svabenik vor 3 Tagen
Ursprung
Commit
4332cc09aa
8 geänderte Dateien mit 108 neuen und 53 gelöschten Zeilen
  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 Datei anzeigen

@@ -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)


+ 2
- 0
config.yaml Datei anzeigen

@@ -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


+ 14
- 6
internal/config/config.go Datei anzeigen

@@ -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
}


+ 52
- 39
internal/detector/detector.go Datei anzeigen

@@ -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
}


+ 1
- 1
internal/detector/detector_test.go Datei anzeigen

@@ -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()


+ 13
- 5
internal/runtime/runtime.go Datei anzeigen

@@ -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 {


+ 10
- 0
web/app.js Datei anzeigen

@@ -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;


+ 6
- 0
web/index.html Datei anzeigen

@@ -173,12 +173,18 @@
<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>
<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>
<select id="avgSelect">
<option value="0">Off</option><option value="0.4">Fast</option>
<option value="0.2">Medium</option><option value="0.1">Slow</option>
</select>
</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">
<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>


Laden…
Abbrechen
Speichern