From dd66141a1d4041a68cf48f5219540c71309cf803 Mon Sep 17 00:00:00 2001 From: Dustin Pianalto Date: Thu, 6 Mar 2025 12:56:40 -0900 Subject: [PATCH] initial commit --- Dockerfile.direct | 26 +-- derby/derby.go | 556 +++++++++++++++++++++++----------------------- examples/main.go | 248 ++++++++++----------- 3 files changed, 415 insertions(+), 415 deletions(-) diff --git a/Dockerfile.direct b/Dockerfile.direct index da1f7cc..46bb206 100644 --- a/Dockerfile.direct +++ b/Dockerfile.direct @@ -1,14 +1,14 @@ -FROM golang:1.24.1-bullseye - -WORKDIR /app - -COPY go.mod ./ -# COPY go.sum ./ # Uncomment if you have a go.sum file - -RUN go mod download - -COPY . . - -RUN go build -o track-gopher ./examples/main.go - +FROM golang:1.24.1-bullseye + +WORKDIR /app + +COPY go.mod ./ +# COPY go.sum ./ # Uncomment if you have a go.sum file + +RUN go mod download + +COPY . . + +RUN go build -o track-gopher ./examples/main.go + ENTRYPOINT ["./track-gopher"] \ No newline at end of file diff --git a/derby/derby.go b/derby/derby.go index 5b27c66..b93f062 100644 --- a/derby/derby.go +++ b/derby/derby.go @@ -1,278 +1,278 @@ -package derby - -import ( - "bufio" - "errors" - "fmt" - "io" - "strconv" - "strings" - "sync" - "time" - - "go.bug.st/serial" -) - -// Result represents a single lane result -type Result struct { - Lane int - Time float64 - FinishPlace int - FinishSymbol string -} - -// RaceStatus represents the current status of a race -type RaceStatus int - -const ( - StatusIdle RaceStatus = iota - StatusRunning - StatusFinished -) - -// Event types that can be sent on the event channel -type EventType int - -const ( - EventRaceStart EventType = iota - EventLaneFinish - EventRaceComplete -) - -// Event represents a race event -type Event struct { - Type EventType - Result *Result // Only populated for EventLaneFinish -} - -// DerbyClock represents the connection to the derby clock device -type DerbyClock struct { - port serial.Port - reader *bufio.Reader - eventChan chan Event - status RaceStatus - results []*Result - mu sync.Mutex - stopReading chan struct{} -} - -// NewDerbyClock creates a new connection to the derby clock -func NewDerbyClock(portName string, baudRate int) (*DerbyClock, error) { - mode := &serial.Mode{ - BaudRate: baudRate, - DataBits: 8, - Parity: serial.NoParity, - StopBits: serial.OneStopBit, - } - - port, err := serial.Open(portName, mode) - if err != nil { - return nil, fmt.Errorf("failed to open serial port: %w", err) - } - - dc := &DerbyClock{ - port: port, - reader: bufio.NewReader(port), - eventChan: make(chan Event, 10), // Buffer for up to 10 events - status: StatusIdle, - stopReading: make(chan struct{}), - } - - // Start the reader goroutine - go dc.readLoop() - - return dc, nil -} - -// Close closes the connection to the derby clock -func (dc *DerbyClock) Close() error { - // Signal the reading goroutine to stop - close(dc.stopReading) - - // Close the event channel - close(dc.eventChan) - - // Close the serial port - return dc.port.Close() -} - -// Events returns the channel for race events -func (dc *DerbyClock) Events() <-chan Event { - return dc.eventChan -} - -// Reset sends the reset command to the derby clock -func (dc *DerbyClock) Reset() error { - dc.mu.Lock() - defer dc.mu.Unlock() - - // Clear any previous results - dc.results = nil - dc.status = StatusIdle - - // Send the reset command - _, err := dc.port.Write([]byte("R")) - if err != nil { - return fmt.Errorf("failed to send reset command: %w", err) - } - - return nil -} - -// ForceEnd sends the force end command to the derby clock -func (dc *DerbyClock) ForceEnd() error { - dc.mu.Lock() - defer dc.mu.Unlock() - - if dc.status != StatusRunning { - return errors.New("race is not currently running") - } - - // Send the force end command - _, err := dc.port.Write([]byte("F")) - if err != nil { - return fmt.Errorf("failed to send force end command: %w", err) - } - - return nil -} - -// Status returns the current race status -func (dc *DerbyClock) Status() RaceStatus { - dc.mu.Lock() - defer dc.mu.Unlock() - return dc.status -} - -// Results returns a copy of the current race results -func (dc *DerbyClock) Results() []*Result { - dc.mu.Lock() - defer dc.mu.Unlock() - - // Make a copy of the results - resultsCopy := make([]*Result, len(dc.results)) - for i, r := range dc.results { - resultCopy := *r - resultsCopy[i] = &resultCopy - } - - return resultsCopy -} - -// readLoop continuously reads from the serial port -func (dc *DerbyClock) readLoop() { - for { - select { - case <-dc.stopReading: - return - default: - line, err := dc.reader.ReadString('\n') - if err != nil { - if err != io.EOF { - // Only log if it's not EOF - fmt.Printf("Error reading from serial port: %v\n", err) - } - time.Sleep(100 * time.Millisecond) - continue - } - - // Process the line - dc.processLine(line) - } - } -} - -// processLine handles a line of text from the derby clock -func (dc *DerbyClock) processLine(line string) { - // Trim any whitespace and carriage returns - line = strings.TrimSpace(line) - - if line == "C" { - // Race has started - dc.mu.Lock() - dc.status = StatusRunning - dc.results = nil - dc.mu.Unlock() - - // Send race start event - dc.eventChan <- Event{Type: EventRaceStart} - return - } - - // Check if this is a results line - if strings.Contains(line, "=") { - results := parseResults(line) - - dc.mu.Lock() - // Add results to our stored results - for _, result := range results { - dc.results = append(dc.results, result) - - // Send lane finish event - dc.eventChan <- Event{ - Type: EventLaneFinish, - Result: result, - } - } - - // If we have results, the race is now finished - if len(results) > 0 { - dc.status = StatusFinished - - // Send race complete event - dc.eventChan <- Event{Type: EventRaceComplete} - } - dc.mu.Unlock() - } -} - -// parseResults parses a line of results from the derby clock -func parseResults(line string) []*Result { - parts := strings.Split(line, " ") - results := make([]*Result, 0, len(parts)) - - for i, part := range parts { - if !strings.Contains(part, "=") { - continue - } - - // Format is "n=0.0000c" - laneStr := strings.Split(part, "=")[0] - timeAndPlace := strings.Split(part, "=")[1] - - if len(timeAndPlace) < 2 { - continue - } - - // Extract time and place character - timeStr := timeAndPlace[:len(timeAndPlace)-1] - placeChar := timeAndPlace[len(timeAndPlace)-1:] - - // Parse lane number - lane, err := strconv.Atoi(laneStr) - if err != nil { - continue - } - - // Parse time - timeVal, err := strconv.ParseFloat(timeStr, 64) - if err != nil { - continue - } - - // Determine place - place := i + 1 - - // Create result - result := &Result{ - Lane: lane, - Time: timeVal, - FinishPlace: place, - FinishSymbol: placeChar, - } - - results = append(results, result) - } - - return results -} +package derby + +import ( + "bufio" + "errors" + "fmt" + "io" + "strconv" + "strings" + "sync" + "time" + + "go.bug.st/serial" +) + +// Result represents a single lane result +type Result struct { + Lane int + Time float64 + FinishPlace int + FinishSymbol string +} + +// RaceStatus represents the current status of a race +type RaceStatus int + +const ( + StatusIdle RaceStatus = iota + StatusRunning + StatusFinished +) + +// Event types that can be sent on the event channel +type EventType int + +const ( + EventRaceStart EventType = iota + EventLaneFinish + EventRaceComplete +) + +// Event represents a race event +type Event struct { + Type EventType + Result *Result // Only populated for EventLaneFinish +} + +// DerbyClock represents the connection to the derby clock device +type DerbyClock struct { + port serial.Port + reader *bufio.Reader + eventChan chan Event + status RaceStatus + results []*Result + mu sync.Mutex + stopReading chan struct{} +} + +// NewDerbyClock creates a new connection to the derby clock +func NewDerbyClock(portName string, baudRate int) (*DerbyClock, error) { + mode := &serial.Mode{ + BaudRate: baudRate, + DataBits: 8, + Parity: serial.NoParity, + StopBits: serial.OneStopBit, + } + + port, err := serial.Open(portName, mode) + if err != nil { + return nil, fmt.Errorf("failed to open serial port: %w", err) + } + + dc := &DerbyClock{ + port: port, + reader: bufio.NewReader(port), + eventChan: make(chan Event, 10), // Buffer for up to 10 events + status: StatusIdle, + stopReading: make(chan struct{}), + } + + // Start the reader goroutine + go dc.readLoop() + + return dc, nil +} + +// Close closes the connection to the derby clock +func (dc *DerbyClock) Close() error { + // Signal the reading goroutine to stop + close(dc.stopReading) + + // Close the event channel + close(dc.eventChan) + + // Close the serial port + return dc.port.Close() +} + +// Events returns the channel for race events +func (dc *DerbyClock) Events() <-chan Event { + return dc.eventChan +} + +// Reset sends the reset command to the derby clock +func (dc *DerbyClock) Reset() error { + dc.mu.Lock() + defer dc.mu.Unlock() + + // Clear any previous results + dc.results = nil + dc.status = StatusIdle + + // Send the reset command + _, err := dc.port.Write([]byte("R")) + if err != nil { + return fmt.Errorf("failed to send reset command: %w", err) + } + + return nil +} + +// ForceEnd sends the force end command to the derby clock +func (dc *DerbyClock) ForceEnd() error { + dc.mu.Lock() + defer dc.mu.Unlock() + + if dc.status != StatusRunning { + return errors.New("race is not currently running") + } + + // Send the force end command + _, err := dc.port.Write([]byte("F")) + if err != nil { + return fmt.Errorf("failed to send force end command: %w", err) + } + + return nil +} + +// Status returns the current race status +func (dc *DerbyClock) Status() RaceStatus { + dc.mu.Lock() + defer dc.mu.Unlock() + return dc.status +} + +// Results returns a copy of the current race results +func (dc *DerbyClock) Results() []*Result { + dc.mu.Lock() + defer dc.mu.Unlock() + + // Make a copy of the results + resultsCopy := make([]*Result, len(dc.results)) + for i, r := range dc.results { + resultCopy := *r + resultsCopy[i] = &resultCopy + } + + return resultsCopy +} + +// readLoop continuously reads from the serial port +func (dc *DerbyClock) readLoop() { + for { + select { + case <-dc.stopReading: + return + default: + line, err := dc.reader.ReadString('\n') + if err != nil { + if err != io.EOF { + // Only log if it's not EOF + fmt.Printf("Error reading from serial port: %v\n", err) + } + time.Sleep(100 * time.Millisecond) + continue + } + + // Process the line + dc.processLine(line) + } + } +} + +// processLine handles a line of text from the derby clock +func (dc *DerbyClock) processLine(line string) { + // Trim any whitespace and carriage returns + line = strings.TrimSpace(line) + + if line == "C" { + // Race has started + dc.mu.Lock() + dc.status = StatusRunning + dc.results = nil + dc.mu.Unlock() + + // Send race start event + dc.eventChan <- Event{Type: EventRaceStart} + return + } + + // Check if this is a results line + if strings.Contains(line, "=") { + results := parseResults(line) + + dc.mu.Lock() + // Add results to our stored results + for _, result := range results { + dc.results = append(dc.results, result) + + // Send lane finish event + dc.eventChan <- Event{ + Type: EventLaneFinish, + Result: result, + } + } + + // If we have results, the race is now finished + if len(results) > 0 { + dc.status = StatusFinished + + // Send race complete event + dc.eventChan <- Event{Type: EventRaceComplete} + } + dc.mu.Unlock() + } +} + +// parseResults parses a line of results from the derby clock +func parseResults(line string) []*Result { + parts := strings.Split(line, " ") + results := make([]*Result, 0, len(parts)) + + for i, part := range parts { + if !strings.Contains(part, "=") { + continue + } + + // Format is "n=0.0000c" + laneStr := strings.Split(part, "=")[0] + timeAndPlace := strings.Split(part, "=")[1] + + if len(timeAndPlace) < 2 { + continue + } + + // Extract time and place character + timeStr := timeAndPlace[:len(timeAndPlace)-1] + placeChar := timeAndPlace[len(timeAndPlace)-1:] + + // Parse lane number + lane, err := strconv.Atoi(laneStr) + if err != nil { + continue + } + + // Parse time + timeVal, err := strconv.ParseFloat(timeStr, 64) + if err != nil { + continue + } + + // Determine place + place := i + 1 + + // Create result + result := &Result{ + Lane: lane, + Time: timeVal, + FinishPlace: place, + FinishSymbol: placeChar, + } + + results = append(results, result) + } + + return results +} diff --git a/examples/main.go b/examples/main.go index 4a54d34..772546b 100644 --- a/examples/main.go +++ b/examples/main.go @@ -1,124 +1,124 @@ -package main - -import ( - "bufio" - "fmt" - "os" - "os/signal" - "strings" - "syscall" - "time" - - "track-gopher/derby" -) - -func main() { - // Replace with your actual serial port - portName := "/dev/ttyACM0" - if len(os.Args) > 1 { - portName = os.Args[1] - } - - // Create a new derby clock connection - clock, err := derby.NewDerbyClock(portName, 9600) - 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) - - // Reset the clock to start fresh - if err := clock.Reset(); err != nil { - fmt.Printf("Error resetting clock: %v\n", err) - os.Exit(1) - } - 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() { - for event := range clock.Events() { - 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) - - case derby.EventRaceComplete: - fmt.Println("\nšŸ† Race complete! Final results:") - for _, result := range clock.Results() { - fmt.Printf("Place %d: Lane %d - %.4f seconds\n", - result.FinishPlace, result.Lane, result.Time) - } - fmt.Println("\nEnter command (r/f/q/?):") - } - } - }() - - // Handle keyboard input - go func() { - reader := bufio.NewReader(os.Stdin) - for { - 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...") - sigChan <- syscall.SIGTERM - 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.") - } - } - } - }() - - // Wait for signal to exit - <-sigChan - fmt.Println("Shutting down...") - time.Sleep(500 * time.Millisecond) // Give a moment for any pending operations to complete -} +package main + +import ( + "bufio" + "fmt" + "os" + "os/signal" + "strings" + "syscall" + "time" + + "track-gopher/derby" +) + +func main() { + // Replace with your actual serial port + portName := "/dev/ttyACM0" + if len(os.Args) > 1 { + portName = os.Args[1] + } + + // Create a new derby clock connection + clock, err := derby.NewDerbyClock(portName, 9600) + 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) + + // Reset the clock to start fresh + if err := clock.Reset(); err != nil { + fmt.Printf("Error resetting clock: %v\n", err) + os.Exit(1) + } + 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() { + for event := range clock.Events() { + 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) + + case derby.EventRaceComplete: + fmt.Println("\nšŸ† Race complete! Final results:") + for _, result := range clock.Results() { + fmt.Printf("Place %d: Lane %d - %.4f seconds\n", + result.FinishPlace, result.Lane, result.Time) + } + fmt.Println("\nEnter command (r/f/q/?):") + } + } + }() + + // Handle keyboard input + go func() { + reader := bufio.NewReader(os.Stdin) + for { + 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...") + sigChan <- syscall.SIGTERM + 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.") + } + } + } + }() + + // Wait for signal to exit + <-sigChan + fmt.Println("Shutting down...") + time.Sleep(500 * time.Millisecond) // Give a moment for any pending operations to complete +}