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.Stop(); 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.Event.(derby.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.") } } } } }