Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

211 Zeilen
5.1KB

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