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.

508 lines
17KB

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