Wideband autonomous SDR analysis engine forked from sdr-visual-suite
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

568 lines
19KB

  1. package runtime
  2. import (
  3. "errors"
  4. "fmt"
  5. "math"
  6. "strings"
  7. "sync"
  8. "sdr-wideband-suite/internal/config"
  9. )
  10. type PipelineUpdate struct {
  11. Mode *string `json:"mode"`
  12. Profile *string `json:"profile"`
  13. Intent *string `json:"intent"`
  14. MonitorStartHz *float64 `json:"monitor_start_hz"`
  15. MonitorEndHz *float64 `json:"monitor_end_hz"`
  16. MonitorSpanHz *float64 `json:"monitor_span_hz"`
  17. MonitorWindows *[]config.MonitorWindow `json:"monitor_windows"`
  18. SignalPriorities *[]string `json:"signal_priorities"`
  19. AutoRecordClasses *[]string `json:"auto_record_classes"`
  20. AutoDecodeClasses *[]string `json:"auto_decode_classes"`
  21. }
  22. type SurveillanceUpdate struct {
  23. AnalysisFFTSize *int `json:"analysis_fft_size"`
  24. FrameRate *int `json:"frame_rate"`
  25. Strategy *string `json:"strategy"`
  26. DisplayBins *int `json:"display_bins"`
  27. DisplayFPS *int `json:"display_fps"`
  28. DerivedDetection *string `json:"derived_detection"`
  29. }
  30. type RefinementUpdate struct {
  31. Enabled *bool `json:"enabled"`
  32. MaxConcurrent *int `json:"max_concurrent"`
  33. DetailFFTSize *int `json:"detail_fft_size"`
  34. MinCandidateSNRDb *float64 `json:"min_candidate_snr_db"`
  35. MinSpanHz *float64 `json:"min_span_hz"`
  36. MaxSpanHz *float64 `json:"max_span_hz"`
  37. AutoSpan *bool `json:"auto_span"`
  38. }
  39. type ResourcesUpdate struct {
  40. PreferGPU *bool `json:"prefer_gpu"`
  41. MaxRefinementJobs *int `json:"max_refinement_jobs"`
  42. MaxRecordingStreams *int `json:"max_recording_streams"`
  43. MaxDecodeJobs *int `json:"max_decode_jobs"`
  44. DecisionHoldMs *int `json:"decision_hold_ms"`
  45. }
  46. type ConfigUpdate struct {
  47. CenterHz *float64 `json:"center_hz"`
  48. SampleRate *int `json:"sample_rate"`
  49. FFTSize *int `json:"fft_size"`
  50. GainDb *float64 `json:"gain_db"`
  51. TunerBwKHz *int `json:"tuner_bw_khz"`
  52. UseGPUFFT *bool `json:"use_gpu_fft"`
  53. ClassifierMode *string `json:"classifier_mode"`
  54. Pipeline *PipelineUpdate `json:"pipeline"`
  55. Surveillance *SurveillanceUpdate `json:"surveillance"`
  56. Refinement *RefinementUpdate `json:"refinement"`
  57. Resources *ResourcesUpdate `json:"resources"`
  58. Detector *DetectorUpdate `json:"detector"`
  59. Recorder *RecorderUpdate `json:"recorder"`
  60. }
  61. type DetectorUpdate struct {
  62. ThresholdDb *float64 `json:"threshold_db"`
  63. MinDuration *int `json:"min_duration_ms"`
  64. HoldMs *int `json:"hold_ms"`
  65. EmaAlpha *float64 `json:"ema_alpha"`
  66. HysteresisDb *float64 `json:"hysteresis_db"`
  67. MinStableFrames *int `json:"min_stable_frames"`
  68. GapToleranceMs *int `json:"gap_tolerance_ms"`
  69. CFARMode *string `json:"cfar_mode"`
  70. CFARGuardHz *float64 `json:"cfar_guard_hz"`
  71. CFARTrainHz *float64 `json:"cfar_train_hz"`
  72. CFARGuardCells *int `json:"cfar_guard_cells"`
  73. CFARTrainCells *int `json:"cfar_train_cells"`
  74. CFARRank *int `json:"cfar_rank"`
  75. CFARScaleDb *float64 `json:"cfar_scale_db"`
  76. CFARWrapAround *bool `json:"cfar_wrap_around"`
  77. EdgeMarginDb *float64 `json:"edge_margin_db"`
  78. MergeGapHz *float64 `json:"merge_gap_hz"`
  79. ClassHistorySize *int `json:"class_history_size"`
  80. ClassSwitchRatio *float64 `json:"class_switch_ratio"`
  81. }
  82. type SettingsUpdate struct {
  83. AGC *bool `json:"agc"`
  84. DCBlock *bool `json:"dc_block"`
  85. IQBalance *bool `json:"iq_balance"`
  86. }
  87. type RecorderUpdate struct {
  88. Enabled *bool `json:"enabled"`
  89. MinSNRDb *float64 `json:"min_snr_db"`
  90. MinDuration *string `json:"min_duration"`
  91. MaxDuration *string `json:"max_duration"`
  92. PrerollMs *int `json:"preroll_ms"`
  93. RecordIQ *bool `json:"record_iq"`
  94. RecordAudio *bool `json:"record_audio"`
  95. AutoDemod *bool `json:"auto_demod"`
  96. AutoDecode *bool `json:"auto_decode"`
  97. MaxDiskMB *int `json:"max_disk_mb"`
  98. OutputDir *string `json:"output_dir"`
  99. ClassFilter *[]string `json:"class_filter"`
  100. RingSeconds *int `json:"ring_seconds"`
  101. }
  102. type Manager struct {
  103. mu sync.RWMutex
  104. cfg config.Config
  105. }
  106. func New(cfg config.Config) *Manager {
  107. return &Manager{cfg: cfg}
  108. }
  109. func (m *Manager) Snapshot() config.Config {
  110. m.mu.RLock()
  111. defer m.mu.RUnlock()
  112. return m.cfg
  113. }
  114. func (m *Manager) Replace(cfg config.Config) {
  115. m.mu.Lock()
  116. defer m.mu.Unlock()
  117. m.cfg = cfg
  118. }
  119. func (m *Manager) ApplyConfig(update ConfigUpdate) (config.Config, error) {
  120. m.mu.Lock()
  121. defer m.mu.Unlock()
  122. next := m.cfg
  123. if update.CenterHz != nil {
  124. if *update.CenterHz < 1e3 || *update.CenterHz > 2e9 {
  125. return m.cfg, errors.New("center_hz out of range")
  126. }
  127. next.CenterHz = *update.CenterHz
  128. }
  129. if update.SampleRate != nil {
  130. if *update.SampleRate <= 0 {
  131. return m.cfg, errors.New("sample_rate must be > 0")
  132. }
  133. next.SampleRate = *update.SampleRate
  134. }
  135. if update.FFTSize != nil {
  136. if *update.FFTSize <= 0 {
  137. return m.cfg, errors.New("fft_size must be > 0")
  138. }
  139. if *update.FFTSize&(*update.FFTSize-1) != 0 {
  140. return m.cfg, errors.New("fft_size must be a power of 2")
  141. }
  142. next.FFTSize = *update.FFTSize
  143. next.Surveillance.AnalysisFFTSize = *update.FFTSize
  144. }
  145. if update.GainDb != nil {
  146. next.GainDb = *update.GainDb
  147. }
  148. if update.TunerBwKHz != nil {
  149. if *update.TunerBwKHz <= 0 {
  150. return m.cfg, errors.New("tuner_bw_khz must be > 0")
  151. }
  152. next.TunerBwKHz = *update.TunerBwKHz
  153. }
  154. if update.UseGPUFFT != nil {
  155. next.UseGPUFFT = *update.UseGPUFFT
  156. }
  157. if update.ClassifierMode != nil {
  158. mode := *update.ClassifierMode
  159. switch mode {
  160. case "rule", "math", "combined":
  161. next.ClassifierMode = mode
  162. default:
  163. return m.cfg, errors.New("classifier_mode must be rule, math, or combined")
  164. }
  165. }
  166. if update.Pipeline != nil {
  167. if update.Pipeline.Mode != nil {
  168. next.Pipeline.Mode = *update.Pipeline.Mode
  169. }
  170. if update.Pipeline.Profile != nil {
  171. next.Pipeline.Profile = *update.Pipeline.Profile
  172. }
  173. if update.Pipeline.Intent != nil {
  174. next.Pipeline.Goals.Intent = *update.Pipeline.Intent
  175. }
  176. if update.Pipeline.MonitorStartHz != nil {
  177. next.Pipeline.Goals.MonitorStartHz = *update.Pipeline.MonitorStartHz
  178. }
  179. if update.Pipeline.MonitorEndHz != nil {
  180. next.Pipeline.Goals.MonitorEndHz = *update.Pipeline.MonitorEndHz
  181. }
  182. if update.Pipeline.MonitorSpanHz != nil {
  183. if *update.Pipeline.MonitorSpanHz <= 0 {
  184. return m.cfg, errors.New("monitor_span_hz must be > 0")
  185. }
  186. next.Pipeline.Goals.MonitorSpanHz = *update.Pipeline.MonitorSpanHz
  187. }
  188. if update.Pipeline.MonitorWindows != nil {
  189. windows := *update.Pipeline.MonitorWindows
  190. if err := validateMonitorWindows(windows); err != nil {
  191. return m.cfg, err
  192. }
  193. next.Pipeline.Goals.MonitorWindows = append([]config.MonitorWindow(nil), windows...)
  194. }
  195. if update.Pipeline.SignalPriorities != nil {
  196. next.Pipeline.Goals.SignalPriorities = append([]string(nil), (*update.Pipeline.SignalPriorities)...)
  197. }
  198. if update.Pipeline.AutoRecordClasses != nil {
  199. next.Pipeline.Goals.AutoRecordClasses = append([]string(nil), (*update.Pipeline.AutoRecordClasses)...)
  200. }
  201. if update.Pipeline.AutoDecodeClasses != nil {
  202. next.Pipeline.Goals.AutoDecodeClasses = append([]string(nil), (*update.Pipeline.AutoDecodeClasses)...)
  203. }
  204. if next.Pipeline.Goals.MonitorStartHz != 0 && next.Pipeline.Goals.MonitorEndHz != 0 && next.Pipeline.Goals.MonitorEndHz <= next.Pipeline.Goals.MonitorStartHz {
  205. return m.cfg, errors.New("monitor_end_hz must be > monitor_start_hz")
  206. }
  207. if next.Pipeline.Goals.MonitorSpanHz <= 0 && next.Pipeline.Goals.MonitorStartHz != 0 && next.Pipeline.Goals.MonitorEndHz != 0 && next.Pipeline.Goals.MonitorEndHz > next.Pipeline.Goals.MonitorStartHz {
  208. next.Pipeline.Goals.MonitorSpanHz = next.Pipeline.Goals.MonitorEndHz - next.Pipeline.Goals.MonitorStartHz
  209. }
  210. }
  211. if update.Surveillance != nil {
  212. if update.Surveillance.AnalysisFFTSize != nil {
  213. v := *update.Surveillance.AnalysisFFTSize
  214. if v <= 0 {
  215. return m.cfg, errors.New("surveillance.analysis_fft_size must be > 0")
  216. }
  217. if v&(v-1) != 0 {
  218. return m.cfg, errors.New("surveillance.analysis_fft_size must be a power of 2")
  219. }
  220. next.Surveillance.AnalysisFFTSize = v
  221. next.FFTSize = v
  222. }
  223. if update.Surveillance.FrameRate != nil {
  224. v := *update.Surveillance.FrameRate
  225. if v <= 0 {
  226. return m.cfg, errors.New("surveillance.frame_rate must be > 0")
  227. }
  228. next.Surveillance.FrameRate = v
  229. next.FrameRate = v
  230. }
  231. if update.Surveillance.Strategy != nil {
  232. next.Surveillance.Strategy = *update.Surveillance.Strategy
  233. }
  234. if update.Surveillance.DisplayBins != nil {
  235. v := *update.Surveillance.DisplayBins
  236. if v <= 0 {
  237. return m.cfg, errors.New("surveillance.display_bins must be > 0")
  238. }
  239. next.Surveillance.DisplayBins = v
  240. }
  241. if update.Surveillance.DisplayFPS != nil {
  242. v := *update.Surveillance.DisplayFPS
  243. if v <= 0 {
  244. return m.cfg, errors.New("surveillance.display_fps must be > 0")
  245. }
  246. next.Surveillance.DisplayFPS = v
  247. }
  248. if update.Surveillance.DerivedDetection != nil {
  249. mode := strings.ToLower(strings.TrimSpace(*update.Surveillance.DerivedDetection))
  250. switch mode {
  251. case "auto", "on", "off", "true", "false", "enabled", "disabled", "enable", "disable":
  252. next.Surveillance.DerivedDetection = mode
  253. default:
  254. return m.cfg, errors.New("surveillance.derived_detection must be auto, on, or off")
  255. }
  256. }
  257. }
  258. if update.Refinement != nil {
  259. if update.Refinement.Enabled != nil {
  260. next.Refinement.Enabled = *update.Refinement.Enabled
  261. }
  262. if update.Refinement.MaxConcurrent != nil {
  263. if *update.Refinement.MaxConcurrent <= 0 {
  264. return m.cfg, errors.New("refinement.max_concurrent must be > 0")
  265. }
  266. next.Refinement.MaxConcurrent = *update.Refinement.MaxConcurrent
  267. }
  268. if update.Refinement.DetailFFTSize != nil {
  269. v := *update.Refinement.DetailFFTSize
  270. if v <= 0 {
  271. return m.cfg, errors.New("refinement.detail_fft_size must be > 0")
  272. }
  273. if v&(v-1) != 0 {
  274. return m.cfg, errors.New("refinement.detail_fft_size must be a power of 2")
  275. }
  276. next.Refinement.DetailFFTSize = v
  277. }
  278. if update.Refinement.MinCandidateSNRDb != nil {
  279. next.Refinement.MinCandidateSNRDb = *update.Refinement.MinCandidateSNRDb
  280. }
  281. if update.Refinement.MinSpanHz != nil {
  282. if *update.Refinement.MinSpanHz < 0 {
  283. return m.cfg, errors.New("refinement.min_span_hz must be >= 0")
  284. }
  285. next.Refinement.MinSpanHz = *update.Refinement.MinSpanHz
  286. }
  287. if update.Refinement.MaxSpanHz != nil {
  288. if *update.Refinement.MaxSpanHz < 0 {
  289. return m.cfg, errors.New("refinement.max_span_hz must be >= 0")
  290. }
  291. next.Refinement.MaxSpanHz = *update.Refinement.MaxSpanHz
  292. }
  293. if update.Refinement.AutoSpan != nil {
  294. next.Refinement.AutoSpan = update.Refinement.AutoSpan
  295. }
  296. if next.Refinement.MaxSpanHz > 0 && next.Refinement.MinSpanHz > next.Refinement.MaxSpanHz {
  297. return m.cfg, errors.New("refinement.min_span_hz must be <= refinement.max_span_hz")
  298. }
  299. }
  300. if update.Resources != nil {
  301. if update.Resources.PreferGPU != nil {
  302. next.Resources.PreferGPU = *update.Resources.PreferGPU
  303. }
  304. if update.Resources.MaxRefinementJobs != nil {
  305. if *update.Resources.MaxRefinementJobs <= 0 {
  306. return m.cfg, errors.New("resources.max_refinement_jobs must be > 0")
  307. }
  308. next.Resources.MaxRefinementJobs = *update.Resources.MaxRefinementJobs
  309. }
  310. if update.Resources.MaxRecordingStreams != nil {
  311. if *update.Resources.MaxRecordingStreams <= 0 {
  312. return m.cfg, errors.New("resources.max_recording_streams must be > 0")
  313. }
  314. next.Resources.MaxRecordingStreams = *update.Resources.MaxRecordingStreams
  315. }
  316. if update.Resources.MaxDecodeJobs != nil {
  317. if *update.Resources.MaxDecodeJobs <= 0 {
  318. return m.cfg, errors.New("resources.max_decode_jobs must be > 0")
  319. }
  320. next.Resources.MaxDecodeJobs = *update.Resources.MaxDecodeJobs
  321. }
  322. if update.Resources.DecisionHoldMs != nil {
  323. if *update.Resources.DecisionHoldMs < 0 {
  324. return m.cfg, errors.New("resources.decision_hold_ms must be >= 0")
  325. }
  326. next.Resources.DecisionHoldMs = *update.Resources.DecisionHoldMs
  327. }
  328. }
  329. if update.Detector != nil {
  330. if update.Detector.ThresholdDb != nil {
  331. next.Detector.ThresholdDb = *update.Detector.ThresholdDb
  332. }
  333. if update.Detector.MinDuration != nil {
  334. if *update.Detector.MinDuration <= 0 {
  335. return m.cfg, errors.New("min_duration_ms must be > 0")
  336. }
  337. next.Detector.MinDurationMs = *update.Detector.MinDuration
  338. }
  339. if update.Detector.HoldMs != nil {
  340. if *update.Detector.HoldMs <= 0 {
  341. return m.cfg, errors.New("hold_ms must be > 0")
  342. }
  343. next.Detector.HoldMs = *update.Detector.HoldMs
  344. }
  345. if update.Detector.EmaAlpha != nil {
  346. v := *update.Detector.EmaAlpha
  347. if math.IsNaN(v) || math.IsInf(v, 0) || v < 0 || v > 1 {
  348. return m.cfg, errors.New("ema_alpha must be between 0 and 1")
  349. }
  350. next.Detector.EmaAlpha = v
  351. }
  352. if update.Detector.HysteresisDb != nil {
  353. v := *update.Detector.HysteresisDb
  354. if math.IsNaN(v) || math.IsInf(v, 0) || v < 0 {
  355. return m.cfg, errors.New("hysteresis_db must be >= 0")
  356. }
  357. next.Detector.HysteresisDb = v
  358. }
  359. if update.Detector.MinStableFrames != nil {
  360. if *update.Detector.MinStableFrames < 1 {
  361. return m.cfg, errors.New("min_stable_frames must be >= 1")
  362. }
  363. next.Detector.MinStableFrames = *update.Detector.MinStableFrames
  364. }
  365. if update.Detector.GapToleranceMs != nil {
  366. if *update.Detector.GapToleranceMs < 0 {
  367. return m.cfg, errors.New("gap_tolerance_ms must be >= 0")
  368. }
  369. next.Detector.GapToleranceMs = *update.Detector.GapToleranceMs
  370. }
  371. if update.Detector.CFARMode != nil {
  372. mode := strings.ToUpper(strings.TrimSpace(*update.Detector.CFARMode))
  373. switch mode {
  374. case "OFF", "CA", "OS", "GOSCA", "CASO":
  375. next.Detector.CFARMode = mode
  376. default:
  377. return m.cfg, errors.New("cfar_mode must be OFF, CA, OS, GOSCA, or CASO")
  378. }
  379. }
  380. if update.Detector.CFARWrapAround != nil {
  381. next.Detector.CFARWrapAround = *update.Detector.CFARWrapAround
  382. }
  383. if update.Detector.CFARGuardHz != nil {
  384. if *update.Detector.CFARGuardHz < 0 {
  385. return m.cfg, errors.New("cfar_guard_hz must be >= 0")
  386. }
  387. next.Detector.CFARGuardHz = *update.Detector.CFARGuardHz
  388. }
  389. if update.Detector.CFARTrainHz != nil {
  390. if *update.Detector.CFARTrainHz <= 0 {
  391. return m.cfg, errors.New("cfar_train_hz must be > 0")
  392. }
  393. next.Detector.CFARTrainHz = *update.Detector.CFARTrainHz
  394. }
  395. if update.Detector.CFARGuardCells != nil {
  396. if *update.Detector.CFARGuardCells < 0 {
  397. return m.cfg, errors.New("cfar_guard_cells must be >= 0")
  398. }
  399. next.Detector.CFARGuardCells = *update.Detector.CFARGuardCells
  400. }
  401. if update.Detector.CFARTrainCells != nil {
  402. if *update.Detector.CFARTrainCells <= 0 {
  403. return m.cfg, errors.New("cfar_train_cells must be > 0")
  404. }
  405. next.Detector.CFARTrainCells = *update.Detector.CFARTrainCells
  406. }
  407. if update.Detector.CFARRank != nil {
  408. if *update.Detector.CFARRank <= 0 {
  409. return m.cfg, errors.New("cfar_rank must be > 0")
  410. }
  411. if next.Detector.CFARTrainCells > 0 && *update.Detector.CFARRank > 2*next.Detector.CFARTrainCells {
  412. return m.cfg, errors.New("cfar_rank must be <= 2 * cfar_train_cells")
  413. }
  414. next.Detector.CFARRank = *update.Detector.CFARRank
  415. }
  416. if update.Detector.CFARScaleDb != nil {
  417. next.Detector.CFARScaleDb = *update.Detector.CFARScaleDb
  418. }
  419. if update.Detector.EdgeMarginDb != nil {
  420. v := *update.Detector.EdgeMarginDb
  421. if math.IsNaN(v) || math.IsInf(v, 0) || v < 0 {
  422. return m.cfg, errors.New("edge_margin_db must be >= 0")
  423. }
  424. next.Detector.EdgeMarginDb = v
  425. }
  426. if update.Detector.MergeGapHz != nil {
  427. v := *update.Detector.MergeGapHz
  428. if math.IsNaN(v) || math.IsInf(v, 0) || v < 0 {
  429. return m.cfg, errors.New("merge_gap_hz must be >= 0")
  430. }
  431. next.Detector.MergeGapHz = v
  432. }
  433. if update.Detector.ClassHistorySize != nil {
  434. if *update.Detector.ClassHistorySize < 1 {
  435. return m.cfg, errors.New("class_history_size must be >= 1")
  436. }
  437. next.Detector.ClassHistorySize = *update.Detector.ClassHistorySize
  438. }
  439. if update.Detector.ClassSwitchRatio != nil {
  440. v := *update.Detector.ClassSwitchRatio
  441. if math.IsNaN(v) || math.IsInf(v, 0) || v < 0.1 || v > 1.0 {
  442. return m.cfg, errors.New("class_switch_ratio must be between 0.1 and 1.0")
  443. }
  444. next.Detector.ClassSwitchRatio = v
  445. }
  446. }
  447. if update.Recorder != nil {
  448. if update.Recorder.Enabled != nil {
  449. next.Recorder.Enabled = *update.Recorder.Enabled
  450. }
  451. if update.Recorder.MinSNRDb != nil {
  452. next.Recorder.MinSNRDb = *update.Recorder.MinSNRDb
  453. }
  454. if update.Recorder.MinDuration != nil {
  455. next.Recorder.MinDuration = *update.Recorder.MinDuration
  456. }
  457. if update.Recorder.MaxDuration != nil {
  458. next.Recorder.MaxDuration = *update.Recorder.MaxDuration
  459. }
  460. if update.Recorder.PrerollMs != nil {
  461. next.Recorder.PrerollMs = *update.Recorder.PrerollMs
  462. }
  463. if update.Recorder.RecordIQ != nil {
  464. next.Recorder.RecordIQ = *update.Recorder.RecordIQ
  465. }
  466. if update.Recorder.RecordAudio != nil {
  467. next.Recorder.RecordAudio = *update.Recorder.RecordAudio
  468. }
  469. if update.Recorder.AutoDemod != nil {
  470. next.Recorder.AutoDemod = *update.Recorder.AutoDemod
  471. }
  472. if update.Recorder.AutoDecode != nil {
  473. next.Recorder.AutoDecode = *update.Recorder.AutoDecode
  474. }
  475. if update.Recorder.MaxDiskMB != nil {
  476. next.Recorder.MaxDiskMB = *update.Recorder.MaxDiskMB
  477. }
  478. if update.Recorder.OutputDir != nil {
  479. next.Recorder.OutputDir = *update.Recorder.OutputDir
  480. }
  481. if update.Recorder.ClassFilter != nil {
  482. next.Recorder.ClassFilter = *update.Recorder.ClassFilter
  483. }
  484. if update.Recorder.RingSeconds != nil {
  485. next.Recorder.RingSeconds = *update.Recorder.RingSeconds
  486. }
  487. }
  488. m.cfg = next
  489. return m.cfg, nil
  490. }
  491. func validateMonitorWindows(windows []config.MonitorWindow) error {
  492. for i, w := range windows {
  493. if !isValidMonitorZone(w.Zone) {
  494. return fmt.Errorf("monitor_windows[%d] zone is invalid", i)
  495. }
  496. if math.IsNaN(w.Priority) || math.IsInf(w.Priority, 0) || w.Priority < -1 || w.Priority > 1 {
  497. return fmt.Errorf("monitor_windows[%d] priority must be between -1 and 1", i)
  498. }
  499. hasStart := w.StartHz != 0 || w.EndHz != 0
  500. if hasStart {
  501. if w.StartHz <= 0 || w.EndHz <= 0 || w.EndHz <= w.StartHz {
  502. return fmt.Errorf("monitor_windows[%d] requires start_hz < end_hz", i)
  503. }
  504. continue
  505. }
  506. if w.CenterHz <= 0 {
  507. return fmt.Errorf("monitor_windows[%d] requires center_hz when start/end not set", i)
  508. }
  509. if w.SpanHz <= 0 {
  510. return fmt.Errorf("monitor_windows[%d] requires span_hz > 0 when start/end not set", i)
  511. }
  512. }
  513. return nil
  514. }
  515. func isValidMonitorZone(zone string) bool {
  516. zone = strings.ToLower(strings.TrimSpace(zone))
  517. if zone == "" {
  518. return true
  519. }
  520. switch zone {
  521. case "neutral", "monitor", "default", "focus", "priority", "hot",
  522. "record", "recording", "record-only",
  523. "decode", "decoding", "decode-only",
  524. "background", "bg", "defer":
  525. return true
  526. default:
  527. return false
  528. }
  529. }
  530. func (m *Manager) ApplySettings(update SettingsUpdate) (config.Config, error) {
  531. m.mu.Lock()
  532. defer m.mu.Unlock()
  533. next := m.cfg
  534. if update.AGC != nil {
  535. next.AGC = *update.AGC
  536. }
  537. if update.DCBlock != nil {
  538. next.DCBlock = *update.DCBlock
  539. }
  540. if update.IQBalance != nil {
  541. next.IQBalance = *update.IQBalance
  542. }
  543. m.cfg = next
  544. return m.cfg, nil
  545. }