diff --git a/README.md b/README.md index 955b001..5f80e98 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ 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: @@ -50,23 +51,19 @@ Example: ### Server Messages -The server periodically (every second) sends JSON-encoded messages to all connected clients to update them on the current state of the Pomodoro session. +The server 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 empty if no session is running). +- 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 | 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}` | +| 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}` | ## Testing diff --git a/cmd/server/main.go b/cmd/server/main.go index 9ce6580..2105b39 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -3,7 +3,6 @@ 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" @@ -12,8 +11,8 @@ import ( func Start() { // Define CLI flags for ListenAddress and ListenPort - listenAddress := flag.String("listenAddress", shared.DefaultServerConfig.ListenAddress, "IP address to listen on") - listenPort := flag.Int("listenPort", shared.DefaultServerConfig.ListenPort, "Port to listen on") + listenAddress := flag.String("listenAddress", "0.0.0.0", "IP address to listen on") + listenPort := flag.Int("listenPort", 8080, "Port to listen on") flag.Parse() serverConfig := models.GoTomatoServerConfig{ @@ -24,7 +23,6 @@ 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 new file mode 100644 index 0000000..1be04ce --- /dev/null +++ b/internal/broadcast/broadcast.go @@ -0,0 +1,27 @@ +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 702d0f7..3124b6c 100644 --- a/internal/pomodoro/pomodoro.go +++ b/internal/pomodoro/pomodoro.go @@ -1,11 +1,15 @@ package pomodoro import ( - "git.smsvc.net/pomodoro/GoTomato/internal/shared" + "git.smsvc.net/pomodoro/GoTomato/internal/broadcast" "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) @@ -13,46 +17,54 @@ 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(config models.GoTomatoPomodoroConfig) { +func RunPomodoro(clients map[*websocket.Conn]*models.Client, config models.GoTomatoPomodoroConfig) { mu.Lock() - shared.Message.Ongoing = true - shared.Message.Paused = false + pomodoroOngoing = true + pomodoroPaused = false mu.Unlock() - shared.Message.TotalSession = config.Sessions - for session := 1; session <= config.Sessions; session++ { - shared.Message.Session = session - shared.Message.Mode = "Work" - if !startTimer(config.Work) { + if !startTimer(clients, config.Work, "Work", session, config.Sessions) { break } if session == config.Sessions { - shared.Message.Mode = "LongBreak" - if !startTimer(config.LongBreak) { + if !startTimer(clients, config.LongBreak, "LongBreak", session, config.Sessions) { break } } else { - shared.Message.Mode = "ShortBreak" - if !startTimer(config.ShortBreak) { + if !startTimer(clients, config.ShortBreak, "ShortBreak", session, config.Sessions) { break } } } - shared.Message = shared.ResetToDefault() + mu.Lock() + pomodoroOngoing = false + mu.Unlock() } // ResetPomodoro resets the running Pomodoro timer. -func ResetPomodoro() { +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 - shared.Message = shared.ResetToDefault() } func PausePomodoro() { mu.Lock() - shared.Message.Paused = true + pomodoroPaused = true mu.Unlock() pomodoroPauseChannel <- true @@ -60,7 +72,7 @@ func PausePomodoro() { func ResumePomodoro() { mu.Lock() - shared.Message.Paused = false + pomodoroPaused = false mu.Unlock() pomodoroResumeChannel <- true } @@ -68,11 +80,11 @@ func ResumePomodoro() { func IsPomodoroOngoing() bool { mu.Lock() defer mu.Unlock() // Ensures that the mutex is unlocked after the function is done - return shared.Message.Ongoing + return pomodoroOngoing } func IsPomodoroPaused() bool { mu.Lock() defer mu.Unlock() // Ensures that the mutex is unlocked after the function is done - return shared.Message.Paused + return pomodoroPaused } diff --git a/internal/pomodoro/timer.go b/internal/pomodoro/timer.go index 4b4c86e..f23d286 100644 --- a/internal/pomodoro/timer.go +++ b/internal/pomodoro/timer.go @@ -1,12 +1,14 @@ package pomodoro import ( - "git.smsvc.net/pomodoro/GoTomato/internal/shared" + "git.smsvc.net/pomodoro/GoTomato/internal/broadcast" + "git.smsvc.net/pomodoro/GoTomato/pkg/models" + "github.com/gorilla/websocket" "time" ) -// runs the countdown and updates shared.Message.TimeLeft every second. -func startTimer(remainingSeconds int) bool { +// 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 { for remainingSeconds > 0 { select { case <-pomodoroResetChannel: @@ -18,15 +20,25 @@ func startTimer(remainingSeconds int) bool { default: // Broadcast the current state to all clients if !IsPomodoroPaused() { - shared.Message.TimeLeft = remainingSeconds + broadcast.BroadcastMessage(clients, models.ServerMessage{ + Mode: mode, + Session: session, + TotalSession: totalSessions, + TimeLeft: remainingSeconds, + }) time.Sleep(time.Second) remainingSeconds-- } } } - // Final shared.Message when time reaches zero - shared.Message.TimeLeft = remainingSeconds + // Final broadcast when time reaches zero + broadcast.BroadcastMessage(clients, models.ServerMessage{ + Mode: mode, + Session: session, + TotalSession: totalSessions, + TimeLeft: 0, + }) return true } diff --git a/internal/shared/configDefaults.go b/internal/shared/configDefaults.go deleted file mode 100644 index ba4be6c..0000000 --- a/internal/shared/configDefaults.go +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index 87e8759..0000000 --- a/internal/shared/state.go +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 02b7f0a..0000000 --- a/internal/websocket/broadcast.go +++ /dev/null @@ -1,30 +0,0 @@ -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 e1ff8b0..1005006 100644 --- a/internal/websocket/client_commands.go +++ b/internal/websocket/client_commands.go @@ -3,17 +3,23 @@ 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" ) -// handleClientCommands listens for commands from WebSocket clients +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. func handleClientCommands(ws *websocket.Conn) { for { var clientCommand models.ClientCommand - var pomodoroConfig = shared.DefaultPomodoroConfig + var pomodoroConfig = models.GoTomatoPomodoroConfig{ + Work: 25 * 60, + ShortBreak: 5 * 60, + LongBreak: 15 * 60, + Sessions: 4, + } _, message, err := ws.ReadMessage() if err != nil { @@ -33,14 +39,14 @@ func handleClientCommands(ws *websocket.Conn) { switch clientCommand.Command { case "start": if !pomodoro.IsPomodoroOngoing() { - if clientCommand.Config != shared.UnsetPomodoroConfig { + if clientCommand.Config != unsetPomodoroConfig { pomodoroConfig = clientCommand.Config } - go pomodoro.RunPomodoro(pomodoroConfig) // Start the timer with the list of clients + go pomodoro.RunPomodoro(Clients, pomodoroConfig) // Start the timer with the list of clients } case "stop": if pomodoro.IsPomodoroOngoing() { - pomodoro.ResetPomodoro() // Reset Pomodoro + pomodoro.ResetPomodoro(Clients) // Reset Pomodoro } case "pause": if pomodoro.IsPomodoroOngoing() && !pomodoro.IsPomodoroPaused() { diff --git a/pkg/models/server.go b/pkg/models/server.go index 7ebe4c3..b4de1c2 100644 --- a/pkg/models/server.go +++ b/pkg/models/server.go @@ -6,6 +6,4 @@ 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 }