Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

187 wiersze
4.4KB

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