Wideband autonomous SDR analysis engine forked from sdr-visual-suite
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

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