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 }