mirror of
https://github.com/Team254/cheesy-arena-lite.git
synced 2026-03-09 13:46:44 -04:00
Added match flow control.
This commit is contained in:
246
arena.go
246
arena.go
@@ -5,8 +5,248 @@
|
||||
|
||||
package main
|
||||
|
||||
import ()
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
var arena struct {
|
||||
DriverStationConnections map[string]*DriverStationConnection
|
||||
// Loop and match timing constants.
|
||||
const arenaLoopPeriodMs = 1
|
||||
const dsPacketPeriodMs = 250
|
||||
const autoDurationSec = 10
|
||||
const pauseDurationSec = 1
|
||||
const teleopDurationSec = 140
|
||||
const endgameTimeLeftSec = 30
|
||||
|
||||
// Progression of match states.
|
||||
const (
|
||||
PRE_MATCH = iota
|
||||
START_MATCH
|
||||
AUTO_PERIOD
|
||||
PAUSE_PERIOD
|
||||
TELEOP_PERIOD
|
||||
ENDGAME_PERIOD
|
||||
POST_MATCH
|
||||
)
|
||||
|
||||
type AllianceStation struct {
|
||||
team *Team
|
||||
driverStationConnection *DriverStationConnection
|
||||
emergencyStop bool
|
||||
bypass bool
|
||||
}
|
||||
|
||||
type Arena struct {
|
||||
allianceStations map[string]*AllianceStation
|
||||
currentMatch *Match
|
||||
matchState int
|
||||
matchStartTime time.Time
|
||||
lastDsPacketTime time.Time
|
||||
}
|
||||
|
||||
var mainArena Arena // Named thusly to avoid polluting the global namespace with something more generic.
|
||||
|
||||
// Sets the arena to its initial state.
|
||||
func (arena *Arena) Setup() {
|
||||
arena.allianceStations = make(map[string]*AllianceStation)
|
||||
arena.allianceStations["R1"] = new(AllianceStation)
|
||||
arena.allianceStations["R2"] = new(AllianceStation)
|
||||
arena.allianceStations["R3"] = new(AllianceStation)
|
||||
arena.allianceStations["B1"] = new(AllianceStation)
|
||||
arena.allianceStations["B2"] = new(AllianceStation)
|
||||
arena.allianceStations["B3"] = new(AllianceStation)
|
||||
|
||||
// Load empty match as current.
|
||||
arena.matchState = PRE_MATCH
|
||||
arena.LoadMatch(new(Match))
|
||||
}
|
||||
|
||||
// Loads a team into an alliance station, cleaning up the previous team there if there is one.
|
||||
func (arena *Arena) AssignTeam(teamId int, station string) error {
|
||||
// Reject invalid station values.
|
||||
if _, ok := arena.allianceStations[station]; !ok {
|
||||
return fmt.Errorf("Invalid alliance station '%s'.", station)
|
||||
}
|
||||
// Do nothing if the station is already assigned to the requested team.
|
||||
dsConn := arena.allianceStations[station].driverStationConnection
|
||||
if dsConn != nil && dsConn.TeamId == teamId {
|
||||
return nil
|
||||
}
|
||||
if dsConn != nil {
|
||||
err := dsConn.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
arena.allianceStations[station].team = nil
|
||||
arena.allianceStations[station].driverStationConnection = nil
|
||||
}
|
||||
|
||||
// Leave the station empty if the team number is zero.
|
||||
if teamId == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Load the team model. Raise an error if a team doesn't exist.
|
||||
team, err := db.GetTeamById(teamId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if team == nil {
|
||||
return fmt.Errorf("Invalid team number '%d'.", teamId)
|
||||
}
|
||||
|
||||
arena.allianceStations[station].team = team
|
||||
arena.allianceStations[station].driverStationConnection, err = NewDriverStationConnection(team.Id, station)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sets up the arena for the given match.
|
||||
func (arena *Arena) LoadMatch(match *Match) error {
|
||||
if arena.matchState != PRE_MATCH {
|
||||
return fmt.Errorf("Cannot load match while there is a match still in progress or with results pending.")
|
||||
}
|
||||
|
||||
arena.currentMatch = match
|
||||
err := arena.AssignTeam(match.Red1, "R1")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = arena.AssignTeam(match.Red2, "R2")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = arena.AssignTeam(match.Red3, "R3")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = arena.AssignTeam(match.Blue1, "B1")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = arena.AssignTeam(match.Blue2, "B2")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = arena.AssignTeam(match.Blue3, "B3")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Starts the match if all conditions are met.
|
||||
func (arena *Arena) StartMatch() error {
|
||||
if arena.matchState != PRE_MATCH {
|
||||
return fmt.Errorf("Cannot start match while there is a match still in progress or with results pending.")
|
||||
}
|
||||
if arena.currentMatch == nil {
|
||||
return fmt.Errorf("Cannot start match when no match is loaded.")
|
||||
}
|
||||
arena.matchState = START_MATCH
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clears out the match and resets the arena state unless there is a match underway.
|
||||
func (arena *Arena) ResetMatch() error {
|
||||
if arena.matchState != POST_MATCH && arena.matchState != PRE_MATCH {
|
||||
return fmt.Errorf("Cannot reset match while it is in progress.")
|
||||
}
|
||||
arena.matchState = PRE_MATCH
|
||||
arena.currentMatch = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// Performs a single iteration of checking inputs and timers and setting outputs accordingly to control the
|
||||
// flow of a match.
|
||||
func (arena *Arena) Update() {
|
||||
// Decide what state the robots need to be in, depending on where we are in the match.
|
||||
auto := false
|
||||
enabled := false
|
||||
sendDsPacket := false
|
||||
matchTimeSec := arena.MatchTimeSec()
|
||||
switch arena.matchState {
|
||||
case PRE_MATCH:
|
||||
auto = true
|
||||
enabled = false
|
||||
case START_MATCH:
|
||||
arena.matchState = AUTO_PERIOD
|
||||
arena.matchStartTime = time.Now()
|
||||
auto = true
|
||||
enabled = true
|
||||
sendDsPacket = true
|
||||
case AUTO_PERIOD:
|
||||
auto = true
|
||||
enabled = true
|
||||
if matchTimeSec >= autoDurationSec {
|
||||
arena.matchState = PAUSE_PERIOD
|
||||
auto = false
|
||||
enabled = false
|
||||
sendDsPacket = true
|
||||
}
|
||||
case PAUSE_PERIOD:
|
||||
auto = false
|
||||
enabled = false
|
||||
if matchTimeSec >= autoDurationSec+pauseDurationSec {
|
||||
arena.matchState = TELEOP_PERIOD
|
||||
auto = false
|
||||
enabled = true
|
||||
sendDsPacket = true
|
||||
}
|
||||
case TELEOP_PERIOD:
|
||||
auto = false
|
||||
enabled = true
|
||||
if matchTimeSec >= autoDurationSec+pauseDurationSec+teleopDurationSec-endgameTimeLeftSec {
|
||||
arena.matchState = ENDGAME_PERIOD
|
||||
sendDsPacket = false
|
||||
}
|
||||
case ENDGAME_PERIOD:
|
||||
auto = false
|
||||
enabled = true
|
||||
if matchTimeSec >= autoDurationSec+pauseDurationSec+teleopDurationSec {
|
||||
arena.matchState = POST_MATCH
|
||||
auto = false
|
||||
enabled = false
|
||||
sendDsPacket = true
|
||||
}
|
||||
}
|
||||
|
||||
// Send a packet if at a period transition point or if it's been long enough since the last one.
|
||||
if sendDsPacket || time.Since(arena.lastDsPacketTime).Seconds()*1000 >= dsPacketPeriodMs {
|
||||
arena.sendDsPacket(auto, enabled)
|
||||
}
|
||||
}
|
||||
|
||||
// Loops indefinitely to track and update the arena components.
|
||||
func (arena *Arena) Run() {
|
||||
for {
|
||||
arena.Update()
|
||||
time.Sleep(time.Millisecond * arenaLoopPeriodMs)
|
||||
}
|
||||
}
|
||||
|
||||
func (arena *Arena) sendDsPacket(auto bool, enabled bool) {
|
||||
for _, allianceStation := range arena.allianceStations {
|
||||
dsConn := allianceStation.driverStationConnection
|
||||
if dsConn != nil {
|
||||
dsConn.Auto = auto
|
||||
dsConn.Enabled = enabled && !allianceStation.emergencyStop && !allianceStation.bypass
|
||||
err := dsConn.Update()
|
||||
if err != nil {
|
||||
// TODO(pat): Handle errors.
|
||||
}
|
||||
}
|
||||
}
|
||||
arena.lastDsPacketTime = time.Now()
|
||||
}
|
||||
|
||||
// Returns the fractional number of seconds since the start of the match.
|
||||
func (arena *Arena) MatchTimeSec() float64 {
|
||||
if arena.matchState == PRE_MATCH || arena.matchState == POST_MATCH {
|
||||
return 0
|
||||
} else {
|
||||
return time.Since(arena.matchStartTime).Seconds()
|
||||
}
|
||||
}
|
||||
|
||||
270
arena_test.go
Normal file
270
arena_test.go
Normal file
@@ -0,0 +1,270 @@
|
||||
// Copyright 2014 Team 254. All Rights Reserved.
|
||||
// Author: pat@patfairbank.com (Patrick Fairbank)
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestAssignTeam(t *testing.T) {
|
||||
clearDb()
|
||||
defer clearDb()
|
||||
var err error
|
||||
db, err = OpenDatabase(testDbPath)
|
||||
assert.Nil(t, err)
|
||||
defer db.Close()
|
||||
team := Team{Id: 254}
|
||||
err = db.CreateTeam(&team)
|
||||
assert.Nil(t, err)
|
||||
err = db.CreateTeam(&Team{Id: 1114})
|
||||
assert.Nil(t, err)
|
||||
mainArena.Setup()
|
||||
|
||||
err = mainArena.AssignTeam(254, "B1")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, team, *mainArena.allianceStations["B1"].team)
|
||||
dsConn := mainArena.allianceStations["B1"].driverStationConnection
|
||||
assert.Equal(t, 254, dsConn.TeamId)
|
||||
assert.Equal(t, "B1", dsConn.AllianceStation)
|
||||
|
||||
// Nothing should happen if the same team is assigned to the same station.
|
||||
err = mainArena.AssignTeam(254, "B1")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, team, *mainArena.allianceStations["B1"].team)
|
||||
dsConn2 := mainArena.allianceStations["B1"].driverStationConnection
|
||||
assert.Equal(t, dsConn, dsConn2) // Pointer equality
|
||||
|
||||
// Test reassignment to another team.
|
||||
err = mainArena.AssignTeam(1114, "B1")
|
||||
assert.Nil(t, err)
|
||||
assert.NotEqual(t, team, *mainArena.allianceStations["B1"].team)
|
||||
assert.Equal(t, 1114, mainArena.allianceStations["B1"].driverStationConnection.TeamId)
|
||||
err = dsConn.conn.Close()
|
||||
assert.NotNil(t, err) // Connection should have already been closed.
|
||||
|
||||
// Check assigning an unknown team.
|
||||
err = mainArena.AssignTeam(1503, "R1")
|
||||
if assert.NotNil(t, err) {
|
||||
assert.Contains(t, err.Error(), "Invalid team number")
|
||||
}
|
||||
|
||||
// Check assigning zero as the team number.
|
||||
err = mainArena.AssignTeam(0, "R2")
|
||||
assert.Nil(t, err)
|
||||
assert.Nil(t, mainArena.allianceStations["R2"].team)
|
||||
assert.Nil(t, mainArena.allianceStations["R2"].driverStationConnection)
|
||||
|
||||
// Check assigning to a non-existent station.
|
||||
err = mainArena.AssignTeam(254, "R4")
|
||||
if assert.NotNil(t, err) {
|
||||
assert.Contains(t, err.Error(), "Invalid alliance station")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArenaMatchFlow(t *testing.T) {
|
||||
clearDb()
|
||||
defer clearDb()
|
||||
var err error
|
||||
db, err = OpenDatabase(testDbPath)
|
||||
assert.Nil(t, err)
|
||||
defer db.Close()
|
||||
err = db.CreateTeam(&Team{Id: 254})
|
||||
assert.Nil(t, err)
|
||||
mainArena.Setup()
|
||||
err = mainArena.AssignTeam(254, "B3")
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Check pre-match state and packet timing.
|
||||
assert.Equal(t, PRE_MATCH, mainArena.matchState)
|
||||
mainArena.Update()
|
||||
assert.Equal(t, true, mainArena.allianceStations["B3"].driverStationConnection.Auto)
|
||||
assert.Equal(t, false, mainArena.allianceStations["B3"].driverStationConnection.Enabled)
|
||||
lastPacketCount := mainArena.allianceStations["B3"].driverStationConnection.packetCount
|
||||
mainArena.lastDsPacketTime = mainArena.lastDsPacketTime.Add(-10 * time.Millisecond)
|
||||
mainArena.Update()
|
||||
assert.Equal(t, lastPacketCount, mainArena.allianceStations["B3"].driverStationConnection.packetCount)
|
||||
mainArena.lastDsPacketTime = mainArena.lastDsPacketTime.Add(-300 * time.Millisecond)
|
||||
mainArena.Update()
|
||||
assert.Equal(t, lastPacketCount+1, mainArena.allianceStations["B3"].driverStationConnection.packetCount)
|
||||
|
||||
// Check match start, autonomous and transition to teleop.
|
||||
mainArena.StartMatch()
|
||||
mainArena.Update()
|
||||
assert.Equal(t, AUTO_PERIOD, mainArena.matchState)
|
||||
assert.Equal(t, true, mainArena.allianceStations["B3"].driverStationConnection.Auto)
|
||||
assert.Equal(t, true, mainArena.allianceStations["B3"].driverStationConnection.Enabled)
|
||||
mainArena.Update()
|
||||
assert.Equal(t, AUTO_PERIOD, mainArena.matchState)
|
||||
assert.Equal(t, true, mainArena.allianceStations["B3"].driverStationConnection.Auto)
|
||||
assert.Equal(t, true, mainArena.allianceStations["B3"].driverStationConnection.Enabled)
|
||||
mainArena.matchStartTime = time.Now().Add(-autoDurationSec * time.Second)
|
||||
mainArena.Update()
|
||||
assert.Equal(t, PAUSE_PERIOD, mainArena.matchState)
|
||||
assert.Equal(t, false, mainArena.allianceStations["B3"].driverStationConnection.Auto)
|
||||
assert.Equal(t, false, mainArena.allianceStations["B3"].driverStationConnection.Enabled)
|
||||
mainArena.Update()
|
||||
assert.Equal(t, PAUSE_PERIOD, mainArena.matchState)
|
||||
assert.Equal(t, false, mainArena.allianceStations["B3"].driverStationConnection.Auto)
|
||||
assert.Equal(t, false, mainArena.allianceStations["B3"].driverStationConnection.Enabled)
|
||||
mainArena.matchStartTime = time.Now().Add(-(autoDurationSec + pauseDurationSec) * time.Second)
|
||||
mainArena.Update()
|
||||
assert.Equal(t, TELEOP_PERIOD, mainArena.matchState)
|
||||
assert.Equal(t, false, mainArena.allianceStations["B3"].driverStationConnection.Auto)
|
||||
assert.Equal(t, true, mainArena.allianceStations["B3"].driverStationConnection.Enabled)
|
||||
mainArena.Update()
|
||||
assert.Equal(t, TELEOP_PERIOD, mainArena.matchState)
|
||||
assert.Equal(t, false, mainArena.allianceStations["B3"].driverStationConnection.Auto)
|
||||
assert.Equal(t, true, mainArena.allianceStations["B3"].driverStationConnection.Enabled)
|
||||
|
||||
// Check e-stop and bypass.
|
||||
mainArena.allianceStations["B3"].emergencyStop = true
|
||||
mainArena.lastDsPacketTime = mainArena.lastDsPacketTime.Add(-300 * time.Millisecond)
|
||||
mainArena.Update()
|
||||
assert.Equal(t, TELEOP_PERIOD, mainArena.matchState)
|
||||
assert.Equal(t, false, mainArena.allianceStations["B3"].driverStationConnection.Auto)
|
||||
assert.Equal(t, false, mainArena.allianceStations["B3"].driverStationConnection.Enabled)
|
||||
mainArena.allianceStations["B3"].bypass = true
|
||||
mainArena.lastDsPacketTime = mainArena.lastDsPacketTime.Add(-300 * time.Millisecond)
|
||||
mainArena.Update()
|
||||
assert.Equal(t, TELEOP_PERIOD, mainArena.matchState)
|
||||
assert.Equal(t, false, mainArena.allianceStations["B3"].driverStationConnection.Auto)
|
||||
assert.Equal(t, false, mainArena.allianceStations["B3"].driverStationConnection.Enabled)
|
||||
mainArena.allianceStations["B3"].emergencyStop = false
|
||||
mainArena.lastDsPacketTime = mainArena.lastDsPacketTime.Add(-300 * time.Millisecond)
|
||||
mainArena.Update()
|
||||
assert.Equal(t, TELEOP_PERIOD, mainArena.matchState)
|
||||
assert.Equal(t, false, mainArena.allianceStations["B3"].driverStationConnection.Auto)
|
||||
assert.Equal(t, false, mainArena.allianceStations["B3"].driverStationConnection.Enabled)
|
||||
mainArena.allianceStations["B3"].bypass = false
|
||||
mainArena.lastDsPacketTime = mainArena.lastDsPacketTime.Add(-300 * time.Millisecond)
|
||||
mainArena.Update()
|
||||
assert.Equal(t, TELEOP_PERIOD, mainArena.matchState)
|
||||
assert.Equal(t, false, mainArena.allianceStations["B3"].driverStationConnection.Auto)
|
||||
assert.Equal(t, true, mainArena.allianceStations["B3"].driverStationConnection.Enabled)
|
||||
|
||||
// Check endgame and match end.
|
||||
mainArena.matchStartTime = time.Now().
|
||||
Add(-(autoDurationSec + pauseDurationSec + teleopDurationSec - endgameTimeLeftSec) * time.Second)
|
||||
mainArena.Update()
|
||||
assert.Equal(t, ENDGAME_PERIOD, mainArena.matchState)
|
||||
assert.Equal(t, false, mainArena.allianceStations["B3"].driverStationConnection.Auto)
|
||||
assert.Equal(t, true, mainArena.allianceStations["B3"].driverStationConnection.Enabled)
|
||||
mainArena.Update()
|
||||
assert.Equal(t, ENDGAME_PERIOD, mainArena.matchState)
|
||||
assert.Equal(t, false, mainArena.allianceStations["B3"].driverStationConnection.Auto)
|
||||
assert.Equal(t, true, mainArena.allianceStations["B3"].driverStationConnection.Enabled)
|
||||
mainArena.matchStartTime = time.Now().Add(-(autoDurationSec + pauseDurationSec + teleopDurationSec) * time.Second)
|
||||
mainArena.Update()
|
||||
assert.Equal(t, POST_MATCH, mainArena.matchState)
|
||||
assert.Equal(t, false, mainArena.allianceStations["B3"].driverStationConnection.Auto)
|
||||
assert.Equal(t, false, mainArena.allianceStations["B3"].driverStationConnection.Enabled)
|
||||
mainArena.Update()
|
||||
assert.Equal(t, POST_MATCH, mainArena.matchState)
|
||||
assert.Equal(t, false, mainArena.allianceStations["B3"].driverStationConnection.Auto)
|
||||
assert.Equal(t, false, mainArena.allianceStations["B3"].driverStationConnection.Enabled)
|
||||
|
||||
mainArena.ResetMatch()
|
||||
mainArena.lastDsPacketTime = mainArena.lastDsPacketTime.Add(-300 * time.Millisecond)
|
||||
mainArena.Update()
|
||||
assert.Equal(t, PRE_MATCH, mainArena.matchState)
|
||||
assert.Equal(t, true, mainArena.allianceStations["B3"].driverStationConnection.Auto)
|
||||
assert.Equal(t, false, mainArena.allianceStations["B3"].driverStationConnection.Enabled)
|
||||
}
|
||||
|
||||
func TestArenaStateEnforcement(t *testing.T) {
|
||||
mainArena.Setup()
|
||||
|
||||
err := mainArena.LoadMatch(new(Match))
|
||||
assert.Nil(t, err)
|
||||
err = mainArena.StartMatch()
|
||||
assert.Nil(t, err)
|
||||
err = mainArena.LoadMatch(new(Match))
|
||||
if assert.NotNil(t, err) {
|
||||
assert.Contains(t, err.Error(), "Cannot load match while")
|
||||
}
|
||||
err = mainArena.StartMatch()
|
||||
if assert.NotNil(t, err) {
|
||||
assert.Contains(t, err.Error(), "Cannot start match while")
|
||||
}
|
||||
err = mainArena.ResetMatch()
|
||||
if assert.NotNil(t, err) {
|
||||
assert.Contains(t, err.Error(), "Cannot reset match while")
|
||||
}
|
||||
mainArena.matchState = AUTO_PERIOD
|
||||
err = mainArena.LoadMatch(new(Match))
|
||||
if assert.NotNil(t, err) {
|
||||
assert.Contains(t, err.Error(), "Cannot load match while")
|
||||
}
|
||||
err = mainArena.StartMatch()
|
||||
if assert.NotNil(t, err) {
|
||||
assert.Contains(t, err.Error(), "Cannot start match while")
|
||||
}
|
||||
err = mainArena.ResetMatch()
|
||||
if assert.NotNil(t, err) {
|
||||
assert.Contains(t, err.Error(), "Cannot reset match while")
|
||||
}
|
||||
mainArena.matchState = PAUSE_PERIOD
|
||||
err = mainArena.LoadMatch(new(Match))
|
||||
if assert.NotNil(t, err) {
|
||||
assert.Contains(t, err.Error(), "Cannot load match while")
|
||||
}
|
||||
err = mainArena.StartMatch()
|
||||
if assert.NotNil(t, err) {
|
||||
assert.Contains(t, err.Error(), "Cannot start match while")
|
||||
}
|
||||
err = mainArena.ResetMatch()
|
||||
if assert.NotNil(t, err) {
|
||||
assert.Contains(t, err.Error(), "Cannot reset match while")
|
||||
}
|
||||
mainArena.matchState = TELEOP_PERIOD
|
||||
err = mainArena.LoadMatch(new(Match))
|
||||
if assert.NotNil(t, err) {
|
||||
assert.Contains(t, err.Error(), "Cannot load match while")
|
||||
}
|
||||
err = mainArena.StartMatch()
|
||||
if assert.NotNil(t, err) {
|
||||
assert.Contains(t, err.Error(), "Cannot start match while")
|
||||
}
|
||||
err = mainArena.ResetMatch()
|
||||
if assert.NotNil(t, err) {
|
||||
assert.Contains(t, err.Error(), "Cannot reset match while")
|
||||
}
|
||||
mainArena.matchState = ENDGAME_PERIOD
|
||||
err = mainArena.LoadMatch(new(Match))
|
||||
if assert.NotNil(t, err) {
|
||||
assert.Contains(t, err.Error(), "Cannot load match while")
|
||||
}
|
||||
err = mainArena.StartMatch()
|
||||
if assert.NotNil(t, err) {
|
||||
assert.Contains(t, err.Error(), "Cannot start match while")
|
||||
}
|
||||
err = mainArena.ResetMatch()
|
||||
if assert.NotNil(t, err) {
|
||||
assert.Contains(t, err.Error(), "Cannot reset match while")
|
||||
}
|
||||
mainArena.matchState = POST_MATCH
|
||||
err = mainArena.LoadMatch(new(Match))
|
||||
if assert.NotNil(t, err) {
|
||||
assert.Contains(t, err.Error(), "Cannot load match while")
|
||||
}
|
||||
err = mainArena.StartMatch()
|
||||
if assert.NotNil(t, err) {
|
||||
assert.Contains(t, err.Error(), "Cannot start match while")
|
||||
}
|
||||
|
||||
err = mainArena.ResetMatch()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, PRE_MATCH, mainArena.matchState)
|
||||
assert.Nil(t, mainArena.currentMatch)
|
||||
err = mainArena.ResetMatch()
|
||||
assert.Nil(t, err)
|
||||
err = mainArena.StartMatch()
|
||||
if assert.NotNil(t, err) {
|
||||
assert.Contains(t, err.Error(), "no match is loaded")
|
||||
}
|
||||
err = mainArena.LoadMatch(new(Match))
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
@@ -17,10 +17,12 @@ import (
|
||||
const driverStationSendPort = 1120
|
||||
const driverStationReceivePort = 1160
|
||||
const driverStationProtocolVersion = "11191100"
|
||||
const driverStationLinkTimeoutMs = 500
|
||||
|
||||
type DriverStationStatus struct {
|
||||
TeamId int
|
||||
AllianceStation string
|
||||
DsLinked bool
|
||||
RobotLinked bool
|
||||
Auto bool
|
||||
Enabled bool
|
||||
@@ -51,17 +53,23 @@ func NewDriverStationConnection(teamId int, station string) (*DriverStationConne
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &DriverStationConnection{TeamId: teamId, AllianceStation: station, conn: conn}, nil
|
||||
return &DriverStationConnection{TeamId: teamId, AllianceStation: station,
|
||||
DriverStationStatus: new(DriverStationStatus), conn: conn}, nil
|
||||
}
|
||||
|
||||
// Builds and sends the next control packet to the Driver Station.
|
||||
func (dsConn *DriverStationConnection) SendControlPacket() error {
|
||||
packet := dsConn.encodeControlPacket()
|
||||
_, err := dsConn.conn.Write(packet[:])
|
||||
// Sends a control packet to the Driver Station and checks for timeout conditions.
|
||||
func (dsConn *DriverStationConnection) Update() error {
|
||||
err := dsConn.sendControlPacket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if time.Since(dsConn.LastPacketTime).Seconds()*1000 > driverStationLinkTimeoutMs {
|
||||
dsConn.DriverStationStatus.DsLinked = false
|
||||
dsConn.DriverStationStatus.RobotLinked = false
|
||||
dsConn.DriverStationStatus.BatteryVoltage = 0
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -90,7 +98,7 @@ func ListenForDsPackets(listener *net.UDPConn) {
|
||||
dsStatus := decodeStatusPacket(data)
|
||||
|
||||
// Update the status and last packet times for this alliance/team in the global struct.
|
||||
dsConn := arena.DriverStationConnections[dsStatus.AllianceStation]
|
||||
dsConn := mainArena.allianceStations[dsStatus.AllianceStation].driverStationConnection
|
||||
if dsConn != nil && dsConn.TeamId == dsStatus.TeamId {
|
||||
dsConn.DriverStationStatus = dsStatus
|
||||
dsConn.LastPacketTime = time.Now()
|
||||
@@ -147,9 +155,21 @@ func (dsConn *DriverStationConnection) encodeControlPacket() [74]byte {
|
||||
return packet
|
||||
}
|
||||
|
||||
// Builds and sends the next control packet to the Driver Station.
|
||||
func (dsConn *DriverStationConnection) sendControlPacket() error {
|
||||
packet := dsConn.encodeControlPacket()
|
||||
_, err := dsConn.conn.Write(packet[:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deserializes a packet from the DS into a structure representing the DS/robot status.
|
||||
func decodeStatusPacket(data [50]byte) *DriverStationStatus {
|
||||
dsStatus := new(DriverStationStatus)
|
||||
dsStatus.DsLinked = true
|
||||
|
||||
// Robot status byte.
|
||||
dsStatus.RobotLinked = (data[2] & 0x02) != 0
|
||||
|
||||
@@ -91,7 +91,7 @@ func TestSendControlPacket(t *testing.T) {
|
||||
defer dsConn.Close()
|
||||
|
||||
// No real way of checking this since the destination IP is remote, so settle for there being no errors.
|
||||
err = dsConn.SendControlPacket()
|
||||
err = dsConn.sendControlPacket()
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
@@ -154,19 +154,21 @@ func TestDecodeStatusPacket(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestListenForDsPackets(t *testing.T) {
|
||||
db, _ = OpenDatabase(testDbPath)
|
||||
|
||||
listener, err := DsPacketListener()
|
||||
assert.Nil(t, err)
|
||||
go ListenForDsPackets(listener)
|
||||
mainArena.Setup()
|
||||
|
||||
arena.DriverStationConnections = make(map[string]*DriverStationConnection)
|
||||
dsConn, err := NewDriverStationConnection(254, "B1")
|
||||
defer dsConn.Close()
|
||||
assert.Nil(t, err)
|
||||
arena.DriverStationConnections["B1"] = dsConn
|
||||
mainArena.allianceStations["B1"].driverStationConnection = dsConn
|
||||
dsConn, err = NewDriverStationConnection(1114, "R3")
|
||||
defer dsConn.Close()
|
||||
assert.Nil(t, err)
|
||||
arena.DriverStationConnections["R3"] = dsConn
|
||||
mainArena.allianceStations["R3"].driverStationConnection = dsConn
|
||||
|
||||
// Create a socket to send fake DS packets to localhost.
|
||||
conn, err := net.Dial("udp4", fmt.Sprintf("127.0.0.1:%d", driverStationReceivePort))
|
||||
@@ -174,14 +176,15 @@ func TestListenForDsPackets(t *testing.T) {
|
||||
|
||||
// Check receiving a packet from an expected team.
|
||||
packet := [50]byte{0, 0, 48, 1, 2, 54, 0, 0, 0, 0, 66, 49, 0, 0, 0, 0, 0, 0, 48, 50, 49, 50, 49, 51, 48, 48,
|
||||
152, 160, 152, 160, 255, 255, 255, 255, 82, 0, 0, 0, 0, 0, 25, 117, 0, 0, 0, 0, 42, 7, 189, 111}
|
||||
152, 160, 152, 160, 1, 0, 255, 255, 82, 0, 0, 0, 0, 0, 25, 117, 0, 0, 0, 0, 42, 7, 189, 111}
|
||||
_, err = conn.Write(packet[:])
|
||||
assert.Nil(t, err)
|
||||
time.Sleep(time.Millisecond * 10) // Allow some time for the goroutine to process the incoming packet.
|
||||
dsStatus := arena.DriverStationConnections["B1"].DriverStationStatus
|
||||
dsStatus := mainArena.allianceStations["B1"].driverStationConnection.DriverStationStatus
|
||||
if assert.NotNil(t, dsStatus) {
|
||||
assert.Equal(t, 254, dsStatus.TeamId)
|
||||
assert.Equal(t, "B1", dsStatus.AllianceStation)
|
||||
assert.Equal(t, true, dsStatus.DsLinked)
|
||||
assert.Equal(t, false, dsStatus.RobotLinked)
|
||||
assert.Equal(t, true, dsStatus.Auto)
|
||||
assert.Equal(t, true, dsStatus.Enabled)
|
||||
@@ -190,33 +193,34 @@ func TestListenForDsPackets(t *testing.T) {
|
||||
assert.Equal(t, "02121300", dsStatus.DsVersion)
|
||||
assert.Equal(t, 39072, dsStatus.PacketCount)
|
||||
assert.Equal(t, 39072, dsStatus.MissedPacketCount)
|
||||
assert.Equal(t, 41215, dsStatus.DsRobotTripTimeMs)
|
||||
assert.Equal(t, 256, dsStatus.DsRobotTripTimeMs)
|
||||
}
|
||||
assert.True(t, time.Since(arena.DriverStationConnections["B1"].LastPacketTime).Seconds() < 0.1)
|
||||
assert.True(t, time.Since(arena.DriverStationConnections["B1"].LastRobotLinkedTime).Seconds() > 100)
|
||||
assert.True(t, time.Since(mainArena.allianceStations["B1"].driverStationConnection.LastPacketTime).Seconds() < 0.1)
|
||||
assert.True(t, time.Since(mainArena.allianceStations["B1"].driverStationConnection.LastRobotLinkedTime).Seconds() > 100)
|
||||
packet[2] = byte(98)
|
||||
_, err = conn.Write(packet[:])
|
||||
assert.Nil(t, err)
|
||||
time.Sleep(time.Millisecond * 10)
|
||||
dsStatus2 := arena.DriverStationConnections["B1"].DriverStationStatus
|
||||
dsStatus2 := mainArena.allianceStations["B1"].driverStationConnection.DriverStationStatus
|
||||
if assert.NotNil(t, dsStatus2) {
|
||||
assert.Equal(t, true, dsStatus2.RobotLinked)
|
||||
assert.Equal(t, false, dsStatus2.Auto)
|
||||
assert.Equal(t, true, dsStatus2.Enabled)
|
||||
assert.Equal(t, false, dsStatus2.EmergencyStop)
|
||||
}
|
||||
assert.True(t, time.Since(arena.DriverStationConnections["B1"].LastPacketTime).Seconds() < 0.1)
|
||||
assert.True(t, time.Since(arena.DriverStationConnections["B1"].LastRobotLinkedTime).Seconds() < 0.1)
|
||||
assert.True(t, time.Since(mainArena.allianceStations["B1"].driverStationConnection.LastPacketTime).Seconds() < 0.1)
|
||||
assert.True(t, time.Since(mainArena.allianceStations["B1"].driverStationConnection.LastRobotLinkedTime).Seconds() < 0.1)
|
||||
|
||||
// Should ignore a packet coming from an expected team in the wrong position.
|
||||
statusBefore := mainArena.allianceStations["R3"].driverStationConnection.DriverStationStatus
|
||||
packet[10] = 'R'
|
||||
packet[11] = '3'
|
||||
packet[2] = 48
|
||||
_, err = conn.Write(packet[:])
|
||||
assert.Nil(t, err)
|
||||
time.Sleep(time.Millisecond * 10)
|
||||
assert.Nil(t, arena.DriverStationConnections["R3"].DriverStationStatus)
|
||||
assert.Equal(t, true, arena.DriverStationConnections["B1"].DriverStationStatus.RobotLinked)
|
||||
assert.Equal(t, statusBefore, mainArena.allianceStations["R3"].driverStationConnection.DriverStationStatus)
|
||||
assert.Equal(t, true, mainArena.allianceStations["B1"].driverStationConnection.DriverStationStatus.RobotLinked)
|
||||
|
||||
// Should ignore a packet coming from an unexpected team.
|
||||
packet[4] = byte(15)
|
||||
@@ -227,5 +231,17 @@ func TestListenForDsPackets(t *testing.T) {
|
||||
_, err = conn.Write(packet[:])
|
||||
assert.Nil(t, err)
|
||||
time.Sleep(time.Millisecond * 10)
|
||||
assert.Equal(t, true, arena.DriverStationConnections["B1"].DriverStationStatus.RobotLinked)
|
||||
assert.Equal(t, true, mainArena.allianceStations["B1"].driverStationConnection.DriverStationStatus.RobotLinked)
|
||||
|
||||
// Should indicate that the connection has dropped if a response isn't received before the timeout.
|
||||
dsConn = mainArena.allianceStations["B1"].driverStationConnection
|
||||
dsConn.Update()
|
||||
assert.Equal(t, true, dsConn.DriverStationStatus.DsLinked)
|
||||
assert.Equal(t, true, dsConn.DriverStationStatus.RobotLinked)
|
||||
assert.NotEqual(t, 0, dsConn.DriverStationStatus.BatteryVoltage)
|
||||
dsConn.LastPacketTime = dsConn.LastPacketTime.Add(-1 * time.Second)
|
||||
dsConn.Update()
|
||||
assert.Equal(t, false, dsConn.DriverStationStatus.DsLinked)
|
||||
assert.Equal(t, false, dsConn.DriverStationStatus.RobotLinked)
|
||||
assert.Equal(t, 0, dsConn.DriverStationStatus.BatteryVoltage)
|
||||
}
|
||||
|
||||
7
main.go
7
main.go
@@ -18,7 +18,12 @@ func main() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
initDb()
|
||||
|
||||
ServeWebInterface()
|
||||
go ServeWebInterface()
|
||||
listener, err := DsPacketListener()
|
||||
checkErr(err)
|
||||
go ListenForDsPackets(listener)
|
||||
mainArena.Setup()
|
||||
mainArena.Run()
|
||||
}
|
||||
|
||||
func initDb() {
|
||||
|
||||
@@ -28,8 +28,6 @@ type MatchPlayList []MatchPlayListItem
|
||||
// Global var to hold the current active tournament so that its matches are displayed by default.
|
||||
var currentMatchType string
|
||||
|
||||
var currentMatch *Match
|
||||
|
||||
// Shows the match play control interface.
|
||||
func MatchPlayHandler(w http.ResponseWriter, r *http.Request) {
|
||||
practiceMatches, err := buildMatchPlayList("practice")
|
||||
@@ -58,15 +56,12 @@ func MatchPlayHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if currentMatchType == "" {
|
||||
currentMatchType = "practice"
|
||||
}
|
||||
if currentMatch == nil {
|
||||
currentMatch = new(Match)
|
||||
}
|
||||
data := struct {
|
||||
*EventSettings
|
||||
MatchesByType map[string]MatchPlayList
|
||||
CurrentMatchType string
|
||||
Match *Match
|
||||
}{eventSettings, matchesByType, currentMatchType, currentMatch}
|
||||
}{eventSettings, matchesByType, currentMatchType, mainArena.currentMatch}
|
||||
err = template.ExecuteTemplate(w, "base", data)
|
||||
if err != nil {
|
||||
handleWebErr(w, err)
|
||||
@@ -87,8 +82,11 @@ func MatchPlayQueueHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(pat): Disallow if there is a match currently being played or there are uncommitted results.
|
||||
currentMatch = match
|
||||
err = mainArena.LoadMatch(match)
|
||||
if err != nil {
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
currentMatchType = match.Type
|
||||
|
||||
http.Redirect(w, r, "/match_play", 302)
|
||||
@@ -211,7 +209,7 @@ func buildMatchPlayList(matchType string) (MatchPlayList, error) {
|
||||
default:
|
||||
matchPlayList[i].ColorClass = ""
|
||||
}
|
||||
if currentMatch != nil && matchPlayList[i].Id == currentMatch.Id {
|
||||
if mainArena.currentMatch != nil && matchPlayList[i].Id == mainArena.currentMatch.Id {
|
||||
matchPlayList[i].ColorClass = "success"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,14 @@ func TestMatchPlayQueue(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
defer db.Close()
|
||||
eventSettings, _ = db.GetEventSettings()
|
||||
mainArena.Setup()
|
||||
|
||||
db.CreateTeam(&Team{Id: 101})
|
||||
db.CreateTeam(&Team{Id: 102})
|
||||
db.CreateTeam(&Team{Id: 103})
|
||||
db.CreateTeam(&Team{Id: 104})
|
||||
db.CreateTeam(&Team{Id: 105})
|
||||
db.CreateTeam(&Team{Id: 106})
|
||||
match := Match{Type: "elimination", DisplayName: "QF4-3", Status: "complete", Winner: "R", Red1: 101,
|
||||
Red2: 102, Red3: 103, Blue1: 104, Blue2: 105, Blue3: 106}
|
||||
db.CreateMatch(&match)
|
||||
|
||||
Reference in New Issue
Block a user