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.

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