Compare commits
9 commits
eba4065c6f
...
bf2685a055
Author | SHA1 | Date | |
---|---|---|---|
bf2685a055 | |||
b7d03aa1d8 | |||
a9d145ee71 | |||
28342058aa | |||
03ab627729 | |||
6ffd9f1e38 | |||
aab6896c7d | |||
234f3c17dc | |||
9615d4d449 |
10 changed files with 117 additions and 99 deletions
19
README.md
19
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 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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
20
internal/shared/configDefaults.go
Normal file
20
internal/shared/configDefaults.go
Normal 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
18
internal/shared/state.go
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
30
internal/websocket/broadcast.go
Normal file
30
internal/websocket/broadcast.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() {
|
||||||
|
|
|
@ -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