initial commit

main
DustyP 9 months ago
parent a6f21edef4
commit dd66141a1d

@ -1,14 +1,14 @@
FROM golang:1.24.1-bullseye FROM golang:1.24.1-bullseye
WORKDIR /app WORKDIR /app
COPY go.mod ./ COPY go.mod ./
# COPY go.sum ./ # Uncomment if you have a go.sum file # COPY go.sum ./ # Uncomment if you have a go.sum file
RUN go mod download RUN go mod download
COPY . . COPY . .
RUN go build -o track-gopher ./examples/main.go RUN go build -o track-gopher ./examples/main.go
ENTRYPOINT ["./track-gopher"] ENTRYPOINT ["./track-gopher"]

@ -1,278 +1,278 @@
package derby package derby
import ( import (
"bufio" "bufio"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
"go.bug.st/serial" "go.bug.st/serial"
) )
// Result represents a single lane result // Result represents a single lane result
type Result struct { type Result struct {
Lane int Lane int
Time float64 Time float64
FinishPlace int FinishPlace int
FinishSymbol string FinishSymbol string
} }
// RaceStatus represents the current status of a race // RaceStatus represents the current status of a race
type RaceStatus int type RaceStatus int
const ( const (
StatusIdle RaceStatus = iota StatusIdle RaceStatus = iota
StatusRunning StatusRunning
StatusFinished StatusFinished
) )
// Event types that can be sent on the event channel // Event types that can be sent on the event channel
type EventType int type EventType int
const ( const (
EventRaceStart EventType = iota EventRaceStart EventType = iota
EventLaneFinish EventLaneFinish
EventRaceComplete EventRaceComplete
) )
// Event represents a race event // Event represents a race event
type Event struct { type Event struct {
Type EventType Type EventType
Result *Result // Only populated for EventLaneFinish Result *Result // Only populated for EventLaneFinish
} }
// DerbyClock represents the connection to the derby clock device // DerbyClock represents the connection to the derby clock device
type DerbyClock struct { type DerbyClock struct {
port serial.Port port serial.Port
reader *bufio.Reader reader *bufio.Reader
eventChan chan Event eventChan chan Event
status RaceStatus status RaceStatus
results []*Result results []*Result
mu sync.Mutex mu sync.Mutex
stopReading chan struct{} stopReading chan struct{}
} }
// NewDerbyClock creates a new connection to the derby clock // NewDerbyClock creates a new connection to the derby clock
func NewDerbyClock(portName string, baudRate int) (*DerbyClock, error) { func NewDerbyClock(portName string, baudRate int) (*DerbyClock, error) {
mode := &serial.Mode{ mode := &serial.Mode{
BaudRate: baudRate, BaudRate: baudRate,
DataBits: 8, DataBits: 8,
Parity: serial.NoParity, Parity: serial.NoParity,
StopBits: serial.OneStopBit, StopBits: serial.OneStopBit,
} }
port, err := serial.Open(portName, mode) port, err := serial.Open(portName, mode)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to open serial port: %w", err) return nil, fmt.Errorf("failed to open serial port: %w", err)
} }
dc := &DerbyClock{ dc := &DerbyClock{
port: port, port: port,
reader: bufio.NewReader(port), reader: bufio.NewReader(port),
eventChan: make(chan Event, 10), // Buffer for up to 10 events eventChan: make(chan Event, 10), // Buffer for up to 10 events
status: StatusIdle, status: StatusIdle,
stopReading: make(chan struct{}), stopReading: make(chan struct{}),
} }
// Start the reader goroutine // Start the reader goroutine
go dc.readLoop() go dc.readLoop()
return dc, nil return dc, nil
} }
// Close closes the connection to the derby clock // Close closes the connection to the derby clock
func (dc *DerbyClock) Close() error { func (dc *DerbyClock) Close() error {
// Signal the reading goroutine to stop // Signal the reading goroutine to stop
close(dc.stopReading) close(dc.stopReading)
// Close the event channel // Close the event channel
close(dc.eventChan) close(dc.eventChan)
// Close the serial port // Close the serial port
return dc.port.Close() return dc.port.Close()
} }
// Events returns the channel for race events // Events returns the channel for race events
func (dc *DerbyClock) Events() <-chan Event { func (dc *DerbyClock) Events() <-chan Event {
return dc.eventChan return dc.eventChan
} }
// Reset sends the reset command to the derby clock // Reset sends the reset command to the derby clock
func (dc *DerbyClock) Reset() error { func (dc *DerbyClock) Reset() error {
dc.mu.Lock() dc.mu.Lock()
defer dc.mu.Unlock() defer dc.mu.Unlock()
// Clear any previous results // Clear any previous results
dc.results = nil dc.results = nil
dc.status = StatusIdle dc.status = StatusIdle
// Send the reset command // Send the reset command
_, err := dc.port.Write([]byte("R")) _, err := dc.port.Write([]byte("R"))
if err != nil { if err != nil {
return fmt.Errorf("failed to send reset command: %w", err) return fmt.Errorf("failed to send reset command: %w", err)
} }
return nil return nil
} }
// ForceEnd sends the force end command to the derby clock // ForceEnd sends the force end command to the derby clock
func (dc *DerbyClock) ForceEnd() error { func (dc *DerbyClock) ForceEnd() error {
dc.mu.Lock() dc.mu.Lock()
defer dc.mu.Unlock() defer dc.mu.Unlock()
if dc.status != StatusRunning { if dc.status != StatusRunning {
return errors.New("race is not currently running") return errors.New("race is not currently running")
} }
// Send the force end command // Send the force end command
_, err := dc.port.Write([]byte("F")) _, err := dc.port.Write([]byte("F"))
if err != nil { if err != nil {
return fmt.Errorf("failed to send force end command: %w", err) return fmt.Errorf("failed to send force end command: %w", err)
} }
return nil return nil
} }
// Status returns the current race status // Status returns the current race status
func (dc *DerbyClock) Status() RaceStatus { func (dc *DerbyClock) Status() RaceStatus {
dc.mu.Lock() dc.mu.Lock()
defer dc.mu.Unlock() defer dc.mu.Unlock()
return dc.status return dc.status
} }
// Results returns a copy of the current race results // Results returns a copy of the current race results
func (dc *DerbyClock) Results() []*Result { func (dc *DerbyClock) Results() []*Result {
dc.mu.Lock() dc.mu.Lock()
defer dc.mu.Unlock() defer dc.mu.Unlock()
// Make a copy of the results // Make a copy of the results
resultsCopy := make([]*Result, len(dc.results)) resultsCopy := make([]*Result, len(dc.results))
for i, r := range dc.results { for i, r := range dc.results {
resultCopy := *r resultCopy := *r
resultsCopy[i] = &resultCopy resultsCopy[i] = &resultCopy
} }
return resultsCopy return resultsCopy
} }
// readLoop continuously reads from the serial port // readLoop continuously reads from the serial port
func (dc *DerbyClock) readLoop() { func (dc *DerbyClock) readLoop() {
for { for {
select { select {
case <-dc.stopReading: case <-dc.stopReading:
return return
default: default:
line, err := dc.reader.ReadString('\n') line, err := dc.reader.ReadString('\n')
if err != nil { if err != nil {
if err != io.EOF { if err != io.EOF {
// Only log if it's not EOF // Only log if it's not EOF
fmt.Printf("Error reading from serial port: %v\n", err) fmt.Printf("Error reading from serial port: %v\n", err)
} }
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
continue continue
} }
// Process the line // Process the line
dc.processLine(line) dc.processLine(line)
} }
} }
} }
// processLine handles a line of text from the derby clock // processLine handles a line of text from the derby clock
func (dc *DerbyClock) processLine(line string) { func (dc *DerbyClock) processLine(line string) {
// Trim any whitespace and carriage returns // Trim any whitespace and carriage returns
line = strings.TrimSpace(line) line = strings.TrimSpace(line)
if line == "C" { if line == "C" {
// Race has started // Race has started
dc.mu.Lock() dc.mu.Lock()
dc.status = StatusRunning dc.status = StatusRunning
dc.results = nil dc.results = nil
dc.mu.Unlock() dc.mu.Unlock()
// Send race start event // Send race start event
dc.eventChan <- Event{Type: EventRaceStart} dc.eventChan <- Event{Type: EventRaceStart}
return return
} }
// Check if this is a results line // Check if this is a results line
if strings.Contains(line, "=") { if strings.Contains(line, "=") {
results := parseResults(line) results := parseResults(line)
dc.mu.Lock() dc.mu.Lock()
// Add results to our stored results // Add results to our stored results
for _, result := range results { for _, result := range results {
dc.results = append(dc.results, result) dc.results = append(dc.results, result)
// Send lane finish event // Send lane finish event
dc.eventChan <- Event{ dc.eventChan <- Event{
Type: EventLaneFinish, Type: EventLaneFinish,
Result: result, Result: result,
} }
} }
// If we have results, the race is now finished // If we have results, the race is now finished
if len(results) > 0 { if len(results) > 0 {
dc.status = StatusFinished dc.status = StatusFinished
// Send race complete event // Send race complete event
dc.eventChan <- Event{Type: EventRaceComplete} dc.eventChan <- Event{Type: EventRaceComplete}
} }
dc.mu.Unlock() dc.mu.Unlock()
} }
} }
// parseResults parses a line of results from the derby clock // parseResults parses a line of results from the derby clock
func parseResults(line string) []*Result { func parseResults(line string) []*Result {
parts := strings.Split(line, " ") parts := strings.Split(line, " ")
results := make([]*Result, 0, len(parts)) results := make([]*Result, 0, len(parts))
for i, part := range parts { for i, part := range parts {
if !strings.Contains(part, "=") { if !strings.Contains(part, "=") {
continue continue
} }
// Format is "n=0.0000c" // Format is "n=0.0000c"
laneStr := strings.Split(part, "=")[0] laneStr := strings.Split(part, "=")[0]
timeAndPlace := strings.Split(part, "=")[1] timeAndPlace := strings.Split(part, "=")[1]
if len(timeAndPlace) < 2 { if len(timeAndPlace) < 2 {
continue continue
} }
// Extract time and place character // Extract time and place character
timeStr := timeAndPlace[:len(timeAndPlace)-1] timeStr := timeAndPlace[:len(timeAndPlace)-1]
placeChar := timeAndPlace[len(timeAndPlace)-1:] placeChar := timeAndPlace[len(timeAndPlace)-1:]
// Parse lane number // Parse lane number
lane, err := strconv.Atoi(laneStr) lane, err := strconv.Atoi(laneStr)
if err != nil { if err != nil {
continue continue
} }
// Parse time // Parse time
timeVal, err := strconv.ParseFloat(timeStr, 64) timeVal, err := strconv.ParseFloat(timeStr, 64)
if err != nil { if err != nil {
continue continue
} }
// Determine place // Determine place
place := i + 1 place := i + 1
// Create result // Create result
result := &Result{ result := &Result{
Lane: lane, Lane: lane,
Time: timeVal, Time: timeVal,
FinishPlace: place, FinishPlace: place,
FinishSymbol: placeChar, FinishSymbol: placeChar,
} }
results = append(results, result) results = append(results, result)
} }
return results return results
} }

@ -1,124 +1,124 @@
package main package main
import ( import (
"bufio" "bufio"
"fmt" "fmt"
"os" "os"
"os/signal" "os/signal"
"strings" "strings"
"syscall" "syscall"
"time" "time"
"track-gopher/derby" "track-gopher/derby"
) )
func main() { func main() {
// Replace with your actual serial port // Replace with your actual serial port
portName := "/dev/ttyACM0" portName := "/dev/ttyACM0"
if len(os.Args) > 1 { if len(os.Args) > 1 {
portName = os.Args[1] portName = os.Args[1]
} }
// Create a new derby clock connection // Create a new derby clock connection
clock, err := derby.NewDerbyClock(portName, 9600) clock, err := derby.NewDerbyClock(portName, 9600)
if err != nil { if err != nil {
fmt.Printf("Error opening derby clock: %v\n", err) fmt.Printf("Error opening derby clock: %v\n", err)
os.Exit(1) os.Exit(1)
} }
defer clock.Close() defer clock.Close()
// Set up signal handling for clean shutdown // Set up signal handling for clean shutdown
sigChan := make(chan os.Signal, 1) sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
// Reset the clock to start fresh // Reset the clock to start fresh
if err := clock.Reset(); err != nil { if err := clock.Reset(); err != nil {
fmt.Printf("Error resetting clock: %v\n", err) fmt.Printf("Error resetting clock: %v\n", err)
os.Exit(1) os.Exit(1)
} }
fmt.Println("Clock reset. Ready to start race.") fmt.Println("Clock reset. Ready to start race.")
// Print instructions // Print instructions
fmt.Println("\nCommands:") fmt.Println("\nCommands:")
fmt.Println(" r - Reset the clock") fmt.Println(" r - Reset the clock")
fmt.Println(" f - Force end the race") fmt.Println(" f - Force end the race")
fmt.Println(" q - Quit the program") fmt.Println(" q - Quit the program")
fmt.Println(" ? - Show this help message") fmt.Println(" ? - Show this help message")
// Process events from the clock // Process events from the clock
go func() { go func() {
for event := range clock.Events() { for event := range clock.Events() {
switch event.Type { switch event.Type {
case derby.EventRaceStart: case derby.EventRaceStart:
fmt.Println("\n🏁 Race started!") fmt.Println("\n🏁 Race started!")
case derby.EventLaneFinish: case derby.EventLaneFinish:
result := event.Result result := event.Result
fmt.Printf("🚗 Lane %d finished in place %d with time %.4f seconds\n", fmt.Printf("🚗 Lane %d finished in place %d with time %.4f seconds\n",
result.Lane, result.FinishPlace, result.Time) result.Lane, result.FinishPlace, result.Time)
case derby.EventRaceComplete: case derby.EventRaceComplete:
fmt.Println("\n🏆 Race complete! Final results:") fmt.Println("\n🏆 Race complete! Final results:")
for _, result := range clock.Results() { for _, result := range clock.Results() {
fmt.Printf("Place %d: Lane %d - %.4f seconds\n", fmt.Printf("Place %d: Lane %d - %.4f seconds\n",
result.FinishPlace, result.Lane, result.Time) result.FinishPlace, result.Lane, result.Time)
} }
fmt.Println("\nEnter command (r/f/q/?):") fmt.Println("\nEnter command (r/f/q/?):")
} }
} }
}() }()
// Handle keyboard input // Handle keyboard input
go func() { go func() {
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
for { for {
fmt.Print("Enter command (r/f/q/?): ") fmt.Print("Enter command (r/f/q/?): ")
input, err := reader.ReadString('\n') input, err := reader.ReadString('\n')
if err != nil { if err != nil {
fmt.Printf("Error reading input: %v\n", err) fmt.Printf("Error reading input: %v\n", err)
continue continue
} }
// Trim whitespace and convert to lowercase // Trim whitespace and convert to lowercase
command := strings.TrimSpace(strings.ToLower(input)) command := strings.TrimSpace(strings.ToLower(input))
switch command { switch command {
case "r": case "r":
fmt.Println("Resetting clock...") fmt.Println("Resetting clock...")
if err := clock.Reset(); err != nil { if err := clock.Reset(); err != nil {
fmt.Printf("Error resetting clock: %v\n", err) fmt.Printf("Error resetting clock: %v\n", err)
} else { } else {
fmt.Println("Clock reset. Ready to start race.") fmt.Println("Clock reset. Ready to start race.")
} }
case "f": case "f":
fmt.Println("Forcing race to end...") fmt.Println("Forcing race to end...")
if err := clock.ForceEnd(); err != nil { if err := clock.ForceEnd(); err != nil {
fmt.Printf("Error forcing race end: %v\n", err) fmt.Printf("Error forcing race end: %v\n", err)
} }
case "q": case "q":
fmt.Println("Quitting...") fmt.Println("Quitting...")
sigChan <- syscall.SIGTERM sigChan <- syscall.SIGTERM
return return
case "?": case "?":
fmt.Println("\nCommands:") fmt.Println("\nCommands:")
fmt.Println(" r - Reset the clock") fmt.Println(" r - Reset the clock")
fmt.Println(" f - Force end the race") fmt.Println(" f - Force end the race")
fmt.Println(" q - Quit the program") fmt.Println(" q - Quit the program")
fmt.Println(" ? - Show this help message") fmt.Println(" ? - Show this help message")
default: default:
if command != "" { if command != "" {
fmt.Println("Unknown command. Type ? for help.") fmt.Println("Unknown command. Type ? for help.")
} }
} }
} }
}() }()
// Wait for signal to exit // Wait for signal to exit
<-sigChan <-sigChan
fmt.Println("Shutting down...") fmt.Println("Shutting down...")
time.Sleep(500 * time.Millisecond) // Give a moment for any pending operations to complete time.Sleep(500 * time.Millisecond) // Give a moment for any pending operations to complete
} }

Loading…
Cancel
Save