mirror of
https://github.com/Team254/cheesy-arena-lite.git
synced 2026-03-09 21:56:50 -04:00
Implemented websocket interface to match play screen.
This commit is contained in:
186
arena.go
186
arena.go
@@ -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()
|
||||
|
||||
333
arena_test.go
333
arena_test.go
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
191
match_play.go
191
match_play.go
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
46
static/js/cheesy-websocket.js
Normal file
46
static/js/cheesy-websocket.js
Normal 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
104
static/js/match_play.js
Normal 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
23
static/lib/jquery.json-2.4.min.js
vendored
Normal 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));
|
||||
45
static/lib/jquery.websocket-0.0.1.js
Normal file
45
static/lib/jquery.websocket-0.0.1.js
Normal 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);
|
||||
@@ -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>
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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
61
web.go
@@ -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")
|
||||
|
||||
@@ -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"):]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user