From a63f3da532646eab9b7c5b5fe370590e59301992 Mon Sep 17 00:00:00 2001 From: Patrick Fairbank Date: Tue, 28 Nov 2023 17:05:38 -0800 Subject: [PATCH] Configure access point via API instead of SSH and remove support for second AP. --- field/arena.go | 82 +----- model/event_settings.go | 12 +- model/event_settings_test.go | 3 +- network/access_point.go | 463 +++++++++++----------------------- network/access_point_test.go | 397 ++++++++++------------------- templates/setup_settings.html | 108 +------- web/setup_settings.go | 17 +- 7 files changed, 305 insertions(+), 777 deletions(-) diff --git a/field/arena.go b/field/arena.go index a8aa7ed6..e1130fda 100644 --- a/field/arena.go +++ b/field/arena.go @@ -50,7 +50,6 @@ type Arena struct { Database *model.Database EventSettings *model.EventSettings accessPoint network.AccessPoint - accessPoint2 network.AccessPoint networkSwitch *network.Switch Plc plc.Plc TbaClient *partner.TbaClient @@ -148,71 +147,26 @@ func (arena *Arena) LoadSettings() error { arena.EventSettings = settings // Initialize the components that depend on settings. - var accessPoint1WifiStatuses, accessPoint2WifiStatuses [6]*network.TeamWifiStatus - if arena.EventSettings.Ap2TeamChannel == 0 { - accessPoint1WifiStatuses = [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, - } - } else { - accessPoint1WifiStatuses = [6]*network.TeamWifiStatus{ - &arena.AllianceStations["R1"].WifiStatus, - &arena.AllianceStations["R2"].WifiStatus, - &arena.AllianceStations["R3"].WifiStatus, - nil, - nil, - nil, - } - accessPoint2WifiStatuses = [6]*network.TeamWifiStatus{ - nil, - nil, - nil, - &arena.AllianceStations["B1"].WifiStatus, - &arena.AllianceStations["B2"].WifiStatus, - &arena.AllianceStations["B3"].WifiStatus, - } + 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( - 1, - settings.ApType == "vivid", settings.ApAddress, - settings.ApUsername, settings.ApPassword, - settings.ApTeamChannel, - settings.NetworkSecurityEnabled, - accessPoint1WifiStatuses, - ) - arena.accessPoint2.SetSettings( - 2, - settings.ApType == "vivid", - settings.Ap2Address, - settings.Ap2Username, - settings.Ap2Password, - settings.Ap2TeamChannel, + settings.ApChannel, settings.NetworkSecurityEnabled, - accessPoint2WifiStatuses, + accessPointWifiStatuses, ) arena.networkSwitch = network.NewSwitch(settings.SwitchAddress, settings.SwitchPassword) arena.Plc.SetAddress(settings.PlcAddress) arena.TbaClient = partner.NewTbaClient(settings.TbaEventCode, settings.TbaSecretId, settings.TbaSecret) arena.NexusClient = partner.NewNexusClient(settings.TbaEventCode) - if arena.EventSettings.NetworkSecurityEnabled && arena.MatchState == PreMatch { - if err = arena.accessPoint.ConfigureAdminSettings(); err != nil { - log.Printf("Failed to configure access point admin settings: %s", err.Error()) - } - if arena.EventSettings.Ap2TeamChannel != 0 { - if err = arena.accessPoint2.ConfigureAdminSettings(); err != nil { - log.Printf("Failed to configure second access point admin settings: %s", err.Error()) - } - } - } - game.MatchTiming.WarmupDurationSec = settings.WarmupDurationSec game.MatchTiming.AutoDurationSec = settings.AutoDurationSec game.MatchTiming.PauseDurationSec = settings.PauseDurationSec @@ -688,7 +642,6 @@ func (arena *Arena) Run() { go arena.listenForDriverStations() go arena.listenForDsUdpPackets() go arena.accessPoint.Run() - go arena.accessPoint2.Run() go arena.Plc.Run() for { @@ -827,21 +780,8 @@ func (arena *Arena) setupNetwork(teams [6]*model.Team, isPreload bool) { } 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 WiFi: %s", err.Error()) } go func() { if err := arena.networkSwitch.ConfigureTeamEthernet(teams); err != nil { diff --git a/model/event_settings.go b/model/event_settings.go index 189e4654..7163fc7a 100644 --- a/model/event_settings.go +++ b/model/event_settings.go @@ -28,15 +28,9 @@ type EventSettings struct { TbaSecret string NexusEnabled bool NetworkSecurityEnabled bool - ApType string ApAddress string - ApUsername string ApPassword string - ApTeamChannel int - Ap2Address string - Ap2Username string - Ap2Password string - Ap2TeamChannel int + ApChannel int SwitchAddress string SwitchPassword string PlcAddress string @@ -68,9 +62,7 @@ func (database *Database) GetEventSettings() (*EventSettings, error) { SelectionRound2Order: "L", SelectionRound3Order: "", TbaDownloadEnabled: true, - ApType: "linksys", - ApTeamChannel: 157, - 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 f457c74e..497270f6 100644 --- a/model/event_settings_test.go +++ b/model/event_settings_test.go @@ -24,8 +24,7 @@ func TestEventSettingsReadWrite(t *testing.T) { SelectionRound2Order: "L", SelectionRound3Order: "", TbaDownloadEnabled: true, - ApType: "linksys", - ApTeamChannel: 157, + ApChannel: 36, WarmupDurationSec: 0, AutoDurationSec: 15, PauseDurationSec: 3, diff --git a/network/access_point.go b/network/access_point.go index 0d9aa230..c96aa642 100644 --- a/network/access_point.go +++ b/network/access_point.go @@ -7,40 +7,30 @@ package network import ( + "bytes" + "encoding/json" "fmt" + "io" "log" - "regexp" + "net/http" "strconv" - "strings" "time" "github.com/Team254/cheesy-arena/model" - "golang.org/x/crypto/ssh" ) const ( - accessPointSshPort = 22 - accessPointConnectTimeoutSec = 1 - accessPointCommandTimeoutSec = 30 - accessPointPollPeriodSec = 3 - accessPointRequestBufferSize = 10 - accessPointConfigRetryIntervalSec = 30 - accessPointConfigBackoffSec = 5 + accessPointApiPort = 8081 + accessPointPollPeriodSec = 1 ) -var accessPointInfoLines = []string{"ESSID: ", "Mode: ", "Tx-Power: ", "Signal: ", "Bit Rate: "} - type AccessPoint struct { - apNumber int - isVividType bool - address string - username string + apiUrl string password string - teamChannel int + channel int networkSecurityEnabled bool - configRequestChan chan [6]*model.Team + Status string TeamWifiStatuses [6]*TeamWifiStatus - initialStatusesFetched bool } type TeamWifiStatus struct { @@ -52,356 +42,195 @@ type TeamWifiStatus struct { SignalNoiseRatio int } -type sshOutput struct { - output string - err error +type configurationRequest struct { + Channel int `json:"channel"` + StationConfigurations map[string]stationConfiguration `json:"stationConfigurations"` +} + +type stationConfiguration struct { + Ssid string `json:"ssid"` + WpaKey string `json:"wpaKey"` +} + +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"` + IsRobotRadioLinked bool `json:"isRobotRadioLinked"` + RxRateMbps float64 `json:"rxRateMbps"` + TxRateMbps float64 `json:"txRateMbps"` + SignalNoiseRatio int `json:"signalNoiseRatio"` + BandwidthUsedMbps float64 `json:"bandwidthUsedMbps"` } func (ap *AccessPoint) SetSettings( - apNumber int, - isVividType bool, - address, username, password string, - teamChannel int, + address, password string, + channel int, networkSecurityEnabled bool, wifiStatuses [6]*TeamWifiStatus, ) { - ap.apNumber = apNumber - ap.isVividType = isVividType - ap.address = address - ap.username = username + ap.apiUrl = fmt.Sprintf("http://%s:%d", address, accessPointApiPort) ap.password = password - ap.teamChannel = teamChannel + ap.channel = channel ap.networkSecurityEnabled = networkSecurityEnabled + ap.Status = "UNKNOWN" ap.TeamWifiStatuses = wifiStatuses - - // Create config channel the first time this method is called. - if ap.configRequestChan == nil { - ap.configRequestChan = make(chan [6]*model.Team, accessPointRequestBufferSize) - } } -// 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 - } - - ap.handleTeamWifiConfiguration(request) - case <-time.After(time.Second * accessPointPollPeriodSec): - ap.updateTeamWifiStatuses() - ap.updateTeamWifiBTU() + time.Sleep(time.Second * accessPointPollPeriodSec) + if err := ap.updateMonitoring(); err != nil { + log.Printf("Failed to update access point monitoring: %v", err) } } } -// Calls the access point to configure the non-team-related settings. -func (ap *AccessPoint) ConfigureAdminSettings() error { +// Calls the access point's API to configure the team SSIDs and WPA keys. +func (ap *AccessPoint) ConfigureTeamWifi(teams [6]*model.Team) error { if !ap.networkSecurityEnabled { return nil } - var device string - if ap.isVividType { - device = "wifi1" - } else { - device = "radio0" + request := configurationRequest{ + Channel: ap.channel, + StationConfigurations: make(map[string]stationConfiguration), } - command := fmt.Sprintf("uci set wireless.%s.channel=%d && uci commit wireless", device, ap.teamChannel) - _, err := ap.runCommand(command) - return err -} - -// Adds a request to set up wireless networks for the given set of teams to the asynchronous queue. -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) handleTeamWifiConfiguration(teams [6]*model.Team) { - if !ap.networkSecurityEnabled { - return + addStation(request.StationConfigurations, "red1", teams[0]) + addStation(request.StationConfigurations, "red2", teams[1]) + addStation(request.StationConfigurations, "red3", teams[2]) + addStation(request.StationConfigurations, "blue1", teams[3]) + addStation(request.StationConfigurations, "blue2", teams[4]) + addStation(request.StationConfigurations, "blue3", teams[5]) + jsonBody, err := json.Marshal(request) + if err != nil { + return err } - if !ap.isVividType { - // Clear the state of the radio before loading teams; the Linksys AP is crash-prone otherwise. - ap.configureTeams([6]*model.Team{nil, nil, nil, nil, nil, nil}) + // Send the configuration to the access point API. + url := ap.apiUrl + "/configuration" + httpRequest, err := http.NewRequest("POST", url, bytes.NewReader(jsonBody)) + if err != nil { + return err } - ap.configureTeams(teams) -} - -func (ap *AccessPoint) configureTeams(teams [6]*model.Team) { - retryCount := 1 - - for { - teamIndex := 0 - for teamIndex < 6 { - config, err := ap.generateTeamAccessPointConfig(teams[teamIndex], teamIndex+1) - if err != nil { - log.Printf("Failed to generate WiFi configuration for AP %d: %v", ap.apNumber, err) - } - - command := addConfigurationHeader(config) - log.Printf("Configuring AP %d with command: %s\n", ap.apNumber, command) - - _, err = ap.runCommand(command) - if err != nil { - log.Printf("Error writing team configuration to AP %d: %v", ap.apNumber, err) - retryCount++ - time.Sleep(time.Second * accessPointConfigRetryIntervalSec) - continue - } - - teamIndex++ - } - - _, _ = ap.runCommand("uci commit wireless") - _, _ = ap.runCommand("wifi reload") - if !ap.isVividType { - // The Linksys AP returns immediately after 'wifi reload' but may not have applied the configuration yet; - // sleep for a bit to compensate. (The Vivid AP waits for the configuration to be applied before returning.) - time.Sleep(time.Second * accessPointConfigBackoffSec) - } - err := ap.updateTeamWifiStatuses() - if err == nil && ap.configIsCorrectForTeams(teams) { - log.Printf("Successfully configured AP %d Wi-Fi after %d attempts.", ap.apNumber, retryCount) - break - } - log.Printf( - "WiFi configuration still incorrect on AP %d after %d attempts; trying again.", ap.apNumber, retryCount, - ) + if ap.password != "" { + httpRequest.Header.Add("Authorization", fmt.Sprintf("Bearer %s", ap.password)) } -} - -// Returns true if the configured networks as read from the access point match the given teams. -func (ap *AccessPoint) configIsCorrectForTeams(teams [6]*model.Team) bool { - if !ap.initialStatusesFetched { - return false + var httpClient http.Client + httpResponse, err := httpClient.Do(httpRequest) + if err != nil { + return err } - - for i, team := range teams { - expectedTeamId := 0 - actualTeamId := 0 - if team != nil { - expectedTeamId = team.Id - } - if ap.TeamWifiStatuses[i] != nil { - actualTeamId = ap.TeamWifiStatuses[i].TeamId - } - if actualTeamId != expectedTeamId { - return false - } + defer httpResponse.Body.Close() + if httpResponse.StatusCode/100 != 2 { + body, _ := io.ReadAll(httpResponse.Body) + return fmt.Errorf("access point returned status %d: %s", httpResponse.StatusCode, string(body)) } - return true + log.Println("Access point accepted the new configuration and will apply it asynchronously.") + return nil } -// Fetches the current wifi network status from the access point and updates the status structure. -func (ap *AccessPoint) updateTeamWifiStatuses() error { +// Fetches the current access point status from the API and updates the status structure. +func (ap *AccessPoint) updateMonitoring() error { if !ap.networkSecurityEnabled { return nil } - output, err := ap.runCommand("iwinfo") - if err == nil { - ap.logWifiInfo(output) - err = ap.decodeWifiInfo(output) - } - + // Fetch the status from the access point API. + url := ap.apiUrl + "/status" + httpRequest, err := http.NewRequest("GET", url, nil) if err != nil { - return fmt.Errorf("Error getting wifi info from AP: %v", err) - } else { - if !ap.initialStatusesFetched { - ap.initialStatusesFetched = true - } + return err } - return nil -} - -// Logs into the access point via SSH and runs the given shell command. -func (ap *AccessPoint) runCommand(command string) (string, error) { - // Open an SSH connection to the AP. - config := &ssh.ClientConfig{User: ap.username, - Auth: []ssh.AuthMethod{ssh.Password(ap.password)}, - HostKeyCallback: ssh.InsecureIgnoreHostKey(), - Timeout: accessPointConnectTimeoutSec * time.Second} - - conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", ap.address, accessPointSshPort), config) - if err != nil { - return "", err + if ap.password != "" { + httpRequest.Header.Add("Authorization", fmt.Sprintf("Bearer %s", ap.password)) } - session, err := conn.NewSession() + var httpClient http.Client + httpResponse, err := httpClient.Do(httpRequest) if err != nil { - return "", err - } - defer session.Close() - defer conn.Close() - - // Run the command with a timeout. - commandChan := make(chan sshOutput, 1) - go func() { - outputBytes, err := session.Output(command) - commandChan <- sshOutput{string(outputBytes), err} - }() - select { - case output := <-commandChan: - return output.output, output.err - case <-time.After(accessPointCommandTimeoutSec * time.Second): - return "", fmt.Errorf("WiFi SSH command timed out after %d seconds", accessPointCommandTimeoutSec) - } -} - -func addConfigurationHeader(commandList string) string { - return fmt.Sprintf("uci batch < 6 { - return "", fmt.Errorf("invalid team position %d", position) - } - - var ssid, key string - if team == nil { - ssid = fmt.Sprintf("no-team-%d", position) - key = fmt.Sprintf("no-team-%d", position) - } else { - if len(team.WpaKey) < 8 || len(team.WpaKey) > 63 { - return "", fmt.Errorf("invalid WPA key '%s' configured for team %d", team.WpaKey, team.Id) - } - ssid = strconv.Itoa(team.Id) - key = team.WpaKey - } - - commands := []string{ - fmt.Sprintf("set wireless.@wifi-iface[%d].disabled='0'", position), - fmt.Sprintf("set wireless.@wifi-iface[%d].ssid='%s'", position, ssid), - fmt.Sprintf("set wireless.@wifi-iface[%d].key='%s'", position, key), - } - if ap.isVividType { - commands = append(commands, fmt.Sprintf("set wireless.@wifi-iface[%d].sae_password='%s'", position, key)) - } - - return strings.Join(commands, "\n"), nil -} - -// Filters the given output from the "iwiinfo" command on the AP and logs the relevant parts. -func (ap *AccessPoint) logWifiInfo(wifiInfo string) { - lines := strings.Split(wifiInfo, "\n") - var filteredLines []string - for _, line := range lines { - for _, infoLine := range accessPointInfoLines { - if strings.Contains(line, infoLine) { - filteredLines = append(filteredLines, line) - break - } - } + ap.Status = "ERROR" + return fmt.Errorf("failed to fetch access point status: %v", err) } - log.Printf("AP %d status:\n%s\n", ap.apNumber, strings.Join(filteredLines, "\n")) -} - -// Parses the given output from the "iwinfo" command on the AP and updates the given status structure with the result. -func (ap *AccessPoint) decodeWifiInfo(wifiInfo string) error { - ssidRe := regexp.MustCompile("ESSID: \"([-\\w ]*)\"") - ssids := ssidRe.FindAllStringSubmatch(wifiInfo, -1) - - // There should be six networks present -- one for each team on the 5GHz radio. - if len(ssids) < 6 { - return fmt.Errorf("Could not parse wifi info; expected 6 team networks, got %d.", len(ssids)) + if httpResponse.StatusCode/100 != 2 { + ap.Status = "ERROR" + body, _ := io.ReadAll(httpResponse.Body) + return fmt.Errorf("access point returned status %d: %s", httpResponse.StatusCode, string(body)) } - for i, wifiStatus := range ap.TeamWifiStatuses { - if wifiStatus != nil { - ssid := ssids[i][1] - wifiStatus.TeamId, _ = strconv.Atoi(ssid) // Any non-numeric SSIDs will be represented by a zero. + // Parse the response and populate the status structure. + var apStatus accessPointStatus + err = json.NewDecoder(httpResponse.Body).Decode(&apStatus) + if err != nil { + ap.Status = "ERROR" + return fmt.Errorf("failed to parse access point status: %v", err) + } + if ap.Status != apStatus.Status { + log.Printf("Access point status changed from %s to %s.", ap.Status, apStatus.Status) + ap.Status = apStatus.Status + if ap.Status == "ACTIVE" { + log.Printf("Access point detailed status:\n%s", apStatus.toLogString()) } } + updateTeamWifiStatus(ap.TeamWifiStatuses[0], apStatus.StationStatuses["red1"]) + updateTeamWifiStatus(ap.TeamWifiStatuses[1], apStatus.StationStatuses["red2"]) + updateTeamWifiStatus(ap.TeamWifiStatuses[2], apStatus.StationStatuses["red3"]) + updateTeamWifiStatus(ap.TeamWifiStatuses[3], apStatus.StationStatuses["blue1"]) + updateTeamWifiStatus(ap.TeamWifiStatuses[4], apStatus.StationStatuses["blue2"]) + updateTeamWifiStatus(ap.TeamWifiStatuses[5], apStatus.StationStatuses["blue3"]) return nil } -// Polls the 6 wlans on the ap for bandwidth use and updates data structure. -func (ap *AccessPoint) updateTeamWifiBTU() error { - if !ap.networkSecurityEnabled { - return nil - } - - var interfaces []string - if ap.isVividType { - interfaces = []string{"ath1", "ath11", "ath12", "ath13", "ath14", "ath15"} - } else { - interfaces = []string{"wlan0", "wlan0-1", "wlan0-2", "wlan0-3", "wlan0-4", "wlan0-5"} - } - - for i := range ap.TeamWifiStatuses { - if ap.TeamWifiStatuses[i] == nil { - continue - } - output, err := ap.runCommand(fmt.Sprintf("luci-bwc -i %s && iwinfo %s assoclist", interfaces[i], interfaces[i])) - if err == nil { - ap.TeamWifiStatuses[i].MBits = parseBtu(output) - ap.TeamWifiStatuses[i].parseAssocList(output) - } - if err != nil { - return fmt.Errorf("Error getting BTU info from AP: %v", err) - } +// 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 } - return nil -} - -// Parses the given data from the access point's onboard bandwidth monitor and returns five-second average bandwidth in -// megabits per second. -func parseBtu(response string) float64 { - mBits := 0.0 - btuRe := regexp.MustCompile("\\[ (\\d+), (\\d+), (\\d+), (\\d+), (\\d+) ]") - btuMatches := btuRe.FindAllStringSubmatch(response, -1) - if len(btuMatches) >= 7 { - firstMatch := btuMatches[len(btuMatches)-6] - lastMatch := btuMatches[len(btuMatches)-1] - rXBytes, _ := strconv.Atoi(lastMatch[2]) - tXBytes, _ := strconv.Atoi(lastMatch[4]) - rXBytesOld, _ := strconv.Atoi(firstMatch[2]) - tXBytesOld, _ := strconv.Atoi(firstMatch[4]) - mBits = float64(rXBytes-rXBytesOld+tXBytes-tXBytesOld) * 0.000008 / 5.0 + stationsConfigurations[station] = stationConfiguration{ + Ssid: strconv.Itoa(team.Id), + WpaKey: team.WpaKey, } - return mBits } -// Parses the given data from the access point's association list and updates the status structure with the result. -func (wifiStatus *TeamWifiStatus) parseAssocList(response string) { - radioLinkRe := regexp.MustCompile("((?:[0-9A-F]{2}:){5}(?:[0-9A-F]{2})).*\\(SNR (\\d+)\\)\\s+(\\d+) ms ago") - rxRateRe := regexp.MustCompile("RX:\\s+(\\d+\\.\\d+)\\s+MBit/s") - txRateRe := regexp.MustCompile("TX:\\s+(\\d+\\.\\d+)\\s+MBit/s") - - wifiStatus.RadioLinked = false - wifiStatus.RxRate = 0 - wifiStatus.TxRate = 0 - wifiStatus.SignalNoiseRatio = 0 - for _, radioLinkMatch := range radioLinkRe.FindAllStringSubmatch(response, -1) { - macAddress := radioLinkMatch[1] - dataAgeMs, _ := strconv.Atoi(radioLinkMatch[3]) - if macAddress != "00:00:00:00:00:00" && dataAgeMs <= 4000 { - wifiStatus.RadioLinked = true - wifiStatus.SignalNoiseRatio, _ = strconv.Atoi(radioLinkMatch[2]) - rxRateMatch := rxRateRe.FindStringSubmatch(response) - if len(rxRateMatch) > 0 { - wifiStatus.RxRate, _ = strconv.ParseFloat(rxRateMatch[1], 64) - } - txRateMatch := txRateRe.FindStringSubmatch(response) - if len(txRateMatch) > 0 { - wifiStatus.TxRate, _ = strconv.ParseFloat(txRateMatch[1], 64) - } - break +// 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.IsRobotRadioLinked + teamWifiStatus.MBits = stationStatus.BandwidthUsedMbps + teamWifiStatus.RxRate = stationStatus.RxRateMbps + teamWifiStatus.TxRate = stationStatus.TxRateMbps + teamWifiStatus.SignalNoiseRatio = stationStatus.SignalNoiseRatio + } +} + +// 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 948510a4..5a03f926 100644 --- a/network/access_point_test.go +++ b/network/access_point_test.go @@ -4,284 +4,147 @@ package network import ( - "fmt" - "io/ioutil" - "math" - "regexp" - "strconv" - "testing" - + "encoding/json" "github.com/Team254/cheesy-arena/model" "github.com/stretchr/testify/assert" + "net/http" + "net/http/httptest" + "testing" ) -func TestGenerateTeamAccessPointConfigForLinksys(t *testing.T) { - model.BaseDir = ".." - ap := AccessPoint{isVividType: false} - - ifaceRe := regexp.MustCompile("^set wireless\\.@wifi-iface\\[(\\d)\\]\\.") - disabledRe := regexp.MustCompile("disabled='([-\\w ]+)'") - ssidRe := regexp.MustCompile("ssid='([-\\w ]*)'") - wpaKeyRe := regexp.MustCompile("key='([-\\w ]*)'") - - // Should reject invalid positions. - for _, position := range []int{-1, 0, 7, 8, 254} { - _, err := ap.generateTeamAccessPointConfig(nil, position) - if assert.NotNil(t, err) { - assert.Equal(t, err.Error(), fmt.Sprintf("invalid team position %d", position)) - } - } - - // Should configure dummy values for all team SSIDs if there are no teams. - for position := 1; position <= 6; position++ { - config, _ := ap.generateTeamAccessPointConfig(nil, position) - ifaces := ifaceRe.FindAllStringSubmatch(config, -1) - disableds := disabledRe.FindAllStringSubmatch(config, -1) - ssids := ssidRe.FindAllStringSubmatch(config, -1) - wpaKeys := wpaKeyRe.FindAllStringSubmatch(config, -1) - if assert.Equal(t, 1, len(disableds)) && assert.Equal(t, 1, len(ssids)) && assert.Equal(t, 1, len(wpaKeys)) { - assert.Equal(t, strconv.Itoa(position), ifaces[0][1]) - assert.Equal(t, "0", disableds[0][1]) - assert.Equal(t, fmt.Sprintf("no-team-%d", position), ssids[0][1]) - assert.Equal(t, fmt.Sprintf("no-team-%d", position), wpaKeys[0][1]) - } - } - - // Should configure a different SSID for each team. - for position := 1; position <= 6; position++ { - team := &model.Team{Id: 254 + position, WpaKey: fmt.Sprintf("aaaaaaa%d", position)} - config, _ := ap.generateTeamAccessPointConfig(team, position) - ifaces := ifaceRe.FindAllStringSubmatch(config, -1) - disableds := disabledRe.FindAllStringSubmatch(config, -1) - ssids := ssidRe.FindAllStringSubmatch(config, -1) - wpaKeys := wpaKeyRe.FindAllStringSubmatch(config, -1) - if assert.Equal(t, 1, len(ssids)) && assert.Equal(t, 1, len(wpaKeys)) { - assert.Equal(t, strconv.Itoa(position), ifaces[0][1]) - assert.Equal(t, "0", disableds[0][1]) - assert.Equal(t, strconv.Itoa(team.Id), ssids[0][1]) - assert.Equal(t, fmt.Sprintf("aaaaaaa%d", position), wpaKeys[0][1]) - } - } - - // Should reject a missing WPA key. - _, err := ap.generateTeamAccessPointConfig(&model.Team{Id: 254}, 4) +func TestAccessPoint_ConfigureTeamWifi(t *testing.T) { + var ap AccessPoint + var request configurationRequest + wifiStatuses := [6]*TeamWifiStatus{{}, {}, {}, {}, {}, {}} + ap.SetSettings("dummy", "password1", 123, true, wifiStatuses) + ap.Status = "INITIAL" + + // 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 + + // 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, + ) + + // 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, + ) + + // 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, "INITIAL", ap.Status) } -func TestGenerateTeamAccessPointConfigForVividHosting(t *testing.T) { - model.BaseDir = ".." - ap := AccessPoint{isVividType: true} - - ifaceRe := regexp.MustCompile("^set wireless\\.@wifi-iface\\[(\\d)\\]\\.") - disabledRe := regexp.MustCompile("disabled='([-\\w ]+)'") - ssidRe := regexp.MustCompile("ssid='([-\\w ]*)'") - wpaKeyRe := regexp.MustCompile("key='([-\\w ]*)'") - saePasswordRe := regexp.MustCompile("sae_password='([-\\w ]*)'") - - // Should reject invalid positions. - for _, position := range []int{-1, 0, 7, 8, 254} { - _, err := ap.generateTeamAccessPointConfig(nil, position) - if assert.NotNil(t, err) { - assert.Equal(t, err.Error(), fmt.Sprintf("invalid team position %d", position)) - } - } - - // Should configure dummy values for all team SSIDs if there are no teams. - for position := 1; position <= 6; position++ { - config, _ := ap.generateTeamAccessPointConfig(nil, position) - ifaces := ifaceRe.FindAllStringSubmatch(config, -1) - disableds := disabledRe.FindAllStringSubmatch(config, -1) - ssids := ssidRe.FindAllStringSubmatch(config, -1) - wpaKeys := wpaKeyRe.FindAllStringSubmatch(config, -1) - saePasswords := saePasswordRe.FindAllStringSubmatch(config, -1) - if assert.Equal(t, 1, len(disableds)) && assert.Equal(t, 1, len(ssids)) && assert.Equal(t, 1, len(wpaKeys)) { - assert.Equal(t, strconv.Itoa(position), ifaces[0][1]) - assert.Equal(t, "0", disableds[0][1]) - assert.Equal(t, fmt.Sprintf("no-team-%d", position), ssids[0][1]) - assert.Equal(t, fmt.Sprintf("no-team-%d", position), wpaKeys[0][1]) - assert.Equal(t, fmt.Sprintf("no-team-%d", position), saePasswords[0][1]) - } +func TestAccessPoint_updateMonitoring(t *testing.T) { + var ap AccessPoint + wifiStatuses := [6]*TeamWifiStatus{{}, {}, {}, {}, {}, {}} + ap.SetSettings("dummy", "password2", 123, true, wifiStatuses) + + 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}, + }, } - // Should configure a different SSID for each team. - for position := 1; position <= 6; position++ { - team := &model.Team{Id: 254 + position, WpaKey: fmt.Sprintf("aaaaaaa%d", position)} - config, _ := ap.generateTeamAccessPointConfig(team, position) - ifaces := ifaceRe.FindAllStringSubmatch(config, -1) - disableds := disabledRe.FindAllStringSubmatch(config, -1) - ssids := ssidRe.FindAllStringSubmatch(config, -1) - wpaKeys := wpaKeyRe.FindAllStringSubmatch(config, -1) - saePasswords := saePasswordRe.FindAllStringSubmatch(config, -1) - if assert.Equal(t, 1, len(ssids)) && assert.Equal(t, 1, len(wpaKeys)) { - assert.Equal(t, strconv.Itoa(position), ifaces[0][1]) - assert.Equal(t, "0", disableds[0][1]) - assert.Equal(t, strconv.Itoa(team.Id), ssids[0][1]) - assert.Equal(t, fmt.Sprintf("aaaaaaa%d", position), wpaKeys[0][1]) - assert.Equal(t, fmt.Sprintf("aaaaaaa%d", position), saePasswords[0][1]) - } + // 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 + + // 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, } - - // Should reject a missing WPA key. - _, err := ap.generateTeamAccessPointConfig(&model.Team{Id: 254}, 4) + 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]) + + // 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(), "invalid WPA key") - } -} - -func TestDecodeWifiInfo(t *testing.T) { - statuses := [6]*TeamWifiStatus{ - nil, - &TeamWifiStatus{}, - &TeamWifiStatus{}, - &TeamWifiStatus{}, - nil, - &TeamWifiStatus{}, + assert.Contains(t, err.Error(), "returned status 404: gosh darn") } - ap := AccessPoint{isVividType: true, TeamWifiStatuses: statuses} - - // Test with zero team networks configured. - output, err := ioutil.ReadFile("testdata/iwinfo_0_teams.txt") - if assert.Nil(t, err) { - assert.Nil(t, ap.decodeWifiInfo(string(output))) - assert.Nil(t, statuses[0]) - assert.Equal(t, 0, statuses[1].TeamId) - assert.Equal(t, 0, statuses[2].TeamId) - assert.Equal(t, 0, statuses[3].TeamId) - assert.Nil(t, statuses[4]) - assert.Equal(t, 0, statuses[5].TeamId) - } - - // Test with two team networks configured. - output, err = ioutil.ReadFile("testdata/iwinfo_2_teams.txt") - if assert.Nil(t, err) { - assert.Nil(t, ap.decodeWifiInfo(string(output))) - assert.Nil(t, statuses[0]) - assert.Equal(t, 2471, statuses[1].TeamId) - assert.Equal(t, 0, statuses[2].TeamId) - assert.Equal(t, 254, statuses[3].TeamId) - assert.Nil(t, statuses[4]) - assert.Equal(t, 0, statuses[5].TeamId) - } - - // Test with six team networks configured. - output, err = ioutil.ReadFile("testdata/iwinfo_6_teams.txt") - if assert.Nil(t, err) { - assert.Nil(t, ap.decodeWifiInfo(string(output))) - assert.Nil(t, statuses[0]) - assert.Equal(t, 1678, statuses[1].TeamId) - assert.Equal(t, 2910, statuses[2].TeamId) - assert.Equal(t, 604, statuses[3].TeamId) - assert.Nil(t, statuses[4]) - assert.Equal(t, 2471, statuses[5].TeamId) - } - - // Test with invalid input. - assert.NotNil(t, ap.decodeWifiInfo("")) - output, err = ioutil.ReadFile("testdata/iwinfo_invalid.txt") - if assert.Nil(t, err) { - assert.NotNil(t, ap.decodeWifiInfo(string(output))) - } -} - -func TestParseBtu(t *testing.T) { - // Response is too short. - assert.Equal(t, 0.0, parseBtu("")) - response := "[ 1687496957, 26097, 177, 71670, 865 ],\n" + - "[ 1687496958, 26097, 177, 71734, 866 ],\n" + - "[ 1687496959, 26097, 177, 71734, 866 ],\n" + - "[ 1687496960, 26097, 177, 71798, 867 ],\n" + - "[ 1687496960, 26097, 177, 71798, 867 ],\n" + - "[ 1687496961, 26097, 177, 71798, 867 ]" - assert.Equal(t, 0.0, parseBtu(response)) - - // Response is normal. - response = "[ 1687496917, 26097, 177, 70454, 846 ],\n" + - "[ 1687496919, 26097, 177, 70454, 846 ],\n" + - "[ 1687496920, 26097, 177, 70518, 847 ],\n" + - "[ 1687496920, 26097, 177, 70518, 847 ],\n" + - "[ 1687496921, 26097, 177, 70582, 848 ],\n" + - "[ 1687496922, 26097, 177, 70582, 848 ],\n" + - "[ 1687496923, 2609700, 177, 7064600, 849 ]" - assert.Equal(t, 15.0, math.Floor(parseBtu(response))) - - // Response also includes associated client information. - response = "[ 1687496917, 26097, 177, 70454, 846 ],\n" + - "[ 1687496919, 26097, 177, 70454, 846 ],\n" + - "[ 1687496920, 26097, 177, 70518, 847 ],\n" + - "[ 1687496920, 26097, 177, 70518, 847 ],\n" + - "[ 1687496921, 26097, 177, 70582, 848 ],\n" + - "[ 1687496922, 26097, 177, 70582, 848 ],\n" + - "[ 1687496923, 2609700, 177, 7064600, 849 ]\n" + - "48:DA:35:B0:00:CF -52 dBm / -95 dBm (SNR 43) 1000 ms ago\n" + - "\tRX: 619.4 MBit/s 4095 Pkts.\n" + - "\tTX: 550.6 MBit/s 0 Pkts.\n" + - "\texpected throughput: unknown" - assert.Equal(t, 15.0, math.Floor(parseBtu(response))) -} - -func TestParseAssocList(t *testing.T) { - var wifiStatus TeamWifiStatus - - wifiStatus.parseAssocList("") - assert.Equal(t, TeamWifiStatus{}, wifiStatus) - - // MAC address is invalid. - response := "00:00:00:00:00:00 -53 dBm / -95 dBm (SNR 42) 0 ms ago\n" + - "\tRX: 550.6 MBit/s 4095 Pkts.\n" + - "\tTX: 550.6 MBit/s 0 Pkts.\n" + - "\texpected throughput: unknown" - wifiStatus.parseAssocList(response) - assert.Equal(t, TeamWifiStatus{}, wifiStatus) - - // Link is valid. - response = "48:DA:35:B0:00:CF -53 dBm / -95 dBm (SNR 42) 0 ms ago\n" + - "\tRX: 550.6 MBit/s 4095 Pkts.\n" + - "\tTX: 254.0 MBit/s 0 Pkts.\n" + - "\texpected throughput: unknown" - wifiStatus.parseAssocList(response) - assert.Equal(t, TeamWifiStatus{RadioLinked: true, RxRate: 550.6, TxRate: 254.0, SignalNoiseRatio: 42}, wifiStatus) - response = "48:DA:35:B0:00:CF -53 dBm / -95 dBm (SNR 7) 4000 ms ago\n" + - "\tRX: 123.4 MBit/s 4095 Pkts.\n" + - "\tTX: 550.6 MBit/s 0 Pkts.\n" + - "\texpected throughput: unknown" - wifiStatus.parseAssocList(response) - assert.Equal(t, TeamWifiStatus{RadioLinked: true, RxRate: 123.4, TxRate: 550.6, SignalNoiseRatio: 7}, wifiStatus) - - // Link is stale. - response = "48:DA:35:B0:00:CF -53 dBm / -95 dBm (SNR 42) 4001 ms ago\n" + - "\tRX: 550.6 MBit/s 4095 Pkts.\n" + - "\tTX: 550.6 MBit/s 0 Pkts.\n" + - "\texpected throughput: unknown" - wifiStatus.parseAssocList(response) - assert.Equal(t, TeamWifiStatus{}, wifiStatus) - - // Response also includes BTU information. - response = "[ 1687496917, 26097, 177, 70454, 846 ],\n" + - "[ 1687496919, 26097, 177, 70454, 846 ],\n" + - "[ 1687496920, 26097, 177, 70518, 847 ],\n" + - "[ 1687496920, 26097, 177, 70518, 847 ],\n" + - "[ 1687496921, 26097, 177, 70582, 848 ],\n" + - "[ 1687496922, 26097, 177, 70582, 848 ],\n" + - "[ 1687496923, 2609700, 177, 7064600, 849 ]\n" + - "48:DA:35:B0:00:CF -52 dBm / -95 dBm (SNR 43) 1000 ms ago\n" + - "\tRX: 619.4 MBit/s 4095 Pkts.\n" + - "\tTX: 550.6 MBit/s 0 Pkts.\n" + - "\texpected throughput: unknown" - wifiStatus.parseAssocList(response) - assert.Equal(t, TeamWifiStatus{RadioLinked: true, RxRate: 619.4, TxRate: 550.6, SignalNoiseRatio: 43}, wifiStatus) - response = "[ 1687496917, 26097, 177, 70454, 846 ],\n" + - "[ 1687496919, 26097, 177, 70454, 846 ],\n" + - "[ 1687496920, 26097, 177, 70518, 847 ],\n" + - "[ 1687496920, 26097, 177, 70518, 847 ],\n" + - "[ 1687496921, 26097, 177, 70582, 848 ],\n" + - "[ 1687496922, 26097, 177, 70582, 848 ],\n" + - "[ 1687496923, 2609700, 177, 7064600, 849 ]\n" + - "00:00:00:00:00:00 -52 dBm / -95 dBm (SNR 43) 0 ms ago\n" + - "\tRX: 619.4 MBit/s 4095 Pkts.\n" + - "\tTX: 550.6 MBit/s 0 Pkts.\n" + - "\texpected throughput: unknown" - wifiStatus.parseAssocList(response) - assert.Equal(t, TeamWifiStatus{}, wifiStatus) + assert.Equal(t, "ERROR", ap.Status) } diff --git a/templates/setup_settings.html b/templates/setup_settings.html index 0cbd650e..789da6c6 100644 --- a/templates/setup_settings.html +++ b/templates/setup_settings.html @@ -166,25 +166,6 @@ -
- -
-
- -
-
- -
-
-
@@ -192,41 +173,27 @@
- -
- -
-
-
- +
- -
- -
-
-
- +
- + + + + + + + + {{range $i, $j := seq 29}} - - {{(add 5 (multiply $i 8))}} + {{end}} @@ -244,42 +211,6 @@
-

If you have a second access point (of identical type) available and want to use one for each alliance to - increase available bandwidth, configure the second one below.

-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
PLC @@ -441,16 +372,5 @@ numPlayoffAlliances.val(8); } }; - updateAccessPointType = function(isVividType) { - const apTeamChannel5 = $("select[name=apTeamChannel5]"); - const apTeamChannel6 = $("select[name=apTeamChannel6]"); - if (isVividType) { - apTeamChannel5.prop("disabled", true); - apTeamChannel6.prop("disabled", false); - } else { - apTeamChannel5.prop("disabled", false); - apTeamChannel6.prop("disabled", true); - } - }; {{end}} diff --git a/web/setup_settings.go b/web/setup_settings.go index 57752225..6ab0ec44 100644 --- a/web/setup_settings.go +++ b/web/setup_settings.go @@ -78,19 +78,9 @@ func (web *Web) settingsPostHandler(w http.ResponseWriter, r *http.Request) { eventSettings.TbaSecret = r.PostFormValue("tbaSecret") eventSettings.NexusEnabled = r.PostFormValue("nexusEnabled") == "on" eventSettings.NetworkSecurityEnabled = r.PostFormValue("networkSecurityEnabled") == "on" - eventSettings.ApType = r.PostFormValue("apType") eventSettings.ApAddress = r.PostFormValue("apAddress") - eventSettings.ApUsername = r.PostFormValue("apUsername") eventSettings.ApPassword = r.PostFormValue("apPassword") - if eventSettings.ApType == "vivid" { - eventSettings.ApTeamChannel, _ = strconv.Atoi(r.PostFormValue("apTeamChannel6")) - } else { - eventSettings.ApTeamChannel, _ = strconv.Atoi(r.PostFormValue("apTeamChannel5")) - } - eventSettings.Ap2Address = r.PostFormValue("ap2Address") - eventSettings.Ap2Username = r.PostFormValue("ap2Username") - eventSettings.Ap2Password = r.PostFormValue("ap2Password") - eventSettings.Ap2TeamChannel, _ = strconv.Atoi(r.PostFormValue("ap2TeamChannel")) + eventSettings.ApChannel, _ = strconv.Atoi(r.PostFormValue("apChannel")) eventSettings.SwitchAddress = r.PostFormValue("switchAddress") eventSettings.SwitchPassword = r.PostFormValue("switchPassword") eventSettings.PlcAddress = r.PostFormValue("plcAddress") @@ -106,11 +96,6 @@ func (web *Web) settingsPostHandler(w http.ResponseWriter, r *http.Request) { strconv.Atoi(r.PostFormValue("sustainabilityBonusLinkThresholdWithCoop")) eventSettings.ActivationBonusPointThreshold, _ = strconv.Atoi(r.PostFormValue("activationBonusPointThreshold")) - if eventSettings.Ap2TeamChannel != 0 && eventSettings.Ap2TeamChannel == eventSettings.ApTeamChannel { - web.renderSettings(w, r, "Cannot use same channel for both access points.") - return - } - err := web.arena.Database.UpdateEventSettings(eventSettings) if err != nil { handleWebErr(w, err)