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.

429 line
15KB

  1. package config
  2. import (
  3. "math"
  4. "os"
  5. "time"
  6. "gopkg.in/yaml.v3"
  7. )
  8. type Band struct {
  9. Name string `yaml:"name" json:"name"`
  10. StartHz float64 `yaml:"start_hz" json:"start_hz"`
  11. EndHz float64 `yaml:"end_hz" json:"end_hz"`
  12. }
  13. type DetectorConfig struct {
  14. ThresholdDb float64 `yaml:"threshold_db" json:"threshold_db"`
  15. MinDurationMs int `yaml:"min_duration_ms" json:"min_duration_ms"`
  16. HoldMs int `yaml:"hold_ms" json:"hold_ms"`
  17. EmaAlpha float64 `yaml:"ema_alpha" json:"ema_alpha"`
  18. HysteresisDb float64 `yaml:"hysteresis_db" json:"hysteresis_db"`
  19. MinStableFrames int `yaml:"min_stable_frames" json:"min_stable_frames"`
  20. GapToleranceMs int `yaml:"gap_tolerance_ms" json:"gap_tolerance_ms"`
  21. CFARMode string `yaml:"cfar_mode" json:"cfar_mode"`
  22. CFARGuardHz float64 `yaml:"cfar_guard_hz" json:"cfar_guard_hz"`
  23. CFARTrainHz float64 `yaml:"cfar_train_hz" json:"cfar_train_hz"`
  24. CFARGuardCells int `yaml:"cfar_guard_cells,omitempty" json:"cfar_guard_cells,omitempty"`
  25. CFARTrainCells int `yaml:"cfar_train_cells,omitempty" json:"cfar_train_cells,omitempty"`
  26. CFARRank int `yaml:"cfar_rank" json:"cfar_rank"`
  27. CFARScaleDb float64 `yaml:"cfar_scale_db" json:"cfar_scale_db"`
  28. CFARWrapAround bool `yaml:"cfar_wrap_around" json:"cfar_wrap_around"`
  29. EdgeMarginDb float64 `yaml:"edge_margin_db" json:"edge_margin_db"`
  30. MaxSignalBwHz float64 `yaml:"max_signal_bw_hz" json:"max_signal_bw_hz"`
  31. MergeGapHz float64 `yaml:"merge_gap_hz" json:"merge_gap_hz"`
  32. ClassHistorySize int `yaml:"class_history_size" json:"class_history_size"`
  33. ClassSwitchRatio float64 `yaml:"class_switch_ratio" json:"class_switch_ratio"`
  34. // Deprecated (backward compatibility)
  35. CFAREnabled *bool `yaml:"cfar_enabled,omitempty" json:"cfar_enabled,omitempty"`
  36. }
  37. type RecorderConfig struct {
  38. Enabled bool `yaml:"enabled" json:"enabled"`
  39. MinSNRDb float64 `yaml:"min_snr_db" json:"min_snr_db"`
  40. MinDuration string `yaml:"min_duration" json:"min_duration"`
  41. MaxDuration string `yaml:"max_duration" json:"max_duration"`
  42. PrerollMs int `yaml:"preroll_ms" json:"preroll_ms"`
  43. RecordIQ bool `yaml:"record_iq" json:"record_iq"`
  44. RecordAudio bool `yaml:"record_audio" json:"record_audio"`
  45. AutoDemod bool `yaml:"auto_demod" json:"auto_demod"`
  46. AutoDecode bool `yaml:"auto_decode" json:"auto_decode"`
  47. MaxDiskMB int `yaml:"max_disk_mb" json:"max_disk_mb"`
  48. OutputDir string `yaml:"output_dir" json:"output_dir"`
  49. ClassFilter []string `yaml:"class_filter" json:"class_filter"`
  50. RingSeconds int `yaml:"ring_seconds" json:"ring_seconds"`
  51. // Audio quality settings (AQ-2, AQ-3, AQ-5)
  52. DeemphasisUs float64 `yaml:"deemphasis_us" json:"deemphasis_us"` // De-emphasis time constant in µs. 50=Europe, 75=US/Japan, 0=disabled. Default: 50
  53. ExtractionTaps int `yaml:"extraction_fir_taps" json:"extraction_fir_taps"` // FIR tap count for extraction filter. Default: 101, max 301
  54. ExtractionBwMult float64 `yaml:"extraction_bw_mult" json:"extraction_bw_mult"` // BW multiplier for extraction. Default: 1.2 (20% wider than detected)
  55. }
  56. type DecoderConfig struct {
  57. FT8Cmd string `yaml:"ft8_cmd" json:"ft8_cmd"`
  58. WSPRCmd string `yaml:"wspr_cmd" json:"wspr_cmd"`
  59. DMRCmd string `yaml:"dmr_cmd" json:"dmr_cmd"`
  60. DStarCmd string `yaml:"dstar_cmd" json:"dstar_cmd"`
  61. FSKCmd string `yaml:"fsk_cmd" json:"fsk_cmd"`
  62. PSKCmd string `yaml:"psk_cmd" json:"psk_cmd"`
  63. }
  64. type PipelineGoalConfig struct {
  65. Intent string `yaml:"intent" json:"intent"`
  66. MonitorStartHz float64 `yaml:"monitor_start_hz" json:"monitor_start_hz"`
  67. MonitorEndHz float64 `yaml:"monitor_end_hz" json:"monitor_end_hz"`
  68. MonitorSpanHz float64 `yaml:"monitor_span_hz" json:"monitor_span_hz"`
  69. SignalPriorities []string `yaml:"signal_priorities" json:"signal_priorities"`
  70. AutoRecordClasses []string `yaml:"auto_record_classes" json:"auto_record_classes"`
  71. AutoDecodeClasses []string `yaml:"auto_decode_classes" json:"auto_decode_classes"`
  72. }
  73. type PipelineConfig struct {
  74. Mode string `yaml:"mode" json:"mode"`
  75. Goals PipelineGoalConfig `yaml:"goals" json:"goals"`
  76. }
  77. type SurveillanceConfig struct {
  78. AnalysisFFTSize int `yaml:"analysis_fft_size" json:"analysis_fft_size"`
  79. FrameRate int `yaml:"frame_rate" json:"frame_rate"`
  80. Strategy string `yaml:"strategy" json:"strategy"`
  81. DisplayBins int `yaml:"display_bins" json:"display_bins"`
  82. DisplayFPS int `yaml:"display_fps" json:"display_fps"`
  83. }
  84. type RefinementConfig struct {
  85. Enabled bool `yaml:"enabled" json:"enabled"`
  86. MaxConcurrent int `yaml:"max_concurrent" json:"max_concurrent"`
  87. MinCandidateSNRDb float64 `yaml:"min_candidate_snr_db" json:"min_candidate_snr_db"`
  88. }
  89. type ResourceConfig struct {
  90. PreferGPU bool `yaml:"prefer_gpu" json:"prefer_gpu"`
  91. MaxRefinementJobs int `yaml:"max_refinement_jobs" json:"max_refinement_jobs"`
  92. MaxRecordingStreams int `yaml:"max_recording_streams" json:"max_recording_streams"`
  93. }
  94. type ProfileConfig struct {
  95. Name string `yaml:"name" json:"name"`
  96. Description string `yaml:"description" json:"description"`
  97. Pipeline *PipelineConfig `yaml:"pipeline,omitempty" json:"pipeline,omitempty"`
  98. Surveillance *SurveillanceConfig `yaml:"surveillance,omitempty" json:"surveillance,omitempty"`
  99. Refinement *RefinementConfig `yaml:"refinement,omitempty" json:"refinement,omitempty"`
  100. Resources *ResourceConfig `yaml:"resources,omitempty" json:"resources,omitempty"`
  101. }
  102. type Config struct {
  103. Bands []Band `yaml:"bands" json:"bands"`
  104. CenterHz float64 `yaml:"center_hz" json:"center_hz"`
  105. SampleRate int `yaml:"sample_rate" json:"sample_rate"`
  106. FFTSize int `yaml:"fft_size" json:"fft_size"`
  107. GainDb float64 `yaml:"gain_db" json:"gain_db"`
  108. TunerBwKHz int `yaml:"tuner_bw_khz" json:"tuner_bw_khz"`
  109. UseGPUFFT bool `yaml:"use_gpu_fft" json:"use_gpu_fft"`
  110. ClassifierMode string `yaml:"classifier_mode" json:"classifier_mode"`
  111. AGC bool `yaml:"agc" json:"agc"`
  112. DCBlock bool `yaml:"dc_block" json:"dc_block"`
  113. IQBalance bool `yaml:"iq_balance" json:"iq_balance"`
  114. Pipeline PipelineConfig `yaml:"pipeline" json:"pipeline"`
  115. Surveillance SurveillanceConfig `yaml:"surveillance" json:"surveillance"`
  116. Refinement RefinementConfig `yaml:"refinement" json:"refinement"`
  117. Resources ResourceConfig `yaml:"resources" json:"resources"`
  118. Profiles []ProfileConfig `yaml:"profiles" json:"profiles"`
  119. Detector DetectorConfig `yaml:"detector" json:"detector"`
  120. Recorder RecorderConfig `yaml:"recorder" json:"recorder"`
  121. Decoder DecoderConfig `yaml:"decoder" json:"decoder"`
  122. WebAddr string `yaml:"web_addr" json:"web_addr"`
  123. EventPath string `yaml:"event_path" json:"event_path"`
  124. FrameRate int `yaml:"frame_rate" json:"frame_rate"`
  125. WaterfallLines int `yaml:"waterfall_lines" json:"waterfall_lines"`
  126. WebRoot string `yaml:"web_root" json:"web_root"`
  127. }
  128. func Default() Config {
  129. return Config{
  130. Bands: []Band{
  131. {Name: "example", StartHz: 99.5e6, EndHz: 100.5e6},
  132. },
  133. CenterHz: 100.0e6,
  134. SampleRate: 2_048_000,
  135. FFTSize: 2048,
  136. GainDb: 30,
  137. TunerBwKHz: 1536,
  138. UseGPUFFT: false,
  139. ClassifierMode: "combined",
  140. AGC: false,
  141. DCBlock: false,
  142. IQBalance: false,
  143. Pipeline: PipelineConfig{
  144. Mode: "legacy",
  145. Goals: PipelineGoalConfig{
  146. Intent: "general-monitoring",
  147. },
  148. },
  149. Surveillance: SurveillanceConfig{
  150. AnalysisFFTSize: 2048,
  151. FrameRate: 15,
  152. Strategy: "single-resolution",
  153. DisplayBins: 2048,
  154. DisplayFPS: 15,
  155. },
  156. Refinement: RefinementConfig{
  157. Enabled: true,
  158. MaxConcurrent: 8,
  159. MinCandidateSNRDb: 0,
  160. },
  161. Resources: ResourceConfig{
  162. PreferGPU: true,
  163. MaxRefinementJobs: 8,
  164. MaxRecordingStreams: 16,
  165. },
  166. Profiles: []ProfileConfig{
  167. {Name: "legacy", Description: "Current single-band pipeline behavior", Pipeline: &PipelineConfig{Mode: "legacy", Goals: PipelineGoalConfig{Intent: "general-monitoring"}}},
  168. {Name: "wideband-balanced", Description: "Prepared baseline for scalable wideband surveillance", Pipeline: &PipelineConfig{Mode: "wideband-balanced", Goals: PipelineGoalConfig{Intent: "wideband-surveillance"}}},
  169. {Name: "wideband-aggressive", Description: "Higher surveillance/refinement budgets for future broad-span monitoring", Pipeline: &PipelineConfig{Mode: "wideband-aggressive", Goals: PipelineGoalConfig{Intent: "high-density-wideband-surveillance"}}},
  170. {Name: "archive", Description: "Record-first monitoring profile", Pipeline: &PipelineConfig{Mode: "archive", Goals: PipelineGoalConfig{Intent: "archive-and-triage"}}},
  171. },
  172. Detector: DetectorConfig{
  173. ThresholdDb: -20,
  174. MinDurationMs: 250,
  175. HoldMs: 500,
  176. EmaAlpha: 0.2,
  177. HysteresisDb: 3,
  178. MinStableFrames: 3,
  179. GapToleranceMs: 500,
  180. CFARMode: "GOSCA",
  181. CFARGuardHz: 500,
  182. CFARTrainHz: 5000,
  183. CFARGuardCells: 3,
  184. CFARTrainCells: 24,
  185. CFARRank: 36,
  186. CFARScaleDb: 6,
  187. CFARWrapAround: true,
  188. EdgeMarginDb: 3.0,
  189. MaxSignalBwHz: 150000,
  190. MergeGapHz: 5000,
  191. ClassHistorySize: 10,
  192. ClassSwitchRatio: 0.6,
  193. },
  194. Recorder: RecorderConfig{
  195. Enabled: false,
  196. MinSNRDb: 10,
  197. MinDuration: "1s",
  198. MaxDuration: "300s",
  199. PrerollMs: 500,
  200. RecordIQ: true,
  201. RecordAudio: false,
  202. AutoDemod: true,
  203. AutoDecode: false,
  204. MaxDiskMB: 0,
  205. OutputDir: "data/recordings",
  206. RingSeconds: 8,
  207. DeemphasisUs: 50,
  208. ExtractionTaps: 101,
  209. ExtractionBwMult: 1.2,
  210. },
  211. Decoder: DecoderConfig{},
  212. WebAddr: ":8080",
  213. EventPath: "data/events.jsonl",
  214. FrameRate: 15,
  215. WaterfallLines: 200,
  216. WebRoot: "web",
  217. }
  218. }
  219. func Load(path string) (Config, error) {
  220. cfg := Default()
  221. if b, err := os.ReadFile(autosavePath(path)); err == nil {
  222. if err := yaml.Unmarshal(b, &cfg); err == nil {
  223. return applyDefaults(cfg), nil
  224. }
  225. }
  226. b, err := os.ReadFile(path)
  227. if err != nil {
  228. return cfg, err
  229. }
  230. if err := yaml.Unmarshal(b, &cfg); err != nil {
  231. return cfg, err
  232. }
  233. return applyDefaults(cfg), nil
  234. }
  235. func applyDefaults(cfg Config) Config {
  236. if cfg.Detector.MinDurationMs <= 0 {
  237. cfg.Detector.MinDurationMs = 250
  238. }
  239. if cfg.Detector.HoldMs <= 0 {
  240. cfg.Detector.HoldMs = 500
  241. }
  242. if cfg.Detector.MinStableFrames <= 0 {
  243. cfg.Detector.MinStableFrames = 3
  244. }
  245. if cfg.Detector.GapToleranceMs <= 0 {
  246. cfg.Detector.GapToleranceMs = cfg.Detector.HoldMs
  247. }
  248. if cfg.Detector.CFARMode == "" {
  249. if cfg.Detector.CFAREnabled != nil {
  250. if *cfg.Detector.CFAREnabled {
  251. cfg.Detector.CFARMode = "OS"
  252. } else {
  253. cfg.Detector.CFARMode = "OFF"
  254. }
  255. } else {
  256. cfg.Detector.CFARMode = "GOSCA"
  257. }
  258. }
  259. if cfg.Detector.CFARGuardHz <= 0 && cfg.Detector.CFARGuardCells > 0 {
  260. cfg.Detector.CFARGuardHz = float64(cfg.Detector.CFARGuardCells) * 62.5
  261. }
  262. if cfg.Detector.CFARTrainHz <= 0 && cfg.Detector.CFARTrainCells > 0 {
  263. cfg.Detector.CFARTrainHz = float64(cfg.Detector.CFARTrainCells) * 62.5
  264. }
  265. if cfg.Detector.CFARGuardHz <= 0 {
  266. cfg.Detector.CFARGuardHz = 500
  267. }
  268. if cfg.Detector.CFARTrainHz <= 0 {
  269. cfg.Detector.CFARTrainHz = 5000
  270. }
  271. if cfg.Detector.CFARGuardCells <= 0 {
  272. cfg.Detector.CFARGuardCells = 3
  273. }
  274. if cfg.Detector.CFARTrainCells <= 0 {
  275. cfg.Detector.CFARTrainCells = 24
  276. }
  277. if cfg.Detector.CFARRank <= 0 || cfg.Detector.CFARRank > 2*cfg.Detector.CFARTrainCells {
  278. cfg.Detector.CFARRank = int(math.Round(0.75 * float64(2*cfg.Detector.CFARTrainCells)))
  279. if cfg.Detector.CFARRank <= 0 {
  280. cfg.Detector.CFARRank = 1
  281. }
  282. }
  283. if cfg.Detector.CFARScaleDb <= 0 {
  284. cfg.Detector.CFARScaleDb = 6
  285. }
  286. if cfg.Detector.EdgeMarginDb <= 0 {
  287. cfg.Detector.EdgeMarginDb = 3.0
  288. }
  289. if cfg.Detector.MaxSignalBwHz <= 0 {
  290. cfg.Detector.MaxSignalBwHz = 150000
  291. }
  292. if cfg.Detector.MergeGapHz <= 0 {
  293. cfg.Detector.MergeGapHz = 5000
  294. }
  295. if cfg.Detector.ClassHistorySize <= 0 {
  296. cfg.Detector.ClassHistorySize = 10
  297. }
  298. if cfg.Detector.ClassSwitchRatio <= 0 || cfg.Detector.ClassSwitchRatio > 1 {
  299. cfg.Detector.ClassSwitchRatio = 0.6
  300. }
  301. if cfg.Pipeline.Mode == "" {
  302. cfg.Pipeline.Mode = "legacy"
  303. }
  304. if cfg.Pipeline.Goals.Intent == "" {
  305. cfg.Pipeline.Goals.Intent = "general-monitoring"
  306. }
  307. if cfg.Pipeline.Goals.MonitorSpanHz <= 0 && cfg.Pipeline.Goals.MonitorStartHz != 0 && cfg.Pipeline.Goals.MonitorEndHz != 0 && cfg.Pipeline.Goals.MonitorEndHz > cfg.Pipeline.Goals.MonitorStartHz {
  308. cfg.Pipeline.Goals.MonitorSpanHz = cfg.Pipeline.Goals.MonitorEndHz - cfg.Pipeline.Goals.MonitorStartHz
  309. }
  310. if cfg.Surveillance.AnalysisFFTSize <= 0 {
  311. cfg.Surveillance.AnalysisFFTSize = cfg.FFTSize
  312. }
  313. if cfg.Surveillance.FrameRate <= 0 {
  314. cfg.Surveillance.FrameRate = cfg.FrameRate
  315. }
  316. if cfg.Surveillance.Strategy == "" {
  317. cfg.Surveillance.Strategy = "single-resolution"
  318. }
  319. if cfg.Surveillance.DisplayBins <= 0 {
  320. cfg.Surveillance.DisplayBins = cfg.FFTSize
  321. }
  322. if cfg.Surveillance.DisplayFPS <= 0 {
  323. cfg.Surveillance.DisplayFPS = cfg.FrameRate
  324. }
  325. if !cfg.Refinement.Enabled {
  326. // keep explicit false if user disabled it; enable by default only when unset-like zero config
  327. if cfg.Refinement.MaxConcurrent == 0 && cfg.Refinement.MinCandidateSNRDb == 0 {
  328. cfg.Refinement.Enabled = true
  329. }
  330. }
  331. if cfg.Refinement.MaxConcurrent <= 0 {
  332. cfg.Refinement.MaxConcurrent = 8
  333. }
  334. if cfg.Resources.MaxRefinementJobs <= 0 {
  335. cfg.Resources.MaxRefinementJobs = cfg.Refinement.MaxConcurrent
  336. }
  337. if cfg.Resources.MaxRecordingStreams <= 0 {
  338. cfg.Resources.MaxRecordingStreams = 16
  339. }
  340. if cfg.FrameRate <= 0 {
  341. cfg.FrameRate = 15
  342. }
  343. if cfg.WaterfallLines <= 0 {
  344. cfg.WaterfallLines = 200
  345. }
  346. if cfg.WebRoot == "" {
  347. cfg.WebRoot = "web"
  348. }
  349. if cfg.WebAddr == "" {
  350. cfg.WebAddr = ":8080"
  351. }
  352. if cfg.EventPath == "" {
  353. cfg.EventPath = "data/events.jsonl"
  354. }
  355. if cfg.SampleRate <= 0 {
  356. cfg.SampleRate = 2_048_000
  357. }
  358. if cfg.ClassifierMode == "" {
  359. cfg.ClassifierMode = "combined"
  360. }
  361. switch cfg.ClassifierMode {
  362. case "rule", "math", "combined":
  363. default:
  364. cfg.ClassifierMode = "combined"
  365. }
  366. if cfg.FFTSize <= 0 {
  367. cfg.FFTSize = 2048
  368. }
  369. if cfg.Surveillance.AnalysisFFTSize > 0 {
  370. cfg.FFTSize = cfg.Surveillance.AnalysisFFTSize
  371. } else {
  372. cfg.Surveillance.AnalysisFFTSize = cfg.FFTSize
  373. }
  374. if cfg.TunerBwKHz <= 0 {
  375. cfg.TunerBwKHz = 1536
  376. }
  377. if cfg.CenterHz == 0 {
  378. cfg.CenterHz = 100.0e6
  379. }
  380. if cfg.Recorder.OutputDir == "" {
  381. cfg.Recorder.OutputDir = "data/recordings"
  382. }
  383. if cfg.Recorder.RingSeconds <= 0 {
  384. cfg.Recorder.RingSeconds = 8
  385. }
  386. if cfg.Recorder.DeemphasisUs == 0 {
  387. cfg.Recorder.DeemphasisUs = 50
  388. }
  389. if cfg.Recorder.ExtractionTaps <= 0 {
  390. cfg.Recorder.ExtractionTaps = 101
  391. }
  392. if cfg.Recorder.ExtractionTaps > 301 {
  393. cfg.Recorder.ExtractionTaps = 301
  394. }
  395. if cfg.Recorder.ExtractionTaps%2 == 0 {
  396. cfg.Recorder.ExtractionTaps++ // must be odd
  397. }
  398. if cfg.Recorder.ExtractionBwMult <= 0 {
  399. cfg.Recorder.ExtractionBwMult = 1.2
  400. }
  401. return cfg
  402. }
  403. func (c Config) FrameInterval() time.Duration {
  404. fps := c.FrameRate
  405. if fps <= 0 {
  406. fps = 15
  407. }
  408. return time.Second / time.Duration(fps)
  409. }