Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

217 linhas
5.8KB

  1. package control
  2. import (
  3. "encoding/json"
  4. "net/http"
  5. "sync"
  6. "github.com/jan/fm-rds-tx/internal/config"
  7. drypkg "github.com/jan/fm-rds-tx/internal/dryrun"
  8. "github.com/jan/fm-rds-tx/internal/platform"
  9. )
  10. // TXController is an optional interface the Server uses to start/stop TX.
  11. type TXController interface {
  12. StartTX() error
  13. StopTX() error
  14. TXStats() map[string]any
  15. }
  16. type Server struct {
  17. mu sync.RWMutex
  18. cfg config.Config
  19. tx TXController
  20. drv platform.SoapyDriver // optional, for runtime stats
  21. }
  22. type ConfigPatch struct {
  23. FrequencyMHz *float64 `json:"frequencyMHz,omitempty"`
  24. OutputDrive *float64 `json:"outputDrive,omitempty"`
  25. ToneLeftHz *float64 `json:"toneLeftHz,omitempty"`
  26. ToneRightHz *float64 `json:"toneRightHz,omitempty"`
  27. ToneAmplitude *float64 `json:"toneAmplitude,omitempty"`
  28. PS *string `json:"ps,omitempty"`
  29. RadioText *string `json:"radioText,omitempty"`
  30. PreEmphasisTauUS *float64 `json:"preEmphasisTauUS,omitempty"`
  31. LimiterEnabled *bool `json:"limiterEnabled,omitempty"`
  32. LimiterCeiling *float64 `json:"limiterCeiling,omitempty"`
  33. }
  34. func NewServer(cfg config.Config) *Server {
  35. return &Server{cfg: cfg}
  36. }
  37. func (s *Server) SetTXController(tx TXController) {
  38. s.mu.Lock()
  39. s.tx = tx
  40. s.mu.Unlock()
  41. }
  42. func (s *Server) SetDriver(drv platform.SoapyDriver) {
  43. s.mu.Lock()
  44. s.drv = drv
  45. s.mu.Unlock()
  46. }
  47. func (s *Server) Handler() http.Handler {
  48. mux := http.NewServeMux()
  49. mux.HandleFunc("/healthz", s.handleHealth)
  50. mux.HandleFunc("/status", s.handleStatus)
  51. mux.HandleFunc("/dry-run", s.handleDryRun)
  52. mux.HandleFunc("/config", s.handleConfig)
  53. mux.HandleFunc("/runtime", s.handleRuntime)
  54. mux.HandleFunc("/tx/start", s.handleTXStart)
  55. mux.HandleFunc("/tx/stop", s.handleTXStop)
  56. return mux
  57. }
  58. func (s *Server) handleHealth(w http.ResponseWriter, _ *http.Request) {
  59. w.Header().Set("Content-Type", "application/json")
  60. _ = json.NewEncoder(w).Encode(map[string]any{"ok": true})
  61. }
  62. func (s *Server) handleStatus(w http.ResponseWriter, _ *http.Request) {
  63. s.mu.RLock()
  64. cfg := s.cfg
  65. s.mu.RUnlock()
  66. w.Header().Set("Content-Type", "application/json")
  67. _ = json.NewEncoder(w).Encode(map[string]any{
  68. "service": "fm-rds-tx",
  69. "backend": cfg.Backend.Kind,
  70. "frequencyMHz": cfg.FM.FrequencyMHz,
  71. "stereoEnabled": cfg.FM.StereoEnabled,
  72. "rdsEnabled": cfg.RDS.Enabled,
  73. "preEmphasisTauUS": cfg.FM.PreEmphasisTauUS,
  74. "limiterEnabled": cfg.FM.LimiterEnabled,
  75. "fmModulationEnabled": cfg.FM.FMModulationEnabled,
  76. })
  77. }
  78. func (s *Server) handleRuntime(w http.ResponseWriter, _ *http.Request) {
  79. s.mu.RLock()
  80. drv := s.drv
  81. tx := s.tx
  82. s.mu.RUnlock()
  83. result := map[string]any{}
  84. if drv != nil {
  85. result["driver"] = drv.Stats()
  86. }
  87. if tx != nil {
  88. result["engine"] = tx.TXStats()
  89. }
  90. w.Header().Set("Content-Type", "application/json")
  91. _ = json.NewEncoder(w).Encode(result)
  92. }
  93. func (s *Server) handleTXStart(w http.ResponseWriter, r *http.Request) {
  94. if r.Method != http.MethodPost {
  95. http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
  96. return
  97. }
  98. s.mu.RLock()
  99. tx := s.tx
  100. s.mu.RUnlock()
  101. if tx == nil {
  102. http.Error(w, "tx controller not available", http.StatusServiceUnavailable)
  103. return
  104. }
  105. if err := tx.StartTX(); err != nil {
  106. http.Error(w, err.Error(), http.StatusConflict)
  107. return
  108. }
  109. w.Header().Set("Content-Type", "application/json")
  110. _ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "action": "started"})
  111. }
  112. func (s *Server) handleTXStop(w http.ResponseWriter, r *http.Request) {
  113. if r.Method != http.MethodPost {
  114. http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
  115. return
  116. }
  117. s.mu.RLock()
  118. tx := s.tx
  119. s.mu.RUnlock()
  120. if tx == nil {
  121. http.Error(w, "tx controller not available", http.StatusServiceUnavailable)
  122. return
  123. }
  124. if err := tx.StopTX(); err != nil {
  125. http.Error(w, err.Error(), http.StatusInternalServerError)
  126. return
  127. }
  128. w.Header().Set("Content-Type", "application/json")
  129. _ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "action": "stopped"})
  130. }
  131. func (s *Server) handleDryRun(w http.ResponseWriter, _ *http.Request) {
  132. s.mu.RLock()
  133. cfg := s.cfg
  134. s.mu.RUnlock()
  135. w.Header().Set("Content-Type", "application/json")
  136. _ = json.NewEncoder(w).Encode(drypkg.Generate(cfg))
  137. }
  138. func (s *Server) handleConfig(w http.ResponseWriter, r *http.Request) {
  139. switch r.Method {
  140. case http.MethodGet:
  141. s.mu.RLock()
  142. cfg := s.cfg
  143. s.mu.RUnlock()
  144. w.Header().Set("Content-Type", "application/json")
  145. _ = json.NewEncoder(w).Encode(cfg)
  146. case http.MethodPost:
  147. var patch ConfigPatch
  148. if err := json.NewDecoder(r.Body).Decode(&patch); err != nil {
  149. http.Error(w, err.Error(), http.StatusBadRequest)
  150. return
  151. }
  152. s.mu.Lock()
  153. next := s.cfg
  154. if patch.FrequencyMHz != nil {
  155. next.FM.FrequencyMHz = *patch.FrequencyMHz
  156. }
  157. if patch.OutputDrive != nil {
  158. next.FM.OutputDrive = *patch.OutputDrive
  159. }
  160. if patch.ToneLeftHz != nil {
  161. next.Audio.ToneLeftHz = *patch.ToneLeftHz
  162. }
  163. if patch.ToneRightHz != nil {
  164. next.Audio.ToneRightHz = *patch.ToneRightHz
  165. }
  166. if patch.ToneAmplitude != nil {
  167. next.Audio.ToneAmplitude = *patch.ToneAmplitude
  168. }
  169. if patch.PS != nil {
  170. next.RDS.PS = *patch.PS
  171. }
  172. if patch.RadioText != nil {
  173. next.RDS.RadioText = *patch.RadioText
  174. }
  175. if patch.PreEmphasisTauUS != nil {
  176. next.FM.PreEmphasisTauUS = *patch.PreEmphasisTauUS
  177. }
  178. if patch.LimiterEnabled != nil {
  179. next.FM.LimiterEnabled = *patch.LimiterEnabled
  180. }
  181. if patch.LimiterCeiling != nil {
  182. next.FM.LimiterCeiling = *patch.LimiterCeiling
  183. }
  184. if err := next.Validate(); err != nil {
  185. s.mu.Unlock()
  186. http.Error(w, err.Error(), http.StatusBadRequest)
  187. return
  188. }
  189. s.cfg = next
  190. s.mu.Unlock()
  191. w.Header().Set("Content-Type", "application/json")
  192. _ = json.NewEncoder(w).Encode(map[string]any{"ok": true})
  193. default:
  194. http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
  195. }
  196. }