Compare commits
No commits in common. "bf2685a055aecb7626f009e0a8f4e5c37bd8897c" and "eba4065c6f32fe5671ae256c531c2a9949d5388b" have entirely different histories.
bf2685a055
...
eba4065c6f
10 changed files with 99 additions and 117 deletions
19
README.md
19
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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
27
internal/broadcast/broadcast.go
Normal file
27
internal/broadcast/broadcast.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue