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.

420 line
13KB

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