Compare commits

...

9 commits

Author SHA1 Message Date
bf2685a055 fix: send correct server message on pomodoro end
- replace manual state reset with a dedicated ResetToDefault function
- remove locking mechanism during state updates

🤖
2024-10-21 13:42:32 +02:00
b7d03aa1d8 break: empty Message.Mode when no pomodoro ongoing
- change "none" to an empty string for the mode field
- update README to reflect the new mode representation
- ensure consistency across session end/reset and welcome messages

🤖
2024-10-21 13:14:26 +02:00
a9d145ee71 feat: create shared config defaults
- add shared configuration defaults for server and pomodoro

🤖
2024-10-21 13:07:19 +02:00
28342058aa doc: update comments 2024-10-21 13:07:19 +02:00
03ab627729 feat: simplify timer function
- remove "mode" parameter from startTimer function
- update shared.Message.Mode for each pomodoro state
- update comments

🤖
2024-10-21 13:07:19 +02:00
6ffd9f1e38 refactor: move broadcast package to websocket
- update import paths to reflect the new package name
- change function call to use the new websocket package
- adjust client iteration to use the renamed Clients variable

🤖
2024-10-21 13:07:19 +02:00
aab6896c7d refactor: move broadcast.Message to shared.Message
- create a new shared state file to manage message state
- replace broadcast.Message with shared.Message in pomodoro logic
- update websocket client handling to use shared.Clients
- remove unnecessary broadcast imports from various files
- ensure consistent message handling across the application

🤖
2024-10-21 13:07:19 +02:00
234f3c17dc feat: simplify pomodoro functions by removing client parameter
- remove clients parameter from RunPomodoro function
- update startTimer to no longer require clients parameter
- modify ResetPomodoro to eliminate clients parameter
- adjust client_commands to reflect changes in function signatures

🤖
2024-10-21 13:07:19 +02:00
9615d4d449 feat: implement permanent broadcast message functionality
- add SendPermanentBroadCastMessage to continuously send updates
- refactor BroadcastMessage to use a shared Message struct
- update pomodoro logic to modify broadcast.Message directly
- adjust client command handling to use broadcast.Clients map
- enhance ServerMessage struct with "Ongoing" and "Paused" fields

🤖
2024-10-21 13:07:19 +02:00
10 changed files with 117 additions and 99 deletions

View file

@ -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. 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 ### 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: 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 ### 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: 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). - 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). - 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). - 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 | | Message Type | Example |
| --------------------- | ------------------------------------------------ | --------------------------------------------------- | | --------------------- | --------------------------------------------------- |
| Welcome Message | Upon initial client connection | `{"mode": "none", "session": 0, "total_sessions": 0, "time_left": 0}` | | Welcome Message | `{"mode": "", "session":0, "total_sessions":0, "time_left":0, "ongoing": false, "paused": false}` |
| Session Update | Periodically during a session | `{"mode": "Work", "session": 1, "total_sessions": 4, "time_left": 900}` | | Session Running | `{"mode": "Work", "session": 1, "total_sessions": 4, "time_left": 900, "ongoing": true, "paused": false}` |
| Session End/Reset | After session completes or is reset | `{"mode": "none", "session": 0, "total_sessions": 0, "time_left": 0}` | | 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 ## Testing

View file

@ -3,6 +3,7 @@ package server
import ( import (
"flag" "flag"
"fmt" "fmt"
"git.smsvc.net/pomodoro/GoTomato/internal/shared"
"git.smsvc.net/pomodoro/GoTomato/internal/websocket" "git.smsvc.net/pomodoro/GoTomato/internal/websocket"
"git.smsvc.net/pomodoro/GoTomato/pkg/models" "git.smsvc.net/pomodoro/GoTomato/pkg/models"
"log" "log"
@ -11,8 +12,8 @@ import (
func Start() { func Start() {
// Define CLI flags for ListenAddress and ListenPort // Define CLI flags for ListenAddress and ListenPort
listenAddress := flag.String("listenAddress", "0.0.0.0", "IP address to listen on") listenAddress := flag.String("listenAddress", shared.DefaultServerConfig.ListenAddress, "IP address to listen on")
listenPort := flag.Int("listenPort", 8080, "Port to listen on") listenPort := flag.Int("listenPort", shared.DefaultServerConfig.ListenPort, "Port to listen on")
flag.Parse() flag.Parse()
serverConfig := models.GoTomatoServerConfig{ serverConfig := models.GoTomatoServerConfig{
@ -23,6 +24,7 @@ func Start() {
listen := fmt.Sprintf("%s:%d", serverConfig.ListenAddress, serverConfig.ListenPort) listen := fmt.Sprintf("%s:%d", serverConfig.ListenAddress, serverConfig.ListenPort)
http.HandleFunc("/ws", websocket.HandleConnections) http.HandleFunc("/ws", websocket.HandleConnections)
go websocket.SendPermanentBroadCastMessage()
log.Printf("Pomodoro WebSocket server started on %s\n", listen) log.Printf("Pomodoro WebSocket server started on %s\n", listen)
err := http.ListenAndServe(listen, nil) err := http.ListenAndServe(listen, nil)

View file

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

View file

@ -1,15 +1,11 @@
package pomodoro package pomodoro
import ( import (
"git.smsvc.net/pomodoro/GoTomato/internal/broadcast" "git.smsvc.net/pomodoro/GoTomato/internal/shared"
"git.smsvc.net/pomodoro/GoTomato/pkg/models" "git.smsvc.net/pomodoro/GoTomato/pkg/models"
"github.com/gorilla/websocket"
"sync" "sync"
) )
var pomodoroOngoing bool
var pomodoroPaused bool
var pomodoroResetChannel = make(chan bool, 1) var pomodoroResetChannel = make(chan bool, 1)
var pomodoroPauseChannel = make(chan bool, 1) var pomodoroPauseChannel = make(chan bool, 1)
var pomodoroResumeChannel = 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 var mu sync.Mutex // to synchronize access to shared state
// RunPomodoro iterates the Pomodoro work/break sessions. // 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() mu.Lock()
pomodoroOngoing = true shared.Message.Ongoing = true
pomodoroPaused = false shared.Message.Paused = false
mu.Unlock() mu.Unlock()
shared.Message.TotalSession = config.Sessions
for session := 1; session <= config.Sessions; session++ { 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 break
} }
if session == config.Sessions { if session == config.Sessions {
if !startTimer(clients, config.LongBreak, "LongBreak", session, config.Sessions) { shared.Message.Mode = "LongBreak"
if !startTimer(config.LongBreak) {
break break
} }
} else { } else {
if !startTimer(clients, config.ShortBreak, "ShortBreak", session, config.Sessions) { shared.Message.Mode = "ShortBreak"
if !startTimer(config.ShortBreak) {
break break
} }
} }
} }
mu.Lock() shared.Message = shared.ResetToDefault()
pomodoroOngoing = false
mu.Unlock()
} }
// ResetPomodoro resets the running Pomodoro timer. // ResetPomodoro resets the running Pomodoro timer.
func ResetPomodoro(clients map[*websocket.Conn]*models.Client) { func ResetPomodoro() {
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 // Send a reset signal to stop any running timers
pomodoroResetChannel <- true pomodoroResetChannel <- true
shared.Message = shared.ResetToDefault()
} }
func PausePomodoro() { func PausePomodoro() {
mu.Lock() mu.Lock()
pomodoroPaused = true shared.Message.Paused = true
mu.Unlock() mu.Unlock()
pomodoroPauseChannel <- true pomodoroPauseChannel <- true
@ -72,7 +60,7 @@ func PausePomodoro() {
func ResumePomodoro() { func ResumePomodoro() {
mu.Lock() mu.Lock()
pomodoroPaused = false shared.Message.Paused = false
mu.Unlock() mu.Unlock()
pomodoroResumeChannel <- true pomodoroResumeChannel <- true
} }
@ -80,11 +68,11 @@ func ResumePomodoro() {
func IsPomodoroOngoing() bool { func IsPomodoroOngoing() bool {
mu.Lock() mu.Lock()
defer mu.Unlock() // Ensures that the mutex is unlocked after the function is done defer mu.Unlock() // Ensures that the mutex is unlocked after the function is done
return pomodoroOngoing return shared.Message.Ongoing
} }
func IsPomodoroPaused() bool { func IsPomodoroPaused() bool {
mu.Lock() mu.Lock()
defer mu.Unlock() // Ensures that the mutex is unlocked after the function is done defer mu.Unlock() // Ensures that the mutex is unlocked after the function is done
return pomodoroPaused return shared.Message.Paused
} }

View file

@ -1,14 +1,12 @@
package pomodoro package pomodoro
import ( 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"
"time" "time"
) )
// startTimer runs the countdown and broadcasts every second. // runs the countdown and updates shared.Message.TimeLeft every second.
func startTimer(clients map[*websocket.Conn]*models.Client, remainingSeconds int, mode string, session int, totalSessions int) bool { func startTimer(remainingSeconds int) bool {
for remainingSeconds > 0 { for remainingSeconds > 0 {
select { select {
case <-pomodoroResetChannel: case <-pomodoroResetChannel:
@ -20,25 +18,15 @@ func startTimer(clients map[*websocket.Conn]*models.Client, remainingSeconds int
default: default:
// Broadcast the current state to all clients // Broadcast the current state to all clients
if !IsPomodoroPaused() { if !IsPomodoroPaused() {
broadcast.BroadcastMessage(clients, models.ServerMessage{ shared.Message.TimeLeft = remainingSeconds
Mode: mode,
Session: session,
TotalSession: totalSessions,
TimeLeft: remainingSeconds,
})
time.Sleep(time.Second) time.Sleep(time.Second)
remainingSeconds-- remainingSeconds--
} }
} }
} }
// Final broadcast when time reaches zero // Final shared.Message when time reaches zero
broadcast.BroadcastMessage(clients, models.ServerMessage{ shared.Message.TimeLeft = remainingSeconds
Mode: mode,
Session: session,
TotalSession: totalSessions,
TimeLeft: 0,
})
return true return true
} }

View file

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

18
internal/shared/state.go Normal file
View file

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

View file

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

View file

@ -3,23 +3,17 @@ package websocket
import ( import (
"encoding/json" "encoding/json"
"git.smsvc.net/pomodoro/GoTomato/internal/pomodoro" "git.smsvc.net/pomodoro/GoTomato/internal/pomodoro"
"git.smsvc.net/pomodoro/GoTomato/internal/shared"
"git.smsvc.net/pomodoro/GoTomato/pkg/models" "git.smsvc.net/pomodoro/GoTomato/pkg/models"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"log" "log"
) )
var unsetPomodoroConfig models.GoTomatoPomodoroConfig // used to check if client passed a config json // handleClientCommands listens for commands from WebSocket clients
// handleClientCommands listens for commands from WebSocket clients and dispatches to the timer.
func handleClientCommands(ws *websocket.Conn) { func handleClientCommands(ws *websocket.Conn) {
for { for {
var clientCommand models.ClientCommand var clientCommand models.ClientCommand
var pomodoroConfig = models.GoTomatoPomodoroConfig{ var pomodoroConfig = shared.DefaultPomodoroConfig
Work: 25 * 60,
ShortBreak: 5 * 60,
LongBreak: 15 * 60,
Sessions: 4,
}
_, message, err := ws.ReadMessage() _, message, err := ws.ReadMessage()
if err != nil { if err != nil {
@ -39,14 +33,14 @@ func handleClientCommands(ws *websocket.Conn) {
switch clientCommand.Command { switch clientCommand.Command {
case "start": case "start":
if !pomodoro.IsPomodoroOngoing() { if !pomodoro.IsPomodoroOngoing() {
if clientCommand.Config != unsetPomodoroConfig { if clientCommand.Config != shared.UnsetPomodoroConfig {
pomodoroConfig = clientCommand.Config 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": case "stop":
if pomodoro.IsPomodoroOngoing() { if pomodoro.IsPomodoroOngoing() {
pomodoro.ResetPomodoro(Clients) // Reset Pomodoro pomodoro.ResetPomodoro() // Reset Pomodoro
} }
case "pause": case "pause":
if pomodoro.IsPomodoroOngoing() && !pomodoro.IsPomodoroPaused() { if pomodoro.IsPomodoroOngoing() && !pomodoro.IsPomodoroPaused() {

View file

@ -6,4 +6,6 @@ type ServerMessage struct {
Session int `json:"session"` // Current session number Session int `json:"session"` // Current session number
TotalSession int `json:"total_sessions"` // Total number of sessions TotalSession int `json:"total_sessions"` // Total number of sessions
TimeLeft int `json:"time_left"` // Remaining time in seconds TimeLeft int `json:"time_left"` // Remaining time in seconds
Ongoing bool `json:"ongoing"` // Ongoing pomodoro
Paused bool `json:"paused"` // Is timer paused
} }