Implemented websocket interface to match play screen.

This commit is contained in:
Patrick Fairbank
2014-07-06 00:34:40 -07:00
parent 118619d477
commit 86c22accea
16 changed files with 1051 additions and 247 deletions

186
arena.go
View File

@@ -11,12 +11,14 @@ import (
)
// Loop and match timing constants.
const arenaLoopPeriodMs = 1
const dsPacketPeriodMs = 250
const autoDurationSec = 10
const pauseDurationSec = 1
const teleopDurationSec = 140
const endgameTimeLeftSec = 30
const (
arenaLoopPeriodMs = 10
dsPacketPeriodMs = 250
autoDurationSec = 10
pauseDurationSec = 1
teleopDurationSec = 140
endgameTimeLeftSec = 30
)
// Progression of match states.
const (
@@ -30,16 +32,17 @@ const (
)
type AllianceStation struct {
team *Team
driverStationConnection *DriverStationConnection
emergencyStop bool
bypass bool
DsConn *DriverStationConnection
EmergencyStop bool
Bypass bool
team *Team
}
type Arena struct {
allianceStations map[string]*AllianceStation
AllianceStations map[string]*AllianceStation
MatchState int
CanStartMatch bool
currentMatch *Match
matchState int
matchStartTime time.Time
lastDsPacketTime time.Time
}
@@ -48,28 +51,28 @@ var mainArena Arena // Named thusly to avoid polluting the global namespace with
// 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)
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))
arena.MatchState = PRE_MATCH
arena.LoadTestMatch()
}
// 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 {
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
dsConn := arena.AllianceStations[station].DsConn
if dsConn != nil && dsConn.TeamId == teamId {
return nil
}
@@ -78,8 +81,8 @@ func (arena *Arena) AssignTeam(teamId int, station string) error {
if err != nil {
return err
}
arena.allianceStations[station].team = nil
arena.allianceStations[station].driverStationConnection = nil
arena.AllianceStations[station].team = nil
arena.AllianceStations[station].DsConn = nil
}
// Leave the station empty if the team number is zero.
@@ -96,8 +99,8 @@ func (arena *Arena) AssignTeam(teamId int, station string) error {
return fmt.Errorf("Invalid team number '%d'.", teamId)
}
arena.allianceStations[station].team = team
arena.allianceStations[station].driverStationConnection, err = NewDriverStationConnection(team.Id, station)
arena.AllianceStations[station].team = team
arena.AllianceStations[station].DsConn, err = NewDriverStationConnection(team.Id, station)
if err != nil {
return err
}
@@ -106,7 +109,7 @@ func (arena *Arena) AssignTeam(teamId int, station string) error {
// Sets up the arena for the given match.
func (arena *Arena) LoadMatch(match *Match) error {
if arena.matchState != PRE_MATCH {
if arena.MatchState != PRE_MATCH {
return fmt.Errorf("Cannot load match while there is a match still in progress or with results pending.")
}
@@ -138,54 +141,126 @@ func (arena *Arena) LoadMatch(match *Match) error {
return nil
}
// Starts the match if all conditions are met.
func (arena *Arena) StartMatch() error {
if arena.matchState != PRE_MATCH {
// Sets a new test match as the current match.
func (arena *Arena) LoadTestMatch() error {
return arena.LoadMatch(&Match{Type: "test"})
}
// Loads the first unplayed match of the current match type.
func (arena *Arena) LoadNextMatch() error {
if arena.currentMatch.Type == "test" {
return arena.LoadTestMatch()
}
matches, err := db.GetMatchesByType(arena.currentMatch.Type)
if err != nil {
return err
}
for _, match := range matches {
if match.Status != "complete" {
err = arena.LoadMatch(&match)
if err != nil {
return err
}
break
}
}
return nil
}
// Assigns the given team to the given station, also substituting it into the match record.
func (arena *Arena) SubstituteTeam(teamId int, station string) error {
if arena.currentMatch.Type != "test" && arena.currentMatch.Type != "practice" {
return fmt.Errorf("Can only substitute teams for test and practice matches.")
}
err := arena.AssignTeam(teamId, station)
if err != nil {
return err
}
switch station {
case "R1":
arena.currentMatch.Red1 = teamId
case "R2":
arena.currentMatch.Red2 = teamId
case "R3":
arena.currentMatch.Red3 = teamId
case "B1":
arena.currentMatch.Blue1 = teamId
case "B2":
arena.currentMatch.Blue2 = teamId
case "B3":
arena.currentMatch.Blue3 = teamId
}
return nil
}
// Returns nil if the match can be started, and an error otherwise.
func (arena *Arena) CheckCanStartMatch() 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.")
}
for _, allianceStation := range arena.allianceStations {
if allianceStation.emergencyStop {
for _, allianceStation := range arena.AllianceStations {
if allianceStation.EmergencyStop {
return fmt.Errorf("Cannot start match while an emergency stop is active.")
}
if !allianceStation.bypass {
dsConn := allianceStation.driverStationConnection
if dsConn == nil || !dsConn.DriverStationStatus.RobotLinked {
if !allianceStation.Bypass {
if allianceStation.DsConn == nil || !allianceStation.DsConn.DriverStationStatus.RobotLinked {
return fmt.Errorf("Cannot start match until all robots are connected or bypassed.")
}
}
}
return nil
}
arena.matchState = START_MATCH
// Starts the match if all conditions are met.
func (arena *Arena) StartMatch() error {
err := arena.CheckCanStartMatch()
if err == nil {
arena.MatchState = START_MATCH
}
return err
}
// Kills the current match if it is underway.
func (arena *Arena) AbortMatch() error {
if arena.MatchState == PRE_MATCH || arena.MatchState == POST_MATCH {
return fmt.Errorf("Cannot abort match when it is not in progress.")
}
arena.MatchState = POST_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 {
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
arena.MatchState = PRE_MATCH
arena.AllianceStations["R1"].Bypass = false
arena.AllianceStations["R2"].Bypass = false
arena.AllianceStations["R3"].Bypass = false
arena.AllianceStations["B1"].Bypass = false
arena.AllianceStations["B2"].Bypass = false
arena.AllianceStations["B3"].Bypass = false
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() {
arena.CanStartMatch = arena.CheckCanStartMatch() == nil
// 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 {
switch arena.MatchState {
case PRE_MATCH:
auto = true
enabled = false
case START_MATCH:
arena.matchState = AUTO_PERIOD
arena.MatchState = AUTO_PERIOD
arena.matchStartTime = time.Now()
auto = true
enabled = true
@@ -194,7 +269,7 @@ func (arena *Arena) Update() {
auto = true
enabled = true
if matchTimeSec >= autoDurationSec {
arena.matchState = PAUSE_PERIOD
arena.MatchState = PAUSE_PERIOD
auto = false
enabled = false
sendDsPacket = true
@@ -203,7 +278,7 @@ func (arena *Arena) Update() {
auto = false
enabled = false
if matchTimeSec >= autoDurationSec+pauseDurationSec {
arena.matchState = TELEOP_PERIOD
arena.MatchState = TELEOP_PERIOD
auto = false
enabled = true
sendDsPacket = true
@@ -212,14 +287,14 @@ func (arena *Arena) Update() {
auto = false
enabled = true
if matchTimeSec >= autoDurationSec+pauseDurationSec+teleopDurationSec-endgameTimeLeftSec {
arena.matchState = ENDGAME_PERIOD
arena.MatchState = ENDGAME_PERIOD
sendDsPacket = false
}
case ENDGAME_PERIOD:
auto = false
enabled = true
if matchTimeSec >= autoDurationSec+pauseDurationSec+teleopDurationSec {
arena.matchState = POST_MATCH
arena.MatchState = POST_MATCH
auto = false
enabled = false
sendDsPacket = true
@@ -241,12 +316,11 @@ func (arena *Arena) Run() {
}
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()
for _, allianceStation := range arena.AllianceStations {
if allianceStation.DsConn != nil {
allianceStation.DsConn.Auto = auto
allianceStation.DsConn.Enabled = enabled && !allianceStation.EmergencyStop && !allianceStation.Bypass
err := allianceStation.DsConn.Update()
if err != nil {
// TODO(pat): Handle errors.
}
@@ -257,7 +331,7 @@ func (arena *Arena) sendDsPacket(auto bool, enabled bool) {
// 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 {
if arena.MatchState == PRE_MATCH || arena.MatchState == POST_MATCH {
return 0
} else {
return time.Since(arena.matchStartTime).Seconds()

View File

@@ -25,23 +25,23 @@ func TestAssignTeam(t *testing.T) {
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, team, *mainArena.AllianceStations["B1"].team)
dsConn := mainArena.AllianceStations["B1"].DsConn
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, team, *mainArena.AllianceStations["B1"].team)
dsConn2 := mainArena.AllianceStations["B1"].DsConn
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)
assert.NotEqual(t, team, *mainArena.AllianceStations["B1"].team)
assert.Equal(t, 1114, mainArena.AllianceStations["B1"].DsConn.TeamId)
err = dsConn.conn.Close()
assert.NotNil(t, err) // Connection should have already been closed.
@@ -54,8 +54,8 @@ func TestAssignTeam(t *testing.T) {
// 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)
assert.Nil(t, mainArena.AllianceStations["R2"].team)
assert.Nil(t, mainArena.AllianceStations["R2"].DsConn)
// Check assigning to a non-existent station.
err = mainArena.AssignTeam(254, "R4")
@@ -77,120 +77,126 @@ func TestArenaMatchFlow(t *testing.T) {
assert.Nil(t, err)
// Check pre-match state and packet timing.
assert.Equal(t, PRE_MATCH, mainArena.matchState)
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
assert.Equal(t, true, mainArena.AllianceStations["B3"].DsConn.Auto)
assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Enabled)
lastPacketCount := mainArena.AllianceStations["B3"].DsConn.packetCount
mainArena.lastDsPacketTime = mainArena.lastDsPacketTime.Add(-10 * time.Millisecond)
mainArena.Update()
assert.Equal(t, lastPacketCount, mainArena.allianceStations["B3"].driverStationConnection.packetCount)
assert.Equal(t, lastPacketCount, mainArena.AllianceStations["B3"].DsConn.packetCount)
mainArena.lastDsPacketTime = mainArena.lastDsPacketTime.Add(-300 * time.Millisecond)
mainArena.Update()
assert.Equal(t, lastPacketCount+1, mainArena.allianceStations["B3"].driverStationConnection.packetCount)
assert.Equal(t, lastPacketCount+1, mainArena.AllianceStations["B3"].DsConn.packetCount)
// Check match start, autonomous and transition to teleop.
mainArena.allianceStations["R1"].bypass = true
mainArena.allianceStations["R2"].bypass = true
mainArena.allianceStations["R3"].bypass = true
mainArena.allianceStations["B1"].bypass = true
mainArena.allianceStations["B2"].bypass = true
mainArena.allianceStations["B3"].driverStationConnection.DriverStationStatus.RobotLinked = true
mainArena.AllianceStations["R1"].Bypass = true
mainArena.AllianceStations["R2"].Bypass = true
mainArena.AllianceStations["R3"].Bypass = true
mainArena.AllianceStations["B1"].Bypass = true
mainArena.AllianceStations["B2"].Bypass = true
mainArena.AllianceStations["B3"].DsConn.DriverStationStatus.RobotLinked = true
err = mainArena.StartMatch()
assert.Nil(t, err)
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)
assert.Equal(t, AUTO_PERIOD, mainArena.MatchState)
assert.Equal(t, true, mainArena.AllianceStations["B3"].DsConn.Auto)
assert.Equal(t, true, mainArena.AllianceStations["B3"].DsConn.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)
assert.Equal(t, AUTO_PERIOD, mainArena.MatchState)
assert.Equal(t, true, mainArena.AllianceStations["B3"].DsConn.Auto)
assert.Equal(t, true, mainArena.AllianceStations["B3"].DsConn.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)
assert.Equal(t, PAUSE_PERIOD, mainArena.MatchState)
assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Auto)
assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.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)
assert.Equal(t, PAUSE_PERIOD, mainArena.MatchState)
assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Auto)
assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.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)
assert.Equal(t, TELEOP_PERIOD, mainArena.MatchState)
assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Auto)
assert.Equal(t, true, mainArena.AllianceStations["B3"].DsConn.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)
assert.Equal(t, TELEOP_PERIOD, mainArena.MatchState)
assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Auto)
assert.Equal(t, true, mainArena.AllianceStations["B3"].DsConn.Enabled)
// Check e-stop and bypass.
mainArena.allianceStations["B3"].emergencyStop = true
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
assert.Equal(t, TELEOP_PERIOD, mainArena.MatchState)
assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Auto)
assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.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
assert.Equal(t, TELEOP_PERIOD, mainArena.MatchState)
assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Auto)
assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.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
assert.Equal(t, TELEOP_PERIOD, mainArena.MatchState)
assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Auto)
assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.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)
assert.Equal(t, TELEOP_PERIOD, mainArena.MatchState)
assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Auto)
assert.Equal(t, true, mainArena.AllianceStations["B3"].DsConn.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)
assert.Equal(t, ENDGAME_PERIOD, mainArena.MatchState)
assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Auto)
assert.Equal(t, true, mainArena.AllianceStations["B3"].DsConn.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)
assert.Equal(t, ENDGAME_PERIOD, mainArena.MatchState)
assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Auto)
assert.Equal(t, true, mainArena.AllianceStations["B3"].DsConn.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)
assert.Equal(t, POST_MATCH, mainArena.MatchState)
assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Auto)
assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.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)
assert.Equal(t, POST_MATCH, mainArena.MatchState)
assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Auto)
assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Enabled)
mainArena.AllianceStations["R1"].Bypass = true
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)
assert.Equal(t, PRE_MATCH, mainArena.MatchState)
assert.Equal(t, true, mainArena.AllianceStations["B3"].DsConn.Auto)
assert.Equal(t, false, mainArena.AllianceStations["B3"].DsConn.Enabled)
assert.Equal(t, false, mainArena.AllianceStations["R1"].Bypass)
}
func TestArenaStateEnforcement(t *testing.T) {
mainArena.Setup()
mainArena.allianceStations["R1"].bypass = true
mainArena.allianceStations["R2"].bypass = true
mainArena.allianceStations["R3"].bypass = true
mainArena.allianceStations["B1"].bypass = true
mainArena.allianceStations["B2"].bypass = true
mainArena.allianceStations["B3"].bypass = true
mainArena.AllianceStations["R1"].Bypass = true
mainArena.AllianceStations["R2"].Bypass = true
mainArena.AllianceStations["R3"].Bypass = true
mainArena.AllianceStations["B1"].Bypass = true
mainArena.AllianceStations["B2"].Bypass = true
mainArena.AllianceStations["B3"].Bypass = true
err := mainArena.LoadMatch(new(Match))
assert.Nil(t, err)
err = mainArena.AbortMatch()
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "Cannot abort match when")
}
err = mainArena.StartMatch()
assert.Nil(t, err)
err = mainArena.LoadMatch(new(Match))
@@ -205,7 +211,7 @@ func TestArenaStateEnforcement(t *testing.T) {
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "Cannot reset match while")
}
mainArena.matchState = AUTO_PERIOD
mainArena.MatchState = AUTO_PERIOD
err = mainArena.LoadMatch(new(Match))
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "Cannot load match while")
@@ -218,7 +224,7 @@ func TestArenaStateEnforcement(t *testing.T) {
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "Cannot reset match while")
}
mainArena.matchState = PAUSE_PERIOD
mainArena.MatchState = PAUSE_PERIOD
err = mainArena.LoadMatch(new(Match))
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "Cannot load match while")
@@ -231,7 +237,7 @@ func TestArenaStateEnforcement(t *testing.T) {
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "Cannot reset match while")
}
mainArena.matchState = TELEOP_PERIOD
mainArena.MatchState = TELEOP_PERIOD
err = mainArena.LoadMatch(new(Match))
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "Cannot load match while")
@@ -244,7 +250,7 @@ func TestArenaStateEnforcement(t *testing.T) {
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "Cannot reset match while")
}
mainArena.matchState = ENDGAME_PERIOD
mainArena.MatchState = ENDGAME_PERIOD
err = mainArena.LoadMatch(new(Match))
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "Cannot load match while")
@@ -257,7 +263,9 @@ func TestArenaStateEnforcement(t *testing.T) {
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "Cannot reset match while")
}
mainArena.matchState = POST_MATCH
err = mainArena.AbortMatch()
assert.Nil(t, err)
mainArena.MatchState = POST_MATCH
err = mainArena.LoadMatch(new(Match))
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "Cannot load match while")
@@ -266,17 +274,16 @@ func TestArenaStateEnforcement(t *testing.T) {
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "Cannot start match while")
}
err = mainArena.AbortMatch()
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "Cannot abort match when")
}
err = mainArena.ResetMatch()
assert.Nil(t, err)
assert.Equal(t, PRE_MATCH, mainArena.matchState)
assert.Nil(t, mainArena.currentMatch)
assert.Equal(t, PRE_MATCH, mainArena.MatchState)
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)
}
@@ -300,30 +307,30 @@ func TestMatchStartRobotLinkEnforcement(t *testing.T) {
err = mainArena.LoadMatch(&match)
assert.Nil(t, err)
for _, station := range mainArena.allianceStations {
station.driverStationConnection.DriverStationStatus.RobotLinked = true
for _, station := range mainArena.AllianceStations {
station.DsConn.DriverStationStatus.RobotLinked = true
}
err = mainArena.StartMatch()
assert.Nil(t, err)
mainArena.matchState = PRE_MATCH
mainArena.MatchState = PRE_MATCH
// Check with a single team e-stopped, not linked and bypassed.
mainArena.allianceStations["R1"].emergencyStop = true
mainArena.AllianceStations["R1"].EmergencyStop = true
err = mainArena.StartMatch()
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "while an emergency stop is active")
}
mainArena.allianceStations["R1"].emergencyStop = false
mainArena.allianceStations["R1"].driverStationConnection.DriverStationStatus.RobotLinked = false
mainArena.AllianceStations["R1"].EmergencyStop = false
mainArena.AllianceStations["R1"].DsConn.DriverStationStatus.RobotLinked = false
err = mainArena.StartMatch()
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "until all robots are connected or bypassed")
}
mainArena.allianceStations["R1"].bypass = true
mainArena.AllianceStations["R1"].Bypass = true
err = mainArena.StartMatch()
assert.Nil(t, err)
mainArena.allianceStations["R1"].bypass = false
mainArena.matchState = PRE_MATCH
mainArena.AllianceStations["R1"].Bypass = false
mainArena.MatchState = PRE_MATCH
// Check with a team missing.
err = mainArena.AssignTeam(0, "R1")
@@ -332,10 +339,10 @@ func TestMatchStartRobotLinkEnforcement(t *testing.T) {
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "until all robots are connected or bypassed")
}
mainArena.allianceStations["R1"].bypass = true
mainArena.AllianceStations["R1"].Bypass = true
err = mainArena.StartMatch()
assert.Nil(t, err)
mainArena.matchState = PRE_MATCH
mainArena.MatchState = PRE_MATCH
// Check with no teams present.
mainArena.LoadMatch(new(Match))
@@ -343,18 +350,134 @@ func TestMatchStartRobotLinkEnforcement(t *testing.T) {
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "until all robots are connected or bypassed")
}
mainArena.allianceStations["R1"].bypass = true
mainArena.allianceStations["R2"].bypass = true
mainArena.allianceStations["R3"].bypass = true
mainArena.allianceStations["B1"].bypass = true
mainArena.allianceStations["B2"].bypass = true
mainArena.allianceStations["B3"].bypass = true
mainArena.allianceStations["B3"].emergencyStop = true
mainArena.AllianceStations["R1"].Bypass = true
mainArena.AllianceStations["R2"].Bypass = true
mainArena.AllianceStations["R3"].Bypass = true
mainArena.AllianceStations["B1"].Bypass = true
mainArena.AllianceStations["B2"].Bypass = true
mainArena.AllianceStations["B3"].Bypass = true
mainArena.AllianceStations["B3"].EmergencyStop = true
err = mainArena.StartMatch()
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "while an emergency stop is active")
}
mainArena.allianceStations["B3"].emergencyStop = false
mainArena.AllianceStations["B3"].EmergencyStop = false
err = mainArena.StartMatch()
assert.Nil(t, err)
}
func TestLoadNextMatch(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
mainArena.Setup()
db.CreateTeam(&Team{Id: 1114})
practiceMatch1 := Match{Type: "practice", DisplayName: "1"}
practiceMatch2 := Match{Type: "practice", DisplayName: "2", Status: "complete"}
practiceMatch3 := Match{Type: "practice", DisplayName: "3"}
db.CreateMatch(&practiceMatch1)
db.CreateMatch(&practiceMatch2)
db.CreateMatch(&practiceMatch3)
qualificationMatch1 := Match{Type: "qualification", DisplayName: "1", Status: "complete"}
qualificationMatch2 := Match{Type: "qualification", DisplayName: "2"}
db.CreateMatch(&qualificationMatch1)
db.CreateMatch(&qualificationMatch2)
// Test match should be followed by another, empty test match.
assert.Equal(t, 0, mainArena.currentMatch.Id)
err = mainArena.SubstituteTeam(1114, "R1")
assert.Nil(t, err)
mainArena.currentMatch.Status = "complete"
err = mainArena.LoadNextMatch()
assert.Nil(t, err)
assert.Equal(t, 0, mainArena.currentMatch.Id)
assert.Equal(t, 0, mainArena.currentMatch.Red1)
assert.NotEqual(t, "complete", mainArena.currentMatch.Status)
// Other matches should be loaded by type until they're all complete.
err = mainArena.LoadMatch(&practiceMatch2)
assert.Nil(t, err)
err = mainArena.LoadNextMatch()
assert.Nil(t, err)
assert.Equal(t, practiceMatch1.Id, mainArena.currentMatch.Id)
practiceMatch1.Status = "complete"
db.SaveMatch(&practiceMatch1)
err = mainArena.LoadNextMatch()
assert.Nil(t, err)
assert.Equal(t, practiceMatch3.Id, mainArena.currentMatch.Id)
practiceMatch3.Status = "complete"
db.SaveMatch(&practiceMatch3)
err = mainArena.LoadNextMatch()
assert.Nil(t, err)
assert.Equal(t, practiceMatch3.Id, mainArena.currentMatch.Id)
assert.Equal(t, "complete", practiceMatch3.Status)
err = mainArena.LoadMatch(&qualificationMatch1)
assert.Nil(t, err)
err = mainArena.LoadNextMatch()
assert.Nil(t, err)
assert.Equal(t, qualificationMatch2.Id, mainArena.currentMatch.Id)
}
func TestSubstituteTeam(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
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})
db.CreateTeam(&Team{Id: 107})
// Substitute teams into test match.
err = mainArena.SubstituteTeam(101, "B1")
assert.Nil(t, err)
assert.Equal(t, 101, mainArena.currentMatch.Blue1)
assert.Equal(t, 101, mainArena.AllianceStations["B1"].team.Id)
err = mainArena.SubstituteTeam(1503, "R1")
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "Invalid team number")
}
err = mainArena.AssignTeam(104, "R4")
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "Invalid alliance station")
}
// Substitute teams into practice match. Replacement should also be persisted in the DB.
match := Match{Type: "practice", Red1: 101, Red2: 102, Red3: 103, Blue1: 104, Blue2: 105, Blue3: 106}
db.CreateMatch(&match)
mainArena.LoadMatch(&match)
err = mainArena.SubstituteTeam(107, "R1")
assert.Nil(t, err)
assert.Equal(t, 107, mainArena.currentMatch.Red1)
assert.Equal(t, 107, mainArena.AllianceStations["R1"].team.Id)
CommitMatchScore(mainArena.currentMatch, &MatchResult{MatchId: mainArena.currentMatch.Id})
match2, _ := db.GetMatchById(match.Id)
assert.Equal(t, 107, match2.Red1)
// Check that substitution is disallowed in qualification and elimination matches.
match = Match{Type: "qualification", Red1: 101, Red2: 102, Red3: 103, Blue1: 104, Blue2: 105, Blue3: 106}
db.CreateMatch(&match)
mainArena.LoadMatch(&match)
err = mainArena.SubstituteTeam(107, "R1")
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "Can only substitute teams for test and practice matches")
}
match = Match{Type: "elimination", Red1: 101, Red2: 102, Red3: 103, Blue1: 104, Blue2: 105, Blue3: 106}
db.CreateMatch(&match)
mainArena.LoadMatch(&match)
err = mainArena.SubstituteTeam(107, "R1")
if assert.NotNil(t, err) {
assert.Contains(t, err.Error(), "Can only substitute teams for test and practice matches")
}
}

View File

@@ -98,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 := mainArena.allianceStations[dsStatus.AllianceStation].driverStationConnection
dsConn := mainArena.AllianceStations[dsStatus.AllianceStation].DsConn
if dsConn != nil && dsConn.TeamId == dsStatus.TeamId {
dsConn.DriverStationStatus = dsStatus
dsConn.LastPacketTime = time.Now()

View File

@@ -164,11 +164,11 @@ func TestListenForDsPackets(t *testing.T) {
dsConn, err := NewDriverStationConnection(254, "B1")
defer dsConn.Close()
assert.Nil(t, err)
mainArena.allianceStations["B1"].driverStationConnection = dsConn
mainArena.AllianceStations["B1"].DsConn = dsConn
dsConn, err = NewDriverStationConnection(1114, "R3")
defer dsConn.Close()
assert.Nil(t, err)
mainArena.allianceStations["R3"].driverStationConnection = dsConn
mainArena.AllianceStations["R3"].DsConn = dsConn
// Create a socket to send fake DS packets to localhost.
conn, err := net.Dial("udp4", fmt.Sprintf("127.0.0.1:%d", driverStationReceivePort))
@@ -180,7 +180,7 @@ func TestListenForDsPackets(t *testing.T) {
_, 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 := mainArena.allianceStations["B1"].driverStationConnection.DriverStationStatus
dsStatus := mainArena.AllianceStations["B1"].DsConn.DriverStationStatus
if assert.NotNil(t, dsStatus) {
assert.Equal(t, 254, dsStatus.TeamId)
assert.Equal(t, "B1", dsStatus.AllianceStation)
@@ -195,32 +195,32 @@ func TestListenForDsPackets(t *testing.T) {
assert.Equal(t, 39072, dsStatus.MissedPacketCount)
assert.Equal(t, 256, dsStatus.DsRobotTripTimeMs)
}
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)
assert.True(t, time.Since(mainArena.AllianceStations["B1"].DsConn.LastPacketTime).Seconds() < 0.1)
assert.True(t, time.Since(mainArena.AllianceStations["B1"].DsConn.LastRobotLinkedTime).Seconds() > 100)
packet[2] = byte(98)
_, err = conn.Write(packet[:])
assert.Nil(t, err)
time.Sleep(time.Millisecond * 10)
dsStatus2 := mainArena.allianceStations["B1"].driverStationConnection.DriverStationStatus
dsStatus2 := mainArena.AllianceStations["B1"].DsConn.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(mainArena.allianceStations["B1"].driverStationConnection.LastPacketTime).Seconds() < 0.1)
assert.True(t, time.Since(mainArena.allianceStations["B1"].driverStationConnection.LastRobotLinkedTime).Seconds() < 0.1)
assert.True(t, time.Since(mainArena.AllianceStations["B1"].DsConn.LastPacketTime).Seconds() < 0.1)
assert.True(t, time.Since(mainArena.AllianceStations["B1"].DsConn.LastRobotLinkedTime).Seconds() < 0.1)
// Should ignore a packet coming from an expected team in the wrong position.
statusBefore := mainArena.allianceStations["R3"].driverStationConnection.DriverStationStatus
statusBefore := mainArena.AllianceStations["R3"].DsConn.DriverStationStatus
packet[10] = 'R'
packet[11] = '3'
packet[2] = 48
_, err = conn.Write(packet[:])
assert.Nil(t, err)
time.Sleep(time.Millisecond * 10)
assert.Equal(t, statusBefore, mainArena.allianceStations["R3"].driverStationConnection.DriverStationStatus)
assert.Equal(t, true, mainArena.allianceStations["B1"].driverStationConnection.DriverStationStatus.RobotLinked)
assert.Equal(t, statusBefore, mainArena.AllianceStations["R3"].DsConn.DriverStationStatus)
assert.Equal(t, true, mainArena.AllianceStations["B1"].DsConn.DriverStationStatus.RobotLinked)
// Should ignore a packet coming from an unexpected team.
packet[4] = byte(15)
@@ -231,10 +231,10 @@ func TestListenForDsPackets(t *testing.T) {
_, err = conn.Write(packet[:])
assert.Nil(t, err)
time.Sleep(time.Millisecond * 10)
assert.Equal(t, true, mainArena.allianceStations["B1"].driverStationConnection.DriverStationStatus.RobotLinked)
assert.Equal(t, true, mainArena.AllianceStations["B1"].DsConn.DriverStationStatus.RobotLinked)
// Should indicate that the connection has dropped if a response isn't received before the timeout.
dsConn = mainArena.allianceStations["B1"].driverStationConnection
dsConn = mainArena.AllianceStations["B1"].DsConn
dsConn.Update()
assert.Equal(t, true, dsConn.DriverStationStatus.DsLinked)
assert.Equal(t, true, dsConn.DriverStationStatus.RobotLinked)

View File

@@ -8,11 +8,15 @@ package main
import (
"fmt"
"github.com/gorilla/mux"
"github.com/mitchellh/mapstructure"
"html/template"
"io"
"log"
"math/rand"
"net/http"
"sort"
"strconv"
"time"
)
type MatchPlayListItem struct {
@@ -46,7 +50,8 @@ func MatchPlayHandler(w http.ResponseWriter, r *http.Request) {
return
}
template, err := template.ParseFiles("templates/match_play.html", "templates/base.html")
template := template.New("").Funcs(templateHelpers)
_, err = template.ParseFiles("templates/match_play.html", "templates/base.html")
if err != nil {
handleWebErr(w, err)
return
@@ -56,12 +61,14 @@ func MatchPlayHandler(w http.ResponseWriter, r *http.Request) {
if currentMatchType == "" {
currentMatchType = "practice"
}
allowSubstitution := mainArena.currentMatch.Type == "test" || mainArena.currentMatch.Type == "practice"
data := struct {
*EventSettings
MatchesByType map[string]MatchPlayList
CurrentMatchType string
Match *Match
}{eventSettings, matchesByType, currentMatchType, mainArena.currentMatch}
MatchesByType map[string]MatchPlayList
CurrentMatchType string
Match *Match
AllowSubstitution bool
}{eventSettings, matchesByType, currentMatchType, mainArena.currentMatch, allowSubstitution}
err = template.ExecuteTemplate(w, "base", data)
if err != nil {
handleWebErr(w, err)
@@ -69,25 +76,30 @@ func MatchPlayHandler(w http.ResponseWriter, r *http.Request) {
}
}
func MatchPlayQueueHandler(w http.ResponseWriter, r *http.Request) {
func MatchPlayLoadHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
matchId, _ := strconv.Atoi(vars["matchId"])
match, err := db.GetMatchById(matchId)
var match *Match
var err error
if matchId == 0 {
err = mainArena.LoadTestMatch()
} else {
match, err = db.GetMatchById(matchId)
if err != nil {
handleWebErr(w, err)
return
}
if match == nil {
handleWebErr(w, fmt.Errorf("Invalid match ID %d.", matchId))
return
}
err = mainArena.LoadMatch(match)
}
if err != nil {
handleWebErr(w, err)
return
}
if match == nil {
handleWebErr(w, fmt.Errorf("Invalid match ID %d.", matchId))
return
}
err = mainArena.LoadMatch(match)
if err != nil {
handleWebErr(w, err)
return
}
currentMatchType = match.Type
currentMatchType = mainArena.currentMatch.Type
http.Redirect(w, r, "/match_play", 302)
}
@@ -118,7 +130,150 @@ func MatchPlayFakeResultHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/match_play", 302)
}
// The websocket endpoint for the match play client to send control commands and receive status updates.
func MatchPlayWebsocketHandler(w http.ResponseWriter, r *http.Request) {
// Allow disabling of period updates, for easier testing.
_, disableUpdates := r.URL.Query()["test"]
websocket, err := NewWebsocket(w, r)
if err != nil {
handleWebErr(w, err)
return
}
defer websocket.Close()
// Send the arena status immediately upon connection.
err = websocket.Write("status", mainArena)
if err != nil {
log.Printf("Websocket error: %s", err)
return
}
if !disableUpdates {
// Spin off a goroutine to periodically send a status update.
go func() {
for {
err = websocket.Write("status", mainArena)
if err != nil {
// The client has probably closed the connection; nothing to do here.
break
}
time.Sleep(500 * time.Millisecond)
}
}()
}
// Loop, waiting for commands and responding to them, until the client closes the connection.
for {
messageType, data, err := websocket.Read()
if err != nil {
if err == io.EOF {
// Client has closed the connection; nothing to do here.
return
}
log.Printf("Websocket error: %s", err)
return
}
switch messageType {
case "substituteTeam":
args := struct {
Team int
Position string
}{}
err = mapstructure.Decode(data, &args)
if err != nil {
websocket.WriteError(err.Error())
continue
}
err = mainArena.SubstituteTeam(args.Team, args.Position)
if err != nil {
websocket.WriteError(err.Error())
continue
}
case "toggleBypass":
station, ok := data.(string)
if !ok {
websocket.WriteError(fmt.Sprintf("Failed to parse '%s' message.", messageType))
continue
}
if _, ok := mainArena.AllianceStations[station]; !ok {
websocket.WriteError(fmt.Sprintf("Invalid alliance station '%s'.", station))
continue
}
mainArena.AllianceStations[station].Bypass = !mainArena.AllianceStations[station].Bypass
case "startMatch":
err = mainArena.StartMatch()
if err != nil {
websocket.WriteError(err.Error())
continue
}
case "abortMatch":
err = mainArena.AbortMatch()
if err != nil {
websocket.WriteError(err.Error())
continue
}
case "commitResults":
// TODO(pat): Deal with scoring here. For now, use an empty match result set for a 0-0 tie.
err = CommitMatchScore(mainArena.currentMatch, &MatchResult{MatchId: mainArena.currentMatch.Id})
if err != nil {
websocket.WriteError(err.Error())
continue
}
err = mainArena.ResetMatch()
if err != nil {
websocket.WriteError(err.Error())
continue
}
err = mainArena.LoadNextMatch()
if err != nil {
websocket.WriteError(err.Error())
continue
}
err = websocket.Write("reload", nil)
if err != nil {
log.Printf("Websocket error: %s", err)
return
}
continue // Skip sending the status update, as the client is about to terminate and reload.
case "discardResults":
err = mainArena.ResetMatch()
if err != nil {
websocket.WriteError(err.Error())
continue
}
err = mainArena.LoadNextMatch()
if err != nil {
websocket.WriteError(err.Error())
continue
}
err = websocket.Write("reload", nil)
if err != nil {
log.Printf("Websocket error: %s", err)
return
}
continue // Skip sending the status update, as the client is about to terminate and reload.
default:
websocket.WriteError(fmt.Sprintf("Invalid message type '%s'.", messageType))
continue
}
// Send out the status again after handling the command, as it most likely changed as a result.
err = websocket.Write("status", mainArena)
if err != nil {
log.Printf("Websocket error: %s", err)
return
}
}
}
func CommitMatchScore(match *Match, matchResult *MatchResult) error {
if match.Type == "test" {
// Do nothing since this is a test match and doesn't exist in the database.
return nil
}
if matchResult.PlayNumber == 0 {
// Determine the play number for this new match result.
prevMatchResult, err := db.GetMatchResultForMatch(match.Id)

View File

@@ -5,6 +5,7 @@ package main
import (
"fmt"
"github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"testing"
)
@@ -39,7 +40,7 @@ func TestMatchPlay(t *testing.T) {
assert.Contains(t, recorder.Body.String(), "SF1-2")
}
func TestMatchPlayQueue(t *testing.T) {
func TestMatchPlayLoad(t *testing.T) {
clearDb()
defer clearDb()
var err error
@@ -67,8 +68,8 @@ func TestMatchPlayQueue(t *testing.T) {
assert.NotContains(t, recorder.Body.String(), "105")
assert.NotContains(t, recorder.Body.String(), "106")
// Queue the match and check for the team numbers again.
recorder = getHttpResponse(fmt.Sprintf("/match_play/%d/queue", match.Id))
// Load the match and check for the team numbers again.
recorder = getHttpResponse(fmt.Sprintf("/match_play/%d/load", match.Id))
assert.Equal(t, 302, recorder.Code)
recorder = getHttpResponse("/match_play")
assert.Equal(t, 200, recorder.Code)
@@ -78,6 +79,18 @@ func TestMatchPlayQueue(t *testing.T) {
assert.Contains(t, recorder.Body.String(), "104")
assert.Contains(t, recorder.Body.String(), "105")
assert.Contains(t, recorder.Body.String(), "106")
// Load a test match.
recorder = getHttpResponse("/match_play/0/load")
assert.Equal(t, 302, recorder.Code)
recorder = getHttpResponse("/match_play")
assert.Equal(t, 200, recorder.Code)
assert.NotContains(t, recorder.Body.String(), "101")
assert.NotContains(t, recorder.Body.String(), "102")
assert.NotContains(t, recorder.Body.String(), "103")
assert.NotContains(t, recorder.Body.String(), "104")
assert.NotContains(t, recorder.Body.String(), "105")
assert.NotContains(t, recorder.Body.String(), "106")
}
func TestMatchPlayErrors(t *testing.T) {
@@ -89,8 +102,141 @@ func TestMatchPlayErrors(t *testing.T) {
defer db.Close()
eventSettings, _ = db.GetEventSettings()
// Queue an invalid match.
recorder := getHttpResponse("/match_play/1114/queue")
// Load an invalid match.
recorder := getHttpResponse("/match_play/1114/load")
assert.Equal(t, 500, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Invalid match")
}
func TestCommitMatch(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
eventSettings, _ = db.GetEventSettings()
// Committing test match should do nothing.
match := &Match{Id: 0, Type: "test", Red1: 101, Red2: 102, Red3: 103, Blue1: 104, Blue2: 105, Blue3: 106}
err = CommitMatchScore(match, &MatchResult{MatchId: match.Id})
assert.Nil(t, err)
matchResult, err := db.GetMatchResultForMatch(match.Id)
assert.Nil(t, err)
assert.Nil(t, matchResult)
// Committing the same match more than once should create a second match result record.
match.Id = 1
match.Type = "qualification"
db.CreateMatch(match)
matchResult = &MatchResult{MatchId: match.Id, BlueScore: Score{AutoHigh: 1}}
err = CommitMatchScore(match, matchResult)
assert.Nil(t, err)
assert.Equal(t, 1, matchResult.PlayNumber)
match, _ = db.GetMatchById(1)
assert.Equal(t, "B", match.Winner)
matchResult = &MatchResult{MatchId: match.Id, RedScore: Score{AutoHigh: 1}}
err = CommitMatchScore(match, matchResult)
assert.Nil(t, err)
assert.Equal(t, 2, matchResult.PlayNumber)
match, _ = db.GetMatchById(1)
assert.Equal(t, "R", match.Winner)
matchResult = &MatchResult{MatchId: match.Id}
err = CommitMatchScore(match, matchResult)
assert.Nil(t, err)
assert.Equal(t, 3, matchResult.PlayNumber)
match, _ = db.GetMatchById(1)
assert.Equal(t, "T", match.Winner)
}
func TestMatchPlayWebsocket(t *testing.T) {
clearDb()
defer clearDb()
var err error
db, err = OpenDatabase(testDbPath)
assert.Nil(t, err)
defer db.Close()
db.CreateTeam(&Team{Id: 254})
mainArena.Setup()
server, wsUrl := startTestServer()
defer server.Close()
conn, _, err := websocket.DefaultDialer.Dial(wsUrl+"/match_play/websocket?test", nil)
assert.Nil(t, err)
defer conn.Close()
ws := &Websocket{conn}
// Should get a status update right after connection.
messageType, _, err := ws.Read()
assert.Nil(t, err)
assert.Equal(t, "status", messageType)
// Test that a server-side error is communicated to the client.
ws.Write("nonexistenttype", nil)
assert.Contains(t, readWebsocketError(t, ws), "Invalid message type")
// Test match setup commands.
ws.Write("substituteTeam", nil)
assert.Contains(t, readWebsocketError(t, ws), "Invalid alliance station")
ws.Write("substituteTeam", map[string]interface{}{"team": 254, "position": "B5"})
assert.Contains(t, readWebsocketError(t, ws), "Invalid alliance station")
ws.Write("substituteTeam", map[string]interface{}{"team": 254, "position": "B1"})
readWebsocketType(t, ws, "status")
assert.Equal(t, 254, mainArena.currentMatch.Blue1)
ws.Write("substituteTeam", map[string]interface{}{"team": 0, "position": "B1"})
readWebsocketType(t, ws, "status")
assert.Equal(t, 0, mainArena.currentMatch.Blue1)
ws.Write("toggleBypass", nil)
assert.Contains(t, readWebsocketError(t, ws), "Failed to parse")
ws.Write("toggleBypass", "R4")
assert.Contains(t, readWebsocketError(t, ws), "Invalid alliance station")
ws.Write("toggleBypass", "R3")
readWebsocketType(t, ws, "status")
assert.Equal(t, true, mainArena.AllianceStations["R3"].Bypass)
ws.Write("toggleBypass", "R3")
readWebsocketType(t, ws, "status")
assert.Equal(t, false, mainArena.AllianceStations["R3"].Bypass)
// Go through match flow.
ws.Write("abortMatch", nil)
assert.Contains(t, readWebsocketError(t, ws), "Cannot abort match")
ws.Write("startMatch", nil)
assert.Contains(t, readWebsocketError(t, ws), "Cannot start match")
mainArena.AllianceStations["R1"].Bypass = true
mainArena.AllianceStations["R2"].Bypass = true
mainArena.AllianceStations["R3"].Bypass = true
mainArena.AllianceStations["B1"].Bypass = true
mainArena.AllianceStations["B2"].Bypass = true
mainArena.AllianceStations["B3"].Bypass = true
ws.Write("startMatch", nil)
readWebsocketType(t, ws, "status")
assert.Equal(t, START_MATCH, mainArena.MatchState)
ws.Write("commitResults", nil)
assert.Contains(t, readWebsocketError(t, ws), "Cannot reset match")
ws.Write("discardResults", nil)
assert.Contains(t, readWebsocketError(t, ws), "Cannot reset match")
ws.Write("abortMatch", nil)
readWebsocketType(t, ws, "status")
assert.Equal(t, POST_MATCH, mainArena.MatchState)
ws.Write("commitResults", nil)
readWebsocketType(t, ws, "reload")
assert.Equal(t, PRE_MATCH, mainArena.MatchState)
ws.Write("discardResults", nil)
readWebsocketType(t, ws, "reload")
assert.Equal(t, PRE_MATCH, mainArena.MatchState)
}
func readWebsocketError(t *testing.T, ws *Websocket) string {
messageType, data, err := ws.Read()
if assert.Nil(t, err) && assert.Equal(t, "error", messageType) {
return data.(string)
}
return "error"
}
func readWebsocketType(t *testing.T, ws *Websocket, expectedMessageType string) {
messageType, _, err := ws.Read()
if assert.Nil(t, err) {
assert.Equal(t, expectedMessageType, messageType)
}
}

View File

@@ -1,3 +1,13 @@
// Bootstrap overrides.
.form-control[disabled] {
cursor: default;
}
.btn[disabled] {
color: #000;
opacity: 0.2;
}
// New styles.
.red-text {
color: #f00;
}
@@ -20,18 +30,27 @@
padding-left: 0;
padding-right: 0;
}
.ds-status, .robot-status, .battery-status, .bypass-button {
.ds-status, .robot-status, .battery-status, .bypass-status {
background-color: #aaa;
color: #000;
border: 1px solid #999;
border-radius: 4px;
padding: 5px;
padding: 0px;
width: 40px;
height: 27px;
margin: 2px;
font-family: Arial;
line-height: 25px;
font-size: 14px;
margin: 0 auto;
}
.bypass-button {
.bypass-status {
cursor: pointer;
}
[data-status-ok="true"] {
background-color: #0e8;
}
}
[data-status-ok="false"] {
background-color: #e66;
}
.btn-match-play {
width: 165px;
}

View File

@@ -0,0 +1,46 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Shared code for initiating websocket connections back to the server for full-duplex communication.
var CheesyWebsocket = function(path, events) {
var that = this;
var url = "ws://" + window.location.hostname;
if (window.location.port != "") {
url += ":" + window.location.port;
}
url += path;
// Insert a default error-handling event if a custom one doesn't already exist.
if (!events.hasOwnProperty("error")) {
events.error = function(event) {
// Data is just an error string.
console.log(event.data);
}
}
// Insert an event to allow the server to force-reload the client.
events.reload = function(event) {
location.reload();
};
this.connect = function() {
this.websocket = $.websocket(url, {
open: function() {
console.log("Websocket connected to the server at " + url + ".")
},
close: function() {
console.log("Websocket lost connection to the server. Reconnecting in 3 seconds...");
setTimeout(that.connect, 3000);
},
events: events
});
};
this.send = function(type, data) {
this.websocket.send(type, data);
};
this.connect();
}

104
static/js/match_play.js Normal file
View File

@@ -0,0 +1,104 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Client-side logic for the match play page.
var websocket;
var matchStates = {
0: "PRE_MATCH",
1: "START_MATCH",
2: "AUTO_PERIOD",
3: "PAUSE_PERIOD",
4: "TELEOP_PERIOD",
5: "ENDGAME_PERIOD",
6: "POST_MATCH"
};
var substituteTeam = function(team, position) {
websocket.send("substituteTeam", { team: parseInt(team), position: position })
};
var toggleBypass = function(station) {
websocket.send("toggleBypass", station);
};
var startMatch = function() {
websocket.send("startMatch");
};
var abortMatch = function() {
websocket.send("abortMatch");
};
var commitResults = function() {
websocket.send("commitResults");
};
var discardResults = function() {
websocket.send("discardResults");
};
var handleStatus = function(data) {
// Update the team status view.
$.each(data.AllianceStations, function(station, stationStatus) {
if (stationStatus.DsConn) {
var dsStatus = stationStatus.DsConn.DriverStationStatus;
$("#status" + station + " .ds-status").attr("data-status-ok", dsStatus.DsLinked);
$("#status" + station + " .robot-status").attr("data-status-ok", dsStatus.RobotLinked);
$("#status" + station + " .battery-status").attr("data-status-ok", dsStatus.BatteryVoltage > 0);
$("#status" + station + " .battery-status").text(dsStatus.BatteryVoltage.toFixed(1) + "V");
} else {
$("#status" + station + " .ds-status").attr("data-status-ok", "");
$("#status" + station + " .robot-status").attr("data-status-ok", "");
$("#status" + station + " .battery-status").attr("data-status-ok", "");
$("#status" + station + " .battery-status").text("");
}
if (stationStatus.EmergencyStop) {
$("#status" + station + " .bypass-status").attr("data-status-ok", false);
$("#status" + station + " .bypass-status").text("ES");
} else if (stationStatus.Bypass) {
$("#status" + station + " .bypass-status").attr("data-status-ok", false);
$("#status" + station + " .bypass-status").text("B");
} else {
$("#status" + station + " .bypass-status").attr("data-status-ok", true);
$("#status" + station + " .bypass-status").text("");
}
});
// Enable/disable the buttons based on the current match state.
switch (matchStates[data.MatchState]) {
case "PRE_MATCH":
$("#startMatch").prop("disabled", !data.CanStartMatch);
$("#abortMatch").prop("disabled", true);
$("#commitResults").prop("disabled", true);
$("#discardResults").prop("disabled", true);
break;
case "START_MATCH":
case "AUTO_PERIOD":
case "PAUSE_PERIOD":
case "TELEOP_PERIOD":
case "ENDGAME_PERIOD":
$("#startMatch").prop("disabled", true);
$("#abortMatch").prop("disabled", false);
$("#commitResults").prop("disabled", true);
$("#discardResults").prop("disabled", true);
break;
case "POST_MATCH":
$("#startMatch").prop("disabled", true);
$("#abortMatch").prop("disabled", true);
$("#commitResults").prop("disabled", false);
$("#discardResults").prop("disabled", false);
break;
}
};
$(function() {
// Activate tooltips above the status headers.
$("[data-toggle=tooltip]").tooltip({"placement": "top"});
// Set up the websocket back to the server.
websocket = new CheesyWebsocket("/match_play/websocket", {
status: function(event) { handleStatus(event.data); }
});
});

23
static/lib/jquery.json-2.4.min.js vendored Normal file
View File

@@ -0,0 +1,23 @@
/*! jQuery JSON plugin 2.4.0 | code.google.com/p/jquery-json */
(function($){'use strict';var escape=/["\\\x00-\x1f\x7f-\x9f]/g,meta={'\b':'\\b','\t':'\\t','\n':'\\n','\f':'\\f','\r':'\\r','"':'\\"','\\':'\\\\'},hasOwn=Object.prototype.hasOwnProperty;$.toJSON=typeof JSON==='object'&&JSON.stringify?JSON.stringify:function(o){if(o===null){return'null';}
var pairs,k,name,val,type=$.type(o);if(type==='undefined'){return undefined;}
if(type==='number'||type==='boolean'){return String(o);}
if(type==='string'){return $.quoteString(o);}
if(typeof o.toJSON==='function'){return $.toJSON(o.toJSON());}
if(type==='date'){var month=o.getUTCMonth()+1,day=o.getUTCDate(),year=o.getUTCFullYear(),hours=o.getUTCHours(),minutes=o.getUTCMinutes(),seconds=o.getUTCSeconds(),milli=o.getUTCMilliseconds();if(month<10){month='0'+month;}
if(day<10){day='0'+day;}
if(hours<10){hours='0'+hours;}
if(minutes<10){minutes='0'+minutes;}
if(seconds<10){seconds='0'+seconds;}
if(milli<100){milli='0'+milli;}
if(milli<10){milli='0'+milli;}
return'"'+year+'-'+month+'-'+day+'T'+
hours+':'+minutes+':'+seconds+'.'+milli+'Z"';}
pairs=[];if($.isArray(o)){for(k=0;k<o.length;k++){pairs.push($.toJSON(o[k])||'null');}
return'['+pairs.join(',')+']';}
if(typeof o==='object'){for(k in o){if(hasOwn.call(o,k)){type=typeof k;if(type==='number'){name='"'+k+'"';}else if(type==='string'){name=$.quoteString(k);}else{continue;}
type=typeof o[k];if(type!=='function'&&type!=='undefined'){val=$.toJSON(o[k]);pairs.push(name+':'+val);}}}
return'{'+pairs.join(',')+'}';}};$.evalJSON=typeof JSON==='object'&&JSON.parse?JSON.parse:function(str){return eval('('+str+')');};$.secureEvalJSON=typeof JSON==='object'&&JSON.parse?JSON.parse:function(str){var filtered=str.replace(/\\["\\\/bfnrtu]/g,'@').replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,']').replace(/(?:^|:|,)(?:\s*\[)+/g,'');if(/^[\],:{}\s]*$/.test(filtered)){return eval('('+str+')');}
throw new SyntaxError('Error parsing JSON, source is not valid.');};$.quoteString=function(str){if(str.match(escape)){return'"'+str.replace(escape,function(a){var c=meta[a];if(typeof c==='string'){return c;}
c=a.charCodeAt();return'\\u00'+Math.floor(c/16).toString(16)+(c%16).toString(16);})+'"';}
return'"'+str+'"';};}(jQuery));

View File

@@ -0,0 +1,45 @@
/*
* jQuery Web Sockets Plugin v0.0.1
* http://code.google.com/p/jquery-websocket/
*
* This document is licensed as free software under the terms of the
* MIT License: http://www.opensource.org/licenses/mit-license.php
*
* Copyright (c) 2010 by shootaroo (Shotaro Tsubouchi).
*/
(function($){
$.extend({
websocketSettings: {
open: function(){},
close: function(){},
message: function(){},
options: {},
events: {}
},
websocket: function(url, s) {
var ws = WebSocket ? new WebSocket( url ) : {
send: function(m){ return false },
close: function(){}
};
ws._settings = $.extend($.websocketSettings, s);
$(ws)
.bind('open', $.websocketSettings.open)
.bind('close', $.websocketSettings.close)
.bind('message', $.websocketSettings.message)
.bind('message', function(e){
var m = $.evalJSON(e.originalEvent.data);
var h = $.websocketSettings.events[m.type];
if (h) h.call(this, m);
});
ws._send = ws.send;
ws.send = function(type, data) {
var m = {type: type};
m = $.extend(true, m, $.extend(true, {}, $.websocketSettings.options, m));
if (data) m['data'] = data;
return this._send($.toJSON(m));
}
$(window).unload(function(){ ws.close(); ws = null });
return ws;
}
});
})(jQuery);

View File

@@ -27,13 +27,6 @@
<li><a href="/setup/alliance_selection">Alliance Selection</a></li>
</ul>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Test</a>
<ul class="dropdown-menu">
<li class="disabled"><a href="#">Field Test</a></li>
<li class="disabled"><a href="#">Match Test</a></li>
</ul>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Run</a>
<ul class="dropdown-menu">
@@ -81,11 +74,14 @@
{{template "body" .}}
</div>
<script src="/static/lib/jquery.min.js"></script>
<script src="/static/lib/jquery.json-2.4.min.js"></script>
<script src="/static/lib/jquery.websocket-0.0.1.js"></script>
<script src="/static/lib/bootstrap.min.js"></script>
<script src="/static/js/bootstrap-colorpicker.min.js"></script>
<script src="/static/js/moment.min.js"></script>
<script src="/static/js/bootstrap-datetimepicker.min.js"></script>
<script src="/static/lib/handlebars-1.3.0.js"></script>
<script src="/static/js/cheesy-websocket.js"></script>
{{template "script" .}}
</body>
</html>

View File

@@ -4,6 +4,8 @@
<h2>Welcome to Cheesy Arena.</h2>
<p>Use the navigation bar at the top to configure the event, play and score matches, view and print reports,
or launch displays.</p>
<p>For ad-hoc match control for testing or scrimmaging, go directly to
<a href="/match_play">Match Play</a>.</p>
</div>
{{end}}
{{define "script"}}{{end}}

View File

@@ -2,6 +2,7 @@
{{define "body"}}
<div class="row">
<div class="col-lg-4">
<a href="/match_play/0/load"><b class="btn btn-info">Load Test Match</b></a><br /><br />
<ul class="nav nav-tabs" style="margin-bottom: 15px;">
<li{{if eq .CurrentMatchType "practice" }} class="active"{{end}}>
<a href="#practice" data-toggle="tab">Practice</a>
@@ -33,8 +34,8 @@
<a href="/match_play/{{$match.Id}}/generate_fake_result">
<b class="btn btn-info btn-xs">Generate Fake Result</b>
</a>
<a href="/match_play/{{$match.Id}}/queue">
<b class="btn btn-info btn-xs">Queue</b>
<a href="/match_play/{{$match.Id}}/load">
<b class="btn btn-info btn-xs">Load</b>
</a>
</td>
</tr>
@@ -55,18 +56,9 @@
<div class="col-lg-2" data-toggle="tooltip" title="Battery">B</div>
<div class="col-lg-2" data-toggle="tooltip" title="Bypass/Disable">Byp</div>
</div>
<div class="row form-group">
<div class="col-lg-1">1 </div>
{{template "matchPlayTeam" .Match.Blue1}}
</div>
<div class="row form-group">
<div class="col-lg-1">2 </div>
{{template "matchPlayTeam" .Match.Blue2}}
</div>
<div class="row form-group">
<div class="col-lg-1">3 </div>
{{template "matchPlayTeam" .Match.Blue3}}
</div>
{{template "matchPlayTeam" dict "team" .Match.Blue1 "color" "B" "position" 1 "data" .}}
{{template "matchPlayTeam" dict "team" .Match.Blue2 "color" "B" "position" 2 "data" .}}
{{template "matchPlayTeam" dict "team" .Match.Blue3 "color" "B" "position" 3 "data" .}}
</div>
<div class="col-lg-6 well well-darkred">
<div class="row form-group">
@@ -76,34 +68,48 @@
<div class="col-lg-2" data-toggle="tooltip" title="Battery">B</div>
<div class="col-lg-2" data-toggle="tooltip" title="Bypass/Disable">Byp</div>
</div>
<div class="row form-group">
<div class="col-lg-1">3 </div>
{{template "matchPlayTeam" .Match.Red3}}
</div>
<div class="row form-group">
<div class="col-lg-1">2 </div>
{{template "matchPlayTeam" .Match.Red2}}
</div>
<div class="row form-group">
<div class="col-lg-1">1 </div>
{{template "matchPlayTeam" .Match.Red1}}
</div>
{{template "matchPlayTeam" dict "team" .Match.Red3 "color" "R" "position" 3 "data" .}}
{{template "matchPlayTeam" dict "team" .Match.Red2 "color" "R" "position" 2 "data" .}}
{{template "matchPlayTeam" dict "team" .Match.Red1 "color" "R" "position" 1 "data" .}}
</div>
</div>
<div>
<button type="button" id="startMatch" class="btn btn-success btn-lg btn-match-play"
onclick="startMatch();" disabled>
Start Match
</button>
<button type="button" id="abortMatch" class="btn btn-primary btn-lg btn-match-play"
onclick="abortMatch();" disabled>
Abort Match
</button>
<button type="button" id="commitResults" class="btn btn-info btn-lg btn-match-play"
onclick="commitResults();" disabled>
Commit Results
</button>
<button type="button" id="discardResults" class="btn btn-danger btn-lg btn-match-play"
onclick="discardResults();" disabled>
Discard Results
</button>
</div>
</div>
</div>
{{end}}
{{define "script"}}
<script>
$("[data-toggle=tooltip]").tooltip({"placement": "top"});
</script>
<script src="/static/js/match_play.js"></script>
{{end}}
{{define "matchPlayTeam"}}
<div class="col-lg-3">
<input type="text" class="form-control input-sm" value="{{.}}">
<div class="row form-group" id="status{{.color}}{{.position}}">
<div class="col-lg-1">{{.position}} </div>
<div class="col-lg-3">
<input type="text" class="form-control input-sm" value="{{if ne 0 .team}}{{.team}}{{end}}"
onblur="substituteTeam($(this).val(), '{{.color}}{{.position}}');"
{{if not .data.AllowSubstitution}}disabled{{end}}>
</div>
<div class="col-lg-2 col-no-padding"><div class="ds-status"></div></div>
<div class="col-lg-2 col-no-padding"><div class="robot-status"></div></div>
<div class="col-lg-2 col-no-padding"><div class="battery-status"></div></div>
<div class="col-lg-2 col-no-padding">
<div class="bypass-status" onclick="toggleBypass('{{.color}}{{.position}}');"></div>
</div>
</div>
<div class="col-lg-2 col-no-padding"><div class="ds-status" id="team{{.}}Ds"></div></div>
<div class="col-lg-2 col-no-padding"><div class="robot-status" id="team{{.}}Robot"></div></div>
<div class="col-lg-2 col-no-padding"><div class="battery-status" id="team{{.}}Battery"></div></div>
<div class="col-lg-2 col-no-padding"><div class="bypass-button" id="team{{.}}Bypass"></div></div>
{{end}}

61
web.go
View File

@@ -8,6 +8,7 @@ package main
import (
"fmt"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
"html/template"
"log"
"net/http"
@@ -15,6 +16,63 @@ import (
const httpPort = 8080
var websocketUpgrader = websocket.Upgrader{ReadBufferSize: 1024, WriteBufferSize: 2014}
// Helper functions that can be used inside templates.
var templateHelpers = template.FuncMap{
// Allows sub-templates to be invoked with multiple arguments.
"dict": func(values ...interface{}) (map[string]interface{}, error) {
if len(values)%2 != 0 {
return nil, fmt.Errorf("Invalid dict call.")
}
dict := make(map[string]interface{}, len(values)/2)
for i := 0; i < len(values); i += 2 {
key, ok := values[i].(string)
if !ok {
return nil, fmt.Errorf("Dict keys must be strings.")
}
dict[key] = values[i+1]
}
return dict, nil
},
}
// Wraps the Gorilla Websocket module for convenience.
type Websocket struct {
conn *websocket.Conn
}
type WebsocketMessage struct {
Type string `json:"type"`
Data interface{} `json:"data"`
}
func NewWebsocket(w http.ResponseWriter, r *http.Request) (*Websocket, error) {
conn, err := websocketUpgrader.Upgrade(w, r, nil)
if err != nil {
return nil, err
}
return &Websocket{conn}, nil
}
func (websocket *Websocket) Close() {
websocket.conn.Close()
}
func (websocket *Websocket) Read() (string, interface{}, error) {
var message WebsocketMessage
err := websocket.conn.ReadJSON(&message)
return message.Type, message.Data, err
}
func (websocket *Websocket) Write(messageType string, data interface{}) error {
return websocket.conn.WriteJSON(WebsocketMessage{messageType, data})
}
func (websocket *Websocket) WriteError(errorMessage string) error {
return websocket.conn.WriteJSON(WebsocketMessage{"error", errorMessage})
}
func IndexHandler(w http.ResponseWriter, r *http.Request) {
template, err := template.ParseFiles("templates/index.html", "templates/base.html")
if err != nil {
@@ -62,8 +120,9 @@ func newHandler() http.Handler {
router.HandleFunc("/setup/alliance_selection/reset", AllianceSelectionResetHandler).Methods("POST")
router.HandleFunc("/setup/alliance_selection/finalize", AllianceSelectionFinalizeHandler).Methods("POST")
router.HandleFunc("/match_play", MatchPlayHandler).Methods("GET")
router.HandleFunc("/match_play/{matchId}/queue", MatchPlayQueueHandler).Methods("GET")
router.HandleFunc("/match_play/{matchId}/load", MatchPlayLoadHandler).Methods("GET")
router.HandleFunc("/match_play/{matchId}/generate_fake_result", MatchPlayFakeResultHandler).Methods("GET")
router.HandleFunc("/match_play/websocket", MatchPlayWebsocketHandler).Methods("GET")
router.HandleFunc("/match_review", MatchReviewHandler).Methods("GET")
router.HandleFunc("/match_review/{matchId}/edit", MatchReviewEditGetHandler).Methods("GET")
router.HandleFunc("/match_review/{matchId}/edit", MatchReviewEditPostHandler).Methods("POST")

View File

@@ -39,3 +39,9 @@ func postHttpResponse(path string, body string) *httptest.ResponseRecorder {
newHandler().ServeHTTP(recorder, req)
return recorder
}
// Starts a real local HTTP server that can be used by more sophisticated tests.
func startTestServer() (*httptest.Server, string) {
server := httptest.NewServer(newHandler())
return server, "ws" + server.URL[len("http"):]
}