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