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
🤖
This commit is contained in:
parent
eba4065c6f
commit
9615d4d449
8 changed files with 75 additions and 65 deletions
17
README.md
17
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.
|
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 "none" 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": "none", "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": "none", "session": 0, "total_sessions": 0, "time_left": 0, "ongoing": false, "paused": false}` |
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ package server
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.smsvc.net/pomodoro/GoTomato/internal/broadcast"
|
||||||
"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"
|
||||||
|
@ -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 broadcast.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)
|
||||||
|
|
|
@ -5,23 +5,37 @@ import (
|
||||||
"git.smsvc.net/pomodoro/GoTomato/pkg/models"
|
"git.smsvc.net/pomodoro/GoTomato/pkg/models"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"log"
|
"log"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
}
|
||||||
|
|
||||||
// BroadcastMessage sends a message to all connected WebSocket clients.
|
// BroadcastMessage sends a message to all connected WebSocket clients.
|
||||||
func BroadcastMessage(clients map[*websocket.Conn]*models.Client, message models.ServerMessage) {
|
func SendPermanentBroadCastMessage() {
|
||||||
|
for {
|
||||||
// Marshal the message into JSON format
|
// Marshal the message into JSON format
|
||||||
jsonMessage, err := json.Marshal(message)
|
jsonMessage, err := json.Marshal(Message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error marshalling message: %v", err)
|
log.Printf("Error marshalling message: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iterate over all connected clients and broadcast the message
|
// Iterate over all connected clients and broadcast the message
|
||||||
for _, client := range clients {
|
for _, client := range Clients {
|
||||||
err := client.SendMessage(websocket.TextMessage, jsonMessage)
|
err := client.SendMessage(websocket.TextMessage, jsonMessage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error broadcasting to client: %v", err)
|
log.Printf("Error broadcasting to client: %v", err)
|
||||||
// The client is responsible for closing itself on error
|
// The client is responsible for closing itself on error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,9 +7,6 @@ import (
|
||||||
"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)
|
||||||
|
@ -19,52 +16,53 @@ 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(clients map[*websocket.Conn]*models.Client, config models.GoTomatoPomodoroConfig) {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
pomodoroOngoing = true
|
broadcast.Message.Ongoing = true
|
||||||
pomodoroPaused = false
|
broadcast.Message.Paused = false
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
|
|
||||||
|
broadcast.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) {
|
broadcast.Message.Session = session
|
||||||
|
if !startTimer(clients, config.Work, "Work") {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if session == config.Sessions {
|
if session == config.Sessions {
|
||||||
if !startTimer(clients, config.LongBreak, "LongBreak", session, config.Sessions) {
|
if !startTimer(clients, config.LongBreak, "LongBreak") {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if !startTimer(clients, config.ShortBreak, "ShortBreak", session, config.Sessions) {
|
if !startTimer(clients, config.ShortBreak, "ShortBreak") {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
pomodoroOngoing = false
|
broadcast.Message.Ongoing = false
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResetPomodoro resets the running Pomodoro timer.
|
// ResetPomodoro resets the running Pomodoro timer.
|
||||||
func ResetPomodoro(clients map[*websocket.Conn]*models.Client) {
|
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
|
// Send a reset signal to stop any running timers
|
||||||
pomodoroResetChannel <- true
|
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() {
|
func PausePomodoro() {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
pomodoroPaused = true
|
broadcast.Message.Paused = true
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
|
|
||||||
pomodoroPauseChannel <- true
|
pomodoroPauseChannel <- true
|
||||||
|
@ -72,7 +70,7 @@ func PausePomodoro() {
|
||||||
|
|
||||||
func ResumePomodoro() {
|
func ResumePomodoro() {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
pomodoroPaused = false
|
broadcast.Message.Paused = false
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
pomodoroResumeChannel <- true
|
pomodoroResumeChannel <- true
|
||||||
}
|
}
|
||||||
|
@ -80,11 +78,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 broadcast.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 broadcast.Message.Paused
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// startTimer runs the countdown and broadcasts every second.
|
// 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 {
|
for remainingSeconds > 0 {
|
||||||
select {
|
select {
|
||||||
case <-pomodoroResetChannel:
|
case <-pomodoroResetChannel:
|
||||||
|
@ -20,12 +20,8 @@ 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{
|
broadcast.Message.Mode = mode
|
||||||
Mode: mode,
|
broadcast.Message.TimeLeft = remainingSeconds
|
||||||
Session: session,
|
|
||||||
TotalSession: totalSessions,
|
|
||||||
TimeLeft: remainingSeconds,
|
|
||||||
})
|
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
remainingSeconds--
|
remainingSeconds--
|
||||||
}
|
}
|
||||||
|
@ -33,12 +29,7 @@ func startTimer(clients map[*websocket.Conn]*models.Client, remainingSeconds int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Final broadcast when time reaches zero
|
// Final broadcast when time reaches zero
|
||||||
broadcast.BroadcastMessage(clients, models.ServerMessage{
|
broadcast.Message.TimeLeft = remainingSeconds
|
||||||
Mode: mode,
|
|
||||||
Session: session,
|
|
||||||
TotalSession: totalSessions,
|
|
||||||
TimeLeft: 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package websocket
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"git.smsvc.net/pomodoro/GoTomato/internal/broadcast"
|
||||||
"git.smsvc.net/pomodoro/GoTomato/internal/pomodoro"
|
"git.smsvc.net/pomodoro/GoTomato/internal/pomodoro"
|
||||||
"git.smsvc.net/pomodoro/GoTomato/pkg/models"
|
"git.smsvc.net/pomodoro/GoTomato/pkg/models"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
|
@ -24,7 +25,7 @@ func handleClientCommands(ws *websocket.Conn) {
|
||||||
_, message, err := ws.ReadMessage()
|
_, message, err := ws.ReadMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Client disconnected: %v", err)
|
log.Printf("Client disconnected: %v", err)
|
||||||
delete(Clients, ws)
|
delete(broadcast.Clients, ws)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,11 +43,11 @@ func handleClientCommands(ws *websocket.Conn) {
|
||||||
if clientCommand.Config != unsetPomodoroConfig {
|
if clientCommand.Config != unsetPomodoroConfig {
|
||||||
pomodoroConfig = clientCommand.Config
|
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":
|
case "stop":
|
||||||
if pomodoro.IsPomodoroOngoing() {
|
if pomodoro.IsPomodoroOngoing() {
|
||||||
pomodoro.ResetPomodoro(Clients) // Reset Pomodoro
|
pomodoro.ResetPomodoro(broadcast.Clients) // Reset Pomodoro
|
||||||
}
|
}
|
||||||
case "pause":
|
case "pause":
|
||||||
if pomodoro.IsPomodoroOngoing() && !pomodoro.IsPomodoroPaused() {
|
if pomodoro.IsPomodoroOngoing() && !pomodoro.IsPomodoroPaused() {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package websocket
|
package websocket
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"git.smsvc.net/pomodoro/GoTomato/internal/broadcast"
|
||||||
"git.smsvc.net/pomodoro/GoTomato/pkg/models"
|
"git.smsvc.net/pomodoro/GoTomato/pkg/models"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"log"
|
"log"
|
||||||
|
@ -8,8 +9,6 @@ import (
|
||||||
"sync"
|
"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
|
var mu sync.Mutex // Mutex to protect access to the Clients map
|
||||||
|
|
||||||
// Upgrader to upgrade HTTP requests to WebSocket connections
|
// Upgrader to upgrade HTTP requests to WebSocket connections
|
||||||
|
@ -29,7 +28,7 @@ func HandleConnections(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// Register the new client
|
// Register the new client
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
Clients[ws] = &models.Client{
|
broadcast.Clients[ws] = &models.Client{
|
||||||
Conn: ws, // Store the WebSocket connection
|
Conn: ws, // Store the WebSocket connection
|
||||||
}
|
}
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue