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.

279 line
8.2KB

  1. package runtime
  2. import (
  3. "errors"
  4. "math"
  5. "strings"
  6. "sync"
  7. "sdr-visual-suite/internal/config"
  8. )
  9. type ConfigUpdate struct {
  10. CenterHz *float64 `json:"center_hz"`
  11. SampleRate *int `json:"sample_rate"`
  12. FFTSize *int `json:"fft_size"`
  13. GainDb *float64 `json:"gain_db"`
  14. TunerBwKHz *int `json:"tuner_bw_khz"`
  15. UseGPUFFT *bool `json:"use_gpu_fft"`
  16. Detector *DetectorUpdate `json:"detector"`
  17. Recorder *RecorderUpdate `json:"recorder"`
  18. }
  19. type DetectorUpdate struct {
  20. ThresholdDb *float64 `json:"threshold_db"`
  21. MinDuration *int `json:"min_duration_ms"`
  22. HoldMs *int `json:"hold_ms"`
  23. EmaAlpha *float64 `json:"ema_alpha"`
  24. HysteresisDb *float64 `json:"hysteresis_db"`
  25. MinStableFrames *int `json:"min_stable_frames"`
  26. GapToleranceMs *int `json:"gap_tolerance_ms"`
  27. CFARMode *string `json:"cfar_mode"`
  28. CFARGuardCells *int `json:"cfar_guard_cells"`
  29. CFARTrainCells *int `json:"cfar_train_cells"`
  30. CFARRank *int `json:"cfar_rank"`
  31. CFARScaleDb *float64 `json:"cfar_scale_db"`
  32. CFARWrapAround *bool `json:"cfar_wrap_around"`
  33. EdgeMarginDb *float64 `json:"edge_margin_db"`
  34. MergeGapHz *float64 `json:"merge_gap_hz"`
  35. }
  36. type SettingsUpdate struct {
  37. AGC *bool `json:"agc"`
  38. DCBlock *bool `json:"dc_block"`
  39. IQBalance *bool `json:"iq_balance"`
  40. }
  41. type RecorderUpdate struct {
  42. Enabled *bool `json:"enabled"`
  43. MinSNRDb *float64 `json:"min_snr_db"`
  44. MinDuration *string `json:"min_duration"`
  45. MaxDuration *string `json:"max_duration"`
  46. PrerollMs *int `json:"preroll_ms"`
  47. RecordIQ *bool `json:"record_iq"`
  48. RecordAudio *bool `json:"record_audio"`
  49. AutoDemod *bool `json:"auto_demod"`
  50. AutoDecode *bool `json:"auto_decode"`
  51. MaxDiskMB *int `json:"max_disk_mb"`
  52. OutputDir *string `json:"output_dir"`
  53. ClassFilter *[]string `json:"class_filter"`
  54. RingSeconds *int `json:"ring_seconds"`
  55. }
  56. type Manager struct {
  57. mu sync.RWMutex
  58. cfg config.Config
  59. }
  60. func New(cfg config.Config) *Manager {
  61. return &Manager{cfg: cfg}
  62. }
  63. func (m *Manager) Snapshot() config.Config {
  64. m.mu.RLock()
  65. defer m.mu.RUnlock()
  66. return m.cfg
  67. }
  68. func (m *Manager) Replace(cfg config.Config) {
  69. m.mu.Lock()
  70. defer m.mu.Unlock()
  71. m.cfg = cfg
  72. }
  73. func (m *Manager) ApplyConfig(update ConfigUpdate) (config.Config, error) {
  74. m.mu.Lock()
  75. defer m.mu.Unlock()
  76. next := m.cfg
  77. if update.CenterHz != nil {
  78. if *update.CenterHz < 1e3 || *update.CenterHz > 2e9 {
  79. return m.cfg, errors.New("center_hz out of range")
  80. }
  81. next.CenterHz = *update.CenterHz
  82. }
  83. if update.SampleRate != nil {
  84. if *update.SampleRate <= 0 {
  85. return m.cfg, errors.New("sample_rate must be > 0")
  86. }
  87. next.SampleRate = *update.SampleRate
  88. }
  89. if update.FFTSize != nil {
  90. if *update.FFTSize <= 0 {
  91. return m.cfg, errors.New("fft_size must be > 0")
  92. }
  93. if *update.FFTSize&(*update.FFTSize-1) != 0 {
  94. return m.cfg, errors.New("fft_size must be a power of 2")
  95. }
  96. next.FFTSize = *update.FFTSize
  97. }
  98. if update.GainDb != nil {
  99. next.GainDb = *update.GainDb
  100. }
  101. if update.TunerBwKHz != nil {
  102. if *update.TunerBwKHz <= 0 {
  103. return m.cfg, errors.New("tuner_bw_khz must be > 0")
  104. }
  105. next.TunerBwKHz = *update.TunerBwKHz
  106. }
  107. if update.UseGPUFFT != nil {
  108. next.UseGPUFFT = *update.UseGPUFFT
  109. }
  110. if update.Detector != nil {
  111. if update.Detector.ThresholdDb != nil {
  112. next.Detector.ThresholdDb = *update.Detector.ThresholdDb
  113. }
  114. if update.Detector.MinDuration != nil {
  115. if *update.Detector.MinDuration <= 0 {
  116. return m.cfg, errors.New("min_duration_ms must be > 0")
  117. }
  118. next.Detector.MinDurationMs = *update.Detector.MinDuration
  119. }
  120. if update.Detector.HoldMs != nil {
  121. if *update.Detector.HoldMs <= 0 {
  122. return m.cfg, errors.New("hold_ms must be > 0")
  123. }
  124. next.Detector.HoldMs = *update.Detector.HoldMs
  125. }
  126. if update.Detector.EmaAlpha != nil {
  127. v := *update.Detector.EmaAlpha
  128. if math.IsNaN(v) || math.IsInf(v, 0) || v < 0 || v > 1 {
  129. return m.cfg, errors.New("ema_alpha must be between 0 and 1")
  130. }
  131. next.Detector.EmaAlpha = v
  132. }
  133. if update.Detector.HysteresisDb != nil {
  134. v := *update.Detector.HysteresisDb
  135. if math.IsNaN(v) || math.IsInf(v, 0) || v < 0 {
  136. return m.cfg, errors.New("hysteresis_db must be >= 0")
  137. }
  138. next.Detector.HysteresisDb = v
  139. }
  140. if update.Detector.MinStableFrames != nil {
  141. if *update.Detector.MinStableFrames < 1 {
  142. return m.cfg, errors.New("min_stable_frames must be >= 1")
  143. }
  144. next.Detector.MinStableFrames = *update.Detector.MinStableFrames
  145. }
  146. if update.Detector.GapToleranceMs != nil {
  147. if *update.Detector.GapToleranceMs < 0 {
  148. return m.cfg, errors.New("gap_tolerance_ms must be >= 0")
  149. }
  150. next.Detector.GapToleranceMs = *update.Detector.GapToleranceMs
  151. }
  152. if update.Detector.CFARMode != nil {
  153. mode := strings.ToUpper(strings.TrimSpace(*update.Detector.CFARMode))
  154. switch mode {
  155. case "OFF", "CA", "OS", "GOSCA", "CASO":
  156. next.Detector.CFARMode = mode
  157. default:
  158. return m.cfg, errors.New("cfar_mode must be OFF, CA, OS, GOSCA, or CASO")
  159. }
  160. }
  161. if update.Detector.CFARWrapAround != nil {
  162. next.Detector.CFARWrapAround = *update.Detector.CFARWrapAround
  163. }
  164. if update.Detector.CFARGuardCells != nil {
  165. if *update.Detector.CFARGuardCells < 0 {
  166. return m.cfg, errors.New("cfar_guard_cells must be >= 0")
  167. }
  168. next.Detector.CFARGuardCells = *update.Detector.CFARGuardCells
  169. }
  170. if update.Detector.CFARTrainCells != nil {
  171. if *update.Detector.CFARTrainCells <= 0 {
  172. return m.cfg, errors.New("cfar_train_cells must be > 0")
  173. }
  174. next.Detector.CFARTrainCells = *update.Detector.CFARTrainCells
  175. }
  176. if update.Detector.CFARRank != nil {
  177. if *update.Detector.CFARRank <= 0 {
  178. return m.cfg, errors.New("cfar_rank must be > 0")
  179. }
  180. if next.Detector.CFARTrainCells > 0 && *update.Detector.CFARRank > 2*next.Detector.CFARTrainCells {
  181. return m.cfg, errors.New("cfar_rank must be <= 2 * cfar_train_cells")
  182. }
  183. next.Detector.CFARRank = *update.Detector.CFARRank
  184. }
  185. if update.Detector.CFARScaleDb != nil {
  186. next.Detector.CFARScaleDb = *update.Detector.CFARScaleDb
  187. }
  188. if update.Detector.EdgeMarginDb != nil {
  189. v := *update.Detector.EdgeMarginDb
  190. if math.IsNaN(v) || math.IsInf(v, 0) || v < 0 {
  191. return m.cfg, errors.New("edge_margin_db must be >= 0")
  192. }
  193. next.Detector.EdgeMarginDb = v
  194. }
  195. if update.Detector.MergeGapHz != nil {
  196. v := *update.Detector.MergeGapHz
  197. if math.IsNaN(v) || math.IsInf(v, 0) || v < 0 {
  198. return m.cfg, errors.New("merge_gap_hz must be >= 0")
  199. }
  200. next.Detector.MergeGapHz = v
  201. }
  202. }
  203. if update.Recorder != nil {
  204. if update.Recorder.Enabled != nil {
  205. next.Recorder.Enabled = *update.Recorder.Enabled
  206. }
  207. if update.Recorder.MinSNRDb != nil {
  208. next.Recorder.MinSNRDb = *update.Recorder.MinSNRDb
  209. }
  210. if update.Recorder.MinDuration != nil {
  211. next.Recorder.MinDuration = *update.Recorder.MinDuration
  212. }
  213. if update.Recorder.MaxDuration != nil {
  214. next.Recorder.MaxDuration = *update.Recorder.MaxDuration
  215. }
  216. if update.Recorder.PrerollMs != nil {
  217. next.Recorder.PrerollMs = *update.Recorder.PrerollMs
  218. }
  219. if update.Recorder.RecordIQ != nil {
  220. next.Recorder.RecordIQ = *update.Recorder.RecordIQ
  221. }
  222. if update.Recorder.RecordAudio != nil {
  223. next.Recorder.RecordAudio = *update.Recorder.RecordAudio
  224. }
  225. if update.Recorder.AutoDemod != nil {
  226. next.Recorder.AutoDemod = *update.Recorder.AutoDemod
  227. }
  228. if update.Recorder.AutoDecode != nil {
  229. next.Recorder.AutoDecode = *update.Recorder.AutoDecode
  230. }
  231. if update.Recorder.MaxDiskMB != nil {
  232. next.Recorder.MaxDiskMB = *update.Recorder.MaxDiskMB
  233. }
  234. if update.Recorder.OutputDir != nil {
  235. next.Recorder.OutputDir = *update.Recorder.OutputDir
  236. }
  237. if update.Recorder.ClassFilter != nil {
  238. next.Recorder.ClassFilter = *update.Recorder.ClassFilter
  239. }
  240. if update.Recorder.RingSeconds != nil {
  241. next.Recorder.RingSeconds = *update.Recorder.RingSeconds
  242. }
  243. }
  244. m.cfg = next
  245. return m.cfg, nil
  246. }
  247. func (m *Manager) ApplySettings(update SettingsUpdate) (config.Config, error) {
  248. m.mu.Lock()
  249. defer m.mu.Unlock()
  250. next := m.cfg
  251. if update.AGC != nil {
  252. next.AGC = *update.AGC
  253. }
  254. if update.DCBlock != nil {
  255. next.DCBlock = *update.DCBlock
  256. }
  257. if update.IQBalance != nil {
  258. next.IQBalance = *update.IQBalance
  259. }
  260. m.cfg = next
  261. return m.cfg, nil
  262. }