Explorar el Código

feat: migrate CFAR guard and train settings to Hz

master
Jan Svabenik hace 2 días
padre
commit
8886b3b00c
Se han modificado 7 ficheros con 113 adiciones y 26 borrados
  1. +2
    -2
      cmd/sdrd/http_handlers.go
  2. +18
    -2
      internal/config/config.go
  3. +9
    -8
      internal/detector/detector.go
  4. +55
    -0
      internal/detector/detector_test.go
  5. +14
    -0
      internal/runtime/runtime.go
  6. +13
    -12
      web/app.js
  7. +2
    -2
      web/index.html

+ 2
- 2
cmd/sdrd/http_handlers.go Ver fichero

@@ -57,8 +57,8 @@ func registerAPIHandlers(mux *http.ServeMux, cfgPath string, cfgManager *runtime
prev.Detector.MinStableFrames != next.Detector.MinStableFrames ||
prev.Detector.GapToleranceMs != next.Detector.GapToleranceMs ||
prev.Detector.CFARMode != next.Detector.CFARMode ||
prev.Detector.CFARGuardCells != next.Detector.CFARGuardCells ||
prev.Detector.CFARTrainCells != next.Detector.CFARTrainCells ||
prev.Detector.CFARGuardHz != next.Detector.CFARGuardHz ||
prev.Detector.CFARTrainHz != next.Detector.CFARTrainHz ||
prev.Detector.CFARRank != next.Detector.CFARRank ||
prev.Detector.CFARScaleDb != next.Detector.CFARScaleDb ||
prev.Detector.CFARWrapAround != next.Detector.CFARWrapAround ||


+ 18
- 2
internal/config/config.go Ver fichero

@@ -23,8 +23,10 @@ type DetectorConfig struct {
MinStableFrames int `yaml:"min_stable_frames" json:"min_stable_frames"`
GapToleranceMs int `yaml:"gap_tolerance_ms" json:"gap_tolerance_ms"`
CFARMode string `yaml:"cfar_mode" json:"cfar_mode"`
CFARGuardCells int `yaml:"cfar_guard_cells" json:"cfar_guard_cells"`
CFARTrainCells int `yaml:"cfar_train_cells" json:"cfar_train_cells"`
CFARGuardHz float64 `yaml:"cfar_guard_hz" json:"cfar_guard_hz"`
CFARTrainHz float64 `yaml:"cfar_train_hz" json:"cfar_train_hz"`
CFARGuardCells int `yaml:"cfar_guard_cells,omitempty" json:"cfar_guard_cells,omitempty"`
CFARTrainCells int `yaml:"cfar_train_cells,omitempty" json:"cfar_train_cells,omitempty"`
CFARRank int `yaml:"cfar_rank" json:"cfar_rank"`
CFARScaleDb float64 `yaml:"cfar_scale_db" json:"cfar_scale_db"`
CFARWrapAround bool `yaml:"cfar_wrap_around" json:"cfar_wrap_around"`
@@ -105,6 +107,8 @@ func Default() Config {
MinStableFrames: 3,
GapToleranceMs: 500,
CFARMode: "GOSCA",
CFARGuardHz: 500,
CFARTrainHz: 5000,
CFARGuardCells: 3,
CFARTrainCells: 24,
CFARRank: 36,
@@ -178,6 +182,18 @@ func applyDefaults(cfg Config) Config {
cfg.Detector.CFARMode = "GOSCA"
}
}
if cfg.Detector.CFARGuardHz <= 0 && cfg.Detector.CFARGuardCells > 0 {
cfg.Detector.CFARGuardHz = float64(cfg.Detector.CFARGuardCells) * 62.5
}
if cfg.Detector.CFARTrainHz <= 0 && cfg.Detector.CFARTrainCells > 0 {
cfg.Detector.CFARTrainHz = float64(cfg.Detector.CFARTrainCells) * 62.5
}
if cfg.Detector.CFARGuardHz <= 0 {
cfg.Detector.CFARGuardHz = 500
}
if cfg.Detector.CFARTrainHz <= 0 {
cfg.Detector.CFARTrainHz = 5000
}
if cfg.Detector.CFARGuardCells <= 0 {
cfg.Detector.CFARGuardCells = 3
}


+ 9
- 8
internal/detector/detector.go Ver fichero

@@ -80,8 +80,15 @@ func New(detCfg config.DetectorConfig, sampleRate int, fftSize int) *Detector {
hysteresis := detCfg.HysteresisDb
minStable := detCfg.MinStableFrames
cfarMode := detCfg.CFARMode
cfarGuard := detCfg.CFARGuardCells
cfarTrain := detCfg.CFARTrainCells
binWidth := float64(sampleRate) / float64(fftSize)
cfarGuard := int(math.Ceil(detCfg.CFARGuardHz / binWidth))
if cfarGuard < 0 {
cfarGuard = 0
}
cfarTrain := int(math.Ceil(detCfg.CFARTrainHz / binWidth))
if cfarTrain < 1 {
cfarTrain = 1
}
cfarRank := detCfg.CFARRank
cfarScaleDb := detCfg.CFARScaleDb
cfarWrap := detCfg.CFARWrapAround
@@ -108,12 +115,6 @@ func New(detCfg config.DetectorConfig, sampleRate int, fftSize int) *Detector {
if gapTolerance <= 0 {
gapTolerance = hold
}
if cfarGuard < 0 {
cfarGuard = 2
}
if cfarTrain <= 0 {
cfarTrain = 16
}
if cfarScaleDb <= 0 {
cfarScaleDb = 6
}


+ 55
- 0
internal/detector/detector_test.go Ver fichero

@@ -137,3 +137,58 @@ func TestMergeGap(t *testing.T) {
t.Errorf("merged BW too narrow: %.0f Hz", signals[0].BWHz)
}
}

func TestGuardTrainHzScaling(t *testing.T) {
sampleRate := 2048000
guardHz := 500.0
trainHz := 5000.0
cfg := config.DetectorConfig{
ThresholdDb: -20,
MinDurationMs: 1,
HoldMs: 10,
EmaAlpha: 1.0,
HysteresisDb: 3,
MinStableFrames: 1,
GapToleranceMs: 10,
CFARMode: "OFF",
CFARGuardHz: guardHz,
CFARTrainHz: trainHz,
CFARScaleDb: 6,
CFARWrapAround: true,
}
d1 := New(cfg, sampleRate, 2048)
d2 := New(cfg, sampleRate, 65536)
spec2048 := makeSignalSpectrum(2048, sampleRate, 500e3, 12e3, -20, -100)
spec65536 := makeSignalSpectrum(65536, sampleRate, 500e3, 12e3, -20, -100)
now := time.Now()
_, sig1 := d1.Process(now, spec2048, 434e6)
_, sig2 := d2.Process(now, spec65536, 434e6)
if len(sig1) == 0 || len(sig2) == 0 {
t.Fatalf("detection failed: fft2048=%d signals, fft65536=%d signals", len(sig1), len(sig2))
}
bw1 := sig1[0].BWHz
bw2 := sig2[0].BWHz
ratio := bw1 / bw2
if ratio < 0.8 || ratio > 1.2 {
t.Errorf("BW mismatch across FFT sizes: fft2048=%.0f Hz, fft65536=%.0f Hz", bw1, bw2)
}
}

func makeSignalSpectrum(fftSize int, sampleRate int, offsetHz float64, bwHz float64, signalDb float64, noiseDb float64) []float64 {
spectrum := make([]float64, fftSize)
for i := range spectrum {
spectrum[i] = noiseDb
}
binWidth := float64(sampleRate) / float64(fftSize)
centerBin := int(float64(fftSize)/2 + offsetHz/binWidth)
halfBins := int((bwHz / 2) / binWidth)
if halfBins < 1 {
halfBins = 1
}
for i := centerBin - halfBins; i <= centerBin+halfBins; i++ {
if i >= 0 && i < fftSize {
spectrum[i] = signalDb
}
}
return spectrum
}

+ 14
- 0
internal/runtime/runtime.go Ver fichero

@@ -29,6 +29,8 @@ type DetectorUpdate struct {
MinStableFrames *int `json:"min_stable_frames"`
GapToleranceMs *int `json:"gap_tolerance_ms"`
CFARMode *string `json:"cfar_mode"`
CFARGuardHz *float64 `json:"cfar_guard_hz"`
CFARTrainHz *float64 `json:"cfar_train_hz"`
CFARGuardCells *int `json:"cfar_guard_cells"`
CFARTrainCells *int `json:"cfar_train_cells"`
CFARRank *int `json:"cfar_rank"`
@@ -173,6 +175,18 @@ func (m *Manager) ApplyConfig(update ConfigUpdate) (config.Config, error) {
if update.Detector.CFARWrapAround != nil {
next.Detector.CFARWrapAround = *update.Detector.CFARWrapAround
}
if update.Detector.CFARGuardHz != nil {
if *update.Detector.CFARGuardHz < 0 {
return m.cfg, errors.New("cfar_guard_hz must be >= 0")
}
next.Detector.CFARGuardHz = *update.Detector.CFARGuardHz
}
if update.Detector.CFARTrainHz != nil {
if *update.Detector.CFARTrainHz <= 0 {
return m.cfg, errors.New("cfar_train_hz must be > 0")
}
next.Detector.CFARTrainHz = *update.Detector.CFARTrainHz
}
if update.Detector.CFARGuardCells != nil {
if *update.Detector.CFARGuardCells < 0 {
return m.cfg, errors.New("cfar_guard_cells must be >= 0")


+ 13
- 12
web/app.js Ver fichero

@@ -32,8 +32,8 @@ const thresholdRange = qs('thresholdRange');
const thresholdInput = qs('thresholdInput');
const cfarModeSelect = qs('cfarModeSelect');
const cfarWrapToggle = qs('cfarWrapToggle');
const cfarGuardInput = qs('cfarGuardInput');
const cfarTrainInput = qs('cfarTrainInput');
const cfarGuardHzInput = qs('cfarGuardHzInput');
const cfarTrainHzInput = qs('cfarTrainHzInput');
const cfarRankInput = qs('cfarRankInput');
const cfarScaleInput = qs('cfarScaleInput');
const minDurationInput = qs('minDurationInput');
@@ -408,8 +408,8 @@ function applyConfigToUI(cfg) {
thresholdInput.value = cfg.detector.threshold_db;
if (cfarModeSelect) cfarModeSelect.value = cfg.detector.cfar_mode || 'OFF';
if (cfarWrapToggle) cfarWrapToggle.checked = cfg.detector.cfar_wrap_around !== false;
if (cfarGuardInput) cfarGuardInput.value = cfg.detector.cfar_guard_cells ?? 2;
if (cfarTrainInput) cfarTrainInput.value = cfg.detector.cfar_train_cells ?? 16;
if (cfarGuardHzInput) cfarGuardHzInput.value = cfg.detector.cfar_guard_hz ?? 500;
if (cfarTrainHzInput) cfarTrainHzInput.value = cfg.detector.cfar_train_hz ?? 5000;
if (cfarRankInput) cfarRankInput.value = cfg.detector.cfar_rank ?? 24;
if (cfarScaleInput) cfarScaleInput.value = cfg.detector.cfar_scale_db ?? 6;
const rankRow = cfarRankInput?.closest('.field');
@@ -651,8 +651,9 @@ function drawCfarEdgeOverlay(ctx, w, h, startHz, endHz) {
const mode = currentConfig?.detector?.cfar_mode || 'OFF';
if (mode === 'OFF') return;
if (currentConfig?.detector?.cfar_wrap_around) return;
const guard = currentConfig.detector.cfar_guard_cells ?? 0;
const train = currentConfig.detector.cfar_train_cells ?? 0;
const binWidth = (currentConfig.sample_rate || 2048000) / (latest.fft_size || latest.spectrum_db?.length || 32768);
const guard = Math.ceil((currentConfig.detector.cfar_guard_hz ?? 500) / binWidth);
const train = Math.ceil((currentConfig.detector.cfar_train_hz ?? 5000) / binWidth);
const bins = guard + train;
if (bins <= 0) return;
const fftSize = latest.fft_size || latest.spectrum_db?.length;
@@ -1380,13 +1381,13 @@ if (cfarModeSelect) cfarModeSelect.addEventListener('change', () => {
if (cfarWrapToggle) cfarWrapToggle.addEventListener('change', () => {
queueConfigUpdate({ detector: { cfar_wrap_around: cfarWrapToggle.checked } });
});
if (cfarGuardInput) cfarGuardInput.addEventListener('change', () => {
const v = parseInt(cfarGuardInput.value, 10);
if (Number.isFinite(v)) queueConfigUpdate({ detector: { cfar_guard_cells: v } });
if (cfarGuardHzInput) cfarGuardHzInput.addEventListener('change', () => {
const v = parseFloat(cfarGuardHzInput.value);
if (Number.isFinite(v) && v >= 0) queueConfigUpdate({ detector: { cfar_guard_hz: v } });
});
if (cfarTrainInput) cfarTrainInput.addEventListener('change', () => {
const v = parseInt(cfarTrainInput.value, 10);
if (Number.isFinite(v)) queueConfigUpdate({ detector: { cfar_train_cells: v } });
if (cfarTrainHzInput) cfarTrainHzInput.addEventListener('change', () => {
const v = parseFloat(cfarTrainHzInput.value);
if (Number.isFinite(v) && v > 0) queueConfigUpdate({ detector: { cfar_train_hz: v } });
});
if (cfarRankInput) cfarRankInput.addEventListener('change', () => {
const v = parseInt(cfarRankInput.value, 10);


+ 2
- 2
web/index.html Ver fichero

@@ -187,8 +187,8 @@
<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>CFAR Guard Cells</span><input id="cfarGuardInput" type="number" step="1" min="0" /></label>
<label class="field"><span>CFAR Train Cells</span><input id="cfarTrainInput" type="number" step="1" min="1" /></label>
<label class="field"><span>CFAR Guard (Hz)</span><input id="cfarGuardHzInput" type="number" step="100" min="0" /></label>
<label class="field"><span>CFAR Train (Hz)</span><input id="cfarTrainHzInput" type="number" step="500" min="100" /></label>
<label class="field"><span>CFAR Rank</span><input id="cfarRankInput" type="number" step="1" min="1" /></label>
<label class="field"><span>CFAR Scale (dB)</span><input id="cfarScaleInput" type="number" step="0.5" min="0" /></label>
<label class="field"><span>Min Duration (ms)</span><input id="minDurationInput" type="number" step="50" min="50" /></label>


Cargando…
Cancelar
Guardar