Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module waveloggoat
go 1.25

require (
github.com/gorilla/websocket v1.4.2
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b
github.com/sirupsen/logrus v1.9.3
)
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00=
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand Down
210 changes: 188 additions & 22 deletions waveloggoat.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ import (
"runtime"
"strconv"
"strings"
"sync"
"time"

"github.com/gorilla/websocket"
"github.com/kolo/xmlrpc"
"github.com/sirupsen/logrus"
)
Expand All @@ -26,6 +28,19 @@ var log = logrus.New()
// version is set at build time using ldflags
var version = "dev"

// WebSocketMessage represents the JSON message sent to wavelog
// Matches WaveLogGate format exactly
type WebSocketMessage struct {
Type string `json:"type"` // radio_status
Message string `json:"message,omitempty"` // Welcome message only
Frequency int `json:"frequency,omitempty"` // Frequency in Hz
FrequencyRX int `json:"frequency_rx,omitempty"` // RX frequency for split mode
Mode string `json:"mode,omitempty"` // Operating mode
Power int `json:"power,omitempty"` // Power in watts
Radio string `json:"radio,omitempty"` // Radio name
Timestamp int64 `json:"timestamp,omitempty"` // Unix timestamp
}

// RigData holds the radio state as provided by flrig or hamlib.
type RigData struct {
FreqVFOA float64
Expand All @@ -45,21 +60,21 @@ type WavelogJSONRequest struct {
Mode string `json:"mode"`
FrequencyRX int `json:"frequency_rx,omitempty"`
ModeRX string `json:"mode_rx,omitempty"`
// Split may come in a later WaveLog version
// PTT may come in a a later WaveLog version
}

type ProfileConfig struct {
WavelogURL string `json:"wavelog_url"`
WavelogKey string `json:"wavelog_key"`
RadioName string `json:"radio_name"`
FlrigHost string `json:"flrig_host"`
FlrigPort int `json:"flrig_port"`
HamlibHost string `json:"hamlib_host"`
HamlibPort int `json:"hamlib_port"`
Interval string `json:"interval"`
DataSource string `json:"data_source"` // "flrig" or "hamlib"
LogLevel string `json:"log_level"` // "error", "warn", "info", "debug"
WavelogURL string `json:"wavelog_url"`
WavelogKey string `json:"wavelog_key"`
RadioName string `json:"radio_name"`
FlrigHost string `json:"flrig_host"`
FlrigPort int `json:"flrig_port"`
HamlibHost string `json:"hamlib_host"`
HamlibPort int `json:"hamlib_port"`
Interval string `json:"interval"`
DataSource string `json:"data_source"` // "flrig" or "hamlib"
LogLevel string `json:"log_level"` // "error", "warn", "info", "debug"
WebSocketEnable bool `json:"websocket_enable"` // enable WebSocket server
WebSocketPort int `json:"websocket_port"` // WebSocket server port (default: 54322)
}

type ConfigFile struct {
Expand Down Expand Up @@ -308,18 +323,133 @@ func postToWavelog(config ProfileConfig, data RigData) error {
return nil
}

type WebSocketServer struct {
clients map[*websocket.Conn]bool
clientsMu sync.RWMutex
upgrader websocket.Upgrader
port int
}

func broadcastToWavelog(server *WebSocketServer, message WebSocketMessage) {
messageBytes, err := json.Marshal(message)
if err != nil {
log.Errorf("Failed to marshal WebSocket message: %v", err)
return
}

server.clientsMu.RLock()
defer server.clientsMu.RUnlock()

for client := range server.clients {
if err := client.WriteMessage(websocket.TextMessage, messageBytes); err != nil {
log.Errorf("Failed to send message to Wavelog: %v", err)
client.Close()
delete(server.clients, client)
}
}
log.Debugf("Broadcasted radio status to Wavelog: freq=%d, mode=%s", message.Frequency, message.Mode)
}

func startWebSocketServer(port int) (*WebSocketServer, error) {
server := &WebSocketServer{
clients: make(map[*websocket.Conn]bool),
port: port,
upgrader: websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
},
}

mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Upgrade HTTP connection to WebSocket
conn, err := server.upgrader.Upgrade(w, r, nil)
if err != nil {
log.Debugf("WebSocket upgrade failed: %v", err)
return
}

server.clientsMu.Lock()
server.clients[conn] = true
server.clientsMu.Unlock()

log.Infof("WebSocket client connected")

welcomeMsg := WebSocketMessage{
Type: "welcome",
Message: "Connected to WaveLogGoat WebSocket server",
}
welcomeBytes, err := json.Marshal(welcomeMsg)
if err != nil {
log.Errorf("Failed to marshal welcome message: %v", err)
conn.Close()
server.clientsMu.Lock()
delete(server.clients, conn)
server.clientsMu.Unlock()
return
}

if err := conn.WriteMessage(websocket.TextMessage, welcomeBytes); err != nil {
log.Errorf("Failed to send welcome message: %v", err)
conn.Close()
server.clientsMu.Lock()
delete(server.clients, conn)
server.clientsMu.Unlock()
return
}

defer func() {
conn.Close()
server.clientsMu.Lock()
delete(server.clients, conn)
server.clientsMu.Unlock()
log.Infof("WebSocket client disconnected")
}()

for {
_, _, err := conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
log.Errorf("WebSocket error: %v", err)
}
break
}
// WaveLogGate doesn't process incoming messages, just ignore them
}
})

httpServer := &http.Server{
Addr: ":" + strconv.Itoa(port),
Handler: mux,
}

log.Infof("Starting WebSocket server on port %d", port)
log.Infof("WebSocket endpoint: ws://localhost:%d/", port)

go func() {
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Errorf("WebSocket server error: %v", err)
}
}()

return server, nil
}

func main() {
defaultConfig := ProfileConfig{
WavelogURL: "http://localhost/index.php",
WavelogKey: "YOUR_API_KEY",
RadioName: "RIG",
FlrigHost: "127.0.0.1",
FlrigPort: 12345,
HamlibHost: "127.0.0.1",
HamlibPort: 4532,
Interval: "1s",
DataSource: "flrig",
LogLevel: "error",
WavelogURL: "http://localhost/index.php",
WavelogKey: "YOUR_API_KEY",
RadioName: "RIG",
FlrigHost: "127.0.0.1",
FlrigPort: 12345,
HamlibHost: "127.0.0.1",
HamlibPort: 4532,
Interval: "1s",
DataSource: "flrig",
LogLevel: "error",
WebSocketEnable: true,
WebSocketPort: 54322,
}

var currentProfileName string
Expand All @@ -342,6 +472,8 @@ func main() {
interval := flag.String("interval", defaultConfig.Interval, "Polling interval (e.g., 1s, 1500ms).")
dataSource := flag.String("data-source", defaultConfig.DataSource, "Data source: 'flrig' or 'hamlib'.")
logLevel := flag.String("log-level", defaultConfig.LogLevel, "Logging level: 'debug', 'info', 'warn', or 'error'.")
websocketEnable := flag.Bool("websocket-enable", defaultConfig.WebSocketEnable, "Enable WebSocket server for real-time radio status.")
websocketPort := flag.Int("websocket-port", defaultConfig.WebSocketPort, "WebSocket server port (default: 54322).")

// Parse flags initially to handle the special -save-profile and -set-default-profile flags
flag.Parse()
Expand Down Expand Up @@ -408,6 +540,10 @@ func main() {
currentProfileConfig.DataSource = *dataSource
case "log-level":
currentProfileConfig.LogLevel = *logLevel
case "websocket-enable":
currentProfileConfig.WebSocketEnable = *websocketEnable
case "websocket-port":
currentProfileConfig.WebSocketPort = *websocketPort
}
})

Expand Down Expand Up @@ -462,9 +598,21 @@ func main() {
log.Fatalf("Fatal: Invalid interval duration format: %v", err)
}

var webSocketServer *WebSocketServer
if currentProfileConfig.WebSocketEnable {
var err error
webSocketServer, err = startWebSocketServer(currentProfileConfig.WebSocketPort)
if err != nil {
log.Errorf("Failed to start WebSocket server: %v", err)
}
}

var lastData RigData
lastUpdate := time.Time{}
log.Infof("Starting WaveLogGoat polling every %s...", intervalDuration)
if currentProfileConfig.WebSocketEnable {
log.Infof("WebSocket server enabled on port %d", currentProfileConfig.WebSocketPort)
}

for {
time.Sleep(intervalDuration)
Expand Down Expand Up @@ -498,5 +646,23 @@ func main() {
lastData = currentData
lastUpdate = time.Now()
log.Debug("Successfully updated Wavelog.")

// Broadcast to WebSocket clients if enabled
if currentProfileConfig.WebSocketEnable && webSocketServer != nil {
wsMessage := WebSocketMessage{
Type: "radio_status",
Frequency: int(currentData.FreqVFOA),
Mode: currentData.Mode,
Power: int(currentData.Power),
Radio: currentProfileConfig.RadioName,
Timestamp: time.Now().Unix(),
}
// Include frequency_rx for split mode (exactly like WaveLogGate)
if currentData.Split != 0 {
wsMessage.FrequencyRX = int(currentData.FreqVFOA)
wsMessage.Frequency = int(currentData.FreqVFOB)
}
broadcastToWavelog(webSocketServer, wsMessage)
}
}
}