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.

191 líneas
4.9KB

  1. package main
  2. import (
  3. "log"
  4. "sort"
  5. "strconv"
  6. "time"
  7. "sdr-visual-suite/internal/config"
  8. "sdr-visual-suite/internal/demod/gpudemod"
  9. "sdr-visual-suite/internal/detector"
  10. "sdr-visual-suite/internal/dsp"
  11. )
  12. func mustParseDuration(raw string, fallback time.Duration) time.Duration {
  13. if raw == "" {
  14. return fallback
  15. }
  16. if d, err := time.ParseDuration(raw); err == nil {
  17. return d
  18. }
  19. return fallback
  20. }
  21. func buildDecoderMap(cfg config.Config) map[string]string {
  22. out := map[string]string{}
  23. if cfg.Decoder.FT8Cmd != "" {
  24. out["FT8"] = cfg.Decoder.FT8Cmd
  25. }
  26. if cfg.Decoder.WSPRCmd != "" {
  27. out["WSPR"] = cfg.Decoder.WSPRCmd
  28. }
  29. if cfg.Decoder.DMRCmd != "" {
  30. out["DMR"] = cfg.Decoder.DMRCmd
  31. }
  32. if cfg.Decoder.DStarCmd != "" {
  33. out["D-STAR"] = cfg.Decoder.DStarCmd
  34. }
  35. if cfg.Decoder.FSKCmd != "" {
  36. out["FSK"] = cfg.Decoder.FSKCmd
  37. }
  38. if cfg.Decoder.PSKCmd != "" {
  39. out["PSK"] = cfg.Decoder.PSKCmd
  40. }
  41. return out
  42. }
  43. func decoderKeys(cfg config.Config) []string {
  44. m := buildDecoderMap(cfg)
  45. keys := make([]string, 0, len(m))
  46. for k := range m {
  47. keys = append(keys, k)
  48. }
  49. sort.Strings(keys)
  50. return keys
  51. }
  52. func (m *extractionManager) reset() {
  53. if m == nil {
  54. return
  55. }
  56. m.mu.Lock()
  57. defer m.mu.Unlock()
  58. if m.runner != nil {
  59. m.runner.Close()
  60. m.runner = nil
  61. }
  62. }
  63. func (m *extractionManager) get(sampleCount int, sampleRate int) *gpudemod.BatchRunner {
  64. if m == nil || sampleCount <= 0 || sampleRate <= 0 || !gpudemod.Available() {
  65. return nil
  66. }
  67. m.mu.Lock()
  68. defer m.mu.Unlock()
  69. if m.runner == nil {
  70. if r, err := gpudemod.NewBatchRunner(sampleCount, sampleRate); err == nil {
  71. m.runner = r
  72. } else {
  73. log.Printf("gpudemod: batch runner init failed: %v", err)
  74. }
  75. return m.runner
  76. }
  77. return m.runner
  78. }
  79. func extractSignalIQ(iq []complex64, sampleRate int, centerHz float64, sigHz float64, bwHz float64) []complex64 {
  80. if len(iq) == 0 || sampleRate <= 0 {
  81. return nil
  82. }
  83. results, _ := extractSignalIQBatch(nil, iq, sampleRate, centerHz, []detector.Signal{{CenterHz: sigHz, BWHz: bwHz}})
  84. if len(results) == 0 {
  85. return nil
  86. }
  87. return results[0]
  88. }
  89. func extractSignalIQBatch(extractMgr *extractionManager, iq []complex64, sampleRate int, centerHz float64, signals []detector.Signal) ([][]complex64, []int) {
  90. out := make([][]complex64, len(signals))
  91. rates := make([]int, len(signals))
  92. if len(iq) == 0 || sampleRate <= 0 || len(signals) == 0 {
  93. return out, rates
  94. }
  95. decimTarget := 200000
  96. if decimTarget <= 0 {
  97. decimTarget = sampleRate
  98. }
  99. runner := extractMgr.get(len(iq), sampleRate)
  100. if runner != nil {
  101. jobs := make([]gpudemod.ExtractJob, len(signals))
  102. for i, sig := range signals {
  103. bw := sig.BWHz
  104. // Minimum extraction BW: ensure enough bandwidth for demod features
  105. // FM broadcast (87.5-108 MHz) needs >=150kHz for stereo pilot + RDS at 57kHz
  106. // Also widen for any signal classified as WFM (in case of re-extraction)
  107. sigMHz := sig.CenterHz / 1e6
  108. isWFM := (sigMHz >= 87.5 && sigMHz <= 108.0) || (sig.Class != nil && (sig.Class.ModType == "WFM" || sig.Class.ModType == "WFM_STEREO"))
  109. if isWFM {
  110. if bw < 150000 {
  111. bw = 150000
  112. }
  113. } else if bw < 20000 {
  114. bw = 20000
  115. }
  116. jobs[i] = gpudemod.ExtractJob{OffsetHz: sig.CenterHz - centerHz, BW: bw, OutRate: decimTarget}
  117. }
  118. if gpuOuts, gpuRates, err := runner.ShiftFilterDecimateBatch(iq, jobs); err == nil && len(gpuOuts) == len(signals) {
  119. // batch extraction OK (silent)
  120. for i := range gpuOuts {
  121. out[i] = gpuOuts[i]
  122. if i < len(gpuRates) {
  123. rates[i] = gpuRates[i]
  124. }
  125. }
  126. return out, rates
  127. } else if err != nil {
  128. log.Printf("gpudemod: batch extraction failed for %d signals: %v", len(signals), err)
  129. }
  130. }
  131. // CPU extraction fallback (silent — see batch extraction failed above if applicable)
  132. for i, sig := range signals {
  133. offset := sig.CenterHz - centerHz
  134. shifted := dsp.FreqShift(iq, sampleRate, offset)
  135. bw := sig.BWHz
  136. // FM broadcast (87.5-108 MHz) needs >=150kHz for stereo + RDS
  137. sigMHz := sig.CenterHz / 1e6
  138. isWFM := (sigMHz >= 87.5 && sigMHz <= 108.0) || (sig.Class != nil && (sig.Class.ModType == "WFM" || sig.Class.ModType == "WFM_STEREO"))
  139. if isWFM {
  140. if bw < 150000 {
  141. bw = 150000
  142. }
  143. } else if bw < 20000 {
  144. bw = 20000
  145. }
  146. cutoff := bw / 2
  147. if cutoff < 200 {
  148. cutoff = 200
  149. }
  150. if cutoff > float64(sampleRate)/2-1 {
  151. cutoff = float64(sampleRate)/2 - 1
  152. }
  153. taps := dsp.LowpassFIR(cutoff, sampleRate, 101)
  154. filtered := dsp.ApplyFIR(shifted, taps)
  155. decim := sampleRate / decimTarget
  156. if decim < 1 {
  157. decim = 1
  158. }
  159. out[i] = dsp.Decimate(filtered, decim)
  160. rates[i] = sampleRate / decim
  161. }
  162. return out, rates
  163. }
  164. func parseSince(raw string) (time.Time, error) {
  165. if raw == "" {
  166. return time.Time{}, nil
  167. }
  168. if ms, err := strconv.ParseInt(raw, 10, 64); err == nil {
  169. if ms > 1e12 {
  170. return time.UnixMilli(ms), nil
  171. }
  172. return time.Unix(ms, 0), nil
  173. }
  174. if t, err := time.Parse(time.RFC3339Nano, raw); err == nil {
  175. return t, nil
  176. }
  177. return time.Parse(time.RFC3339, raw)
  178. }