瀏覽代碼

feat: expose and render CFAR thresholds

master
Jan Svabenik 2 天之前
父節點
當前提交
66f7b1d682
共有 3 個文件被更改,包括 78 次插入19 次删除
  1. +18
    -12
      cmd/sdrd/main.go
  2. +24
    -7
      internal/detector/detector.go
  3. +36
    -0
      web/app.js

+ 18
- 12
cmd/sdrd/main.go 查看文件

@@ -35,12 +35,14 @@ import (
)

type SpectrumFrame struct {
Timestamp int64 `json:"ts"`
CenterHz float64 `json:"center_hz"`
SampleHz int `json:"sample_rate"`
FFTSize int `json:"fft_size"`
Spectrum []float64 `json:"spectrum_db"`
Signals []detector.Signal `json:"signals"`
Timestamp int64 `json:"ts"`
CenterHz float64 `json:"center_hz"`
SampleHz int `json:"sample_rate"`
FFTSize int `json:"fft_size"`
Spectrum []float64 `json:"spectrum_db"`
Thresholds []float64 `json:"thresholds,omitempty"`
NoiseFloor float64 `json:"noise_floor,omitempty"`
Signals []detector.Signal `json:"signals"`
}

type client struct {
@@ -837,6 +839,8 @@ func runDSP(ctx context.Context, srcMgr *sourceManager, cfg config.Config, det *
}
now := time.Now()
finished, signals := det.Process(now, spectrum, cfg.CenterHz)
thresholds := det.LastThresholds()
noiseFloor := det.LastNoiseFloor()
// enrich classification with temporal IQ features on per-signal snippet
if len(iq) > 0 {
for i := range signals {
@@ -860,12 +864,14 @@ func runDSP(ctx context.Context, srcMgr *sourceManager, cfg config.Config, det *
go rec.OnEvents(evCopy)
}
h.broadcast(SpectrumFrame{
Timestamp: now.UnixMilli(),
CenterHz: cfg.CenterHz,
SampleHz: cfg.SampleRate,
FFTSize: cfg.FFTSize,
Spectrum: spectrum,
Signals: signals,
Timestamp: now.UnixMilli(),
CenterHz: cfg.CenterHz,
SampleHz: cfg.SampleRate,
FFTSize: cfg.FFTSize,
Spectrum: spectrum,
Thresholds: thresholds,
NoiseFloor: noiseFloor,
Signals: signals,
})
}
}


+ 24
- 7
internal/detector/detector.go 查看文件

@@ -31,14 +31,16 @@ type Detector struct {
MinStableFrames int
GapTolerance time.Duration
CFARScaleDb float64
binWidth float64
nbins int
sampleRate int
binWidth float64
nbins int
sampleRate int

ema []float64
active map[int64]*activeEvent
nextID int64
cfarEngine cfar.CFAR
ema []float64
active map[int64]*activeEvent
nextID int64
cfarEngine cfar.CFAR
lastThresholds []float64
lastNoiseFloor float64
}

type activeEvent struct {
@@ -62,6 +64,7 @@ type Signal struct {
BWHz float64 `json:"bw_hz"`
PeakDb float64 `json:"peak_db"`
SNRDb float64 `json:"snr_db"`
NoiseDb float64 `json:"noise_db,omitempty"`
Class *classifier.Classification `json:"class,omitempty"`
}

@@ -135,6 +138,17 @@ func (d *Detector) Process(now time.Time, spectrum []float64, centerHz float64)
return finished, signals
}

func (d *Detector) LastThresholds() []float64 {
if len(d.lastThresholds) == 0 {
return nil
}
return append([]float64(nil), d.lastThresholds...)
}

func (d *Detector) LastNoiseFloor() float64 {
return d.lastNoiseFloor
}

// UpdateClasses refreshes active event classes from current signals.
func (d *Detector) UpdateClasses(signals []Signal) {
for _, s := range signals {
@@ -160,7 +174,9 @@ func (d *Detector) detectSignals(spectrum []float64, centerHz float64) []Signal
if d.cfarEngine != nil {
thresholds = d.cfarEngine.Thresholds(smooth)
}
d.lastThresholds = append(d.lastThresholds[:0], thresholds...)
noiseGlobal := median(smooth)
d.lastNoiseFloor = noiseGlobal
var signals []Signal
in := false
start := 0
@@ -214,6 +230,7 @@ func (d *Detector) makeSignal(first, last int, peak float64, peakBin int, noise
BWHz: bw,
PeakDb: peak,
SNRDb: snr,
NoiseDb: noise,
}
}



+ 36
- 0
web/app.js 查看文件

@@ -211,6 +211,41 @@ function maxInBinRange(spectrum, b0, b1) {
return max;
}

function sampleOverlayAtX(overlay, x, width, centerHz, sampleRate) {
if (!Array.isArray(overlay) || overlay.length === 0 || width <= 0) return null;
const n = overlay.length;
const span = sampleRate / zoom;
const startHz = centerHz - span / 2 + pan * span;
const endHz = centerHz + span / 2 + pan * span;
const f1 = startHz + (x / width) * (endHz - startHz);
const f2 = startHz + ((x + 1) / width) * (endHz - startHz);
const b0 = binForFreq(f1, centerHz, sampleRate, n);
const b1 = binForFreq(f2, centerHz, sampleRate, n);
return maxInBinRange(overlay, b0, b1);
}

function drawThresholdOverlay(ctx, w, h, minDb, maxDb) {
if (!latest?.thresholds?.length) return;
ctx.save();
ctx.strokeStyle = 'rgba(255, 196, 92, 0.9)';
ctx.lineWidth = 1.25;
if (ctx.setLineDash) ctx.setLineDash([6, 4]);
ctx.beginPath();
for (let x = 0; x < w; x++) {
const v = sampleOverlayAtX(latest.thresholds, x, w, latest.center_hz, latest.sample_rate);
if (v == null || Number.isNaN(v)) continue;
const y = h - ((v - minDb) / (maxDb - minDb)) * (h - 18) - 6;
if (x === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
if (ctx.setLineDash) ctx.setLineDash([]);
ctx.fillStyle = 'rgba(255, 196, 92, 0.95)';
ctx.font = '11px Inter, sans-serif';
ctx.fillText('CFAR', 8, 14);
ctx.restore();
}

function markSpectrumDirty() {
processingDirty = true;
}
@@ -645,6 +680,7 @@ function renderSpectrum() {
else ctx.lineTo(x, y);
}
ctx.stroke();
drawThresholdOverlay(ctx, w, h, minDb, maxDb);

if (Array.isArray(latest.signals)) {
latest.signals.forEach((s, index) => {


Loading…
取消
儲存