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.
237 lines
6.1 KiB
237 lines
6.1 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")
|
|
useHTTP2 := flag.Bool("http2", true, "Enable HTTP/2 (requires TLS)")
|
|
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, *useHTTP2, 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, useHTTP2 bool, 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, useHTTP2, 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.")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|