diff --git a/README.md b/README.md index 5f80e98..a7c41f3 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,6 @@ This section describes the WebSocket commands that clients can send to the Pomod Both client commands (sent to the server) and server messages (sent to the client) use the JSON format. The structure of these messages are described in the sections below. - ### Client Commands Clients communicate with the server by sending JSON-encoded commands. Each command must include a `command` field specifying the action to perform. Here are the available commands: @@ -51,19 +50,23 @@ Example: ### Server Messages -The server sends JSON-encoded messages to all connected clients to update them on the current state of the Pomodoro session. +The server periodically (every second) sends JSON-encoded messages to all connected clients to update them on the current state of the Pomodoro session. These messages contain the following fields: - mode: Indicates the current phase of the Pomodoro session ("Work", "ShortBreak", "LongBreak", or "none" if no session is running). - session: The current session number (e.g., 1 for the first work session). - total_sessions: The total number of sessions for the current Pomodoro cycle (e.g., 4 if the cycle consists of 4 work/break sessions). - time_left: The remaining time for the current mode, in seconds (e.g., 900 for 15 minutes). +- ongoing: Wether a pomodoro is currently ongoing +- paused: Wether the timer is paused -| Message Type | Sent by Server | Example | -| --------------------- | ------------------------------------------------ | --------------------------------------------------- | -| Welcome Message | Upon initial client connection | `{"mode": "none", "session": 0, "total_sessions": 0, "time_left": 0}` | -| Session Update | Periodically during a session | `{"mode": "Work", "session": 1, "total_sessions": 4, "time_left": 900}` | -| Session End/Reset | After session completes or is reset | `{"mode": "none", "session": 0, "total_sessions": 0, "time_left": 0}` | +| Message Type | Example | +| --------------------- | --------------------------------------------------- | +| Welcome Message | `{"mode": "none", "session":0, "total_sessions":0, "time_left":0, "ongoing": false, "paused": false}` | +| Session Running | `{"mode": "Work", "session": 1, "total_sessions": 4, "time_left": 900, "ongoing": true, "paused": false}` | +| Session Running | `{"mode": "ShortBreak", "session": 2, "total_sessions": 4, "time_left": 50, "ongoing": true, "paused": false}` | +| Session Paused | `{"mode": "Work", "session": 2, "total_sessions": 4, "time_left": 456, "ongoing": true, "paused": true}` | +| Session End/Reset | `{"mode": "none", "session": 0, "total_sessions": 0, "time_left": 0, "ongoing": false, "paused": false}` | ## Testing diff --git a/cmd/server/main.go b/cmd/server/main.go index 2105b39..bf3f949 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -3,6 +3,7 @@ package server import ( "flag" "fmt" + "git.smsvc.net/pomodoro/GoTomato/internal/broadcast" "git.smsvc.net/pomodoro/GoTomato/internal/websocket" "git.smsvc.net/pomodoro/GoTomato/pkg/models" "log" @@ -23,6 +24,7 @@ func Start() { listen := fmt.Sprintf("%s:%d", serverConfig.ListenAddress, serverConfig.ListenPort) http.HandleFunc("/ws", websocket.HandleConnections) + go broadcast.SendPermanentBroadCastMessage() log.Printf("Pomodoro WebSocket server started on %s\n", listen) err := http.ListenAndServe(listen, nil) diff --git a/internal/broadcast/broadcast.go b/internal/broadcast/broadcast.go index 1be04ce..bd93d06 100644 --- a/internal/broadcast/broadcast.go +++ b/internal/broadcast/broadcast.go @@ -5,23 +5,37 @@ import ( "git.smsvc.net/pomodoro/GoTomato/pkg/models" "github.com/gorilla/websocket" "log" + "time" ) -// BroadcastMessage sends a message to all connected WebSocket clients. -func BroadcastMessage(clients map[*websocket.Conn]*models.Client, message models.ServerMessage) { - // Marshal the message into JSON format - jsonMessage, err := json.Marshal(message) - if err != nil { - log.Printf("Error marshalling message: %v", err) - return - } +// Clients is a map of connected WebSocket clients, where each client is represented by the Client struct +var Clients = make(map[*websocket.Conn]*models.Client) +var Message = models.ServerMessage{ + Mode: "none", + Session: 0, + TotalSession: 0, + TimeLeft: 0, + Ongoing: false, + Paused: false, +} - // Iterate over all connected clients and broadcast the message - for _, client := range clients { - err := client.SendMessage(websocket.TextMessage, jsonMessage) +// BroadcastMessage sends a message to all connected WebSocket clients. +func SendPermanentBroadCastMessage() { + for { + // Marshal the message into JSON format + jsonMessage, err := json.Marshal(Message) if err != nil { - log.Printf("Error broadcasting to client: %v", err) - // The client is responsible for closing itself on error + log.Printf("Error marshalling message: %v", err) + return } + // Iterate over all connected clients and broadcast the message + for _, client := range Clients { + err := client.SendMessage(websocket.TextMessage, jsonMessage) + if err != nil { + log.Printf("Error broadcasting to client: %v", err) + // The client is responsible for closing itself on error + } + } + time.Sleep(time.Second) } } diff --git a/internal/pomodoro/pomodoro.go b/internal/pomodoro/pomodoro.go index 3124b6c..21b2fa2 100644 --- a/internal/pomodoro/pomodoro.go +++ b/internal/pomodoro/pomodoro.go @@ -7,9 +7,6 @@ import ( "sync" ) -var pomodoroOngoing bool -var pomodoroPaused bool - var pomodoroResetChannel = make(chan bool, 1) var pomodoroPauseChannel = make(chan bool, 1) var pomodoroResumeChannel = make(chan bool, 1) @@ -19,52 +16,53 @@ var mu sync.Mutex // to synchronize access to shared state // RunPomodoro iterates the Pomodoro work/break sessions. func RunPomodoro(clients map[*websocket.Conn]*models.Client, config models.GoTomatoPomodoroConfig) { mu.Lock() - pomodoroOngoing = true - pomodoroPaused = false + broadcast.Message.Ongoing = true + broadcast.Message.Paused = false mu.Unlock() + broadcast.Message.TotalSession = config.Sessions + for session := 1; session <= config.Sessions; session++ { - if !startTimer(clients, config.Work, "Work", session, config.Sessions) { + broadcast.Message.Session = session + if !startTimer(clients, config.Work, "Work") { break } if session == config.Sessions { - if !startTimer(clients, config.LongBreak, "LongBreak", session, config.Sessions) { + if !startTimer(clients, config.LongBreak, "LongBreak") { break } } else { - if !startTimer(clients, config.ShortBreak, "ShortBreak", session, config.Sessions) { + if !startTimer(clients, config.ShortBreak, "ShortBreak") { break } } } mu.Lock() - pomodoroOngoing = false + broadcast.Message.Ongoing = false mu.Unlock() } // ResetPomodoro resets the running Pomodoro timer. func ResetPomodoro(clients map[*websocket.Conn]*models.Client) { - mu.Lock() - pomodoroOngoing = false // Reset the running state - pomodoroPaused = false // Reset the paused state - mu.Unlock() - - // Broadcast the reset message to all clients - broadcast.BroadcastMessage(clients, models.ServerMessage{ - Mode: "none", - Session: 0, - TotalSession: 0, - TimeLeft: 0, - }) - // Send a reset signal to stop any running timers pomodoroResetChannel <- true + + mu.Lock() + broadcast.Message.Ongoing = false + broadcast.Message.Paused = false + mu.Unlock() + + // Reset message + broadcast.Message.Mode = "none" + broadcast.Message.Session = 0 + broadcast.Message.TotalSession = 0 + broadcast.Message.TimeLeft = 0 } func PausePomodoro() { mu.Lock() - pomodoroPaused = true + broadcast.Message.Paused = true mu.Unlock() pomodoroPauseChannel <- true @@ -72,7 +70,7 @@ func PausePomodoro() { func ResumePomodoro() { mu.Lock() - pomodoroPaused = false + broadcast.Message.Paused = false mu.Unlock() pomodoroResumeChannel <- true } @@ -80,11 +78,11 @@ func ResumePomodoro() { func IsPomodoroOngoing() bool { mu.Lock() defer mu.Unlock() // Ensures that the mutex is unlocked after the function is done - return pomodoroOngoing + return broadcast.Message.Ongoing } func IsPomodoroPaused() bool { mu.Lock() defer mu.Unlock() // Ensures that the mutex is unlocked after the function is done - return pomodoroPaused + return broadcast.Message.Paused } diff --git a/internal/pomodoro/timer.go b/internal/pomodoro/timer.go index f23d286..272b989 100644 --- a/internal/pomodoro/timer.go +++ b/internal/pomodoro/timer.go @@ -8,7 +8,7 @@ import ( ) // startTimer runs the countdown and broadcasts every second. -func startTimer(clients map[*websocket.Conn]*models.Client, remainingSeconds int, mode string, session int, totalSessions int) bool { +func startTimer(clients map[*websocket.Conn]*models.Client, remainingSeconds int, mode string) bool { for remainingSeconds > 0 { select { case <-pomodoroResetChannel: @@ -20,12 +20,8 @@ func startTimer(clients map[*websocket.Conn]*models.Client, remainingSeconds int default: // Broadcast the current state to all clients if !IsPomodoroPaused() { - broadcast.BroadcastMessage(clients, models.ServerMessage{ - Mode: mode, - Session: session, - TotalSession: totalSessions, - TimeLeft: remainingSeconds, - }) + broadcast.Message.Mode = mode + broadcast.Message.TimeLeft = remainingSeconds time.Sleep(time.Second) remainingSeconds-- } @@ -33,12 +29,7 @@ func startTimer(clients map[*websocket.Conn]*models.Client, remainingSeconds int } // Final broadcast when time reaches zero - broadcast.BroadcastMessage(clients, models.ServerMessage{ - Mode: mode, - Session: session, - TotalSession: totalSessions, - TimeLeft: 0, - }) + broadcast.Message.TimeLeft = remainingSeconds return true } diff --git a/internal/websocket/client_commands.go b/internal/websocket/client_commands.go index 1005006..b1c6f88 100644 --- a/internal/websocket/client_commands.go +++ b/internal/websocket/client_commands.go @@ -2,6 +2,7 @@ package websocket import ( "encoding/json" + "git.smsvc.net/pomodoro/GoTomato/internal/broadcast" "git.smsvc.net/pomodoro/GoTomato/internal/pomodoro" "git.smsvc.net/pomodoro/GoTomato/pkg/models" "github.com/gorilla/websocket" @@ -24,7 +25,7 @@ func handleClientCommands(ws *websocket.Conn) { _, message, err := ws.ReadMessage() if err != nil { log.Printf("Client disconnected: %v", err) - delete(Clients, ws) + delete(broadcast.Clients, ws) break } @@ -42,11 +43,11 @@ func handleClientCommands(ws *websocket.Conn) { if clientCommand.Config != unsetPomodoroConfig { pomodoroConfig = clientCommand.Config } - go pomodoro.RunPomodoro(Clients, pomodoroConfig) // Start the timer with the list of clients + go pomodoro.RunPomodoro(broadcast.Clients, pomodoroConfig) // Start the timer with the list of clients } case "stop": if pomodoro.IsPomodoroOngoing() { - pomodoro.ResetPomodoro(Clients) // Reset Pomodoro + pomodoro.ResetPomodoro(broadcast.Clients) // Reset Pomodoro } case "pause": if pomodoro.IsPomodoroOngoing() && !pomodoro.IsPomodoroPaused() { diff --git a/internal/websocket/handle_connections.go b/internal/websocket/handle_connections.go index be49918..cbf0f04 100644 --- a/internal/websocket/handle_connections.go +++ b/internal/websocket/handle_connections.go @@ -1,6 +1,7 @@ package websocket import ( + "git.smsvc.net/pomodoro/GoTomato/internal/broadcast" "git.smsvc.net/pomodoro/GoTomato/pkg/models" "github.com/gorilla/websocket" "log" @@ -8,8 +9,6 @@ import ( "sync" ) -// Clients is a map of connected WebSocket clients, where each client is represented by the Client struct -var Clients = make(map[*websocket.Conn]*models.Client) var mu sync.Mutex // Mutex to protect access to the Clients map // Upgrader to upgrade HTTP requests to WebSocket connections @@ -29,7 +28,7 @@ func HandleConnections(w http.ResponseWriter, r *http.Request) { // Register the new client mu.Lock() - Clients[ws] = &models.Client{ + broadcast.Clients[ws] = &models.Client{ Conn: ws, // Store the WebSocket connection } mu.Unlock() diff --git a/pkg/models/server.go b/pkg/models/server.go index b4de1c2..7ebe4c3 100644 --- a/pkg/models/server.go +++ b/pkg/models/server.go @@ -6,4 +6,6 @@ type ServerMessage struct { Session int `json:"session"` // Current session number TotalSession int `json:"total_sessions"` // Total number of sessions TimeLeft int `json:"time_left"` // Remaining time in seconds + Ongoing bool `json:"ongoing"` // Ongoing pomodoro + Paused bool `json:"paused"` // Is timer paused }