You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

303 lines
6.8 KiB

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
}