initial commit

main
DustyP 9 months ago
parent 3227ed183d
commit a6f21edef4

@ -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"]

@ -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
}

@ -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
}

@ -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
)

@ -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=
Loading…
Cancel
Save