package runtime import ( "errors" "sync" "sdr-visual-suite/internal/config" ) type ConfigUpdate struct { CenterHz *float64 `json:"center_hz"` SampleRate *int `json:"sample_rate"` FFTSize *int `json:"fft_size"` GainDb *float64 `json:"gain_db"` TunerBwKHz *int `json:"tuner_bw_khz"` UseGPUFFT *bool `json:"use_gpu_fft"` Detector *DetectorUpdate `json:"detector"` Recorder *RecorderUpdate `json:"recorder"` } 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"` } type SettingsUpdate struct { AGC *bool `json:"agc"` DCBlock *bool `json:"dc_block"` IQBalance *bool `json:"iq_balance"` } type RecorderUpdate struct { Enabled *bool `json:"enabled"` MinSNRDb *float64 `json:"min_snr_db"` MinDuration *string `json:"min_duration"` MaxDuration *string `json:"max_duration"` PrerollMs *int `json:"preroll_ms"` RecordIQ *bool `json:"record_iq"` RecordAudio *bool `json:"record_audio"` AutoDemod *bool `json:"auto_demod"` AutoDecode *bool `json:"auto_decode"` MaxDiskMB *int `json:"max_disk_mb"` OutputDir *string `json:"output_dir"` ClassFilter *[]string `json:"class_filter"` RingSeconds *int `json:"ring_seconds"` } type Manager struct { mu sync.RWMutex cfg config.Config } func New(cfg config.Config) *Manager { return &Manager{cfg: cfg} } func (m *Manager) Snapshot() config.Config { m.mu.RLock() defer m.mu.RUnlock() return m.cfg } func (m *Manager) Replace(cfg config.Config) { m.mu.Lock() defer m.mu.Unlock() m.cfg = cfg } func (m *Manager) ApplyConfig(update ConfigUpdate) (config.Config, error) { m.mu.Lock() defer m.mu.Unlock() next := m.cfg if update.CenterHz != nil { if *update.CenterHz < 1e3 || *update.CenterHz > 2e9 { return m.cfg, errors.New("center_hz out of range") } next.CenterHz = *update.CenterHz } if update.SampleRate != nil { if *update.SampleRate <= 0 { return m.cfg, errors.New("sample_rate must be > 0") } next.SampleRate = *update.SampleRate } if update.FFTSize != nil { if *update.FFTSize <= 0 { return m.cfg, errors.New("fft_size must be > 0") } if *update.FFTSize&(*update.FFTSize-1) != 0 { return m.cfg, errors.New("fft_size must be a power of 2") } next.FFTSize = *update.FFTSize } if update.GainDb != nil { next.GainDb = *update.GainDb } if update.TunerBwKHz != nil { if *update.TunerBwKHz <= 0 { return m.cfg, errors.New("tuner_bw_khz must be > 0") } next.TunerBwKHz = *update.TunerBwKHz } if update.UseGPUFFT != nil { next.UseGPUFFT = *update.UseGPUFFT } if update.Detector != nil { if update.Detector.ThresholdDb != nil { next.Detector.ThresholdDb = *update.Detector.ThresholdDb } if update.Detector.MinDuration != nil { if *update.Detector.MinDuration <= 0 { return m.cfg, errors.New("min_duration_ms must be > 0") } next.Detector.MinDurationMs = *update.Detector.MinDuration } if update.Detector.HoldMs != nil { if *update.Detector.HoldMs <= 0 { return m.cfg, errors.New("hold_ms must be > 0") } next.Detector.HoldMs = *update.Detector.HoldMs } if update.Detector.EmaAlpha != nil { next.Detector.EmaAlpha = *update.Detector.EmaAlpha } if update.Detector.HysteresisDb != nil { next.Detector.HysteresisDb = *update.Detector.HysteresisDb } } if update.Recorder != nil { if update.Recorder.Enabled != nil { next.Recorder.Enabled = *update.Recorder.Enabled } if update.Recorder.MinSNRDb != nil { next.Recorder.MinSNRDb = *update.Recorder.MinSNRDb } if update.Recorder.MinDuration != nil { next.Recorder.MinDuration = *update.Recorder.MinDuration } if update.Recorder.MaxDuration != nil { next.Recorder.MaxDuration = *update.Recorder.MaxDuration } if update.Recorder.PrerollMs != nil { next.Recorder.PrerollMs = *update.Recorder.PrerollMs } if update.Recorder.RecordIQ != nil { next.Recorder.RecordIQ = *update.Recorder.RecordIQ } if update.Recorder.RecordAudio != nil { next.Recorder.RecordAudio = *update.Recorder.RecordAudio } if update.Recorder.AutoDemod != nil { next.Recorder.AutoDemod = *update.Recorder.AutoDemod } if update.Recorder.AutoDecode != nil { next.Recorder.AutoDecode = *update.Recorder.AutoDecode } if update.Recorder.MaxDiskMB != nil { next.Recorder.MaxDiskMB = *update.Recorder.MaxDiskMB } if update.Recorder.OutputDir != nil { next.Recorder.OutputDir = *update.Recorder.OutputDir } if update.Recorder.ClassFilter != nil { next.Recorder.ClassFilter = *update.Recorder.ClassFilter } if update.Recorder.RingSeconds != nil { next.Recorder.RingSeconds = *update.Recorder.RingSeconds } } m.cfg = next return m.cfg, nil } func (m *Manager) ApplySettings(update SettingsUpdate) (config.Config, error) { m.mu.Lock() defer m.mu.Unlock() next := m.cfg if update.AGC != nil { next.AGC = *update.AGC } if update.DCBlock != nil { next.DCBlock = *update.DCBlock } if update.IQBalance != nil { next.IQBalance = *update.IQBalance } m.cfg = next return m.cfg, nil }