You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

236 lines
6.0 KiB

package main
import (
"bufio"
"context"
"flag"
"fmt"
"os"
"os/signal"
"strings"
"sync"
"syscall"
"time"
"log/slog"
"track-gopher/derby"
"track-gopher/web"
)
func main() {
// Create logger
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
// Use the logger
logger.Info("Starting derby race application")
// Parse command line flags
portName := flag.String("port", "/dev/ttyACM0", "Serial port for the derby clock")
baudRate := flag.Int("baud", 19200, "Baud rate for the serial port")
webPort := flag.Int("web-port", 8080, "Port for the web server")
dbPath := flag.String("db", "./data/derby.db", "Path to SQLite database file")
noWeb := flag.Bool("no-web", false, "Disable web interface")
noTerminal := flag.Bool("no-terminal", false, "Disable terminal interface")
flag.Parse()
// Create a new derby clock connection
clock, err := derby.NewDerbyClock(*portName, *baudRate)
if err != nil {
fmt.Printf("Error opening derby clock: %v\n", err)
os.Exit(1)
}
defer clock.Close()
// Set up signal handling for clean shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
var wg sync.WaitGroup
// Create multiple event channels for fan-out
terminalEvents := make(chan derby.Event, 10)
webEvents := make(chan derby.Event, 10)
// Start the event broadcaster with fan-out to multiple channels
go func() {
for event := range clock.Events() {
// Clone the event to both channels
terminalEvents <- event
webEvents <- event
}
close(terminalEvents)
close(webEvents)
}()
// Create a context for graceful shutdown
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Start web interface if enabled
if !*noWeb {
wg.Add(1)
go func() {
defer wg.Done()
startWebInterface(clock, webEvents, *webPort, *dbPath, ctx)
}()
}
// Start terminal interface if enabled
if !*noTerminal {
wg.Add(1)
go func() {
defer wg.Done()
startTerminalInterface(clock, terminalEvents, ctx)
}()
}
// Wait for signal to exit
<-sigChan
fmt.Println("Shutting down...")
// Cancel context to signal all components to shut down
cancel()
// Give a moment for any pending operations to complete
time.Sleep(500 * time.Millisecond)
// Wait for all interfaces to shut down
wg.Wait()
}
// startWebInterface initializes and runs the web interface
func startWebInterface(clock *derby.DerbyClock, events <-chan derby.Event, webPort int, dbPath string, ctx context.Context) {
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
// Create and start the web server
server, err := web.NewServer(clock, events, dbPath, webPort, logger)
if err != nil {
logger.Error("Error creating web server", "error", err)
return
}
fmt.Printf("Web interface available at http://localhost:%d\n", webPort)
// Start the web server
if err := server.Start(); err != nil {
logger.Error("Web server error", "error", err)
return
}
// Wait for context cancellation
<-ctx.Done()
// Gracefully shut down the server
if err := server.Shutdown(ctx); err != nil {
logger.Error("Error shutting down web server", "error", err)
}
}
// startTerminalInterface initializes and runs the terminal interface
func startTerminalInterface(clock *derby.DerbyClock, events <-chan derby.Event, ctx context.Context) {
fmt.Println("Terminal interface started")
// Reset the clock to start fresh
if err := clock.Reset(); err != nil {
fmt.Printf("Error resetting clock: %v\n", err)
return
}
fmt.Println("Clock reset. Ready to start race.")
// Print instructions
fmt.Println("\nCommands:")
fmt.Println(" r - Reset the clock")
fmt.Println(" f - Force end the race")
fmt.Println(" q - Quit the program")
fmt.Println(" ? - Show this help message")
// Process events from the clock
go func() {
raceResults := make([]*derby.Result, 0)
for {
select {
case event, ok := <-events:
if !ok {
return
}
switch event.Type {
case derby.EventRaceStart:
fmt.Println("\n🏁 Race started!")
case derby.EventLaneFinish:
result := event.Result
fmt.Printf("🚗 Lane %d finished in place %d with time %.4f seconds\n",
result.Lane, result.FinishPlace, result.Time)
raceResults = append(raceResults, result)
case derby.EventRaceComplete:
fmt.Println("\n🏆 Race complete! Final results:")
for _, result := range raceResults {
fmt.Printf("Place %d: Lane %d - %.4f seconds\n",
result.FinishPlace, result.Lane, result.Time)
}
fmt.Println("\nEnter command (r/f/q/?):")
raceResults = nil
}
case <-ctx.Done():
return
}
}
}()
// Handle keyboard input
reader := bufio.NewReader(os.Stdin)
for {
select {
case <-ctx.Done():
return
default:
fmt.Print("Enter command (r/f/q/?): ")
input, err := reader.ReadString('\n')
if err != nil {
fmt.Printf("Error reading input: %v\n", err)
continue
}
// Trim whitespace and convert to lowercase
command := strings.TrimSpace(strings.ToLower(input))
switch command {
case "r":
fmt.Println("Resetting clock...")
if err := clock.Reset(); err != nil {
fmt.Printf("Error resetting clock: %v\n", err)
} else {
fmt.Println("Clock reset. Ready to start race.")
}
case "f":
fmt.Println("Forcing race to end...")
if err := clock.ForceEnd(); err != nil {
fmt.Printf("Error forcing race end: %v\n", err)
}
case "q":
fmt.Println("Quitting...")
return
case "?":
fmt.Println("\nCommands:")
fmt.Println(" r - Reset the clock")
fmt.Println(" f - Force end the race")
fmt.Println(" q - Quit the program")
fmt.Println(" ? - Show this help message")
default:
if command != "" {
fmt.Println("Unknown command. Type ? for help.")
}
}
}
}
}