diff --git a/field/arena.go b/field/arena.go index 7d4a24ff..19b7a949 100755 --- a/field/arena.go +++ b/field/arena.go @@ -14,12 +14,13 @@ import ( "github.com/FRCTeam1987/crimson-arena/partner" "github.com/FRCTeam1987/crimson-arena/plc" "log" + "reflect" "time" ) const ( arenaLoopPeriodMs = 10 - dsPacketPeriodMs = 250 + dsPacketPeriodMs = 500 periodicTaskPeriodSec = 30 matchEndScoreDwellSec = 3 postTimeoutSec = 4 @@ -48,7 +49,6 @@ type Arena struct { Database *model.Database EventSettings *model.EventSettings accessPoint network.AccessPoint - accessPoint2 network.AccessPoint networkSwitch *network.Switch dnsMasq *network.DnsMasq Plc plc.Plc @@ -68,7 +68,6 @@ type Arena struct { lastDsPacketTime time.Time lastPeriodicTaskTime time.Time EventStatus EventStatus - FieldVolunteers bool FieldReset bool AudienceDisplayMode string SavedMatch *model.Match @@ -82,15 +81,17 @@ type Arena struct { MuteMatchSounds bool matchAborted bool soundsPlayed map[*game.MatchSound]struct{} + preloadedTeams *[6]*model.Team } type AllianceStation struct { - DsConn *DriverStationConnection - Ethernet bool - Astop bool - Estop bool - Bypass bool - Team *model.Team + DsConn *DriverStationConnection + Ethernet bool + Astop bool + Estop bool + Bypass bool + Team *model.Team + WifiStatus network.TeamWifiStatus } // Creates the arena and sets it to its initial state. @@ -98,16 +99,6 @@ func NewArena(dbPath string) (*Arena, error) { arena := new(Arena) arena.configureNotifiers() - var err error - arena.Database, err = model.OpenDatabase(dbPath) - if err != nil { - return nil, err - } - err = arena.LoadSettings() - if err != nil { - return nil, err - } - arena.AllianceStations = make(map[string]*AllianceStation) arena.AllianceStations["R1"] = new(AllianceStation) arena.AllianceStations["R2"] = new(AllianceStation) @@ -118,6 +109,16 @@ func NewArena(dbPath string) (*Arena, error) { arena.Displays = make(map[string]*Display) + var err error + arena.Database, err = model.OpenDatabase(dbPath) + if err != nil { + return nil, err + } + err = arena.LoadSettings() + if err != nil { + return nil, err + } + // Load empty match as current. arena.MatchState = PreMatch arena.LoadTestMatch() @@ -148,24 +149,27 @@ func (arena *Arena) LoadSettings() error { arena.EventSettings = settings // Initialize the components that depend on settings. - arena.accessPoint.SetSettings(settings.ApAddress, settings.ApUsername, settings.ApPassword, - settings.ApTeamChannel, settings.ApAdminChannel, settings.ApAdminWpaKey, settings.NetworkSecurityEnabled) - arena.accessPoint2.SetSettings(settings.Ap2Address, settings.Ap2Username, settings.Ap2Password, - settings.Ap2TeamChannel, 0, "", settings.NetworkSecurityEnabled) + accessPointWifiStatuses := [6]*network.TeamWifiStatus{ + &arena.AllianceStations["R1"].WifiStatus, + &arena.AllianceStations["R2"].WifiStatus, + &arena.AllianceStations["R3"].WifiStatus, + &arena.AllianceStations["B1"].WifiStatus, + &arena.AllianceStations["B2"].WifiStatus, + &arena.AllianceStations["B3"].WifiStatus, + } + + arena.accessPoint.SetSettings( + settings.ApAddress, + settings.ApPassword, + settings.ApChannel, + settings.NetworkSecurityEnabled, + accessPointWifiStatuses, + ) arena.networkSwitch = network.NewSwitch(settings.SwitchAddress, settings.SwitchPassword) arena.dnsMasq = network.NewDnsMasq() arena.Plc.SetAddress(settings.PlcAddress) arena.TbaClient = partner.NewTbaClient(settings.TbaEventCode, settings.TbaSecretId, settings.TbaSecret) - if arena.EventSettings.NetworkSecurityEnabled && arena.MatchState == PreMatch { - if err = arena.accessPoint.ConfigureAdminWifi(); err != nil { - log.Printf("Failed to configure admin WiFi: %s", err.Error()) - } - if err = arena.accessPoint2.ConfigureAdminWifi(); err != nil { - log.Printf("Failed to configure admin WiFi: %s", err.Error()) - } - } - game.MatchTiming.WarmupDurationSec = settings.WarmupDurationSec game.MatchTiming.AutoDurationSec = settings.AutoDurationSec game.MatchTiming.PauseDurationSec = settings.PauseDurationSec @@ -194,7 +198,7 @@ func (arena *Arena) CreatePlayoffBracket() error { case "double": arena.PlayoffBracket, err = bracket.NewDoubleEliminationBracket(arena.EventSettings.NumElimAlliances) default: - err = fmt.Errorf("Invalid playoff type: %v", arena.EventSettings.ElimType) + err = fmt.Errorf("invalid playoff type: %v", arena.EventSettings.ElimType) } return err } @@ -215,7 +219,7 @@ func (arena *Arena) UpdatePlayoffBracket(startTime *time.Time) error { // Sets up the arena for the given match. func (arena *Arena) LoadMatch(match *model.Match) error { if arena.MatchState != PreMatch { - return fmt.Errorf("Cannot load match while there is a match still in progress or with results pending.") + return fmt.Errorf("cannot load match while there is a match still in progress or with results pending") } arena.CurrentMatch = match @@ -244,15 +248,22 @@ func (arena *Arena) LoadMatch(match *model.Match) error { return err } - arena.setupNetwork([6]*model.Team{arena.AllianceStations["R1"].Team, arena.AllianceStations["R2"].Team, - arena.AllianceStations["R3"].Team, arena.AllianceStations["B1"].Team, arena.AllianceStations["B2"].Team, - arena.AllianceStations["B3"].Team}) + arena.setupNetwork( + [6]*model.Team{ + arena.AllianceStations["R1"].Team, + arena.AllianceStations["R2"].Team, + arena.AllianceStations["R3"].Team, + arena.AllianceStations["B1"].Team, + arena.AllianceStations["B2"].Team, + arena.AllianceStations["B3"].Team, + }, + false, + ) // Reset the arena state and game scores. arena.soundsPlayed = make(map[*game.MatchSound]struct{}) arena.RedScore = new(game.Score) arena.BlueScore = new(game.Score) - arena.FieldVolunteers = false arena.FieldReset = false arena.Plc.ResetMatch() @@ -285,7 +296,7 @@ func (arena *Arena) LoadNextMatch() error { // 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.ShouldAllowSubstitution() { - return fmt.Errorf("Can't substitute teams for qualification matches.") + return fmt.Errorf("can't substitute teams for qualification matches") } err := arena.assignTeam(teamId, station) if err != nil { @@ -305,9 +316,17 @@ func (arena *Arena) SubstituteTeam(teamId int, station string) error { case "B3": arena.CurrentMatch.Blue3 = teamId } - arena.setupNetwork([6]*model.Team{arena.AllianceStations["R1"].Team, arena.AllianceStations["R2"].Team, - arena.AllianceStations["R3"].Team, arena.AllianceStations["B1"].Team, arena.AllianceStations["B2"].Team, - arena.AllianceStations["B3"].Team}) + arena.setupNetwork( + [6]*model.Team{ + arena.AllianceStations["R1"].Team, + arena.AllianceStations["R2"].Team, + arena.AllianceStations["R3"].Team, + arena.AllianceStations["B1"].Team, + arena.AllianceStations["B2"].Team, + arena.AllianceStations["B3"].Team, + }, + false, + ) arena.MatchLoadNotifier.Notify() if arena.CurrentMatch.Type != "test" { @@ -330,7 +349,7 @@ func (arena *Arena) StartMatch() error { // Save the missed packet count to subtract it from the running count. for _, allianceStation := range arena.AllianceStations { if allianceStation.DsConn != nil { - err = allianceStation.DsConn.signalMatchStart(arena.CurrentMatch) + err = allianceStation.DsConn.signalMatchStart(arena.CurrentMatch, &allianceStation.WifiStatus) if err != nil { log.Println(err) } @@ -352,7 +371,7 @@ func (arena *Arena) StartMatch() error { // Kills the current match or timeout if it is underway. func (arena *Arena) AbortMatch() error { if arena.MatchState == PreMatch || arena.MatchState == PostMatch || arena.MatchState == PostTimeout { - return fmt.Errorf("Cannot abort match when it is not in progress.") + return fmt.Errorf("cannot abort match when it is not in progress") } if arena.MatchState == TimeoutActive { @@ -376,7 +395,7 @@ func (arena *Arena) AbortMatch() error { // Clears out the match and resets the arena state unless there is a match underway. func (arena *Arena) ResetMatch() error { if arena.MatchState != PostMatch && arena.MatchState != PreMatch { - return fmt.Errorf("Cannot reset match while it is in progress.") + return fmt.Errorf("cannot reset match while it is in progress") } arena.MatchState = PreMatch arena.matchAborted = false @@ -393,7 +412,7 @@ func (arena *Arena) ResetMatch() error { // Starts a timeout of the given duration. func (arena *Arena) StartTimeout(durationSec int) error { if arena.MatchState != PreMatch { - return fmt.Errorf("Cannot start timeout while there is a match still in progress or with results pending.") + return fmt.Errorf("cannot start timeout while there is a match still in progress or with results pending") } game.MatchTiming.TimeoutDurationSec = durationSec @@ -491,6 +510,7 @@ func (arena *Arena) Update() { enabled = true } } + arena.FieldReset = false case PausePeriod: auto = false enabled = false @@ -525,6 +545,7 @@ func (arena *Arena) Update() { arena.preLoadNextMatch() }() } + arena.FieldReset = false case TimeoutActive: if matchTimeSec >= float64(game.MatchTiming.TimeoutDurationSec) { arena.MatchState = PostTimeout @@ -570,7 +591,6 @@ func (arena *Arena) Run() { go arena.listenForDriverStations() go arena.listenForDsUdpPackets() go arena.accessPoint.Run() - go arena.accessPoint2.Run() go arena.Plc.Run() for { @@ -597,7 +617,7 @@ func (arena *Arena) BlueScoreSummary() *game.ScoreSummary { 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) + return fmt.Errorf("invalid alliance station '%s'", station) } // Do nothing if the station is already assigned to the requested team. @@ -675,27 +695,25 @@ func (arena *Arena) preLoadNextMatch() { log.Printf("Failed to get model for Team %d while pre-loading next match: %s", teamId, err.Error()) } } - arena.setupNetwork(teams) + arena.setupNetwork(teams, true) } // Asynchronously reconfigures the networking hardware for the new set of teams. -func (arena *Arena) setupNetwork(teams [6]*model.Team) { +func (arena *Arena) setupNetwork(teams [6]*model.Team, isPreload bool) { + if isPreload { + arena.preloadedTeams = &teams + } else if arena.preloadedTeams != nil { + preloadedTeams := *arena.preloadedTeams + arena.preloadedTeams = nil + if reflect.DeepEqual(teams, preloadedTeams) { + // Skip configuring the network; this is the same set of teams that was preloaded. + return + } + } + if arena.EventSettings.NetworkSecurityEnabled { - if arena.EventSettings.Ap2TeamChannel == 0 { - // Only one AP is being used. - if err := arena.accessPoint.ConfigureTeamWifi(teams); err != nil { - log.Printf("Failed to configure team WiFi: %s", err.Error()) - } - } else { - // Two APs are being used. Configure the first for the red teams and the second for the blue teams. - if err := arena.accessPoint.ConfigureTeamWifi([6]*model.Team{teams[0], teams[1], teams[2], nil, nil, - nil}); err != nil { - log.Printf("Failed to configure red alliance WiFi: %s", err.Error()) - } - if err := arena.accessPoint2.ConfigureTeamWifi([6]*model.Team{nil, nil, nil, teams[3], teams[4], - teams[5]}); err != nil { - log.Printf("Failed to configure blue alliance WiFi: %s", err.Error()) - } + if err := arena.accessPoint.ConfigureTeamWifi(teams); err != nil { + log.Printf("Failed to configure team Wi-Fi: %s", err.Error()) } go func() { if err := arena.networkSwitch.ConfigureTeamEthernet(teams); err != nil { @@ -711,7 +729,7 @@ func (arena *Arena) setupNetwork(teams [6]*model.Team) { // Returns nil if the match can be started, and an error otherwise. func (arena *Arena) checkCanStartMatch() error { if arena.MatchState != PreMatch { - return fmt.Errorf("Cannot start match while there is a match still in progress or with results pending.") + return fmt.Errorf("cannot start match while there is a match still in progress or with results pending") } err := arena.checkAllianceStationsReady("R1", "R2", "R3", "B1", "B2", "B3") @@ -726,14 +744,14 @@ func (arena *Arena) checkCanStartMatch() error { if arena.Plc.IsEnabled() { if !arena.Plc.IsHealthy { - return fmt.Errorf("Cannot start match while PLC is not healthy.") + return fmt.Errorf("cannot start match while PLC is not healthy") } if arena.Plc.GetFieldEstop() { - return fmt.Errorf("Cannot start match while field emergency stop is active.") + return fmt.Errorf("cannot start match while field emergency stop is active") } for name, status := range arena.Plc.GetArmorBlockStatuses() { if !status { - return fmt.Errorf("Cannot start match while PLC ArmorBlock '%s' is not connected.", name) + return fmt.Errorf("cannot start match while PLC ArmorBlock '%s' is not connected", name) } } } @@ -745,19 +763,19 @@ func (arena *Arena) checkAllianceStationsReady(stations ...string) error { for _, station := range stations { allianceStation := arena.AllianceStations[station] if allianceStation.Estop { - return fmt.Errorf("Cannot start match while an emergency stop is active.") + return fmt.Errorf("cannot start match while an emergency stop is active") } if !allianceStation.Bypass { if allianceStation.DsConn == nil || !allianceStation.DsConn.RobotLinked { - return fmt.Errorf("Cannot start match until all robots are connected or bypassed.") + return fmt.Errorf("cannot start match until all robots are connected or bypassed") } if station[0] == 'R' { if !arena.Scc.IsSccConnected("red") { - return fmt.Errorf("Cannot start match without red alliance SCC connected") + return fmt.Errorf("cannot start match without red alliance SCC connected") } } else if station[0] == 'B' { if !arena.Scc.IsSccConnected("blue") { - return fmt.Errorf("Cannot start match without blue alliance SCC connected") + return fmt.Errorf("cannot start match without blue alliance SCC connected") } } } @@ -770,7 +788,7 @@ func (arena *Arena) checkSccEstops() error { for alliance, status := range arena.Scc.status { for i := range status.EStops { if status.EStops[i] { - return fmt.Errorf("Cannot start match with %s %d emergency stop active", + return fmt.Errorf("cannot start match with %s %d emergency stop active", alliance, i+1) } } @@ -837,6 +855,9 @@ func (arena *Arena) handlePlcInput() { // Updates the PLC's coils based on its inputs and the current scoring state. func (arena *Arena) handlePlcOutput() { + redAllianceReady := arena.checkAllianceStationsReady("R1", "R2", "R3") == nil + blueAllianceReady := arena.checkAllianceStationsReady("B1", "B2", "B3") == nil + switch arena.MatchState { case PreMatch: if arena.lastMatchState != PreMatch { @@ -848,8 +869,6 @@ func (arena *Arena) handlePlcOutput() { case PostTimeout: // Set the stack light state -- solid alliance color(s) if robots are not connected, solid orange if scores are // not input, or blinking green if ready. - redAllianceReady := arena.checkAllianceStationsReady("R1", "R2", "R3") == nil - blueAllianceReady := arena.checkAllianceStationsReady("B1", "B2", "B3") == nil greenStackLight := redAllianceReady && blueAllianceReady && arena.Plc.GetCycleState(2, 0, 2) arena.Plc.SetStackLights(!redAllianceReady, !blueAllianceReady, false, greenStackLight) arena.Plc.SetStackBuzzer(redAllianceReady && blueAllianceReady) @@ -874,10 +893,13 @@ func (arena *Arena) handlePlcOutput() { }() } case AutoPeriod: + arena.Plc.SetStackBuzzer(false) + arena.Plc.SetStackLights(!redAllianceReady, !blueAllianceReady, false, true) fallthrough case PausePeriod: fallthrough case TeleopPeriod: + arena.Plc.SetStackLights(!redAllianceReady, !blueAllianceReady, false, true) } } @@ -908,7 +930,7 @@ func (arena *Arena) handleEstop(station string, state bool) { } func (arena *Arena) handleSounds(matchTimeSec float64) { - if arena.MatchState == PreMatch { + if arena.MatchState == PreMatch || arena.MatchState == TimeoutActive || arena.MatchState == PostTimeout { // Only apply this logic during a match. return } @@ -918,11 +940,6 @@ func (arena *Arena) handleSounds(matchTimeSec float64) { // Skip sounds with negative timestamps; they are meant to only be triggered explicitly. continue } - if sound.Timeout && !(arena.MatchState == TimeoutActive || arena.MatchState == PostTimeout) || - !sound.Timeout && (arena.MatchState == TimeoutActive || arena.MatchState == PostTimeout) { - // Skip timeout sounds if this is a regular match, and vice versa. - continue - } if _, ok := arena.soundsPlayed[sound]; !ok { if matchTimeSec > sound.MatchTimeSec && matchTimeSec-sound.MatchTimeSec < 1 { arena.playSound(sound.Name) diff --git a/field/arena_notifiers.go b/field/arena_notifiers.go index 48edf0e5..d4bf7a1d 100755 --- a/field/arena_notifiers.go +++ b/field/arena_notifiers.go @@ -9,7 +9,6 @@ import ( "github.com/FRCTeam1987/crimson-arena/bracket" "github.com/FRCTeam1987/crimson-arena/game" "github.com/FRCTeam1987/crimson-arena/model" - "github.com/FRCTeam1987/crimson-arena/network" "github.com/FRCTeam1987/crimson-arena/websocket" "strconv" ) @@ -66,57 +65,48 @@ func (arena *Arena) configureNotifiers() { arena.SCCNotifier = websocket.NewNotifier("sccstatus", arena.generateSCCStatusMessage) } -func (arena *Arena) generateAllianceSelectionMessage() interface{} { +func (arena *Arena) generateAllianceSelectionMessage() any { return &arena.AllianceSelectionAlliances } -func (arena *Arena) generateAllianceStationDisplayModeMessage() interface{} { +func (arena *Arena) generateAllianceStationDisplayModeMessage() any { return arena.AllianceStationDisplayMode } -func (arena *Arena) generateArenaStatusMessage() interface{} { - // Convert AP team wifi network status array to a map by station for ease of client use. - teamWifiStatuses := make(map[string]network.TeamWifiStatus) - for i, station := range []string{"R1", "R2", "R3", "B1", "B2", "B3"} { - if arena.EventSettings.Ap2TeamChannel == 0 || i < 3 { - teamWifiStatuses[station] = arena.accessPoint.TeamWifiStatuses[i] - } else { - teamWifiStatuses[station] = arena.accessPoint2.TeamWifiStatuses[i] - } - } - - startMatchErr := arena.checkCanStartMatch() - startMatchErrString := "" - if startMatchErr != nil { - startMatchErrString = startMatchErr.Error() - } +func (arena *Arena) generateArenaStatusMessage() any { return &struct { MatchId int AllianceStations map[string]*AllianceStation - TeamWifiStatuses map[string]network.TeamWifiStatus MatchState CanStartMatch bool - CanStartMatchReason string + AccessPointStatus string + SwitchStart string PlcIsHealthy bool FieldEstop bool PlcArmorBlockStatuses map[string]bool ScoringSccConnected bool RedSccConnected bool BlueSccConnected bool - }{arena.CurrentMatch.Id, arena.AllianceStations, teamWifiStatuses, arena.MatchState, - startMatchErr == nil, startMatchErrString, - arena.Plc.IsHealthy, arena.Plc.GetFieldEstop(), + }{ + arena.CurrentMatch.Id, + arena.AllianceStations, + arena.MatchState, + arena.checkCanStartMatch() == nil, + arena.accessPoint.Status, + arena.networkSwitch.Status, + arena.Plc.IsHealthy, + arena.Plc.GetFieldEstop(), arena.Plc.GetArmorBlockStatuses(), arena.Scc.IsSccConnected("scoring"), arena.Scc.IsSccConnected("red"), arena.Scc.IsSccConnected("blue")} } -func (arena *Arena) generateAudienceDisplayModeMessage() interface{} { +func (arena *Arena) generateAudienceDisplayModeMessage() any { return arena.AudienceDisplayMode } -func (arena *Arena) generateDisplayConfigurationMessage() interface{} { +func (arena *Arena) generateDisplayConfigurationMessage() any { // Notify() for this notifier must always called from a method that has a lock on the display mutex. // Make a copy of the map to avoid potential data races; otherwise the same map would get iterated through as it is // serialized to JSON, outside the mutex lock. @@ -127,18 +117,18 @@ func (arena *Arena) generateDisplayConfigurationMessage() interface{} { return displaysCopy } -func (arena *Arena) generateEventStatusMessage() interface{} { +func (arena *Arena) generateEventStatusMessage() any { return arena.EventStatus } -func (arena *Arena) generateLowerThirdMessage() interface{} { +func (arena *Arena) generateLowerThirdMessage() any { return &struct { LowerThird *model.LowerThird ShowLowerThird bool }{arena.LowerThird, arena.ShowLowerThird} } -func (arena *Arena) generateMatchLoadMessage() interface{} { +func (arena *Arena) generateMatchLoadMessage() any { teams := make(map[string]*model.Team) for station, allianceStation := range arena.AllianceStations { teams[station] = allianceStation.Team @@ -187,15 +177,15 @@ func (arena *Arena) generateMatchLoadMessage() interface{} { } } -func (arena *Arena) generateMatchTimeMessage() interface{} { +func (arena *Arena) generateMatchTimeMessage() any { return MatchTimeMessage{arena.MatchState, int(arena.MatchTimeSec())} } -func (arena *Arena) generateMatchTimingMessage() interface{} { +func (arena *Arena) generateMatchTimingMessage() any { return &game.MatchTiming } -func (arena *Arena) generateRealtimeScoreMessage() interface{} { +func (arena *Arena) generateRealtimeScoreMessage() any { fields := struct { Red *audienceAllianceScoreFields Blue *audienceAllianceScoreFields @@ -207,11 +197,11 @@ func (arena *Arena) generateRealtimeScoreMessage() interface{} { return &fields } -func (arena *Arena) generateSCCStatusMessage() interface{} { +func (arena *Arena) generateSCCStatusMessage() any { return arena.Scc.GenerateNotifierStatus() } -func (arena *Arena) generateScorePostedMessage() interface{} { +func (arena *Arena) generateScorePostedMessage() any { // For elimination matches, summarize the state of the series. var seriesStatus, seriesLeader string var matchup *bracket.Matchup @@ -244,7 +234,7 @@ func (arena *Arena) generateScorePostedMessage() interface{} { } } -func (arena *Arena) generateFieldLightsMessage() interface{} { +func (arena *Arena) generateFieldLightsMessage() any { return &struct { Lights string }{arena.FieldLights.GetCurrentStateAsString()} diff --git a/field/arena_test.go b/field/arena_test.go index 52cead82..817f0212 100644 --- a/field/arena_test.go +++ b/field/arena_test.go @@ -59,7 +59,7 @@ func TestArenaCheckCanStartMatch(t *testing.T) { // Check robot state constraints. err := arena.checkCanStartMatch() if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot start match until all robots are connected or bypassed") + assert.Contains(t, err.Error(), "cannot start match until all robots are connected or bypassed") } arena.AllianceStations["R1"].Bypass = true arena.AllianceStations["R2"].Bypass = true @@ -68,7 +68,7 @@ func TestArenaCheckCanStartMatch(t *testing.T) { arena.AllianceStations["B2"].Bypass = true err = arena.checkCanStartMatch() if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot start match until all robots are connected or bypassed") + assert.Contains(t, err.Error(), "cannot start match until all robots are connected or bypassed") } arena.AllianceStations["B3"].Bypass = true assert.Nil(t, arena.checkCanStartMatch()) @@ -77,7 +77,7 @@ func TestArenaCheckCanStartMatch(t *testing.T) { arena.Plc.SetAddress("1.2.3.4") err = arena.checkCanStartMatch() if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot start match while PLC is not healthy") + assert.Contains(t, err.Error(), "cannot start match while PLC is not healthy") } arena.Plc.SetAddress("") assert.Nil(t, arena.checkCanStartMatch()) @@ -102,7 +102,7 @@ func TestArenaMatchFlow(t *testing.T) { arena.lastDsPacketTime = arena.lastDsPacketTime.Add(-10 * time.Millisecond) arena.Update() assert.Equal(t, lastPacketCount, arena.AllianceStations["B3"].DsConn.packetCount) - arena.lastDsPacketTime = arena.lastDsPacketTime.Add(-300 * time.Millisecond) + arena.lastDsPacketTime = arena.lastDsPacketTime.Add(-550 * time.Millisecond) arena.Update() assert.Equal(t, lastPacketCount+1, arena.AllianceStations["B3"].DsConn.packetCount) @@ -155,25 +155,25 @@ func TestArenaMatchFlow(t *testing.T) { // Check e-stop and bypass. arena.AllianceStations["B3"].Estop = true - arena.lastDsPacketTime = arena.lastDsPacketTime.Add(-300 * time.Millisecond) + arena.lastDsPacketTime = arena.lastDsPacketTime.Add(-550 * time.Millisecond) arena.Update() assert.Equal(t, TeleopPeriod, arena.MatchState) assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Auto) assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Enabled) arena.AllianceStations["B3"].Bypass = true - arena.lastDsPacketTime = arena.lastDsPacketTime.Add(-300 * time.Millisecond) + arena.lastDsPacketTime = arena.lastDsPacketTime.Add(-550 * time.Millisecond) arena.Update() assert.Equal(t, TeleopPeriod, arena.MatchState) assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Auto) assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Enabled) arena.AllianceStations["B3"].Estop = false - arena.lastDsPacketTime = arena.lastDsPacketTime.Add(-300 * time.Millisecond) + arena.lastDsPacketTime = arena.lastDsPacketTime.Add(-550 * time.Millisecond) arena.Update() assert.Equal(t, TeleopPeriod, arena.MatchState) assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Auto) assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Enabled) arena.AllianceStations["B3"].Bypass = false - arena.lastDsPacketTime = arena.lastDsPacketTime.Add(-300 * time.Millisecond) + arena.lastDsPacketTime = arena.lastDsPacketTime.Add(-550 * time.Millisecond) arena.Update() assert.Equal(t, TeleopPeriod, arena.MatchState) assert.Equal(t, false, arena.AllianceStations["B3"].DsConn.Auto) @@ -194,7 +194,7 @@ func TestArenaMatchFlow(t *testing.T) { arena.AllianceStations["R1"].Bypass = true arena.ResetMatch() - arena.lastDsPacketTime = arena.lastDsPacketTime.Add(-300 * time.Millisecond) + arena.lastDsPacketTime = arena.lastDsPacketTime.Add(-550 * time.Millisecond) arena.Update() assert.Equal(t, PreMatch, arena.MatchState) assert.Equal(t, true, arena.AllianceStations["B3"].DsConn.Auto) @@ -216,73 +216,73 @@ func TestArenaStateEnforcement(t *testing.T) { assert.Nil(t, err) err = arena.AbortMatch() if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot abort match when") + assert.Contains(t, err.Error(), "cannot abort match when") } err = arena.StartMatch() assert.Nil(t, err) err = arena.LoadMatch(new(model.Match)) if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot load match while") + assert.Contains(t, err.Error(), "cannot load match while") } err = arena.StartMatch() if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot start match while") + assert.Contains(t, err.Error(), "cannot start match while") } err = arena.ResetMatch() if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot reset match while") + assert.Contains(t, err.Error(), "cannot reset match while") } arena.MatchState = AutoPeriod err = arena.LoadMatch(new(model.Match)) if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot load match while") + assert.Contains(t, err.Error(), "cannot load match while") } err = arena.StartMatch() if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot start match while") + assert.Contains(t, err.Error(), "cannot start match while") } err = arena.ResetMatch() if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot reset match while") + assert.Contains(t, err.Error(), "cannot reset match while") } arena.MatchState = PausePeriod err = arena.LoadMatch(new(model.Match)) if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot load match while") + assert.Contains(t, err.Error(), "cannot load match while") } err = arena.StartMatch() if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot start match while") + assert.Contains(t, err.Error(), "cannot start match while") } err = arena.ResetMatch() if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot reset match while") + assert.Contains(t, err.Error(), "cannot reset match while") } arena.MatchState = TeleopPeriod err = arena.LoadMatch(new(model.Match)) if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot load match while") + assert.Contains(t, err.Error(), "cannot load match while") } err = arena.StartMatch() if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot start match while") + assert.Contains(t, err.Error(), "cannot start match while") } err = arena.ResetMatch() if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot reset match while") + assert.Contains(t, err.Error(), "cannot reset match while") } arena.MatchState = PostMatch err = arena.LoadMatch(new(model.Match)) if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot load match while") + assert.Contains(t, err.Error(), "cannot load match while") } err = arena.StartMatch() if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot start match while") + assert.Contains(t, err.Error(), "cannot start match while") } err = arena.AbortMatch() if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Cannot abort match when") + assert.Contains(t, err.Error(), "cannot abort match when") } err = arena.ResetMatch() diff --git a/field/display.go b/field/display.go index b3c3c1a6..1d764e78 100644 --- a/field/display.go +++ b/field/display.go @@ -139,7 +139,7 @@ func (display *Display) ToUrl() string { return builder.String() } -func (display *Display) generateDisplayConfigurationMessage() interface{} { +func (display *Display) generateDisplayConfigurationMessage() any { return display.ToUrl() } diff --git a/field/driver_station_connection.go b/field/driver_station_connection.go index 21f26b28..b3e37b1e 100644 --- a/field/driver_station_connection.go +++ b/field/driver_station_connection.go @@ -36,6 +36,7 @@ type DriverStationConnection struct { Estop bool DsLinked bool RadioLinked bool + RioLinked bool RobotLinked bool BatteryVoltage float64 DsRobotTripTimeMs int @@ -99,6 +100,7 @@ func (arena *Arena) listenForDsUdpPackets() { dsConn.DsLinked = true dsConn.lastPacketTime = time.Now() + dsConn.RioLinked = data[3]&0x08 != 0 dsConn.RadioLinked = data[3]&0x10 != 0 dsConn.RobotLinked = data[3]&0x20 != 0 if dsConn.RobotLinked { @@ -120,6 +122,7 @@ func (dsConn *DriverStationConnection) update(arena *Arena) error { if time.Since(dsConn.lastPacketTime).Seconds() > driverStationUdpLinkTimeoutSec { dsConn.DsLinked = false + dsConn.RioLinked = false dsConn.RadioLinked = false dsConn.RobotLinked = false dsConn.BatteryVoltage = 0 @@ -142,11 +145,11 @@ func (dsConn *DriverStationConnection) close() { } // Called at the start of the match to allow for driver station initialization. -func (dsConn *DriverStationConnection) signalMatchStart(match *model.Match) error { +func (dsConn *DriverStationConnection) signalMatchStart(match *model.Match, wifiStatus *network.TeamWifiStatus) error { // Zero out missed packet count and begin logging. dsConn.missedPacketOffset = dsConn.MissedPacketCount var err error - dsConn.log, err = NewTeamMatchLog(dsConn.TeamId, match) + dsConn.log, err = NewTeamMatchLog(dsConn.TeamId, match, wifiStatus) return err } @@ -223,15 +226,9 @@ func (dsConn *DriverStationConnection) encodeControlPacket(arena *Arena) [22]byt // Remaining number of seconds in match. var matchSecondsRemaining int switch arena.MatchState { - case PreMatch: - fallthrough - case TimeoutActive: - fallthrough - case PostTimeout: + case PreMatch, TimeoutActive, PostTimeout: matchSecondsRemaining = game.MatchTiming.AutoDurationSec - case StartMatch: - fallthrough - case AutoPeriod: + case StartMatch, AutoPeriod: matchSecondsRemaining = game.MatchTiming.AutoDurationSec - int(arena.MatchTimeSec()) case PausePeriod: matchSecondsRemaining = game.MatchTiming.TeleopDurationSec diff --git a/field/team_match_log.go b/field/team_match_log.go index 2dff53a2..d0358930 100644 --- a/field/team_match_log.go +++ b/field/team_match_log.go @@ -8,6 +8,7 @@ package field import ( "fmt" "github.com/FRCTeam1987/crimson-arena/model" + "github.com/FRCTeam1987/crimson-arena/network" "log" "os" "path/filepath" @@ -17,12 +18,13 @@ import ( const logsDir = "static/logs" type TeamMatchLog struct { - logger *log.Logger - logFile *os.File + logger *log.Logger + logFile *os.File + wifiStatus *network.TeamWifiStatus } // Creates a file to log to for the given match and team. -func NewTeamMatchLog(teamId int, match *model.Match) (*TeamMatchLog, error) { +func NewTeamMatchLog(teamId int, match *model.Match, wifiStatus *network.TeamWifiStatus) (*TeamMatchLog, error) { err := os.MkdirAll(filepath.Join(model.BaseDir, logsDir), 0755) if err != nil { return nil, err @@ -35,18 +37,35 @@ func NewTeamMatchLog(teamId int, match *model.Match) (*TeamMatchLog, error) { return nil, err } - log := TeamMatchLog{log.New(logFile, "", 0), logFile} - log.logger.Println("matchTimeSec,packetType,teamId,allianceStation,dsLinked,radioLinked,robotLinked,auto,enabled," + - "emergencyStop,batteryVoltage,missedPacketCount,dsRobotTripTimeMs") + log := TeamMatchLog{log.New(logFile, "", 0), logFile, wifiStatus} + log.logger.Println("matchTimeSec,packetType,teamId,allianceStation,dsLinked,radioLinked,rioLinked,robotLinked,auto,enabled," + + "emergencyStop,batteryVoltage,missedPacketCount,dsRobotTripTimeMs,rxRate,txRate,signalNoiseRatio") return &log, nil } // Adds a line to the log when a packet is received. func (log *TeamMatchLog) LogDsPacket(matchTimeSec float64, packetType int, dsConn *DriverStationConnection) { - log.logger.Printf("%f,%d,%d,%s,%v,%v,%v,%v,%v,%v,%f,%d,%d", matchTimeSec, packetType, dsConn.TeamId, - dsConn.AllianceStation, dsConn.DsLinked, dsConn.RadioLinked, dsConn.RobotLinked, dsConn.Auto, dsConn.Enabled, dsConn.Estop, - dsConn.BatteryVoltage, dsConn.MissedPacketCount, dsConn.DsRobotTripTimeMs) + log.logger.Printf( + "%f,%d,%d,%s,%v,%v,%v,%v,%v,%v,%f,%d,%d,%f,%f,%d", + matchTimeSec, + packetType, + dsConn.TeamId, + dsConn.AllianceStation, + dsConn.DsLinked, + dsConn.RadioLinked, + dsConn.RioLinked, + dsConn.RobotLinked, + dsConn.Auto, + dsConn.Enabled, + dsConn.Estop, + dsConn.BatteryVoltage, + dsConn.MissedPacketCount, + dsConn.DsRobotTripTimeMs, + log.wifiStatus.RxRate, + log.wifiStatus.TxRate, + log.wifiStatus.SignalNoiseRatio, + ) } func (log *TeamMatchLog) Close() { diff --git a/game/match_sounds.go b/game/match_sounds.go index 7b3155eb..00891e12 100644 --- a/game/match_sounds.go +++ b/game/match_sounds.go @@ -9,7 +9,6 @@ type MatchSound struct { Name string FileExtension string MatchTimeSec float64 - Timeout bool } // List of sounds and how many seconds into the match they are played. A negative time indicates that the sound can only @@ -22,19 +21,16 @@ func UpdateMatchSounds() { "start", "wav", 0, - false, }, { "end", "wav", float64(MatchTiming.AutoDurationSec), - false, }, { "resume", "wav", float64(MatchTiming.AutoDurationSec + MatchTiming.PauseDurationSec), - false, }, { "warning", @@ -43,37 +39,21 @@ func UpdateMatchSounds() { MatchTiming.AutoDurationSec + MatchTiming.PauseDurationSec + MatchTiming.TeleopDurationSec - MatchTiming.WarningRemainingDurationSec, ), - false, }, { "end", "wav", float64(MatchTiming.AutoDurationSec + MatchTiming.PauseDurationSec + MatchTiming.TeleopDurationSec), - false, - }, - { - "timeout_warning", - "wav", - float64(MatchTiming.TimeoutDurationSec - MatchTiming.TimeoutWarningRemainingDurationSec), - true, - }, - { - "end", - "wav", - float64(MatchTiming.TimeoutDurationSec), - true, }, { "abort", "wav", -1, - false, }, { "match_result", "wav", -1, - false, }, } } diff --git a/game/match_timing.go b/game/match_timing.go index 080728bd..6783f617 100644 --- a/game/match_timing.go +++ b/game/match_timing.go @@ -8,14 +8,13 @@ package game import "time" var MatchTiming = struct { - WarmupDurationSec int - AutoDurationSec int - PauseDurationSec int - TeleopDurationSec int - WarningRemainingDurationSec int - TimeoutDurationSec int - TimeoutWarningRemainingDurationSec int -}{0, 15, 2, 135, 30, 0, 60} + WarmupDurationSec int + AutoDurationSec int + PauseDurationSec int + TeleopDurationSec int + WarningRemainingDurationSec int + TimeoutDurationSec int +}{0, 15, 3, 135, 20, 0} func GetDurationToAutoEnd() time.Duration { return time.Duration(MatchTiming.WarmupDurationSec+MatchTiming.AutoDurationSec) * time.Second diff --git a/model/event_settings.go b/model/event_settings.go index bad0e725..e813d307 100755 --- a/model/event_settings.go +++ b/model/event_settings.go @@ -21,15 +21,8 @@ type EventSettings struct { TbaSecret string NetworkSecurityEnabled bool ApAddress string - ApUsername string ApPassword string - ApTeamChannel int - ApAdminChannel int - ApAdminWpaKey string - Ap2Address string - Ap2Username string - Ap2Password string - Ap2TeamChannel int + ApChannel int SwitchAddress string SwitchPassword string PlcAddress string @@ -58,10 +51,7 @@ func (database *Database) GetEventSettings() (*EventSettings, error) { SelectionRound2Order: "L", SelectionRound3Order: "", TBADownloadEnabled: true, - ApTeamChannel: 157, - ApAdminChannel: 0, - ApAdminWpaKey: "1234Five", - Ap2TeamChannel: 0, + ApChannel: 36, WarmupDurationSec: game.MatchTiming.WarmupDurationSec, AutoDurationSec: game.MatchTiming.AutoDurationSec, PauseDurationSec: game.MatchTiming.PauseDurationSec, diff --git a/model/event_settings_test.go b/model/event_settings_test.go index f4d393e9..a43cec57 100644 --- a/model/event_settings_test.go +++ b/model/event_settings_test.go @@ -24,14 +24,12 @@ func TestEventSettingsReadWrite(t *testing.T) { SelectionRound2Order: "L", SelectionRound3Order: "", TBADownloadEnabled: true, - ApTeamChannel: 157, - ApAdminChannel: 0, - ApAdminWpaKey: "1234Five", + ApChannel: 36, WarmupDurationSec: 0, AutoDurationSec: 15, - PauseDurationSec: 2, + PauseDurationSec: 3, TeleopDurationSec: 135, - WarningRemainingDurationSec: 30, + WarningRemainingDurationSec: 20, }, *eventSettings, ) diff --git a/network/access_point.go b/network/access_point.go index d159402e..1fd215e9 100644 --- a/network/access_point.go +++ b/network/access_point.go @@ -1,277 +1,279 @@ // Copyright 2017 Team 254. All Rights Reserved. // Author: pat@patfairbank.com (Patrick Fairbank) // -// Methods for configuring a Linksys WRT1900ACS access point running OpenWRT for team SSIDs and VLANs. +// Methods for configuring a Linksys WRT1900ACS or Vivid-Hosting VH-109 access point running OpenWRT for team SSIDs and +// VLANs. package network import ( + "bytes" + "encoding/json" + "errors" "fmt" - "github.com/FRCTeam1987/crimson-arena/model" - "golang.org/x/crypto/ssh" + "io" "log" - "regexp" + "net/http" "strconv" - "strings" + "syscall" "time" + + "github.com/FRCTeam1987/crimson-arena/model" ) const ( - accessPointSshPort = 22 - accessPointConnectTimeoutSec = 1 - accessPointCommandTimeoutSec = 5 - accessPointPollPeriodSec = 3 - accessPointRequestBufferSize = 10 - accessPointConfigRetryIntervalSec = 5 + accessPointPollPeriodSec = 1 ) type AccessPoint struct { - address string - username string + apiUrl string password string - teamChannel int - adminChannel int - adminWpaKey string + channel int networkSecurityEnabled bool - configRequestChan chan [6]*model.Team - TeamWifiStatuses [6]TeamWifiStatus - initialStatusesFetched bool + Status string + TeamWifiStatuses [6]*TeamWifiStatus + lastConfiguredTeams [6]*model.Team } type TeamWifiStatus struct { - TeamId int - RadioLinked bool + TeamId int + RadioLinked bool + MBits float64 + RxRate float64 + TxRate float64 + SignalNoiseRatio int +} + +type configurationRequest struct { + Channel int `json:"channel"` + StationConfigurations map[string]stationConfiguration `json:"stationConfigurations"` } -type sshOutput struct { - output string - err error +type stationConfiguration struct { + Ssid string `json:"ssid"` + WpaKey string `json:"wpaKey"` } -func (ap *AccessPoint) SetSettings(address, username, password string, teamChannel, adminChannel int, - adminWpaKey string, networkSecurityEnabled bool) { - ap.address = address - ap.username = username +type accessPointStatus struct { + Channel int `json:"channel"` + Status string `json:"status"` + StationStatuses map[string]*stationStatus `json:"stationStatuses"` +} + +type stationStatus struct { + Ssid string `json:"ssid"` + HashedWpaKey string `json:"hashedWpaKey"` + WpaKeySalt string `json:"wpaKeySalt"` + IsLinked bool `json:"isLinked"` + RxRateMbps float64 `json:"rxRateMbps"` + TxRateMbps float64 `json:"txRateMbps"` + SignalNoiseRatio int `json:"signalNoiseRatio"` + BandwidthUsedMbps float64 `json:"bandwidthUsedMbps"` +} + +func (ap *AccessPoint) SetSettings( + address, password string, + channel int, + networkSecurityEnabled bool, + wifiStatuses [6]*TeamWifiStatus, +) { + ap.apiUrl = fmt.Sprintf("http://%s", address) ap.password = password - ap.teamChannel = teamChannel - ap.adminChannel = adminChannel - ap.adminWpaKey = adminWpaKey + ap.channel = channel ap.networkSecurityEnabled = networkSecurityEnabled - - // Create config channel the first time this method is called. - if ap.configRequestChan == nil { - ap.configRequestChan = make(chan [6]*model.Team, accessPointRequestBufferSize) - } + ap.Status = "UNKNOWN" + ap.TeamWifiStatuses = wifiStatuses } -// Loops indefinitely to read status from and write configurations to the access point. +// Loops indefinitely to read status from the access point. func (ap *AccessPoint) Run() { for { - // Check if there are any pending configuration requests; if not, periodically poll wifi status. - select { - case request := <-ap.configRequestChan: - // If there are multiple requests queued up, only consider the latest one. - numExtraRequests := len(ap.configRequestChan) - for i := 0; i < numExtraRequests; i++ { - request = <-ap.configRequestChan + time.Sleep(time.Second * accessPointPollPeriodSec) + if err := ap.updateMonitoring(); err != nil { + log.Printf("Failed to update access point monitoring: %v", err) + continue + } + // If the access point is in a good state but doesn't match the expected configuration, try again. + if ap.Status == "ACTIVE" && !ap.statusMatchesLastConfiguration() { + log.Println("Access point is ACTIVE but does not match expected configuration; retrying configuration.") + if err := ap.ConfigureTeamWifi(ap.lastConfiguredTeams); err != nil { + log.Printf("Failed to reconfigure access point: %v", err) } - ap.handleTeamWifiConfiguration(request) - case <-time.After(time.Second * accessPointPollPeriodSec): - ap.updateTeamWifiStatuses() } } } -// Adds a request to set up wireless networks for the given set of teams to the asynchronous queue. +// Calls the access point's API to configure the team SSIDs and WPA keys. func (ap *AccessPoint) ConfigureTeamWifi(teams [6]*model.Team) error { - // Use a channel to serialize configuration requests; the monitoring goroutine will service them. - select { - case ap.configRequestChan <- teams: - return nil - default: - return fmt.Errorf("WiFi config request buffer full") - } -} - -func (ap *AccessPoint) ConfigureAdminWifi() error { if !ap.networkSecurityEnabled { return nil } - disabled := 0 - if ap.adminChannel == 0 { - disabled = 1 - } - commands := []string{ - fmt.Sprintf("set wireless.radio0.channel='%d'", ap.teamChannel), - fmt.Sprintf("set wireless.radio1.disabled='%d'", disabled), - fmt.Sprintf("set wireless.radio1.channel='%d'", ap.adminChannel), - fmt.Sprintf("set wireless.@wifi-iface[0].key='%s'", ap.adminWpaKey), - "commit wireless", + ap.Status = "CONFIGURING" + ap.lastConfiguredTeams = teams + request := configurationRequest{ + Channel: ap.channel, + StationConfigurations: make(map[string]stationConfiguration), } - command := fmt.Sprintf("uci batch < 63 { - return "", fmt.Errorf("Invalid WPA key '%s' configured for team %d.", team.WpaKey, team.Id) - } - - *commands = append(*commands, fmt.Sprintf("set wireless.@wifi-iface[%d].disabled='0'", position), - fmt.Sprintf("set wireless.@wifi-iface[%d].ssid='%d'", position, team.Id), - fmt.Sprintf("set wireless.@wifi-iface[%d].key='%s'", position, team.WpaKey)) +// Returns true if the access point's current status matches the last configuration that was sent to it. +func (ap *AccessPoint) statusMatchesLastConfiguration() bool { + for i := 0; i < 6; i++ { + var expectedTeamId, actualTeamId int + if ap.lastConfiguredTeams[i] != nil { + expectedTeamId = ap.lastConfiguredTeams[i].Id + } + if ap.TeamWifiStatuses[i] != nil { + actualTeamId = ap.TeamWifiStatuses[i].TeamId + } + if expectedTeamId != actualTeamId { + return false } } - *commands = append(*commands, "commit wireless") - return strings.Join(*commands, "\n"), nil + return true } -// Parses the given output from the "iwinfo" command on the AP and updates the given status structure with the result. -func decodeWifiInfo(wifiInfo string, statuses []TeamWifiStatus) error { - ssidRe := regexp.MustCompile("ESSID: \"([-\\w ]*)\"") - ssids := ssidRe.FindAllStringSubmatch(wifiInfo, -1) - linkQualityRe := regexp.MustCompile("Link Quality: ([-\\w ]+)/([-\\w ]+)") - linkQualities := linkQualityRe.FindAllStringSubmatch(wifiInfo, -1) +func (ap *AccessPoint) checkAndLogApiError(err error) { + if errors.Is(err, syscall.ECONNREFUSED) { + log.Printf( + "\x1b[31mThe access point appears to be present at %s but is refusing API connection requests. Note that "+ + "from 2024 onwards, you must manually install the API server on the Linksys API before it can be used "+ + "with Cheesy Arena. See https://github.com/patfair/frc-radio-api for installation instructions."+ + "\u001B[0m", + ap.apiUrl, + ) + } +} - // There should be at least six networks present -- one for each team on the 5GHz radio, plus one on the 2.4GHz - // radio if the admin network is enabled. - if len(ssids) < 6 || len(linkQualities) < 6 { - return fmt.Errorf("Could not parse wifi info; expected 6 team networks, got %d.", len(ssids)) +// Generates the configuration for the given team's station and adds it to the map. If the team is nil, no entry is +// added for the station. +func addStation(stationsConfigurations map[string]stationConfiguration, station string, team *model.Team) { + if team == nil { + return + } + stationsConfigurations[station] = stationConfiguration{ + Ssid: strconv.Itoa(team.Id), + WpaKey: team.WpaKey, } +} - for i := range statuses { - ssid := ssids[i][1] - statuses[i].TeamId, _ = strconv.Atoi(ssid) // Any non-numeric SSIDs will be represented by a zero. - linkQualityNumerator := linkQualities[i][1] - statuses[i].RadioLinked = linkQualityNumerator != "unknown" +// Updates the given team's wifi status structure with the given station status. +func updateTeamWifiStatus(teamWifiStatus *TeamWifiStatus, stationStatus *stationStatus) { + if stationStatus == nil { + teamWifiStatus.TeamId = 0 + teamWifiStatus.RadioLinked = false + teamWifiStatus.MBits = 0 + teamWifiStatus.RxRate = 0 + teamWifiStatus.TxRate = 0 + teamWifiStatus.SignalNoiseRatio = 0 + } else { + teamWifiStatus.TeamId, _ = strconv.Atoi(stationStatus.Ssid) + teamWifiStatus.RadioLinked = stationStatus.IsLinked + teamWifiStatus.MBits = stationStatus.BandwidthUsedMbps + teamWifiStatus.RxRate = stationStatus.RxRateMbps + teamWifiStatus.TxRate = stationStatus.TxRateMbps + teamWifiStatus.SignalNoiseRatio = stationStatus.SignalNoiseRatio } +} - return nil +// Returns an abbreviated string representation of the access point status for inclusion in the log. +func (apStatus *accessPointStatus) toLogString() string { + var buffer bytes.Buffer + buffer.WriteString(fmt.Sprintf("Channel: %d\n", apStatus.Channel)) + for _, station := range []string{"red1", "red2", "red3", "blue1", "blue2", "blue3"} { + stationStatus := apStatus.StationStatuses[station] + ssid := "[empty]" + if stationStatus != nil { + ssid = stationStatus.Ssid + } + buffer.WriteString(fmt.Sprintf("%-6s %s\n", station+":", ssid)) + } + return buffer.String() } diff --git a/network/access_point_test.go b/network/access_point_test.go index 2ca5e99f..30d1b0d1 100644 --- a/network/access_point_test.go +++ b/network/access_point_test.go @@ -4,134 +4,166 @@ package network import ( - "fmt" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "github.com/FRCTeam1987/crimson-arena/model" "github.com/stretchr/testify/assert" - "io/ioutil" - "regexp" - "testing" ) -func TestConfigureAccessPoint(t *testing.T) { - model.BaseDir = ".." +func TestAccessPoint_ConfigureTeamWifi(t *testing.T) { + var ap AccessPoint + var request configurationRequest + wifiStatuses := [6]*TeamWifiStatus{{}, {}, {}, {}, {}, {}} + ap.SetSettings("dummy", "password1", 123, true, wifiStatuses) - disabledRe := regexp.MustCompile("disabled='([-\\w ]+)'") - ssidRe := regexp.MustCompile("ssid='([-\\w ]*)'") - wpaKeyRe := regexp.MustCompile("key='([-\\w ]*)'") - - // Should put dummy values for all team SSIDs if there are no teams. - config, _ := generateAccessPointConfig([6]*model.Team{nil, nil, nil, nil, nil, nil}) - disableds := disabledRe.FindAllStringSubmatch(config, -1) - ssids := ssidRe.FindAllStringSubmatch(config, -1) - wpaKeys := wpaKeyRe.FindAllStringSubmatch(config, -1) - if assert.Equal(t, 6, len(disableds)) && assert.Equal(t, 6, len(ssids)) && assert.Equal(t, 6, len(wpaKeys)) { - for i := 0; i < 6; i++ { - assert.Equal(t, "0", disableds[i][1]) - assert.Equal(t, fmt.Sprintf("no-team-%d", i+1), ssids[i][1]) - assert.Equal(t, fmt.Sprintf("no-team-%d", i+1), wpaKeys[i][1]) - } - } + // Mock the radio API server. + radioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.URL.Path, "/configuration") + assert.Equal(t, "Bearer password1", r.Header.Get("Authorization")) + assert.Nil(t, json.NewDecoder(r.Body).Decode(&request)) + })) + ap.apiUrl = radioServer.URL - // Should configure two SSIDs for two teams and put dummy values for the rest. - config, _ = generateAccessPointConfig([6]*model.Team{{Id: 254, WpaKey: "aaaaaaaa"}, nil, nil, nil, nil, - {Id: 1114, WpaKey: "bbbbbbbb"}}) - disableds = disabledRe.FindAllStringSubmatch(config, -1) - ssids = ssidRe.FindAllStringSubmatch(config, -1) - wpaKeys = wpaKeyRe.FindAllStringSubmatch(config, -1) - if assert.Equal(t, 6, len(disableds)) && assert.Equal(t, 6, len(ssids)) && assert.Equal(t, 6, len(wpaKeys)) { - assert.Equal(t, "0", disableds[0][1]) - assert.Equal(t, "254", ssids[0][1]) - assert.Equal(t, "aaaaaaaa", wpaKeys[0][1]) - for i := 1; i < 5; i++ { - assert.Equal(t, "0", disableds[i][1]) - assert.Equal(t, fmt.Sprintf("no-team-%d", i+1), ssids[i][1]) - assert.Equal(t, fmt.Sprintf("no-team-%d", i+1), wpaKeys[i][1]) - } - assert.Equal(t, "0", disableds[5][1]) - assert.Equal(t, "1114", ssids[5][1]) - assert.Equal(t, "bbbbbbbb", wpaKeys[5][1]) - } + // All stations assigned. + team1 := &model.Team{Id: 254, WpaKey: "11111111"} + team2 := &model.Team{Id: 1114, WpaKey: "22222222"} + team3 := &model.Team{Id: 469, WpaKey: "33333333"} + team4 := &model.Team{Id: 2046, WpaKey: "44444444"} + team5 := &model.Team{Id: 2056, WpaKey: "55555555"} + team6 := &model.Team{Id: 1678, WpaKey: "66666666"} + assert.Nil(t, ap.ConfigureTeamWifi([6]*model.Team{team1, team2, team3, team4, team5, team6})) + assert.Equal( + t, + configurationRequest{ + Channel: 123, + StationConfigurations: map[string]stationConfiguration{ + "red1": {"254", "11111111"}, + "red2": {"1114", "22222222"}, + "red3": {"469", "33333333"}, + "blue1": {"2046", "44444444"}, + "blue2": {"2056", "55555555"}, + "blue3": {"1678", "66666666"}, + }, + }, + request, + ) - // Should configure all SSIDs for six teams. - config, _ = generateAccessPointConfig([6]*model.Team{{Id: 1, WpaKey: "11111111"}, {Id: 2, WpaKey: "22222222"}, - {Id: 3, WpaKey: "33333333"}, {Id: 4, WpaKey: "44444444"}, {Id: 5, WpaKey: "55555555"}, - {Id: 6, WpaKey: "66666666"}}) - disableds = disabledRe.FindAllStringSubmatch(config, -1) - ssids = ssidRe.FindAllStringSubmatch(config, -1) - wpaKeys = wpaKeyRe.FindAllStringSubmatch(config, -1) - if assert.Equal(t, 6, len(ssids)) && assert.Equal(t, 6, len(wpaKeys)) { - for i := 0; i < 6; i++ { - assert.Equal(t, "0", disableds[i][1]) - } - assert.Equal(t, "1", ssids[0][1]) - assert.Equal(t, "11111111", wpaKeys[0][1]) - assert.Equal(t, "2", ssids[1][1]) - assert.Equal(t, "22222222", wpaKeys[1][1]) - assert.Equal(t, "3", ssids[2][1]) - assert.Equal(t, "33333333", wpaKeys[2][1]) - assert.Equal(t, "4", ssids[3][1]) - assert.Equal(t, "44444444", wpaKeys[3][1]) - assert.Equal(t, "5", ssids[4][1]) - assert.Equal(t, "55555555", wpaKeys[4][1]) - assert.Equal(t, "6", ssids[5][1]) - assert.Equal(t, "66666666", wpaKeys[5][1]) - } + // Different channel and only some stations assigned. + ap.channel = 456 + request = configurationRequest{} + assert.Nil(t, ap.ConfigureTeamWifi([6]*model.Team{nil, nil, team2, nil, team1, nil})) + assert.Equal( + t, + configurationRequest{ + Channel: 456, + StationConfigurations: map[string]stationConfiguration{ + "red3": {"1114", "22222222"}, + "blue2": {"254", "11111111"}, + }, + }, + request, + ) - // Should reject a missing WPA key. - _, err := generateAccessPointConfig([6]*model.Team{{Id: 254}, nil, nil, nil, nil, nil}) + // Radio API returns an error. + radioServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.URL.Path, "/configuration") + http.Error(w, "oh noes", 507) + })) + ap.apiUrl = radioServer.URL + err := ap.ConfigureTeamWifi([6]*model.Team{team1, team2, team3, team4, team5, team6}) if assert.NotNil(t, err) { - assert.Contains(t, err.Error(), "Invalid WPA key") + assert.Contains(t, err.Error(), "returned status 507: oh noes") } + assert.Equal(t, "CONFIGURING", ap.Status) } -func TestDecodeWifiInfo(t *testing.T) { - var statuses [6]TeamWifiStatus +func TestAccessPoint_updateMonitoring(t *testing.T) { + var ap AccessPoint + wifiStatuses := [6]*TeamWifiStatus{{}, {}, {}, {}, {}, {}} + ap.SetSettings("dummy", "password2", 123, true, wifiStatuses) - // Test with zero team networks configured. - output, err := ioutil.ReadFile("testdata/iwinfo_0_teams.txt") - if assert.Nil(t, err) { - assert.Nil(t, decodeWifiInfo(string(output), statuses[:])) - assertTeamWifiStatus(t, 0, false, statuses[0]) - assertTeamWifiStatus(t, 0, false, statuses[1]) - assertTeamWifiStatus(t, 0, false, statuses[2]) - assertTeamWifiStatus(t, 0, false, statuses[3]) - assertTeamWifiStatus(t, 0, false, statuses[4]) - assertTeamWifiStatus(t, 0, false, statuses[5]) + apStatus := accessPointStatus{ + Channel: 456, + Status: "ACTIVE", + StationStatuses: map[string]*stationStatus{ + "red1": {"254", "hash111", "salt1", true, 1, 2, 3, 4}, + "red2": {"1114", "hash222", "salt2", false, 5, 6, 7, 8}, + "red3": {"469", "hash333", "salt3", true, 9, 10, 11, 12}, + "blue1": {"2046", "hash444", "salt4", false, 13, 14, 15, 16}, + "blue2": {"2056", "hash555", "salt5", true, 17, 18, 19, 20}, + "blue3": {"1678", "hash666", "salt6", false, 21, 22, 23, 24}, + }, } - // Test with two team networks configured. - output, err = ioutil.ReadFile("testdata/iwinfo_2_teams.txt") - if assert.Nil(t, err) { - assert.Nil(t, decodeWifiInfo(string(output), statuses[:])) - assertTeamWifiStatus(t, 0, false, statuses[0]) - assertTeamWifiStatus(t, 2471, true, statuses[1]) - assertTeamWifiStatus(t, 0, false, statuses[2]) - assertTeamWifiStatus(t, 254, false, statuses[3]) - assertTeamWifiStatus(t, 0, false, statuses[4]) - assertTeamWifiStatus(t, 0, false, statuses[5]) - } + // Mock the radio API server. + radioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.URL.Path, "/status") + assert.Equal(t, "Bearer password2", r.Header.Get("Authorization")) + assert.Nil(t, json.NewEncoder(w).Encode(apStatus)) + })) + ap.apiUrl = radioServer.URL - // Test with six team networks configured. - output, err = ioutil.ReadFile("testdata/iwinfo_6_teams.txt") - if assert.Nil(t, err) { - assert.Nil(t, decodeWifiInfo(string(output), statuses[:])) - assertTeamWifiStatus(t, 254, false, statuses[0]) - assertTeamWifiStatus(t, 1678, false, statuses[1]) - assertTeamWifiStatus(t, 2910, true, statuses[2]) - assertTeamWifiStatus(t, 604, false, statuses[3]) - assertTeamWifiStatus(t, 8, false, statuses[4]) - assertTeamWifiStatus(t, 2471, true, statuses[5]) + // All stations assigned. + assert.Nil(t, ap.updateMonitoring()) + assert.Equal(t, 123, ap.channel) // Should not have changed to reflect the radio API. + assert.Equal(t, "ACTIVE", ap.Status) + assert.Equal(t, TeamWifiStatus{254, true, 4, 1, 2, 3}, *wifiStatuses[0]) + assert.Equal(t, TeamWifiStatus{1114, false, 8, 5, 6, 7}, *wifiStatuses[1]) + assert.Equal(t, TeamWifiStatus{469, true, 12, 9, 10, 11}, *wifiStatuses[2]) + assert.Equal(t, TeamWifiStatus{2046, false, 16, 13, 14, 15}, *wifiStatuses[3]) + assert.Equal(t, TeamWifiStatus{2056, true, 20, 17, 18, 19}, *wifiStatuses[4]) + assert.Equal(t, TeamWifiStatus{1678, false, 24, 21, 22, 23}, *wifiStatuses[5]) + + // Only some stations assigned. + apStatus.Status = "CONFIGURING" + apStatus.StationStatuses = map[string]*stationStatus{ + "red1": nil, + "red2": nil, + "red3": {"469", "hash333", "salt3", true, 9, 10, 11, 12}, + "blue1": nil, + "blue2": {"2056", "hash555", "salt5", true, 17, 18, 19, 20}, + "blue3": nil, } + assert.Nil(t, ap.updateMonitoring()) + assert.Equal(t, "CONFIGURING", ap.Status) + assert.Equal(t, TeamWifiStatus{}, *wifiStatuses[0]) + assert.Equal(t, TeamWifiStatus{}, *wifiStatuses[1]) + assert.Equal(t, TeamWifiStatus{469, true, 12, 9, 10, 11}, *wifiStatuses[2]) + assert.Equal(t, TeamWifiStatus{}, *wifiStatuses[3]) + assert.Equal(t, TeamWifiStatus{2056, true, 20, 17, 18, 19}, *wifiStatuses[4]) + assert.Equal(t, TeamWifiStatus{}, *wifiStatuses[5]) - // Test with invalid input. - assert.NotNil(t, decodeWifiInfo("", statuses[:])) - output, err = ioutil.ReadFile("testdata/iwinfo_invalid.txt") - if assert.Nil(t, err) { - assert.NotNil(t, decodeWifiInfo(string(output), statuses[:])) + // Radio API returns an error. + radioServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.URL.Path, "/status") + http.Error(w, "gosh darn", 404) + })) + ap.apiUrl = radioServer.URL + err := ap.updateMonitoring() + if assert.NotNil(t, err) { + assert.Contains(t, err.Error(), "returned status 404: gosh darn") } + assert.Equal(t, "ERROR", ap.Status) } -func assertTeamWifiStatus(t *testing.T, expectedTeamId int, expectedRadioLinked bool, status TeamWifiStatus) { - assert.Equal(t, expectedTeamId, status.TeamId) - assert.Equal(t, expectedRadioLinked, status.RadioLinked) +func TestAccessPoint_statusMatchesLastConfiguration(t *testing.T) { + var ap AccessPoint + wifiStatuses := [6]*TeamWifiStatus{{}, {}, {}, {}, {}, {}} + ap.SetSettings("dummy", "", 123, true, wifiStatuses) + assert.True(t, ap.statusMatchesLastConfiguration()) + team1 := &model.Team{Id: 254, WpaKey: "11111111"} + team2 := &model.Team{Id: 1678, WpaKey: "22222222"} + ap.ConfigureTeamWifi([6]*model.Team{nil, team1, nil, team2, nil, nil}) + assert.False(t, ap.statusMatchesLastConfiguration()) + ap.TeamWifiStatuses[1].TeamId = 254 + assert.False(t, ap.statusMatchesLastConfiguration()) + ap.TeamWifiStatuses[3].TeamId = 1677 + assert.False(t, ap.statusMatchesLastConfiguration()) + ap.TeamWifiStatuses[3].TeamId = 1678 + assert.True(t, ap.statusMatchesLastConfiguration()) + ap.TeamWifiStatuses[4].TeamId = 111 + assert.False(t, ap.statusMatchesLastConfiguration()) } diff --git a/network/dnsmasq.go b/network/dnsmasq.go index 0b50d952..bd6d85b3 100644 --- a/network/dnsmasq.go +++ b/network/dnsmasq.go @@ -44,14 +44,15 @@ func (dm *DnsMasq) ConfigureTeamEthernet(teams [6]*model.Team) error { if oldTeamVlans[team.Id] == vlan { delete(oldTeamVlans, team.Id) } else { + teamPartialIp := fmt.Sprintf("%d.%d", team.Id/100, team.Id%100) contents := []byte(fmt.Sprintf( "# Options for VLAN%d\n"+ "# Team %d\n"+ "\n"+ - "dhcp-range=set:vlan%d,10.%d.%d.101,10.%d.%d.199,255.255.255.0,12h\n"+ - "dhcp-option=tag:vlan%d,3,10.%d.%d.61\n", - vlan, team.Id, vlan, team.Id/100, team.Id%100, team.Id/100, team.Id%100, - vlan, team.Id/100, team.Id%100)) + "dhcp-range=set:vlan%d,10.%s.20,10.%s.199,255.255.255.0,12h\n"+ + "dhcp-option=tag:vlan%d,3,10.%s.4\n", + vlan, team.Id, vlan, teamPartialIp, teamPartialIp, + vlan, teamPartialIp)) err := ioutil.WriteFile(fmt.Sprintf("/etc/dnsmasq.d/vlan%d.conf", vlan), contents, 0664) if err != nil { log.Printf("Failed to configure VLAN%d for team %d: %s", vlan, team.Id, err.Error()) diff --git a/network/switch.go b/network/switch.go index 1db669f8..74d75b34 100644 --- a/network/switch.go +++ b/network/switch.go @@ -11,12 +11,16 @@ import ( "fmt" "github.com/FRCTeam1987/crimson-arena/model" "net" - "regexp" - "strconv" "sync" + "time" ) -const switchTelnetPort = 23 +const ( + switchConfigBackoffDurationSec = 5 + switchConfigPauseDurationSec = 2 + switchTeamGatewayAddress = 4 + switchTelnetPort = 23 +) const ( red1Vlan = 10 @@ -28,16 +32,26 @@ const ( ) type Switch struct { - address string - port int - password string - mutex sync.Mutex + address string + port int + password string + mutex sync.Mutex + configBackoffDuration time.Duration + configPauseDuration time.Duration + Status string } var ServerIpAddress = "10.0.100.5" // The DS will try to connect to this address only. func NewSwitch(address, password string) *Switch { - return &Switch{address: address, port: switchTelnetPort, password: password} + return &Switch{ + address: address, + port: switchTelnetPort, + password: password, + configBackoffDuration: switchConfigBackoffDurationSec * time.Second, + configPauseDuration: switchConfigPauseDurationSec * time.Second, + Status: "UNKNOWN", + } } // Sets up wired networks for the given set of teams. @@ -45,80 +59,52 @@ func (sw *Switch) ConfigureTeamEthernet(teams [6]*model.Team) error { // Make sure multiple configurations aren't being set at the same time. sw.mutex.Lock() defer sw.mutex.Unlock() + sw.Status = "CONFIGURING" - // Determine what new team VLANs are needed and build the commands to set them up. - oldTeamVlans, err := sw.getTeamVlans() + // Remove old team VLANs to reset the switch state. + removeTeamVlansCommand := "" + for vlan := 10; vlan <= 60; vlan += 10 { + removeTeamVlansCommand += fmt.Sprintf( + "interface Vlan%d\nno ip address\nno access-list 1%d\nno ip dhcp pool dhcp%d\n", vlan, vlan, vlan, + ) + } + _, err := sw.runConfigCommand(removeTeamVlansCommand) if err != nil { + sw.Status = "ERROR" return err } + time.Sleep(sw.configPauseDuration) + // Create the new team VLANs. addTeamVlansCommand := "" - replaceTeamVlan := func(team *model.Team, vlan int) { - if team == nil { - return - } - if oldTeamVlans[team.Id] == vlan { - delete(oldTeamVlans, team.Id) - } else { - addTeamVlansCommand += fmt.Sprintf( - "no access-list 1%d\n"+ - "access-list 1%d permit ip 10.%d.%d.0 0.0.0.255 host %s\n"+ - "access-list 1%d permit udp any eq bootpc any eq bootps\n"+ - "interface Vlan%d\nip address 10.%d.%d.61 255.255.255.0\n", - vlan, vlan, team.Id/100, team.Id%100, ServerIpAddress, vlan, vlan, team.Id/100, - team.Id%100) - } - } - replaceTeamVlan(teams[0], red1Vlan) - replaceTeamVlan(teams[1], red2Vlan) - replaceTeamVlan(teams[2], red3Vlan) - replaceTeamVlan(teams[3], blue1Vlan) - replaceTeamVlan(teams[4], blue2Vlan) - replaceTeamVlan(teams[5], blue3Vlan) - - // Build the command to remove the team VLANs that are no longer needed. - removeTeamVlansCommand := "" - for _, vlan := range oldTeamVlans { - removeTeamVlansCommand += fmt.Sprintf("interface Vlan%d\nno ip address\nno access-list 1%d\n", vlan, vlan) + addTeamVlan := func(team *model.Team, vlan int) { + teamPartialIp := fmt.Sprintf("%d.%d", team.Id/100, team.Id%100) + addTeamVlansCommand += fmt.Sprintf( + "no access-list 1%d\n"+ + "access-list 1%d permit ip 10.%s.0 0.0.0.255 host %s\n"+ + "access-list 1%d permit udp any eq bootpc any eq bootps\n"+ + "access-list 1%d permit icmp any any\n"+ + "interface Vlan%d\nip address 10.%s.%d 255.255.255.0\n", + vlan, vlan, teamPartialIp, ServerIpAddress, vlan, vlan, vlan, teamPartialIp, switchTeamGatewayAddress) } - - // Build and run the overall command to do everything in a single telnet session. - command := removeTeamVlansCommand + addTeamVlansCommand - if len(command) > 0 { - _, err = sw.runConfigCommand(removeTeamVlansCommand + addTeamVlansCommand) + addTeamVlan(teams[0], red1Vlan) + addTeamVlan(teams[1], red2Vlan) + addTeamVlan(teams[2], red3Vlan) + addTeamVlan(teams[3], blue1Vlan) + addTeamVlan(teams[4], blue2Vlan) + addTeamVlan(teams[5], blue3Vlan) + if len(addTeamVlansCommand) > 0 { + _, err = sw.runConfigCommand(addTeamVlansCommand) if err != nil { + sw.Status = "ERROR" return err } } - return nil -} - -// Returns a map of currently-configured teams to VLANs. -func (sw *Switch) getTeamVlans() (map[int]int, error) { - // Get the entire config dump. - config, err := sw.runCommand("show running-config\n") - if err != nil { - return nil, err - } - - // Parse out the team IDs and VLANs from the config dump. - re := regexp.MustCompile("(?s)interface Vlan(\\d\\d)\\s+ip address 10\\.(\\d+)\\.(\\d+)\\.61") - teamVlanMatches := re.FindAllStringSubmatch(config, -1) - if teamVlanMatches == nil { - // There are probably no teams currently configured. - return nil, nil - } + // Give some time for the configuration to take before another one can be attempted. + time.Sleep(sw.configBackoffDuration) - // Build the map of team to VLAN. - teamVlans := make(map[int]int) - for _, match := range teamVlanMatches { - team100s, _ := strconv.Atoi(match[2]) - team1s, _ := strconv.Atoi(match[3]) - team := int(team100s)*100 + team1s - vlan, _ := strconv.Atoi(match[1]) - teamVlans[team] = vlan - } - return teamVlans, nil + sw.Status = "ACTIVE" + return nil } // Logs into the switch via Telnet and runs the given command in user exec mode. Reads the output and diff --git a/network/switch_test.go b/network/switch_test.go index f4dce5ff..360c9e39 100644 --- a/network/switch_test.go +++ b/network/switch_test.go @@ -15,57 +15,121 @@ import ( func TestConfigureSwitch(t *testing.T) { sw := NewSwitch("127.0.0.1", "password") + assert.Equal(t, "UNKNOWN", sw.Status) sw.port = 9050 - var command string + sw.configBackoffDuration = time.Millisecond + sw.configPauseDuration = time.Millisecond + var command1, command2 string + expectedResetCommand := "password\nenable\npassword\nterminal length 0\nconfig terminal\n" + + "interface Vlan10\nno ip address\nno access-list 110\nno ip dhcp pool dhcp10\n" + + "interface Vlan20\nno ip address\nno access-list 120\nno ip dhcp pool dhcp20\n" + + "interface Vlan30\nno ip address\nno access-list 130\nno ip dhcp pool dhcp30\n" + + "interface Vlan40\nno ip address\nno access-list 140\nno ip dhcp pool dhcp40\n" + + "interface Vlan50\nno ip address\nno access-list 150\nno ip dhcp pool dhcp50\n" + + "interface Vlan60\nno ip address\nno access-list 160\nno ip dhcp pool dhcp60\n" + + "end\ncopy running-config startup-config\n\nexit\n" - // Should do nothing if current configuration is blank. - mockTelnet(t, sw.port, "", &command) + // Should remove all previous VLANs and do nothing else if current configuration is blank. + mockTelnet(t, sw.port, &command1, &command2) assert.Nil(t, sw.ConfigureTeamEthernet([6]*model.Team{nil, nil, nil, nil, nil, nil})) - assert.Equal(t, "", command) + assert.Equal(t, expectedResetCommand, command1) + assert.Equal(t, "", command2) + assert.Equal(t, "ACTIVE", sw.Status) - // Should remove any existing teams but not other SSIDs. + // Should configure one team if only one is present. sw.port += 1 - mockTelnet(t, sw.port, - "interface Vlan100\nip address 10.0.100.2\ninterface Vlan50\nip address 10.2.54.61\n", &command) - assert.Nil(t, sw.ConfigureTeamEthernet([6]*model.Team{nil, nil, nil, nil, nil, nil})) - assert.Equal(t, "password\nenable\npassword\nterminal length 0\nconfig terminal\ninterface Vlan50\nno ip"+ - " address\nno access-list 150\nend\ncopy running-config startup-config\n\nexit\n", command) + mockTelnet(t, sw.port, &command1, &command2) + assert.Nil(t, sw.ConfigureTeamEthernet([6]*model.Team{nil, nil, nil, nil, {Id: 254}, nil})) + assert.Equal(t, expectedResetCommand, command1) + assert.Equal( + t, + "password\nenable\npassword\nterminal length 0\nconfig terminal\n"+ + "ip dhcp excluded-address 10.2.54.1 10.2.54.100\nip dhcp pool dhcp50\n"+ + "network 10.2.54.0 255.255.255.0\ndefault-router 10.2.54.4\nlease 7\n"+ + "access-list 150 permit ip 10.2.54.0 0.0.0.255 host 10.0.100.5\n"+ + "access-list 150 permit udp any eq bootpc any eq bootps\n"+ + "access-list 150 permit icmp any any\n"+ + "interface Vlan50\nip address 10.2.54.4 255.255.255.0\n"+ + "end\ncopy running-config startup-config\n\nexit\n", + command2, + ) - // Should configure new teams and leave existing ones alone if still needed. + // Should configure all teams if all are present. sw.port += 1 - mockTelnet(t, sw.port, "interface Vlan50\nip address 10.2.54.61\n", &command) - assert.Nil(t, sw.ConfigureTeamEthernet([6]*model.Team{nil, &model.Team{Id: 1114}, nil, nil, &model.Team{Id: 254}, - nil})) - assert.Equal(t, "password\nenable\npassword\nterminal length 0\nconfig terminal\n"+ - "ip dhcp excluded-address 10.11.14.1 10.11.14.100\nno ip dhcp pool dhcp20\nip dhcp pool dhcp20\n"+ - "network 10.11.14.0 255.255.255.0\ndefault-router 10.11.14.61\nlease 7\nno access-list 120\n"+ - "access-list 120 permit ip 10.11.14.0 0.0.0.255 host 10.0.100.5\n"+ - "access-list 120 permit udp any eq bootpc any eq bootps\ninterface Vlan20\n"+ - "ip address 10.11.14.61 255.255.255.0\nend\ncopy running-config startup-config\n\nexit\n", command) + mockTelnet(t, sw.port, &command1, &command2) + assert.Nil( + t, + sw.ConfigureTeamEthernet([6]*model.Team{{Id: 1114}, {Id: 254}, {Id: 296}, {Id: 1503}, {Id: 1678}, {Id: 1538}}), + ) + assert.Equal(t, expectedResetCommand, command1) + assert.Equal( + t, + "password\nenable\npassword\nterminal length 0\nconfig terminal\n"+ + "ip dhcp excluded-address 10.11.14.1 10.11.14.100\nip dhcp pool dhcp10\n"+ + "network 10.11.14.0 255.255.255.0\ndefault-router 10.11.14.4\nlease 7\n"+ + "access-list 110 permit ip 10.11.14.0 0.0.0.255 host 10.0.100.5\n"+ + "access-list 110 permit udp any eq bootpc any eq bootps\n"+ + "access-list 110 permit icmp any any\n"+ + "interface Vlan10\nip address 10.11.14.4 255.255.255.0\n"+ + "ip dhcp excluded-address 10.2.54.1 10.2.54.100\nip dhcp pool dhcp20\n"+ + "network 10.2.54.0 255.255.255.0\ndefault-router 10.2.54.4\nlease 7\n"+ + "access-list 120 permit ip 10.2.54.0 0.0.0.255 host 10.0.100.5\n"+ + "access-list 120 permit udp any eq bootpc any eq bootps\n"+ + "access-list 120 permit icmp any any\n"+ + "interface Vlan20\nip address 10.2.54.4 255.255.255.0\n"+ + "ip dhcp excluded-address 10.2.96.1 10.2.96.100\nip dhcp pool dhcp30\n"+ + "network 10.2.96.0 255.255.255.0\ndefault-router 10.2.96.4\nlease 7\n"+ + "access-list 130 permit ip 10.2.96.0 0.0.0.255 host 10.0.100.5\n"+ + "access-list 130 permit udp any eq bootpc any eq bootps\n"+ + "access-list 130 permit icmp any any\n"+ + "interface Vlan30\nip address 10.2.96.4 255.255.255.0\n"+ + "ip dhcp excluded-address 10.15.3.1 10.15.3.100\nip dhcp pool dhcp40\n"+ + "network 10.15.3.0 255.255.255.0\ndefault-router 10.15.3.4\nlease 7\n"+ + "access-list 140 permit ip 10.15.3.0 0.0.0.255 host 10.0.100.5\n"+ + "access-list 140 permit udp any eq bootpc any eq bootps\n"+ + "access-list 140 permit icmp any any\n"+ + "interface Vlan40\nip address 10.15.3.4 255.255.255.0\n"+ + "ip dhcp excluded-address 10.16.78.1 10.16.78.100\nip dhcp pool dhcp50\n"+ + "network 10.16.78.0 255.255.255.0\ndefault-router 10.16.78.4\nlease 7\n"+ + "access-list 150 permit ip 10.16.78.0 0.0.0.255 host 10.0.100.5\n"+ + "access-list 150 permit udp any eq bootpc any eq bootps\n"+ + "access-list 150 permit icmp any any\n"+ + "interface Vlan50\nip address 10.16.78.4 255.255.255.0\n"+ + "ip dhcp excluded-address 10.15.38.1 10.15.38.100\nip dhcp pool dhcp60\n"+ + "network 10.15.38.0 255.255.255.0\ndefault-router 10.15.38.4\nlease 7\n"+ + "access-list 160 permit ip 10.15.38.0 0.0.0.255 host 10.0.100.5\n"+ + "access-list 160 permit udp any eq bootpc any eq bootps\n"+ + "access-list 160 permit icmp any any\n"+ + "interface Vlan60\nip address 10.15.38.4 255.255.255.0\n"+ + "end\ncopy running-config startup-config\n\nexit\n", + command2, + ) } -func mockTelnet(t *testing.T, port int, response string, command *string) { +func mockTelnet(t *testing.T, port int, command1 *string, command2 *string) { go func() { - // Fake the first connection which should just get the configuration. ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) assert.Nil(t, err) defer ln.Close() - conn, err := ln.Accept() + *command1 = "" + *command2 = "" + + // Fake the first connection. + conn1, err := ln.Accept() assert.Nil(t, err) - conn.SetReadDeadline(time.Now().Add(10 * time.Millisecond)) + conn1.SetReadDeadline(time.Now().Add(10 * time.Millisecond)) var reader bytes.Buffer - reader.ReadFrom(conn) - assert.Contains(t, reader.String(), "terminal length 0\nshow running-config\nexit\n") - conn.Write([]byte(response)) - conn.Close() + reader.ReadFrom(conn1) + *command1 = reader.String() + conn1.Close() - // Fake the second connection which should configure stuff. + // Fake the second connection. conn2, err := ln.Accept() assert.Nil(t, err) conn2.SetReadDeadline(time.Now().Add(10 * time.Millisecond)) - var reader2 bytes.Buffer - reader2.ReadFrom(conn2) - *command = reader2.String() + reader.Reset() + reader.ReadFrom(conn2) + *command2 = reader.String() conn2.Close() }() time.Sleep(100 * time.Millisecond) // Give it some time to open the socket. diff --git a/partner/tba.go b/partner/tba.go index f80f4fab..fae740af 100755 --- a/partner/tba.go +++ b/partner/tba.go @@ -95,8 +95,8 @@ type TbaEvent struct { } type TbaMediaItem struct { - Details map[string]interface{} `json:"details"` - Type string `json:"type"` + Details map[string]any `json:"details"` + Type string `json:"type"` } type TbaPublishedAward struct { diff --git a/plc/plc.go b/plc/plc.go index 34546851..35b8b165 100644 --- a/plc/plc.go +++ b/plc/plc.go @@ -336,7 +336,7 @@ func (plc *Plc) writeCoils() bool { return true } -func (plc *Plc) generateIoChangeMessage() interface{} { +func (plc *Plc) generateIoChangeMessage() any { return &struct { Inputs []bool Registers []uint16 diff --git a/static/audio/end.wav b/static/audio/end.wav index 374182e9..041d4cdd 100644 Binary files a/static/audio/end.wav and b/static/audio/end.wav differ diff --git a/static/audio/timeout_warning.wav b/static/audio/timeout_warning.wav deleted file mode 100644 index bec7c6f0..00000000 Binary files a/static/audio/timeout_warning.wav and /dev/null differ diff --git a/static/css/alliance_station_display.css b/static/css/alliance_station_display.css index d16a0ac7..7eaabc03 100644 --- a/static/css/alliance_station_display.css +++ b/static/css/alliance_station_display.css @@ -118,6 +118,7 @@ body[data-position=left] #inMatch #redScore { } body[data-position=middle] #inMatch #timeRemaining { display: block; + font-size: 32vw; } body[data-position=right] #inMatch #blueScore { display: block; @@ -131,7 +132,7 @@ body[data-position=right] #inMatch #blueScore { left: 0; right: 0; margin: 0 auto; - font-size: 450px; + font-size: 35vw; line-height: 450px; text-align: center; } diff --git a/static/css/cheesy-arena.css b/static/css/cheesy-arena.css index 412045df..03d60a15 100644 --- a/static/css/cheesy-arena.css +++ b/static/css/cheesy-arena.css @@ -61,12 +61,15 @@ .bypass-status { cursor: pointer; } -[data-status-ok="true"] { +[data-status-ok="true"], [data-status-ternary="2"] { background-color: #0e8; } -[data-status-ok="false"] { +[data-status-ok="false"], [data-status-ternary="0"] { background-color: #e66; } +[data-status-ternary="1"] { + background-color: #fc3; +} [data-blink="true"] { background-color: #fc0; } @@ -85,6 +88,18 @@ .label-scoring[data-ready=true] { background-color: #0c6; } +.label-status { + background-color: #e66; +} +.label-status[data-status=BOOTING] { + background-color: #f92; +} +.label-status[data-status=CONFIGURING] { + background-color: #08f; +} +.label-status[data-status=ACTIVE] { + background-color: #0c6; +} .label-saved-match { background-color: #999; } diff --git a/static/css/field_monitor_display.css b/static/css/field_monitor_display.css index 7e9c0c06..e9becf59 100644 --- a/static/css/field_monitor_display.css +++ b/static/css/field_monitor_display.css @@ -29,6 +29,9 @@ body { font-size: 1.5vw; text-transform: uppercase; } +#eventStatusRow[data-fta="false"] { + display: none; +} .left-position, .right-position { width: 8%; height: 100%; @@ -57,20 +60,23 @@ body { font-size: 6vw; } .team-id[data-status=no-link], .team-notes[data-status=no-link] { - background-color: #963; + background-color: #AAA; } .team-id[data-status=ds-linked], .team-notes[data-status=ds-linked] { - background-color: #ff0; - color: #333; + background-color: #EDAB33; } .team-id[data-status=robot-linked], .team-notes[data-status=robot-linked] { - background-color: #0a3; + background-color: #00ff00; + color: #333; +} +.team-id[data-status=rio-linked], .team-notes[data-status=rio-linked] { + background-color: #00cc00; } .team-id[data-status=radio-linked], .team-notes[data-status=radio-linked] { - background-color: #ff00ff; + background-color: #AA3377; } .team-id[data-status=wrong-station], .team-notes[data-status=wrong-station] { - background-color: #246f92; + background-color: #4477AA; } .team-box-row { display: flex; @@ -84,7 +90,8 @@ body { background-color: #333; } .team-box[data-status-ok="true"] { - background-color: #0a3; + background-color: #00ff00; + color: #333; } .team-box[data-status-ok="false"] { background-color: #f44; diff --git a/static/js/field_monitor_display.js b/static/js/field_monitor_display.js index e9c811e8..45ddd4d1 100644 --- a/static/js/field_monitor_display.js +++ b/static/js/field_monitor_display.js @@ -41,6 +41,8 @@ var handleArenaStatus = function(data) { status = "wrong-station"; } else if (stationStatus.DsConn.RobotLinked) { status = "robot-linked"; + } else if (stationStatus.DsConn.RioLinked) { + status = "rio-linked"; } else if (stationStatus.DsConn.RadioLinked) { status = "radio-linked"; } else if (stationStatus.DsConn.DsLinked) { @@ -65,7 +67,7 @@ var handleArenaStatus = function(data) { teamEthernetElement.text("ETH"); } - var wifiStatus = data.TeamWifiStatuses[station]; + const wifiStatus = stationStatus.WifiStatus; teamRadioTextElement.text(wifiStatus.TeamId); if (stationStatus.DsConn) { @@ -75,7 +77,8 @@ var handleArenaStatus = function(data) { teamDsElement.text(dsConn.MissedPacketCount); // Format the radio status box according to the connection status of the robot radio. - var radioOkay = stationStatus.Team && stationStatus.Team.Id === wifiStatus.TeamId && wifiStatus.RadioLinked; + var radioOkay = stationStatus.Team && stationStatus.Team.Id === wifiStatus.TeamId && + (wifiStatus.RadioLinked || dsConn.RobotLinked); teamRadioElement.attr("data-status-ok", radioOkay); // Format the robot status box. diff --git a/static/js/match_play.js b/static/js/match_play.js index efed94f3..daec12cd 100755 --- a/static/js/match_play.js +++ b/static/js/match_play.js @@ -28,11 +28,6 @@ var abortMatch = function() { websocket.send("abortMatch"); }; -// Sends a websocket message to signal to the volunteers that they may enter the field. -var signalVolunteers = function() { - websocket.send("signalVolunteers"); -}; - // Sends a websocket message to signal to the teams that they may enter the field. var signalReset = function() { websocket.send("signalReset"); @@ -118,7 +113,7 @@ var handleArenaStatus = function(data) { // Update the team status view. $.each(data.AllianceStations, function(station, stationStatus) { - var wifiStatus = data.TeamWifiStatuses[station]; + var wifiStatus = stationStatus.WifiStatus; $("#status" + station + " .radio-status").text(wifiStatus.TeamId); if (stationStatus.DsConn) { @@ -126,10 +121,6 @@ var handleArenaStatus = function(data) { var dsConn = stationStatus.DsConn; $("#status" + station + " .ds-status").attr("data-status-ok", dsConn.DsLinked); - // Format the radio status box according to the connection status of the robot radio. - var radioOkay = stationStatus.Team && stationStatus.Team.Id === wifiStatus.TeamId && wifiStatus.RadioLinked; - $("#status" + station + " .radio-status").attr("data-status-ok", radioOkay); - // Format the robot status box. var robotOkay = dsConn.BatteryVoltage > lowBatteryThreshold && dsConn.RobotLinked; $("#status" + station + " .robot-status").attr("data-status-ok", robotOkay); @@ -142,19 +133,20 @@ var handleArenaStatus = function(data) { $("#status" + station + " .ds-status").attr("data-status-ok", ""); $("#status" + station + " .robot-status").attr("data-status-ok", ""); $("#status" + station + " .robot-status").text(""); + } - // Format the robot status box according to whether the AP is configured with the correct SSID. - var expectedTeamId = stationStatus.Team ? stationStatus.Team.Id : 0; - if (wifiStatus.TeamId === expectedTeamId) { - if (wifiStatus.RadioLinked) { - $("#status" + station + " .radio-status").attr("data-status-ok", true); - } else { - $("#status" + station + " .radio-status").attr("data-status-ok", ""); - } + // Format the radio status box according to whether the AP is configured with the correct SSID and the connection + // status of the robot radio. + const expectedTeamId = stationStatus.Team ? stationStatus.Team.Id : 0; + let radioStatus = 0; + if (expectedTeamId === wifiStatus.TeamId) { + if (wifiStatus.RadioLinked || stationStatus.DsConn?.RobotLinked) { + radioStatus = 2; } else { - $("#status" + station + " .radio-status").attr("data-status-ok", false); + radioStatus = 1; } } + $(`#status${station} .radio-status`).attr("data-status-ternary", radioStatus); if (stationStatus.Estop) { $("#status" + station + " .bypass-status").attr("data-status-ok", false); @@ -179,8 +171,7 @@ var handleArenaStatus = function(data) { $("#matchStartReason").html(data.CanStartMatchReason); } $("#abortMatch").prop("disabled", true); - $("#signalVolunteers").prop("disabled", true); - $("#signalReset").prop("disabled", true); + $("#signalReset").prop("disabled", false); $("#commitResults").prop("disabled", true); $("#discardResults").prop("disabled", true); $("#editResults").prop("disabled", true); @@ -205,7 +196,6 @@ var handleArenaStatus = function(data) { case "TELEOP_PERIOD": $("#startMatch").prop("disabled", true); $("#abortMatch").prop("disabled", false); - $("#signalVolunteers").prop("disabled", true); $("#signalReset").prop("disabled", true); $("#commitResults").prop("disabled", true); $("#discardResults").prop("disabled", true); @@ -221,7 +211,6 @@ var handleArenaStatus = function(data) { case "POST_MATCH": $("#startMatch").prop("disabled", true); $("#abortMatch").prop("disabled", true); - $("#signalVolunteers").prop("disabled", false); $("#signalReset").prop("disabled", false); $("#commitResults").prop("disabled", false); $("#discardResults").prop("disabled", false); @@ -237,7 +226,6 @@ var handleArenaStatus = function(data) { case "TIMEOUT_ACTIVE": $("#startMatch").prop("disabled", true); $("#abortMatch").prop("disabled", false); - $("#signalVolunteers").prop("disabled", true); $("#signalReset").prop("disabled", true); $("#commitResults").prop("disabled", true); $("#discardResults").prop("disabled", true); @@ -253,7 +241,6 @@ var handleArenaStatus = function(data) { case "POST_TIMEOUT": $("#startMatch").prop("disabled", true); $("#abortMatch").prop("disabled", true); - $("#signalVolunteers").prop("disabled", true); $("#signalReset").prop("disabled", true); $("#commitResults").prop("disabled", true); $("#discardResults").prop("disabled", true); @@ -268,6 +255,9 @@ var handleArenaStatus = function(data) { break; } + $("#accessPointStatus").attr("data-status", data.AccessPointStatus); + $("#switchStatus").attr("data-status", data.SwitchStatus); + if (data.PlcIsHealthy) { $("#plcStatus").text("Connected"); $("#plcStatus").attr("data-ready", true); diff --git a/switch_config.txt b/switch_config.txt index 6d5444e4..b5db9293 100644 --- a/switch_config.txt +++ b/switch_config.txt @@ -23,7 +23,8 @@ no aaa new-model system mtu routing 1500 ip routing ! -ip dhcp excluded-address 10.0.100.1 10.0.100.100 +ip dhcp excluded-address 10.0.100.1 10.0.100.125 +ip dhcp excluded-address 10.0.100.200 10.0.100.225 ! ip dhcp pool dhcppool network 10.0.100.0 255.255.255.0 @@ -57,72 +58,72 @@ vlan internal allocation policy ascending interface GigabitEthernet0/1 switchport trunk encapsulation dot1q switchport trunk native vlan 100 + switchport trunk allowed vlan 10,20,30,100 switchport mode trunk ! interface GigabitEthernet0/2 switchport trunk encapsulation dot1q switchport trunk native vlan 100 + switchport trunk allowed vlan 10,20,30,100 switchport mode trunk ! interface GigabitEthernet0/3 - switchport access vlan 100 switchport trunk encapsulation dot1q switchport trunk native vlan 100 + switchport trunk allowed vlan 40,50,60,100 switchport mode trunk ! interface GigabitEthernet0/4 switchport trunk encapsulation dot1q switchport trunk native vlan 100 + switchport trunk allowed vlan 40,50,60,100 switchport mode trunk ! interface GigabitEthernet0/5 - switchport access vlan 100 - switchport mode access + switchport trunk encapsulation dot1q + switchport trunk native vlan 100 + switchport mode trunk ! interface GigabitEthernet0/6 - switchport access vlan 10 - switchport mode access -! -interface GigabitEthernet0/7 switchport trunk encapsulation dot1q switchport trunk native vlan 100 switchport mode trunk ! +interface GigabitEthernet0/7 + switchport access vlan 100 + switchport mode access +! interface GigabitEthernet0/8 - switchport access vlan 20 + switchport access vlan 100 switchport mode access ! interface GigabitEthernet0/9 - switchport trunk encapsulation dot1q - switchport trunk native vlan 100 - switchport mode trunk + switchport access vlan 100 + switchport mode access ! interface GigabitEthernet0/10 - switchport access vlan 30 + switchport access vlan 100 switchport mode access ! interface GigabitEthernet0/11 - switchport trunk encapsulation dot1q - switchport trunk native vlan 100 - switchport mode trunk + switchport access vlan 10 + switchport mode access ! interface GigabitEthernet0/12 switchport access vlan 40 switchport mode access ! interface GigabitEthernet0/13 - switchport trunk encapsulation dot1q - switchport trunk native vlan 100 - switchport mode trunk + switchport access 20 + switchport mode access ! interface GigabitEthernet0/14 switchport access vlan 50 switchport mode access ! interface GigabitEthernet0/15 - switchport trunk encapsulation dot1q - switchport trunk native vlan 100 - switchport mode trunk + switchport access vlan 30 + switchport mode access ! interface GigabitEthernet0/16 switchport access vlan 60 @@ -249,64 +250,58 @@ interface GigabitEthernet0/46 switchport mode access ! interface GigabitEthernet0/47 - switchport trunk encapsulation dot1q - switchport trunk native vlan 100 - switchport mode trunk + switchport access vlan 100 + switchport mode access ! interface GigabitEthernet0/48 - switchport trunk encapsulation dot1q - switchport trunk native vlan 100 - switchport mode trunk + switchport access vlan 100 + switchport mode access ! interface GigabitEthernet0/49 - switchport trunk encapsulation dot1q - switchport trunk native vlan 100 - switchport mode trunk + switchport access vlan 100 + switchport mode access ! interface GigabitEthernet0/50 - switchport trunk encapsulation dot1q - switchport trunk native vlan 100 - switchport mode trunk + switchport access vlan 100 + switchport mode access ! interface GigabitEthernet0/51 - switchport trunk encapsulation dot1q - switchport trunk native vlan 100 - switchport mode trunk + switchport access vlan 100 + switchport mode access ! interface GigabitEthernet0/52 - switchport trunk encapsulation dot1q - switchport trunk native vlan 100 - switchport mode trunk + switchport access vlan 100 + switchport mode access ! interface Vlan1 - ip address 10.0.0.61 255.255.255.0 + ip address 10.0.0.4 255.255.255.0 ! interface Vlan10 - ip address 10.0.1.61 255.255.255.0 + ip address 10.0.1.4 255.255.255.0 ip access-group 110 in ! interface Vlan20 - ip address 10.0.2.61 255.255.255.0 + ip address 10.0.2.4 255.255.255.0 ip access-group 120 in ! interface Vlan30 - ip address 10.0.3.61 255.255.255.0 + ip address 10.0.3.4 255.255.255.0 ip access-group 130 in ! interface Vlan40 - ip address 10.0.4.61 255.255.255.0 + ip address 10.0.4.4 255.255.255.0 ip access-group 140 in ! interface Vlan50 - ip address 10.0.5.61 255.255.255.0 + ip address 10.0.5.4 255.255.255.0 ip access-group 150 in ! interface Vlan60 - ip address 10.0.6.61 255.255.255.0 + ip address 10.0.6.4 255.255.255.0 ip access-group 160 in ! interface Vlan100 - ip address 10.0.100.2 255.255.255.0 + ip address 10.0.100.3 255.255.255.0 ! ip classless ip http server diff --git a/templates/base.html b/templates/base.html index cb54ab08..33e594e1 100755 --- a/templates/base.html +++ b/templates/base.html @@ -6,7 +6,7 @@ */}} {{define "base"}} - + {{template "head_common" .}} diff --git a/templates/match_play.html b/templates/match_play.html index 76e26f0b..8e188fa4 100755 --- a/templates/match_play.html +++ b/templates/match_play.html @@ -116,10 +116,6 @@ onclick="abortMatch();" disabled> Abort Match -