25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.

147 satır
4.0KB

  1. package recorder
  2. import (
  3. "errors"
  4. "fmt"
  5. "os"
  6. "path/filepath"
  7. "strings"
  8. "time"
  9. "sdr-visual-suite/internal/detector"
  10. )
  11. type Policy struct {
  12. Enabled bool `yaml:"enabled" json:"enabled"`
  13. MinSNRDb float64 `yaml:"min_snr_db" json:"min_snr_db"`
  14. MinDuration time.Duration `yaml:"min_duration" json:"min_duration"`
  15. MaxDuration time.Duration `yaml:"max_duration" json:"max_duration"`
  16. PrerollMs int `yaml:"preroll_ms" json:"preroll_ms"`
  17. RecordIQ bool `yaml:"record_iq" json:"record_iq"`
  18. RecordAudio bool `yaml:"record_audio" json:"record_audio"`
  19. AutoDemod bool `yaml:"auto_demod" json:"auto_demod"`
  20. AutoDecode bool `yaml:"auto_decode" json:"auto_decode"`
  21. MaxDiskMB int `yaml:"max_disk_mb" json:"max_disk_mb"`
  22. OutputDir string `yaml:"output_dir" json:"output_dir"`
  23. ClassFilter []string `yaml:"class_filter" json:"class_filter"`
  24. RingSeconds int `yaml:"ring_seconds" json:"ring_seconds"`
  25. }
  26. type Manager struct {
  27. policy Policy
  28. ring *Ring
  29. sampleRate int
  30. blockSize int
  31. centerHz float64
  32. decodeCommands map[string]string
  33. }
  34. func New(sampleRate int, blockSize int, policy Policy, centerHz float64, decodeCommands map[string]string) *Manager {
  35. if policy.OutputDir == "" {
  36. policy.OutputDir = "data/recordings"
  37. }
  38. if policy.RingSeconds <= 0 {
  39. policy.RingSeconds = 8
  40. }
  41. return &Manager{policy: policy, ring: NewRing(sampleRate, blockSize, policy.RingSeconds), sampleRate: sampleRate, blockSize: blockSize, centerHz: centerHz, decodeCommands: decodeCommands}
  42. }
  43. func (m *Manager) Update(sampleRate int, blockSize int, policy Policy, centerHz float64, decodeCommands map[string]string) {
  44. m.policy = policy
  45. m.sampleRate = sampleRate
  46. m.blockSize = blockSize
  47. m.centerHz = centerHz
  48. m.decodeCommands = decodeCommands
  49. if m.ring == nil {
  50. m.ring = NewRing(sampleRate, blockSize, policy.RingSeconds)
  51. return
  52. }
  53. m.ring.Reset(sampleRate, blockSize, policy.RingSeconds)
  54. }
  55. func (m *Manager) Ingest(t0 time.Time, samples []complex64) {
  56. if m == nil || m.ring == nil {
  57. return
  58. }
  59. m.ring.Push(t0, samples)
  60. }
  61. func (m *Manager) OnEvents(events []detector.Event) {
  62. if m == nil || !m.policy.Enabled || len(events) == 0 {
  63. return
  64. }
  65. for _, ev := range events {
  66. _ = m.recordEvent(ev)
  67. }
  68. }
  69. func (m *Manager) recordEvent(ev detector.Event) error {
  70. if !m.policy.Enabled {
  71. return nil
  72. }
  73. if ev.SNRDb < m.policy.MinSNRDb {
  74. return nil
  75. }
  76. dur := ev.End.Sub(ev.Start)
  77. if m.policy.MinDuration > 0 && dur < m.policy.MinDuration {
  78. return nil
  79. }
  80. if m.policy.MaxDuration > 0 && dur > m.policy.MaxDuration {
  81. return nil
  82. }
  83. if len(m.policy.ClassFilter) > 0 && ev.Class != nil {
  84. match := false
  85. for _, c := range m.policy.ClassFilter {
  86. if strings.EqualFold(c, string(ev.Class.ModType)) {
  87. match = true
  88. break
  89. }
  90. }
  91. if !match {
  92. return nil
  93. }
  94. }
  95. if !m.policy.RecordIQ && !m.policy.RecordAudio {
  96. return nil
  97. }
  98. start := ev.Start.Add(-time.Duration(m.policy.PrerollMs) * time.Millisecond)
  99. end := ev.End
  100. if start.After(end) {
  101. return errors.New("invalid event window")
  102. }
  103. segment := m.ring.Slice(start, end)
  104. if len(segment) == 0 {
  105. return errors.New("no iq in ring")
  106. }
  107. dir := filepath.Join(m.policy.OutputDir, fmt.Sprintf("%s_%0.fHz_evt%d", ev.Start.Format("2006-01-02T15-04-05"), ev.CenterHz, ev.ID))
  108. if err := os.MkdirAll(dir, 0o755); err != nil {
  109. return err
  110. }
  111. files := map[string]any{}
  112. var iqPath string
  113. if m.policy.RecordIQ {
  114. iqPath = filepath.Join(dir, "signal.cf32")
  115. if err := writeCF32(iqPath, segment); err != nil {
  116. return err
  117. }
  118. files["iq"] = "signal.cf32"
  119. files["iq_format"] = "cf32"
  120. files["iq_sample_rate"] = m.sampleRate
  121. }
  122. // Optional demod + audio
  123. if m.policy.RecordAudio && m.policy.AutoDemod && ev.Class != nil {
  124. if err := m.demodAndWrite(dir, ev, segment, files); err != nil {
  125. return err
  126. }
  127. }
  128. if m.policy.AutoDecode && iqPath != "" && ev.Class != nil {
  129. m.runDecodeIfConfigured(string(ev.Class.ModType), iqPath, m.sampleRate, files, dir)
  130. }
  131. return writeMeta(dir, ev, m.sampleRate, files)
  132. }