From 478c2a21cd72b3c498626afda82d16fcf03ab4e9 Mon Sep 17 00:00:00 2001 From: Kiet CHau Date: Sat, 20 Jan 2024 14:04:23 -0800 Subject: [PATCH 01/31] Changes that went into the 1.0 firmware --- main.go | 2 +- radio/configuration_request_ap.go | 9 +++++++++ radio/configuration_request_ap_test.go | 16 +++++++++++++++- radio/configuration_request_robot.go | 11 +++++++++-- radio/configuration_request_robot_test.go | 10 ++++++++++ radio/radio_common.go | 3 +++ web/firmware_api.go | 4 ++-- web/web_server_common.go | 4 ++-- 8 files changed, 51 insertions(+), 8 deletions(-) diff --git a/main.go b/main.go index 597f8ea..bc8196e 100644 --- a/main.go +++ b/main.go @@ -7,7 +7,7 @@ import ( "os" ) -const logFilePath = "/root/frc-radio-api.log" +const logFilePath = "/tmp/frc-radio-api.log" func main() { // Set up logging to file. diff --git a/radio/configuration_request_ap.go b/radio/configuration_request_ap.go index c2b23e2..a4b0327 100644 --- a/radio/configuration_request_ap.go +++ b/radio/configuration_request_ap.go @@ -6,8 +6,11 @@ package radio import ( "errors" "fmt" + "regexp" ) +const stationSsidRegex = "^[a-zA-Z0-9-]*$" + // ConfigurationRequest represents a JSON request to configure the radio. type ConfigurationRequest struct { // 5GHz or 6GHz channel number for the radio to use. Set to 0 to leave unchanged. @@ -69,6 +72,9 @@ func (request ConfigurationRequest) Validate(radio *Radio) error { if stationConfiguration.Ssid == "" { return fmt.Errorf("SSID for station %s cannot be blank", stationName) } + if !regexp.MustCompile(stationSsidRegex).MatchString(stationConfiguration.Ssid) { + return fmt.Errorf("invalid SSID for station %s (expecting alphanumeric with hyphens)", stationName) + } if len(stationConfiguration.WpaKey) < minWpaKeyLength || len(stationConfiguration.WpaKey) > maxWpaKeyLength { return fmt.Errorf( "invalid WPA key length for station %s: %d (expecting %d-%d)", @@ -78,6 +84,9 @@ func (request ConfigurationRequest) Validate(radio *Radio) error { maxWpaKeyLength, ) } + if !regexp.MustCompile(alphanumericRegex).MatchString(stationConfiguration.WpaKey) { + return fmt.Errorf("invalid WPA key for station %s (expecting alphanumeric)", stationName) + } } return nil diff --git a/radio/configuration_request_ap_test.go b/radio/configuration_request_ap_test.go index 8b658bd..9b8e194 100644 --- a/radio/configuration_request_ap_test.go +++ b/radio/configuration_request_ap_test.go @@ -41,9 +41,16 @@ func TestConfigurationRequest_Validate(t *testing.T) { err = request.Validate(linksysRadio) assert.EqualError(t, err, "SSID for station blue1 cannot be blank") + // Invalid characters in SSID. + request = ConfigurationRequest{ + StationConfigurations: map[string]StationConfiguration{"blue1": {Ssid: "abc_XYZ", WpaKey: "12345678"}}, + } + err = request.Validate(linksysRadio) + assert.EqualError(t, err, "invalid SSID for station blue1 (expecting alphanumeric with hyphens)") + // Too-short WPA key. request = ConfigurationRequest{ - StationConfigurations: map[string]StationConfiguration{"blue1": {Ssid: "254", WpaKey: "1234567"}}, + StationConfigurations: map[string]StationConfiguration{"blue1": {Ssid: "254-suffix", WpaKey: "1234567"}}, } err = request.Validate(linksysRadio) assert.EqualError(t, err, "invalid WPA key length for station blue1: 7 (expecting 8-16)") @@ -54,4 +61,11 @@ func TestConfigurationRequest_Validate(t *testing.T) { } err = request.Validate(linksysRadio) assert.EqualError(t, err, "invalid WPA key length for station blue1: 17 (expecting 8-16)") + + // Invalid characters in WPA key. + request = ConfigurationRequest{ + StationConfigurations: map[string]StationConfiguration{"blue1": {Ssid: "254", WpaKey: "aAbC2__+#"}}, + } + err = request.Validate(linksysRadio) + assert.EqualError(t, err, "invalid WPA key for station blue1 (expecting alphanumeric)") } diff --git a/radio/configuration_request_robot.go b/radio/configuration_request_robot.go index b7a5935..55f1b02 100644 --- a/radio/configuration_request_robot.go +++ b/radio/configuration_request_robot.go @@ -19,10 +19,11 @@ type ConfigurationRequest struct { // Team number to configure the radio for. Must be between 1 and 25499. TeamNumber int `json:"teamNumber"` - // Team-specific WPA key for the 6GHz network used by the FMS. Must be at least eight characters long. + // Team-specific WPA key for the 6GHz network used by the FMS. Must be at least eight alphanumeric characters long. WpaKey6 string `json:"wpaKey6"` - // WPA key for the 2.4GHz network broadcast by the radio for team use. Must be at least eight characters long. + // WPA key for the 2.4GHz network broadcast by the radio for team use. Must be at least eight alphanumeric + // characters long. WpaKey24 string `json:"wpaKey24"` } @@ -48,12 +49,18 @@ func (request ConfigurationRequest) Validate(radio *Radio) error { "invalid wpaKey6 length: %d (expecting %d-%d)", len(request.WpaKey6), minWpaKeyLength, maxWpaKeyLength, ) } + if !regexp.MustCompile(alphanumericRegex).MatchString(request.WpaKey6) { + return errors.New("invalid wpaKey6 (expecting alphanumeric)") + } if len(request.WpaKey24) < minWpaKeyLength || len(request.WpaKey24) > maxWpaKeyLength { return fmt.Errorf( "invalid wpaKey24 length: %d (expecting %d-%d)", len(request.WpaKey24), minWpaKeyLength, maxWpaKeyLength, ) } + if !regexp.MustCompile(alphanumericRegex).MatchString(request.WpaKey24) { + return errors.New("invalid wpaKey24 (expecting alphanumeric)") + } return nil } diff --git a/radio/configuration_request_robot_test.go b/radio/configuration_request_robot_test.go index 7521c1c..f9f65a7 100644 --- a/radio/configuration_request_robot_test.go +++ b/radio/configuration_request_robot_test.go @@ -53,6 +53,11 @@ func TestConfigurationRequest_Validate(t *testing.T) { err = request.Validate(radio) assert.EqualError(t, err, "invalid wpaKey6 length: 17 (expecting 8-16)") + // Invalid 6GHz WPA key. + request.WpaKey6 = "abc123!@#" + err = request.Validate(radio) + assert.EqualError(t, err, "invalid wpaKey6 (expecting alphanumeric)") + // Too-short 2.4GHz WPA key. request.WpaKey6 = "12345678" request.WpaKey24 = "1234567" @@ -63,4 +68,9 @@ func TestConfigurationRequest_Validate(t *testing.T) { request.WpaKey24 = "12345678123456789" err = request.Validate(radio) assert.EqualError(t, err, "invalid wpaKey24 length: 17 (expecting 8-16)") + + // Invalid 2.4GHz WPA key. + request.WpaKey24 = "abc123!@#" + err = request.Validate(radio) + assert.EqualError(t, err, "invalid wpaKey24 (expecting alphanumeric)") } diff --git a/radio/radio_common.go b/radio/radio_common.go index 0f561b3..be51d2c 100644 --- a/radio/radio_common.go +++ b/radio/radio_common.go @@ -34,6 +34,9 @@ const ( // Maximum length for WPA keys. maxWpaKeyLength = 16 + // Regex to validate a string as alphanumeric. + alphanumericRegex = "^[a-zA-Z0-9]*$" + // Valid characters in the randomly generated salt used to obscure the WPA key. saltCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" diff --git a/web/firmware_api.go b/web/firmware_api.go index 132df46..176f59f 100644 --- a/web/firmware_api.go +++ b/web/firmware_api.go @@ -21,7 +21,7 @@ const ( maxRequestSizeBytes = 64 * 1024 * 1024 // 64 MB // Maximum size of the firmware file that can be held in memory at once (based on device memory limitations). - maxMemorySizeBytes = 20 * 1024 * 1024 // 20 MB + maxMemorySizeBytes = 2 * 1024 * 1024 // 2 MB // Path to the optional file containing the private key for decrypting new firmware. firmwareDecryptionKeyFilePath = "/root/frc-radio-api-firmware-key.txt" @@ -95,7 +95,7 @@ func (web *WebServer) firmwareHandler(w http.ResponseWriter, r *http.Request) { }() w.WriteHeader(http.StatusAccepted) - _, _ = fmt.Fprintln(w, "New firmware received and will be applied now.") + _, _ = fmt.Fprintln(w, "New firmware received and will be applied now. The radio will reboot several times. The firmware upgrade process is complete when the SYS light is slowly blinking.") } // decryptAndSaveFirmwareFile decrypts the given uploaded file and saves it to the hardcoded path for new firmware. diff --git a/web/web_server_common.go b/web/web_server_common.go index 57a1010..e4a28f2 100644 --- a/web/web_server_common.go +++ b/web/web_server_common.go @@ -13,7 +13,7 @@ import ( const ( // TCP port that the web server listens on. - port = 8081 + port = 80 // Path to the optional file containing the password for the API. passwordFilePath = "/root/frc-radio-api-password.txt" @@ -85,7 +85,7 @@ func (web *WebServer) newRouter() http.Handler { // rootHandler redirects the root URL to the status page. func (web *WebServer) rootHandler(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "/status", http.StatusFound) + http.Redirect(w, r, "/configuration", http.StatusFound) } // healthHandler returns a simple "OK" response to indicate that the server is running. From cee441b8fb59b8616a9217ee2326b9a8b8902853 Mon Sep 17 00:00:00 2001 From: Kiet CHau Date: Sat, 27 Apr 2024 09:58:53 -0700 Subject: [PATCH 02/31] post champs 1.1.2 changes --- radio/radio_robot.go | 5 ++++- web/configuration_page.html | 36 +++--------------------------------- 2 files changed, 7 insertions(+), 34 deletions(-) diff --git a/radio/radio_robot.go b/radio/radio_robot.go index e55d73c..0ea61e4 100644 --- a/radio/radio_robot.go +++ b/radio/radio_robot.go @@ -140,6 +140,8 @@ func (radio *Radio) configure(request ConfigurationRequest) error { // Handle IP address when in STA mode. uciTree.SetType("network", "lan", "ipaddr", uci.TypeOption, fmt.Sprintf("10.%s.1", teamPartialIp)) uciTree.SetType("network", "lan", "gateway", uci.TypeOption, fmt.Sprintf("10.%s.4", teamPartialIp)) + uciTree.SetType("dhcp", "lan", "start", uci.TypeOption, "200") + uciTree.SetType("dhcp", "lan", "limit", uci.TypeOption, "20") } else { uciTree.SetType("wireless", wifiInterface6, "mode", uci.TypeOption, "ap") @@ -155,6 +157,8 @@ func (radio *Radio) configure(request ConfigurationRequest) error { // Handle IP address when in AP mode. uciTree.SetType("network", "lan", "ipaddr", uci.TypeOption, fmt.Sprintf("10.%s.4", teamPartialIp)) uciTree.SetType("network", "lan", "gateway", uci.TypeOption, fmt.Sprintf("10.%s.4", teamPartialIp)) + uciTree.SetType("dhcp", "lan", "start", uci.TypeOption, "20") + uciTree.SetType("dhcp", "lan", "limit", uci.TypeOption, "180") } // Handle DHCP. @@ -172,7 +176,6 @@ func (radio *Radio) configure(request ConfigurationRequest) error { } time.Sleep(wifiReloadBackoffDuration) - var err error radio.Ssid, err = getSsid(radioInterface6) if err != nil { return err diff --git a/web/configuration_page.html b/web/configuration_page.html index 6f3255d..a5c9224 100644 --- a/web/configuration_page.html +++ b/web/configuration_page.html @@ -100,69 +100,39 @@
- + -
Wi-Fi 6E 20 MHz channel
+
Wi-Fi 6E Channel
@@ -356,4 +326,4 @@

Firmware Upload

- \ No newline at end of file + From 3cc3473a3c57e7dadab455c9ac1d4fe5774f8e68 Mon Sep 17 00:00:00 2001 From: Patrick Fairbank Date: Fri, 5 Jan 2024 10:05:23 -0800 Subject: [PATCH 03/31] Add tag comments to configuration page files. --- web/configuration_page.go | 1 + web/configuration_page_test.go | 1 + 2 files changed, 2 insertions(+) diff --git a/web/configuration_page.go b/web/configuration_page.go index 66879f0..81e5176 100644 --- a/web/configuration_page.go +++ b/web/configuration_page.go @@ -1,3 +1,4 @@ +// This file is specific to the robot radio version of the API. //go:build robot package web diff --git a/web/configuration_page_test.go b/web/configuration_page_test.go index 986d954..a3c0a03 100644 --- a/web/configuration_page_test.go +++ b/web/configuration_page_test.go @@ -1,3 +1,4 @@ +// This file is specific to the robot radio version of the API. //go:build robot package web From 753c965bca063c955d350036e27eed3aa5df6b62 Mon Sep 17 00:00:00 2001 From: Patrick Fairbank Date: Fri, 5 Jan 2024 10:06:53 -0800 Subject: [PATCH 04/31] Run 'go mod tidy'. --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 45b4330..6f9ad7d 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,13 @@ module github.com/patfair/frc-radio-api go 1.20 require ( + filippo.io/age v1.1.1 github.com/digineo/go-uci v0.0.0-20210918132103-37c7b10c14fa github.com/gorilla/mux v1.8.1 github.com/stretchr/testify v1.8.4 ) require ( - filippo.io/age v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/crypto v0.4.0 // indirect From f5f4df6b5e339793bfe2fbfb6bbfef8272a96b1c Mon Sep 17 00:00:00 2001 From: Patrick Fairbank Date: Sun, 21 Jan 2024 10:58:38 -0800 Subject: [PATCH 05/31] Use different HTTP ports between Linksys and Vivid-Hosting and fix tests. --- radio/configuration_request_ap.go | 4 ++-- radio/configuration_request_ap_test.go | 8 ++++---- radio/radio_ap.go | 16 ++++++++-------- radio/radio_ap_test.go | 4 ++-- radio/radio_common.go | 12 ++++++------ radio/radiotype_string.go | 20 ++++++++++---------- web/web_server_ap.go | 22 +++++++++++++++++++++- web/web_server_ap_test.go | 22 ++++++++++++++++++++-- web/web_server_common.go | 10 +--------- web/web_server_common_test.go | 7 ------- web/web_server_robot.go | 12 +++++++++++- web/web_server_robot_test.go | 22 ++++++++++++++++++++++ 12 files changed, 107 insertions(+), 52 deletions(-) create mode 100644 web/web_server_robot_test.go diff --git a/radio/configuration_request_ap.go b/radio/configuration_request_ap.go index a4b0327..1b03bda 100644 --- a/radio/configuration_request_ap.go +++ b/radio/configuration_request_ap.go @@ -42,14 +42,14 @@ func (request ConfigurationRequest) Validate(radio *Radio) error { // Validate channel number. valid := false switch radio.Type { - case typeLinksys: + case TypeLinksys: for _, channel := range validLinksysChannels { if request.Channel == channel { valid = true break } } - case typeVividHosting: + case TypeVividHosting: valid = isValid6GhzChannel(request.Channel) } if !valid { diff --git a/radio/configuration_request_ap_test.go b/radio/configuration_request_ap_test.go index 9b8e194..1e4fb2a 100644 --- a/radio/configuration_request_ap_test.go +++ b/radio/configuration_request_ap_test.go @@ -9,8 +9,8 @@ import ( ) func TestConfigurationRequest_Validate(t *testing.T) { - linksysRadio := &Radio{Type: typeLinksys} - vividHostingRadio := &Radio{Type: typeVividHosting} + linksysRadio := &Radio{Type: TypeLinksys} + vividHostingRadio := &Radio{Type: TypeVividHosting} // Empty request. request := ConfigurationRequest{} @@ -20,12 +20,12 @@ func TestConfigurationRequest_Validate(t *testing.T) { // Invalid 5GHz channel. request.Channel = 5 err = request.Validate(linksysRadio) - assert.EqualError(t, err, "invalid channel for typeLinksys: 5") + assert.EqualError(t, err, "invalid channel for TypeLinksys: 5") // Invalid 6GHz channel. request.Channel = 36 err = request.Validate(vividHostingRadio) - assert.EqualError(t, err, "invalid channel for typeVividHosting: 36") + assert.EqualError(t, err, "invalid channel for TypeVividHosting: 36") // Invalid station. request = ConfigurationRequest{ diff --git a/radio/radio_ap.go b/radio/radio_ap.go index 79fc89c..df06c4e 100644 --- a/radio/radio_ap.go +++ b/radio/radio_ap.go @@ -35,7 +35,7 @@ type Radio struct { ConfigurationRequestChannel chan ConfigurationRequest `json:"-"` // Hardware type of the radio. - Type radioType `json:"-"` + Type RadioType `json:"-"` // Name of the radio's Wi-Fi device, dependent on the hardware type. device string @@ -51,7 +51,7 @@ func NewRadio() *Radio { ConfigurationRequestChannel: make(chan ConfigurationRequest, configurationRequestBufferSize), } radio.determineAndSetType() - if radio.Type == typeUnknown { + if radio.Type == TypeUnknown { log.Fatal("Unable to determine radio hardware type; exiting.") } log.Printf("Detected radio hardware type: %v", radio.Type) @@ -59,7 +59,7 @@ func NewRadio() *Radio { // Initialize the device and station interface names that are dependent on the hardware type. switch radio.Type { - case typeLinksys: + case TypeLinksys: radio.device = "radio0" radio.stationInterfaces = map[station]string{ red1: "wlan0", @@ -69,7 +69,7 @@ func NewRadio() *Radio { blue2: "wlan0-4", blue3: "wlan0-5", } - case typeVividHosting: + case TypeVividHosting: radio.device = "wifi1" radio.stationInterfaces = map[station]string{ red1: "ath1", @@ -93,9 +93,9 @@ func NewRadio() *Radio { func (radio *Radio) determineAndSetType() { model, _ := uciTree.GetLast("system", "@system[0]", "model") if strings.Contains(model, "VH") { - radio.Type = typeVividHosting + radio.Type = TypeVividHosting } else { - radio.Type = typeLinksys + radio.Type = TypeLinksys } } @@ -119,7 +119,7 @@ func (radio *Radio) configure(request ConfigurationRequest) error { radio.Channel = request.Channel } - if radio.Type == typeLinksys { + if radio.Type == TypeLinksys { // Clear the state of the radio before loading teams; the Linksys AP is crash-prone otherwise. if err := radio.configureStations(map[string]StationConfiguration{}); err != nil { return err @@ -148,7 +148,7 @@ func (radio *Radio) configureStations(stationConfigurations map[string]StationCo wifiInterface := fmt.Sprintf("@wifi-iface[%d]", position) uciTree.SetType("wireless", wifiInterface, "ssid", uci.TypeOption, ssid) uciTree.SetType("wireless", wifiInterface, "key", uci.TypeOption, wpaKey) - if radio.Type == typeVividHosting { + if radio.Type == TypeVividHosting { uciTree.SetType("wireless", wifiInterface, "sae_password", uci.TypeOption, wpaKey) } diff --git a/radio/radio_ap_test.go b/radio/radio_ap_test.go index d0a905f..8b3d875 100644 --- a/radio/radio_ap_test.go +++ b/radio/radio_ap_test.go @@ -26,7 +26,7 @@ func TestNewRadio(t *testing.T) { assert.Nil(t, stationStatus) } } - assert.Equal(t, typeVividHosting, radio.Type) + assert.Equal(t, TypeVividHosting, radio.Type) assert.NotNil(t, radio.ConfigurationRequestChannel) assert.Equal(t, "wifi1", radio.device) assert.Equal( @@ -54,7 +54,7 @@ func TestNewRadio(t *testing.T) { assert.Nil(t, stationStatus) } } - assert.Equal(t, typeLinksys, radio.Type) + assert.Equal(t, TypeLinksys, radio.Type) assert.NotNil(t, radio.ConfigurationRequestChannel) assert.Equal(t, "radio0", radio.device) assert.Equal( diff --git a/radio/radio_common.go b/radio/radio_common.go index be51d2c..d129274 100644 --- a/radio/radio_common.go +++ b/radio/radio_common.go @@ -44,15 +44,15 @@ const ( saltLength = 16 ) -// radioType represents the hardware type of the radio. +// RadioType represents the hardware type of the radio. // -//go:generate stringer -type=radioType -type radioType int +//go:generate stringer -type=RadioType +type RadioType int const ( - typeUnknown radioType = iota - typeLinksys - typeVividHosting + TypeUnknown RadioType = iota + TypeLinksys + TypeVividHosting ) // radioStatus represents the configuration stage of the radio. diff --git a/radio/radiotype_string.go b/radio/radiotype_string.go index f1f1118..c4f4a91 100644 --- a/radio/radiotype_string.go +++ b/radio/radiotype_string.go @@ -1,4 +1,4 @@ -// Code generated by "stringer -type=radioType"; DO NOT EDIT. +// Code generated by "stringer -type=RadioType"; DO NOT EDIT. package radio @@ -8,18 +8,18 @@ func _() { // An "invalid array index" compiler error signifies that the constant values have changed. // Re-run the stringer command to generate them again. var x [1]struct{} - _ = x[typeUnknown-0] - _ = x[typeLinksys-1] - _ = x[typeVividHosting-2] + _ = x[TypeUnknown-0] + _ = x[TypeLinksys-1] + _ = x[TypeVividHosting-2] } -const _radioType_name = "typeUnknowntypeLinksystypeVividHosting" +const _RadioType_name = "TypeUnknownTypeLinksysTypeVividHosting" -var _radioType_index = [...]uint8{0, 11, 22, 38} +var _RadioType_index = [...]uint8{0, 11, 22, 38} -func (i radioType) String() string { - if i < 0 || i >= radioType(len(_radioType_index)-1) { - return "radioType(" + strconv.FormatInt(int64(i), 10) + ")" +func (i RadioType) String() string { + if i < 0 || i >= RadioType(len(_RadioType_index)-1) { + return "RadioType(" + strconv.FormatInt(int64(i), 10) + ")" } - return _radioType_name[_radioType_index[i]:_radioType_index[i+1]] + return _RadioType_name[_RadioType_index[i]:_RadioType_index[i+1]] } diff --git a/web/web_server_ap.go b/web/web_server_ap.go index 51793c4..78bb27e 100644 --- a/web/web_server_ap.go +++ b/web/web_server_ap.go @@ -6,14 +6,29 @@ package web import ( "fmt" "github.com/gorilla/mux" + "github.com/patfair/frc-radio-api/radio" "log" "net" + "net/http" "regexp" "time" ) +const ( + // TCP port that the web server listens on. + portLinksys = 8081 + portVividHosting = 80 +) + // getListenAddress returns the address and port that the web server should listen on. -func getListenAddress() string { +func getListenAddress(r *radio.Radio) string { + var port int + if r.Type == radio.TypeLinksys { + port = portLinksys + } else { + port = portVividHosting + } + var ipAddress string for { var err error @@ -54,3 +69,8 @@ func getVlan100IpAddress() (string, error) { // addRoutes adds additional route handlers to the router if needed. func addRoutes(router *mux.Router, web *WebServer) {} + +// rootHandler redirects the root URL to the status page. +func (web *WebServer) rootHandler(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/status", http.StatusFound) +} diff --git a/web/web_server_ap_test.go b/web/web_server_ap_test.go index 3680132..afc3585 100644 --- a/web/web_server_ap_test.go +++ b/web/web_server_ap_test.go @@ -1,22 +1,40 @@ +// This file is specific to the access point version of the API. //go:build !robot -// This file is specific to the access point version of the API. package web import ( + "github.com/patfair/frc-radio-api/radio" "github.com/stretchr/testify/assert" "testing" ) func TestGetVlan100IpAddress(t *testing.T) { ipAddress, err := getVlan100IpAddress() + r := &radio.Radio{Type: radio.TypeLinksys} // Branch the test verification logic since it may or may not be Run on a system with a 10.0.100.x interface and // mocking the system calls to be deterministic is onerous. if err == nil { assert.Regexp(t, "^10\\.0\\.100\\.\\d+$", ipAddress) - assert.Equal(t, ipAddress+":8081", getListenAddress()) + assert.Equal(t, ipAddress+":8081", getListenAddress(r)) + } else { + assert.Contains(t, err.Error(), "no IP address found on VLAN 100") + } + + // Change the type to Vivid-Hosting and check that the port is different. + r.Type = radio.TypeVividHosting + if err == nil { + assert.Regexp(t, "^10\\.0\\.100\\.\\d+$", ipAddress) + assert.Equal(t, ipAddress+":80", getListenAddress(r)) } else { assert.Contains(t, err.Error(), "no IP address found on VLAN 100") } } + +func TestWeb_rootHandler(t *testing.T) { + var web WebServer + recorder := web.getHttpResponse("/") + assert.Equal(t, 302, recorder.Code) + assert.Equal(t, "/status", recorder.Header().Get("Location")) +} diff --git a/web/web_server_common.go b/web/web_server_common.go index e4a28f2..8d35c49 100644 --- a/web/web_server_common.go +++ b/web/web_server_common.go @@ -12,9 +12,6 @@ import ( ) const ( - // TCP port that the web server listens on. - port = 80 - // Path to the optional file containing the password for the API. passwordFilePath = "/root/frc-radio-api-password.txt" @@ -43,7 +40,7 @@ func NewWebServer(radio *radio.Radio) *WebServer { func (web *WebServer) Run() { web.setUpSecrets() - listenAddress := getListenAddress() + listenAddress := getListenAddress(web.radio) log.Printf("Server listening on %s\n", listenAddress) if err := http.ListenAndServe(listenAddress, web.newRouter()); err != nil { log.Fatal(err) @@ -83,11 +80,6 @@ func (web *WebServer) newRouter() http.Handler { return router } -// rootHandler redirects the root URL to the status page. -func (web *WebServer) rootHandler(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "/configuration", http.StatusFound) -} - // healthHandler returns a simple "OK" response to indicate that the server is running. func (web *WebServer) healthHandler(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprintln(w, "OK") diff --git a/web/web_server_common_test.go b/web/web_server_common_test.go index 433aa46..9153fa3 100644 --- a/web/web_server_common_test.go +++ b/web/web_server_common_test.go @@ -5,13 +5,6 @@ import ( "testing" ) -func TestWeb_rootHandler(t *testing.T) { - var web WebServer - recorder := web.getHttpResponse("/") - assert.Equal(t, 302, recorder.Code) - assert.Equal(t, recorder.Header().Get("Location"), "/status") -} - func TestWeb_healthHandler(t *testing.T) { var web WebServer recorder := web.getHttpResponse("/health") diff --git a/web/web_server_robot.go b/web/web_server_robot.go index 9370e67..0fcf054 100644 --- a/web/web_server_robot.go +++ b/web/web_server_robot.go @@ -6,10 +6,15 @@ package web import ( "fmt" "github.com/gorilla/mux" + "github.com/patfair/frc-radio-api/radio" + "net/http" ) +// TCP port that the web server listens on. +const port = 80 + // getListenAddress returns the address and port that the web server should listen on. -func getListenAddress() string { +func getListenAddress(r *radio.Radio) string { return fmt.Sprintf(":%d", port) } @@ -17,3 +22,8 @@ func getListenAddress() string { func addRoutes(router *mux.Router, web *WebServer) { router.HandleFunc("/configuration", web.configurationPageHandler).Methods("GET") } + +// rootHandler redirects the root URL to the configuration page. +func (web *WebServer) rootHandler(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/configuration", http.StatusFound) +} diff --git a/web/web_server_robot_test.go b/web/web_server_robot_test.go new file mode 100644 index 0000000..5829a6e --- /dev/null +++ b/web/web_server_robot_test.go @@ -0,0 +1,22 @@ +// This file is specific to the robot radio version of the API. +//go:build robot + +package web + +import ( + "github.com/patfair/frc-radio-api/radio" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestGetListenAddress(t *testing.T) { + r := &radio.Radio{} + assert.Equal(t, ":80", getListenAddress(r)) +} + +func TestWeb_rootHandler(t *testing.T) { + var web WebServer + recorder := web.getHttpResponse("/") + assert.Equal(t, 302, recorder.Code) + assert.Equal(t, "/configuration", recorder.Header().Get("Location")) +} From 6bc931d2634c3ab80b6b47adb450e3bbabbb7297 Mon Sep 17 00:00:00 2001 From: Patrick Fairbank Date: Sun, 21 Jan 2024 11:11:26 -0800 Subject: [PATCH 06/31] Set up rotation of logs. --- main.go | 43 +++++++++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/main.go b/main.go index bc8196e..09d7969 100644 --- a/main.go +++ b/main.go @@ -7,18 +7,19 @@ import ( "os" ) -const logFilePath = "/tmp/frc-radio-api.log" +const ( + // Path of the current log file. + logFilePath = "/root/frc-radio-api.log" + + // Path of the old log file, which is rotated when the current log file gets too big. + oldLogFilePath = "/root/frc-radio-api.log.old" + + // Maximum size of the current log file in bytes. + logFileMaxSizeBytes = 3 * 1 << 19 // 1.5 MB +) func main() { - // Set up logging to file. - logFile, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666) - if err == nil { - defer logFile.Close() - log.SetOutput(logFile) - } else { - log.Printf("error opening log file; logging to stdout instead: %v", err) - } - log.Println("Starting FRC Radio API...") + setupLogging() radio := radio.NewRadio() @@ -29,3 +30,25 @@ func main() { // Run the radio event loop in the main thread. radio.Run() } + +// setupLogging sets up logging to a file, or to stdout if the file can't be opened. +func setupLogging() { + // Rotate the log file if the current one is too big. + if fileInfo, err := os.Stat(logFilePath); err == nil { + if fileInfo.Size() >= logFileMaxSizeBytes { + if err := os.Rename(logFilePath, oldLogFilePath); err != nil { + log.Printf("error rotating log file: %v", err) + } + } + } + + logFile, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666) + if err == nil { + defer logFile.Close() + log.SetOutput(logFile) + } else { + log.Printf("error opening log file; logging to stdout instead: %v", err) + } + log.Println("Starting FRC Radio API...") + +} From 993d17ca753d24385e1003fea5de51f2cbca837b Mon Sep 17 00:00:00 2001 From: Patrick Fairbank Date: Mon, 3 Jun 2024 20:39:43 -0700 Subject: [PATCH 07/31] Add support for setting the channel bandwidth. --- README.md | 3 +++ main.go | 4 +++- radio/configuration_request_ap.go | 16 +++++++++++++++- radio/configuration_request_ap_test.go | 10 ++++++++++ radio/radio_ap.go | 8 ++++++++ web/configuration_api_test.go | 3 +++ 6 files changed, 42 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 40eb578..f1f5c22 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ The `/status` GET endpoint returns the current status of the access point. It re $ curl http://10.0.100.2:8081/status { "channel": 93, + "channelBandwidth": "HT40", "status": "ACTIVE", "stationStatuses": { "blue1": null, @@ -92,6 +93,7 @@ The `/configuration` POST endpoint allows the access point to be configured. It ``` $ curl http://10.0.100.2:8081/configuration -XPOST -d '{ "channel": 93, + "channelBandwidth": "HT20", "stationConfigurations": { "red1": {"ssid": "1111", "wpaKey": "11111111"}, "blue2": {"ssid": "5555", "wpaKey": "55555555"} @@ -105,6 +107,7 @@ The `/status` endpoint can then be polled to check whether the configuration has $ curl http://10.0.100.2:8081/status { "channel": 93, + "channelBandwidth": "HT20", "status": "CONFIGURING", "stationStatuses": { "blue1": null, diff --git a/main.go b/main.go index 09d7969..dcaa126 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "github.com/patfair/frc-radio-api/radio" "github.com/patfair/frc-radio-api/web" "log" @@ -22,9 +23,11 @@ func main() { setupLogging() radio := radio.NewRadio() + fmt.Println("created radio") // Launch the web server in a separate thread. webServer := web.NewWebServer(radio) + fmt.Println("created webserver") go webServer.Run() // Run the radio event loop in the main thread. @@ -50,5 +53,4 @@ func setupLogging() { log.Printf("error opening log file; logging to stdout instead: %v", err) } log.Println("Starting FRC Radio API...") - } diff --git a/radio/configuration_request_ap.go b/radio/configuration_request_ap.go index 1b03bda..aed5905 100644 --- a/radio/configuration_request_ap.go +++ b/radio/configuration_request_ap.go @@ -16,6 +16,10 @@ type ConfigurationRequest struct { // 5GHz or 6GHz channel number for the radio to use. Set to 0 to leave unchanged. Channel int `json:"channel"` + // Channel bandwidth mode for the radio to use. Valid values are "HT20" and "HT40". Set to an empty string to leave + // unchanged. + ChannelBandwidth string `json:"channelBandwidth"` + // SSID and WPA key for each team station, keyed by alliance and number (e.g. "red1", "blue3). If a station is not // included, its network will be disabled by setting its SSID to a placeholder. StationConfigurations map[string]StationConfiguration `json:"stationConfigurations"` @@ -34,7 +38,7 @@ var validLinksysChannels = []int{36, 40, 44, 48, 149, 153, 157, 161, 165} // Validate checks that all parameters within the configuration request have valid values. func (request ConfigurationRequest) Validate(radio *Radio) error { - if request.Channel == 0 && len(request.StationConfigurations) == 0 { + if request.Channel == 0 && request.ChannelBandwidth == "" && len(request.StationConfigurations) == 0 { return errors.New("empty configuration request") } @@ -57,6 +61,16 @@ func (request ConfigurationRequest) Validate(radio *Radio) error { } } + if request.ChannelBandwidth != "" { + // Validate channel bandwidth. + if radio.Type == TypeLinksys { + return fmt.Errorf("channel bandwidth cannot be changed on %s", radio.Type.String()) + } + if request.ChannelBandwidth != "HT20" && request.ChannelBandwidth != "HT40" { + return fmt.Errorf("invalid channel bandwidth: %s", request.ChannelBandwidth) + } + } + // Validate station configurations. for stationName, stationConfiguration := range request.StationConfigurations { stationNameValid := false diff --git a/radio/configuration_request_ap_test.go b/radio/configuration_request_ap_test.go index 1e4fb2a..30843aa 100644 --- a/radio/configuration_request_ap_test.go +++ b/radio/configuration_request_ap_test.go @@ -27,6 +27,16 @@ func TestConfigurationRequest_Validate(t *testing.T) { err = request.Validate(vividHostingRadio) assert.EqualError(t, err, "invalid channel for TypeVividHosting: 36") + // Invalid channel bandwidth. + request = ConfigurationRequest{ChannelBandwidth: "HT30"} + err = request.Validate(vividHostingRadio) + assert.EqualError(t, err, "invalid channel bandwidth: HT30") + + // Channel bandwidth not supported on Linksys. + request = ConfigurationRequest{ChannelBandwidth: "HT20"} + err = request.Validate(linksysRadio) + assert.EqualError(t, err, "channel bandwidth cannot be changed on TypeLinksys") + // Invalid station. request = ConfigurationRequest{ StationConfigurations: map[string]StationConfiguration{"red4": {Ssid: "254", WpaKey: "12345678"}}, diff --git a/radio/radio_ap.go b/radio/radio_ap.go index df06c4e..ade2cb7 100644 --- a/radio/radio_ap.go +++ b/radio/radio_ap.go @@ -22,6 +22,9 @@ type Radio struct { // 5GHz or 6GHz channel number the radio is broadcasting on. Channel int `json:"channel"` + // Channel bandwidth mode for the radio to use. Valid values are "HT20" and "HT40". + ChannelBandwidth string `json:"channelWidth"` + // Enum representing the current configuration stage of the radio. Status radioStatus `json:"status"` @@ -109,6 +112,7 @@ func (radio *Radio) isStarted() bool { func (radio *Radio) setInitialState() { channel, _ := uciTree.GetLast("wireless", radio.device, "channel") radio.Channel, _ = strconv.Atoi(channel) + radio.ChannelBandwidth, _ = uciTree.GetLast("wireless", radio.device, "htmode") _ = radio.updateStationStatuses() } @@ -118,6 +122,10 @@ func (radio *Radio) configure(request ConfigurationRequest) error { uciTree.SetType("wireless", radio.device, "channel", uci.TypeOption, strconv.Itoa(request.Channel)) radio.Channel = request.Channel } + if request.ChannelBandwidth != "" { + uciTree.SetType("wireless", radio.device, "htmode", uci.TypeOption, request.ChannelBandwidth) + radio.ChannelBandwidth = request.ChannelBandwidth + } if radio.Type == TypeLinksys { // Clear the state of the radio before loading teams; the Linksys AP is crash-prone otherwise. diff --git a/web/configuration_api_test.go b/web/configuration_api_test.go index 6929b8b..61e0a30 100644 --- a/web/configuration_api_test.go +++ b/web/configuration_api_test.go @@ -11,6 +11,7 @@ import ( func TestWeb_configurationHandler(t *testing.T) { ap := radio.NewRadio() + ap.Type = radio.TypeVividHosting web := NewWebServer(ap) // Empty request should result in an error. @@ -40,6 +41,7 @@ func TestWeb_configurationHandler(t *testing.T) { ` { "channel": 149, + "channelBandwidth": "HT20", "stationConfigurations": { "red1": {"ssid": "9991", "wpaKey": "11111111"}, "red2": {"ssid": "9992", "wpaKey": "22222222"}, @@ -56,6 +58,7 @@ func TestWeb_configurationHandler(t *testing.T) { if assert.Equal(t, 1, len(ap.ConfigurationRequestChannel)) { request := <-ap.ConfigurationRequestChannel assert.Equal(t, 149, request.Channel) + assert.Equal(t, "HT20", request.ChannelBandwidth) assert.Equal(t, 6, len(request.StationConfigurations)) assert.Equal( t, radio.StationConfiguration{Ssid: "9991", WpaKey: "11111111"}, request.StationConfigurations["red1"], From 44d9108e943b76215a74bc207188ab60f26bb120 Mon Sep 17 00:00:00 2001 From: Patrick Fairbank Date: Mon, 3 Jun 2024 20:47:45 -0700 Subject: [PATCH 08/31] Fix build/test failures from vh109-webpage branch. --- radio/radio_robot.go | 1 + radio/radio_robot_test.go | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/radio/radio_robot.go b/radio/radio_robot.go index 0ea61e4..f358c58 100644 --- a/radio/radio_robot.go +++ b/radio/radio_robot.go @@ -176,6 +176,7 @@ func (radio *Radio) configure(request ConfigurationRequest) error { } time.Sleep(wifiReloadBackoffDuration) + var err error radio.Ssid, err = getSsid(radioInterface6) if err != nil { return err diff --git a/radio/radio_robot_test.go b/radio/radio_robot_test.go index 2f241f7..c35c97b 100644 --- a/radio/radio_robot_test.go +++ b/radio/radio_robot_test.go @@ -97,7 +97,7 @@ func TestRadio_handleConfigurationRequest(t *testing.T) { radio.ConfigurationRequestChannel <- dummyRequest2 radio.ConfigurationRequestChannel <- request assert.Nil(t, radio.handleConfigurationRequest(dummyRequest1)) - assert.Equal(t, 16, fakeTree.setCount) + assert.Equal(t, 18, fakeTree.setCount) assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].ssid"], "12345") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].key"], "11111111") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].mode"], "sta") @@ -109,7 +109,8 @@ func TestRadio_handleConfigurationRequest(t *testing.T) { assert.Equal(t, fakeTree.valuesFromSet["wireless.wifi0.disabled"], "0") assert.Equal(t, fakeTree.valuesFromSet["network.lan.ipaddr"], "10.123.45.1") assert.Equal(t, fakeTree.valuesFromSet["network.lan.gateway"], "10.123.45.4") - assert.Equal(t, fakeTree.valuesFromSet["dhcp.@host[-1]"], "***DELETED***") + assert.Equal(t, fakeTree.valuesFromSet["dhcp.lan.start"], "200") + assert.Equal(t, fakeTree.valuesFromSet["dhcp.lan.limit"], "20") assert.Equal(t, fakeTree.valuesFromSet["dhcp.@host[0]"], "***ADDED***") assert.Equal(t, fakeTree.valuesFromSet["dhcp.lan.dhcp_option"], "3,10.123.45.4") assert.Equal(t, fakeTree.valuesFromSet["dhcp.@host[0].name"], "roboRIO-12345-FRC") @@ -133,7 +134,7 @@ func TestRadio_handleConfigurationRequest(t *testing.T) { fakeTree.valuesForGet["wireless.@wifi-iface[1].key"] = "11111111" request = ConfigurationRequest{Mode: modeTeamAccessPoint, TeamNumber: 12345, WpaKey6: "11111111", Channel: 229} assert.Nil(t, radio.handleConfigurationRequest(request)) - assert.Equal(t, 12, fakeTree.setCount) + assert.Equal(t, 14, fakeTree.setCount) assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].ssid"], "12345") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].key"], "11111111") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].mode"], "ap") @@ -141,6 +142,8 @@ func TestRadio_handleConfigurationRequest(t *testing.T) { assert.Equal(t, fakeTree.valuesFromSet["wireless.wifi0.disabled"], "1") assert.Equal(t, fakeTree.valuesFromSet["network.lan.ipaddr"], "10.123.45.4") assert.Equal(t, fakeTree.valuesFromSet["network.lan.gateway"], "10.123.45.4") + assert.Equal(t, fakeTree.valuesFromSet["dhcp.lan.start"], "20") + assert.Equal(t, fakeTree.valuesFromSet["dhcp.lan.limit"], "180") assert.Equal(t, fakeTree.valuesFromSet["dhcp.@host[-1]"], "***DELETED***") assert.Equal(t, fakeTree.valuesFromSet["dhcp.@host[0]"], "***ADDED***") assert.Equal(t, fakeTree.valuesFromSet["dhcp.lan.dhcp_option"], "3,10.123.45.4") From b8abfef24a2d73ebedafd96fa77b096bbef0d733 Mon Sep 17 00:00:00 2001 From: Patrick Fairbank Date: Mon, 3 Jun 2024 20:50:31 -0700 Subject: [PATCH 09/31] Fix name of channel bandwidth parameter. --- radio/radio_ap.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/radio/radio_ap.go b/radio/radio_ap.go index ade2cb7..a9865c6 100644 --- a/radio/radio_ap.go +++ b/radio/radio_ap.go @@ -23,7 +23,7 @@ type Radio struct { Channel int `json:"channel"` // Channel bandwidth mode for the radio to use. Valid values are "HT20" and "HT40". - ChannelBandwidth string `json:"channelWidth"` + ChannelBandwidth string `json:"channelBandwidth"` // Enum representing the current configuration stage of the radio. Status radioStatus `json:"status"` From 120b3e3064c491bef737b6ea274a600499af1443 Mon Sep 17 00:00:00 2001 From: Patrick Fairbank Date: Mon, 3 Jun 2024 21:07:05 -0700 Subject: [PATCH 10/31] Change channel bandwidth valid values to '20MHz' and '40MHz' instead of 'HT20' and 'HT40'. --- radio/configuration_request_ap.go | 6 +++--- radio/configuration_request_ap_test.go | 6 +++--- radio/radio_ap.go | 23 ++++++++++++++++++++--- radio/radio_ap_test.go | 2 ++ web/configuration_api_test.go | 4 ++-- 5 files changed, 30 insertions(+), 11 deletions(-) diff --git a/radio/configuration_request_ap.go b/radio/configuration_request_ap.go index aed5905..9503f6a 100644 --- a/radio/configuration_request_ap.go +++ b/radio/configuration_request_ap.go @@ -16,8 +16,8 @@ type ConfigurationRequest struct { // 5GHz or 6GHz channel number for the radio to use. Set to 0 to leave unchanged. Channel int `json:"channel"` - // Channel bandwidth mode for the radio to use. Valid values are "HT20" and "HT40". Set to an empty string to leave - // unchanged. + // Channel bandwidth mode for the radio to use. Valid values are "20MHz" and "40MHz". Set to an empty string to + // leave unchanged. ChannelBandwidth string `json:"channelBandwidth"` // SSID and WPA key for each team station, keyed by alliance and number (e.g. "red1", "blue3). If a station is not @@ -66,7 +66,7 @@ func (request ConfigurationRequest) Validate(radio *Radio) error { if radio.Type == TypeLinksys { return fmt.Errorf("channel bandwidth cannot be changed on %s", radio.Type.String()) } - if request.ChannelBandwidth != "HT20" && request.ChannelBandwidth != "HT40" { + if request.ChannelBandwidth != "20MHz" && request.ChannelBandwidth != "40MHz" { return fmt.Errorf("invalid channel bandwidth: %s", request.ChannelBandwidth) } } diff --git a/radio/configuration_request_ap_test.go b/radio/configuration_request_ap_test.go index 30843aa..fdd0a17 100644 --- a/radio/configuration_request_ap_test.go +++ b/radio/configuration_request_ap_test.go @@ -28,12 +28,12 @@ func TestConfigurationRequest_Validate(t *testing.T) { assert.EqualError(t, err, "invalid channel for TypeVividHosting: 36") // Invalid channel bandwidth. - request = ConfigurationRequest{ChannelBandwidth: "HT30"} + request = ConfigurationRequest{ChannelBandwidth: "30MHz"} err = request.Validate(vividHostingRadio) - assert.EqualError(t, err, "invalid channel bandwidth: HT30") + assert.EqualError(t, err, "invalid channel bandwidth: 30MHz") // Channel bandwidth not supported on Linksys. - request = ConfigurationRequest{ChannelBandwidth: "HT20"} + request = ConfigurationRequest{ChannelBandwidth: "20MHz"} err = request.Validate(linksysRadio) assert.EqualError(t, err, "channel bandwidth cannot be changed on TypeLinksys") diff --git a/radio/radio_ap.go b/radio/radio_ap.go index a9865c6..fe61e10 100644 --- a/radio/radio_ap.go +++ b/radio/radio_ap.go @@ -22,7 +22,7 @@ type Radio struct { // 5GHz or 6GHz channel number the radio is broadcasting on. Channel int `json:"channel"` - // Channel bandwidth mode for the radio to use. Valid values are "HT20" and "HT40". + // Channel bandwidth mode for the radio to use. Valid values are "20MHz" and "40MHz". ChannelBandwidth string `json:"channelBandwidth"` // Enum representing the current configuration stage of the radio. @@ -112,7 +112,15 @@ func (radio *Radio) isStarted() bool { func (radio *Radio) setInitialState() { channel, _ := uciTree.GetLast("wireless", radio.device, "channel") radio.Channel, _ = strconv.Atoi(channel) - radio.ChannelBandwidth, _ = uciTree.GetLast("wireless", radio.device, "htmode") + htmode, _ := uciTree.GetLast("wireless", radio.device, "htmode") + switch htmode { + case "HT20": + radio.ChannelBandwidth = "20MHz" + case "HT40": + radio.ChannelBandwidth = "40MHz" + default: + radio.ChannelBandwidth = "INVALID" + } _ = radio.updateStationStatuses() } @@ -123,7 +131,16 @@ func (radio *Radio) configure(request ConfigurationRequest) error { radio.Channel = request.Channel } if request.ChannelBandwidth != "" { - uciTree.SetType("wireless", radio.device, "htmode", uci.TypeOption, request.ChannelBandwidth) + var htmode string + switch request.ChannelBandwidth { + case "20MHz": + htmode = "HT20" + case "40MHz": + htmode = "HT40" + default: + return fmt.Errorf("invalid channel bandwidth: %s", request.ChannelBandwidth) + } + uciTree.SetType("wireless", radio.device, "htmode", uci.TypeOption, htmode) radio.ChannelBandwidth = request.ChannelBandwidth } diff --git a/radio/radio_ap_test.go b/radio/radio_ap_test.go index 8b3d875..7a08f39 100644 --- a/radio/radio_ap_test.go +++ b/radio/radio_ap_test.go @@ -101,6 +101,7 @@ func TestRadio_setInitialState(t *testing.T) { radio := NewRadio() fakeTree.valuesForGet["wireless.wifi1.channel"] = "23" + fakeTree.valuesForGet["wireless.wifi1.htmode"] = "HT20" fakeShell.commandOutput["iwinfo ath1 info"] = "ath1\nESSID: \"1111\"\n" fakeShell.commandOutput["iwinfo ath11 info"] = "ath11\nESSID: \"no-team-2\"\n" fakeShell.commandOutput["iwinfo ath12 info"] = "ath12\nESSID: \"no-team-3\"\n" @@ -109,6 +110,7 @@ func TestRadio_setInitialState(t *testing.T) { fakeShell.commandOutput["iwinfo ath15 info"] = "ath15\nESSID: \"6666\"\n" radio.setInitialState() assert.Equal(t, 23, radio.Channel) + assert.Equal(t, "20MHz", radio.ChannelBandwidth) assert.Equal(t, "1111", radio.StationStatuses["red1"].Ssid) assert.Nil(t, radio.StationStatuses["red2"]) assert.Nil(t, radio.StationStatuses["red3"]) diff --git a/web/configuration_api_test.go b/web/configuration_api_test.go index 61e0a30..3b80bc9 100644 --- a/web/configuration_api_test.go +++ b/web/configuration_api_test.go @@ -41,7 +41,7 @@ func TestWeb_configurationHandler(t *testing.T) { ` { "channel": 149, - "channelBandwidth": "HT20", + "channelBandwidth": "20MHz", "stationConfigurations": { "red1": {"ssid": "9991", "wpaKey": "11111111"}, "red2": {"ssid": "9992", "wpaKey": "22222222"}, @@ -58,7 +58,7 @@ func TestWeb_configurationHandler(t *testing.T) { if assert.Equal(t, 1, len(ap.ConfigurationRequestChannel)) { request := <-ap.ConfigurationRequestChannel assert.Equal(t, 149, request.Channel) - assert.Equal(t, "HT20", request.ChannelBandwidth) + assert.Equal(t, "20MHz", request.ChannelBandwidth) assert.Equal(t, 6, len(request.StationConfigurations)) assert.Equal( t, radio.StationConfiguration{Ssid: "9991", WpaKey: "11111111"}, request.StationConfigurations["red1"], From ed4d7a266fae56e34c7ae492c55b8579819fc32e Mon Sep 17 00:00:00 2001 From: Patrick Fairbank Date: Sat, 8 Jun 2024 15:30:57 -0700 Subject: [PATCH 11/31] Parse more fields from assoclist on the AP and include them in StationStatus. --- README.md | 22 ++++++++++---- radio/radio_ap_test.go | 20 +++--------- radio/station_status.go | 59 ++++++++++++++++++++++++++---------- radio/station_status_test.go | 40 +++++++++++++++++++----- 4 files changed, 95 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index f1f5c22..e9083be 100644 --- a/README.md +++ b/README.md @@ -60,9 +60,14 @@ $ curl http://10.0.100.2:8081/status "hashedWpaKey": "2d0d7870bef68c589212a2bc47b650091585005cdd9404842dc9e3d27809b6c2", "wpaKeySalt": "Tj5DuBrAYhfFvNMZ", "isRobotRadioLinked": false, + "macAddress": "", + "signalDbm": 0, + "noiseDbm": 0, + "signalNoiseRatio": 0, "rxRateMbps": 0, + "rxPackets": 0, "txRateMbps": 0, - "signalNoiseRatio": 0, + "txPackets": 0, "bandwidthUsedMbps": 0 }, "blue3": null, @@ -70,11 +75,16 @@ $ curl http://10.0.100.2:8081/status "ssid": "1111", "hashedWpaKey": "e418de38d25cd254d0faf73f3206631b9eed8fdd8094004da655749cf536af7a", "wpaKeySalt": "B4Vx1KSX1TPzErKA", - "isRobotRadioLinked": false, - "rxRateMbps": 0, - "txRateMbps": 0, - "signalNoiseRatio": 0, - "bandwidthUsedMbps": 0 + "isRobotRadioLinked": true, + "macAddress": "48:DA:35:B0:01:CF", + "signalDbm": -53, + "noiseDbm": -93, + "signalNoiseRatio": 40, + "rxRateMbps": 860.3, + "rxPackets": 4095, + "txRateMbps": 6, + "txPackets": 5246, + "bandwidthUsedMbps": 4.102 }, "red2": null, "red3": null diff --git a/radio/radio_ap_test.go b/radio/radio_ap_test.go index 7a08f39..7dcb50b 100644 --- a/radio/radio_ap_test.go +++ b/radio/radio_ap_test.go @@ -390,25 +390,13 @@ func TestRadio_updateStationMonitoring(t *testing.T) { fakeShell.commandOutput["luci-bwc -i wlan0-4"] = "" fakeShell.commandErrors["iwinfo wlan0-4 assoclist"] = errors.New("oops") radio.updateMonitoring() + assert.True(t, radio.StationStatuses["red1"].IsRobotRadioLinked) + assert.Equal(t, 550.6, radio.StationStatuses["red1"].RxRateMbps) + assert.Equal(t, -999.0, radio.StationStatuses["red1"].BandwidthUsedMbps) assert.Equal( t, StationStatus{ - IsRobotRadioLinked: true, - RxRateMbps: 550.6, - TxRateMbps: 254.0, - SignalNoiseRatio: 42, - BandwidthUsedMbps: -999, - }, - *radio.StationStatuses["red1"], - ) - assert.Equal( - t, - StationStatus{ - IsRobotRadioLinked: false, - RxRateMbps: 0, - TxRateMbps: 0, - SignalNoiseRatio: 0, - BandwidthUsedMbps: 15.324, + BandwidthUsedMbps: 15.324, }, *radio.StationStatuses["red3"], ) diff --git a/radio/station_status.go b/radio/station_status.go index 7658db0..65aec58 100644 --- a/radio/station_status.go +++ b/radio/station_status.go @@ -26,14 +26,29 @@ type StationStatus struct { // Whether a robot radio is currently associated to the access point on this station's SSID. IsRobotRadioLinked bool `json:"isRobotRadioLinked"` + // MAC address of the robot radio currently associated to the access point on this station's SSID. + MacAddress string `json:"macAddress"` + + // Signal strength of the robot radio's link to the access point, in decibel-milliwatts. + SignalDbm int `json:"signalDbm"` + + // Noise level of the robot radio's link to the access point, in decibel-milliwatts. + NoiseDbm int `json:"noiseDbm"` + + // Current signal-to-noise ratio (SNR) in decibels. + SignalNoiseRatio int `json:"signalNoiseRatio"` + // Upper-bound link receive rate (from the robot radio to the access point) in megabits per second. RxRateMbps float64 `json:"rxRateMbps"` + // Number of packets received from the robot radio. + RxPackets int `json:"rxPackets"` + // Upper-bound link transmit rate (from the access point to the robot radio) in megabits per second. TxRateMbps float64 `json:"txRateMbps"` - // Current signal-to-noise ratio (SNR) in decibels. - SignalNoiseRatio int `json:"signalNoiseRatio"` + // Number of packets transmitted to the robot radio. + TxPackets int `json:"txPackets"` // Current five-second average total (rx + tx) bandwidth in megabits per second. BandwidthUsedMbps float64 `json:"bandwidthUsedMbps"` @@ -58,27 +73,39 @@ func (status *StationStatus) parseBandwidthUsed(response string) { // Parses the given data from the access point's association list and updates the status structure with the result. func (status *StationStatus) 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") + line1Re := regexp.MustCompile( + "((?:[0-9A-F]{2}:){5}(?:[0-9A-F]{2}))\\s+(-\\d+) dBm / (-\\d+) dBm \\(SNR (\\d+)\\)\\s+(\\d+) ms ago", + ) + line2Re := regexp.MustCompile("RX:\\s+(\\d+\\.\\d+)\\s+MBit/s\\s+(\\d+) Pkts.") + line3R3 := regexp.MustCompile("TX:\\s+(\\d+\\.\\d+)\\s+MBit/s\\s+(\\d+) Pkts.") status.IsRobotRadioLinked = false + status.MacAddress = "" + status.SignalDbm = 0 + status.NoiseDbm = 0 + status.SignalNoiseRatio = 0 status.RxRateMbps = 0 + status.RxPackets = 0 status.TxRateMbps = 0 - status.SignalNoiseRatio = 0 - for _, radioLinkMatch := range radioLinkRe.FindAllStringSubmatch(response, -1) { - macAddress := radioLinkMatch[1] - dataAgeMs, _ := strconv.Atoi(radioLinkMatch[3]) + status.TxPackets = 0 + for _, line1Match := range line1Re.FindAllStringSubmatch(response, -1) { + macAddress := line1Match[1] + dataAgeMs, _ := strconv.Atoi(line1Match[5]) if macAddress != "00:00:00:00:00:00" && dataAgeMs <= 4000 { status.IsRobotRadioLinked = true - status.SignalNoiseRatio, _ = strconv.Atoi(radioLinkMatch[2]) - rxRateMatch := rxRateRe.FindStringSubmatch(response) - if len(rxRateMatch) > 0 { - status.RxRateMbps, _ = strconv.ParseFloat(rxRateMatch[1], 64) + status.MacAddress = macAddress + status.SignalDbm, _ = strconv.Atoi(line1Match[2]) + status.NoiseDbm, _ = strconv.Atoi(line1Match[3]) + status.SignalNoiseRatio, _ = strconv.Atoi(line1Match[4]) + line2Match := line2Re.FindStringSubmatch(response) + if len(line2Match) > 0 { + status.RxRateMbps, _ = strconv.ParseFloat(line2Match[1], 64) + status.RxPackets, _ = strconv.Atoi(line2Match[2]) } - txRateMatch := txRateRe.FindStringSubmatch(response) - if len(txRateMatch) > 0 { - status.TxRateMbps, _ = strconv.ParseFloat(txRateMatch[1], 64) + line3Match := line3R3.FindStringSubmatch(response) + if len(line3Match) > 0 { + status.TxRateMbps, _ = strconv.ParseFloat(line3Match[1], 64) + status.TxPackets, _ = strconv.Atoi(line3Match[2]) } break } diff --git a/radio/station_status_test.go b/radio/station_status_test.go index d78032b..01cd83e 100644 --- a/radio/station_status_test.go +++ b/radio/station_status_test.go @@ -44,7 +44,7 @@ func TestStationStatus_ParseAssocList(t *testing.T) { // 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" + + "\tTX: 550.6 MBit/s 123 Pkts.\n" + "\texpected throughput: unknown" status.parseAssocList(response) assert.Equal(t, StationStatus{}, status) @@ -52,25 +52,49 @@ func TestStationStatus_ParseAssocList(t *testing.T) { // 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" + + "\tTX: 254.0 MBit/s 123 Pkts.\n" + "\texpected throughput: unknown" status.parseAssocList(response) assert.Equal( - t, StationStatus{IsRobotRadioLinked: true, RxRateMbps: 550.6, TxRateMbps: 254.0, SignalNoiseRatio: 42}, status, + t, + StationStatus{ + IsRobotRadioLinked: true, + MacAddress: "48:DA:35:B0:00:CF", + SignalDbm: -53, + NoiseDbm: -95, + SignalNoiseRatio: 42, + RxRateMbps: 550.6, + RxPackets: 4095, + TxRateMbps: 254.0, + TxPackets: 123, + }, + status, ) - 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" + + response = "37:DA:35:B0:00:BE -64 dBm / -84 dBm (SNR 7) 4000 ms ago\n" + + "\tRX: 123.4 MBit/s 5091 Pkts.\n" + + "\tTX: 550.6 MBit/s 789 Pkts.\n" + "\texpected throughput: unknown" status.parseAssocList(response) assert.Equal( - t, StationStatus{IsRobotRadioLinked: true, RxRateMbps: 123.4, TxRateMbps: 550.6, SignalNoiseRatio: 7}, status, + t, + StationStatus{ + IsRobotRadioLinked: true, + MacAddress: "37:DA:35:B0:00:BE", + SignalDbm: -64, + NoiseDbm: -84, + SignalNoiseRatio: 7, + RxRateMbps: 123.4, + RxPackets: 5091, + TxRateMbps: 550.6, + TxPackets: 789, + }, + status, ) // 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" + + "\tTX: 550.6 MBit/s 123 Pkts.\n" + "\texpected throughput: unknown" status.parseAssocList(response) assert.Equal(t, StationStatus{}, status) From 2d5d5ae54e9472bbda9655f22d48f0e457714201 Mon Sep 17 00:00:00 2001 From: Patrick Fairbank Date: Sun, 9 Jun 2024 15:51:56 -0700 Subject: [PATCH 12/31] Update ports in install scripts. --- install-access-point | 3 ++- install-robot-radio | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/install-access-point b/install-access-point index 3abb658..0832d55 100755 --- a/install-access-point +++ b/install-access-point @@ -5,7 +5,6 @@ DEFAULT_TARGET=10.0.100.2 BINARY_FILE=frc-radio-api USER=root SSH_ARGS="-o ConnectTimeout=5 -o StrictHostKeyChecking=no" -API_PORT=8081 read -p "Device IP address [$DEFAULT_TARGET]: " TARGET TARGET=${TARGET:-$DEFAULT_TARGET} @@ -24,9 +23,11 @@ RADIO_TYPE=`ssh $SSH_ARGS $USER@$TARGET "uci get system.@system[0].model 2>&1 | if [ $RADIO_TYPE = "0" ]; then echo "Detected Vivid-Hosting radio type." CONFIG_FILE=wireless-boot-vh + API_PORT=80 else echo "Detected Linksys radio type." CONFIG_FILE=wireless-boot-linksys + API_PORT=8081 fi echo "\nDeploying to $TARGET..." diff --git a/install-robot-radio b/install-robot-radio index 0410982..be25f50 100755 --- a/install-robot-radio +++ b/install-robot-radio @@ -4,7 +4,7 @@ set -e BINARY_FILE=frc-radio-api USER=root SSH_ARGS="-o ConnectTimeout=5 -o StrictHostKeyChecking=no" -API_PORT=8081 +API_PORT=80 read -p "Device IP address: " TARGET TARGET=${TARGET:-$DEFAULT_TARGET} From e593f2f9862943459ff06457f2baab5fdc8d4669 Mon Sep 17 00:00:00 2001 From: Patrick Fairbank Date: Tue, 11 Jun 2024 20:04:30 -0700 Subject: [PATCH 13/31] Add parsing of Rx/Tx bytes from ifconfig. --- README.md | 4 ++++ radio/radio_ap.go | 10 ++++++++++ radio/radio_ap_test.go | 13 ++++++++++++- radio/station_status.go | 23 ++++++++++++++++++++++- radio/station_status_test.go | 17 +++++++++++++++++ 5 files changed, 65 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e9083be..85ee798 100644 --- a/README.md +++ b/README.md @@ -66,8 +66,10 @@ $ curl http://10.0.100.2:8081/status "signalNoiseRatio": 0, "rxRateMbps": 0, "rxPackets": 0, + "rxBytes": 0, "txRateMbps": 0, "txPackets": 0, + "txBytes": 0, "bandwidthUsedMbps": 0 }, "blue3": null, @@ -82,8 +84,10 @@ $ curl http://10.0.100.2:8081/status "signalNoiseRatio": 40, "rxRateMbps": 860.3, "rxPackets": 4095, + "rxBytes": 5177, "txRateMbps": 6, "txPackets": 5246, + "txBytes": 11830, "bandwidthUsedMbps": 4.102 }, "red2": null, diff --git a/radio/radio_ap.go b/radio/radio_ap.go index fe61e10..20267fb 100644 --- a/radio/radio_ap.go +++ b/radio/radio_ap.go @@ -272,5 +272,15 @@ func (radio *Radio) updateMonitoring() { } else { stationStatus.parseAssocList(output) } + + // Update the number of bytes received and transmitted. + output, err = shell.runCommand("ifconfig", stationInterface) + if err != nil { + log.Printf("Error running 'ifconfig %s': %v", stationInterface, err) + stationStatus.RxBytes = monitoringErrorCode + stationStatus.TxBytes = monitoringErrorCode + } else { + stationStatus.parseIfconfig(output) + } } } diff --git a/radio/radio_ap_test.go b/radio/radio_ap_test.go index 7dcb50b..b588ac8 100644 --- a/radio/radio_ap_test.go +++ b/radio/radio_ap_test.go @@ -379,6 +379,8 @@ func TestRadio_updateStationMonitoring(t *testing.T) { "\tRX: 550.6 MBit/s 4095 Pkts.\n" + "\tTX: 254.0 MBit/s 0 Pkts.\n" + "\texpected throughput: unknown" + fakeShell.commandOutput["ifconfig wlan0"] = "wlan0\tLink encap:Ethernet HWaddr 00:00:00:00:00:00\n" + + "\tRX bytes:12345 (12.3 KiB) TX bytes:98765 (98.7 KiB)" fakeShell.commandOutput["luci-bwc -i wlan0-2"] = "[ 1687496917, 26097, 177, 70454, 846 ],\n" + "[ 1687496919, 26097, 177, 70454, 846 ],\n" + "[ 1687496920, 26097, 177, 70518, 847 ],\n" + @@ -387,12 +389,16 @@ func TestRadio_updateStationMonitoring(t *testing.T) { "[ 1687496922, 26097, 177, 70582, 848 ],\n" + "[ 1687496923, 2609700, 177, 7064600, 849 ]" fakeShell.commandOutput["iwinfo wlan0-2 assoclist"] = "" + fakeShell.commandOutput["ifconfig wlan0-2"] = "" fakeShell.commandOutput["luci-bwc -i wlan0-4"] = "" fakeShell.commandErrors["iwinfo wlan0-4 assoclist"] = errors.New("oops") + fakeShell.commandErrors["ifconfig wlan0-4"] = errors.New("oops") radio.updateMonitoring() assert.True(t, radio.StationStatuses["red1"].IsRobotRadioLinked) assert.Equal(t, 550.6, radio.StationStatuses["red1"].RxRateMbps) assert.Equal(t, -999.0, radio.StationStatuses["red1"].BandwidthUsedMbps) + assert.Equal(t, 12345, radio.StationStatuses["red1"].RxBytes) + assert.Equal(t, 98765, radio.StationStatuses["red1"].TxBytes) assert.Equal( t, StationStatus{ @@ -405,17 +411,22 @@ func TestRadio_updateStationMonitoring(t *testing.T) { StationStatus{ IsRobotRadioLinked: false, RxRateMbps: -999, + RxBytes: -999, TxRateMbps: -999, + TxBytes: -999, SignalNoiseRatio: -999, BandwidthUsedMbps: 0, }, *radio.StationStatuses["blue2"], ) - assert.Equal(t, 6, len(fakeShell.commandsRun)) + assert.Equal(t, 9, len(fakeShell.commandsRun)) assert.Contains(t, fakeShell.commandsRun, "luci-bwc -i wlan0") assert.Contains(t, fakeShell.commandsRun, "iwinfo wlan0 assoclist") + assert.Contains(t, fakeShell.commandsRun, "ifconfig wlan0") assert.Contains(t, fakeShell.commandsRun, "luci-bwc -i wlan0-2") assert.Contains(t, fakeShell.commandsRun, "iwinfo wlan0-2 assoclist") + assert.Contains(t, fakeShell.commandsRun, "ifconfig wlan0-2") assert.Contains(t, fakeShell.commandsRun, "luci-bwc -i wlan0-4") assert.Contains(t, fakeShell.commandsRun, "iwinfo wlan0-4 assoclist") + assert.Contains(t, fakeShell.commandsRun, "ifconfig wlan0-4") } diff --git a/radio/station_status.go b/radio/station_status.go index 65aec58..4447084 100644 --- a/radio/station_status.go +++ b/radio/station_status.go @@ -44,12 +44,18 @@ type StationStatus struct { // Number of packets received from the robot radio. RxPackets int `json:"rxPackets"` + // Number of bytes received from the robot radio. + RxBytes int `json:"rxBytes"` + // Upper-bound link transmit rate (from the access point to the robot radio) in megabits per second. TxRateMbps float64 `json:"txRateMbps"` // Number of packets transmitted to the robot radio. TxPackets int `json:"txPackets"` + // Number of bytes transmitted to the robot radio. + TxBytes int `json:"txBytes"` + // Current five-second average total (rx + tx) bandwidth in megabits per second. BandwidthUsedMbps float64 `json:"bandwidthUsedMbps"` } @@ -71,7 +77,8 @@ func (status *StationStatus) parseBandwidthUsed(response string) { } } -// Parses the given data from the access point's association list and updates the status structure with the result. +// parseAssocList parses the given data from the access point's association list and updates the status structure with +// the result. func (status *StationStatus) parseAssocList(response string) { line1Re := regexp.MustCompile( "((?:[0-9A-F]{2}:){5}(?:[0-9A-F]{2}))\\s+(-\\d+) dBm / (-\\d+) dBm \\(SNR (\\d+)\\)\\s+(\\d+) ms ago", @@ -111,3 +118,17 @@ func (status *StationStatus) parseAssocList(response string) { } } } + +// parseIfconfig parses the given output from the access point's ifconfig command and updates the status structure with +// the result. +func (status *StationStatus) parseIfconfig(response string) { + bytesRe := regexp.MustCompile("RX bytes:(\\d+) .* TX bytes:(\\d+) ") + + status.RxBytes = 0 + status.TxBytes = 0 + bytesMatch := bytesRe.FindStringSubmatch(response) + if len(bytesMatch) > 0 { + status.RxBytes, _ = strconv.Atoi(bytesMatch[1]) + status.TxBytes, _ = strconv.Atoi(bytesMatch[2]) + } +} diff --git a/radio/station_status_test.go b/radio/station_status_test.go index 01cd83e..de0b82c 100644 --- a/radio/station_status_test.go +++ b/radio/station_status_test.go @@ -99,3 +99,20 @@ func TestStationStatus_ParseAssocList(t *testing.T) { status.parseAssocList(response) assert.Equal(t, StationStatus{}, status) } + +func TestStationStatus_ParseIfconfig(t *testing.T) { + var status StationStatus + + status.parseIfconfig("") + assert.Equal(t, StationStatus{}, status) + + response := "ath15\tLink encap:Ethernet HWaddr 4A:DA:35:B0:00:2C\n" + + "\tinet6 addr: fe80::48da:35ff:feb0:2c/64 Scope:Link\n" + + "\tUP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1\n" + + "\tRX packets:690 errors:0 dropped:0 overruns:0 frame:0\n" + + "\tTX packets:727 errors:0 dropped:0 overruns:0 carrier:0\n " + + "\tcollisions:0 txqueuelen:0\n" + + "\tRX bytes:45311 (44.2 KiB) TX bytes:48699 (47.5 KiB)\n" + status.parseIfconfig(response) + assert.Equal(t, StationStatus{RxBytes: 45311, TxBytes: 48699}, status) +} From 1c8e6bb025dfba9013d0a248c865761988bb1f1c Mon Sep 17 00:00:00 2001 From: Patrick Fairbank Date: Thu, 13 Jun 2024 17:19:15 -0700 Subject: [PATCH 14/31] Rename StationStatus to NetworkStatus to make it more shareable with the robot radio. --- README.md | 4 +- .../{station_status.go => network_status.go} | 57 +++++++++-------- ..._status_test.go => network_status_test.go} | 62 +++++++++---------- radio/radio_ap.go | 6 +- radio/radio_ap_test.go | 26 ++++---- web/status_api_test.go | 18 +++--- 6 files changed, 88 insertions(+), 85 deletions(-) rename radio/{station_status.go => network_status.go} (62%) rename radio/{station_status_test.go => network_status_test.go} (73%) diff --git a/README.md b/README.md index 85ee798..bd3d1e7 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ $ curl http://10.0.100.2:8081/status "ssid": "5555", "hashedWpaKey": "2d0d7870bef68c589212a2bc47b650091585005cdd9404842dc9e3d27809b6c2", "wpaKeySalt": "Tj5DuBrAYhfFvNMZ", - "isRobotRadioLinked": false, + "isLinked": false, "macAddress": "", "signalDbm": 0, "noiseDbm": 0, @@ -77,7 +77,7 @@ $ curl http://10.0.100.2:8081/status "ssid": "1111", "hashedWpaKey": "e418de38d25cd254d0faf73f3206631b9eed8fdd8094004da655749cf536af7a", "wpaKeySalt": "B4Vx1KSX1TPzErKA", - "isRobotRadioLinked": true, + "isLinked": true, "macAddress": "48:DA:35:B0:01:CF", "signalDbm": -53, "noiseDbm": -93, diff --git a/radio/station_status.go b/radio/network_status.go similarity index 62% rename from radio/station_status.go rename to radio/network_status.go index 4447084..e484405 100644 --- a/radio/station_status.go +++ b/radio/network_status.go @@ -9,12 +9,13 @@ import ( "strconv" ) -// StationStatus encapsulates the status of a single team station on the q point. -type StationStatus struct { - // Team-specific SSID for the station, usually equal to the team number as a string. +// NetworkStatus encapsulates the status of a single Wi-Fi interface on the device (i.e. a team SSID network on the +// access point or one of the two interfaces on the robot radio). +type NetworkStatus struct { + // SSID for the network. Ssid string `json:"ssid"` - // SHA-256 hash of the WPA key and salt for the station, encoded as a hexadecimal string. The WPA key is not exposed + // SHA-256 hash of the WPA key and salt for the network, encoded as a hexadecimal string. The WPA key is not exposed // directly to prevent unauthorized users from learning its value. However, a user who already knows the WPA key can // verify that it is correct by concatenating it with the WpaKeySalt and hashing the result using SHA-256; the // result should match the HashedWpaKey. @@ -23,46 +24,48 @@ type StationStatus struct { // Randomly generated salt used to hash the WPA key. WpaKeySalt string `json:"wpaKeySalt"` - // Whether a robot radio is currently associated to the access point on this station's SSID. - IsRobotRadioLinked bool `json:"isRobotRadioLinked"` + // Whether this network is currently associated with a remote device. + IsLinked bool `json:"isLinked"` - // MAC address of the robot radio currently associated to the access point on this station's SSID. + // MAC address of the remote device currently associated with this network. Blank if not associated. MacAddress string `json:"macAddress"` - // Signal strength of the robot radio's link to the access point, in decibel-milliwatts. + // Signal strength of the link to the remote device, in decibel-milliwatts. Zero if not associated. SignalDbm int `json:"signalDbm"` - // Noise level of the robot radio's link to the access point, in decibel-milliwatts. + // Noise level of the link to the remote device, in decibel-milliwatts. Zero if not associated. NoiseDbm int `json:"noiseDbm"` - // Current signal-to-noise ratio (SNR) in decibels. + // Current signal-to-noise ratio (SNR) in decibels. Zero if not associated. SignalNoiseRatio int `json:"signalNoiseRatio"` - // Upper-bound link receive rate (from the robot radio to the access point) in megabits per second. + // Upper-bound link receive rate (from the remote device to this one) in megabits per second. Zero if not + // associated. RxRateMbps float64 `json:"rxRateMbps"` - // Number of packets received from the robot radio. + // Number of packets received from the remote device. Zero if not associated. RxPackets int `json:"rxPackets"` - // Number of bytes received from the robot radio. + // Number of bytes received from the remote device. Zero if not associated. RxBytes int `json:"rxBytes"` - // Upper-bound link transmit rate (from the access point to the robot radio) in megabits per second. + // Upper-bound link transmit rate (from this device to the remote one) in megabits per second. Zero if not + // associated. TxRateMbps float64 `json:"txRateMbps"` - // Number of packets transmitted to the robot radio. + // Number of packets transmitted to the remote device. Zero if not associated. TxPackets int `json:"txPackets"` - // Number of bytes transmitted to the robot radio. + // Number of bytes transmitted to the remote device. Zero if not associated. TxBytes int `json:"txBytes"` // Current five-second average total (rx + tx) bandwidth in megabits per second. BandwidthUsedMbps float64 `json:"bandwidthUsedMbps"` } -// parseBandwidthUsed parses the given data from the access point's onboard bandwidth monitor and returns five-second -// average bandwidth in megabits per second. -func (status *StationStatus) parseBandwidthUsed(response string) { +// parseBandwidthUsed parses the given data from the radio's onboard bandwidth monitor and returns five-second average +// bandwidth in megabits per second. +func (status *NetworkStatus) parseBandwidthUsed(response string) { status.BandwidthUsedMbps = 0.0 btuRe := regexp.MustCompile("\\[ (\\d+), (\\d+), (\\d+), (\\d+), (\\d+) ]") btuMatches := btuRe.FindAllStringSubmatch(response, -1) @@ -77,16 +80,16 @@ func (status *StationStatus) parseBandwidthUsed(response string) { } } -// parseAssocList parses the given data from the access point's association list and updates the status structure with -// the result. -func (status *StationStatus) parseAssocList(response string) { +// parseAssocList parses the given data from the radio's association list and updates the status structure with the +// result. +func (status *NetworkStatus) parseAssocList(response string) { line1Re := regexp.MustCompile( "((?:[0-9A-F]{2}:){5}(?:[0-9A-F]{2}))\\s+(-\\d+) dBm / (-\\d+) dBm \\(SNR (\\d+)\\)\\s+(\\d+) ms ago", ) line2Re := regexp.MustCompile("RX:\\s+(\\d+\\.\\d+)\\s+MBit/s\\s+(\\d+) Pkts.") line3R3 := regexp.MustCompile("TX:\\s+(\\d+\\.\\d+)\\s+MBit/s\\s+(\\d+) Pkts.") - status.IsRobotRadioLinked = false + status.IsLinked = false status.MacAddress = "" status.SignalDbm = 0 status.NoiseDbm = 0 @@ -99,7 +102,7 @@ func (status *StationStatus) parseAssocList(response string) { macAddress := line1Match[1] dataAgeMs, _ := strconv.Atoi(line1Match[5]) if macAddress != "00:00:00:00:00:00" && dataAgeMs <= 4000 { - status.IsRobotRadioLinked = true + status.IsLinked = true status.MacAddress = macAddress status.SignalDbm, _ = strconv.Atoi(line1Match[2]) status.NoiseDbm, _ = strconv.Atoi(line1Match[3]) @@ -119,9 +122,9 @@ func (status *StationStatus) parseAssocList(response string) { } } -// parseIfconfig parses the given output from the access point's ifconfig command and updates the status structure with -// the result. -func (status *StationStatus) parseIfconfig(response string) { +// parseIfconfig parses the given output from the radio's ifconfig command and updates the status structure with the +// result. +func (status *NetworkStatus) parseIfconfig(response string) { bytesRe := regexp.MustCompile("RX bytes:(\\d+) .* TX bytes:(\\d+) ") status.RxBytes = 0 diff --git a/radio/station_status_test.go b/radio/network_status_test.go similarity index 73% rename from radio/station_status_test.go rename to radio/network_status_test.go index de0b82c..dc9ca55 100644 --- a/radio/station_status_test.go +++ b/radio/network_status_test.go @@ -8,8 +8,8 @@ import ( "testing" ) -func TestStationStatus_ParseBandwithUsed(t *testing.T) { - var status StationStatus +func TestNetworkStatus_ParseBandwithUsed(t *testing.T) { + var status NetworkStatus // Response is too short. status.parseBandwidthUsed("") @@ -35,11 +35,11 @@ func TestStationStatus_ParseBandwithUsed(t *testing.T) { assert.Equal(t, 15.324, status.BandwidthUsedMbps) } -func TestStationStatus_ParseAssocList(t *testing.T) { - var status StationStatus +func TestNetworkStatus_ParseAssocList(t *testing.T) { + var status NetworkStatus status.parseAssocList("") - assert.Equal(t, StationStatus{}, status) + assert.Equal(t, NetworkStatus{}, status) // MAC address is invalid. response := "00:00:00:00:00:00 -53 dBm / -95 dBm (SNR 42) 0 ms ago\n" + @@ -47,7 +47,7 @@ func TestStationStatus_ParseAssocList(t *testing.T) { "\tTX: 550.6 MBit/s 123 Pkts.\n" + "\texpected throughput: unknown" status.parseAssocList(response) - assert.Equal(t, StationStatus{}, status) + assert.Equal(t, NetworkStatus{}, status) // Link is valid. response = "48:DA:35:B0:00:CF -53 dBm / -95 dBm (SNR 42) 0 ms ago\n" + @@ -57,16 +57,16 @@ func TestStationStatus_ParseAssocList(t *testing.T) { status.parseAssocList(response) assert.Equal( t, - StationStatus{ - IsRobotRadioLinked: true, - MacAddress: "48:DA:35:B0:00:CF", - SignalDbm: -53, - NoiseDbm: -95, - SignalNoiseRatio: 42, - RxRateMbps: 550.6, - RxPackets: 4095, - TxRateMbps: 254.0, - TxPackets: 123, + NetworkStatus{ + IsLinked: true, + MacAddress: "48:DA:35:B0:00:CF", + SignalDbm: -53, + NoiseDbm: -95, + SignalNoiseRatio: 42, + RxRateMbps: 550.6, + RxPackets: 4095, + TxRateMbps: 254.0, + TxPackets: 123, }, status, ) @@ -77,16 +77,16 @@ func TestStationStatus_ParseAssocList(t *testing.T) { status.parseAssocList(response) assert.Equal( t, - StationStatus{ - IsRobotRadioLinked: true, - MacAddress: "37:DA:35:B0:00:BE", - SignalDbm: -64, - NoiseDbm: -84, - SignalNoiseRatio: 7, - RxRateMbps: 123.4, - RxPackets: 5091, - TxRateMbps: 550.6, - TxPackets: 789, + NetworkStatus{ + IsLinked: true, + MacAddress: "37:DA:35:B0:00:BE", + SignalDbm: -64, + NoiseDbm: -84, + SignalNoiseRatio: 7, + RxRateMbps: 123.4, + RxPackets: 5091, + TxRateMbps: 550.6, + TxPackets: 789, }, status, ) @@ -97,14 +97,14 @@ func TestStationStatus_ParseAssocList(t *testing.T) { "\tTX: 550.6 MBit/s 123 Pkts.\n" + "\texpected throughput: unknown" status.parseAssocList(response) - assert.Equal(t, StationStatus{}, status) + assert.Equal(t, NetworkStatus{}, status) } -func TestStationStatus_ParseIfconfig(t *testing.T) { - var status StationStatus +func TestNetworkStatus_ParseIfconfig(t *testing.T) { + var status NetworkStatus status.parseIfconfig("") - assert.Equal(t, StationStatus{}, status) + assert.Equal(t, NetworkStatus{}, status) response := "ath15\tLink encap:Ethernet HWaddr 4A:DA:35:B0:00:2C\n" + "\tinet6 addr: fe80::48da:35ff:feb0:2c/64 Scope:Link\n" + @@ -114,5 +114,5 @@ func TestStationStatus_ParseIfconfig(t *testing.T) { "\tcollisions:0 txqueuelen:0\n" + "\tRX bytes:45311 (44.2 KiB) TX bytes:48699 (47.5 KiB)\n" status.parseIfconfig(response) - assert.Equal(t, StationStatus{RxBytes: 45311, TxBytes: 48699}, status) + assert.Equal(t, NetworkStatus{RxBytes: 45311, TxBytes: 48699}, status) } diff --git a/radio/radio_ap.go b/radio/radio_ap.go index 20267fb..423bd68 100644 --- a/radio/radio_ap.go +++ b/radio/radio_ap.go @@ -29,7 +29,7 @@ type Radio struct { Status radioStatus `json:"status"` // Map of team station names to their current status. - StationStatuses map[string]*StationStatus `json:"stationStatuses"` + StationStatuses map[string]*NetworkStatus `json:"stationStatuses"` // Version of the radio software. Version string `json:"version"` @@ -84,7 +84,7 @@ func NewRadio() *Radio { } } - radio.StationStatuses = make(map[string]*StationStatus) + radio.StationStatuses = make(map[string]*NetworkStatus) for i := 0; i < int(stationCount); i++ { radio.StationStatuses[station(i).String()] = nil } @@ -214,7 +214,7 @@ func (radio *Radio) updateStationStatuses() error { if strings.HasPrefix(ssid, "no-team-") { radio.StationStatuses[station.String()] = nil } else { - var status StationStatus + var status NetworkStatus status.Ssid = ssid status.HashedWpaKey, status.WpaKeySalt = radio.getHashedWpaKeyAndSalt(int(station) + 1) radio.StationStatuses[station.String()] = &status diff --git a/radio/radio_ap_test.go b/radio/radio_ap_test.go index b588ac8..ff19178 100644 --- a/radio/radio_ap_test.go +++ b/radio/radio_ap_test.go @@ -371,9 +371,9 @@ func TestRadio_updateStationMonitoring(t *testing.T) { // Some teams assigned. fakeShell.reset() - radio.StationStatuses["red1"] = &StationStatus{} - radio.StationStatuses["red3"] = &StationStatus{} - radio.StationStatuses["blue2"] = &StationStatus{} + radio.StationStatuses["red1"] = &NetworkStatus{} + radio.StationStatuses["red3"] = &NetworkStatus{} + radio.StationStatuses["blue2"] = &NetworkStatus{} fakeShell.commandErrors["luci-bwc -i wlan0"] = errors.New("oops") fakeShell.commandOutput["iwinfo wlan0 assoclist"] = "48:DA:35:B0:00:CF -53 dBm / -95 dBm (SNR 42) 0 ms ago\n" + "\tRX: 550.6 MBit/s 4095 Pkts.\n" + @@ -394,28 +394,28 @@ func TestRadio_updateStationMonitoring(t *testing.T) { fakeShell.commandErrors["iwinfo wlan0-4 assoclist"] = errors.New("oops") fakeShell.commandErrors["ifconfig wlan0-4"] = errors.New("oops") radio.updateMonitoring() - assert.True(t, radio.StationStatuses["red1"].IsRobotRadioLinked) + assert.True(t, radio.StationStatuses["red1"].IsLinked) assert.Equal(t, 550.6, radio.StationStatuses["red1"].RxRateMbps) assert.Equal(t, -999.0, radio.StationStatuses["red1"].BandwidthUsedMbps) assert.Equal(t, 12345, radio.StationStatuses["red1"].RxBytes) assert.Equal(t, 98765, radio.StationStatuses["red1"].TxBytes) assert.Equal( t, - StationStatus{ + NetworkStatus{ BandwidthUsedMbps: 15.324, }, *radio.StationStatuses["red3"], ) assert.Equal( t, - StationStatus{ - IsRobotRadioLinked: false, - RxRateMbps: -999, - RxBytes: -999, - TxRateMbps: -999, - TxBytes: -999, - SignalNoiseRatio: -999, - BandwidthUsedMbps: 0, + NetworkStatus{ + IsLinked: false, + RxRateMbps: -999, + RxBytes: -999, + TxRateMbps: -999, + TxBytes: -999, + SignalNoiseRatio: -999, + BandwidthUsedMbps: 0, }, *radio.StationStatuses["blue2"], ) diff --git a/web/status_api_test.go b/web/status_api_test.go index dcc669b..2151dc1 100644 --- a/web/status_api_test.go +++ b/web/status_api_test.go @@ -16,15 +16,15 @@ func TestWeb_statusHandler(t *testing.T) { ap.Channel = 136 ap.Status = "ACTIVE" - ap.StationStatuses["blue1"] = &radio.StationStatus{ - Ssid: "254", - HashedWpaKey: "foo", - WpaKeySalt: "bar", - IsRobotRadioLinked: true, - RxRateMbps: 1.0, - TxRateMbps: 2.0, - SignalNoiseRatio: 3, - BandwidthUsedMbps: 4.0, + ap.StationStatuses["blue1"] = &radio.NetworkStatus{ + Ssid: "254", + HashedWpaKey: "foo", + WpaKeySalt: "bar", + IsLinked: true, + RxRateMbps: 1.0, + TxRateMbps: 2.0, + SignalNoiseRatio: 3, + BandwidthUsedMbps: 4.0, } recorder := web.getHttpResponse("/status") From ff34b4b5f5d01fd427d795d17c0e82c740241c39 Mon Sep 17 00:00:00 2001 From: Patrick Fairbank Date: Fri, 14 Jun 2024 17:05:14 -0700 Subject: [PATCH 15/31] Add network monitoring to robot radio and include results in status API. --- radio/network_status.go | 43 ++++++++++++++++++-- radio/network_status_test.go | 3 -- radio/radio_ap.go | 35 +---------------- radio/radio_ap_test.go | 2 +- radio/radio_robot.go | 61 +++++++++++++++-------------- radio/radio_robot_test.go | 76 +++++++++++++++++++++++++++++++----- 6 files changed, 141 insertions(+), 79 deletions(-) diff --git a/radio/network_status.go b/radio/network_status.go index e484405..5192fb4 100644 --- a/radio/network_status.go +++ b/radio/network_status.go @@ -1,14 +1,17 @@ -// This file is specific to the access point version of the API. -//go:build !robot - package radio import ( + "log" "math" "regexp" "strconv" ) +const ( + // Sentinel value used to populate status fields when a monitoring command failed. + monitoringErrorCode = -999 +) + // NetworkStatus encapsulates the status of a single Wi-Fi interface on the device (i.e. a team SSID network on the // access point or one of the two interfaces on the robot radio). type NetworkStatus struct { @@ -63,6 +66,40 @@ type NetworkStatus struct { BandwidthUsedMbps float64 `json:"bandwidthUsedMbps"` } +// updateMonitoring polls the access point for the current bandwidth usage and link state of the given network interface +// and updates the in-memory state. +func (status *NetworkStatus) updateMonitoring(networkInterface string) { + // Update the bandwidth usage. + output, err := shell.runCommand("luci-bwc", "-i", networkInterface) + if err != nil { + log.Printf("Error running 'luci-bwc -i %s': %v", networkInterface, err) + status.BandwidthUsedMbps = monitoringErrorCode + } else { + status.parseBandwidthUsed(output) + } + + // Update the link state of any associated robot radios. + output, err = shell.runCommand("iwinfo", networkInterface, "assoclist") + if err != nil { + log.Printf("Error running 'iwinfo %s assoclist': %v", networkInterface, err) + status.RxRateMbps = monitoringErrorCode + status.TxRateMbps = monitoringErrorCode + status.SignalNoiseRatio = monitoringErrorCode + } else { + status.parseAssocList(output) + } + + // Update the number of bytes received and transmitted. + output, err = shell.runCommand("ifconfig", networkInterface) + if err != nil { + log.Printf("Error running 'ifconfig %s': %v", networkInterface, err) + status.RxBytes = monitoringErrorCode + status.TxBytes = monitoringErrorCode + } else { + status.parseIfconfig(output) + } +} + // parseBandwidthUsed parses the given data from the radio's onboard bandwidth monitor and returns five-second average // bandwidth in megabits per second. func (status *NetworkStatus) parseBandwidthUsed(response string) { diff --git a/radio/network_status_test.go b/radio/network_status_test.go index dc9ca55..581ea71 100644 --- a/radio/network_status_test.go +++ b/radio/network_status_test.go @@ -1,6 +1,3 @@ -// This file is specific to the access point version of the API. -//go:build !robot - package radio import ( diff --git a/radio/radio_ap.go b/radio/radio_ap.go index 423bd68..6b93299 100644 --- a/radio/radio_ap.go +++ b/radio/radio_ap.go @@ -12,11 +12,6 @@ import ( "time" ) -const ( - // Sentinel value used to populate status fields when a monitoring command failed. - monitoringErrorCode = -999 -) - // Radio holds the current state of the access point's configuration and any robot radios connected to it. type Radio struct { // 5GHz or 6GHz channel number the radio is broadcasting on. @@ -253,34 +248,6 @@ func (radio *Radio) updateMonitoring() { continue } - // Update the bandwidth usage. - output, err := shell.runCommand("luci-bwc", "-i", stationInterface) - if err != nil { - log.Printf("Error running 'luci-bwc -i %s': %v", stationInterface, err) - stationStatus.BandwidthUsedMbps = monitoringErrorCode - } else { - stationStatus.parseBandwidthUsed(output) - } - - // Update the link state of any associated robot radios. - output, err = shell.runCommand("iwinfo", stationInterface, "assoclist") - if err != nil { - log.Printf("Error running 'iwinfo %s assoclist': %v", stationInterface, err) - stationStatus.RxRateMbps = monitoringErrorCode - stationStatus.TxRateMbps = monitoringErrorCode - stationStatus.SignalNoiseRatio = monitoringErrorCode - } else { - stationStatus.parseAssocList(output) - } - - // Update the number of bytes received and transmitted. - output, err = shell.runCommand("ifconfig", stationInterface) - if err != nil { - log.Printf("Error running 'ifconfig %s': %v", stationInterface, err) - stationStatus.RxBytes = monitoringErrorCode - stationStatus.TxBytes = monitoringErrorCode - } else { - stationStatus.parseIfconfig(output) - } + stationStatus.updateMonitoring(stationInterface) } } diff --git a/radio/radio_ap_test.go b/radio/radio_ap_test.go index ff19178..de5b496 100644 --- a/radio/radio_ap_test.go +++ b/radio/radio_ap_test.go @@ -359,7 +359,7 @@ func TestRadio_handleConfigurationRequestErrors(t *testing.T) { assert.Greater(t, fakeTree.commitCount, 20) } -func TestRadio_updateStationMonitoring(t *testing.T) { +func TestRadio_updateMonitoring(t *testing.T) { fakeShell := newFakeShell(t) shell = fakeShell fakeShell.commandOutput["sh -c source /etc/openwrt_release && echo $DISTRIB_DESCRIPTION"] = "" diff --git a/radio/radio_robot.go b/radio/radio_robot.go index f358c58..44926b3 100644 --- a/radio/radio_robot.go +++ b/radio/radio_robot.go @@ -12,15 +12,6 @@ import ( ) const ( - // Name of the radio's 6GHz Wi-Fi device. - radioDevice6 = "wifi1" - - // Name of the radio's 6GHz Wi-Fi interface. - radioInterface6 = "ath1" - - // Index of the radio's 6GHz Wi-Fi interface section in the UCI configuration. - radioInterfaceIndex6 = 1 - // Name of the radio's 2.4GHz Wi-Fi device. radioDevice24 = "wifi0" @@ -29,6 +20,15 @@ const ( // Index of the radio's 2.4GHz Wi-Fi interface section in the UCI configuration. radioInterfaceIndex24 = 0 + + // Name of the radio's 6GHz Wi-Fi device. + radioDevice6 = "wifi1" + + // Name of the radio's 6GHz Wi-Fi interface. + radioInterface6 = "ath1" + + // Index of the radio's 6GHz Wi-Fi interface section in the UCI configuration. + radioInterfaceIndex6 = 1 ) // Radio holds the current state of the access point's configuration and any robot radios connected to it. @@ -42,17 +42,11 @@ type Radio struct { // Team number that the radio is currently configured for. TeamNumber int `json:"teamNumber"` - // Team-specific SSID. - Ssid string `json:"ssid"` + // Status of the radio's 2.4GHz network. + NetworkStatus24 NetworkStatus `json:"networkStatus24"` - // SHA-256 hash of the WPA key and salt for the team, encoded as a hexadecimal string. The WPA key is not exposed - // directly to prevent unauthorized users from learning its value. However, a user who already knows the WPA key can - // verify that it is correct by concatenating it with the WpaKeySalt and hashing the result using SHA-256; the - // result should match the HashedWpaKey. - HashedWpaKey string `json:"hashedWpaKey"` - - // Randomly generated salt used to hash the WPA key. - WpaKeySalt string `json:"wpaKeySalt"` + // Status of the radio's 6GHz network. + NetworkStatus6 NetworkStatus `json:"networkStatus6"` // Enum representing the current configuration stage of the radio. Status radioStatus `json:"status"` @@ -95,8 +89,9 @@ func (radio *Radio) isStarted() bool { // setInitialState initializes the in-memory state to match the radio's current configuration. func (radio *Radio) setInitialState() { - wifiInterface := fmt.Sprintf("@wifi-iface[%d]", radioInterfaceIndex6) - mode, _ := uciTree.GetLast("wireless", wifiInterface, "mode") + wifiInterface24 := fmt.Sprintf("@wifi-iface[%d]", radioInterfaceIndex24) + wifiInterface6 := fmt.Sprintf("@wifi-iface[%d]", radioInterfaceIndex6) + mode, _ := uciTree.GetLast("wireless", wifiInterface6, "mode") if mode == "sta" { radio.Mode = modeTeamRobotRadio radio.Channel = "" @@ -104,9 +99,14 @@ func (radio *Radio) setInitialState() { radio.Mode = modeTeamAccessPoint radio.Channel, _ = uciTree.GetLast("wireless", radioDevice6, "channel") } - radio.Ssid, _ = uciTree.GetLast("wireless", wifiInterface, "ssid") - radio.TeamNumber, _ = strconv.Atoi(radio.Ssid) - radio.HashedWpaKey, radio.WpaKeySalt = radio.getHashedWpaKeyAndSalt(radioInterfaceIndex6) + + radio.NetworkStatus24.Ssid, _ = uciTree.GetLast("wireless", wifiInterface24, "ssid") + radio.NetworkStatus24.HashedWpaKey, radio.NetworkStatus24.WpaKeySalt = + radio.getHashedWpaKeyAndSalt(radioInterfaceIndex24) + radio.NetworkStatus6.Ssid, _ = uciTree.GetLast("wireless", wifiInterface6, "ssid") + radio.NetworkStatus6.HashedWpaKey, radio.NetworkStatus6.WpaKeySalt = + radio.getHashedWpaKeyAndSalt(radioInterfaceIndex6) + radio.TeamNumber, _ = strconv.Atoi(radio.NetworkStatus6.Ssid) } // configure configures the radio with the given configuration. @@ -177,12 +177,13 @@ func (radio *Radio) configure(request ConfigurationRequest) error { time.Sleep(wifiReloadBackoffDuration) var err error - radio.Ssid, err = getSsid(radioInterface6) + radio.NetworkStatus6.Ssid, err = getSsid(radioInterface6) if err != nil { return err } - radio.TeamNumber, _ = strconv.Atoi(radio.Ssid) - radio.HashedWpaKey, radio.WpaKeySalt = radio.getHashedWpaKeyAndSalt(radioInterfaceIndex6) + radio.TeamNumber, _ = strconv.Atoi(radio.NetworkStatus6.Ssid) + radio.NetworkStatus6.HashedWpaKey, radio.NetworkStatus6.WpaKeySalt = + radio.getHashedWpaKeyAndSalt(radioInterfaceIndex6) if radio.TeamNumber == request.TeamNumber { log.Printf("Successfully configured robot radio after %d attempts.", retryCount) break @@ -196,7 +197,9 @@ func (radio *Radio) configure(request ConfigurationRequest) error { return nil } -// updateMonitoring is a no-op for the robot radio, for the time being, since the API is only used for -// one-time-per-event configuration. +// updateMonitoring polls the access point for the current bandwidth usage and link state of each network and updates +// the in-memory state. func (radio *Radio) updateMonitoring() { + radio.NetworkStatus6.updateMonitoring(radioInterface6) + radio.NetworkStatus24.updateMonitoring(radioInterface24) } diff --git a/radio/radio_robot_test.go b/radio/radio_robot_test.go index c35c97b..c217c2a 100644 --- a/radio/radio_robot_test.go +++ b/radio/radio_robot_test.go @@ -46,13 +46,22 @@ func TestRadio_setInitialState(t *testing.T) { fakeShell.commandOutput["sh -c source /etc/openwrt_release && echo $DISTRIB_DESCRIPTION"] = "" radio := NewRadio() + fakeTree.valuesForGet["wireless.@wifi-iface[0].ssid"] = "FRC-12345" + fakeTree.valuesForGet["wireless.@wifi-iface[0].key"] = "22222222" fakeTree.valuesForGet["wireless.@wifi-iface[1].ssid"] = "12345" fakeTree.valuesForGet["wireless.@wifi-iface[1].key"] = "11111111" radio.setInitialState() + assert.Equal(t, "FRC-12345", radio.NetworkStatus24.Ssid) + assert.Equal( + t, "9f2aa7d5cd1da94305923def2685e7b1c099218868746465a1608384adf2a613", radio.NetworkStatus24.HashedWpaKey, + ) + assert.Equal(t, "mUNERA9rI2cvTK4U", radio.NetworkStatus24.WpaKeySalt) + assert.Equal(t, "12345", radio.NetworkStatus6.Ssid) + assert.Equal( + t, "8441e86a503c6028f7d308d18f0eb15e734862db94ce55e9e590c1febdee991c", radio.NetworkStatus6.HashedWpaKey, + ) + assert.Equal(t, "HomcjcEQvymkzADm", radio.NetworkStatus6.WpaKeySalt) assert.Equal(t, 12345, radio.TeamNumber) - assert.Equal(t, "12345", radio.Ssid) - assert.Equal(t, "c10cc0a95c29b83a73a3d0730f77bbf852016ea4f08aaf5d4291017c6c23bffd", radio.HashedWpaKey) - assert.Equal(t, "mUNERA9rI2cvTK4U", radio.WpaKeySalt) // Test with team radio mode. fakeTree.valuesForGet["wireless.@wifi-iface[1].mode"] = "sta" @@ -119,9 +128,11 @@ func TestRadio_handleConfigurationRequest(t *testing.T) { assert.Contains(t, fakeShell.commandsRun, "wifi reload") assert.Contains(t, fakeShell.commandsRun, "iwinfo ath1 info") assert.Equal(t, 12345, radio.TeamNumber) - assert.Equal(t, "12345", radio.Ssid) - assert.Equal(t, "c10cc0a95c29b83a73a3d0730f77bbf852016ea4f08aaf5d4291017c6c23bffd", radio.HashedWpaKey) - assert.Equal(t, "mUNERA9rI2cvTK4U", radio.WpaKeySalt) + assert.Equal(t, "12345", radio.NetworkStatus6.Ssid) + assert.Equal( + t, "c10cc0a95c29b83a73a3d0730f77bbf852016ea4f08aaf5d4291017c6c23bffd", radio.NetworkStatus6.HashedWpaKey, + ) + assert.Equal(t, "mUNERA9rI2cvTK4U", radio.NetworkStatus6.WpaKeySalt) assert.Equal(t, statusActive, radio.Status) assert.Equal(t, modeTeamRobotRadio, radio.Mode) assert.Equal(t, "", radio.Channel) @@ -153,9 +164,11 @@ func TestRadio_handleConfigurationRequest(t *testing.T) { assert.Contains(t, fakeShell.commandsRun, "wifi reload") assert.Contains(t, fakeShell.commandsRun, "iwinfo ath1 info") assert.Equal(t, 12345, radio.TeamNumber) - assert.Equal(t, "12345", radio.Ssid) - assert.Equal(t, "8441e86a503c6028f7d308d18f0eb15e734862db94ce55e9e590c1febdee991c", radio.HashedWpaKey) - assert.Equal(t, "HomcjcEQvymkzADm", radio.WpaKeySalt) + assert.Equal(t, "12345", radio.NetworkStatus6.Ssid) + assert.Equal( + t, "8441e86a503c6028f7d308d18f0eb15e734862db94ce55e9e590c1febdee991c", radio.NetworkStatus6.HashedWpaKey, + ) + assert.Equal(t, "HomcjcEQvymkzADm", radio.NetworkStatus6.WpaKeySalt) assert.Equal(t, statusActive, radio.Status) assert.Equal(t, modeTeamAccessPoint, radio.Mode) assert.Equal(t, "229", radio.Channel) @@ -222,3 +235,48 @@ func TestRadio_handleConfigurationRequestErrors(t *testing.T) { assert.Nil(t, radio.handleConfigurationRequest(request)) assert.Greater(t, fakeTree.commitCount, 5) } + +func TestRadio_updateMonitoring(t *testing.T) { + fakeShell := newFakeShell(t) + shell = fakeShell + fakeShell.commandOutput["sh -c source /etc/openwrt_release && echo $DISTRIB_DESCRIPTION"] = "" + radio := NewRadio() + + fakeShell.reset() + fakeShell.commandErrors["luci-bwc -i ath0"] = errors.New("oops") + fakeShell.commandOutput["iwinfo ath0 assoclist"] = "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" + fakeShell.commandOutput["ifconfig ath0"] = "ath0\tLink encap:Ethernet HWaddr 00:00:00:00:00:00\n" + + "\tRX bytes:12345 (12.3 KiB) TX bytes:98765 (98.7 KiB)" + fakeShell.commandOutput["luci-bwc -i ath1"] = "[ 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 ]" + fakeShell.commandOutput["iwinfo ath1 assoclist"] = "" + fakeShell.commandOutput["ifconfig ath1"] = "" + radio.updateMonitoring() + assert.True(t, radio.NetworkStatus24.IsLinked) + assert.Equal(t, 550.6, radio.NetworkStatus24.RxRateMbps) + assert.Equal(t, -999.0, radio.NetworkStatus24.BandwidthUsedMbps) + assert.Equal(t, 12345, radio.NetworkStatus24.RxBytes) + assert.Equal(t, 98765, radio.NetworkStatus24.TxBytes) + assert.Equal( + t, + NetworkStatus{ + BandwidthUsedMbps: 15.324, + }, + radio.NetworkStatus6, + ) + assert.Equal(t, 6, len(fakeShell.commandsRun)) + assert.Contains(t, fakeShell.commandsRun, "luci-bwc -i ath0") + assert.Contains(t, fakeShell.commandsRun, "iwinfo ath0 assoclist") + assert.Contains(t, fakeShell.commandsRun, "ifconfig ath0") + assert.Contains(t, fakeShell.commandsRun, "luci-bwc -i ath1") + assert.Contains(t, fakeShell.commandsRun, "iwinfo ath1 assoclist") + assert.Contains(t, fakeShell.commandsRun, "ifconfig ath1") +} From 6cc22f7cb156c4c137fe5f4f58510b4c703871d6 Mon Sep 17 00:00:00 2001 From: Patrick Fairbank Date: Sun, 16 Jun 2024 14:43:30 -0700 Subject: [PATCH 16/31] Update README.md for robot radio monitoring. --- README.md | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index bd3d1e7..80e9b29 100644 --- a/README.md +++ b/README.md @@ -150,9 +150,40 @@ The `/status` GET endpoint returns the current status of the robot radio. It ret $ curl http://10.12.34.1:8081/status { "teamNumber": 1234, - "ssid": "1234", - "hashedWpaKey": "d40e29b90743ddf71c75bfaedab1333e23bf43eb29f5c8c1ba55756e96e99d84", - "wpaKeySalt": "DzCKbEIu53vCmf0p", + "networkStatus24": { + "ssid": "FRC-1234", + "hashedWpaKey": "5147695f755c47cda0c60ec59b6a278cc3a6b217e78ad4a4480f9d027a139c40", + "wpaKeySalt": "n5OZJgKdhjWQgRXL", + "isLinked": false, + "macAddress": "", + "signalDbm": 0, + "noiseDbm": 0, + "signalNoiseRatio": 0, + "rxRateMbps": 0, + "rxPackets": 0, + "rxBytes": 0, + "txRateMbps": 0, + "txPackets": 0, + "txBytes": 0, + "bandwidthUsedMbps": 0 + }, + "networkStatus6": { + "ssid": "1234", + "hashedWpaKey": "4430f81c11c7bad4d36a886be2ca3b34deb5fd6c8a71ccaf244a22c44ce062e8", + "wpaKeySalt": "darLGfhgtJazer9C", + "isLinked": true, + "macAddress": "4A:DA:35:B0:3A:27", + "signalDbm": -56, + "noiseDbm": -93, + "signalNoiseRatio": 37, + "rxRateMbps": 7.3, + "rxPackets": 4095, + "rxBytes": 344, + "txRateMbps": 516.2, + "txPackets": 0, + "txBytes": 52765, + "bandwidthUsedMbps": 0.002 + }, "status": "ACTIVE", "version": "1.2.3" } @@ -174,8 +205,8 @@ $ curl http://10.56.78.1:8081/status { "teamNumber": 5678, "ssid": "5678", - "hashedWpaKey": "63b7edb8b5c6b832dd495220e67d65414238165b92ef1feb52d6f39c052ac693", - "wpaKeySalt": "6BjRXMUm3kExcAiR", + "networkStatus24": [...], + "networkStatus6": [...], "status": "ACTIVE" } ``` From bff5d40209bad8a4418f23ff4da8d63a2856e7fc Mon Sep 17 00:00:00 2001 From: Patrick Fairbank Date: Sat, 22 Jun 2024 15:31:47 -0700 Subject: [PATCH 17/31] Add support for spare VLANs and for changing which VLANs are used for each alliance. --- README.md | 6 + radio/configuration_request_ap.go | 27 +++- radio/configuration_request_ap_test.go | 17 +++ radio/radio_ap.go | 128 ++++++++++++++---- radio/radio_ap_test.go | 175 +++++++++++++++++++++---- radio/station.go | 4 +- radio/station_string.go | 8 +- wireless-boot-linksys | 13 +- wireless-boot-vh | 48 +++++++ 9 files changed, 364 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index 80e9b29..a155e84 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,8 @@ $ curl http://10.0.100.2:8081/status { "channel": 93, "channelBandwidth": "HT40", + "redVlans": "40_50_60", + "blueVlans": "10_20_30", "status": "ACTIVE", "stationStatuses": { "blue1": null, @@ -108,6 +110,8 @@ The `/configuration` POST endpoint allows the access point to be configured. It $ curl http://10.0.100.2:8081/configuration -XPOST -d '{ "channel": 93, "channelBandwidth": "HT20", + "redVlans": "40_50_60", + "blueVlans": "70_80_90", "stationConfigurations": { "red1": {"ssid": "1111", "wpaKey": "11111111"}, "blue2": {"ssid": "5555", "wpaKey": "55555555"} @@ -122,6 +126,8 @@ $ curl http://10.0.100.2:8081/status { "channel": 93, "channelBandwidth": "HT20", + "redVlans": "40_50_60", + "blueVlans": "70_80_90", "status": "CONFIGURING", "stationStatuses": { "blue1": null, diff --git a/radio/configuration_request_ap.go b/radio/configuration_request_ap.go index 9503f6a..777be47 100644 --- a/radio/configuration_request_ap.go +++ b/radio/configuration_request_ap.go @@ -20,6 +20,12 @@ type ConfigurationRequest struct { // leave unchanged. ChannelBandwidth string `json:"channelBandwidth"` + // VLANs to use for the teams of the red alliance. Valid values are "10_20_30", "40_50_60", and "70_80_90". + RedVlans AllianceVlans `json:"redVlans"` + + // VLANs to use for the teams of the blue alliance. Valid values are "10_20_30", "40_50_60", and "70_80_90". + BlueVlans AllianceVlans `json:"blueVlans"` + // SSID and WPA key for each team station, keyed by alliance and number (e.g. "red1", "blue3). If a station is not // included, its network will be disabled by setting its SSID to a placeholder. StationConfigurations map[string]StationConfiguration `json:"stationConfigurations"` @@ -38,7 +44,8 @@ var validLinksysChannels = []int{36, 40, 44, 48, 149, 153, 157, 161, 165} // Validate checks that all parameters within the configuration request have valid values. func (request ConfigurationRequest) Validate(radio *Radio) error { - if request.Channel == 0 && request.ChannelBandwidth == "" && len(request.StationConfigurations) == 0 { + if request.Channel == 0 && request.ChannelBandwidth == "" && len(request.StationConfigurations) == 0 && + request.RedVlans == "" && request.BlueVlans == "" { return errors.New("empty configuration request") } @@ -71,10 +78,26 @@ func (request ConfigurationRequest) Validate(radio *Radio) error { } } + if request.RedVlans != "" || request.BlueVlans != "" { + if request.RedVlans == "" || request.BlueVlans == "" { + return errors.New("both red and blue VLANs must be specified") + } + validVlans := map[AllianceVlans]struct{}{Vlans102030: {}, Vlans405060: {}, Vlans708090: {}} + if _, ok := validVlans[request.RedVlans]; !ok { + return fmt.Errorf("invalid value for red VLANs: %s", request.RedVlans) + } + if _, ok := validVlans[request.BlueVlans]; !ok { + return fmt.Errorf("invalid value for blue VLANs: %s", request.BlueVlans) + } + if request.RedVlans == request.BlueVlans { + return fmt.Errorf("red and blue VLANs cannot be the same") + } + } + // Validate station configurations. for stationName, stationConfiguration := range request.StationConfigurations { stationNameValid := false - for name := red1; name < stationCount; name++ { + for name := red1; name <= blue3; name++ { if stationName == name.String() { stationNameValid = true break diff --git a/radio/configuration_request_ap_test.go b/radio/configuration_request_ap_test.go index fdd0a17..92a5d3b 100644 --- a/radio/configuration_request_ap_test.go +++ b/radio/configuration_request_ap_test.go @@ -37,6 +37,23 @@ func TestConfigurationRequest_Validate(t *testing.T) { err = request.Validate(linksysRadio) assert.EqualError(t, err, "channel bandwidth cannot be changed on TypeLinksys") + // Invalid VLANs. + request = ConfigurationRequest{RedVlans: "10_20_30"} + err = request.Validate(linksysRadio) + assert.EqualError(t, err, "both red and blue VLANs must be specified") + request = ConfigurationRequest{BlueVlans: "10_20_30"} + err = request.Validate(linksysRadio) + assert.EqualError(t, err, "both red and blue VLANs must be specified") + request = ConfigurationRequest{RedVlans: "20_30_40", BlueVlans: "30_40_50"} + err = request.Validate(linksysRadio) + assert.EqualError(t, err, "invalid value for red VLANs: 20_30_40") + request = ConfigurationRequest{RedVlans: "70_80_90", BlueVlans: "30_40_50"} + err = request.Validate(linksysRadio) + assert.EqualError(t, err, "invalid value for blue VLANs: 30_40_50") + request = ConfigurationRequest{RedVlans: "70_80_90", BlueVlans: "70_80_90"} + err = request.Validate(linksysRadio) + assert.EqualError(t, err, "red and blue VLANs cannot be the same") + // Invalid station. request = ConfigurationRequest{ StationConfigurations: map[string]StationConfiguration{"red4": {Ssid: "254", WpaKey: "12345678"}}, diff --git a/radio/radio_ap.go b/radio/radio_ap.go index 6b93299..eb29faa 100644 --- a/radio/radio_ap.go +++ b/radio/radio_ap.go @@ -20,6 +20,12 @@ type Radio struct { // Channel bandwidth mode for the radio to use. Valid values are "20MHz" and "40MHz". ChannelBandwidth string `json:"channelBandwidth"` + // VLANs to use for the teams of the red alliance. Valid values are "10_20_30", "40_50_60", and "70_80_90". + RedVlans AllianceVlans `json:"redVlans"` + + // VLANs to use for the teams of the blue alliance. Valid values are "10_20_30", "40_50_60", and "70_80_90". + BlueVlans AllianceVlans `json:"blueVlans"` + // Enum representing the current configuration stage of the radio. Status radioStatus `json:"status"` @@ -38,13 +44,28 @@ type Radio struct { // Name of the radio's Wi-Fi device, dependent on the hardware type. device string - // Map of team station names to their Wi-Fi interface names, dependent on the hardware type. - stationInterfaces map[station]string + // Extra set of VLANs that are not used for team networks. Valid values are "10_20_30", "40_50_60", and "70_80_90". + spareVlans AllianceVlans + + // List of Wi-Fi interface names in order of their corresponding VLAN, dependent on the hardware type. + vlanInterfaces []string } +// AllianceVlans represents which three VLANs are used for the teams of an alliance. +type AllianceVlans string + +const ( + Vlans102030 AllianceVlans = "10_20_30" + Vlans405060 AllianceVlans = "40_50_60" + Vlans708090 AllianceVlans = "70_80_90" +) + // NewRadio creates a new Radio instance and initializes its fields to default values. func NewRadio() *Radio { radio := Radio{ + RedVlans: Vlans102030, + BlueVlans: Vlans405060, + spareVlans: Vlans708090, Status: statusBooting, ConfigurationRequestChannel: make(chan ConfigurationRequest, configurationRequestBufferSize), } @@ -59,34 +80,78 @@ func NewRadio() *Radio { switch radio.Type { case TypeLinksys: radio.device = "radio0" - radio.stationInterfaces = map[station]string{ - red1: "wlan0", - red2: "wlan0-1", - red3: "wlan0-2", - blue1: "wlan0-3", - blue2: "wlan0-4", - blue3: "wlan0-5", + radio.vlanInterfaces = []string{ + "wlan0", + "wlan0-1", + "wlan0-2", + "wlan0-3", + "wlan0-4", + "wlan0-5", + "wlan0-6", + "wlan0-7", + "wlan0-8", } case TypeVividHosting: radio.device = "wifi1" - radio.stationInterfaces = map[station]string{ - red1: "ath1", - red2: "ath11", - red3: "ath12", - blue1: "ath13", - blue2: "ath14", - blue3: "ath15", + radio.vlanInterfaces = []string{ + "ath1", + "ath11", + "ath12", + "ath13", + "ath14", + "ath15", + "ath16", + "ath17", + "ath18", } } radio.StationStatuses = make(map[string]*NetworkStatus) - for i := 0; i < int(stationCount); i++ { - radio.StationStatuses[station(i).String()] = nil + for station := red1; station <= blue3; station++ { + radio.StationStatuses[station.String()] = nil } return &radio } +// getStationInterfaceIndex returns the Wi-Fi interface index for the given team station. +func (radio *Radio) getStationInterfaceIndex(station station) int { + var vlans AllianceVlans + var offset int + if station == red1 || station == red2 || station == red3 { + vlans = radio.RedVlans + offset = int(station) - int(red1) + } else if station == blue1 || station == blue2 || station == blue3 { + vlans = radio.BlueVlans + offset = int(station) - int(blue1) + } else if station == spare1 || station == spare2 || station == spare3 { + vlans = radio.spareVlans + offset = int(station) - int(spare1) + } + + switch vlans { + case Vlans102030: + return offset + case Vlans405060: + return 3 + offset + case Vlans708090: + return 6 + offset + default: + // Invalid station. + return -1 + } +} + +// getStationInterfaceName returns the Wi-Fi interface name for the given team station. +func (radio *Radio) getStationInterfaceName(station station) string { + index := radio.getStationInterfaceIndex(station) + if index == -1 { + // Invalid station. + return "" + } + return radio.vlanInterfaces[index] +} + // determineAndSetType determines the model of the radio. func (radio *Radio) determineAndSetType() { model, _ := uciTree.GetLast("system", "@system[0]", "model") @@ -99,7 +164,7 @@ func (radio *Radio) determineAndSetType() { // isStarted returns true if the Wi-Fi interface is up and running. func (radio *Radio) isStarted() bool { - _, err := shell.runCommand("iwinfo", radio.stationInterfaces[blue3], "info") + _, err := shell.runCommand("iwinfo", radio.getStationInterfaceName(blue3), "info") return err == nil } @@ -138,6 +203,17 @@ func (radio *Radio) configure(request ConfigurationRequest) error { uciTree.SetType("wireless", radio.device, "htmode", uci.TypeOption, htmode) radio.ChannelBandwidth = request.ChannelBandwidth } + if request.RedVlans != "" && request.BlueVlans != "" { + radio.RedVlans = request.RedVlans + radio.BlueVlans = request.BlueVlans + if radio.RedVlans != Vlans708090 && radio.BlueVlans != Vlans708090 { + radio.spareVlans = Vlans708090 + } else if radio.RedVlans != Vlans405060 && radio.BlueVlans != Vlans405060 { + radio.spareVlans = Vlans405060 + } else { + radio.spareVlans = Vlans102030 + } + } if radio.Type == TypeLinksys { // Clear the state of the radio before loading teams; the Linksys AP is crash-prone otherwise. @@ -154,10 +230,10 @@ func (radio *Radio) configureStations(stationConfigurations map[string]StationCo retryCount := 1 for { - for stationIndex := 0; stationIndex < 6; stationIndex++ { - position := stationIndex + 1 + for station := red1; station <= spare3; station++ { + position := radio.getStationInterfaceIndex(station) + 1 var ssid, wpaKey string - if config, ok := stationConfigurations[station(stationIndex).String()]; ok { + if config, ok := stationConfigurations[station.String()]; ok { ssid = config.Ssid wpaKey = config.WpaKey } else { @@ -201,8 +277,8 @@ func (radio *Radio) configureStations(stationConfigurations map[string]StationCo // updateStationStatuses fetches the current Wi-Fi status (SSID, WPA key, etc.) for each team station and updates the // in-memory state. func (radio *Radio) updateStationStatuses() error { - for station, stationInterface := range radio.stationInterfaces { - ssid, err := getSsid(stationInterface) + for station := red1; station <= blue3; station++ { + ssid, err := getSsid(radio.getStationInterfaceName(station)) if err != nil { return err } @@ -241,13 +317,13 @@ func (radio *Radio) stationSsidsAreCorrect(stationConfigurations map[string]Stat // updateMonitoring polls the access point for the current bandwidth usage and link state of each team station and // updates the in-memory state. func (radio *Radio) updateMonitoring() { - for station, stationInterface := range radio.stationInterfaces { + for station := red1; station <= blue3; station++ { stationStatus := radio.StationStatuses[station.String()] if stationStatus == nil { // Skip stations that don't have a team assigned. continue } - stationStatus.updateMonitoring(stationInterface) + stationStatus.updateMonitoring(radio.getStationInterfaceName(station)) } } diff --git a/radio/radio_ap_test.go b/radio/radio_ap_test.go index de5b496..9393425 100644 --- a/radio/radio_ap_test.go +++ b/radio/radio_ap_test.go @@ -19,8 +19,8 @@ func TestNewRadio(t *testing.T) { radio := NewRadio() assert.Equal(t, 0, radio.Channel) assert.Equal(t, statusBooting, radio.Status) - if assert.Equal(t, int(stationCount), len(radio.StationStatuses)) { - for i := 0; i < int(stationCount); i++ { + if assert.Equal(t, 6, len(radio.StationStatuses)) { + for i := 0; i < 6; i++ { stationStatus, ok := radio.StationStatuses[station(i).String()] assert.True(t, ok) assert.Nil(t, stationStatus) @@ -31,15 +31,18 @@ func TestNewRadio(t *testing.T) { assert.Equal(t, "wifi1", radio.device) assert.Equal( t, - map[station]string{ - red1: "ath1", - red2: "ath11", - red3: "ath12", - blue1: "ath13", - blue2: "ath14", - blue3: "ath15", + []string{ + "ath1", + "ath11", + "ath12", + "ath13", + "ath14", + "ath15", + "ath16", + "ath17", + "ath18", }, - radio.stationInterfaces, + radio.vlanInterfaces, ) // Using Linksys radio. @@ -47,8 +50,8 @@ func TestNewRadio(t *testing.T) { radio = NewRadio() assert.Equal(t, 0, radio.Channel) assert.Equal(t, statusBooting, radio.Status) - if assert.Equal(t, int(stationCount), len(radio.StationStatuses)) { - for i := 0; i < int(stationCount); i++ { + if assert.Equal(t, 6, len(radio.StationStatuses)) { + for i := 0; i < 6; i++ { stationStatus, ok := radio.StationStatuses[station(i).String()] assert.True(t, ok) assert.Nil(t, stationStatus) @@ -59,18 +62,40 @@ func TestNewRadio(t *testing.T) { assert.Equal(t, "radio0", radio.device) assert.Equal( t, - map[station]string{ - red1: "wlan0", - red2: "wlan0-1", - red3: "wlan0-2", - blue1: "wlan0-3", - blue2: "wlan0-4", - blue3: "wlan0-5", + []string{ + "wlan0", + "wlan0-1", + "wlan0-2", + "wlan0-3", + "wlan0-4", + "wlan0-5", + "wlan0-6", + "wlan0-7", + "wlan0-8", }, - radio.stationInterfaces, + radio.vlanInterfaces, ) } +func TestRadio_getInterfaceForStation(t *testing.T) { + radio := NewRadio() + assert.Equal(t, "wlan0", radio.getStationInterfaceName(red1)) + assert.Equal(t, "wlan0-1", radio.getStationInterfaceName(red2)) + assert.Equal(t, "wlan0-2", radio.getStationInterfaceName(red3)) + assert.Equal(t, "wlan0-3", radio.getStationInterfaceName(blue1)) + assert.Equal(t, "wlan0-4", radio.getStationInterfaceName(blue2)) + assert.Equal(t, "wlan0-5", radio.getStationInterfaceName(blue3)) + + radio.RedVlans = Vlans708090 + radio.BlueVlans = Vlans102030 + assert.Equal(t, "wlan0-6", radio.getStationInterfaceName(red1)) + assert.Equal(t, "wlan0-7", radio.getStationInterfaceName(red2)) + assert.Equal(t, "wlan0-8", radio.getStationInterfaceName(red3)) + assert.Equal(t, "wlan0", radio.getStationInterfaceName(blue1)) + assert.Equal(t, "wlan0-1", radio.getStationInterfaceName(blue2)) + assert.Equal(t, "wlan0-2", radio.getStationInterfaceName(blue3)) +} + func TestRadio_isStarted(t *testing.T) { fakeShell := newFakeShell(t) shell = fakeShell @@ -156,7 +181,7 @@ func TestRadio_handleConfigurationRequestVividHosting(t *testing.T) { radio.ConfigurationRequestChannel <- dummyRequest2 radio.ConfigurationRequestChannel <- request assert.Nil(t, radio.handleConfigurationRequest(dummyRequest1)) - assert.Equal(t, 19, fakeTree.setCount) + assert.Equal(t, 28, fakeTree.setCount) assert.Equal(t, fakeTree.valuesFromSet["wireless.wifi1.channel"], "5") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].ssid"], "1111") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].key"], "11111111") @@ -176,7 +201,17 @@ func TestRadio_handleConfigurationRequestVividHosting(t *testing.T) { assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[6].ssid"], "6666") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[6].key"], "66666666") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[6].sae_password"], "66666666") - assert.Equal(t, 6, fakeTree.commitCount) + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[7].ssid"], "no-team-7") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[7].key"], "no-team-7") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[7].sae_password"], "no-team-7") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[8].ssid"], "no-team-8") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[8].key"], "no-team-8") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[8].sae_password"], "no-team-8") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[9].ssid"], "no-team-9") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[9].key"], "no-team-9") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[9].sae_password"], "no-team-9") + assert.Equal(t, 9, fakeTree.commitCount) + assert.Equal(t, 8, len(fakeShell.commandsRun)) assert.Contains(t, fakeShell.commandsRun, "wifi reload wifi1") assert.Contains(t, fakeShell.commandsRun, "iwinfo ath1 info") assert.Contains(t, fakeShell.commandsRun, "iwinfo ath11 info") @@ -233,7 +268,7 @@ func TestRadio_handleConfigurationRequestLinksys(t *testing.T) { // Allow some time for the first config-clearing change to be processed. time.Sleep(150 * time.Millisecond) - assert.Equal(t, 13, fakeTree.setCount) + assert.Equal(t, 19, fakeTree.setCount) assert.Equal(t, fakeTree.valuesFromSet["wireless.radio0.channel"], "5") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].ssid"], "no-team-1") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].key"], "no-team-1") @@ -247,7 +282,14 @@ func TestRadio_handleConfigurationRequestLinksys(t *testing.T) { assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[5].key"], "no-team-5") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[6].ssid"], "no-team-6") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[6].key"], "no-team-6") - assert.Equal(t, 6, fakeTree.commitCount) + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[7].ssid"], "no-team-7") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[7].key"], "no-team-7") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[8].ssid"], "no-team-8") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[8].key"], "no-team-8") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[9].ssid"], "no-team-9") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[9].key"], "no-team-9") + assert.Equal(t, 9, fakeTree.commitCount) + assert.Equal(t, 8, len(fakeShell.commandsRun)) assert.Contains(t, fakeShell.commandsRun, "wifi reload radio0") assert.Contains(t, fakeShell.commandsRun, "iwinfo wlan0 info") assert.Contains(t, fakeShell.commandsRun, "iwinfo wlan0-1 info") @@ -268,7 +310,7 @@ func TestRadio_handleConfigurationRequestLinksys(t *testing.T) { fakeShell.commandOutput["iwinfo wlan0-5 info"] = "wlan0-5\nESSID: \"no-team-6\"\n" }() assert.Nil(t, radio.handleConfigurationRequest(dummyRequest1)) - assert.Equal(t, 12, fakeTree.setCount) + assert.Equal(t, 18, fakeTree.setCount) assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].ssid"], "no-team-1") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].key"], "no-team-1") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[2].ssid"], "2222") @@ -281,7 +323,14 @@ func TestRadio_handleConfigurationRequestLinksys(t *testing.T) { assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[5].key"], "55555555") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[6].ssid"], "no-team-6") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[6].key"], "no-team-6") - assert.Equal(t, 6, fakeTree.commitCount) + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[7].ssid"], "no-team-7") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[7].key"], "no-team-7") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[8].ssid"], "no-team-8") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[8].key"], "no-team-8") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[9].ssid"], "no-team-9") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[9].key"], "no-team-9") + assert.Equal(t, 9, fakeTree.commitCount) + assert.Equal(t, 7, len(fakeShell.commandsRun)) assert.Contains(t, fakeShell.commandsRun, "wifi reload radio0") assert.Contains(t, fakeShell.commandsRun, "iwinfo wlan0 info") assert.Contains(t, fakeShell.commandsRun, "iwinfo wlan0-1 info") @@ -291,6 +340,80 @@ func TestRadio_handleConfigurationRequestLinksys(t *testing.T) { assert.Contains(t, fakeShell.commandsRun, "iwinfo wlan0-5 info") } +func TestRadio_handleConfigurationRequestSpareVlans(t *testing.T) { + fakeTree := newFakeUciTree() + uciTree = fakeTree + fakeTree.valuesForGet["system.@system[0].model"] = "VH-109(AP)" + fakeShell := newFakeShell(t) + shell = fakeShell + wifiReloadBackoffDuration = 10 * time.Millisecond + fakeShell.commandOutput["cat /etc/vh_firmware"] = "" + radio := NewRadio() + + fakeShell.commandOutput["wifi reload wifi1"] = "" + fakeShell.commandOutput["iwinfo ath16 info"] = "ath1\nESSID: \"1111\"\n" + fakeShell.commandOutput["iwinfo ath17 info"] = "ath11\nESSID: \"no-team-2\"\n" + fakeShell.commandOutput["iwinfo ath18 info"] = "ath12\nESSID: \"3333\"\n" + fakeShell.commandOutput["iwinfo ath1 info"] = "ath13\nESSID: \"no-team-4\"\n" + fakeShell.commandOutput["iwinfo ath11 info"] = "ath14\nESSID: \"5555\"\n" + fakeShell.commandOutput["iwinfo ath12 info"] = "ath15\nESSID: \"6666\"\n" + request := ConfigurationRequest{ + RedVlans: Vlans708090, + BlueVlans: Vlans102030, + StationConfigurations: map[string]StationConfiguration{ + "red1": {Ssid: "1111", WpaKey: "11111111"}, + "red3": {Ssid: "3333", WpaKey: "33333333"}, + "blue2": {Ssid: "5555", WpaKey: "55555555"}, + "blue3": {Ssid: "6666", WpaKey: "66666666"}, + }, + } + assert.Nil(t, radio.handleConfigurationRequest(request)) + assert.Equal(t, 27, fakeTree.setCount) + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].ssid"], "no-team-1") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].key"], "no-team-1") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].sae_password"], "no-team-1") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[2].ssid"], "5555") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[2].key"], "55555555") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[2].sae_password"], "55555555") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[3].ssid"], "6666") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[3].key"], "66666666") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[3].sae_password"], "66666666") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[4].ssid"], "no-team-4") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[4].key"], "no-team-4") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[4].sae_password"], "no-team-4") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[5].ssid"], "no-team-5") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[5].key"], "no-team-5") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[5].sae_password"], "no-team-5") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[6].ssid"], "no-team-6") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[6].key"], "no-team-6") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[6].sae_password"], "no-team-6") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[7].ssid"], "1111") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[7].key"], "11111111") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[7].sae_password"], "11111111") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[8].ssid"], "no-team-8") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[8].key"], "no-team-8") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[8].sae_password"], "no-team-8") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[9].ssid"], "3333") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[9].key"], "33333333") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[9].sae_password"], "33333333") + assert.Equal(t, 9, fakeTree.commitCount) + assert.Equal(t, 8, len(fakeShell.commandsRun)) + assert.Contains(t, fakeShell.commandsRun, "wifi reload wifi1") + assert.Contains(t, fakeShell.commandsRun, "iwinfo ath16 info") + assert.Contains(t, fakeShell.commandsRun, "iwinfo ath17 info") + assert.Contains(t, fakeShell.commandsRun, "iwinfo ath18 info") + assert.Contains(t, fakeShell.commandsRun, "iwinfo ath1 info") + assert.Contains(t, fakeShell.commandsRun, "iwinfo ath11 info") + assert.Contains(t, fakeShell.commandsRun, "iwinfo ath12 info") + + assert.Equal(t, "1111", radio.StationStatuses["red1"].Ssid) + assert.Nil(t, radio.StationStatuses["red2"]) + assert.Equal(t, "3333", radio.StationStatuses["red3"].Ssid) + assert.Nil(t, radio.StationStatuses["blue1"]) + assert.Equal(t, "5555", radio.StationStatuses["blue2"].Ssid) + assert.Equal(t, "6666", radio.StationStatuses["blue3"].Ssid) +} + func TestRadio_handleConfigurationRequestErrors(t *testing.T) { fakeTree := newFakeUciTree() uciTree = fakeTree diff --git a/radio/station.go b/radio/station.go index c935fd4..75693db 100644 --- a/radio/station.go +++ b/radio/station.go @@ -12,5 +12,7 @@ const ( blue1 blue2 blue3 - stationCount + spare1 + spare2 + spare3 ) diff --git a/radio/station_string.go b/radio/station_string.go index f0be91c..55f0a06 100644 --- a/radio/station_string.go +++ b/radio/station_string.go @@ -14,12 +14,14 @@ func _() { _ = x[blue1-3] _ = x[blue2-4] _ = x[blue3-5] - _ = x[stationCount-6] + _ = x[spare1-6] + _ = x[spare2-7] + _ = x[spare3-8] } -const _station_name = "red1red2red3blue1blue2blue3stationCount" +const _station_name = "red1red2red3blue1blue2blue3spare1spare2spare3" -var _station_index = [...]uint8{0, 4, 8, 12, 17, 22, 27, 39} +var _station_index = [...]uint8{0, 4, 8, 12, 17, 22, 27, 33, 39, 45} func (i station) String() string { if i < 0 || i >= station(len(_station_index)-1) { diff --git a/wireless-boot-linksys b/wireless-boot-linksys index fd28f23..6020935 100644 --- a/wireless-boot-linksys +++ b/wireless-boot-linksys @@ -125,7 +125,9 @@ config wifi-iface option hidden '1' option network 'vlan70' option encryption 'psk2+ccmp' - option disabled '1' + option ssid 'no-team-7' + option key 'no-team-7' + option disabled '0' config wifi-iface option device 'radio0' @@ -137,7 +139,9 @@ config wifi-iface option hidden '1' option network 'vlan80' option encryption 'psk2+ccmp' - option disabled '1' + option ssid 'no-team-8' + option key 'no-team-8' + option disabled '0' config wifi-iface option device 'radio0' @@ -149,5 +153,6 @@ config wifi-iface option hidden '1' option network 'vlan90' option encryption 'psk2+ccmp' - option disabled '1' - + option ssid 'no-team-9' + option key 'no-team-9' + option disabled '0' diff --git a/wireless-boot-vh b/wireless-boot-vh index 2a85d50..4519e5a 100644 --- a/wireless-boot-vh +++ b/wireless-boot-vh @@ -130,3 +130,51 @@ config wifi-iface option ssid 'no-team-6' option key 'no-team-6' option sae_password 'no-team-6' + +config wifi-iface + option device 'wifi1' + option mode 'ap' + option disablecoext '1' + option en_6g_sec_comp '0' + option sae '1' + option ieee80211w '1' + option wds '1' + option encryption 'psk2+ccmp' + option disabled '0' + option hidden '0' + option network 'vlan70' + option ssid 'no-team-7' + option key 'no-team-7' + option sae_password 'no-team-7' + +config wifi-iface + option device 'wifi1' + option mode 'ap' + option disablecoext '1' + option en_6g_sec_comp '0' + option sae '1' + option ieee80211w '1' + option wds '1' + option encryption 'psk2+ccmp' + option disabled '0' + option hidden '0' + option network 'vlan80' + option ssid 'no-team-8' + option key 'no-team-8' + option sae_password 'no-team-8' + +config wifi-iface + option device 'wifi1' + option mode 'ap' + option disablecoext '1' + option en_6g_sec_comp '0' + option sae '1' + option ieee80211w '1' + option wds '1' + option encryption 'psk2+ccmp' + option disabled '0' + option hidden '0' + option network 'vlan90' + option ssid 'no-team-9' + option key 'no-team-9' + option sae_password 'no-team-9' From 6a85b66c917940af135db1e2852ad39dcdbcd606 Mon Sep 17 00:00:00 2001 From: Patrick Fairbank Date: Sat, 29 Jun 2024 14:19:29 -0700 Subject: [PATCH 18/31] Add ability to set syslog IP address via the API. --- radio/configuration_request_ap.go | 13 ++++++++++++- radio/configuration_request_ap_test.go | 5 +++++ radio/radio_ap.go | 15 +++++++++++++++ radio/radio_ap_test.go | 13 ++++++++++--- 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/radio/configuration_request_ap.go b/radio/configuration_request_ap.go index 777be47..ceceb8c 100644 --- a/radio/configuration_request_ap.go +++ b/radio/configuration_request_ap.go @@ -29,6 +29,9 @@ type ConfigurationRequest struct { // SSID and WPA key for each team station, keyed by alliance and number (e.g. "red1", "blue3). If a station is not // included, its network will be disabled by setting its SSID to a placeholder. StationConfigurations map[string]StationConfiguration `json:"stationConfigurations"` + + // IP address of the syslog server to send logs to (via UDP on port 514). + SyslogIpAddress string `json:"syslogIpAddress"` } // StationConfiguration represents the configuration for a single team station. @@ -45,7 +48,7 @@ var validLinksysChannels = []int{36, 40, 44, 48, 149, 153, 157, 161, 165} // Validate checks that all parameters within the configuration request have valid values. func (request ConfigurationRequest) Validate(radio *Radio) error { if request.Channel == 0 && request.ChannelBandwidth == "" && len(request.StationConfigurations) == 0 && - request.RedVlans == "" && request.BlueVlans == "" { + request.RedVlans == "" && request.BlueVlans == "" && request.SyslogIpAddress == "" { return errors.New("empty configuration request") } @@ -126,5 +129,13 @@ func (request ConfigurationRequest) Validate(radio *Radio) error { } } + // Validate syslog IP address. + if request.SyslogIpAddress != "" { + match, _ := regexp.MatchString("^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}$", request.SyslogIpAddress) + if !match { + return fmt.Errorf("invalid syslog IP address: %s", request.SyslogIpAddress) + } + } + return nil } diff --git a/radio/configuration_request_ap_test.go b/radio/configuration_request_ap_test.go index 92a5d3b..26cca5d 100644 --- a/radio/configuration_request_ap_test.go +++ b/radio/configuration_request_ap_test.go @@ -95,4 +95,9 @@ func TestConfigurationRequest_Validate(t *testing.T) { } err = request.Validate(linksysRadio) assert.EqualError(t, err, "invalid WPA key for station blue1 (expecting alphanumeric)") + + // Invalid syslog IP address. + request = ConfigurationRequest{SyslogIpAddress: "10.0.100.256"} + err = request.Validate(linksysRadio) + assert.EqualError(t, err, "invalid syslog IP address: 10.0.100.256") } diff --git a/radio/radio_ap.go b/radio/radio_ap.go index eb29faa..c735bf8 100644 --- a/radio/radio_ap.go +++ b/radio/radio_ap.go @@ -32,6 +32,9 @@ type Radio struct { // Map of team station names to their current status. StationStatuses map[string]*NetworkStatus `json:"stationStatuses"` + // IP address of the syslog server to send logs to (via UDP on port 514). + SyslogIpAddress string `json:"syslogIpAddress"` + // Version of the radio software. Version string `json:"version"` @@ -182,6 +185,8 @@ func (radio *Radio) setInitialState() { radio.ChannelBandwidth = "INVALID" } _ = radio.updateStationStatuses() + + radio.SyslogIpAddress, _ = uciTree.GetLast("system", "@system[0]", "log_ip") } // configure configures the radio with the given configuration. @@ -214,6 +219,16 @@ func (radio *Radio) configure(request ConfigurationRequest) error { radio.spareVlans = Vlans102030 } } + if request.SyslogIpAddress != "" { + uciTree.SetType("system", "@system[0]", "log_ip", uci.TypeOption, request.SyslogIpAddress) + if err := uciTree.Commit(); err != nil { + return fmt.Errorf("failed to commit system configuration: %v", err) + } + radio.SyslogIpAddress = request.SyslogIpAddress + if _, err := shell.runCommand("/etc/init.d/log", "restart"); err != nil { + return fmt.Errorf("failed to restart syslog service: %v", err) + } + } if radio.Type == TypeLinksys { // Clear the state of the radio before loading teams; the Linksys AP is crash-prone otherwise. diff --git a/radio/radio_ap_test.go b/radio/radio_ap_test.go index 9393425..b1c0fd3 100644 --- a/radio/radio_ap_test.go +++ b/radio/radio_ap_test.go @@ -127,6 +127,7 @@ func TestRadio_setInitialState(t *testing.T) { fakeTree.valuesForGet["wireless.wifi1.channel"] = "23" fakeTree.valuesForGet["wireless.wifi1.htmode"] = "HT20" + fakeTree.valuesForGet["system.@system[0].log_ip"] = "10.20.30.40" fakeShell.commandOutput["iwinfo ath1 info"] = "ath1\nESSID: \"1111\"\n" fakeShell.commandOutput["iwinfo ath11 info"] = "ath11\nESSID: \"no-team-2\"\n" fakeShell.commandOutput["iwinfo ath12 info"] = "ath12\nESSID: \"no-team-3\"\n" @@ -142,6 +143,7 @@ func TestRadio_setInitialState(t *testing.T) { assert.Nil(t, radio.StationStatuses["blue1"]) assert.Nil(t, radio.StationStatuses["blue2"]) assert.Equal(t, "6666", radio.StationStatuses["blue3"].Ssid) + assert.Equal(t, "10.20.30.40", radio.SyslogIpAddress) } func TestRadio_handleConfigurationRequestVividHosting(t *testing.T) { @@ -154,6 +156,7 @@ func TestRadio_handleConfigurationRequestVividHosting(t *testing.T) { fakeShell.commandOutput["cat /etc/vh_firmware"] = "" radio := NewRadio() + fakeShell.commandOutput["/etc/init.d/log restart"] = "" fakeShell.commandOutput["wifi reload wifi1"] = "" fakeShell.commandOutput["iwinfo ath1 info"] = "ath1\nESSID: \"1111\"\n" fakeShell.commandOutput["iwinfo ath11 info"] = "ath11\nESSID: \"no-team-2\"\n" @@ -177,12 +180,14 @@ func TestRadio_handleConfigurationRequestVividHosting(t *testing.T) { "blue2": {Ssid: "5555", WpaKey: "55555555"}, "blue3": {Ssid: "6666", WpaKey: "66666666"}, }, + SyslogIpAddress: "12.34.56.78", } radio.ConfigurationRequestChannel <- dummyRequest2 radio.ConfigurationRequestChannel <- request assert.Nil(t, radio.handleConfigurationRequest(dummyRequest1)) - assert.Equal(t, 28, fakeTree.setCount) + assert.Equal(t, 29, fakeTree.setCount) assert.Equal(t, fakeTree.valuesFromSet["wireless.wifi1.channel"], "5") + assert.Equal(t, fakeTree.valuesFromSet["system.@system[0].log_ip"], "12.34.56.78") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].ssid"], "1111") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].key"], "11111111") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].sae_password"], "11111111") @@ -210,8 +215,9 @@ func TestRadio_handleConfigurationRequestVividHosting(t *testing.T) { assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[9].ssid"], "no-team-9") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[9].key"], "no-team-9") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[9].sae_password"], "no-team-9") - assert.Equal(t, 9, fakeTree.commitCount) - assert.Equal(t, 8, len(fakeShell.commandsRun)) + assert.Equal(t, 10, fakeTree.commitCount) + assert.Equal(t, 9, len(fakeShell.commandsRun)) + assert.Contains(t, fakeShell.commandsRun, "/etc/init.d/log restart") assert.Contains(t, fakeShell.commandsRun, "wifi reload wifi1") assert.Contains(t, fakeShell.commandsRun, "iwinfo ath1 info") assert.Contains(t, fakeShell.commandsRun, "iwinfo ath11 info") @@ -220,6 +226,7 @@ func TestRadio_handleConfigurationRequestVividHosting(t *testing.T) { assert.Contains(t, fakeShell.commandsRun, "iwinfo ath14 info") assert.Contains(t, fakeShell.commandsRun, "iwinfo ath15 info") + assert.Equal(t, "12.34.56.78", radio.SyslogIpAddress) assert.Equal(t, "1111", radio.StationStatuses["red1"].Ssid) assert.Nil(t, radio.StationStatuses["red2"]) assert.Equal(t, "3333", radio.StationStatuses["red3"].Ssid) From 8575a07770615d137d24b2f5cc9b8bb7ea241ea1 Mon Sep 17 00:00:00 2001 From: Patrick Fairbank Date: Sat, 29 Jun 2024 14:24:05 -0700 Subject: [PATCH 19/31] Add syslogIpAddress to README.md. --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a155e84..9da4a27 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ $ curl http://10.0.100.2:8081/status "red2": null, "red3": null }, + "syslogIpAddress": "10.0.100.5", "version": "1.2.3" } ``` @@ -115,7 +116,8 @@ $ curl http://10.0.100.2:8081/configuration -XPOST -d '{ "stationConfigurations": { "red1": {"ssid": "1111", "wpaKey": "11111111"}, "blue2": {"ssid": "5555", "wpaKey": "55555555"} - } + }, + "syslogIpAddress": "10.0.100.40" }' New configuration received and will be applied asynchronously. ``` @@ -136,7 +138,8 @@ $ curl http://10.0.100.2:8081/status "red1": null, "red2": null, "red3": null - } + }, + "syslogIpAddress": "10.0.100.40" } ``` From 667bac67bd4d128023b84080e471fe029611d6ff Mon Sep 17 00:00:00 2001 From: Patrick Fairbank Date: Sat, 29 Jun 2024 15:14:45 -0700 Subject: [PATCH 20/31] Change VLANs associated with each wireless interface rather than having nine fixed wireless interfaces. --- radio/radio_ap.go | 90 +++++++------------- radio/radio_ap_test.go | 180 +++++++++++++++++++--------------------- radio/station.go | 3 - radio/station_string.go | 7 +- wireless-boot-linksys | 42 ---------- wireless-boot-vh | 48 ----------- 6 files changed, 118 insertions(+), 252 deletions(-) diff --git a/radio/radio_ap.go b/radio/radio_ap.go index c735bf8..389b39a 100644 --- a/radio/radio_ap.go +++ b/radio/radio_ap.go @@ -47,11 +47,8 @@ type Radio struct { // Name of the radio's Wi-Fi device, dependent on the hardware type. device string - // Extra set of VLANs that are not used for team networks. Valid values are "10_20_30", "40_50_60", and "70_80_90". - spareVlans AllianceVlans - - // List of Wi-Fi interface names in order of their corresponding VLAN, dependent on the hardware type. - vlanInterfaces []string + // Map of team station names to their Wi-Fi interface names, dependent on the hardware type. + stationInterfaces map[station]string } // AllianceVlans represents which three VLANs are used for the teams of an alliance. @@ -68,7 +65,6 @@ func NewRadio() *Radio { radio := Radio{ RedVlans: Vlans102030, BlueVlans: Vlans405060, - spareVlans: Vlans708090, Status: statusBooting, ConfigurationRequestChannel: make(chan ConfigurationRequest, configurationRequestBufferSize), } @@ -83,29 +79,23 @@ func NewRadio() *Radio { switch radio.Type { case TypeLinksys: radio.device = "radio0" - radio.vlanInterfaces = []string{ - "wlan0", - "wlan0-1", - "wlan0-2", - "wlan0-3", - "wlan0-4", - "wlan0-5", - "wlan0-6", - "wlan0-7", - "wlan0-8", + radio.stationInterfaces = map[station]string{ + red1: "wlan0", + red2: "wlan0-1", + red3: "wlan0-2", + blue1: "wlan0-3", + blue2: "wlan0-4", + blue3: "wlan0-5", } case TypeVividHosting: radio.device = "wifi1" - radio.vlanInterfaces = []string{ - "ath1", - "ath11", - "ath12", - "ath13", - "ath14", - "ath15", - "ath16", - "ath17", - "ath18", + radio.stationInterfaces = map[station]string{ + red1: "ath1", + red2: "ath11", + red3: "ath12", + blue1: "ath13", + blue2: "ath14", + blue3: "ath15", } } @@ -117,44 +107,31 @@ func NewRadio() *Radio { return &radio } -// getStationInterfaceIndex returns the Wi-Fi interface index for the given team station. -func (radio *Radio) getStationInterfaceIndex(station station) int { +// getStationVlan returns the VLAN number for the given team station. +func (radio *Radio) getStationVlan(station station) int { var vlans AllianceVlans - var offset int + var position int if station == red1 || station == red2 || station == red3 { vlans = radio.RedVlans - offset = int(station) - int(red1) + position = int(station) - int(red1) + 1 } else if station == blue1 || station == blue2 || station == blue3 { vlans = radio.BlueVlans - offset = int(station) - int(blue1) - } else if station == spare1 || station == spare2 || station == spare3 { - vlans = radio.spareVlans - offset = int(station) - int(spare1) + position = int(station) - int(blue1) + 1 } switch vlans { case Vlans102030: - return offset + return 10 * position case Vlans405060: - return 3 + offset + return 10 * (position + 3) case Vlans708090: - return 6 + offset + return 10 * (position + 6) default: // Invalid station. return -1 } } -// getStationInterfaceName returns the Wi-Fi interface name for the given team station. -func (radio *Radio) getStationInterfaceName(station station) string { - index := radio.getStationInterfaceIndex(station) - if index == -1 { - // Invalid station. - return "" - } - return radio.vlanInterfaces[index] -} - // determineAndSetType determines the model of the radio. func (radio *Radio) determineAndSetType() { model, _ := uciTree.GetLast("system", "@system[0]", "model") @@ -167,7 +144,7 @@ func (radio *Radio) determineAndSetType() { // isStarted returns true if the Wi-Fi interface is up and running. func (radio *Radio) isStarted() bool { - _, err := shell.runCommand("iwinfo", radio.getStationInterfaceName(blue3), "info") + _, err := shell.runCommand("iwinfo", radio.stationInterfaces[blue3], "info") return err == nil } @@ -211,13 +188,6 @@ func (radio *Radio) configure(request ConfigurationRequest) error { if request.RedVlans != "" && request.BlueVlans != "" { radio.RedVlans = request.RedVlans radio.BlueVlans = request.BlueVlans - if radio.RedVlans != Vlans708090 && radio.BlueVlans != Vlans708090 { - radio.spareVlans = Vlans708090 - } else if radio.RedVlans != Vlans405060 && radio.BlueVlans != Vlans405060 { - radio.spareVlans = Vlans405060 - } else { - radio.spareVlans = Vlans102030 - } } if request.SyslogIpAddress != "" { uciTree.SetType("system", "@system[0]", "log_ip", uci.TypeOption, request.SyslogIpAddress) @@ -245,8 +215,8 @@ func (radio *Radio) configureStations(stationConfigurations map[string]StationCo retryCount := 1 for { - for station := red1; station <= spare3; station++ { - position := radio.getStationInterfaceIndex(station) + 1 + for station := red1; station <= blue3; station++ { + position := int(station) + 1 var ssid, wpaKey string if config, ok := stationConfigurations[station.String()]; ok { ssid = config.Ssid @@ -262,6 +232,8 @@ func (radio *Radio) configureStations(stationConfigurations map[string]StationCo if radio.Type == TypeVividHosting { uciTree.SetType("wireless", wifiInterface, "sae_password", uci.TypeOption, wpaKey) } + vlan := fmt.Sprintf("vlan%d", radio.getStationVlan(station)) + uciTree.SetType("wireless", wifiInterface, "network", uci.TypeOption, vlan) if err := uciTree.Commit(); err != nil { return fmt.Errorf("failed to commit wireless configuration: %v", err) @@ -293,7 +265,7 @@ func (radio *Radio) configureStations(stationConfigurations map[string]StationCo // in-memory state. func (radio *Radio) updateStationStatuses() error { for station := red1; station <= blue3; station++ { - ssid, err := getSsid(radio.getStationInterfaceName(station)) + ssid, err := getSsid(radio.stationInterfaces[station]) if err != nil { return err } @@ -339,6 +311,6 @@ func (radio *Radio) updateMonitoring() { continue } - stationStatus.updateMonitoring(radio.getStationInterfaceName(station)) + stationStatus.updateMonitoring(radio.stationInterfaces[station]) } } diff --git a/radio/radio_ap_test.go b/radio/radio_ap_test.go index b1c0fd3..3f52d34 100644 --- a/radio/radio_ap_test.go +++ b/radio/radio_ap_test.go @@ -31,18 +31,15 @@ func TestNewRadio(t *testing.T) { assert.Equal(t, "wifi1", radio.device) assert.Equal( t, - []string{ - "ath1", - "ath11", - "ath12", - "ath13", - "ath14", - "ath15", - "ath16", - "ath17", - "ath18", + map[station]string{ + red1: "ath1", + red2: "ath11", + red3: "ath12", + blue1: "ath13", + blue2: "ath14", + blue3: "ath15", }, - radio.vlanInterfaces, + radio.stationInterfaces, ) // Using Linksys radio. @@ -62,38 +59,37 @@ func TestNewRadio(t *testing.T) { assert.Equal(t, "radio0", radio.device) assert.Equal( t, - []string{ - "wlan0", - "wlan0-1", - "wlan0-2", - "wlan0-3", - "wlan0-4", - "wlan0-5", - "wlan0-6", - "wlan0-7", - "wlan0-8", + map[station]string{ + red1: "wlan0", + red2: "wlan0-1", + red3: "wlan0-2", + blue1: "wlan0-3", + blue2: "wlan0-4", + blue3: "wlan0-5", }, - radio.vlanInterfaces, + radio.stationInterfaces, ) } -func TestRadio_getInterfaceForStation(t *testing.T) { +func TestRadio_getStationVlan(t *testing.T) { radio := NewRadio() - assert.Equal(t, "wlan0", radio.getStationInterfaceName(red1)) - assert.Equal(t, "wlan0-1", radio.getStationInterfaceName(red2)) - assert.Equal(t, "wlan0-2", radio.getStationInterfaceName(red3)) - assert.Equal(t, "wlan0-3", radio.getStationInterfaceName(blue1)) - assert.Equal(t, "wlan0-4", radio.getStationInterfaceName(blue2)) - assert.Equal(t, "wlan0-5", radio.getStationInterfaceName(blue3)) + assert.Equal(t, 10, radio.getStationVlan(red1)) + assert.Equal(t, 20, radio.getStationVlan(red2)) + assert.Equal(t, 30, radio.getStationVlan(red3)) + assert.Equal(t, 40, radio.getStationVlan(blue1)) + assert.Equal(t, 50, radio.getStationVlan(blue2)) + assert.Equal(t, 60, radio.getStationVlan(blue3)) + assert.Equal(t, -1, radio.getStationVlan(6)) radio.RedVlans = Vlans708090 radio.BlueVlans = Vlans102030 - assert.Equal(t, "wlan0-6", radio.getStationInterfaceName(red1)) - assert.Equal(t, "wlan0-7", radio.getStationInterfaceName(red2)) - assert.Equal(t, "wlan0-8", radio.getStationInterfaceName(red3)) - assert.Equal(t, "wlan0", radio.getStationInterfaceName(blue1)) - assert.Equal(t, "wlan0-1", radio.getStationInterfaceName(blue2)) - assert.Equal(t, "wlan0-2", radio.getStationInterfaceName(blue3)) + assert.Equal(t, 70, radio.getStationVlan(red1)) + assert.Equal(t, 80, radio.getStationVlan(red2)) + assert.Equal(t, 90, radio.getStationVlan(red3)) + assert.Equal(t, 10, radio.getStationVlan(blue1)) + assert.Equal(t, 20, radio.getStationVlan(blue2)) + assert.Equal(t, 30, radio.getStationVlan(blue3)) + assert.Equal(t, -1, radio.getStationVlan(6)) } func TestRadio_isStarted(t *testing.T) { @@ -185,37 +181,34 @@ func TestRadio_handleConfigurationRequestVividHosting(t *testing.T) { radio.ConfigurationRequestChannel <- dummyRequest2 radio.ConfigurationRequestChannel <- request assert.Nil(t, radio.handleConfigurationRequest(dummyRequest1)) - assert.Equal(t, 29, fakeTree.setCount) + assert.Equal(t, 26, fakeTree.setCount) assert.Equal(t, fakeTree.valuesFromSet["wireless.wifi1.channel"], "5") assert.Equal(t, fakeTree.valuesFromSet["system.@system[0].log_ip"], "12.34.56.78") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].ssid"], "1111") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].key"], "11111111") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].sae_password"], "11111111") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].network"], "vlan10") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[2].ssid"], "no-team-2") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[2].key"], "no-team-2") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[2].sae_password"], "no-team-2") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[2].network"], "vlan20") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[3].ssid"], "3333") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[3].key"], "33333333") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[3].sae_password"], "33333333") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[3].network"], "vlan30") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[4].ssid"], "no-team-4") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[4].key"], "no-team-4") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[4].sae_password"], "no-team-4") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[4].network"], "vlan40") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[5].ssid"], "5555") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[5].key"], "55555555") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[5].sae_password"], "55555555") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[5].network"], "vlan50") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[6].ssid"], "6666") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[6].key"], "66666666") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[6].sae_password"], "66666666") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[7].ssid"], "no-team-7") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[7].key"], "no-team-7") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[7].sae_password"], "no-team-7") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[8].ssid"], "no-team-8") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[8].key"], "no-team-8") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[8].sae_password"], "no-team-8") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[9].ssid"], "no-team-9") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[9].key"], "no-team-9") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[9].sae_password"], "no-team-9") - assert.Equal(t, 10, fakeTree.commitCount) + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[6].network"], "vlan60") + assert.Equal(t, 7, fakeTree.commitCount) assert.Equal(t, 9, len(fakeShell.commandsRun)) assert.Contains(t, fakeShell.commandsRun, "/etc/init.d/log restart") assert.Contains(t, fakeShell.commandsRun, "wifi reload wifi1") @@ -279,23 +272,23 @@ func TestRadio_handleConfigurationRequestLinksys(t *testing.T) { assert.Equal(t, fakeTree.valuesFromSet["wireless.radio0.channel"], "5") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].ssid"], "no-team-1") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].key"], "no-team-1") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].network"], "vlan10") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[2].ssid"], "no-team-2") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[2].key"], "no-team-2") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[2].network"], "vlan20") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[3].ssid"], "no-team-3") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[3].key"], "no-team-3") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[3].network"], "vlan30") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[4].ssid"], "no-team-4") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[4].key"], "no-team-4") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[4].network"], "vlan40") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[5].ssid"], "no-team-5") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[5].key"], "no-team-5") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[5].network"], "vlan50") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[6].ssid"], "no-team-6") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[6].key"], "no-team-6") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[7].ssid"], "no-team-7") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[7].key"], "no-team-7") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[8].ssid"], "no-team-8") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[8].key"], "no-team-8") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[9].ssid"], "no-team-9") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[9].key"], "no-team-9") - assert.Equal(t, 9, fakeTree.commitCount) + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[6].network"], "vlan60") + assert.Equal(t, 6, fakeTree.commitCount) assert.Equal(t, 8, len(fakeShell.commandsRun)) assert.Contains(t, fakeShell.commandsRun, "wifi reload radio0") assert.Contains(t, fakeShell.commandsRun, "iwinfo wlan0 info") @@ -320,23 +313,23 @@ func TestRadio_handleConfigurationRequestLinksys(t *testing.T) { assert.Equal(t, 18, fakeTree.setCount) assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].ssid"], "no-team-1") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].key"], "no-team-1") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].network"], "vlan10") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[2].ssid"], "2222") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[2].key"], "22222222") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[2].network"], "vlan20") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[3].ssid"], "3333") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[3].key"], "33333333") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[3].network"], "vlan30") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[4].ssid"], "4444") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[4].key"], "44444444") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[4].network"], "vlan40") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[5].ssid"], "5555") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[5].key"], "55555555") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[5].network"], "vlan50") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[6].ssid"], "no-team-6") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[6].key"], "no-team-6") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[7].ssid"], "no-team-7") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[7].key"], "no-team-7") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[8].ssid"], "no-team-8") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[8].key"], "no-team-8") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[9].ssid"], "no-team-9") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[9].key"], "no-team-9") - assert.Equal(t, 9, fakeTree.commitCount) + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[6].network"], "vlan60") + assert.Equal(t, 6, fakeTree.commitCount) assert.Equal(t, 7, len(fakeShell.commandsRun)) assert.Contains(t, fakeShell.commandsRun, "wifi reload radio0") assert.Contains(t, fakeShell.commandsRun, "iwinfo wlan0 info") @@ -358,12 +351,12 @@ func TestRadio_handleConfigurationRequestSpareVlans(t *testing.T) { radio := NewRadio() fakeShell.commandOutput["wifi reload wifi1"] = "" - fakeShell.commandOutput["iwinfo ath16 info"] = "ath1\nESSID: \"1111\"\n" - fakeShell.commandOutput["iwinfo ath17 info"] = "ath11\nESSID: \"no-team-2\"\n" - fakeShell.commandOutput["iwinfo ath18 info"] = "ath12\nESSID: \"3333\"\n" - fakeShell.commandOutput["iwinfo ath1 info"] = "ath13\nESSID: \"no-team-4\"\n" - fakeShell.commandOutput["iwinfo ath11 info"] = "ath14\nESSID: \"5555\"\n" - fakeShell.commandOutput["iwinfo ath12 info"] = "ath15\nESSID: \"6666\"\n" + fakeShell.commandOutput["iwinfo ath1 info"] = "ath1\nESSID: \"1111\"\n" + fakeShell.commandOutput["iwinfo ath11 info"] = "ath11\nESSID: \"no-team-2\"\n" + fakeShell.commandOutput["iwinfo ath12 info"] = "ath12\nESSID: \"3333\"\n" + fakeShell.commandOutput["iwinfo ath13 info"] = "ath13\nESSID: \"no-team-4\"\n" + fakeShell.commandOutput["iwinfo ath14 info"] = "ath14\nESSID: \"5555\"\n" + fakeShell.commandOutput["iwinfo ath15 info"] = "ath15\nESSID: \"6666\"\n" request := ConfigurationRequest{ RedVlans: Vlans708090, BlueVlans: Vlans102030, @@ -375,43 +368,40 @@ func TestRadio_handleConfigurationRequestSpareVlans(t *testing.T) { }, } assert.Nil(t, radio.handleConfigurationRequest(request)) - assert.Equal(t, 27, fakeTree.setCount) - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].ssid"], "no-team-1") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].key"], "no-team-1") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].sae_password"], "no-team-1") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[2].ssid"], "5555") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[2].key"], "55555555") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[2].sae_password"], "55555555") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[3].ssid"], "6666") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[3].key"], "66666666") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[3].sae_password"], "66666666") + assert.Equal(t, 24, fakeTree.setCount) + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].ssid"], "1111") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].key"], "11111111") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].sae_password"], "11111111") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].network"], "vlan70") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[2].ssid"], "no-team-2") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[2].key"], "no-team-2") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[2].sae_password"], "no-team-2") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[2].network"], "vlan80") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[3].ssid"], "3333") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[3].key"], "33333333") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[3].sae_password"], "33333333") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[3].network"], "vlan90") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[4].ssid"], "no-team-4") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[4].key"], "no-team-4") assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[4].sae_password"], "no-team-4") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[5].ssid"], "no-team-5") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[5].key"], "no-team-5") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[5].sae_password"], "no-team-5") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[6].ssid"], "no-team-6") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[6].key"], "no-team-6") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[6].sae_password"], "no-team-6") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[7].ssid"], "1111") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[7].key"], "11111111") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[7].sae_password"], "11111111") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[8].ssid"], "no-team-8") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[8].key"], "no-team-8") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[8].sae_password"], "no-team-8") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[9].ssid"], "3333") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[9].key"], "33333333") - assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[9].sae_password"], "33333333") - assert.Equal(t, 9, fakeTree.commitCount) + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[4].network"], "vlan10") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[5].ssid"], "5555") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[5].key"], "55555555") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[5].sae_password"], "55555555") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[5].network"], "vlan20") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[6].ssid"], "6666") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[6].key"], "66666666") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[6].sae_password"], "66666666") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[6].network"], "vlan30") + assert.Equal(t, 6, fakeTree.commitCount) assert.Equal(t, 8, len(fakeShell.commandsRun)) assert.Contains(t, fakeShell.commandsRun, "wifi reload wifi1") - assert.Contains(t, fakeShell.commandsRun, "iwinfo ath16 info") - assert.Contains(t, fakeShell.commandsRun, "iwinfo ath17 info") - assert.Contains(t, fakeShell.commandsRun, "iwinfo ath18 info") assert.Contains(t, fakeShell.commandsRun, "iwinfo ath1 info") assert.Contains(t, fakeShell.commandsRun, "iwinfo ath11 info") assert.Contains(t, fakeShell.commandsRun, "iwinfo ath12 info") + assert.Contains(t, fakeShell.commandsRun, "iwinfo ath13 info") + assert.Contains(t, fakeShell.commandsRun, "iwinfo ath14 info") + assert.Contains(t, fakeShell.commandsRun, "iwinfo ath15 info") assert.Equal(t, "1111", radio.StationStatuses["red1"].Ssid) assert.Nil(t, radio.StationStatuses["red2"]) diff --git a/radio/station.go b/radio/station.go index 75693db..90c1f69 100644 --- a/radio/station.go +++ b/radio/station.go @@ -12,7 +12,4 @@ const ( blue1 blue2 blue3 - spare1 - spare2 - spare3 ) diff --git a/radio/station_string.go b/radio/station_string.go index 55f0a06..309b596 100644 --- a/radio/station_string.go +++ b/radio/station_string.go @@ -14,14 +14,11 @@ func _() { _ = x[blue1-3] _ = x[blue2-4] _ = x[blue3-5] - _ = x[spare1-6] - _ = x[spare2-7] - _ = x[spare3-8] } -const _station_name = "red1red2red3blue1blue2blue3spare1spare2spare3" +const _station_name = "red1red2red3blue1blue2blue3" -var _station_index = [...]uint8{0, 4, 8, 12, 17, 22, 27, 33, 39, 45} +var _station_index = [...]uint8{0, 4, 8, 12, 17, 22, 27} func (i station) String() string { if i < 0 || i >= station(len(_station_index)-1) { diff --git a/wireless-boot-linksys b/wireless-boot-linksys index 6020935..288742c 100644 --- a/wireless-boot-linksys +++ b/wireless-boot-linksys @@ -114,45 +114,3 @@ config wifi-iface option ssid 'no-team-6' option key 'no-team-6' option disabled '0' - -config wifi-iface - option device 'radio0' - option mode 'ap' - option isolate '0' - option bgscan '0' - option wds '0' - option maxassoc '1' - option hidden '1' - option network 'vlan70' - option encryption 'psk2+ccmp' - option ssid 'no-team-7' - option key 'no-team-7' - option disabled '0' - -config wifi-iface - option device 'radio0' - option mode 'ap' - option isolate '0' - option bgscan '0' - option wds '0' - option maxassoc '1' - option hidden '1' - option network 'vlan80' - option encryption 'psk2+ccmp' - option ssid 'no-team-8' - option key 'no-team-8' - option disabled '0' - -config wifi-iface - option device 'radio0' - option mode 'ap' - option isolate '0' - option bgscan '0' - option wds '0' - option maxassoc '1' - option hidden '1' - option network 'vlan90' - option encryption 'psk2+ccmp' - option ssid 'no-team-9' - option key 'no-team-9' - option disabled '0' diff --git a/wireless-boot-vh b/wireless-boot-vh index 4519e5a..2a85d50 100644 --- a/wireless-boot-vh +++ b/wireless-boot-vh @@ -130,51 +130,3 @@ config wifi-iface option ssid 'no-team-6' option key 'no-team-6' option sae_password 'no-team-6' - -config wifi-iface - option device 'wifi1' - option mode 'ap' - option disablecoext '1' - option en_6g_sec_comp '0' - option sae '1' - option ieee80211w '1' - option wds '1' - option encryption 'psk2+ccmp' - option disabled '0' - option hidden '0' - option network 'vlan70' - option ssid 'no-team-7' - option key 'no-team-7' - option sae_password 'no-team-7' - -config wifi-iface - option device 'wifi1' - option mode 'ap' - option disablecoext '1' - option en_6g_sec_comp '0' - option sae '1' - option ieee80211w '1' - option wds '1' - option encryption 'psk2+ccmp' - option disabled '0' - option hidden '0' - option network 'vlan80' - option ssid 'no-team-8' - option key 'no-team-8' - option sae_password 'no-team-8' - -config wifi-iface - option device 'wifi1' - option mode 'ap' - option disablecoext '1' - option en_6g_sec_comp '0' - option sae '1' - option ieee80211w '1' - option wds '1' - option encryption 'psk2+ccmp' - option disabled '0' - option hidden '0' - option network 'vlan90' - option ssid 'no-team-9' - option key 'no-team-9' - option sae_password 'no-team-9' From 1a255e62aced3e98c2bc459167c9fffae789033f Mon Sep 17 00:00:00 2001 From: Scott Semtner Date: Thu, 29 Aug 2024 15:33:47 -0700 Subject: [PATCH 21/31] Fix log file closing prematurely --- main.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/main.go b/main.go index dcaa126..9942f07 100644 --- a/main.go +++ b/main.go @@ -20,7 +20,11 @@ const ( ) func main() { - setupLogging() + logFile := setupLogging() + log.Println("Starting FRC Radio API...") + if logFile != nil { + defer logFile.Close() + } radio := radio.NewRadio() fmt.Println("created radio") @@ -35,7 +39,7 @@ func main() { } // setupLogging sets up logging to a file, or to stdout if the file can't be opened. -func setupLogging() { +func setupLogging() *os.File { // Rotate the log file if the current one is too big. if fileInfo, err := os.Stat(logFilePath); err == nil { if fileInfo.Size() >= logFileMaxSizeBytes { @@ -47,10 +51,10 @@ func setupLogging() { logFile, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666) if err == nil { - defer logFile.Close() log.SetOutput(logFile) + return logFile } else { log.Printf("error opening log file; logging to stdout instead: %v", err) + return nil } - log.Println("Starting FRC Radio API...") } From d14b4d82b7c79ef93100e318b8109ca7f02bcf97 Mon Sep 17 00:00:00 2001 From: Scott Semtner Date: Fri, 30 Aug 2024 14:04:50 -0700 Subject: [PATCH 22/31] Add ability to append suffix to ssids --- radio/configuration_request_robot.go | 21 ++++++++++++++++++ radio/configuration_request_robot_test.go | 18 +++++++++++++++- radio/radio_robot.go | 26 ++++++++++++++++++----- radio/radio_robot_test.go | 25 ++++++++++++++++++++++ 4 files changed, 84 insertions(+), 6 deletions(-) diff --git a/radio/configuration_request_robot.go b/radio/configuration_request_robot.go index 55f1b02..b197601 100644 --- a/radio/configuration_request_robot.go +++ b/radio/configuration_request_robot.go @@ -5,6 +5,15 @@ package radio import ( "fmt" + "regexp" +) + +const ( + // Maximum length for the SSID suffix. + maxSsidSuffixLength = 8 + + // Regex to validate the SSID suffix. + ssidSuffixRegex = "^[a-zA-Z0-9]*$" ) // ConfigurationRequest represents a JSON request to configure the radio. @@ -19,6 +28,9 @@ type ConfigurationRequest struct { // Team number to configure the radio for. Must be between 1 and 25499. TeamNumber int `json:"teamNumber"` + // Suffix to be appended to all WPA SSIDs. Must be alphanumeric and at most eight characters long. + SsidSuffix string `json:"ssidSuffix"` + // Team-specific WPA key for the 6GHz network used by the FMS. Must be at least eight alphanumeric characters long. WpaKey6 string `json:"wpaKey6"` @@ -44,6 +56,15 @@ func (request ConfigurationRequest) Validate(radio *Radio) error { return fmt.Errorf("invalid team number: %d", request.TeamNumber) } + if len(request.SsidSuffix) > maxSsidSuffixLength { + return fmt.Errorf( + "invalid ssidSuffix length: %d (expecting 0-%d)", len(request.SsidSuffix), maxSsidSuffixLength, + ) + } + if !regexp.MustCompile(ssidSuffixRegex).MatchString(request.SsidSuffix) { + return fmt.Errorf("invalid ssidSuffix: %s (expecting alphanumeric)", request.SsidSuffix) + } + if len(request.WpaKey6) < minWpaKeyLength || len(request.WpaKey6) > maxWpaKeyLength { return fmt.Errorf( "invalid wpaKey6 length: %d (expecting %d-%d)", len(request.WpaKey6), minWpaKeyLength, maxWpaKeyLength, diff --git a/radio/configuration_request_robot_test.go b/radio/configuration_request_robot_test.go index f9f65a7..8f9f479 100644 --- a/radio/configuration_request_robot_test.go +++ b/radio/configuration_request_robot_test.go @@ -42,8 +42,24 @@ func TestConfigurationRequest_Validate(t *testing.T) { err = request.Validate(radio) assert.EqualError(t, err, "invalid team number: 25500") - // Too-short 6GHz WPA key. + // Valid SSID suffix. request.TeamNumber = 254 + request.SsidSuffix = "Abc123" + err = request.Validate(radio) + assert.Nil(t, err) + + // Too-long SSID suffix. + request.SsidSuffix = "123456789" + err = request.Validate(radio) + assert.EqualError(t, err, "invalid ssidSuffix length: 9 (expecting 0-8)") + + // Invalid SSID suffix. + request.SsidSuffix = "123/abc_" + err = request.Validate(radio) + assert.EqualError(t, err, "invalid ssidSuffix: 123/abc_ (expecting alphanumeric)") + + // Too-short 6GHz WPA key. + request.SsidSuffix = "" request.WpaKey6 = "1234567" err = request.Validate(radio) assert.EqualError(t, err, "invalid wpaKey6 length: 7 (expecting 8-16)") diff --git a/radio/radio_robot.go b/radio/radio_robot.go index 44926b3..7944f97 100644 --- a/radio/radio_robot.go +++ b/radio/radio_robot.go @@ -8,6 +8,7 @@ import ( "github.com/digineo/go-uci" "log" "strconv" + "strings" "time" ) @@ -29,6 +30,9 @@ const ( // Index of the radio's 6GHz Wi-Fi interface section in the UCI configuration. radioInterfaceIndex6 = 1 + + // Seperator between the team number and SSID suffix in the Wi-Fi SSIDs. + ssidSuffixSeperator = "-" ) // Radio holds the current state of the access point's configuration and any robot radios connected to it. @@ -42,6 +46,9 @@ type Radio struct { // Team number that the radio is currently configured for. TeamNumber int `json:"teamNumber"` + // Suffix currently appended to the 6GHz network SSID. + SsidSuffix string `json:"ssidSuffix,omitempty"` + // Status of the radio's 2.4GHz network. NetworkStatus24 NetworkStatus `json:"networkStatus24"` @@ -106,7 +113,9 @@ func (radio *Radio) setInitialState() { radio.NetworkStatus6.Ssid, _ = uciTree.GetLast("wireless", wifiInterface6, "ssid") radio.NetworkStatus6.HashedWpaKey, radio.NetworkStatus6.WpaKeySalt = radio.getHashedWpaKeyAndSalt(radioInterfaceIndex6) - radio.TeamNumber, _ = strconv.Atoi(radio.NetworkStatus6.Ssid) + teamNumber, suffix, _ := strings.Cut(radio.NetworkStatus6.Ssid, ssidSuffixSeperator) + radio.TeamNumber, _ = strconv.Atoi(teamNumber) + radio.SsidSuffix = suffix } // configure configures the radio with the given configuration. @@ -117,7 +126,12 @@ func (radio *Radio) configure(request ConfigurationRequest) error { radio.Mode = request.Mode // Handle Wi-Fi. - ssid := strconv.Itoa(request.TeamNumber) + var ssid string + if len(request.SsidSuffix) > 0 { + ssid = fmt.Sprintf("%d%s%s", request.TeamNumber, ssidSuffixSeperator, request.SsidSuffix) + } else { + ssid = strconv.Itoa(request.TeamNumber) + } wifiInterface6 := fmt.Sprintf("@wifi-iface[%d]", radioInterfaceIndex6) wifiInterface24 := fmt.Sprintf("@wifi-iface[%d]", radioInterfaceIndex24) uciTree.SetType("wireless", wifiInterface6, "ssid", uci.TypeOption, ssid) @@ -127,7 +141,7 @@ func (radio *Radio) configure(request ConfigurationRequest) error { if request.Mode == modeTeamRobotRadio { uciTree.SetType("wireless", wifiInterface6, "mode", uci.TypeOption, "sta") uciTree.SetType( - "wireless", wifiInterface24, "ssid", uci.TypeOption, fmt.Sprintf("FRC-%d", request.TeamNumber), + "wireless", wifiInterface24, "ssid", uci.TypeOption, fmt.Sprintf("FRC-%s", ssid), ) uciTree.SetType("wireless", wifiInterface24, "key", uci.TypeOption, request.WpaKey24) uciTree.SetType("wireless", wifiInterface24, "mode", uci.TypeOption, "ap") @@ -181,10 +195,12 @@ func (radio *Radio) configure(request ConfigurationRequest) error { if err != nil { return err } - radio.TeamNumber, _ = strconv.Atoi(radio.NetworkStatus6.Ssid) + teamNumber, suffix, _ := strings.Cut(radio.NetworkStatus6.Ssid, ssidSuffixSeperator) + radio.TeamNumber, _ = strconv.Atoi(teamNumber) + radio.SsidSuffix = suffix radio.NetworkStatus6.HashedWpaKey, radio.NetworkStatus6.WpaKeySalt = radio.getHashedWpaKeyAndSalt(radioInterfaceIndex6) - if radio.TeamNumber == request.TeamNumber { + if radio.TeamNumber == request.TeamNumber && radio.SsidSuffix == request.SsidSuffix { log.Printf("Successfully configured robot radio after %d attempts.", retryCount) break } diff --git a/radio/radio_robot_test.go b/radio/radio_robot_test.go index c217c2a..ae30979 100644 --- a/radio/radio_robot_test.go +++ b/radio/radio_robot_test.go @@ -62,6 +62,7 @@ func TestRadio_setInitialState(t *testing.T) { ) assert.Equal(t, "HomcjcEQvymkzADm", radio.NetworkStatus6.WpaKeySalt) assert.Equal(t, 12345, radio.TeamNumber) + assert.Equal(t, "", radio.SsidSuffix) // Test with team radio mode. fakeTree.valuesForGet["wireless.@wifi-iface[1].mode"] = "sta" @@ -82,6 +83,13 @@ func TestRadio_setInitialState(t *testing.T) { radio.setInitialState() assert.Equal(t, modeTeamAccessPoint, radio.Mode) assert.Equal(t, "auto", radio.Channel) + + // Test with SSID suffix. + fakeTree.valuesForGet["wireless.@wifi-iface[0].ssid"] = "FRC-12345-suffix" + fakeTree.valuesForGet["wireless.@wifi-iface[1].ssid"] = "12345-suffix" + radio.setInitialState() + assert.Equal(t, 12345, radio.TeamNumber) + assert.Equal(t, "suffix", radio.SsidSuffix) } func TestRadio_handleConfigurationRequest(t *testing.T) { @@ -190,6 +198,23 @@ func TestRadio_handleConfigurationRequest(t *testing.T) { assert.Equal(t, fakeTree.valuesFromSet["wireless.wifi1.channel"], "***DELETED***") assert.Equal(t, modeTeamRobotRadio, radio.Mode) assert.Equal(t, "", radio.Channel) + + // Configure to team radio mode with SSID suffix. + fakeShell.commandOutput["iwinfo ath1 info"] = "ath0\nESSID: \"12345-suffix\"\n" + request = ConfigurationRequest{Mode: modeTeamRobotRadio, TeamNumber: 12345, SsidSuffix: "suffix", WpaKey6: "11111111", WpaKey24: "22222222"} + assert.Nil(t, radio.handleConfigurationRequest(request)) + assert.Equal(t, modeTeamRobotRadio, radio.Mode) + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[0].ssid"], "FRC-12345-suffix") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].ssid"], "12345-suffix") + assert.Equal(t, "", radio.Channel) + + // Configure to team access point mode with SSID Suffix. + request = ConfigurationRequest{Mode: modeTeamAccessPoint, TeamNumber: 12345, SsidSuffix: "suffix", WpaKey6: "11111111"} + assert.Nil(t, radio.handleConfigurationRequest(request)) + assert.Equal(t, modeTeamAccessPoint, radio.Mode) + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[0].ssid"], "FRC-12345-suffix") + assert.Equal(t, fakeTree.valuesFromSet["wireless.@wifi-iface[1].ssid"], "12345-suffix") + assert.Equal(t, "auto", radio.Channel) } func TestRadio_handleConfigurationRequestErrors(t *testing.T) { From f835ac9e0347eb42ceb878cdeb68c230eec64341 Mon Sep 17 00:00:00 2001 From: Scott Semtner Date: Fri, 30 Aug 2024 14:05:11 -0700 Subject: [PATCH 23/31] Add ssid suffix to configuration page --- web/configuration_page.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/web/configuration_page.html b/web/configuration_page.html index a5c9224..33aeff9 100644 --- a/web/configuration_page.html +++ b/web/configuration_page.html @@ -87,6 +87,12 @@
FRC team number
+
+ + +
Optional suffix to be appended to both SSIDs, up to 8 alphanumeric characters
+
+
From fb6a0d02b3136da649853af4e33ddd6343e33820 Mon Sep 17 00:00:00 2001 From: Scott Semtner Date: Thu, 5 Sep 2024 11:30:36 -0700 Subject: [PATCH 24/31] add ConnectionQuality to NetworkStatus --- README.md | 12 ++++--- radio/network_status.go | 24 +++++++++++++ radio/network_status_test.go | 66 +++++++++++++++++++++++++----------- radio/radio_ap_test.go | 2 ++ radio/radio_robot_test.go | 1 + web/status_api_test.go | 1 + 6 files changed, 83 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 9da4a27..30e1ccf 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,8 @@ $ curl http://10.0.100.2:8081/status "txRateMbps": 0, "txPackets": 0, "txBytes": 0, - "bandwidthUsedMbps": 0 + "bandwidthUsedMbps": 0, + "connectionQuality": "" }, "blue3": null, "red1": { @@ -90,7 +91,8 @@ $ curl http://10.0.100.2:8081/status "txRateMbps": 6, "txPackets": 5246, "txBytes": 11830, - "bandwidthUsedMbps": 4.102 + "bandwidthUsedMbps": 4.102, + "connectionQuality": "excellent" }, "red2": null, "red3": null @@ -174,7 +176,8 @@ $ curl http://10.12.34.1:8081/status "txRateMbps": 0, "txPackets": 0, "txBytes": 0, - "bandwidthUsedMbps": 0 + "bandwidthUsedMbps": 0, + "connectionQuality": "" }, "networkStatus6": { "ssid": "1234", @@ -191,7 +194,8 @@ $ curl http://10.12.34.1:8081/status "txRateMbps": 516.2, "txPackets": 0, "txBytes": 52765, - "bandwidthUsedMbps": 0.002 + "bandwidthUsedMbps": 0.002, + "connectionQuality": "warning" }, "status": "ACTIVE", "version": "1.2.3" diff --git a/radio/network_status.go b/radio/network_status.go index 5192fb4..4251a7d 100644 --- a/radio/network_status.go +++ b/radio/network_status.go @@ -10,6 +10,11 @@ import ( const ( // Sentinel value used to populate status fields when a monitoring command failed. monitoringErrorCode = -999 + + // Cutoff values used to determine the connection quality of the interface based on RX rate. + connectionQualityExcellentMinimum = 412.9 + connectionQualityGoodMinimum = 309.7 + connectionQualityCautionMinimum = 172.1 ) // NetworkStatus encapsulates the status of a single Wi-Fi interface on the device (i.e. a team SSID network on the @@ -64,6 +69,9 @@ type NetworkStatus struct { // Current five-second average total (rx + tx) bandwidth in megabits per second. BandwidthUsedMbps float64 `json:"bandwidthUsedMbps"` + + // Human-readable string describing connection quality to the remote device. Based on RX rate. Blank if not associated. + ConnectionQuality string `json:"connectionQuality"` } // updateMonitoring polls the access point for the current bandwidth usage and link state of the given network interface @@ -135,6 +143,7 @@ func (status *NetworkStatus) parseAssocList(response string) { status.RxPackets = 0 status.TxRateMbps = 0 status.TxPackets = 0 + status.ConnectionQuality = "" for _, line1Match := range line1Re.FindAllStringSubmatch(response, -1) { macAddress := line1Match[1] dataAgeMs, _ := strconv.Atoi(line1Match[5]) @@ -148,6 +157,7 @@ func (status *NetworkStatus) parseAssocList(response string) { if len(line2Match) > 0 { status.RxRateMbps, _ = strconv.ParseFloat(line2Match[1], 64) status.RxPackets, _ = strconv.Atoi(line2Match[2]) + status.determineConnectionQuality() } line3Match := line3R3.FindStringSubmatch(response) if len(line3Match) > 0 { @@ -172,3 +182,17 @@ func (status *NetworkStatus) parseIfconfig(response string) { status.TxBytes, _ = strconv.Atoi(bytesMatch[2]) } } + +// determineConnectionQuality uses the stored RxRateMbps value to determine a connection quality string and updates the +// status structure with the result. +func (status *NetworkStatus) determineConnectionQuality() { + if status.RxRateMbps >= connectionQualityExcellentMinimum { + status.ConnectionQuality = "excellent" + } else if status.RxRateMbps >= connectionQualityGoodMinimum { + status.ConnectionQuality = "good" + } else if status.RxRateMbps >= connectionQualityCautionMinimum { + status.ConnectionQuality = "caution" + } else { + status.ConnectionQuality = "warning" + } +} diff --git a/radio/network_status_test.go b/radio/network_status_test.go index 581ea71..7708ae2 100644 --- a/radio/network_status_test.go +++ b/radio/network_status_test.go @@ -5,7 +5,7 @@ import ( "testing" ) -func TestNetworkStatus_ParseBandwithUsed(t *testing.T) { +func TestNetworkStatus_ParseBandwidthUsed(t *testing.T) { var status NetworkStatus // Response is too short. @@ -55,15 +55,16 @@ func TestNetworkStatus_ParseAssocList(t *testing.T) { assert.Equal( t, NetworkStatus{ - IsLinked: true, - MacAddress: "48:DA:35:B0:00:CF", - SignalDbm: -53, - NoiseDbm: -95, - SignalNoiseRatio: 42, - RxRateMbps: 550.6, - RxPackets: 4095, - TxRateMbps: 254.0, - TxPackets: 123, + IsLinked: true, + MacAddress: "48:DA:35:B0:00:CF", + SignalDbm: -53, + NoiseDbm: -95, + SignalNoiseRatio: 42, + RxRateMbps: 550.6, + RxPackets: 4095, + TxRateMbps: 254.0, + TxPackets: 123, + ConnectionQuality: "excellent", }, status, ) @@ -75,15 +76,16 @@ func TestNetworkStatus_ParseAssocList(t *testing.T) { assert.Equal( t, NetworkStatus{ - IsLinked: true, - MacAddress: "37:DA:35:B0:00:BE", - SignalDbm: -64, - NoiseDbm: -84, - SignalNoiseRatio: 7, - RxRateMbps: 123.4, - RxPackets: 5091, - TxRateMbps: 550.6, - TxPackets: 789, + IsLinked: true, + MacAddress: "37:DA:35:B0:00:BE", + SignalDbm: -64, + NoiseDbm: -84, + SignalNoiseRatio: 7, + RxRateMbps: 123.4, + RxPackets: 5091, + TxRateMbps: 550.6, + TxPackets: 789, + ConnectionQuality: "warning", }, status, ) @@ -113,3 +115,29 @@ func TestNetworkStatus_ParseIfconfig(t *testing.T) { status.parseIfconfig(response) assert.Equal(t, NetworkStatus{RxBytes: 45311, TxBytes: 48699}, status) } + +func TestNetworkStatus_DetermineConnectionQuality(t *testing.T) { + var status NetworkStatus + + assert.Equal(t, NetworkStatus{}, status) + + status.RxRateMbps = connectionQualityExcellentMinimum + status.determineConnectionQuality() + assert.Equal(t, "excellent", status.ConnectionQuality) + + status.RxRateMbps = connectionQualityGoodMinimum + status.determineConnectionQuality() + assert.Equal(t, "good", status.ConnectionQuality) + + status.RxRateMbps = connectionQualityCautionMinimum + status.determineConnectionQuality() + assert.Equal(t, "caution", status.ConnectionQuality) + + status.RxRateMbps = 0.1 + status.determineConnectionQuality() + assert.Equal(t, "warning", status.ConnectionQuality) + + // Ensure ConnectionQuality resets to blank. + status.parseAssocList("") + assert.Equal(t, NetworkStatus{}, status) +} diff --git a/radio/radio_ap_test.go b/radio/radio_ap_test.go index 3f52d34..81e525a 100644 --- a/radio/radio_ap_test.go +++ b/radio/radio_ap_test.go @@ -519,6 +519,7 @@ func TestRadio_updateMonitoring(t *testing.T) { assert.Equal(t, -999.0, radio.StationStatuses["red1"].BandwidthUsedMbps) assert.Equal(t, 12345, radio.StationStatuses["red1"].RxBytes) assert.Equal(t, 98765, radio.StationStatuses["red1"].TxBytes) + assert.Equal(t, "excellent", radio.StationStatuses["red1"].ConnectionQuality) assert.Equal( t, NetworkStatus{ @@ -536,6 +537,7 @@ func TestRadio_updateMonitoring(t *testing.T) { TxBytes: -999, SignalNoiseRatio: -999, BandwidthUsedMbps: 0, + ConnectionQuality: "", }, *radio.StationStatuses["blue2"], ) diff --git a/radio/radio_robot_test.go b/radio/radio_robot_test.go index ae30979..2922af4 100644 --- a/radio/radio_robot_test.go +++ b/radio/radio_robot_test.go @@ -297,6 +297,7 @@ func TestRadio_updateMonitoring(t *testing.T) { }, radio.NetworkStatus6, ) + assert.Equal(t, "excellent", radio.NetworkStatus24.ConnectionQuality) assert.Equal(t, 6, len(fakeShell.commandsRun)) assert.Contains(t, fakeShell.commandsRun, "luci-bwc -i ath0") assert.Contains(t, fakeShell.commandsRun, "iwinfo ath0 assoclist") diff --git a/web/status_api_test.go b/web/status_api_test.go index 2151dc1..26e8d16 100644 --- a/web/status_api_test.go +++ b/web/status_api_test.go @@ -25,6 +25,7 @@ func TestWeb_statusHandler(t *testing.T) { TxRateMbps: 2.0, SignalNoiseRatio: 3, BandwidthUsedMbps: 4.0, + ConnectionQuality: "warning", } recorder := web.getHttpResponse("/status") From 4acdc407efb9711d4858d9d13d54d3efb1f7f0a8 Mon Sep 17 00:00:00 2001 From: Scott Semtner Date: Thu, 5 Sep 2024 11:31:11 -0700 Subject: [PATCH 25/31] connectivity stats for team ap on configuration page --- web/configuration_page.html | 70 +++++++++++++++++++++++++++++-------- 1 file changed, 56 insertions(+), 14 deletions(-) diff --git a/web/configuration_page.html b/web/configuration_page.html index 33aeff9..a7aa83f 100644 --- a/web/configuration_page.html +++ b/web/configuration_page.html @@ -156,6 +156,28 @@
+ +


@@ -285,23 +307,39 @@

Firmware Upload

} } - async function getCurrentConfig() { + async function getStatus(setConfig = false) { try { - const res = await fetch("/status"); + const res = await fetch("http://192.168.69.1/status"); const json = await res.json(); - const robotMode = document.getElementById("modeRobotRadio"); - const apMode = document.getElementById("modeAccessPoint"); - if (json.mode === "TEAM_ROBOT_RADIO") { - robotMode.setAttribute("checked", "checked"); - apMode.removeAttribute("checked"); - } else if (json.mode === "TEAM_ACCESS_POINT") { - robotMode.removeAttribute("checked"); - apMode.setAttribute("checked", "checked"); + if (json.mode === "TEAM_ACCESS_POINT") { + document.getElementById("stats").removeAttribute("hidden"); + for (const key of [ + "isLinked", + "macAddress", + "rxRateMbps", + "bandwidthUsedMbps", + "connectionQuality", + ]) { + document.getElementById(key).textContent = json.networkStatus6[key]; + } } - document.getElementById("teamNumber").value = json.teamNumber; - document.getElementById("channel").value = json.channel; + if (setConfig) { + const robotMode = document.getElementById("modeRobotRadio"); + const apMode = document.getElementById("modeAccessPoint"); + if (json.mode === "TEAM_ROBOT_RADIO") { + robotMode.setAttribute("checked", "checked"); + apMode.removeAttribute("checked"); + document.getElementById("stats").setAttribute("hidden", ""); + } else if (json.mode === "TEAM_ACCESS_POINT") { + robotMode.removeAttribute("checked"); + apMode.setAttribute("checked", "checked"); + } + + document.getElementById("teamNumber").value = json.teamNumber; + document.getElementById("channel").value = json.channel; + } } catch (e) { console.error(e); } @@ -325,10 +363,14 @@

Firmware Upload

}) setLoading("config", true); - updateVisibleFields(new FormData(form.config).get("mode")) - getCurrentConfig() + updateVisibleFields(new FormData(form.config).get("mode")); + getStatus(true) .then(() => updateVisibleFields(new FormData(form.config).get("mode"))) .then(() => setLoading("config", false)); + + setInterval(() => { + getStatus(); + }, 5000); From 96b751b591fc364ff77f34dffe8e2c0569ea1878 Mon Sep 17 00:00:00 2001 From: Scott Semtner Date: Thu, 5 Sep 2024 13:49:09 -0700 Subject: [PATCH 26/31] base ConnectionQuality on tx rate for team robot mode --- radio/network_status.go | 18 +++++++++++++----- radio/network_status_test.go | 16 ++++++++-------- radio/radio_robot.go | 10 ++++++++++ 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/radio/network_status.go b/radio/network_status.go index 4251a7d..4de49c6 100644 --- a/radio/network_status.go +++ b/radio/network_status.go @@ -72,6 +72,9 @@ type NetworkStatus struct { // Human-readable string describing connection quality to the remote device. Based on RX rate. Blank if not associated. ConnectionQuality string `json:"connectionQuality"` + + // Flag representing whether the interface is for a robot. + IsRobot bool `json:"-"` } // updateMonitoring polls the access point for the current bandwidth usage and link state of the given network interface @@ -157,12 +160,17 @@ func (status *NetworkStatus) parseAssocList(response string) { if len(line2Match) > 0 { status.RxRateMbps, _ = strconv.ParseFloat(line2Match[1], 64) status.RxPackets, _ = strconv.Atoi(line2Match[2]) - status.determineConnectionQuality() + if !status.IsRobot { + status.determineConnectionQuality(status.RxRateMbps) + } } line3Match := line3R3.FindStringSubmatch(response) if len(line3Match) > 0 { status.TxRateMbps, _ = strconv.ParseFloat(line3Match[1], 64) status.TxPackets, _ = strconv.Atoi(line3Match[2]) + if status.IsRobot { + status.determineConnectionQuality(status.TxRateMbps) + } } break } @@ -185,12 +193,12 @@ func (status *NetworkStatus) parseIfconfig(response string) { // determineConnectionQuality uses the stored RxRateMbps value to determine a connection quality string and updates the // status structure with the result. -func (status *NetworkStatus) determineConnectionQuality() { - if status.RxRateMbps >= connectionQualityExcellentMinimum { +func (status *NetworkStatus) determineConnectionQuality(rate float64) { + if rate >= connectionQualityExcellentMinimum { status.ConnectionQuality = "excellent" - } else if status.RxRateMbps >= connectionQualityGoodMinimum { + } else if rate >= connectionQualityGoodMinimum { status.ConnectionQuality = "good" - } else if status.RxRateMbps >= connectionQualityCautionMinimum { + } else if rate >= connectionQualityCautionMinimum { status.ConnectionQuality = "caution" } else { status.ConnectionQuality = "warning" diff --git a/radio/network_status_test.go b/radio/network_status_test.go index 7708ae2..c724b8d 100644 --- a/radio/network_status_test.go +++ b/radio/network_status_test.go @@ -68,6 +68,10 @@ func TestNetworkStatus_ParseAssocList(t *testing.T) { }, status, ) + status.IsRobot = true + status.parseAssocList(response) + assert.Equal(t, "caution", status.ConnectionQuality) + status.IsRobot = false response = "37:DA:35:B0:00:BE -64 dBm / -84 dBm (SNR 7) 4000 ms ago\n" + "\tRX: 123.4 MBit/s 5091 Pkts.\n" + "\tTX: 550.6 MBit/s 789 Pkts.\n" + @@ -121,20 +125,16 @@ func TestNetworkStatus_DetermineConnectionQuality(t *testing.T) { assert.Equal(t, NetworkStatus{}, status) - status.RxRateMbps = connectionQualityExcellentMinimum - status.determineConnectionQuality() + status.determineConnectionQuality(connectionQualityExcellentMinimum) assert.Equal(t, "excellent", status.ConnectionQuality) - status.RxRateMbps = connectionQualityGoodMinimum - status.determineConnectionQuality() + status.determineConnectionQuality(connectionQualityGoodMinimum) assert.Equal(t, "good", status.ConnectionQuality) - status.RxRateMbps = connectionQualityCautionMinimum - status.determineConnectionQuality() + status.determineConnectionQuality(connectionQualityCautionMinimum) assert.Equal(t, "caution", status.ConnectionQuality) - status.RxRateMbps = 0.1 - status.determineConnectionQuality() + status.determineConnectionQuality(0.1) assert.Equal(t, "warning", status.ConnectionQuality) // Ensure ConnectionQuality resets to blank. diff --git a/radio/radio_robot.go b/radio/radio_robot.go index 7944f97..5b8f610 100644 --- a/radio/radio_robot.go +++ b/radio/radio_robot.go @@ -102,6 +102,8 @@ func (radio *Radio) setInitialState() { if mode == "sta" { radio.Mode = modeTeamRobotRadio radio.Channel = "" + radio.NetworkStatus24.IsRobot = true + radio.NetworkStatus6.IsRobot = true } else { radio.Mode = modeTeamAccessPoint radio.Channel, _ = uciTree.GetLast("wireless", radioDevice6, "channel") @@ -156,6 +158,10 @@ func (radio *Radio) configure(request ConfigurationRequest) error { uciTree.SetType("network", "lan", "gateway", uci.TypeOption, fmt.Sprintf("10.%s.4", teamPartialIp)) uciTree.SetType("dhcp", "lan", "start", uci.TypeOption, "200") uciTree.SetType("dhcp", "lan", "limit", uci.TypeOption, "20") + + // Handle NetworkStatus as robot. + radio.NetworkStatus24.IsRobot = true + radio.NetworkStatus6.IsRobot = true } else { uciTree.SetType("wireless", wifiInterface6, "mode", uci.TypeOption, "ap") @@ -173,6 +179,10 @@ func (radio *Radio) configure(request ConfigurationRequest) error { uciTree.SetType("network", "lan", "gateway", uci.TypeOption, fmt.Sprintf("10.%s.4", teamPartialIp)) uciTree.SetType("dhcp", "lan", "start", uci.TypeOption, "20") uciTree.SetType("dhcp", "lan", "limit", uci.TypeOption, "180") + + // Handle NetworkStatus as AP + radio.NetworkStatus24.IsRobot = false + radio.NetworkStatus24.IsRobot = false } // Handle DHCP. From 6c3f177dfbb8e905df2b93e2f6ce054caec8fe08 Mon Sep 17 00:00:00 2001 From: Scott Semtner Date: Fri, 6 Sep 2024 16:36:46 -0700 Subject: [PATCH 27/31] add connection bars to configuration page --- web/configuration_page.html | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/web/configuration_page.html b/web/configuration_page.html index a7aa83f..9f5d9c7 100644 --- a/web/configuration_page.html +++ b/web/configuration_page.html @@ -14,7 +14,7 @@ @@ -307,9 +306,14 @@

Firmware Upload

} } + function setBar(bars, idx, color) { + const bar = bars.childNodes[idx]; + bar.setAttribute("stroke", color); + } + async function getStatus(setConfig = false) { try { - const res = await fetch("http://192.168.69.1/status"); + const res = await fetch("/status"); const json = await res.json(); if (json.mode === "TEAM_ACCESS_POINT") { @@ -325,6 +329,25 @@

Firmware Upload

} } + const bars = document.getElementById("bars"); + bars.removeAttribute("hidden"); + bars.childNodes.forEach(child => child.setAttribute("stroke", "#d1d5db")); + if (json.networkStatus6.connectionQuality === "excellent") { + setBar(bars, 0, "#22c55e"); + setBar(bars, 1, "#22c55e"); + setBar(bars, 2, "#22c55e"); + setBar(bars, 3, "#22c55e"); + } else if (json.networkStatus6.connectionQuality === "good") { + setBar(bars, 0, "#22c55e"); + setBar(bars, 1, "#22c55e"); + setBar(bars, 2, "#22c55e"); + } else if (json.networkStatus6.connectionQuality === "caution") { + setBar(bars, 0, "#f59e0b"); + setBar(bars, 1, "#f59e0b"); + } else if (json.networkStatus6.connectionQuality === "warning") { + setBar(bars, 0, "#dc2626"); + } + if (setConfig) { const robotMode = document.getElementById("modeRobotRadio"); const apMode = document.getElementById("modeAccessPoint"); From 98e80a7eee82519e9f3815adc45bc5e0c780970f Mon Sep 17 00:00:00 2001 From: Kiet Chau Date: Tue, 10 Sep 2024 23:34:35 -0700 Subject: [PATCH 28/31] Suffix fixes --- radio/configuration_request_robot.go | 3 ++- web/configuration_page.html | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/radio/configuration_request_robot.go b/radio/configuration_request_robot.go index b197601..fd0fb94 100644 --- a/radio/configuration_request_robot.go +++ b/radio/configuration_request_robot.go @@ -4,6 +4,7 @@ package radio import ( + "errors" "fmt" "regexp" ) @@ -62,7 +63,7 @@ func (request ConfigurationRequest) Validate(radio *Radio) error { ) } if !regexp.MustCompile(ssidSuffixRegex).MatchString(request.SsidSuffix) { - return fmt.Errorf("invalid ssidSuffix: %s (expecting alphanumeric)", request.SsidSuffix) + return errors.New("invalid ssidSuffix (expecting alphanumeric)") } if len(request.WpaKey6) < minWpaKeyLength || len(request.WpaKey6) > maxWpaKeyLength { diff --git a/web/configuration_page.html b/web/configuration_page.html index 9f5d9c7..0a3b4a7 100644 --- a/web/configuration_page.html +++ b/web/configuration_page.html @@ -361,6 +361,7 @@

Firmware Upload

} document.getElementById("teamNumber").value = json.teamNumber; + document.getElementById("ssidSuffix").value = json.ssidSuffix; document.getElementById("channel").value = json.channel; } } catch (e) { From f400c6cf7e12141e6c9953a5347042936141b0da Mon Sep 17 00:00:00 2001 From: Kiet Chau Date: Tue, 10 Sep 2024 23:48:55 -0700 Subject: [PATCH 29/31] Fixed test for ssidSuffix --- radio/configuration_request_robot_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/radio/configuration_request_robot_test.go b/radio/configuration_request_robot_test.go index 8f9f479..31b19e0 100644 --- a/radio/configuration_request_robot_test.go +++ b/radio/configuration_request_robot_test.go @@ -56,7 +56,7 @@ func TestConfigurationRequest_Validate(t *testing.T) { // Invalid SSID suffix. request.SsidSuffix = "123/abc_" err = request.Validate(radio) - assert.EqualError(t, err, "invalid ssidSuffix: 123/abc_ (expecting alphanumeric)") + assert.EqualError(t, err, "invalid ssidSuffix (expecting alphanumeric)") // Too-short 6GHz WPA key. request.SsidSuffix = "" From 2a31356db8801ae8d7ba6f37cc22e954d15c37e0 Mon Sep 17 00:00:00 2001 From: Scott Semtner Date: Wed, 11 Sep 2024 13:24:09 -0700 Subject: [PATCH 30/31] don't omit empty ssid suffix --- radio/radio_robot.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/radio/radio_robot.go b/radio/radio_robot.go index 5b8f610..66d49ff 100644 --- a/radio/radio_robot.go +++ b/radio/radio_robot.go @@ -47,7 +47,7 @@ type Radio struct { TeamNumber int `json:"teamNumber"` // Suffix currently appended to the 6GHz network SSID. - SsidSuffix string `json:"ssidSuffix,omitempty"` + SsidSuffix string `json:"ssidSuffix"` // Status of the radio's 2.4GHz network. NetworkStatus24 NetworkStatus `json:"networkStatus24"` From 33d3cdc62661d4c15d445784cbcf3649ceae9eba Mon Sep 17 00:00:00 2001 From: Scott Semtner Date: Fri, 13 Sep 2024 14:48:24 -0700 Subject: [PATCH 31/31] limit ap ssid length --- radio/configuration_request_ap.go | 13 ++++++++++++- radio/configuration_request_ap_test.go | 9 ++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/radio/configuration_request_ap.go b/radio/configuration_request_ap.go index ceceb8c..ea33331 100644 --- a/radio/configuration_request_ap.go +++ b/radio/configuration_request_ap.go @@ -9,7 +9,10 @@ import ( "regexp" ) -const stationSsidRegex = "^[a-zA-Z0-9-]*$" +const ( + maxStationSsidLength = 14 + stationSsidRegex = "^[a-zA-Z0-9-]*$" +) // ConfigurationRequest represents a JSON request to configure the radio. type ConfigurationRequest struct { @@ -112,6 +115,14 @@ func (request ConfigurationRequest) Validate(radio *Radio) error { if stationConfiguration.Ssid == "" { return fmt.Errorf("SSID for station %s cannot be blank", stationName) } + if len(stationConfiguration.Ssid) > maxStationSsidLength { + return fmt.Errorf( + "invalid SSID length for station %s: %d (expecting 1-%d)", + stationName, + len(stationConfiguration.Ssid), + maxStationSsidLength, + ) + } if !regexp.MustCompile(stationSsidRegex).MatchString(stationConfiguration.Ssid) { return fmt.Errorf("invalid SSID for station %s (expecting alphanumeric with hyphens)", stationName) } diff --git a/radio/configuration_request_ap_test.go b/radio/configuration_request_ap_test.go index 26cca5d..a650036 100644 --- a/radio/configuration_request_ap_test.go +++ b/radio/configuration_request_ap_test.go @@ -68,6 +68,13 @@ func TestConfigurationRequest_Validate(t *testing.T) { err = request.Validate(linksysRadio) assert.EqualError(t, err, "SSID for station blue1 cannot be blank") + // Too-long SSID. + request = ConfigurationRequest{ + StationConfigurations: map[string]StationConfiguration{"blue1": {Ssid: "12345-longsuffix", WpaKey: "12345678"}}, + } + err = request.Validate(linksysRadio) + assert.EqualError(t, err, "invalid SSID length for station blue1: 16 (expecting 1-14)") + // Invalid characters in SSID. request = ConfigurationRequest{ StationConfigurations: map[string]StationConfiguration{"blue1": {Ssid: "abc_XYZ", WpaKey: "12345678"}}, @@ -77,7 +84,7 @@ func TestConfigurationRequest_Validate(t *testing.T) { // Too-short WPA key. request = ConfigurationRequest{ - StationConfigurations: map[string]StationConfiguration{"blue1": {Ssid: "254-suffix", WpaKey: "1234567"}}, + StationConfigurations: map[string]StationConfiguration{"blue1": {Ssid: "12345-suffix", WpaKey: "1234567"}}, } err = request.Validate(linksysRadio) assert.EqualError(t, err, "invalid WPA key length for station blue1: 7 (expecting 8-16)")