Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

361 строка
8.7KB

  1. package recorder
  2. import (
  3. "errors"
  4. "fmt"
  5. "log"
  6. "os"
  7. "path/filepath"
  8. "strings"
  9. "sync"
  10. "time"
  11. "sdr-visual-suite/internal/demod/gpudemod"
  12. "sdr-visual-suite/internal/detector"
  13. )
  14. type Policy struct {
  15. Enabled bool `yaml:"enabled" json:"enabled"`
  16. MinSNRDb float64 `yaml:"min_snr_db" json:"min_snr_db"`
  17. MinDuration time.Duration `yaml:"min_duration" json:"min_duration"`
  18. MaxDuration time.Duration `yaml:"max_duration" json:"max_duration"`
  19. PrerollMs int `yaml:"preroll_ms" json:"preroll_ms"`
  20. RecordIQ bool `yaml:"record_iq" json:"record_iq"`
  21. RecordAudio bool `yaml:"record_audio" json:"record_audio"`
  22. AutoDemod bool `yaml:"auto_demod" json:"auto_demod"`
  23. AutoDecode bool `yaml:"auto_decode" json:"auto_decode"`
  24. MaxDiskMB int `yaml:"max_disk_mb" json:"max_disk_mb"`
  25. OutputDir string `yaml:"output_dir" json:"output_dir"`
  26. ClassFilter []string `yaml:"class_filter" json:"class_filter"`
  27. RingSeconds int `yaml:"ring_seconds" json:"ring_seconds"`
  28. }
  29. type Manager struct {
  30. mu sync.RWMutex
  31. policy Policy
  32. ring *Ring
  33. sampleRate int
  34. blockSize int
  35. centerHz float64
  36. decodeCommands map[string]string
  37. queue chan detector.Event
  38. gpuDemod *gpudemod.Engine
  39. closed bool
  40. closeOnce sync.Once
  41. workerWG sync.WaitGroup
  42. // Streaming recorder
  43. streamer *Streamer
  44. streamedIDs map[int64]bool // signal IDs that were streamed (skip retroactive recording)
  45. streamedMu sync.Mutex
  46. }
  47. func New(sampleRate int, blockSize int, policy Policy, centerHz float64, decodeCommands map[string]string) *Manager {
  48. if policy.OutputDir == "" {
  49. policy.OutputDir = "data/recordings"
  50. }
  51. if policy.RingSeconds <= 0 {
  52. policy.RingSeconds = 8
  53. }
  54. m := &Manager{
  55. policy: policy,
  56. ring: NewRing(sampleRate, blockSize, policy.RingSeconds),
  57. sampleRate: sampleRate,
  58. blockSize: blockSize,
  59. centerHz: centerHz,
  60. decodeCommands: decodeCommands,
  61. queue: make(chan detector.Event, 64),
  62. streamer: newStreamer(policy, centerHz),
  63. streamedIDs: make(map[int64]bool),
  64. }
  65. m.initGPUDemod(sampleRate, blockSize)
  66. m.workerWG.Add(1)
  67. go m.worker()
  68. return m
  69. }
  70. func (m *Manager) Update(sampleRate int, blockSize int, policy Policy, centerHz float64, decodeCommands map[string]string) {
  71. m.mu.Lock()
  72. defer m.mu.Unlock()
  73. m.policy = policy
  74. m.centerHz = centerHz
  75. m.decodeCommands = decodeCommands
  76. // Only reset ring and GPU engine if sample parameters actually changed
  77. needRingReset := m.sampleRate != sampleRate || m.blockSize != blockSize
  78. m.sampleRate = sampleRate
  79. m.blockSize = blockSize
  80. if needRingReset {
  81. m.initGPUDemodLocked(sampleRate, blockSize)
  82. if m.ring == nil {
  83. m.ring = NewRing(sampleRate, blockSize, policy.RingSeconds)
  84. } else {
  85. m.ring.Reset(sampleRate, blockSize, policy.RingSeconds)
  86. }
  87. } else if m.ring == nil {
  88. m.ring = NewRing(sampleRate, blockSize, policy.RingSeconds)
  89. }
  90. if m.streamer != nil {
  91. m.streamer.updatePolicy(policy, centerHz)
  92. }
  93. }
  94. func (m *Manager) Ingest(t0 time.Time, samples []complex64) {
  95. if m == nil {
  96. return
  97. }
  98. m.mu.RLock()
  99. ring := m.ring
  100. m.mu.RUnlock()
  101. if ring == nil {
  102. return
  103. }
  104. ring.Push(t0, samples)
  105. }
  106. func (m *Manager) OnEvents(events []detector.Event) {
  107. if m == nil || len(events) == 0 {
  108. return
  109. }
  110. m.mu.RLock()
  111. enabled := m.policy.Enabled
  112. closed := m.closed
  113. m.mu.RUnlock()
  114. if !enabled || closed {
  115. return
  116. }
  117. for _, ev := range events {
  118. select {
  119. case m.queue <- ev:
  120. default:
  121. // drop if queue full
  122. }
  123. }
  124. }
  125. func (m *Manager) worker() {
  126. defer m.workerWG.Done()
  127. for ev := range m.queue {
  128. _ = m.recordEvent(ev)
  129. }
  130. }
  131. func (m *Manager) initGPUDemod(sampleRate int, blockSize int) {
  132. m.mu.Lock()
  133. defer m.mu.Unlock()
  134. m.initGPUDemodLocked(sampleRate, blockSize)
  135. }
  136. func (m *Manager) gpuEngine() *gpudemod.Engine {
  137. m.mu.RLock()
  138. defer m.mu.RUnlock()
  139. return m.gpuDemod
  140. }
  141. func (m *Manager) initGPUDemodLocked(sampleRate int, blockSize int) {
  142. if m.gpuDemod != nil {
  143. m.gpuDemod.Close()
  144. m.gpuDemod = nil
  145. }
  146. if !gpudemod.Available() {
  147. return
  148. }
  149. eng, err := gpudemod.New(blockSize, sampleRate)
  150. if err != nil {
  151. return
  152. }
  153. m.gpuDemod = eng
  154. }
  155. func (m *Manager) Close() {
  156. if m == nil {
  157. return
  158. }
  159. m.closeOnce.Do(func() {
  160. // Close all active streaming sessions first
  161. if m.streamer != nil {
  162. m.streamer.CloseAll()
  163. }
  164. m.mu.Lock()
  165. m.closed = true
  166. if m.queue != nil {
  167. close(m.queue)
  168. }
  169. gpu := m.gpuDemod
  170. m.gpuDemod = nil
  171. m.mu.Unlock()
  172. m.workerWG.Wait()
  173. if gpu != nil {
  174. gpu.Close()
  175. }
  176. })
  177. }
  178. func (m *Manager) recordEvent(ev detector.Event) error {
  179. // Skip events that were already recorded via streaming
  180. m.streamedMu.Lock()
  181. wasStreamed := m.streamedIDs[ev.ID]
  182. delete(m.streamedIDs, ev.ID) // clean up — event is finished
  183. m.streamedMu.Unlock()
  184. if wasStreamed {
  185. log.Printf("STREAM: skipping retroactive recording for signal %d (already streamed)", ev.ID)
  186. return nil
  187. }
  188. m.mu.RLock()
  189. policy := m.policy
  190. ring := m.ring
  191. sampleRate := m.sampleRate
  192. centerHz := m.centerHz
  193. m.mu.RUnlock()
  194. if !policy.Enabled {
  195. return nil
  196. }
  197. if ev.SNRDb < policy.MinSNRDb {
  198. return nil
  199. }
  200. dur := ev.End.Sub(ev.Start)
  201. if policy.MinDuration > 0 && dur < policy.MinDuration {
  202. return nil
  203. }
  204. if policy.MaxDuration > 0 && dur > policy.MaxDuration {
  205. return nil
  206. }
  207. if len(policy.ClassFilter) > 0 && ev.Class != nil {
  208. match := false
  209. for _, c := range policy.ClassFilter {
  210. if strings.EqualFold(c, string(ev.Class.ModType)) {
  211. match = true
  212. break
  213. }
  214. }
  215. if !match {
  216. return nil
  217. }
  218. }
  219. if !policy.RecordIQ && !policy.RecordAudio {
  220. return nil
  221. }
  222. start := ev.Start.Add(-time.Duration(policy.PrerollMs) * time.Millisecond)
  223. end := ev.End
  224. if start.After(end) {
  225. return errors.New("invalid event window")
  226. }
  227. if ring == nil {
  228. return errors.New("no ring buffer")
  229. }
  230. segment := ring.Slice(start, end)
  231. if len(segment) == 0 {
  232. return errors.New("no iq in ring")
  233. }
  234. dir := filepath.Join(policy.OutputDir, fmt.Sprintf("%s_%0.fHz_evt%d", ev.Start.Format("2006-01-02T15-04-05"), ev.CenterHz, ev.ID))
  235. if err := os.MkdirAll(dir, 0o755); err != nil {
  236. return err
  237. }
  238. files := map[string]any{}
  239. var iqPath string
  240. if policy.RecordIQ {
  241. iqPath = filepath.Join(dir, "signal.cf32")
  242. if err := writeCF32(iqPath, segment); err != nil {
  243. return err
  244. }
  245. files["iq"] = "signal.cf32"
  246. files["iq_format"] = "cf32"
  247. files["iq_sample_rate"] = sampleRate
  248. }
  249. if policy.RecordAudio && policy.AutoDemod && ev.Class != nil {
  250. if err := m.demodAndWrite(dir, ev, segment, files); err != nil {
  251. return err
  252. }
  253. }
  254. if policy.AutoDecode && iqPath != "" && ev.Class != nil {
  255. m.runDecodeIfConfigured(string(ev.Class.ModType), iqPath, sampleRate, files, dir)
  256. }
  257. _ = centerHz
  258. return writeMeta(dir, ev, sampleRate, files)
  259. }
  260. // SliceRecent returns the most recent `seconds` of raw IQ from the ring buffer.
  261. // Returns the IQ samples, sample rate, and center frequency.
  262. func (m *Manager) SliceRecent(seconds float64) ([]complex64, int, float64) {
  263. if m == nil {
  264. return nil, 0, 0
  265. }
  266. m.mu.RLock()
  267. ring := m.ring
  268. sr := m.sampleRate
  269. center := m.centerHz
  270. m.mu.RUnlock()
  271. if ring == nil || sr <= 0 {
  272. return nil, 0, 0
  273. }
  274. end := time.Now()
  275. start := end.Add(-time.Duration(seconds * float64(time.Second)))
  276. iq := ring.Slice(start, end)
  277. return iq, sr, center
  278. }
  279. // FeedSnippets is called once per DSP frame with pre-extracted IQ snippets
  280. // (GPU-accelerated FreqShift+FIR+Decimate). The Streamer handles demod with
  281. // persistent state (overlap-save, stereo decode, de-emphasis) asynchronously.
  282. func (m *Manager) FeedSnippets(items []StreamFeedItem) {
  283. if m == nil || m.streamer == nil || len(items) == 0 {
  284. return
  285. }
  286. m.mu.RLock()
  287. closed := m.closed
  288. m.mu.RUnlock()
  289. if closed {
  290. return
  291. }
  292. // Mark all signal IDs so recordEvent skips them
  293. m.streamedMu.Lock()
  294. for _, item := range items {
  295. if item.Signal.ID != 0 {
  296. m.streamedIDs[item.Signal.ID] = true
  297. }
  298. }
  299. m.streamedMu.Unlock()
  300. // Convert to internal type
  301. internal := make([]streamFeedItem, len(items))
  302. for i, item := range items {
  303. internal[i] = streamFeedItem{
  304. signal: item.Signal,
  305. snippet: item.Snippet,
  306. snipRate: item.SnipRate,
  307. }
  308. }
  309. m.streamer.FeedSnippets(internal)
  310. }
  311. // StreamFeedItem is the public type for passing extracted snippets from DSP loop.
  312. type StreamFeedItem struct {
  313. Signal detector.Signal
  314. Snippet []complex64
  315. SnipRate int
  316. }
  317. // Streamer returns the underlying Streamer for live-listen subscriptions.
  318. func (m *Manager) StreamerRef() *Streamer {
  319. if m == nil {
  320. return nil
  321. }
  322. return m.streamer
  323. }
  324. // ActiveStreams returns info about currently active streaming sessions.
  325. func (m *Manager) ActiveStreams() int {
  326. if m == nil || m.streamer == nil {
  327. return 0
  328. }
  329. return m.streamer.ActiveSessions()
  330. }