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.
305 lines
6.8 KiB
305 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
|
|
EventHeatChanged
|
|
EventGroupChanged
|
|
EventHeatRerun
|
|
)
|
|
|
|
// 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)
|
|
results := make([]*Result, 0, 10)
|
|
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 b[0] == 'C' {
|
|
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
|
|
} else if b[0] == ' ' {
|
|
// These characters could indicate a complete result or a separator
|
|
// Try to extract a result from the buffer
|
|
dc.mu.Lock()
|
|
result := dc.tryExtractResult(buffer)
|
|
if result != nil {
|
|
// Send lane finish event
|
|
dc.eventChan <- Event{
|
|
Type: EventLaneFinish,
|
|
Result: result,
|
|
}
|
|
}
|
|
results = append(results, result)
|
|
// Clear the buffer after a result is extracted
|
|
buffer = buffer[:0]
|
|
dc.mu.Unlock()
|
|
} else if b[0] == '\n' {
|
|
// Check if we should consider the race complete
|
|
dc.mu.Lock()
|
|
if dc.status == StatusRunning && len(results) > 0 {
|
|
dc.status = StatusFinished
|
|
dc.mu.Unlock()
|
|
|
|
// Send race complete event
|
|
dc.eventChan <- Event{Type: EventRaceComplete}
|
|
results = nil
|
|
} 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
|
|
}
|