Wideband autonomous SDR analysis engine forked from sdr-visual-suite
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

230 lines
5.5KB

  1. package main
  2. import (
  3. "context"
  4. "encoding/json"
  5. "flag"
  6. "log"
  7. "net/http"
  8. "os"
  9. "os/signal"
  10. "path/filepath"
  11. "strconv"
  12. "sync"
  13. "syscall"
  14. "time"
  15. "github.com/gorilla/websocket"
  16. "sdr-visual-suite/internal/config"
  17. "sdr-visual-suite/internal/detector"
  18. "sdr-visual-suite/internal/events"
  19. fftutil "sdr-visual-suite/internal/fft"
  20. "sdr-visual-suite/internal/mock"
  21. "sdr-visual-suite/internal/sdr"
  22. "sdr-visual-suite/internal/sdrplay"
  23. )
  24. type SpectrumFrame struct {
  25. Timestamp int64 `json:"ts"`
  26. CenterHz float64 `json:"center_hz"`
  27. SampleHz int `json:"sample_rate"`
  28. FFTSize int `json:"fft_size"`
  29. Spectrum []float64 `json:"spectrum_db"`
  30. Signals []detector.Signal `json:"signals"`
  31. }
  32. type hub struct {
  33. mu sync.Mutex
  34. clients map[*websocket.Conn]struct{}
  35. }
  36. func newHub() *hub {
  37. return &hub{clients: map[*websocket.Conn]struct{}{}}
  38. }
  39. func (h *hub) add(c *websocket.Conn) {
  40. h.mu.Lock()
  41. defer h.mu.Unlock()
  42. h.clients[c] = struct{}{}
  43. }
  44. func (h *hub) remove(c *websocket.Conn) {
  45. h.mu.Lock()
  46. defer h.mu.Unlock()
  47. delete(h.clients, c)
  48. }
  49. func (h *hub) broadcast(frame SpectrumFrame) {
  50. h.mu.Lock()
  51. defer h.mu.Unlock()
  52. b, _ := json.Marshal(frame)
  53. for c := range h.clients {
  54. _ = c.WriteMessage(websocket.TextMessage, b)
  55. }
  56. }
  57. func main() {
  58. var cfgPath string
  59. var mockFlag bool
  60. flag.StringVar(&cfgPath, "config", "config.yaml", "path to config YAML")
  61. flag.BoolVar(&mockFlag, "mock", false, "use synthetic IQ source")
  62. flag.Parse()
  63. cfg, err := config.Load(cfgPath)
  64. if err != nil {
  65. log.Fatalf("load config: %v", err)
  66. }
  67. var src sdr.Source
  68. if mockFlag {
  69. src = mock.New(cfg.SampleRate)
  70. } else {
  71. src, err = sdrplay.New(cfg.SampleRate, cfg.CenterHz, cfg.GainDb)
  72. if err != nil {
  73. log.Fatalf("sdrplay init failed: %v (try --mock or build with -tags sdrplay)", err)
  74. }
  75. }
  76. if err := src.Start(); err != nil {
  77. log.Fatalf("source start: %v", err)
  78. }
  79. defer src.Stop()
  80. if err := os.MkdirAll(filepath.Dir(cfg.EventPath), 0o755); err != nil {
  81. log.Fatalf("event path: %v", err)
  82. }
  83. eventFile, err := os.OpenFile(cfg.EventPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
  84. if err != nil {
  85. log.Fatalf("open events: %v", err)
  86. }
  87. defer eventFile.Close()
  88. det := detector.New(cfg.Detector.ThresholdDb, cfg.SampleRate, cfg.FFTSize,
  89. time.Duration(cfg.Detector.MinDurationMs)*time.Millisecond,
  90. time.Duration(cfg.Detector.HoldMs)*time.Millisecond)
  91. window := fftutil.Hann(cfg.FFTSize)
  92. h := newHub()
  93. ctx, cancel := context.WithCancel(context.Background())
  94. defer cancel()
  95. go runDSP(ctx, src, cfg, det, window, h, eventFile)
  96. upgrader := websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}
  97. http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
  98. c, err := upgrader.Upgrade(w, r, nil)
  99. if err != nil {
  100. return
  101. }
  102. h.add(c)
  103. defer func() {
  104. h.remove(c)
  105. _ = c.Close()
  106. }()
  107. for {
  108. _, _, err := c.ReadMessage()
  109. if err != nil {
  110. return
  111. }
  112. }
  113. })
  114. http.HandleFunc("/api/config", func(w http.ResponseWriter, r *http.Request) {
  115. w.Header().Set("Content-Type", "application/json")
  116. _ = json.NewEncoder(w).Encode(cfg)
  117. })
  118. http.HandleFunc("/api/events", func(w http.ResponseWriter, r *http.Request) {
  119. w.Header().Set("Content-Type", "application/json")
  120. limit := 200
  121. if v := r.URL.Query().Get("limit"); v != "" {
  122. if parsed, err := strconv.Atoi(v); err == nil {
  123. limit = parsed
  124. }
  125. }
  126. var since time.Time
  127. if v := r.URL.Query().Get("since"); v != "" {
  128. if parsed, err := parseSince(v); err == nil {
  129. since = parsed
  130. } else {
  131. http.Error(w, "invalid since", http.StatusBadRequest)
  132. return
  133. }
  134. }
  135. evs, err := events.ReadRecent(cfg.EventPath, limit, since)
  136. if err != nil {
  137. http.Error(w, "failed to read events", http.StatusInternalServerError)
  138. return
  139. }
  140. _ = json.NewEncoder(w).Encode(evs)
  141. })
  142. http.Handle("/", http.FileServer(http.Dir(cfg.WebRoot)))
  143. server := &http.Server{Addr: cfg.WebAddr}
  144. go func() {
  145. log.Printf("web listening on %s", cfg.WebAddr)
  146. if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
  147. log.Fatalf("server: %v", err)
  148. }
  149. }()
  150. stop := make(chan os.Signal, 1)
  151. signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
  152. <-stop
  153. ctxTimeout, cancelTimeout := context.WithTimeout(context.Background(), 5*time.Second)
  154. defer cancelTimeout()
  155. _ = server.Shutdown(ctxTimeout)
  156. }
  157. func runDSP(ctx context.Context, src sdr.Source, cfg config.Config, det *detector.Detector, window []float64, h *hub, eventFile *os.File) {
  158. ticker := time.NewTicker(cfg.FrameInterval())
  159. defer ticker.Stop()
  160. enc := json.NewEncoder(eventFile)
  161. for {
  162. select {
  163. case <-ctx.Done():
  164. return
  165. case <-ticker.C:
  166. iq, err := src.ReadIQ(cfg.FFTSize)
  167. if err != nil {
  168. log.Printf("read IQ: %v", err)
  169. continue
  170. }
  171. spectrum := fftutil.Spectrum(iq, window)
  172. now := time.Now()
  173. finished, signals := det.Process(now, spectrum, cfg.CenterHz)
  174. for _, ev := range finished {
  175. _ = enc.Encode(ev)
  176. }
  177. h.broadcast(SpectrumFrame{
  178. Timestamp: now.UnixMilli(),
  179. CenterHz: cfg.CenterHz,
  180. SampleHz: cfg.SampleRate,
  181. FFTSize: cfg.FFTSize,
  182. Spectrum: spectrum,
  183. Signals: signals,
  184. })
  185. }
  186. }
  187. }
  188. func parseSince(raw string) (time.Time, error) {
  189. if raw == "" {
  190. return time.Time{}, nil
  191. }
  192. if ms, err := strconv.ParseInt(raw, 10, 64); err == nil {
  193. if ms > 1e12 {
  194. return time.UnixMilli(ms), nil
  195. }
  196. return time.Unix(ms, 0), nil
  197. }
  198. if t, err := time.Parse(time.RFC3339Nano, raw); err == nil {
  199. return t, nil
  200. }
  201. return time.Parse(time.RFC3339, raw)
  202. }