350 lines
9.5 KiB
Go
350 lines
9.5 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gorilla/mux"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
type HTTPServer struct {
|
|
Router *mux.Router
|
|
FlexRepo *FlexDXClusterRepository
|
|
ContactRepo *Log4OMContactsRepository
|
|
TCPServer *TCPServer
|
|
TCPClient *TCPClient
|
|
FlexClient *FlexClient
|
|
Port string
|
|
Log *log.Logger
|
|
statsCache Stats
|
|
statsMutex sync.RWMutex
|
|
lastUpdate time.Time
|
|
}
|
|
|
|
type Stats struct {
|
|
TotalSpots int `json:"totalSpots"`
|
|
ActiveSpotters int `json:"activeSpotters"`
|
|
NewDXCC int `json:"newDXCC"`
|
|
ConnectedClients int `json:"connectedClients"`
|
|
TotalContacts int `json:"totalContacts"`
|
|
ClusterStatus string `json:"clusterStatus"`
|
|
FlexStatus string `json:"flexStatus"`
|
|
MyCallsign string `json:"myCallsign"`
|
|
Mode string `json:"mode"`
|
|
Filters Filters `json:"filters"`
|
|
}
|
|
|
|
type Filters struct {
|
|
Skimmer bool `json:"skimmer"`
|
|
FT8 bool `json:"ft8"`
|
|
FT4 bool `json:"ft4"`
|
|
}
|
|
|
|
type APIResponse struct {
|
|
Success bool `json:"success"`
|
|
Data interface{} `json:"data,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
|
|
type SendCallsignRequest struct {
|
|
Callsign string `json:"callsign"`
|
|
}
|
|
|
|
func NewHTTPServer(flexRepo *FlexDXClusterRepository, contactRepo *Log4OMContactsRepository,
|
|
tcpServer *TCPServer, tcpClient *TCPClient, flexClient *FlexClient, port string) *HTTPServer {
|
|
|
|
server := &HTTPServer{
|
|
Router: mux.NewRouter(),
|
|
FlexRepo: flexRepo,
|
|
ContactRepo: contactRepo,
|
|
TCPServer: tcpServer,
|
|
TCPClient: tcpClient,
|
|
FlexClient: flexClient,
|
|
Port: port,
|
|
Log: Log,
|
|
}
|
|
|
|
server.setupRoutes()
|
|
return server
|
|
}
|
|
|
|
func (s *HTTPServer) setupRoutes() {
|
|
// Enable CORS
|
|
s.Router.Use(s.corsMiddleware)
|
|
|
|
// API Routes
|
|
api := s.Router.PathPrefix("/api").Subrouter()
|
|
|
|
api.HandleFunc("/stats", s.getStats).Methods("GET", "OPTIONS")
|
|
api.HandleFunc("/spots", s.getSpots).Methods("GET", "OPTIONS")
|
|
api.HandleFunc("/spots/{id}", s.getSpotByID).Methods("GET", "OPTIONS")
|
|
api.HandleFunc("/spotters", s.getTopSpotters).Methods("GET", "OPTIONS")
|
|
api.HandleFunc("/contacts", s.getContacts).Methods("GET", "OPTIONS")
|
|
api.HandleFunc("/send-command", s.sendCommand).Methods("POST", "OPTIONS")
|
|
api.HandleFunc("/filters", s.updateFilters).Methods("POST", "OPTIONS")
|
|
api.HandleFunc("/shutdown", s.shutdownApp).Methods("POST", "OPTIONS")
|
|
api.HandleFunc("/send-callsign", s.handleSendCallsign).Methods("POST", "OPTIONS")
|
|
|
|
// Serve static files (dashboard)
|
|
s.Router.PathPrefix("/").Handler(http.FileServer(http.Dir("./static")))
|
|
}
|
|
|
|
func (s *HTTPServer) corsMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
|
|
|
if r.Method == "OPTIONS" {
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
func (s *HTTPServer) getStats(w http.ResponseWriter, r *http.Request) {
|
|
// Cache stats for 5 seconds to avoid overwhelming the database
|
|
s.statsMutex.RLock()
|
|
if time.Since(s.lastUpdate) < 5*time.Second {
|
|
stats := s.statsCache
|
|
s.statsMutex.RUnlock()
|
|
s.sendJSON(w, APIResponse{Success: true, Data: stats})
|
|
return
|
|
}
|
|
s.statsMutex.RUnlock()
|
|
|
|
// Calculate fresh stats
|
|
s.statsMutex.Lock()
|
|
defer s.statsMutex.Unlock()
|
|
|
|
allSpots := s.FlexRepo.GetAllSpots("0")
|
|
spotters := s.FlexRepo.GetSpotters()
|
|
contacts := s.ContactRepo.CountEntries()
|
|
|
|
newDXCCCount := 0
|
|
for _, spot := range allSpots {
|
|
if spot.NewDXCC {
|
|
newDXCCCount++
|
|
}
|
|
}
|
|
|
|
clusterStatus := "disconnected"
|
|
if s.TCPClient != nil && s.TCPClient.LoggedIn {
|
|
clusterStatus = "connected"
|
|
}
|
|
|
|
flexStatus := "disconnected"
|
|
if s.FlexClient != nil && s.FlexClient.IsConnected {
|
|
flexStatus = "connected"
|
|
}
|
|
|
|
s.statsCache = Stats{
|
|
TotalSpots: len(allSpots),
|
|
ActiveSpotters: len(spotters),
|
|
NewDXCC: newDXCCCount,
|
|
ConnectedClients: len(s.TCPServer.Clients),
|
|
TotalContacts: contacts,
|
|
ClusterStatus: clusterStatus,
|
|
FlexStatus: flexStatus,
|
|
MyCallsign: Cfg.General.Callsign, // Nouveau
|
|
Filters: Filters{
|
|
Skimmer: Cfg.Cluster.Skimmer,
|
|
FT8: Cfg.Cluster.FT8,
|
|
FT4: Cfg.Cluster.FT4,
|
|
},
|
|
}
|
|
s.lastUpdate = time.Now()
|
|
|
|
s.sendJSON(w, APIResponse{Success: true, Data: s.statsCache})
|
|
}
|
|
|
|
func (s *HTTPServer) getSpots(w http.ResponseWriter, r *http.Request) {
|
|
limitStr := r.URL.Query().Get("limit")
|
|
if limitStr == "" {
|
|
limitStr = "50"
|
|
}
|
|
|
|
spots := s.FlexRepo.GetAllSpots(limitStr)
|
|
|
|
s.sendJSON(w, APIResponse{Success: true, Data: spots})
|
|
}
|
|
|
|
func (s *HTTPServer) getSpotByID(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
id := vars["id"]
|
|
|
|
spot, err := s.FlexRepo.FindSpotByFlexSpotNumber(id)
|
|
if err != nil {
|
|
s.sendJSON(w, APIResponse{Success: false, Error: "Spot not found"})
|
|
return
|
|
}
|
|
|
|
s.sendJSON(w, APIResponse{Success: true, Data: spot})
|
|
}
|
|
|
|
func (s *HTTPServer) getTopSpotters(w http.ResponseWriter, r *http.Request) {
|
|
spotters := s.FlexRepo.GetSpotters()
|
|
s.sendJSON(w, APIResponse{Success: true, Data: spotters})
|
|
}
|
|
|
|
func (s *HTTPServer) getContacts(w http.ResponseWriter, r *http.Request) {
|
|
count := s.ContactRepo.CountEntries()
|
|
|
|
data := map[string]interface{}{
|
|
"totalContacts": count,
|
|
}
|
|
|
|
s.sendJSON(w, APIResponse{Success: true, Data: data})
|
|
}
|
|
|
|
type CommandRequest struct {
|
|
Command string `json:"command"`
|
|
}
|
|
|
|
func (s *HTTPServer) sendCommand(w http.ResponseWriter, r *http.Request) {
|
|
var req CommandRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
s.sendJSON(w, APIResponse{Success: false, Error: "Invalid request"})
|
|
return
|
|
}
|
|
|
|
if req.Command == "" {
|
|
s.sendJSON(w, APIResponse{Success: false, Error: "Command is required"})
|
|
return
|
|
}
|
|
|
|
// Send command to cluster via TCPClient
|
|
s.TCPClient.CmdChan <- req.Command
|
|
|
|
s.Log.Infof("Command sent via HTTP API: %s", req.Command)
|
|
s.sendJSON(w, APIResponse{Success: true, Data: map[string]string{"message": "Command sent successfully"}})
|
|
}
|
|
|
|
type FilterRequest struct {
|
|
Skimmer *bool `json:"skimmer,omitempty"`
|
|
FT8 *bool `json:"ft8,omitempty"`
|
|
FT4 *bool `json:"ft4,omitempty"`
|
|
}
|
|
|
|
func (s *HTTPServer) updateFilters(w http.ResponseWriter, r *http.Request) {
|
|
var req FilterRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
s.sendJSON(w, APIResponse{Success: false, Error: "Invalid request"})
|
|
return
|
|
}
|
|
|
|
commands := []string{}
|
|
|
|
if req.Skimmer != nil {
|
|
if *req.Skimmer {
|
|
commands = append(commands, "set/skimmer")
|
|
Cfg.Cluster.Skimmer = true
|
|
} else {
|
|
commands = append(commands, "set/noskimmer")
|
|
Cfg.Cluster.Skimmer = false
|
|
}
|
|
}
|
|
|
|
if req.FT8 != nil {
|
|
if *req.FT8 {
|
|
commands = append(commands, "set/ft8")
|
|
Cfg.Cluster.FT8 = true
|
|
} else {
|
|
commands = append(commands, "set/noft8")
|
|
Cfg.Cluster.FT8 = false
|
|
}
|
|
}
|
|
|
|
if req.FT4 != nil {
|
|
if *req.FT4 {
|
|
commands = append(commands, "set/ft4")
|
|
Cfg.Cluster.FT4 = true
|
|
} else {
|
|
commands = append(commands, "set/noft4")
|
|
Cfg.Cluster.FT4 = false
|
|
}
|
|
}
|
|
|
|
for _, cmd := range commands {
|
|
s.TCPClient.CmdChan <- cmd
|
|
}
|
|
|
|
s.sendJSON(w, APIResponse{Success: true, Data: map[string]string{"message": "Filters updated successfully"}})
|
|
}
|
|
|
|
func (s *HTTPServer) handleSendCallsign(w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
Callsign string `json:"callsign"`
|
|
Frequency string `json:"frequency"`
|
|
Mode string `json:"mode"`
|
|
}
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
s.sendJSON(w, APIResponse{Success: false, Error: "Invalid request"})
|
|
return
|
|
}
|
|
|
|
if req.Callsign == "" {
|
|
s.sendJSON(w, APIResponse{Success: false, Error: "Callsign is required"})
|
|
return
|
|
}
|
|
|
|
// Envoyer l'indicatif à Log4OM via UDP
|
|
SendUDPMessage("<CALLSIGN>" + req.Callsign)
|
|
s.Log.Infof("Sent callsign %s to Log4OM via UDP (127.0.0.1:2241)", req.Callsign)
|
|
|
|
// Tuner la radio Flex sur la fréquence si elle est fournie
|
|
if req.Frequency != "" && s.FlexClient != nil && s.FlexClient.IsConnected {
|
|
// Commande TUNE pour le slice 0 selon l'API FlexRadio
|
|
tuneCmd := fmt.Sprintf("C%v|slice tune 0 %s", CommandNumber, req.Frequency)
|
|
s.FlexClient.Write(tuneCmd)
|
|
CommandNumber++
|
|
time.Sleep(time.Millisecond * 500)
|
|
modeCmd := fmt.Sprintf("C%v|slice s 0 mode=%s", CommandNumber, req.Mode)
|
|
s.FlexClient.Write(modeCmd)
|
|
CommandNumber++
|
|
s.Log.Infof("Sent TUNE command to Flex: %s", tuneCmd)
|
|
}
|
|
|
|
s.sendJSON(w, APIResponse{
|
|
Success: true,
|
|
Message: "Callsign sent to Log4OM and radio tuned",
|
|
Data: map[string]string{"callsign": req.Callsign, "frequency": req.Frequency},
|
|
})
|
|
}
|
|
|
|
func (s *HTTPServer) sendJSON(w http.ResponseWriter, data interface{}) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(data)
|
|
}
|
|
|
|
func (s *HTTPServer) shutdownApp(w http.ResponseWriter, r *http.Request) {
|
|
s.Log.Info("Shutdown request received from dashboard")
|
|
|
|
s.sendJSON(w, APIResponse{Success: true, Data: map[string]string{"message": "Shutting down FlexDXCluster"}})
|
|
|
|
// Shutdown gracefully after a short delay to allow response to be sent
|
|
go func() {
|
|
time.Sleep(500 * time.Millisecond)
|
|
s.Log.Info("Initiating shutdown...")
|
|
os.Exit(0)
|
|
}()
|
|
}
|
|
|
|
func (s *HTTPServer) Start() {
|
|
s.Log.Infof("HTTP Server starting on port %s", s.Port)
|
|
s.Log.Infof("Dashboard available at http://localhost:%s", s.Port)
|
|
|
|
if err := http.ListenAndServe(":"+s.Port, s.Router); err != nil {
|
|
s.Log.Fatalf("Failed to start HTTP server: %v", err)
|
|
}
|
|
}
|