Wideband autonomous SDR analysis engine forked from sdr-visual-suite
25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

307 lines
9.7KB

  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 Config struct {
  65. Bands []Band `yaml:"bands" json:"bands"`
  66. CenterHz float64 `yaml:"center_hz" json:"center_hz"`
  67. SampleRate int `yaml:"sample_rate" json:"sample_rate"`
  68. FFTSize int `yaml:"fft_size" json:"fft_size"`
  69. GainDb float64 `yaml:"gain_db" json:"gain_db"`
  70. TunerBwKHz int `yaml:"tuner_bw_khz" json:"tuner_bw_khz"`
  71. UseGPUFFT bool `yaml:"use_gpu_fft" json:"use_gpu_fft"`
  72. ClassifierMode string `yaml:"classifier_mode" json:"classifier_mode"`
  73. AGC bool `yaml:"agc" json:"agc"`
  74. DCBlock bool `yaml:"dc_block" json:"dc_block"`
  75. IQBalance bool `yaml:"iq_balance" json:"iq_balance"`
  76. Detector DetectorConfig `yaml:"detector" json:"detector"`
  77. Recorder RecorderConfig `yaml:"recorder" json:"recorder"`
  78. Decoder DecoderConfig `yaml:"decoder" json:"decoder"`
  79. WebAddr string `yaml:"web_addr" json:"web_addr"`
  80. EventPath string `yaml:"event_path" json:"event_path"`
  81. FrameRate int `yaml:"frame_rate" json:"frame_rate"`
  82. WaterfallLines int `yaml:"waterfall_lines" json:"waterfall_lines"`
  83. WebRoot string `yaml:"web_root" json:"web_root"`
  84. }
  85. func Default() Config {
  86. return Config{
  87. Bands: []Band{
  88. {Name: "example", StartHz: 99.5e6, EndHz: 100.5e6},
  89. },
  90. CenterHz: 100.0e6,
  91. SampleRate: 2_048_000,
  92. FFTSize: 2048,
  93. GainDb: 30,
  94. TunerBwKHz: 1536,
  95. UseGPUFFT: false,
  96. ClassifierMode: "combined",
  97. AGC: false,
  98. DCBlock: false,
  99. IQBalance: false,
  100. Detector: DetectorConfig{
  101. ThresholdDb: -20,
  102. MinDurationMs: 250,
  103. HoldMs: 500,
  104. EmaAlpha: 0.2,
  105. HysteresisDb: 3,
  106. MinStableFrames: 3,
  107. GapToleranceMs: 500,
  108. CFARMode: "GOSCA",
  109. CFARGuardHz: 500,
  110. CFARTrainHz: 5000,
  111. CFARGuardCells: 3,
  112. CFARTrainCells: 24,
  113. CFARRank: 36,
  114. CFARScaleDb: 6,
  115. CFARWrapAround: true,
  116. EdgeMarginDb: 3.0,
  117. MaxSignalBwHz: 150000,
  118. MergeGapHz: 5000,
  119. ClassHistorySize: 10,
  120. ClassSwitchRatio: 0.6,
  121. },
  122. Recorder: RecorderConfig{
  123. Enabled: false,
  124. MinSNRDb: 10,
  125. MinDuration: "1s",
  126. MaxDuration: "300s",
  127. PrerollMs: 500,
  128. RecordIQ: true,
  129. RecordAudio: false,
  130. AutoDemod: true,
  131. AutoDecode: false,
  132. MaxDiskMB: 0,
  133. OutputDir: "data/recordings",
  134. RingSeconds: 8,
  135. DeemphasisUs: 50,
  136. ExtractionTaps: 101,
  137. ExtractionBwMult: 1.2,
  138. },
  139. Decoder: DecoderConfig{},
  140. WebAddr: ":8080",
  141. EventPath: "data/events.jsonl",
  142. FrameRate: 15,
  143. WaterfallLines: 200,
  144. WebRoot: "web",
  145. }
  146. }
  147. func Load(path string) (Config, error) {
  148. cfg := Default()
  149. if b, err := os.ReadFile(autosavePath(path)); err == nil {
  150. if err := yaml.Unmarshal(b, &cfg); err == nil {
  151. return applyDefaults(cfg), nil
  152. }
  153. }
  154. b, err := os.ReadFile(path)
  155. if err != nil {
  156. return cfg, err
  157. }
  158. if err := yaml.Unmarshal(b, &cfg); err != nil {
  159. return cfg, err
  160. }
  161. return applyDefaults(cfg), nil
  162. }
  163. func applyDefaults(cfg Config) Config {
  164. if cfg.Detector.MinDurationMs <= 0 {
  165. cfg.Detector.MinDurationMs = 250
  166. }
  167. if cfg.Detector.HoldMs <= 0 {
  168. cfg.Detector.HoldMs = 500
  169. }
  170. if cfg.Detector.MinStableFrames <= 0 {
  171. cfg.Detector.MinStableFrames = 3
  172. }
  173. if cfg.Detector.GapToleranceMs <= 0 {
  174. cfg.Detector.GapToleranceMs = cfg.Detector.HoldMs
  175. }
  176. if cfg.Detector.CFARMode == "" {
  177. if cfg.Detector.CFAREnabled != nil {
  178. if *cfg.Detector.CFAREnabled {
  179. cfg.Detector.CFARMode = "OS"
  180. } else {
  181. cfg.Detector.CFARMode = "OFF"
  182. }
  183. } else {
  184. cfg.Detector.CFARMode = "GOSCA"
  185. }
  186. }
  187. if cfg.Detector.CFARGuardHz <= 0 && cfg.Detector.CFARGuardCells > 0 {
  188. cfg.Detector.CFARGuardHz = float64(cfg.Detector.CFARGuardCells) * 62.5
  189. }
  190. if cfg.Detector.CFARTrainHz <= 0 && cfg.Detector.CFARTrainCells > 0 {
  191. cfg.Detector.CFARTrainHz = float64(cfg.Detector.CFARTrainCells) * 62.5
  192. }
  193. if cfg.Detector.CFARGuardHz <= 0 {
  194. cfg.Detector.CFARGuardHz = 500
  195. }
  196. if cfg.Detector.CFARTrainHz <= 0 {
  197. cfg.Detector.CFARTrainHz = 5000
  198. }
  199. if cfg.Detector.CFARGuardCells <= 0 {
  200. cfg.Detector.CFARGuardCells = 3
  201. }
  202. if cfg.Detector.CFARTrainCells <= 0 {
  203. cfg.Detector.CFARTrainCells = 24
  204. }
  205. if cfg.Detector.CFARRank <= 0 || cfg.Detector.CFARRank > 2*cfg.Detector.CFARTrainCells {
  206. cfg.Detector.CFARRank = int(math.Round(0.75 * float64(2*cfg.Detector.CFARTrainCells)))
  207. if cfg.Detector.CFARRank <= 0 {
  208. cfg.Detector.CFARRank = 1
  209. }
  210. }
  211. if cfg.Detector.CFARScaleDb <= 0 {
  212. cfg.Detector.CFARScaleDb = 6
  213. }
  214. if cfg.Detector.EdgeMarginDb <= 0 {
  215. cfg.Detector.EdgeMarginDb = 3.0
  216. }
  217. if cfg.Detector.MaxSignalBwHz <= 0 {
  218. cfg.Detector.MaxSignalBwHz = 150000
  219. }
  220. if cfg.Detector.MergeGapHz <= 0 {
  221. cfg.Detector.MergeGapHz = 5000
  222. }
  223. if cfg.Detector.ClassHistorySize <= 0 {
  224. cfg.Detector.ClassHistorySize = 10
  225. }
  226. if cfg.Detector.ClassSwitchRatio <= 0 || cfg.Detector.ClassSwitchRatio > 1 {
  227. cfg.Detector.ClassSwitchRatio = 0.6
  228. }
  229. if cfg.FrameRate <= 0 {
  230. cfg.FrameRate = 15
  231. }
  232. if cfg.WaterfallLines <= 0 {
  233. cfg.WaterfallLines = 200
  234. }
  235. if cfg.WebRoot == "" {
  236. cfg.WebRoot = "web"
  237. }
  238. if cfg.WebAddr == "" {
  239. cfg.WebAddr = ":8080"
  240. }
  241. if cfg.EventPath == "" {
  242. cfg.EventPath = "data/events.jsonl"
  243. }
  244. if cfg.SampleRate <= 0 {
  245. cfg.SampleRate = 2_048_000
  246. }
  247. if cfg.ClassifierMode == "" {
  248. cfg.ClassifierMode = "combined"
  249. }
  250. switch cfg.ClassifierMode {
  251. case "rule", "math", "combined":
  252. default:
  253. cfg.ClassifierMode = "combined"
  254. }
  255. if cfg.FFTSize <= 0 {
  256. cfg.FFTSize = 2048
  257. }
  258. if cfg.TunerBwKHz <= 0 {
  259. cfg.TunerBwKHz = 1536
  260. }
  261. if cfg.CenterHz == 0 {
  262. cfg.CenterHz = 100.0e6
  263. }
  264. if cfg.Recorder.OutputDir == "" {
  265. cfg.Recorder.OutputDir = "data/recordings"
  266. }
  267. if cfg.Recorder.RingSeconds <= 0 {
  268. cfg.Recorder.RingSeconds = 8
  269. }
  270. if cfg.Recorder.DeemphasisUs == 0 {
  271. cfg.Recorder.DeemphasisUs = 50
  272. }
  273. if cfg.Recorder.ExtractionTaps <= 0 {
  274. cfg.Recorder.ExtractionTaps = 101
  275. }
  276. if cfg.Recorder.ExtractionTaps > 301 {
  277. cfg.Recorder.ExtractionTaps = 301
  278. }
  279. if cfg.Recorder.ExtractionTaps%2 == 0 {
  280. cfg.Recorder.ExtractionTaps++ // must be odd
  281. }
  282. if cfg.Recorder.ExtractionBwMult <= 0 {
  283. cfg.Recorder.ExtractionBwMult = 1.2
  284. }
  285. return cfg
  286. }
  287. func (c Config) FrameInterval() time.Duration {
  288. fps := c.FrameRate
  289. if fps <= 0 {
  290. fps = 15
  291. }
  292. return time.Second / time.Duration(fps)
  293. }