瀏覽代碼

Fix CFAR integration and detector scale

master
Jan Svabenik 3 天之前
父節點
當前提交
81c543ac09
共有 13 個文件被更改,包括 526 次插入120 次删除
  1. +8
    -5
      cmd/sdrd/main.go
  2. +72
    -0
      internal/cfar/ca.go
  3. +64
    -0
      internal/cfar/caso.go
  4. +27
    -0
      internal/cfar/cfar.go
  5. +73
    -0
      internal/cfar/cfar_test.go
  6. +72
    -0
      internal/cfar/gosca.go
  7. +101
    -0
      internal/cfar/os.go
  8. +30
    -0
      internal/cfar/types.go
  9. +33
    -4
      internal/config/config.go
  10. +22
    -103
      internal/detector/detector.go
  11. +1
    -1
      internal/detector/detector_test.go
  12. +14
    -3
      internal/runtime/runtime.go
  13. +9
    -4
      internal/runtime/runtime_test.go

+ 8
- 5
cmd/sdrd/main.go 查看文件

@@ -319,11 +319,12 @@ func main() {
cfg.Detector.HysteresisDb,
cfg.Detector.MinStableFrames,
time.Duration(cfg.Detector.GapToleranceMs)*time.Millisecond,
cfg.Detector.CFAREnabled,
cfg.Detector.CFARMode,
cfg.Detector.CFARGuardCells,
cfg.Detector.CFARTrainCells,
cfg.Detector.CFARRank,
cfg.Detector.CFARScaleDb)
cfg.Detector.CFARScaleDb,
cfg.Detector.CFARWrapAround)

window := fftutil.Hann(cfg.FFTSize)
h := newHub()
@@ -443,11 +444,12 @@ func main() {
prev.Detector.HysteresisDb != next.Detector.HysteresisDb ||
prev.Detector.MinStableFrames != next.Detector.MinStableFrames ||
prev.Detector.GapToleranceMs != next.Detector.GapToleranceMs ||
prev.Detector.CFAREnabled != next.Detector.CFAREnabled ||
prev.Detector.CFARMode != next.Detector.CFARMode ||
prev.Detector.CFARGuardCells != next.Detector.CFARGuardCells ||
prev.Detector.CFARTrainCells != next.Detector.CFARTrainCells ||
prev.Detector.CFARRank != next.Detector.CFARRank ||
prev.Detector.CFARScaleDb != next.Detector.CFARScaleDb ||
prev.Detector.CFARWrapAround != next.Detector.CFARWrapAround ||
prev.SampleRate != next.SampleRate ||
prev.FFTSize != next.FFTSize
windowChanged := prev.FFTSize != next.FFTSize
@@ -461,11 +463,12 @@ func main() {
next.Detector.HysteresisDb,
next.Detector.MinStableFrames,
time.Duration(next.Detector.GapToleranceMs)*time.Millisecond,
next.Detector.CFAREnabled,
next.Detector.CFARMode,
next.Detector.CFARGuardCells,
next.Detector.CFARTrainCells,
next.Detector.CFARRank,
next.Detector.CFARScaleDb)
next.Detector.CFARScaleDb,
next.Detector.CFARWrapAround)
}
if windowChanged {
newWindow = fftutil.Hann(next.FFTSize)


+ 72
- 0
internal/cfar/ca.go 查看文件

@@ -0,0 +1,72 @@
package cfar

// cellAvg implements CA-CFAR with a sliding sum window.
type cellAvg struct {
guard int
train int
scaleDb float64
wrapAround bool
}

func newCA(cfg Config) CFAR {
return &cellAvg{
guard: cfg.GuardCells,
train: cfg.TrainCells,
scaleDb: cfg.ScaleDb,
wrapAround: cfg.WrapAround,
}
}

func (c *cellAvg) Thresholds(spectrum []float64) []float64 {
n := len(spectrum)
if n == 0 {
return nil
}
out := make([]float64, n)
train := c.train
guard := c.guard
total := 2 * train
if total == 0 {
return out
}

at := func(i int) float64 {
if c.wrapAround {
return spectrum[((i%n)+n)%n]
}
if i < 0 || i >= n {
return spectrum[clampInt(i, 0, n-1)]
}
return spectrum[i]
}

var leftSum, rightSum float64
for k := 1; k <= train; k++ {
leftSum += at(0 - guard - k)
rightSum += at(0 + guard + k)
}

invN := 1.0 / float64(total)
out[0] = (leftSum+rightSum)*invN + c.scaleDb

for i := 1; i < n; i++ {
leftSum -= at(i - 1 - guard - train)
leftSum += at(i - guard - 1)

rightSum -= at(i - 1 + guard + 1)
rightSum += at(i + guard + train)

out[i] = (leftSum+rightSum)*invN + c.scaleDb
}
return out
}

func clampInt(v, lo, hi int) int {
if v < lo {
return lo
}
if v > hi {
return hi
}
return v
}

+ 64
- 0
internal/cfar/caso.go 查看文件

@@ -0,0 +1,64 @@
package cfar

type caso struct {
guard int
train int
scaleDb float64
wrapAround bool
}

func newCASO(cfg Config) CFAR {
return &caso{guard: cfg.GuardCells, train: cfg.TrainCells, scaleDb: cfg.ScaleDb, wrapAround: cfg.WrapAround}
}

func (c *caso) Thresholds(spectrum []float64) []float64 {
n := len(spectrum)
if n == 0 {
return nil
}
out := make([]float64, n)
train := c.train
guard := c.guard
if train == 0 {
return out
}
inv := 1.0 / float64(train)

at := func(i int) float64 {
if c.wrapAround {
return spectrum[((i%n)+n)%n]
}
if i < 0 || i >= n {
return spectrum[clampInt(i, 0, n-1)]
}
return spectrum[i]
}

var leftSum, rightSum float64
for k := 1; k <= train; k++ {
leftSum += at(0 - guard - k)
rightSum += at(0 + guard + k)
}
lm := leftSum * inv
rm := rightSum * inv
noise := lm
if rm < noise {
noise = rm
}
out[0] = noise + c.scaleDb

for i := 1; i < n; i++ {
leftSum -= at(i - 1 - guard - train)
leftSum += at(i - guard - 1)
rightSum -= at(i - 1 + guard + 1)
rightSum += at(i + guard + train)
lm = leftSum * inv
rm = rightSum * inv
noise = lm
if rm < noise {
noise = rm
}
out[i] = noise + c.scaleDb
}
return out
}

+ 27
- 0
internal/cfar/cfar.go 查看文件

@@ -0,0 +1,27 @@
package cfar

// New creates a CFAR detector for the given mode.
// Returns nil if mode is ModeOff or empty.
func New(cfg Config) CFAR {
if cfg.TrainCells <= 0 {
return nil
}
if cfg.GuardCells < 0 {
cfg.GuardCells = 0
}
if cfg.ScaleDb <= 0 {
cfg.ScaleDb = 6
}
switch cfg.Mode {
case ModeCA:
return newCA(cfg)
case ModeOS:
return newOS(cfg)
case ModeGOSCA:
return newGOSCA(cfg)
case ModeCASO:
return newCASO(cfg)
default:
return nil
}
}

+ 73
- 0
internal/cfar/cfar_test.go 查看文件

@@ -0,0 +1,73 @@
package cfar

import "testing"

func makeSpectrum(n int, noiseDb float64, signals [][2]int, sigDb float64) []float64 {
s := make([]float64, n)
for i := range s {
s[i] = noiseDb
}
for _, sig := range signals {
for i := sig[0]; i <= sig[1] && i < n; i++ {
s[i] = sigDb
}
}
return s
}

func TestAllVariantsDetectSignal(t *testing.T) {
spec := makeSpectrum(1024, -100, [][2]int{{500, 510}}, -20)
for _, mode := range []Mode{ModeCA, ModeOS, ModeGOSCA, ModeCASO} {
c := New(Config{Mode: mode, GuardCells: 2, TrainCells: 16, Rank: 24, ScaleDb: 6, WrapAround: true})
if c == nil {
t.Fatalf("%s: nil", mode)
}
th := c.Thresholds(spec)
if len(th) != 1024 {
t.Fatalf("%s: len=%d", mode, len(th))
}
if spec[505] < th[505] {
t.Fatalf("%s: signal not above threshold", mode)
}
if spec[200] >= th[200] {
t.Fatalf("%s: noise above threshold", mode)
}
}
}

func TestWrapAroundEdges(t *testing.T) {
spec := makeSpectrum(256, -100, [][2]int{{0, 5}}, -20)
c := New(Config{Mode: ModeCA, GuardCells: 2, TrainCells: 8, ScaleDb: 6, WrapAround: true})
th := c.Thresholds(spec)
if th[0] <= -200 || th[0] > 0 {
t.Fatalf("edge threshold bad: %v", th[0])
}
if th[255] <= -200 || th[255] > 0 {
t.Fatalf("wrap threshold bad: %v", th[255])
}
}

func TestGOSCAMaskingProtection(t *testing.T) {
spec := makeSpectrum(1024, -100, [][2]int{{500, 510}, {530, 540}}, -20)
cGosca := New(Config{Mode: ModeGOSCA, GuardCells: 2, TrainCells: 16, ScaleDb: 6, WrapAround: true})
cCA := New(Config{Mode: ModeCA, GuardCells: 2, TrainCells: 16, ScaleDb: 6, WrapAround: true})
thG := cGosca.Thresholds(spec)
thC := cCA.Thresholds(spec)
midBin := 520
if thG[midBin] < thC[midBin] {
t.Logf("GOSCA=%f CA=%f at bin %d — GOSCA correctly higher", thG[midBin], thC[midBin], midBin)
}
}

func BenchmarkCFAR(b *testing.B) {
spec := makeSpectrum(2048, -100, [][2]int{{500, 510}, {1000, 1020}}, -20)
for _, mode := range []Mode{ModeCA, ModeOS, ModeGOSCA, ModeCASO} {
cfg := Config{Mode: mode, GuardCells: 2, TrainCells: 16, Rank: 24, ScaleDb: 6, WrapAround: true}
c := New(cfg)
b.Run(string(mode), func(b *testing.B) {
for i := 0; i < b.N; i++ {
c.Thresholds(spec)
}
})
}
}

+ 72
- 0
internal/cfar/gosca.go 查看文件

@@ -0,0 +1,72 @@
package cfar

// gosca implements Greatest-Of Selection with Cell Averaging.
type gosca struct {
guard int
train int
scaleDb float64
wrapAround bool
}

func newGOSCA(cfg Config) CFAR {
return &gosca{
guard: cfg.GuardCells,
train: cfg.TrainCells,
scaleDb: cfg.ScaleDb,
wrapAround: cfg.WrapAround,
}
}

func (g *gosca) Thresholds(spectrum []float64) []float64 {
n := len(spectrum)
if n == 0 {
return nil
}
out := make([]float64, n)
train := g.train
guard := g.guard
if train == 0 {
return out
}
inv := 1.0 / float64(train)

at := func(i int) float64 {
if g.wrapAround {
return spectrum[((i%n)+n)%n]
}
if i < 0 || i >= n {
return spectrum[clampInt(i, 0, n-1)]
}
return spectrum[i]
}

var leftSum, rightSum float64
for k := 1; k <= train; k++ {
leftSum += at(0 - guard - k)
rightSum += at(0 + guard + k)
}

leftMean := leftSum * inv
rightMean := rightSum * inv
noise := leftMean
if rightMean > noise {
noise = rightMean
}
out[0] = noise + g.scaleDb

for i := 1; i < n; i++ {
leftSum -= at(i - 1 - guard - train)
leftSum += at(i - guard - 1)
rightSum -= at(i - 1 + guard + 1)
rightSum += at(i + guard + train)

leftMean = leftSum * inv
rightMean = rightSum * inv
noise = leftMean
if rightMean > noise {
noise = rightMean
}
out[i] = noise + g.scaleDb
}
return out
}

+ 101
- 0
internal/cfar/os.go 查看文件

@@ -0,0 +1,101 @@
package cfar

import "sort"

type orderedStat struct {
guard int
train int
rank int
scaleDb float64
wrapAround bool
}

func newOS(cfg Config) CFAR {
rank := cfg.Rank - 1
total := 2 * cfg.TrainCells
if rank < 0 {
rank = 0
}
if rank >= total {
rank = total - 1
}
return &orderedStat{
guard: cfg.GuardCells,
train: cfg.TrainCells,
rank: rank,
scaleDb: cfg.ScaleDb,
wrapAround: cfg.WrapAround,
}
}

func (o *orderedStat) Thresholds(spectrum []float64) []float64 {
n := len(spectrum)
if n == 0 {
return nil
}
out := make([]float64, n)
train := o.train
guard := o.guard

at := func(i int) float64 {
if o.wrapAround {
return spectrum[((i%n)+n)%n]
}
if i < 0 || i >= n {
return spectrum[clampInt(i, 0, n-1)]
}
return spectrum[i]
}

win := make([]float64, 0, 2*train)
for k := 1; k <= train; k++ {
win = append(win, at(0-guard-k))
win = append(win, at(0+guard+k))
}
sort.Float64s(win)
out[0] = win[o.rank] + o.scaleDb

for i := 1; i < n; i++ {
removeFromSorted(&win, at(i-1-guard-train))
removeFromSorted(&win, at(i-1+guard+1))

insertSorted(&win, at(i-guard-1))
insertSorted(&win, at(i+guard+train))

out[i] = win[o.rank] + o.scaleDb
}
return out
}

func insertSorted(s *[]float64, v float64) {
idx := sort.SearchFloat64s(*s, v)
*s = append(*s, 0)
copy((*s)[idx+1:], (*s)[idx:])
(*s)[idx] = v
}

func removeFromSorted(s *[]float64, v float64) {
idx := sort.SearchFloat64s(*s, v)
if idx < len(*s) && (*s)[idx] == v {
*s = append((*s)[:idx], (*s)[idx+1:]...)
return
}
for i := idx - 1; i >= 0; i-- {
if (*s)[i] == v {
*s = append((*s)[:i], (*s)[i+1:]...)
return
}
if (*s)[i] < v {
break
}
}
for i := idx + 1; i < len(*s); i++ {
if (*s)[i] == v {
*s = append((*s)[:i], (*s)[i+1:]...)
return
}
if (*s)[i] > v {
break
}
}
}

+ 30
- 0
internal/cfar/types.go 查看文件

@@ -0,0 +1,30 @@
package cfar

// Mode selects the CFAR algorithm variant.
type Mode string

const (
ModeOff Mode = "OFF"
ModeCA Mode = "CA"
ModeOS Mode = "OS"
ModeGOSCA Mode = "GOSCA"
ModeCASO Mode = "CASO"
)

// Config holds all CFAR parameters.
type Config struct {
Mode Mode
GuardCells int
TrainCells int
Rank int
ScaleDb float64
WrapAround bool
}

// CFAR computes adaptive thresholds for a spectrum.
type CFAR interface {
// Thresholds returns per-bin detection thresholds in dB.
// spectrum is power in dB, length n.
// Returned slice has length n. No NaN values.
Thresholds(spectrum []float64) []float64
}

+ 33
- 4
internal/config/config.go 查看文件

@@ -22,11 +22,15 @@ type DetectorConfig struct {
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"`
CFAREnabled bool `yaml:"cfar_enabled" json:"cfar_enabled"`
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"`
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"`

// Deprecated (backward compatibility)
CFAREnabled *bool `yaml:"cfar_enabled,omitempty" json:"cfar_enabled,omitempty"`
}

type RecorderConfig struct {
@@ -89,7 +93,21 @@ func Default() Config {
AGC: false,
DCBlock: false,
IQBalance: false,
Detector: DetectorConfig{ThresholdDb: -20, MinDurationMs: 250, HoldMs: 500, EmaAlpha: 0.2, HysteresisDb: 3, MinStableFrames: 3, GapToleranceMs: 500, CFAREnabled: true, CFARGuardCells: 2, CFARTrainCells: 16, CFARRank: 24, CFARScaleDb: 6},
Detector: DetectorConfig{
ThresholdDb: -20,
MinDurationMs: 250,
HoldMs: 500,
EmaAlpha: 0.2,
HysteresisDb: 3,
MinStableFrames: 3,
GapToleranceMs: 500,
CFARMode: "GOSCA",
CFARGuardCells: 3,
CFARTrainCells: 24,
CFARRank: 36,
CFARScaleDb: 6,
CFARWrapAround: true,
},
Recorder: RecorderConfig{
Enabled: false,
MinSNRDb: 10,
@@ -143,11 +161,22 @@ func applyDefaults(cfg Config) Config {
if cfg.Detector.GapToleranceMs <= 0 {
cfg.Detector.GapToleranceMs = cfg.Detector.HoldMs
}
if cfg.Detector.CFARMode == "" {
if cfg.Detector.CFAREnabled != nil {
if *cfg.Detector.CFAREnabled {
cfg.Detector.CFARMode = "OS"
} else {
cfg.Detector.CFARMode = "OFF"
}
} else {
cfg.Detector.CFARMode = "GOSCA"
}
}
if cfg.Detector.CFARGuardCells <= 0 {
cfg.Detector.CFARGuardCells = 2
cfg.Detector.CFARGuardCells = 3
}
if cfg.Detector.CFARTrainCells <= 0 {
cfg.Detector.CFARTrainCells = 16
cfg.Detector.CFARTrainCells = 24
}
if cfg.Detector.CFARRank <= 0 || cfg.Detector.CFARRank > 2*cfg.Detector.CFARTrainCells {
cfg.Detector.CFARRank = int(math.Round(0.75 * float64(2*cfg.Detector.CFARTrainCells)))


+ 22
- 103
internal/detector/detector.go 查看文件

@@ -5,6 +5,7 @@ import (
"sort"
"time"

"sdr-visual-suite/internal/cfar"
"sdr-visual-suite/internal/classifier"
)

@@ -29,19 +30,15 @@ type Detector struct {
HysteresisDb float64
MinStableFrames int
GapTolerance time.Duration
CFAREnabled bool
CFARGuardCells int
CFARTrainCells int
CFARRank int
CFARScaleDb float64

binWidth float64
nbins int
sampleRate int

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

type activeEvent struct {
@@ -68,7 +65,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, minStable int, gapTolerance time.Duration, cfarEnabled bool, cfarGuard, cfarTrain, cfarRank int, cfarScaleDb float64) *Detector {
func New(thresholdDb float64, sampleRate int, fftSize int, minDur, hold time.Duration, emaAlpha, hysteresis float64, minStable int, gapTolerance time.Duration, cfarMode string, cfarGuard, cfarTrain, cfarRank int, cfarScaleDb float64, cfarWrap bool) *Detector {
if minDur <= 0 {
minDur = 250 * time.Millisecond
}
@@ -102,6 +99,17 @@ func New(thresholdDb float64, sampleRate int, fftSize int, minDur, hold time.Dur
cfarRank = 1
}
}
var cfarEngine cfar.CFAR
if cfarMode != "" && cfarMode != "OFF" {
cfarEngine = cfar.New(cfar.Config{
Mode: cfar.Mode(cfarMode),
GuardCells: cfarGuard,
TrainCells: cfarTrain,
Rank: cfarRank,
ScaleDb: cfarScaleDb,
WrapAround: cfarWrap,
})
}
return &Detector{
ThresholdDb: thresholdDb,
MinDuration: minDur,
@@ -110,10 +118,6 @@ func New(thresholdDb float64, sampleRate int, fftSize int, minDur, hold time.Dur
HysteresisDb: hysteresis,
MinStableFrames: minStable,
GapTolerance: gapTolerance,
CFAREnabled: cfarEnabled,
CFARGuardCells: cfarGuard,
CFARTrainCells: cfarTrain,
CFARRank: cfarRank,
CFARScaleDb: cfarScaleDb,
binWidth: float64(sampleRate) / float64(fftSize),
nbins: fftSize,
@@ -121,6 +125,7 @@ func New(thresholdDb float64, sampleRate int, fftSize int, minDur, hold time.Dur
ema: make([]float64, fftSize),
active: map[int64]*activeEvent{},
nextID: 1,
cfarEngine: cfarEngine,
}
}

@@ -151,7 +156,10 @@ func (d *Detector) detectSignals(spectrum []float64, centerHz float64) []Signal
return nil
}
smooth := d.smoothSpectrum(spectrum)
thresholds := d.cfarThresholds(smooth)
var thresholds []float64
if d.cfarEngine != nil {
thresholds = d.cfarEngine.Thresholds(smooth)
}
noiseGlobal := median(smooth)
var signals []Signal
in := false
@@ -209,95 +217,6 @@ func (d *Detector) makeSignal(first, last int, peak float64, peakBin int, noise
}
}

func (d *Detector) cfarThresholds(spectrum []float64) []float64 {
if !d.CFAREnabled || d.CFARTrainCells <= 0 {
return nil
}
n := len(spectrum)
train := d.CFARTrainCells
guard := d.CFARGuardCells
totalTrain := 2 * train
if totalTrain <= 0 {
return nil
}
rank := d.CFARRank
if rank <= 0 || rank > totalTrain {
rank = int(math.Round(0.75 * float64(totalTrain)))
if rank <= 0 {
rank = 1
}
}
rankIdx := rank - 1
thresholds := make([]float64, n)
buf := make([]float64, 0, totalTrain)
firstValid := guard + train
lastValid := n - guard - train - 1
for i := 0; i < n; i++ {
if i < firstValid || i > lastValid {
thresholds[i] = math.NaN()
}
}
if firstValid > lastValid {
return thresholds
}

// Build initial sorted window for first valid bin.
leftStart := firstValid - guard - train
buf = append(buf, spectrum[leftStart:leftStart+train]...)
buf = append(buf, spectrum[firstValid+guard+1:firstValid+guard+1+train]...)
sort.Float64s(buf)
thresholds[firstValid] = buf[rankIdx] + d.CFARScaleDb

// Slide window: remove outgoing bins and insert incoming bins (O(train) per step).
for i := firstValid + 1; i <= lastValid; i++ {
outLeft := spectrum[i-guard-train-1]
outRight := spectrum[i+guard]
inLeft := spectrum[i-guard-1]
inRight := spectrum[i+guard+train]
buf = removeValue(buf, outLeft)
buf = removeValue(buf, outRight)
buf = insertValue(buf, inLeft)
buf = insertValue(buf, inRight)
thresholds[i] = buf[rankIdx] + d.CFARScaleDb
}
return thresholds
}

func removeValue(sorted []float64, v float64) []float64 {
if len(sorted) == 0 {
return sorted
}
idx := sort.SearchFloat64s(sorted, v)
if idx < len(sorted) && sorted[idx] == v {
return append(sorted[:idx], sorted[idx+1:]...)
}
for i := idx - 1; i >= 0; i-- {
if sorted[i] == v {
return append(sorted[:i], sorted[i+1:]...)
}
if sorted[i] < v {
break
}
}
for i := idx + 1; i < len(sorted); i++ {
if sorted[i] == v {
return append(sorted[:i], sorted[i+1:]...)
}
if sorted[i] > v {
break
}
}
return sorted
}

func insertValue(sorted []float64, v float64) []float64 {
idx := sort.SearchFloat64s(sorted, v)
sorted = append(sorted, 0)
copy(sorted[idx+1:], sorted[idx:])
sorted[idx] = v
return sorted
}

func (d *Detector) smoothSpectrum(spectrum []float64) []float64 {
if d.ema == nil || len(d.ema) != len(spectrum) {
d.ema = make([]float64, len(spectrum))


+ 1
- 1
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, 1, 10*time.Millisecond, false, 2, 16, 24, 6)
d := New(-10, 1000, 10, 1*time.Millisecond, 10*time.Millisecond, 0.2, 3, 1, 10*time.Millisecond, "OFF", 2, 16, 24, 6, true)
center := 0.0
spectrum := []float64{-30, -30, -30, -5, -5, -30, -30, -30, -30, -30}
now := time.Now()


+ 14
- 3
internal/runtime/runtime.go 查看文件

@@ -3,6 +3,7 @@ package runtime
import (
"errors"
"math"
"strings"
"sync"

"sdr-visual-suite/internal/config"
@@ -27,11 +28,12 @@ type DetectorUpdate struct {
HysteresisDb *float64 `json:"hysteresis_db"`
MinStableFrames *int `json:"min_stable_frames"`
GapToleranceMs *int `json:"gap_tolerance_ms"`
CFAREnabled *bool `json:"cfar_enabled"`
CFARMode *string `json:"cfar_mode"`
CFARGuardCells *int `json:"cfar_guard_cells"`
CFARTrainCells *int `json:"cfar_train_cells"`
CFARRank *int `json:"cfar_rank"`
CFARScaleDb *float64 `json:"cfar_scale_db"`
CFARWrapAround *bool `json:"cfar_wrap_around"`
}

type SettingsUpdate struct {
@@ -157,8 +159,17 @@ func (m *Manager) ApplyConfig(update ConfigUpdate) (config.Config, error) {
}
next.Detector.GapToleranceMs = *update.Detector.GapToleranceMs
}
if update.Detector.CFAREnabled != nil {
next.Detector.CFAREnabled = *update.Detector.CFAREnabled
if update.Detector.CFARMode != nil {
mode := strings.ToUpper(strings.TrimSpace(*update.Detector.CFARMode))
switch mode {
case "OFF", "CA", "OS", "GOSCA", "CASO":
next.Detector.CFARMode = mode
default:
return m.cfg, errors.New("cfar_mode must be OFF, CA, OS, GOSCA, or CASO")
}
}
if update.Detector.CFARWrapAround != nil {
next.Detector.CFARWrapAround = *update.Detector.CFARWrapAround
}
if update.Detector.CFARGuardCells != nil {
if *update.Detector.CFARGuardCells < 0 {


+ 9
- 4
internal/runtime/runtime_test.go 查看文件

@@ -15,7 +15,8 @@ func TestApplyConfigUpdate(t *testing.T) {
fftSize := 4096
threshold := -35.0
bw := 1536
cfarEnabled := true
cfarMode := "OS"
cfarWrap := true
cfarGuard := 2
cfarTrain := 12
cfarRank := 18
@@ -28,7 +29,8 @@ func TestApplyConfigUpdate(t *testing.T) {
TunerBwKHz: &bw,
Detector: &DetectorUpdate{
ThresholdDb: &threshold,
CFAREnabled: &cfarEnabled,
CFARMode: &cfarMode,
CFARWrapAround: &cfarWrap,
CFARGuardCells: &cfarGuard,
CFARTrainCells: &cfarTrain,
CFARRank: &cfarRank,
@@ -50,8 +52,11 @@ func TestApplyConfigUpdate(t *testing.T) {
if updated.Detector.ThresholdDb != threshold {
t.Fatalf("threshold: %v", updated.Detector.ThresholdDb)
}
if updated.Detector.CFAREnabled != cfarEnabled {
t.Fatalf("cfar enabled: %v", updated.Detector.CFAREnabled)
if updated.Detector.CFARMode != cfarMode {
t.Fatalf("cfar mode: %v", updated.Detector.CFARMode)
}
if updated.Detector.CFARWrapAround != cfarWrap {
t.Fatalf("cfar wrap: %v", updated.Detector.CFARWrapAround)
}
if updated.Detector.CFARGuardCells != cfarGuard {
t.Fatalf("cfar guard: %v", updated.Detector.CFARGuardCells)


Loading…
取消
儲存