Wideband autonomous SDR analysis engine forked from sdr-visual-suite
No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.

179 líneas
4.7KB

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