package derby import ( "bufio" "errors" "fmt" "io" "strconv" "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() { buffer := make([]byte, 0, 256) for { select { case <-dc.stopReading: return default: // Read a single byte b := make([]byte, 1) _, err := dc.port.Read(b) 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 } // Add the byte to our buffer buffer = append(buffer, b[0]) // Check for race start signal "C\r\n" if len(buffer) >= 3 && string(buffer[len(buffer)-3:]) == "C\r\n" { dc.mu.Lock() dc.status = StatusRunning dc.mu.Unlock() // Send race start event dc.eventChan <- Event{Type: EventRaceStart} // Clear the buffer buffer = buffer[:0] continue } // Check for lane result pattern (n=t.ttttc) // We need to look for an equals sign followed by digits, a period, more digits, and a finish character if b[0] == ' ' || b[0] == '\n' { // These characters could indicate a complete result or a separator // Try to extract a result from the buffer result := dc.tryExtractResult(buffer) if result != nil { // Send lane finish event dc.eventChan <- Event{ Type: EventLaneFinish, Result: result, } // If we hit a newline, this might be the end of all results if b[0] == '\n' { // Check if we should consider the race complete dc.mu.Lock() if dc.status == StatusRunning { dc.status = StatusFinished dc.mu.Unlock() // Send race complete event dc.eventChan <- Event{Type: EventRaceComplete} } else { dc.mu.Unlock() } // Clear the buffer after a newline buffer = buffer[:0] } } } } } } // tryExtractResult attempts to extract a lane result from the buffer func (dc *DerbyClock) tryExtractResult(buffer []byte) *Result { // Convert buffer to string for easier processing bufStr := string(buffer) // Look for the pattern: n=t.ttttc // where n is the lane number, t.tttt is the time, and c is the finish character for i := 0; i < len(bufStr); i++ { if bufStr[i] == '=' { // Found an equals sign, try to extract a lane number before it j := i - 1 for j >= 0 && bufStr[j] >= '0' && bufStr[j] <= '9' { j-- } j++ // Move back to the first digit if j < i { // We found at least one digit laneStr := bufStr[j:i] // Now look for the time and finish character after the equals sign k := i + 1 for k < len(bufStr) && (bufStr[k] == '.' || (bufStr[k] >= '0' && bufStr[k] <= '9')) { k++ } if k < len(bufStr) && k > i+1 { // We found a time and a finish character timeStr := bufStr[i+1 : k] finishChar := bufStr[k : k+1] // Parse the lane number lane, err := strconv.Atoi(laneStr) if err != nil { continue } // Parse the time timeVal, err := strconv.ParseFloat(timeStr, 64) if err != nil { continue } finishPlace := 0 switch finishChar { case "!": finishPlace = 1 case "\"": finishPlace = 2 case "#": finishPlace = 3 case "$": finishPlace = 4 } // Create and return the result return &Result{ Lane: lane, Time: timeVal, FinishPlace: finishPlace, FinishSymbol: finishChar, } } } } } return nil }