Skip to content

Commit

Permalink
Add network monitoring to robot radio and include results in status API.
Browse files Browse the repository at this point in the history
  • Loading branch information
patfair committed Jun 15, 2024
1 parent 5ea4904 commit 8bdb689
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 79 deletions.
43 changes: 40 additions & 3 deletions radio/network_status.go
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down
3 changes: 0 additions & 3 deletions radio/network_status_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
// This file is specific to the access point version of the API.
//go:build !robot

package radio

import (
Expand Down
35 changes: 1 addition & 34 deletions radio/radio_ap.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
}
}
2 changes: 1 addition & 1 deletion radio/radio_ap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"] = ""
Expand Down
61 changes: 32 additions & 29 deletions radio/radio_robot.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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.
Expand All @@ -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"`
Expand Down Expand Up @@ -95,18 +89,24 @@ 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 = ""
} else {
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.
Expand Down Expand Up @@ -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
Expand All @@ -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)
}
76 changes: 67 additions & 9 deletions radio/radio_robot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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")
}

0 comments on commit 8bdb689

Please sign in to comment.