Wideband autonomous SDR analysis engine forked from sdr-visual-suite
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

644 строки
22KB

  1. package config
  2. import (
  3. "math"
  4. "os"
  5. "strings"
  6. "time"
  7. "gopkg.in/yaml.v3"
  8. )
  9. type Band struct {
  10. Name string `yaml:"name" json:"name"`
  11. StartHz float64 `yaml:"start_hz" json:"start_hz"`
  12. EndHz float64 `yaml:"end_hz" json:"end_hz"`
  13. }
  14. type MonitorWindow struct {
  15. Label string `yaml:"label" json:"label"`
  16. Zone string `yaml:"zone" json:"zone"`
  17. StartHz float64 `yaml:"start_hz" json:"start_hz"`
  18. EndHz float64 `yaml:"end_hz" json:"end_hz"`
  19. CenterHz float64 `yaml:"center_hz" json:"center_hz"`
  20. SpanHz float64 `yaml:"span_hz" json:"span_hz"`
  21. Priority float64 `yaml:"priority" json:"priority"`
  22. AutoRecord bool `yaml:"auto_record" json:"auto_record"`
  23. AutoDecode bool `yaml:"auto_decode" json:"auto_decode"`
  24. }
  25. type DetectorConfig struct {
  26. ThresholdDb float64 `yaml:"threshold_db" json:"threshold_db"`
  27. MinDurationMs int `yaml:"min_duration_ms" json:"min_duration_ms"`
  28. HoldMs int `yaml:"hold_ms" json:"hold_ms"`
  29. EmaAlpha float64 `yaml:"ema_alpha" json:"ema_alpha"`
  30. HysteresisDb float64 `yaml:"hysteresis_db" json:"hysteresis_db"`
  31. MinStableFrames int `yaml:"min_stable_frames" json:"min_stable_frames"`
  32. GapToleranceMs int `yaml:"gap_tolerance_ms" json:"gap_tolerance_ms"`
  33. CFARMode string `yaml:"cfar_mode" json:"cfar_mode"`
  34. CFARGuardHz float64 `yaml:"cfar_guard_hz" json:"cfar_guard_hz"`
  35. CFARTrainHz float64 `yaml:"cfar_train_hz" json:"cfar_train_hz"`
  36. CFARGuardCells int `yaml:"cfar_guard_cells,omitempty" json:"cfar_guard_cells,omitempty"`
  37. CFARTrainCells int `yaml:"cfar_train_cells,omitempty" json:"cfar_train_cells,omitempty"`
  38. CFARRank int `yaml:"cfar_rank" json:"cfar_rank"`
  39. CFARScaleDb float64 `yaml:"cfar_scale_db" json:"cfar_scale_db"`
  40. CFARWrapAround bool `yaml:"cfar_wrap_around" json:"cfar_wrap_around"`
  41. EdgeMarginDb float64 `yaml:"edge_margin_db" json:"edge_margin_db"`
  42. MaxSignalBwHz float64 `yaml:"max_signal_bw_hz" json:"max_signal_bw_hz"`
  43. MergeGapHz float64 `yaml:"merge_gap_hz" json:"merge_gap_hz"`
  44. ClassHistorySize int `yaml:"class_history_size" json:"class_history_size"`
  45. ClassSwitchRatio float64 `yaml:"class_switch_ratio" json:"class_switch_ratio"`
  46. // Deprecated (backward compatibility)
  47. CFAREnabled *bool `yaml:"cfar_enabled,omitempty" json:"cfar_enabled,omitempty"`
  48. }
  49. type RecorderConfig struct {
  50. Enabled bool `yaml:"enabled" json:"enabled"`
  51. MinSNRDb float64 `yaml:"min_snr_db" json:"min_snr_db"`
  52. MinDuration string `yaml:"min_duration" json:"min_duration"`
  53. MaxDuration string `yaml:"max_duration" json:"max_duration"`
  54. PrerollMs int `yaml:"preroll_ms" json:"preroll_ms"`
  55. RecordIQ bool `yaml:"record_iq" json:"record_iq"`
  56. RecordAudio bool `yaml:"record_audio" json:"record_audio"`
  57. AutoDemod bool `yaml:"auto_demod" json:"auto_demod"`
  58. AutoDecode bool `yaml:"auto_decode" json:"auto_decode"`
  59. MaxDiskMB int `yaml:"max_disk_mb" json:"max_disk_mb"`
  60. OutputDir string `yaml:"output_dir" json:"output_dir"`
  61. ClassFilter []string `yaml:"class_filter" json:"class_filter"`
  62. RingSeconds int `yaml:"ring_seconds" json:"ring_seconds"`
  63. // Audio quality settings (AQ-2, AQ-3, AQ-5)
  64. DeemphasisUs float64 `yaml:"deemphasis_us" json:"deemphasis_us"` // De-emphasis time constant in µs. 50=Europe, 75=US/Japan, 0=disabled. Default: 50
  65. ExtractionTaps int `yaml:"extraction_fir_taps" json:"extraction_fir_taps"` // FIR tap count for extraction filter. Default: 101, max 301
  66. ExtractionBwMult float64 `yaml:"extraction_bw_mult" json:"extraction_bw_mult"` // BW multiplier for extraction. Default: 1.2 (20% wider than detected)
  67. DebugLiveAudio bool `yaml:"debug_live_audio" json:"debug_live_audio"`
  68. }
  69. type DecoderConfig struct {
  70. FT8Cmd string `yaml:"ft8_cmd" json:"ft8_cmd"`
  71. WSPRCmd string `yaml:"wspr_cmd" json:"wspr_cmd"`
  72. DMRCmd string `yaml:"dmr_cmd" json:"dmr_cmd"`
  73. DStarCmd string `yaml:"dstar_cmd" json:"dstar_cmd"`
  74. FSKCmd string `yaml:"fsk_cmd" json:"fsk_cmd"`
  75. PSKCmd string `yaml:"psk_cmd" json:"psk_cmd"`
  76. }
  77. type PipelineGoalConfig struct {
  78. Intent string `yaml:"intent" json:"intent"`
  79. MonitorStartHz float64 `yaml:"monitor_start_hz" json:"monitor_start_hz"`
  80. MonitorEndHz float64 `yaml:"monitor_end_hz" json:"monitor_end_hz"`
  81. MonitorSpanHz float64 `yaml:"monitor_span_hz" json:"monitor_span_hz"`
  82. MonitorWindows []MonitorWindow `yaml:"monitor_windows" json:"monitor_windows"`
  83. SignalPriorities []string `yaml:"signal_priorities" json:"signal_priorities"`
  84. AutoRecordClasses []string `yaml:"auto_record_classes" json:"auto_record_classes"`
  85. AutoDecodeClasses []string `yaml:"auto_decode_classes" json:"auto_decode_classes"`
  86. }
  87. type PipelineConfig struct {
  88. Mode string `yaml:"mode" json:"mode"`
  89. Profile string `yaml:"profile,omitempty" json:"profile,omitempty"`
  90. Goals PipelineGoalConfig `yaml:"goals" json:"goals"`
  91. }
  92. type SurveillanceConfig struct {
  93. AnalysisFFTSize int `yaml:"analysis_fft_size" json:"analysis_fft_size"`
  94. FrameRate int `yaml:"frame_rate" json:"frame_rate"`
  95. Strategy string `yaml:"strategy" json:"strategy"`
  96. DisplayBins int `yaml:"display_bins" json:"display_bins"`
  97. DisplayFPS int `yaml:"display_fps" json:"display_fps"`
  98. DerivedDetection string `yaml:"derived_detection" json:"derived_detection"`
  99. }
  100. type RefinementConfig struct {
  101. Enabled bool `yaml:"enabled" json:"enabled"`
  102. MaxConcurrent int `yaml:"max_concurrent" json:"max_concurrent"`
  103. DetailFFTSize int `yaml:"detail_fft_size" json:"detail_fft_size"`
  104. MinCandidateSNRDb float64 `yaml:"min_candidate_snr_db" json:"min_candidate_snr_db"`
  105. MinSpanHz float64 `yaml:"min_span_hz" json:"min_span_hz"`
  106. MaxSpanHz float64 `yaml:"max_span_hz" json:"max_span_hz"`
  107. AutoSpan *bool `yaml:"auto_span" json:"auto_span"`
  108. }
  109. type ResourceConfig struct {
  110. PreferGPU bool `yaml:"prefer_gpu" json:"prefer_gpu"`
  111. MaxRefinementJobs int `yaml:"max_refinement_jobs" json:"max_refinement_jobs"`
  112. MaxRecordingStreams int `yaml:"max_recording_streams" json:"max_recording_streams"`
  113. MaxDecodeJobs int `yaml:"max_decode_jobs" json:"max_decode_jobs"`
  114. DecisionHoldMs int `yaml:"decision_hold_ms" json:"decision_hold_ms"`
  115. }
  116. type ProfileConfig struct {
  117. Name string `yaml:"name" json:"name"`
  118. Description string `yaml:"description" json:"description"`
  119. Pipeline *PipelineConfig `yaml:"pipeline,omitempty" json:"pipeline,omitempty"`
  120. Surveillance *SurveillanceConfig `yaml:"surveillance,omitempty" json:"surveillance,omitempty"`
  121. Refinement *RefinementConfig `yaml:"refinement,omitempty" json:"refinement,omitempty"`
  122. Resources *ResourceConfig `yaml:"resources,omitempty" json:"resources,omitempty"`
  123. }
  124. type Config struct {
  125. Bands []Band `yaml:"bands" json:"bands"`
  126. CenterHz float64 `yaml:"center_hz" json:"center_hz"`
  127. SampleRate int `yaml:"sample_rate" json:"sample_rate"`
  128. FFTSize int `yaml:"fft_size" json:"fft_size"`
  129. GainDb float64 `yaml:"gain_db" json:"gain_db"`
  130. TunerBwKHz int `yaml:"tuner_bw_khz" json:"tuner_bw_khz"`
  131. UseGPUFFT bool `yaml:"use_gpu_fft" json:"use_gpu_fft"`
  132. ClassifierMode string `yaml:"classifier_mode" json:"classifier_mode"`
  133. AGC bool `yaml:"agc" json:"agc"`
  134. DCBlock bool `yaml:"dc_block" json:"dc_block"`
  135. IQBalance bool `yaml:"iq_balance" json:"iq_balance"`
  136. Pipeline PipelineConfig `yaml:"pipeline" json:"pipeline"`
  137. Surveillance SurveillanceConfig `yaml:"surveillance" json:"surveillance"`
  138. Refinement RefinementConfig `yaml:"refinement" json:"refinement"`
  139. Resources ResourceConfig `yaml:"resources" json:"resources"`
  140. Profiles []ProfileConfig `yaml:"profiles" json:"profiles"`
  141. Detector DetectorConfig `yaml:"detector" json:"detector"`
  142. Recorder RecorderConfig `yaml:"recorder" json:"recorder"`
  143. Decoder DecoderConfig `yaml:"decoder" json:"decoder"`
  144. WebAddr string `yaml:"web_addr" json:"web_addr"`
  145. EventPath string `yaml:"event_path" json:"event_path"`
  146. FrameRate int `yaml:"frame_rate" json:"frame_rate"`
  147. WaterfallLines int `yaml:"waterfall_lines" json:"waterfall_lines"`
  148. WebRoot string `yaml:"web_root" json:"web_root"`
  149. }
  150. func Default() Config {
  151. return Config{
  152. Bands: []Band{
  153. {Name: "example", StartHz: 99.5e6, EndHz: 100.5e6},
  154. },
  155. CenterHz: 100.0e6,
  156. SampleRate: 2_048_000,
  157. FFTSize: 2048,
  158. GainDb: 30,
  159. TunerBwKHz: 1536,
  160. UseGPUFFT: false,
  161. ClassifierMode: "combined",
  162. AGC: false,
  163. DCBlock: false,
  164. IQBalance: false,
  165. Pipeline: PipelineConfig{
  166. Mode: "legacy",
  167. Goals: PipelineGoalConfig{
  168. Intent: "general-monitoring",
  169. },
  170. },
  171. Surveillance: SurveillanceConfig{
  172. AnalysisFFTSize: 2048,
  173. FrameRate: 15,
  174. Strategy: "single-resolution",
  175. DisplayBins: 2048,
  176. DisplayFPS: 15,
  177. DerivedDetection: "auto",
  178. },
  179. Refinement: RefinementConfig{
  180. Enabled: true,
  181. MaxConcurrent: 8,
  182. DetailFFTSize: 0,
  183. MinCandidateSNRDb: 0,
  184. MinSpanHz: 0,
  185. MaxSpanHz: 0,
  186. AutoSpan: boolPtr(true),
  187. },
  188. Resources: ResourceConfig{
  189. PreferGPU: true,
  190. MaxRefinementJobs: 8,
  191. MaxRecordingStreams: 16,
  192. MaxDecodeJobs: 16,
  193. DecisionHoldMs: 2000,
  194. },
  195. Profiles: []ProfileConfig{
  196. {
  197. Name: "legacy",
  198. Description: "Current single-band pipeline behavior",
  199. Pipeline: &PipelineConfig{Mode: "legacy", Profile: "legacy", Goals: PipelineGoalConfig{Intent: "general-monitoring"}},
  200. Surveillance: &SurveillanceConfig{
  201. AnalysisFFTSize: 2048,
  202. FrameRate: 15,
  203. Strategy: "single-resolution",
  204. DisplayBins: 2048,
  205. DisplayFPS: 15,
  206. DerivedDetection: "auto",
  207. },
  208. Refinement: &RefinementConfig{
  209. Enabled: true,
  210. MaxConcurrent: 8,
  211. DetailFFTSize: 0,
  212. MinCandidateSNRDb: 0,
  213. MinSpanHz: 0,
  214. MaxSpanHz: 0,
  215. AutoSpan: boolPtr(true),
  216. },
  217. Resources: &ResourceConfig{
  218. PreferGPU: false,
  219. MaxRefinementJobs: 8,
  220. MaxRecordingStreams: 16,
  221. MaxDecodeJobs: 16,
  222. DecisionHoldMs: 2000,
  223. },
  224. },
  225. {
  226. Name: "wideband-balanced",
  227. Description: "Baseline multi-resolution wideband surveillance",
  228. Pipeline: &PipelineConfig{Mode: "wideband-balanced", Profile: "wideband-balanced", Goals: PipelineGoalConfig{
  229. Intent: "wideband-surveillance",
  230. SignalPriorities: []string{"digital", "wfm"},
  231. }},
  232. Surveillance: &SurveillanceConfig{
  233. AnalysisFFTSize: 4096,
  234. FrameRate: 12,
  235. Strategy: "multi-resolution",
  236. DisplayBins: 2048,
  237. DisplayFPS: 12,
  238. DerivedDetection: "auto",
  239. },
  240. Refinement: &RefinementConfig{
  241. Enabled: true,
  242. MaxConcurrent: 16,
  243. DetailFFTSize: 0,
  244. MinCandidateSNRDb: 0,
  245. MinSpanHz: 4000,
  246. MaxSpanHz: 200000,
  247. AutoSpan: boolPtr(true),
  248. },
  249. Resources: &ResourceConfig{
  250. PreferGPU: true,
  251. MaxRefinementJobs: 16,
  252. MaxRecordingStreams: 16,
  253. MaxDecodeJobs: 12,
  254. DecisionHoldMs: 2000,
  255. },
  256. },
  257. {
  258. Name: "wideband-aggressive",
  259. Description: "Higher surveillance/refinement budgets for dense wideband monitoring",
  260. Pipeline: &PipelineConfig{Mode: "wideband-aggressive", Profile: "wideband-aggressive", Goals: PipelineGoalConfig{
  261. Intent: "high-density-wideband-surveillance",
  262. SignalPriorities: []string{"digital", "wfm", "trunk"},
  263. }},
  264. Surveillance: &SurveillanceConfig{
  265. AnalysisFFTSize: 8192,
  266. FrameRate: 10,
  267. Strategy: "multi-resolution",
  268. DisplayBins: 4096,
  269. DisplayFPS: 10,
  270. DerivedDetection: "auto",
  271. },
  272. Refinement: &RefinementConfig{
  273. Enabled: true,
  274. MaxConcurrent: 32,
  275. DetailFFTSize: 0,
  276. MinCandidateSNRDb: 0,
  277. MinSpanHz: 6000,
  278. MaxSpanHz: 250000,
  279. AutoSpan: boolPtr(true),
  280. },
  281. Resources: &ResourceConfig{
  282. PreferGPU: true,
  283. MaxRefinementJobs: 32,
  284. MaxRecordingStreams: 24,
  285. MaxDecodeJobs: 16,
  286. DecisionHoldMs: 2000,
  287. },
  288. },
  289. {
  290. Name: "archive",
  291. Description: "Record-first monitoring profile",
  292. Pipeline: &PipelineConfig{Mode: "archive", Profile: "archive", Goals: PipelineGoalConfig{
  293. Intent: "archive-and-triage",
  294. SignalPriorities: []string{"wfm", "nfm", "digital"},
  295. }},
  296. Surveillance: &SurveillanceConfig{
  297. AnalysisFFTSize: 4096,
  298. FrameRate: 12,
  299. Strategy: "single-resolution",
  300. DisplayBins: 2048,
  301. DisplayFPS: 12,
  302. DerivedDetection: "auto",
  303. },
  304. Refinement: &RefinementConfig{
  305. Enabled: true,
  306. MaxConcurrent: 12,
  307. DetailFFTSize: 0,
  308. MinCandidateSNRDb: 0,
  309. MinSpanHz: 4000,
  310. MaxSpanHz: 200000,
  311. AutoSpan: boolPtr(true),
  312. },
  313. Resources: &ResourceConfig{
  314. PreferGPU: true,
  315. MaxRefinementJobs: 12,
  316. MaxRecordingStreams: 24,
  317. MaxDecodeJobs: 12,
  318. DecisionHoldMs: 2500,
  319. },
  320. },
  321. {
  322. Name: "digital-hunting",
  323. Description: "Digital-first refinement and decode focus",
  324. Pipeline: &PipelineConfig{Mode: "digital-hunting", Profile: "digital-hunting", Goals: PipelineGoalConfig{
  325. Intent: "digital-surveillance",
  326. SignalPriorities: []string{"ft8", "wspr", "fsk", "psk", "dmr"},
  327. }},
  328. Surveillance: &SurveillanceConfig{
  329. AnalysisFFTSize: 4096,
  330. FrameRate: 12,
  331. Strategy: "multi-resolution",
  332. DisplayBins: 2048,
  333. DisplayFPS: 12,
  334. DerivedDetection: "auto",
  335. },
  336. Refinement: &RefinementConfig{
  337. Enabled: true,
  338. MaxConcurrent: 16,
  339. DetailFFTSize: 0,
  340. MinCandidateSNRDb: 0,
  341. MinSpanHz: 3000,
  342. MaxSpanHz: 120000,
  343. AutoSpan: boolPtr(true),
  344. },
  345. Resources: &ResourceConfig{
  346. PreferGPU: true,
  347. MaxRefinementJobs: 16,
  348. MaxRecordingStreams: 12,
  349. MaxDecodeJobs: 16,
  350. DecisionHoldMs: 2000,
  351. },
  352. },
  353. },
  354. Detector: DetectorConfig{
  355. ThresholdDb: -20,
  356. MinDurationMs: 250,
  357. HoldMs: 500,
  358. EmaAlpha: 0.2,
  359. HysteresisDb: 3,
  360. MinStableFrames: 3,
  361. GapToleranceMs: 500,
  362. CFARMode: "GOSCA",
  363. CFARGuardHz: 500,
  364. CFARTrainHz: 5000,
  365. CFARGuardCells: 3,
  366. CFARTrainCells: 24,
  367. CFARRank: 36,
  368. CFARScaleDb: 6,
  369. CFARWrapAround: true,
  370. EdgeMarginDb: 3.0,
  371. MaxSignalBwHz: 150000,
  372. MergeGapHz: 5000,
  373. ClassHistorySize: 10,
  374. ClassSwitchRatio: 0.6,
  375. },
  376. Recorder: RecorderConfig{
  377. Enabled: false,
  378. MinSNRDb: 10,
  379. MinDuration: "1s",
  380. MaxDuration: "300s",
  381. PrerollMs: 500,
  382. RecordIQ: true,
  383. RecordAudio: false,
  384. AutoDemod: true,
  385. AutoDecode: false,
  386. MaxDiskMB: 0,
  387. OutputDir: "data/recordings",
  388. RingSeconds: 8,
  389. DeemphasisUs: 50,
  390. ExtractionTaps: 101,
  391. ExtractionBwMult: 1.2,
  392. },
  393. Decoder: DecoderConfig{},
  394. WebAddr: ":8080",
  395. EventPath: "data/events.jsonl",
  396. FrameRate: 15,
  397. WaterfallLines: 200,
  398. WebRoot: "web",
  399. }
  400. }
  401. func Load(path string) (Config, error) {
  402. cfg := Default()
  403. if b, err := os.ReadFile(autosavePath(path)); err == nil {
  404. if err := yaml.Unmarshal(b, &cfg); err == nil {
  405. return applyDefaults(cfg), nil
  406. }
  407. }
  408. b, err := os.ReadFile(path)
  409. if err != nil {
  410. return cfg, err
  411. }
  412. if err := yaml.Unmarshal(b, &cfg); err != nil {
  413. return cfg, err
  414. }
  415. return applyDefaults(cfg), nil
  416. }
  417. func applyDefaults(cfg Config) Config {
  418. if cfg.Detector.MinDurationMs <= 0 {
  419. cfg.Detector.MinDurationMs = 250
  420. }
  421. if cfg.Detector.HoldMs <= 0 {
  422. cfg.Detector.HoldMs = 500
  423. }
  424. if cfg.Detector.MinStableFrames <= 0 {
  425. cfg.Detector.MinStableFrames = 3
  426. }
  427. if cfg.Detector.GapToleranceMs <= 0 {
  428. cfg.Detector.GapToleranceMs = cfg.Detector.HoldMs
  429. }
  430. if cfg.Detector.CFARMode == "" {
  431. if cfg.Detector.CFAREnabled != nil {
  432. if *cfg.Detector.CFAREnabled {
  433. cfg.Detector.CFARMode = "OS"
  434. } else {
  435. cfg.Detector.CFARMode = "OFF"
  436. }
  437. } else {
  438. cfg.Detector.CFARMode = "GOSCA"
  439. }
  440. }
  441. if cfg.Detector.CFARGuardHz <= 0 && cfg.Detector.CFARGuardCells > 0 {
  442. cfg.Detector.CFARGuardHz = float64(cfg.Detector.CFARGuardCells) * 62.5
  443. }
  444. if cfg.Detector.CFARTrainHz <= 0 && cfg.Detector.CFARTrainCells > 0 {
  445. cfg.Detector.CFARTrainHz = float64(cfg.Detector.CFARTrainCells) * 62.5
  446. }
  447. if cfg.Detector.CFARGuardHz <= 0 {
  448. cfg.Detector.CFARGuardHz = 500
  449. }
  450. if cfg.Detector.CFARTrainHz <= 0 {
  451. cfg.Detector.CFARTrainHz = 5000
  452. }
  453. if cfg.Detector.CFARGuardCells <= 0 {
  454. cfg.Detector.CFARGuardCells = 3
  455. }
  456. if cfg.Detector.CFARTrainCells <= 0 {
  457. cfg.Detector.CFARTrainCells = 24
  458. }
  459. if cfg.Detector.CFARRank <= 0 || cfg.Detector.CFARRank > 2*cfg.Detector.CFARTrainCells {
  460. cfg.Detector.CFARRank = int(math.Round(0.75 * float64(2*cfg.Detector.CFARTrainCells)))
  461. if cfg.Detector.CFARRank <= 0 {
  462. cfg.Detector.CFARRank = 1
  463. }
  464. }
  465. if cfg.Detector.CFARScaleDb <= 0 {
  466. cfg.Detector.CFARScaleDb = 6
  467. }
  468. if cfg.Detector.EdgeMarginDb <= 0 {
  469. cfg.Detector.EdgeMarginDb = 3.0
  470. }
  471. if cfg.Detector.MaxSignalBwHz <= 0 {
  472. cfg.Detector.MaxSignalBwHz = 150000
  473. }
  474. if cfg.Detector.MergeGapHz <= 0 {
  475. cfg.Detector.MergeGapHz = 5000
  476. }
  477. if cfg.Detector.ClassHistorySize <= 0 {
  478. cfg.Detector.ClassHistorySize = 10
  479. }
  480. if cfg.Detector.ClassSwitchRatio <= 0 || cfg.Detector.ClassSwitchRatio > 1 {
  481. cfg.Detector.ClassSwitchRatio = 0.6
  482. }
  483. if cfg.Pipeline.Mode == "" {
  484. cfg.Pipeline.Mode = "legacy"
  485. }
  486. if cfg.Pipeline.Goals.Intent == "" {
  487. cfg.Pipeline.Goals.Intent = "general-monitoring"
  488. }
  489. if cfg.Pipeline.Goals.MonitorSpanHz <= 0 && cfg.Pipeline.Goals.MonitorStartHz != 0 && cfg.Pipeline.Goals.MonitorEndHz != 0 && cfg.Pipeline.Goals.MonitorEndHz > cfg.Pipeline.Goals.MonitorStartHz {
  490. cfg.Pipeline.Goals.MonitorSpanHz = cfg.Pipeline.Goals.MonitorEndHz - cfg.Pipeline.Goals.MonitorStartHz
  491. }
  492. if cfg.Surveillance.AnalysisFFTSize <= 0 {
  493. cfg.Surveillance.AnalysisFFTSize = cfg.FFTSize
  494. }
  495. if cfg.Surveillance.FrameRate <= 0 {
  496. cfg.Surveillance.FrameRate = cfg.FrameRate
  497. }
  498. if cfg.Surveillance.Strategy == "" {
  499. cfg.Surveillance.Strategy = "single-resolution"
  500. }
  501. if cfg.Surveillance.DerivedDetection == "" {
  502. cfg.Surveillance.DerivedDetection = "auto"
  503. }
  504. switch strings.ToLower(strings.TrimSpace(cfg.Surveillance.DerivedDetection)) {
  505. case "auto", "on", "off", "true", "false", "enabled", "disabled", "enable", "disable":
  506. default:
  507. cfg.Surveillance.DerivedDetection = "auto"
  508. }
  509. if cfg.Surveillance.DisplayBins <= 0 {
  510. cfg.Surveillance.DisplayBins = cfg.FFTSize
  511. }
  512. if cfg.Surveillance.DisplayFPS <= 0 {
  513. cfg.Surveillance.DisplayFPS = cfg.FrameRate
  514. }
  515. if !cfg.Refinement.Enabled {
  516. // keep explicit false if user disabled it; enable by default only when unset-like zero config
  517. if cfg.Refinement.MaxConcurrent == 0 && cfg.Refinement.MinCandidateSNRDb == 0 {
  518. cfg.Refinement.Enabled = true
  519. }
  520. }
  521. if cfg.Refinement.MaxConcurrent <= 0 {
  522. cfg.Refinement.MaxConcurrent = 8
  523. }
  524. if cfg.Refinement.DetailFFTSize <= 0 {
  525. cfg.Refinement.DetailFFTSize = cfg.Surveillance.AnalysisFFTSize
  526. }
  527. if cfg.Refinement.DetailFFTSize&(cfg.Refinement.DetailFFTSize-1) != 0 {
  528. cfg.Refinement.DetailFFTSize = cfg.Surveillance.AnalysisFFTSize
  529. }
  530. if cfg.Refinement.MinSpanHz < 0 {
  531. cfg.Refinement.MinSpanHz = 0
  532. }
  533. if cfg.Refinement.MaxSpanHz < 0 {
  534. cfg.Refinement.MaxSpanHz = 0
  535. }
  536. if cfg.Refinement.MaxSpanHz > 0 && cfg.Refinement.MinSpanHz > cfg.Refinement.MaxSpanHz {
  537. cfg.Refinement.MaxSpanHz = cfg.Refinement.MinSpanHz
  538. }
  539. if cfg.Refinement.AutoSpan == nil {
  540. cfg.Refinement.AutoSpan = boolPtr(true)
  541. }
  542. if cfg.Resources.MaxRefinementJobs <= 0 {
  543. cfg.Resources.MaxRefinementJobs = cfg.Refinement.MaxConcurrent
  544. }
  545. if cfg.Resources.MaxRecordingStreams <= 0 {
  546. cfg.Resources.MaxRecordingStreams = 16
  547. }
  548. if cfg.Resources.DecisionHoldMs < 0 {
  549. cfg.Resources.DecisionHoldMs = 0
  550. }
  551. if cfg.Resources.DecisionHoldMs == 0 {
  552. cfg.Resources.DecisionHoldMs = 2000
  553. }
  554. if cfg.FrameRate <= 0 {
  555. cfg.FrameRate = 15
  556. }
  557. if cfg.WaterfallLines <= 0 {
  558. cfg.WaterfallLines = 200
  559. }
  560. if cfg.WebRoot == "" {
  561. cfg.WebRoot = "web"
  562. }
  563. if cfg.WebAddr == "" {
  564. cfg.WebAddr = ":8080"
  565. }
  566. if cfg.EventPath == "" {
  567. cfg.EventPath = "data/events.jsonl"
  568. }
  569. if cfg.SampleRate <= 0 {
  570. cfg.SampleRate = 2_048_000
  571. }
  572. if cfg.ClassifierMode == "" {
  573. cfg.ClassifierMode = "combined"
  574. }
  575. switch cfg.ClassifierMode {
  576. case "rule", "math", "combined":
  577. default:
  578. cfg.ClassifierMode = "combined"
  579. }
  580. if cfg.FFTSize <= 0 {
  581. cfg.FFTSize = 2048
  582. }
  583. if cfg.Surveillance.AnalysisFFTSize > 0 {
  584. cfg.FFTSize = cfg.Surveillance.AnalysisFFTSize
  585. } else {
  586. cfg.Surveillance.AnalysisFFTSize = cfg.FFTSize
  587. }
  588. if cfg.TunerBwKHz <= 0 {
  589. cfg.TunerBwKHz = 1536
  590. }
  591. if cfg.CenterHz == 0 {
  592. cfg.CenterHz = 100.0e6
  593. }
  594. if cfg.Recorder.OutputDir == "" {
  595. cfg.Recorder.OutputDir = "data/recordings"
  596. }
  597. if cfg.Recorder.RingSeconds <= 0 {
  598. cfg.Recorder.RingSeconds = 8
  599. }
  600. if cfg.Recorder.DeemphasisUs == 0 {
  601. cfg.Recorder.DeemphasisUs = 50
  602. }
  603. if cfg.Recorder.ExtractionTaps <= 0 {
  604. cfg.Recorder.ExtractionTaps = 101
  605. }
  606. if cfg.Recorder.ExtractionTaps > 301 {
  607. cfg.Recorder.ExtractionTaps = 301
  608. }
  609. if cfg.Recorder.ExtractionTaps%2 == 0 {
  610. cfg.Recorder.ExtractionTaps++ // must be odd
  611. }
  612. if cfg.Recorder.ExtractionBwMult <= 0 {
  613. cfg.Recorder.ExtractionBwMult = 1.2
  614. }
  615. return cfg
  616. }
  617. func (c Config) FrameInterval() time.Duration {
  618. fps := c.FrameRate
  619. if fps <= 0 {
  620. fps = 15
  621. }
  622. return time.Second / time.Duration(fps)
  623. }