25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.

434 satır
13KB

  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. "strings"
  13. "sync"
  14. "syscall"
  15. "time"
  16. "github.com/gorilla/websocket"
  17. "sdr-visual-suite/internal/config"
  18. "sdr-visual-suite/internal/detector"
  19. "sdr-visual-suite/internal/events"
  20. fftutil "sdr-visual-suite/internal/fft"
  21. "sdr-visual-suite/internal/fft/gpufft"
  22. "sdr-visual-suite/internal/mock"
  23. "sdr-visual-suite/internal/recorder"
  24. "sdr-visual-suite/internal/runtime"
  25. "sdr-visual-suite/internal/sdr"
  26. "sdr-visual-suite/internal/sdrplay"
  27. )
  28. func main() {
  29. var cfgPath string
  30. var mockFlag bool
  31. flag.StringVar(&cfgPath, "config", "config.yaml", "path to config YAML")
  32. flag.BoolVar(&mockFlag, "mock", false, "use synthetic IQ source")
  33. flag.Parse()
  34. cfg, err := config.Load(cfgPath)
  35. if err != nil {
  36. log.Fatalf("load config: %v", err)
  37. }
  38. cfgManager := runtime.New(cfg)
  39. gpuState := &gpuStatus{Available: gpufft.Available()}
  40. newSource := func(cfg config.Config) (sdr.Source, error) {
  41. if mockFlag {
  42. src := mock.New(cfg.SampleRate)
  43. if updatable, ok := interface{}(src).(sdr.ConfigurableSource); ok {
  44. _ = updatable.UpdateConfig(cfg.SampleRate, cfg.CenterHz, cfg.GainDb, cfg.AGC, cfg.TunerBwKHz)
  45. }
  46. return src, nil
  47. }
  48. src, err := sdrplay.New(cfg.SampleRate, cfg.CenterHz, cfg.GainDb, cfg.TunerBwKHz)
  49. if err != nil {
  50. return nil, err
  51. }
  52. if updatable, ok := src.(sdr.ConfigurableSource); ok {
  53. _ = updatable.UpdateConfig(cfg.SampleRate, cfg.CenterHz, cfg.GainDb, cfg.AGC, cfg.TunerBwKHz)
  54. }
  55. return src, nil
  56. }
  57. src, err := newSource(cfg)
  58. if err != nil {
  59. log.Fatalf("sdrplay init failed: %v (try --mock or build with -tags sdrplay)", err)
  60. }
  61. srcMgr := newSourceManager(src, newSource)
  62. if err := srcMgr.Start(); err != nil {
  63. log.Fatalf("source start: %v", err)
  64. }
  65. defer srcMgr.Stop()
  66. if err := os.MkdirAll(filepath.Dir(cfg.EventPath), 0o755); err != nil {
  67. log.Fatalf("event path: %v", err)
  68. }
  69. eventFile, err := os.OpenFile(cfg.EventPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
  70. if err != nil {
  71. log.Fatalf("open events: %v", err)
  72. }
  73. defer eventFile.Close()
  74. eventMu := &sync.RWMutex{}
  75. det := detector.New(cfg.Detector, cfg.SampleRate, cfg.FFTSize)
  76. window := fftutil.Hann(cfg.FFTSize)
  77. h := newHub()
  78. dspUpdates := make(chan dspUpdate, 1)
  79. ctx, cancel := context.WithCancel(context.Background())
  80. defer cancel()
  81. decodeMap := buildDecoderMap(cfg)
  82. recMgr := recorder.New(cfg.SampleRate, cfg.FFTSize, recorder.Policy{
  83. Enabled: cfg.Recorder.Enabled,
  84. MinSNRDb: cfg.Recorder.MinSNRDb,
  85. MinDuration: mustParseDuration(cfg.Recorder.MinDuration, 1*time.Second),
  86. MaxDuration: mustParseDuration(cfg.Recorder.MaxDuration, 300*time.Second),
  87. PrerollMs: cfg.Recorder.PrerollMs,
  88. RecordIQ: cfg.Recorder.RecordIQ,
  89. RecordAudio: cfg.Recorder.RecordAudio,
  90. AutoDemod: cfg.Recorder.AutoDemod,
  91. AutoDecode: cfg.Recorder.AutoDecode,
  92. MaxDiskMB: cfg.Recorder.MaxDiskMB,
  93. OutputDir: cfg.Recorder.OutputDir,
  94. ClassFilter: cfg.Recorder.ClassFilter,
  95. RingSeconds: cfg.Recorder.RingSeconds,
  96. }, cfg.CenterHz, decodeMap)
  97. sigSnap := &signalSnapshot{}
  98. go runDSP(ctx, srcMgr, cfg, det, window, h, eventFile, eventMu, dspUpdates, gpuState, recMgr, sigSnap)
  99. upgrader := websocket.Upgrader{CheckOrigin: func(r *http.Request) bool {
  100. origin := r.Header.Get("Origin")
  101. if origin == "" || origin == "null" {
  102. return true
  103. }
  104. // allow same-host or any local IP
  105. return true
  106. }}
  107. http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
  108. conn, err := upgrader.Upgrade(w, r, nil)
  109. if err != nil {
  110. log.Printf("ws upgrade failed: %v (origin: %s)", err, r.Header.Get("Origin"))
  111. return
  112. }
  113. c := &client{conn: conn, send: make(chan []byte, 32), done: make(chan struct{})}
  114. h.add(c)
  115. defer func() {
  116. h.remove(c)
  117. _ = conn.Close()
  118. }()
  119. conn.SetReadDeadline(time.Now().Add(60 * time.Second))
  120. conn.SetPongHandler(func(string) error {
  121. conn.SetReadDeadline(time.Now().Add(60 * time.Second))
  122. return nil
  123. })
  124. go func() {
  125. ping := time.NewTicker(30 * time.Second)
  126. defer ping.Stop()
  127. for {
  128. select {
  129. case msg, ok := <-c.send:
  130. if !ok {
  131. return
  132. }
  133. _ = conn.SetWriteDeadline(time.Now().Add(200 * time.Millisecond))
  134. if err := conn.WriteMessage(websocket.TextMessage, msg); err != nil {
  135. return
  136. }
  137. case <-ping.C:
  138. _ = conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
  139. if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
  140. log.Printf("ws ping error: %v", err)
  141. return
  142. }
  143. }
  144. }
  145. }()
  146. for {
  147. _, _, err := conn.ReadMessage()
  148. if err != nil {
  149. return
  150. }
  151. }
  152. })
  153. http.HandleFunc("/api/config", func(w http.ResponseWriter, r *http.Request) {
  154. w.Header().Set("Content-Type", "application/json")
  155. switch r.Method {
  156. case http.MethodGet:
  157. _ = json.NewEncoder(w).Encode(cfgManager.Snapshot())
  158. case http.MethodPost:
  159. var update runtime.ConfigUpdate
  160. if err := json.NewDecoder(r.Body).Decode(&update); err != nil {
  161. http.Error(w, "invalid json", http.StatusBadRequest)
  162. return
  163. }
  164. prev := cfgManager.Snapshot()
  165. next, err := cfgManager.ApplyConfig(update)
  166. if err != nil {
  167. http.Error(w, err.Error(), http.StatusBadRequest)
  168. return
  169. }
  170. sourceChanged := prev.CenterHz != next.CenterHz || prev.SampleRate != next.SampleRate || prev.GainDb != next.GainDb || prev.AGC != next.AGC || prev.TunerBwKHz != next.TunerBwKHz
  171. if sourceChanged {
  172. if err := srcMgr.ApplyConfig(next); err != nil {
  173. cfgManager.Replace(prev)
  174. http.Error(w, "failed to apply source config", http.StatusInternalServerError)
  175. return
  176. }
  177. }
  178. if err := config.Save(cfgPath, next); err != nil {
  179. log.Printf("config save failed: %v", err)
  180. }
  181. detChanged := prev.Detector.ThresholdDb != next.Detector.ThresholdDb ||
  182. prev.Detector.MinDurationMs != next.Detector.MinDurationMs ||
  183. prev.Detector.HoldMs != next.Detector.HoldMs ||
  184. prev.Detector.EmaAlpha != next.Detector.EmaAlpha ||
  185. prev.Detector.HysteresisDb != next.Detector.HysteresisDb ||
  186. prev.Detector.MinStableFrames != next.Detector.MinStableFrames ||
  187. prev.Detector.GapToleranceMs != next.Detector.GapToleranceMs ||
  188. prev.Detector.CFARMode != next.Detector.CFARMode ||
  189. prev.Detector.CFARGuardCells != next.Detector.CFARGuardCells ||
  190. prev.Detector.CFARTrainCells != next.Detector.CFARTrainCells ||
  191. prev.Detector.CFARRank != next.Detector.CFARRank ||
  192. prev.Detector.CFARScaleDb != next.Detector.CFARScaleDb ||
  193. prev.Detector.CFARWrapAround != next.Detector.CFARWrapAround ||
  194. prev.SampleRate != next.SampleRate ||
  195. prev.FFTSize != next.FFTSize
  196. windowChanged := prev.FFTSize != next.FFTSize
  197. var newDet *detector.Detector
  198. var newWindow []float64
  199. if detChanged {
  200. newDet = detector.New(next.Detector, next.SampleRate, next.FFTSize)
  201. }
  202. if windowChanged {
  203. newWindow = fftutil.Hann(next.FFTSize)
  204. }
  205. pushDSPUpdate(dspUpdates, dspUpdate{
  206. cfg: next,
  207. det: newDet,
  208. window: newWindow,
  209. dcBlock: next.DCBlock,
  210. iqBalance: next.IQBalance,
  211. useGPUFFT: next.UseGPUFFT,
  212. })
  213. _ = json.NewEncoder(w).Encode(next)
  214. default:
  215. http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
  216. }
  217. })
  218. http.HandleFunc("/api/sdr/settings", func(w http.ResponseWriter, r *http.Request) {
  219. w.Header().Set("Content-Type", "application/json")
  220. if r.Method != http.MethodPost {
  221. http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
  222. return
  223. }
  224. var update runtime.SettingsUpdate
  225. if err := json.NewDecoder(r.Body).Decode(&update); err != nil {
  226. http.Error(w, "invalid json", http.StatusBadRequest)
  227. return
  228. }
  229. prev := cfgManager.Snapshot()
  230. next, err := cfgManager.ApplySettings(update)
  231. if err != nil {
  232. http.Error(w, err.Error(), http.StatusBadRequest)
  233. return
  234. }
  235. if prev.AGC != next.AGC || prev.TunerBwKHz != next.TunerBwKHz {
  236. if err := srcMgr.ApplyConfig(next); err != nil {
  237. cfgManager.Replace(prev)
  238. http.Error(w, "failed to apply sdr settings", http.StatusInternalServerError)
  239. return
  240. }
  241. }
  242. if prev.DCBlock != next.DCBlock || prev.IQBalance != next.IQBalance {
  243. pushDSPUpdate(dspUpdates, dspUpdate{
  244. cfg: next,
  245. dcBlock: next.DCBlock,
  246. iqBalance: next.IQBalance,
  247. })
  248. }
  249. if err := config.Save(cfgPath, next); err != nil {
  250. log.Printf("config save failed: %v", err)
  251. }
  252. _ = json.NewEncoder(w).Encode(next)
  253. })
  254. http.HandleFunc("/api/stats", func(w http.ResponseWriter, r *http.Request) {
  255. w.Header().Set("Content-Type", "application/json")
  256. _ = json.NewEncoder(w).Encode(srcMgr.Stats())
  257. })
  258. http.HandleFunc("/api/gpu", func(w http.ResponseWriter, r *http.Request) {
  259. w.Header().Set("Content-Type", "application/json")
  260. _ = json.NewEncoder(w).Encode(gpuState.snapshot())
  261. })
  262. http.HandleFunc("/api/events", func(w http.ResponseWriter, r *http.Request) {
  263. w.Header().Set("Content-Type", "application/json")
  264. limit := 200
  265. if v := r.URL.Query().Get("limit"); v != "" {
  266. if parsed, err := strconv.Atoi(v); err == nil {
  267. limit = parsed
  268. }
  269. }
  270. var since time.Time
  271. if v := r.URL.Query().Get("since"); v != "" {
  272. if parsed, err := parseSince(v); err == nil {
  273. since = parsed
  274. } else {
  275. http.Error(w, "invalid since", http.StatusBadRequest)
  276. return
  277. }
  278. }
  279. snap := cfgManager.Snapshot()
  280. eventMu.RLock()
  281. evs, err := events.ReadRecent(snap.EventPath, limit, since)
  282. eventMu.RUnlock()
  283. if err != nil {
  284. http.Error(w, "failed to read events", http.StatusInternalServerError)
  285. return
  286. }
  287. _ = json.NewEncoder(w).Encode(evs)
  288. })
  289. http.HandleFunc("/api/signals", func(w http.ResponseWriter, r *http.Request) {
  290. w.Header().Set("Content-Type", "application/json")
  291. if sigSnap == nil {
  292. _ = json.NewEncoder(w).Encode([]detector.Signal{})
  293. return
  294. }
  295. _ = json.NewEncoder(w).Encode(sigSnap.get())
  296. })
  297. http.HandleFunc("/api/decoders", func(w http.ResponseWriter, r *http.Request) {
  298. w.Header().Set("Content-Type", "application/json")
  299. _ = json.NewEncoder(w).Encode(decoderKeys(cfgManager.Snapshot()))
  300. })
  301. http.HandleFunc("/api/recordings", func(w http.ResponseWriter, r *http.Request) {
  302. if r.Method != http.MethodGet {
  303. http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
  304. return
  305. }
  306. w.Header().Set("Content-Type", "application/json")
  307. snap := cfgManager.Snapshot()
  308. list, err := recorder.ListRecordings(snap.Recorder.OutputDir)
  309. if err != nil {
  310. http.Error(w, "failed to list recordings", http.StatusInternalServerError)
  311. return
  312. }
  313. _ = json.NewEncoder(w).Encode(list)
  314. })
  315. http.HandleFunc("/api/recordings/", func(w http.ResponseWriter, r *http.Request) {
  316. w.Header().Set("Content-Type", "application/json")
  317. id := strings.TrimPrefix(r.URL.Path, "/api/recordings/")
  318. if id == "" {
  319. http.Error(w, "missing id", http.StatusBadRequest)
  320. return
  321. }
  322. snap := cfgManager.Snapshot()
  323. base := filepath.Clean(filepath.Join(snap.Recorder.OutputDir, id))
  324. if !strings.HasPrefix(base, filepath.Clean(snap.Recorder.OutputDir)) {
  325. http.Error(w, "invalid path", http.StatusBadRequest)
  326. return
  327. }
  328. if r.URL.Path == "/api/recordings/"+id+"/audio" {
  329. http.ServeFile(w, r, filepath.Join(base, "audio.wav"))
  330. return
  331. }
  332. if r.URL.Path == "/api/recordings/"+id+"/iq" {
  333. http.ServeFile(w, r, filepath.Join(base, "signal.cf32"))
  334. return
  335. }
  336. if r.URL.Path == "/api/recordings/"+id+"/decode" {
  337. mode := r.URL.Query().Get("mode")
  338. cmd := buildDecoderMap(cfgManager.Snapshot())[mode]
  339. if cmd == "" {
  340. http.Error(w, "decoder not configured", http.StatusBadRequest)
  341. return
  342. }
  343. meta, err := recorder.ReadMeta(filepath.Join(base, "meta.json"))
  344. if err != nil {
  345. http.Error(w, "meta read failed", http.StatusInternalServerError)
  346. return
  347. }
  348. audioPath := filepath.Join(base, "audio.wav")
  349. if _, errStat := os.Stat(audioPath); errStat != nil {
  350. audioPath = ""
  351. }
  352. res, err := recorder.DecodeOnDemand(cmd, filepath.Join(base, "signal.cf32"), meta.SampleRate, audioPath)
  353. if err != nil {
  354. http.Error(w, res.Stderr, http.StatusInternalServerError)
  355. return
  356. }
  357. _ = json.NewEncoder(w).Encode(res)
  358. return
  359. }
  360. // default: meta.json
  361. http.ServeFile(w, r, filepath.Join(base, "meta.json"))
  362. })
  363. http.HandleFunc("/api/demod", func(w http.ResponseWriter, r *http.Request) {
  364. if r.Method != http.MethodGet {
  365. http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
  366. return
  367. }
  368. q := r.URL.Query()
  369. freq, _ := strconv.ParseFloat(q.Get("freq"), 64)
  370. bw, _ := strconv.ParseFloat(q.Get("bw"), 64)
  371. sec, _ := strconv.Atoi(q.Get("sec"))
  372. if sec < 1 {
  373. sec = 1
  374. }
  375. if sec > 10 {
  376. sec = 10
  377. }
  378. mode := q.Get("mode")
  379. data, _, err := recMgr.DemodLive(freq, bw, mode, sec)
  380. if err != nil {
  381. http.Error(w, err.Error(), http.StatusBadRequest)
  382. return
  383. }
  384. w.Header().Set("Content-Type", "audio/wav")
  385. _, _ = w.Write(data)
  386. })
  387. http.Handle("/", http.FileServer(http.Dir(cfg.WebRoot)))
  388. server := &http.Server{Addr: cfg.WebAddr}
  389. go func() {
  390. log.Printf("web listening on %s", cfg.WebAddr)
  391. if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
  392. log.Fatalf("server: %v", err)
  393. }
  394. }()
  395. stop := make(chan os.Signal, 1)
  396. signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
  397. <-stop
  398. ctxTimeout, cancelTimeout := context.WithTimeout(context.Background(), 5*time.Second)
  399. defer cancelTimeout()
  400. _ = server.Shutdown(ctxTimeout)
  401. }