Compare commits

..

No commits in common. "bf2685a055aecb7626f009e0a8f4e5c37bd8897c" and "eba4065c6f32fe5671ae256c531c2a9949d5388b" have entirely different histories.

10 changed files with 99 additions and 117 deletions

View file

@ -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

View file

@ -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)

View file

@ -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
}
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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

View file

@ -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,
}
}

View file

@ -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)
}
}

View file

@ -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() {

View file

@ -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
}