diff --git a/README.md b/README.md index 5f80e98..955b001 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). +- mode: Indicates the current phase of the Pomodoro session ("Work", "ShortBreak", "LongBreak", or empty 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": "", "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": "", "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..9ce6580 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/shared" "git.smsvc.net/pomodoro/GoTomato/internal/websocket" "git.smsvc.net/pomodoro/GoTomato/pkg/models" "log" @@ -11,8 +12,8 @@ import ( func Start() { // Define CLI flags for ListenAddress and ListenPort - listenAddress := flag.String("listenAddress", "0.0.0.0", "IP address to listen on") - listenPort := flag.Int("listenPort", 8080, "Port to listen on") + listenAddress := flag.String("listenAddress", shared.DefaultServerConfig.ListenAddress, "IP address to listen on") + listenPort := flag.Int("listenPort", shared.DefaultServerConfig.ListenPort, "Port to listen on") flag.Parse() serverConfig := models.GoTomatoServerConfig{ @@ -23,6 +24,7 @@ func Start() { listen := fmt.Sprintf("%s:%d", serverConfig.ListenAddress, serverConfig.ListenPort) http.HandleFunc("/ws", websocket.HandleConnections) + go websocket.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 deleted file mode 100644 index 1be04ce..0000000 --- a/internal/broadcast/broadcast.go +++ /dev/null @@ -1,27 +0,0 @@ -package broadcast - -import ( - "encoding/json" - "git.smsvc.net/pomodoro/GoTomato/pkg/models" - "github.com/gorilla/websocket" - "log" -) - -// 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 - } - - // 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 - } - } -} diff --git a/internal/pomodoro/pomodoro.go b/internal/pomodoro/pomodoro.go index 3124b6c..702d0f7 100644 --- a/internal/pomodoro/pomodoro.go +++ b/internal/pomodoro/pomodoro.go @@ -1,15 +1,11 @@ package pomodoro import ( - "git.smsvc.net/pomodoro/GoTomato/internal/broadcast" + "git.smsvc.net/pomodoro/GoTomato/internal/shared" "git.smsvc.net/pomodoro/GoTomato/pkg/models" - "github.com/gorilla/websocket" "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) @@ -17,54 +13,46 @@ var pomodoroResumeChannel = make(chan bool, 1) 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) { +func RunPomodoro(config models.GoTomatoPomodoroConfig) { mu.Lock() - pomodoroOngoing = true - pomodoroPaused = false + shared.Message.Ongoing = true + shared.Message.Paused = false mu.Unlock() + shared.Message.TotalSession = config.Sessions + for session := 1; session <= config.Sessions; session++ { - if !startTimer(clients, config.Work, "Work", session, config.Sessions) { + shared.Message.Session = session + shared.Message.Mode = "Work" + if !startTimer(config.Work) { break } if session == config.Sessions { - if !startTimer(clients, config.LongBreak, "LongBreak", session, config.Sessions) { + shared.Message.Mode = "LongBreak" + if !startTimer(config.LongBreak) { break } } else { - if !startTimer(clients, config.ShortBreak, "ShortBreak", session, config.Sessions) { + shared.Message.Mode = "ShortBreak" + if !startTimer(config.ShortBreak) { break } } } - mu.Lock() - pomodoroOngoing = false - mu.Unlock() + shared.Message = shared.ResetToDefault() } // 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, - }) - +func ResetPomodoro() { // Send a reset signal to stop any running timers pomodoroResetChannel <- true + shared.Message = shared.ResetToDefault() } func PausePomodoro() { mu.Lock() - pomodoroPaused = true + shared.Message.Paused = true mu.Unlock() pomodoroPauseChannel <- true @@ -72,7 +60,7 @@ func PausePomodoro() { func ResumePomodoro() { mu.Lock() - pomodoroPaused = false + shared.Message.Paused = false mu.Unlock() pomodoroResumeChannel <- true } @@ -80,11 +68,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 shared.Message.Ongoing } func IsPomodoroPaused() bool { mu.Lock() defer mu.Unlock() // Ensures that the mutex is unlocked after the function is done - return pomodoroPaused + return shared.Message.Paused } diff --git a/internal/pomodoro/timer.go b/internal/pomodoro/timer.go index f23d286..4b4c86e 100644 --- a/internal/pomodoro/timer.go +++ b/internal/pomodoro/timer.go @@ -1,14 +1,12 @@ package pomodoro import ( - "git.smsvc.net/pomodoro/GoTomato/internal/broadcast" - "git.smsvc.net/pomodoro/GoTomato/pkg/models" - "github.com/gorilla/websocket" + "git.smsvc.net/pomodoro/GoTomato/internal/shared" "time" ) -// 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 { +// runs the countdown and updates shared.Message.TimeLeft every second. +func startTimer(remainingSeconds int) bool { for remainingSeconds > 0 { select { case <-pomodoroResetChannel: @@ -20,25 +18,15 @@ 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, - }) + shared.Message.TimeLeft = remainingSeconds time.Sleep(time.Second) remainingSeconds-- } } } - // Final broadcast when time reaches zero - broadcast.BroadcastMessage(clients, models.ServerMessage{ - Mode: mode, - Session: session, - TotalSession: totalSessions, - TimeLeft: 0, - }) + // Final shared.Message when time reaches zero + shared.Message.TimeLeft = remainingSeconds return true } diff --git a/internal/shared/configDefaults.go b/internal/shared/configDefaults.go new file mode 100644 index 0000000..ba4be6c --- /dev/null +++ b/internal/shared/configDefaults.go @@ -0,0 +1,20 @@ +package shared + +import ( + "git.smsvc.net/pomodoro/GoTomato/pkg/models" +) + +var DefaultServerConfig = models.GoTomatoServerConfig{ + ListenAddress: "0.0.0.0", + ListenPort: 8080, +} + +var DefaultPomodoroConfig = models.GoTomatoPomodoroConfig{ + Work: 25 * 60, + ShortBreak: 5 * 60, + LongBreak: 15 * 60, + Sessions: 4, +} + +// used to check if client passed a config json +var UnsetPomodoroConfig models.GoTomatoPomodoroConfig diff --git a/internal/shared/state.go b/internal/shared/state.go new file mode 100644 index 0000000..87e8759 --- /dev/null +++ b/internal/shared/state.go @@ -0,0 +1,18 @@ +package shared + +import ( + "git.smsvc.net/pomodoro/GoTomato/pkg/models" +) + +var Message = ResetToDefault() + +func ResetToDefault() models.ServerMessage { + return models.ServerMessage{ + Mode: "", + Session: 0, + TotalSession: 0, + TimeLeft: 0, + Ongoing: false, + Paused: false, + } +} diff --git a/internal/websocket/broadcast.go b/internal/websocket/broadcast.go new file mode 100644 index 0000000..02b7f0a --- /dev/null +++ b/internal/websocket/broadcast.go @@ -0,0 +1,30 @@ +package websocket + +import ( + "encoding/json" + "git.smsvc.net/pomodoro/GoTomato/internal/shared" + "github.com/gorilla/websocket" + "log" + "time" +) + +// sends continous messages to all connected WebSocket clients. +func SendPermanentBroadCastMessage() { + for { + // Marshal the message into JSON format + jsonMessage, err := json.Marshal(shared.Message) + if err != nil { + 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/websocket/client_commands.go b/internal/websocket/client_commands.go index 1005006..e1ff8b0 100644 --- a/internal/websocket/client_commands.go +++ b/internal/websocket/client_commands.go @@ -3,23 +3,17 @@ package websocket import ( "encoding/json" "git.smsvc.net/pomodoro/GoTomato/internal/pomodoro" + "git.smsvc.net/pomodoro/GoTomato/internal/shared" "git.smsvc.net/pomodoro/GoTomato/pkg/models" "github.com/gorilla/websocket" "log" ) -var unsetPomodoroConfig models.GoTomatoPomodoroConfig // used to check if client passed a config json - -// handleClientCommands listens for commands from WebSocket clients and dispatches to the timer. +// handleClientCommands listens for commands from WebSocket clients func handleClientCommands(ws *websocket.Conn) { for { var clientCommand models.ClientCommand - var pomodoroConfig = models.GoTomatoPomodoroConfig{ - Work: 25 * 60, - ShortBreak: 5 * 60, - LongBreak: 15 * 60, - Sessions: 4, - } + var pomodoroConfig = shared.DefaultPomodoroConfig _, message, err := ws.ReadMessage() if err != nil { @@ -39,14 +33,14 @@ func handleClientCommands(ws *websocket.Conn) { switch clientCommand.Command { case "start": if !pomodoro.IsPomodoroOngoing() { - if clientCommand.Config != unsetPomodoroConfig { + if clientCommand.Config != shared.UnsetPomodoroConfig { pomodoroConfig = clientCommand.Config } - go pomodoro.RunPomodoro(Clients, pomodoroConfig) // Start the timer with the list of clients + go pomodoro.RunPomodoro(pomodoroConfig) // Start the timer with the list of clients } case "stop": if pomodoro.IsPomodoroOngoing() { - pomodoro.ResetPomodoro(Clients) // Reset Pomodoro + pomodoro.ResetPomodoro() // Reset Pomodoro } case "pause": if pomodoro.IsPomodoroOngoing() && !pomodoro.IsPomodoroPaused() { 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 }