Added match flow control.

This commit is contained in:
Patrick Fairbank
2014-07-04 16:55:29 -07:00
parent 4721e7d8f0
commit 576bccacd3
7 changed files with 590 additions and 34 deletions

246
arena.go
View File

@@ -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
View 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)
}

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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() {

View File

@@ -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"
}
}

View File

@@ -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)