Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions board/common/rootfs/usr/libexec/infix/iw.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@
import json
import subprocess
import re
def decode_iw_ssid(ssid):
"""Decode iw escaped SSID (\\xHH) to UTF-8, stripping non-printable chars."""
try:
ssid = ssid.encode().decode('unicode_escape').encode('latin-1').decode('utf-8')
except (UnicodeDecodeError, UnicodeEncodeError):
return ssid
return ''.join(c for c in ssid if c.isprintable())


def run_iw(*args):
Expand Down Expand Up @@ -260,7 +267,7 @@ def parse_interface_info(ifname):

# SSID
elif stripped.startswith('ssid '):
result['ssid'] = ' '.join(stripped.split()[1:])
result['ssid'] = decode_iw_ssid(' '.join(stripped.split()[1:]))

# Channel/frequency
elif stripped.startswith('channel '):
Expand Down Expand Up @@ -322,7 +329,7 @@ def parse_stations(ifname):
try:
if key == 'signal':
# Format: "-42 dBm" or "-42 [-44, -45] dBm"
current['rssi'] = int(value.split()[0])
current['signal-strength'] = int(value.split()[0])
elif key == 'connected time':
# Format: "123 seconds"
current['connected-time'] = int(value.split()[0])
Expand Down Expand Up @@ -488,7 +495,7 @@ def parse_link(ifname):

# SSID: NetworkName
elif stripped.startswith('SSID: '):
result['ssid'] = stripped[6:]
result['ssid'] = decode_iw_ssid(stripped[6:])

# freq: 5180
elif stripped.startswith('freq: '):
Expand All @@ -500,7 +507,7 @@ def parse_link(ifname):
# signal: -42 dBm
elif stripped.startswith('signal: '):
try:
result['rssi'] = int(stripped.split()[1])
result['signal-strength'] = int(stripped.split()[1])
except (ValueError, IndexError):
pass

Expand Down Expand Up @@ -582,7 +589,7 @@ def main():
else:
data = {'error': f'Unknown command: {command}'}

print(json.dumps(data, indent=2))
print(json.dumps(data, indent=2, ensure_ascii=False))

except Exception as e:
print(json.dumps({'error': str(e)}))
Expand Down
4 changes: 2 additions & 2 deletions doc/wifi.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,8 @@ CoffeeShop 00:1a:2b:3c:4d:5e Open bad 1
</code></pre>

In the CLI, signal strength is reported as: excellent, good, fair or bad.
For precise RSSI values in dBm, use NETCONF or RESTCONF to access the
operational datastore directly.
For precise signal strength values in dBm, use NETCONF or RESTCONF to access
the `signal-strength` leaf in the operational datastore.

## Station Mode (Client)

Expand Down
21 changes: 9 additions & 12 deletions src/confd/src/if-wifi.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,15 @@

#define WPA_SUPPLICANT_CONF "/etc/wpa_supplicant-%s.conf"

/*
* Determine WiFi mode from YANG configuration
*/
typedef enum wifi_mode_t {
wifi_station,
wifi_ap,
wifi_unknown
} wifi_mode_t;

static wifi_mode_t wifi_get_mode(struct lyd_node *wifi)
wifi_mode_t wifi_get_mode(struct lyd_node *iface)
{
struct lyd_node *ap;
struct lyd_node *ap, *wifi;

wifi = lydx_get_child(iface, "wifi");
if (!wifi)
return wifi_unknown;


ap = lydx_get_child(wifi, "access-point");
if (ap) {
Expand Down Expand Up @@ -60,7 +57,7 @@ int wifi_mode_changed(struct lyd_node *wifi)
/*
* Generate wpa_supplicant config for station mode
*/
static int wifi_gen_station(struct lyd_node *cif)
int wifi_gen_station(struct lyd_node *cif)
{
const char *ifname, *ssid, *secret_name, *secret, *security_mode, *radio;
struct lyd_node *security, *secret_node, *radio_node, *station, *wifi;
Expand Down Expand Up @@ -213,7 +210,7 @@ int wifi_add_iface(struct lyd_node *cif, struct dagger *net)
return SR_ERR_INTERNAL;
}

mode = wifi_get_mode(wifi);
mode = wifi_get_mode(cif);
probe_timeout = wifi_get_probe_timeout(net->session, radio);

fprintf(iw, "# Generated by Infix confd - WiFi Interface Creation\n");
Expand Down
5 changes: 2 additions & 3 deletions src/confd/src/interfaces.c
Original file line number Diff line number Diff line change
Expand Up @@ -452,9 +452,8 @@ static int netdag_gen_afspec_set(sr_session_ctx_t *session, struct dagger *net,
case IFT_ETH:
return netdag_gen_ethtool(net, cif, dif);
case IFT_WIFI:
/* WiFi daemon config (hostapd/wpa_supplicant) is handled by
* hardware.c when the radio (phy) is configured. Interface
* creation/deletion is handled in netdag_gen_afspec_add(). */
if (wifi_get_mode(cif) == wifi_station)
return wifi_gen_station(cif);
return 0;
case IFT_DUMMY:
case IFT_GRE:
Expand Down
8 changes: 8 additions & 0 deletions src/confd/src/interfaces.h
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,17 @@ int bridge_mcd_gen(struct lyd_node *cifs);
int bridge_port_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip);

/* if-wifi.c */
typedef enum wifi_mode_t {
wifi_station,
wifi_ap,
wifi_unknown
} wifi_mode_t;

int wifi_add_iface(struct lyd_node *cif, struct dagger *net);
int wifi_del_iface(struct lyd_node *dif, struct dagger *net);
int wifi_mode_changed(struct lyd_node *wifi);
int wifi_gen_station(struct lyd_node *cif);
wifi_mode_t wifi_get_mode(struct lyd_node *wifi);

/* if-gre.c */
int gre_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip);
Expand Down
17 changes: 13 additions & 4 deletions src/confd/yang/confd/infix-if-wifi.yang
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ submodule infix-if-wifi {
must "count(/if:interfaces/if:interface[wifi/radio = current()][not(wifi/access-point)]) <= 1" {
error-message "Only one station or scan interface is allowed per radio";
}
must "count(/if:interfaces/if:interface[wifi/radio = current()][not(infix-if:custom-phys-address/*)]) <= 1" {
error-message "Only one interface per radio can use the default MAC address. Configure custom-phys-address on additional interfaces.";
}

description
"Reference to parent WiFi radio (PHY).
Expand Down Expand Up @@ -148,6 +151,9 @@ submodule infix-if-wifi {
leaf ssid {
type string {
length "1..32";
pattern '[^\x00-\x1f\x22\x5c\x7f]*' {
error-message "SSID must not contain control characters, double quotes, or backslashes.";
}
}
mandatory true;
description
Expand Down Expand Up @@ -201,12 +207,12 @@ submodule infix-if-wifi {

/* Operational state */

leaf rssi {
leaf signal-strength {
config false;
type int16;
units "dBm";
description
"Current received signal strength indication (RSSI) in dBm.
"Current signal strength in dBm.

More negative values indicate weaker signal.

Expand Down Expand Up @@ -266,7 +272,7 @@ submodule infix-if-wifi {
description "BSSID (MAC address) of the AP.";
}

leaf rssi {
leaf signal-strength {
type int16;
units "dBm";
description "Signal strength of the network.";
Expand Down Expand Up @@ -319,6 +325,9 @@ submodule infix-if-wifi {
leaf ssid {
type string {
length "1..32";
pattern '[^\x00-\x1f\x22\x5c\x7f]*' {
error-message "SSID must not contain control characters, double quotes, or backslashes.";
}
}
mandatory true;
description
Expand Down Expand Up @@ -415,7 +424,7 @@ submodule infix-if-wifi {
description "Client MAC address.";
}

leaf rssi {
leaf signal-strength {
type int16;
units "dBm";
description "Client signal strength in dBm.";
Expand Down
36 changes: 18 additions & 18 deletions src/statd/python/cli_pretty/cli_pretty.py
Original file line number Diff line number Diff line change
Expand Up @@ -565,12 +565,12 @@ def title(txt, width=None, bold=True):
print(txt)


def rssi_to_status(rssi):
if rssi <= -75:
def signal_to_status(signal):
if signal >= -50:
status = Decore.bright_green("excellent")
elif rssi <= -65:
elif signal >= -60:
status = Decore.green("good")
elif rssi <= -50:
elif signal >= -70:
status = Decore.yellow("poor")
else:
status = Decore.red("bad")
Expand Down Expand Up @@ -1249,7 +1249,7 @@ def pr_wifi_ssids(self):
ssid = result.get('ssid', 'Hidden')
bssid = result.get("bssid", "unknown")
encstr = ", ".join(result.get("encryption", ["Unknown"]))
status = rssi_to_status(result.get("rssi", -100))
status = signal_to_status(result.get("signal-strength", -100))
channel = result.get("channel", "?")

ssid_table.row(ssid, bssid, encstr, status, channel)
Expand Down Expand Up @@ -1283,8 +1283,8 @@ def pr_wifi_stations(self):

for station in stations:
mac = station.get("mac-address", "unknown")
rssi = station.get("rssi")
signal_str = rssi_to_status(rssi) if rssi is not None else "------"
signal = station.get("signal-strength")
signal_str = signal_to_status(signal) if signal is not None else "------"

conn_time = station.get("connected-time", 0)
time_str = f"{conn_time}s"
Expand All @@ -1310,11 +1310,11 @@ def pr_proto_wifi(self, pipe=''):
row = self._pr_proto_common("ethernet", True, pipe);
print(row)
ssid = None
rssi = None
signal = None
mode = None

if self.wifi:
# Detect mode: AP has "stations", Station has "rssi" or "scan-results"
# Detect mode: AP has "stations", Station has "signal-strength" or "scan-results"
ap=self.wifi.get("access-point", {})
if ap:
ssid = ap.get("ssid", "------")
Expand All @@ -1326,11 +1326,11 @@ def pr_proto_wifi(self, pipe=''):
else:
station=self.wifi.get("station", {})
ssid = station.get("ssid", "------")
rssi = station.get("rssi")
signal = station.get("signal-strength")
mode = "Station"
if rssi is not None:
signal = rssi_to_status(rssi)
data_str = f"{mode}, ssid: {ssid}, signal: {signal}"
if signal is not None:
signal_str = signal_to_status(signal)
data_str = f"{mode}, ssid: {ssid}, signal: {signal_str}"
else:
data_str = f"{mode}, ssid: {ssid}"
else:
Expand Down Expand Up @@ -1618,7 +1618,7 @@ def pr_iface(self):
print(f"{'out-octets':<{20}}: {self.out_octets}")

if self.wifi:
# Detect mode: AP has "stations", Station has "rssi" or "scan-results"
# Detect mode: AP has "stations", Station has "signal-strength" or "scan-results"
ap = self.wifi.get('access-point')
if ap:
mode = "access-point"
Expand All @@ -1632,13 +1632,13 @@ def pr_iface(self):
else:
mode = "station"
station = self.wifi.get('station', {})
rssi = station.get('rssi')
signal = station.get('signal-strength')
ssid = station.get('ssid', "----")
print(f"{'mode':<{20}}: {mode}")
print(f"{'ssid':<{20}}: {ssid}")
if rssi is not None:
signal_status = rssi_to_status(rssi)
print(f"{'signal':<{20}}: {rssi} dBm ({signal_status})")
if signal is not None:
signal_status = signal_to_status(signal)
print(f"{'signal':<{20}}: {signal} dBm ({signal_status})")
rx_speed = station.get('rx-speed')
tx_speed = station.get('tx-speed')
if rx_speed is not None:
Expand Down
2 changes: 1 addition & 1 deletion src/statd/python/yanger/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def dirpath(path):
common.LOG.warning("Unsupported model %s", args.model)
sys.exit(1)

print(json.dumps(yang_data, indent=2))
print(json.dumps(yang_data, indent=2, ensure_ascii=False))


if __name__ == "__main__":
Expand Down
24 changes: 15 additions & 9 deletions src/statd/python/yanger/ietf_interfaces/wifi.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,14 @@ def wifi_station(ifname):
"""Get operational data for Station mode using iw + wpa_cli for scanning"""
station_data = {}

# Get link info (includes SSID and RSSI when connected)
# Get link info (includes SSID and signal strength when connected)
link = get_iw_link(ifname)

if link.get('connected'):
if link.get('ssid'):
station_data['ssid'] = link['ssid']
if link.get('rssi') is not None:
station_data['rssi'] = link['rssi']
if link.get('signal-strength') is not None:
station_data['signal-strength'] = link['signal-strength']
if link.get('rx-speed') is not None:
station_data['rx-speed'] = link['rx-speed']
if link.get('tx-speed') is not None:
Expand Down Expand Up @@ -122,29 +122,35 @@ def parse_wpa_scan_result(scan_output):

flags = parts[3].strip()
ssid = parts[4].strip() if len(parts) > 4 else ""
try:
ssid = ssid.encode().decode('unicode_escape').encode('latin-1').decode('utf-8')
except (UnicodeDecodeError, UnicodeEncodeError):
pass
# Strip control chars (terminal injection risk from rogue APs)
ssid = ''.join(c for c in ssid if c.isprintable())

# Skip hidden SSIDs (empty or null-filled)
if not ssid or ssid.isspace() or '\\x00' in ssid:
if not ssid or ssid.isspace():
continue

encryption = extract_encryption(flags)
channel = frequency_to_channel(frequency)

# Keep best RSSI per SSID
if ssid not in networks or rssi > networks[ssid]['rssi']:
# Keep best signal per SSID
if ssid not in networks or rssi > networks[ssid]['signal-strength']:
networks[ssid] = {
'bssid': bssid,
'ssid': ssid,
'rssi': rssi,
'signal-strength': rssi,
'encryption': encryption,
'channel': channel
}
except Exception:
continue

# Sort by RSSI (best first)
# Sort by signal strength (best first)
result = list(networks.values())
result.sort(key=lambda x: x['rssi'], reverse=True)
result.sort(key=lambda x: x['signal-strength'], reverse=True)

return result

Expand Down