From a6f21edef4b08d9af54c027705571c7f3fc6ec21 Mon Sep 17 00:00:00 2001 From: Dustin Pianalto Date: Thu, 6 Mar 2025 12:52:08 -0900 Subject: [PATCH] initial commit --- Dockerfile | 1 + Dockerfile.direct | 14 +++ derby/derby.go | 278 ++++++++++++++++++++++++++++++++++++++++++++++ examples/main.go | 124 +++++++++++++++++++++ go.mod | 10 ++ go.sum | 14 +++ 6 files changed, 441 insertions(+) create mode 100644 Dockerfile create mode 100644 Dockerfile.direct create mode 100644 derby/derby.go create mode 100644 examples/main.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/Dockerfile @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Dockerfile.direct b/Dockerfile.direct new file mode 100644 index 0000000..da1f7cc --- /dev/null +++ b/Dockerfile.direct @@ -0,0 +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 + +ENTRYPOINT ["./track-gopher"] \ No newline at end of file diff --git a/derby/derby.go b/derby/derby.go new file mode 100644 index 0000000..5b27c66 --- /dev/null +++ b/derby/derby.go @@ -0,0 +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 +} diff --git a/examples/main.go b/examples/main.go new file mode 100644 index 0000000..4a54d34 --- /dev/null +++ b/examples/main.go @@ -0,0 +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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e6169c3 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module track-gopher + +go 1.23.5 + +require go.bug.st/serial v1.6.2 + +require ( + github.com/creack/goselect v0.1.2 // indirect + golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e6a7f3c --- /dev/null +++ b/go.sum @@ -0,0 +1,14 @@ +github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= +github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8= +go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE= +golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY= +golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=