mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-23 01:23:00 +02:00
Merge pull request #1337 from kernelkit/wifi-ap2
Add WiFi Access point support
This commit is contained in:
+55
-4
@@ -17,7 +17,25 @@
|
||||
"state": {
|
||||
"admin-state": "unlocked"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "radio0",
|
||||
"class": "infix-hardware:wifi",
|
||||
"infix-hardware:wifi-radio": {
|
||||
"country-code": "DE",
|
||||
"band": "2.4GHz",
|
||||
"channel": "auto"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "radio1",
|
||||
"class": "infix-hardware:wifi",
|
||||
"infix-hardware:wifi-radio": {
|
||||
"country-code": "DE",
|
||||
"band": "5GHz",
|
||||
"channel": "auto"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"ietf-interfaces:interfaces": {
|
||||
@@ -150,12 +168,36 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "wifi0",
|
||||
"type": "infix-if-type:wifi"
|
||||
"name": "wifi0-ap",
|
||||
"type": "infix-if-type:wifi",
|
||||
"infix-interfaces:wifi": {
|
||||
"radio": "radio0",
|
||||
"access-point": {
|
||||
"ssid": "Infix",
|
||||
"security": {
|
||||
"secret": "wifi"
|
||||
}
|
||||
}
|
||||
},
|
||||
"infix-interfaces:bridge-port": {
|
||||
"bridge": "br0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "wifi1",
|
||||
"type": "infix-if-type:wifi"
|
||||
"name": "wifi1-ap",
|
||||
"type": "infix-if-type:wifi",
|
||||
"infix-interfaces:wifi": {
|
||||
"radio": "radio1",
|
||||
"access-point": {
|
||||
"ssid": "Infix5Ghz",
|
||||
"security": {
|
||||
"secret": "wifi"
|
||||
}
|
||||
}
|
||||
},
|
||||
"infix-interfaces:bridge-port": {
|
||||
"bridge": "br0"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -171,6 +213,15 @@
|
||||
"certificates": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"symmetric-keys": {
|
||||
"symmetric-key": [
|
||||
{
|
||||
"name": "wifi",
|
||||
"infix-keystore:symmetric-key": "infixinfix",
|
||||
"infix-keystore:key-format": "infix-crypto-types:wifi-preshared-key-format"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"ietf-netconf-acm:nacm": {
|
||||
|
||||
@@ -108,7 +108,7 @@ To configure WiFi as a client, first store your WiFi password in the keystore:
|
||||
admin@infix:/> configure
|
||||
admin@infix:/config/> edit keystore symmetric-key mywifi
|
||||
admin@infix:/config/keystore/…/mywifi/> set key-format wifi-preshared-key-format
|
||||
admin@infix:/config/keystore/…/mywifi/> set cleartext-symmetric-key YourWiFiPassword
|
||||
admin@infix:/config/keystore/…/mywifi/> set symmetric-key YourWiFiPassword
|
||||
admin@infix:/config/keystore/…/mywifi/> leave
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
service <!> name:hostapd :%i \
|
||||
[2345] hostapd -P/var/run/hostapd-%i.pid /etc/hostapd-%i.conf \
|
||||
-- Wi-Fi Access Point @%i
|
||||
@@ -1,5 +1,5 @@
|
||||
service name:wpa_supplicant :%i \
|
||||
[2345] wpa_supplicant -s -i %i -c /etc/wpa_supplicant-%i.conf -P/var/run/wpa_supplicant-%i.pid \
|
||||
-- WPA supplicant @%i
|
||||
-- Wi-Fi Station @%i
|
||||
|
||||
task name:wifi-scanner :%i [2345] <pid/wpa_supplicant:%i> /usr/libexec/infix/wifi-scanner %i -- Start scanning for SSID @ %i
|
||||
task name:wifi-scanner :%i [2345] <pid/wpa_supplicant:%i> /usr/libexec/infix/wifi-scanner %i -- Start scanning for SSID @%i
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Rename WiFi PHY devices from phy0 to radio0 to avoid name clashes
|
||||
SUBSYSTEM=="ieee80211", ACTION=="add", KERNEL=="phy*", \
|
||||
RUN+="/bin/sh -c '/usr/sbin/iw phy %k set name radio%n'"
|
||||
@@ -0,0 +1,7 @@
|
||||
# Remove kernel-created WiFi interfaces
|
||||
# All WiFi interfaces are now virtual interfaces created by confd
|
||||
SUBSYSTEM=="net", ACTION=="add", KERNEL=="wlan*", \
|
||||
TEST=="/sys/class/net/$name/phy80211/name", \
|
||||
PROGRAM="/bin/cat /sys/class/net/%k/phy80211/name", \
|
||||
TEST!="/run/wifi-cleaned-%c", \
|
||||
RUN+="/bin/sh -c '/usr/sbin/iw dev %k del && touch /run/wifi-cleaned-%c'"
|
||||
@@ -1 +0,0 @@
|
||||
SUBSYSTEM=="net", ACTION=="add", TEST=="/sys/class/net/$name/wireless", NAME="wifi%n"
|
||||
Executable
+454
@@ -0,0 +1,454 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
iw command wrapper that returns structured JSON data
|
||||
|
||||
Usage:
|
||||
iw.py list - List all PHY devices
|
||||
iw.py dev - List all interfaces grouped by PHY
|
||||
iw.py info <device> - Get PHY or interface information
|
||||
iw.py survey <interface> - Get channel survey data
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import subprocess
|
||||
import re
|
||||
|
||||
|
||||
def run_iw(*args):
|
||||
"""Run iw command and return output"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['iw'] + list(args),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return result.stdout
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def normalize_phy_name(name):
|
||||
"""
|
||||
Convert radioN to phyN or vice versa based on what exists in sysfs.
|
||||
Returns the actual phy name that exists.
|
||||
"""
|
||||
import os
|
||||
|
||||
# Try the name as-is first
|
||||
if os.path.exists(f'/sys/class/ieee80211/{name}'):
|
||||
return name
|
||||
|
||||
# Try converting radioN <-> phyN
|
||||
if name.startswith('radio'):
|
||||
phy_name = 'phy' + name[5:]
|
||||
if os.path.exists(f'/sys/class/ieee80211/{phy_name}'):
|
||||
return phy_name
|
||||
elif name.startswith('phy'):
|
||||
radio_name = 'radio' + name[3:]
|
||||
if os.path.exists(f'/sys/class/ieee80211/{radio_name}'):
|
||||
return radio_name
|
||||
|
||||
# Return original if nothing found
|
||||
return name
|
||||
|
||||
|
||||
def parse_phy_info(phy_name):
|
||||
"""
|
||||
Parse 'iw phy <name> info' output or 'iw <name> info' output
|
||||
Returns: {bands, driver, manufacturer, max_txpower, num_virtual_interfaces, interface_combinations}
|
||||
"""
|
||||
# Normalize the phy name
|
||||
actual_phy = normalize_phy_name(phy_name)
|
||||
|
||||
# Try 'iw phy <name> info' first
|
||||
output = run_iw('phy', actual_phy, 'info')
|
||||
|
||||
# If that fails, try 'iw <name> info' (some systems support this)
|
||||
if not output:
|
||||
output = run_iw(actual_phy, 'info')
|
||||
|
||||
if not output:
|
||||
return {}
|
||||
|
||||
result = {
|
||||
'name': phy_name,
|
||||
'bands': [],
|
||||
'driver': None,
|
||||
'manufacturer': None,
|
||||
'max_txpower': None,
|
||||
'num_virtual_interfaces': 0,
|
||||
'interface_combinations': []
|
||||
}
|
||||
|
||||
current_band = None
|
||||
band_num = 0
|
||||
in_combinations = False
|
||||
max_power = None
|
||||
|
||||
for line in output.splitlines():
|
||||
stripped = line.strip()
|
||||
|
||||
# Detect band sections
|
||||
if stripped.startswith('Band '):
|
||||
if current_band and current_band.get('frequencies'):
|
||||
result['bands'].append(current_band)
|
||||
band_num += 1
|
||||
current_band = {
|
||||
'band': band_num,
|
||||
'frequencies': [],
|
||||
'name': None,
|
||||
'ht_capable': False,
|
||||
'vht_capable': False,
|
||||
'he_capable': False
|
||||
}
|
||||
in_combinations = False
|
||||
|
||||
# Parse frequencies (handle both "2412 MHz" and "2412.0 MHz" formats)
|
||||
elif current_band and not in_combinations:
|
||||
freq_match = re.match(r'\* ([0-9.]+) MHz.*?\(([0-9.]+) dBm\)', stripped)
|
||||
if freq_match:
|
||||
freq = int(float(freq_match.group(1))) # Convert "2412.0" to 2412
|
||||
power = float(freq_match.group(2))
|
||||
|
||||
current_band['frequencies'].append(freq)
|
||||
|
||||
# Track max power
|
||||
if max_power is None or power > max_power:
|
||||
max_power = power
|
||||
|
||||
# Check capabilities
|
||||
if 'HT ' in stripped or 'High Throughput' in stripped:
|
||||
current_band['ht_capable'] = True
|
||||
if 'VHT' in stripped or 'Very High Throughput' in stripped:
|
||||
current_band['vht_capable'] = True
|
||||
if 'HE ' in stripped or 'High Efficiency' in stripped:
|
||||
current_band['he_capable'] = True
|
||||
|
||||
# Detect interface combinations section
|
||||
if 'valid interface combinations:' in stripped.lower():
|
||||
in_combinations = True
|
||||
continue
|
||||
|
||||
# Parse interface combinations
|
||||
if in_combinations:
|
||||
if stripped.startswith('*'):
|
||||
# Parse combination line
|
||||
comb_info = {'limits': []}
|
||||
|
||||
# Parse limits: #{ type } <= max
|
||||
limit_matches = re.findall(r'#\{\s*([^}]+)\s*\}\s*<=\s*(\d+)', stripped)
|
||||
for types_str, max_val in limit_matches:
|
||||
types = [t.strip() for t in types_str.split(',')]
|
||||
comb_info['limits'].append({
|
||||
'max': int(max_val),
|
||||
'types': types
|
||||
})
|
||||
|
||||
# Parse total
|
||||
total_match = re.search(r'total\s*<=\s*(\d+)', stripped)
|
||||
if total_match:
|
||||
comb_info['max_total'] = int(total_match.group(1))
|
||||
|
||||
# Parse channels
|
||||
channels_match = re.search(r'#channels\s*<=\s*(\d+)', stripped)
|
||||
if channels_match:
|
||||
comb_info['num_channels'] = int(channels_match.group(1))
|
||||
|
||||
if comb_info.get('limits') or comb_info.get('max_total'):
|
||||
result['interface_combinations'].append(comb_info)
|
||||
|
||||
elif not stripped.startswith('#') and ':' in stripped and not stripped.startswith('*'):
|
||||
# End of combinations section
|
||||
in_combinations = False
|
||||
|
||||
# Add last band
|
||||
if current_band and current_band.get('frequencies'):
|
||||
result['bands'].append(current_band)
|
||||
|
||||
# Determine band names and assign band numbers
|
||||
for band in result['bands']:
|
||||
if band['frequencies']:
|
||||
freq = band['frequencies'][0]
|
||||
if 2400 <= freq <= 2500:
|
||||
band['name'] = '2.4GHz'
|
||||
band['band'] = 1
|
||||
elif 5150 <= freq <= 5900:
|
||||
band['name'] = '5GHz'
|
||||
band['band'] = 2
|
||||
elif 5955 <= freq <= 7115:
|
||||
band['name'] = '6GHz'
|
||||
band['band'] = 3
|
||||
|
||||
# Set max TX power
|
||||
if max_power is not None:
|
||||
result['max_txpower'] = int(max_power)
|
||||
|
||||
# Get driver and manufacturer from sysfs
|
||||
try:
|
||||
driver_link = subprocess.run(
|
||||
['readlink', '-f', f'/sys/class/ieee80211/{actual_phy}/device/driver'],
|
||||
capture_output=True, text=True, timeout=1
|
||||
).stdout.strip()
|
||||
|
||||
if driver_link:
|
||||
driver_name = driver_link.split('/')[-1]
|
||||
result['driver'] = driver_name
|
||||
|
||||
# Map driver to manufacturer
|
||||
driver_lower = driver_name.lower()
|
||||
if 'mt' in driver_lower or 'mediatek' in driver_lower:
|
||||
result['manufacturer'] = 'MediaTek Inc.'
|
||||
elif 'rtw' in driver_lower or 'realtek' in driver_lower:
|
||||
result['manufacturer'] = 'Realtek Semiconductor Corp.'
|
||||
elif 'ath' in driver_lower or 'qca' in driver_lower:
|
||||
result['manufacturer'] = 'Qualcomm Atheros'
|
||||
elif 'iwl' in driver_lower or 'intel' in driver_lower:
|
||||
result['manufacturer'] = 'Intel Corporation'
|
||||
elif 'brcm' in driver_lower or 'broadcom' in driver_lower:
|
||||
result['manufacturer'] = 'Broadcom Inc.'
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Count virtual interfaces
|
||||
dev_output = run_iw('dev')
|
||||
if dev_output:
|
||||
# Extract phy number from actual phy name
|
||||
phy_num = None
|
||||
if actual_phy.startswith('radio'):
|
||||
phy_num = actual_phy[5:]
|
||||
elif actual_phy.startswith('phy'):
|
||||
phy_num = actual_phy[3:]
|
||||
|
||||
if phy_num:
|
||||
count = 0
|
||||
current_phy = None
|
||||
for line in dev_output.splitlines():
|
||||
if line.startswith('phy#'):
|
||||
current_phy = line.replace('phy#', '').strip()
|
||||
elif current_phy == phy_num and 'Interface' in line:
|
||||
count += 1
|
||||
result['num_virtual_interfaces'] = count
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def parse_interface_info(ifname):
|
||||
"""
|
||||
Parse 'iw dev <name> info' output
|
||||
Returns: {ifname, iftype, mac, ssid, frequency, channel, txpower, channel_width}
|
||||
"""
|
||||
output = run_iw('dev', ifname, 'info')
|
||||
if not output:
|
||||
return {}
|
||||
|
||||
result = {'ifname': ifname}
|
||||
|
||||
for line in output.splitlines():
|
||||
stripped = line.strip()
|
||||
|
||||
# Interface type
|
||||
if stripped.startswith('type '):
|
||||
result['iftype'] = stripped.split()[1]
|
||||
|
||||
# MAC address
|
||||
elif stripped.startswith('addr '):
|
||||
result['mac'] = stripped.split()[1]
|
||||
|
||||
# SSID
|
||||
elif stripped.startswith('ssid '):
|
||||
result['ssid'] = ' '.join(stripped.split()[1:])
|
||||
|
||||
# Channel/frequency
|
||||
elif stripped.startswith('channel '):
|
||||
parts = stripped.split()
|
||||
if len(parts) >= 2:
|
||||
result['channel'] = int(parts[1])
|
||||
if 'MHz' in stripped:
|
||||
freq_match = re.search(r'\((\d+) MHz', stripped)
|
||||
if freq_match:
|
||||
result['frequency'] = int(freq_match.group(1))
|
||||
# Channel width
|
||||
if 'width:' in stripped:
|
||||
width_match = re.search(r'width:\s*(\d+)\s*MHz', stripped)
|
||||
if width_match:
|
||||
result['channel_width'] = f"{width_match.group(1)} MHz"
|
||||
|
||||
# TX power
|
||||
elif stripped.startswith('txpower '):
|
||||
power_match = re.search(r'([0-9.]+) dBm', stripped)
|
||||
if power_match:
|
||||
result['txpower'] = float(power_match.group(1))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def parse_survey(ifname):
|
||||
"""
|
||||
Parse 'iw dev <name> survey dump' output
|
||||
Returns: list of {frequency, in_use, noise, active_time, busy_time, receive_time, transmit_time}
|
||||
"""
|
||||
output = run_iw('dev', ifname, 'survey', 'dump')
|
||||
if not output:
|
||||
return []
|
||||
|
||||
channels = []
|
||||
current_channel = None
|
||||
|
||||
for line in output.splitlines():
|
||||
stripped = line.strip()
|
||||
|
||||
# New survey entry
|
||||
if stripped.startswith('Survey data from'):
|
||||
if current_channel:
|
||||
channels.append(current_channel)
|
||||
current_channel = None
|
||||
|
||||
# Frequency
|
||||
elif stripped.startswith('frequency:'):
|
||||
parts = stripped.split()
|
||||
if len(parts) >= 2:
|
||||
freq = int(parts[1])
|
||||
in_use = '[in use]' in stripped
|
||||
current_channel = {
|
||||
'frequency': freq,
|
||||
'in_use': in_use
|
||||
}
|
||||
|
||||
# Channel metrics
|
||||
elif current_channel:
|
||||
if stripped.startswith('noise:'):
|
||||
noise_match = re.search(r'(-?\d+) dBm', stripped)
|
||||
if noise_match:
|
||||
current_channel['noise'] = int(noise_match.group(1))
|
||||
|
||||
elif stripped.startswith('channel active time:'):
|
||||
time_match = re.search(r'(\d+) ms', stripped)
|
||||
if time_match:
|
||||
current_channel['active_time'] = int(time_match.group(1))
|
||||
|
||||
elif stripped.startswith('channel busy time:'):
|
||||
time_match = re.search(r'(\d+) ms', stripped)
|
||||
if time_match:
|
||||
current_channel['busy_time'] = int(time_match.group(1))
|
||||
|
||||
elif stripped.startswith('channel receive time:'):
|
||||
time_match = re.search(r'(\d+) ms', stripped)
|
||||
if time_match:
|
||||
current_channel['receive_time'] = int(time_match.group(1))
|
||||
|
||||
elif stripped.startswith('channel transmit time:'):
|
||||
time_match = re.search(r'(\d+) ms', stripped)
|
||||
if time_match:
|
||||
current_channel['transmit_time'] = int(time_match.group(1))
|
||||
|
||||
# Add last channel
|
||||
if current_channel:
|
||||
channels.append(current_channel)
|
||||
|
||||
return channels
|
||||
|
||||
|
||||
def parse_list():
|
||||
"""
|
||||
Parse 'iw list' output
|
||||
Returns: list of PHY names
|
||||
"""
|
||||
output = run_iw('list')
|
||||
if not output:
|
||||
return []
|
||||
|
||||
phys = []
|
||||
for line in output.splitlines():
|
||||
match = re.match(r'Wiphy (phy\d+|radio\d+)', line)
|
||||
if match:
|
||||
phys.append(match.group(1))
|
||||
|
||||
return phys
|
||||
|
||||
|
||||
def parse_dev():
|
||||
"""
|
||||
Parse 'iw dev' output
|
||||
Returns: dict mapping PHY numbers to list of interfaces
|
||||
"""
|
||||
output = run_iw('dev')
|
||||
if not output:
|
||||
return {}
|
||||
|
||||
result = {}
|
||||
current_phy = None
|
||||
|
||||
for line in output.splitlines():
|
||||
# PHY line: "phy#0" or "phy#1"
|
||||
if line.startswith('phy#'):
|
||||
current_phy = line.replace('phy#', '').strip()
|
||||
if current_phy not in result:
|
||||
result[current_phy] = []
|
||||
# Interface line: " Interface wlan0"
|
||||
elif current_phy and 'Interface' in line:
|
||||
ifname = line.split('Interface')[1].strip()
|
||||
result[current_phy].append(ifname)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(json.dumps({
|
||||
'error': 'Usage: iw.py <command> [device]',
|
||||
'commands': {
|
||||
'list': 'List all PHY devices',
|
||||
'dev': 'List all interfaces grouped by PHY',
|
||||
'info': 'Get PHY or interface information (requires device)',
|
||||
'survey': 'Get channel survey data (requires interface name)'
|
||||
},
|
||||
'examples': [
|
||||
'iw.py list',
|
||||
'iw.py dev',
|
||||
'iw.py info radio0',
|
||||
'iw.py info phy4',
|
||||
'iw.py info wlan0',
|
||||
'iw.py survey wlan0'
|
||||
]
|
||||
}, indent=2))
|
||||
sys.exit(1)
|
||||
|
||||
command = sys.argv[1]
|
||||
|
||||
try:
|
||||
if command == 'list':
|
||||
data = parse_list()
|
||||
elif command == 'dev':
|
||||
data = parse_dev()
|
||||
elif command == 'info':
|
||||
if len(sys.argv) < 3:
|
||||
data = {'error': 'info command requires device argument'}
|
||||
else:
|
||||
device = sys.argv[2]
|
||||
# Auto-detect if device is a PHY (phy*/radio*) or interface
|
||||
if device.startswith('phy') or device.startswith('radio'):
|
||||
data = parse_phy_info(device)
|
||||
else:
|
||||
data = parse_interface_info(device)
|
||||
elif command == 'survey':
|
||||
if len(sys.argv) < 3:
|
||||
data = {'error': 'survey command requires device argument'}
|
||||
else:
|
||||
device = sys.argv[2]
|
||||
data = parse_survey(device)
|
||||
else:
|
||||
data = {'error': f'Unknown command: {command}'}
|
||||
|
||||
print(json.dumps(data, indent=2))
|
||||
|
||||
except Exception as e:
|
||||
print(json.dumps({'error': str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
+724
@@ -0,0 +1,724 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
WiFi Channel Visualization Tool
|
||||
Shows graphical representation of WiFi channel overlap and utilization
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
|
||||
|
||||
class Colors:
|
||||
"""ANSI color codes for terminal output"""
|
||||
RESET = '\033[0m'
|
||||
BOLD = '\033[1m'
|
||||
RED = '\033[91m'
|
||||
GREEN = '\033[92m'
|
||||
YELLOW = '\033[93m'
|
||||
BLUE = '\033[94m'
|
||||
MAGENTA = '\033[95m'
|
||||
CYAN = '\033[96m'
|
||||
GRAY = '\033[90m'
|
||||
BG_RED = '\033[101m'
|
||||
BG_GREEN = '\033[102m'
|
||||
BG_YELLOW = '\033[103m'
|
||||
BG_BLUE = '\033[104m'
|
||||
BG_GRAY = '\033[100m'
|
||||
|
||||
|
||||
def freq_to_channel(freq):
|
||||
"""Convert frequency (MHz) to WiFi channel number"""
|
||||
# 2.4 GHz band
|
||||
if 2412 <= freq <= 2484:
|
||||
if freq == 2484:
|
||||
return 14
|
||||
return (freq - 2412) // 5 + 1
|
||||
# 5 GHz band
|
||||
elif 5170 <= freq <= 5825:
|
||||
return (freq - 5000) // 5
|
||||
# 6 GHz band
|
||||
elif 5955 <= freq <= 7115:
|
||||
return (freq - 5950) // 5
|
||||
return None
|
||||
|
||||
|
||||
def get_channel_frequency(channel, band='2.4'):
|
||||
"""Get center frequency for a channel"""
|
||||
if band == '2.4':
|
||||
if channel == 14:
|
||||
return 2484
|
||||
return 2412 + (channel - 1) * 5
|
||||
elif band == '5':
|
||||
return 5000 + channel * 5
|
||||
return None
|
||||
|
||||
|
||||
def get_busy_percentage(channel_data):
|
||||
"""Calculate channel busy percentage"""
|
||||
active = channel_data.get('active-time', 0)
|
||||
busy = channel_data.get('busy-time', 0)
|
||||
if active > 0:
|
||||
return (busy / active) * 100
|
||||
return 0
|
||||
|
||||
|
||||
def get_utilization_color(busy_pct):
|
||||
"""Get color based on channel utilization"""
|
||||
if busy_pct >= 50:
|
||||
return Colors.RED
|
||||
elif busy_pct >= 25:
|
||||
return Colors.YELLOW
|
||||
elif busy_pct >= 10:
|
||||
return Colors.CYAN
|
||||
else:
|
||||
return Colors.GREEN
|
||||
|
||||
|
||||
def draw_channel_graph_2_4ghz(survey_data):
|
||||
"""Draw channel overlap graph for 2.4 GHz band"""
|
||||
# Parse survey data
|
||||
channels = {}
|
||||
in_use_channel = None
|
||||
|
||||
for ch_data in survey_data:
|
||||
freq = ch_data.get('frequency')
|
||||
ch_num = freq_to_channel(freq)
|
||||
if ch_num and 1 <= ch_num <= 14:
|
||||
busy_pct = get_busy_percentage(ch_data)
|
||||
channels[ch_num] = {
|
||||
'freq': freq,
|
||||
'noise': ch_data.get('noise', -100),
|
||||
'busy': busy_pct,
|
||||
'in_use': ch_data.get('in-use', False),
|
||||
'active_time': ch_data.get('active-time', 0),
|
||||
'busy_time': ch_data.get('busy-time', 0)
|
||||
}
|
||||
if ch_data.get('in-use'):
|
||||
in_use_channel = ch_num
|
||||
|
||||
if not channels:
|
||||
print("No 2.4 GHz channel data available")
|
||||
return
|
||||
|
||||
print(f"\n{Colors.BOLD}2.4 GHz WiFi Channel Overlap Visualization{Colors.RESET}")
|
||||
print("=" * 80)
|
||||
print(f"Channel width: 20 MHz | Channel spacing: 5 MHz")
|
||||
print(f"Non-overlapping channels: 1, 6, 11 (shown in {Colors.GREEN}green{Colors.RESET})")
|
||||
print()
|
||||
|
||||
# Draw frequency scale
|
||||
print("Frequency (MHz):")
|
||||
print("2400 2420 2440 2460 2480")
|
||||
print("|-----------|-----------|-----------|-----------|")
|
||||
|
||||
# Draw each channel as a bar showing its 20 MHz width
|
||||
# Each channel occupies ~4 adjacent channels worth of space
|
||||
for ch in range(1, 14):
|
||||
if ch not in channels:
|
||||
continue
|
||||
|
||||
data = channels[ch]
|
||||
busy_pct = data['busy']
|
||||
is_in_use = data['in_use']
|
||||
noise = data['noise']
|
||||
|
||||
# Determine color based on status
|
||||
if is_in_use:
|
||||
color = Colors.BG_BLUE
|
||||
marker = '█'
|
||||
elif busy_pct >= 50:
|
||||
color = Colors.RED
|
||||
marker = '▓'
|
||||
elif busy_pct >= 25:
|
||||
color = Colors.YELLOW
|
||||
marker = '▒'
|
||||
elif busy_pct > 0:
|
||||
color = Colors.CYAN
|
||||
marker = '░'
|
||||
else:
|
||||
color = Colors.GRAY
|
||||
marker = '·'
|
||||
|
||||
# Non-overlapping channels get green color
|
||||
if ch in [1, 6, 11] and not is_in_use and busy_pct < 10:
|
||||
color = Colors.GREEN
|
||||
|
||||
# Calculate position (each channel is offset by 5 MHz = 1 position)
|
||||
# Channel 1 is at 2412 MHz, base is 2400
|
||||
offset = ((data['freq'] - 2400) // 5)
|
||||
|
||||
# Draw channel bar (20 MHz = 4 positions wide)
|
||||
line = ' ' * 80
|
||||
line_arr = list(line)
|
||||
|
||||
# Mark the channel span (20 MHz width)
|
||||
for i in range(4):
|
||||
pos = offset + i - 2 # Center the 20 MHz around channel
|
||||
if 0 <= pos < len(line_arr):
|
||||
line_arr[pos] = marker
|
||||
|
||||
# Add channel label
|
||||
label_pos = offset
|
||||
if 0 <= label_pos < len(line_arr) - 5:
|
||||
# Clear space for label
|
||||
for i in range(5):
|
||||
if label_pos + i < len(line_arr):
|
||||
line_arr[label_pos + i] = ' '
|
||||
|
||||
line = ''.join(line_arr)
|
||||
|
||||
# Status indicators
|
||||
status = ""
|
||||
if is_in_use:
|
||||
status = f" {Colors.BOLD}[IN USE]{Colors.RESET}"
|
||||
|
||||
busy_color = get_utilization_color(busy_pct)
|
||||
|
||||
print(f"{color}Ch{ch:2d}{Colors.RESET} {color}{line}{Colors.RESET} "
|
||||
f"{busy_color}{busy_pct:5.1f}%{Colors.RESET} "
|
||||
f"{noise:4d}dBm{status}")
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print(f"\n{Colors.BOLD}Legend:{Colors.RESET}")
|
||||
print(f" {Colors.BG_BLUE}██{Colors.RESET} In use (your network)")
|
||||
print(f" {Colors.RED}▓▓{Colors.RESET} High usage (>50%)")
|
||||
print(f" {Colors.YELLOW}▒▒{Colors.RESET} Medium usage (25-50%)")
|
||||
print(f" {Colors.CYAN}░░{Colors.RESET} Low usage (1-25%)")
|
||||
print(f" {Colors.GRAY}··{Colors.RESET} Idle (<1%)")
|
||||
print()
|
||||
|
||||
|
||||
def draw_channel_list(survey_data):
|
||||
"""Draw a simple channel list with utilization bars"""
|
||||
print(f"\n{Colors.BOLD}Channel Utilization{Colors.RESET}")
|
||||
print("=" * 80)
|
||||
print(f"{'Ch':<4} {'Freq':<6} {'Noise':<8} {'Busy%':<8} {'Utilization Bar':<40}")
|
||||
print("-" * 80)
|
||||
|
||||
for ch_data in sorted(survey_data, key=lambda x: x.get('frequency', 0)):
|
||||
freq = ch_data.get('frequency')
|
||||
ch_num = freq_to_channel(freq)
|
||||
if not ch_num:
|
||||
continue
|
||||
|
||||
noise = ch_data.get('noise', -100)
|
||||
busy_pct = get_busy_percentage(ch_data)
|
||||
is_in_use = ch_data.get('in-use', False)
|
||||
|
||||
# Create utilization bar (40 chars wide = 100%)
|
||||
bar_length = int(busy_pct * 40 / 100)
|
||||
bar_color = get_utilization_color(busy_pct)
|
||||
|
||||
if is_in_use:
|
||||
bar = f"{Colors.BG_BLUE}{'█' * bar_length}{Colors.RESET}"
|
||||
marker = f" {Colors.BOLD}◀ IN USE{Colors.RESET}"
|
||||
else:
|
||||
bar = f"{bar_color}{'█' * bar_length}{Colors.RESET}"
|
||||
marker = ""
|
||||
|
||||
empty = '░' * (40 - bar_length)
|
||||
|
||||
print(f"{ch_num:<4} {freq:<6} {noise:<8} {busy_pct:5.1f}% {bar}{Colors.GRAY}{empty}{Colors.RESET}{marker}")
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def draw_overlap_pie(survey_data):
|
||||
"""Draw a pie-style visualization of channel group utilization"""
|
||||
# Parse channel data into 3 non-overlapping groups
|
||||
# Group 1: channels 1-5 (centered on ch 1)
|
||||
# Group 2: channels 4-8 (centered on ch 6)
|
||||
# Group 3: channels 9-13 (centered on ch 11)
|
||||
|
||||
channels = {}
|
||||
in_use_channel = None
|
||||
|
||||
for ch_data in survey_data:
|
||||
freq = ch_data.get('frequency')
|
||||
ch_num = freq_to_channel(freq)
|
||||
if ch_num and 1 <= ch_num <= 13:
|
||||
busy_pct = get_busy_percentage(ch_data)
|
||||
channels[ch_num] = {
|
||||
'busy': busy_pct,
|
||||
'noise': ch_data.get('noise', -100),
|
||||
'in_use': ch_data.get('in-use', False)
|
||||
}
|
||||
if ch_data.get('in-use'):
|
||||
in_use_channel = ch_num
|
||||
|
||||
if not channels:
|
||||
return
|
||||
|
||||
# Calculate group utilization (average of channels in each group)
|
||||
groups = [
|
||||
{'name': 'Ch 1', 'channels': [1, 2, 3, 4, 5], 'center': 1},
|
||||
{'name': 'Ch 6', 'channels': [4, 5, 6, 7, 8], 'center': 6},
|
||||
{'name': 'Ch 11', 'channels': [9, 10, 11, 12, 13], 'center': 11},
|
||||
]
|
||||
|
||||
for group in groups:
|
||||
busy_values = [channels.get(ch, {}).get('busy', 0) for ch in group['channels'] if ch in channels]
|
||||
group['avg_busy'] = sum(busy_values) / len(busy_values) if busy_values else 0
|
||||
group['center_busy'] = channels.get(group['center'], {}).get('busy', 0)
|
||||
group['in_use'] = in_use_channel in group['channels'] if in_use_channel else False
|
||||
|
||||
total_busy = sum(g['avg_busy'] for g in groups)
|
||||
|
||||
print(f"\n{Colors.BOLD}Channel Group Utilization (2.4 GHz){Colors.RESET}")
|
||||
print("=" * 60)
|
||||
print("Non-overlapping channel groups with their overlap zones:\n")
|
||||
|
||||
# Draw ASCII donut/pie
|
||||
pie_width = 50
|
||||
|
||||
# Calculate proportions
|
||||
if total_busy > 0:
|
||||
for group in groups:
|
||||
group['proportion'] = group['avg_busy'] / total_busy
|
||||
group['width'] = max(1, int(group['proportion'] * pie_width))
|
||||
else:
|
||||
for group in groups:
|
||||
group['proportion'] = 1/3
|
||||
group['width'] = pie_width // 3
|
||||
|
||||
# Adjust to exactly fill pie_width
|
||||
total_width = sum(g['width'] for g in groups)
|
||||
if total_width < pie_width:
|
||||
groups[0]['width'] += pie_width - total_width
|
||||
|
||||
# Draw the pie bar
|
||||
pie_chars = ['█', '▓', '░']
|
||||
colors = [Colors.GREEN, Colors.YELLOW, Colors.CYAN]
|
||||
|
||||
pie_line = ""
|
||||
for i, group in enumerate(groups):
|
||||
if group['in_use']:
|
||||
color = Colors.BG_BLUE
|
||||
elif group['avg_busy'] >= 50:
|
||||
color = Colors.RED
|
||||
elif group['avg_busy'] >= 25:
|
||||
color = Colors.YELLOW
|
||||
else:
|
||||
color = Colors.GREEN
|
||||
|
||||
char = pie_chars[i % len(pie_chars)]
|
||||
pie_line += f"{color}{char * group['width']}{Colors.RESET}"
|
||||
|
||||
# Draw centered pie
|
||||
print(f" ┌{'─' * pie_width}┐")
|
||||
print(f" │{pie_line}│")
|
||||
print(f" └{'─' * pie_width}┘")
|
||||
|
||||
# Legend with percentages
|
||||
print()
|
||||
for i, group in enumerate(groups):
|
||||
char = pie_chars[i % len(pie_chars)]
|
||||
|
||||
if group['in_use']:
|
||||
color = Colors.BG_BLUE
|
||||
marker = " ◀ IN USE"
|
||||
elif group['avg_busy'] >= 50:
|
||||
color = Colors.RED
|
||||
marker = ""
|
||||
elif group['avg_busy'] >= 25:
|
||||
color = Colors.YELLOW
|
||||
marker = ""
|
||||
else:
|
||||
color = Colors.GREEN
|
||||
marker = ""
|
||||
|
||||
pct_of_total = group['proportion'] * 100
|
||||
print(f" {color}{char * 3}{Colors.RESET} {group['name']:>5}: "
|
||||
f"{group['center_busy']:5.1f}% busy (center), "
|
||||
f"{group['avg_busy']:5.1f}% avg in overlap zone, "
|
||||
f"{pct_of_total:4.1f}% of total{marker}")
|
||||
|
||||
# Draw overlap diagram
|
||||
print(f"\n{Colors.BOLD}Channel Overlap Diagram:{Colors.RESET}")
|
||||
print(" Ch: 1 2 3 4 5 6 7 8 9 10 11 12 13")
|
||||
print(" ╔═══════════════════╗")
|
||||
print(" G1: ║ 1 ─ 2 ─ 3 ─ 4 ─ 5 ║ (centered on ch 1)")
|
||||
print(" ╚═══════╦═══════════╝")
|
||||
print(" ╔═══════════════════╗")
|
||||
print(" G2: ║ 4 ─ 5 ─ 6 ─ 7 ─ 8 ║ (centered on ch 6)")
|
||||
print(" ╚═══════════╦═══════╝")
|
||||
print(" ╔═══════════════════════╗")
|
||||
print(" G3: ║ 9 ─10 ─11 ─12 ─13 ║ (centered on ch 11)")
|
||||
print(" ╚═══════════════════════╝")
|
||||
print()
|
||||
|
||||
|
||||
def generate_svg(survey_data, output_file=None):
|
||||
"""Generate SVG image(s) showing channel overlap and utilization for both bands"""
|
||||
# Separate channels by band
|
||||
channels_2_4 = {}
|
||||
channels_5 = {}
|
||||
in_use_2_4 = None
|
||||
in_use_5 = None
|
||||
|
||||
for ch_data in survey_data:
|
||||
freq = ch_data.get('frequency')
|
||||
ch_num = freq_to_channel(freq)
|
||||
if not ch_num:
|
||||
continue
|
||||
|
||||
busy_pct = get_busy_percentage(ch_data)
|
||||
ch_info = {
|
||||
'freq': freq,
|
||||
'busy': busy_pct,
|
||||
'noise': ch_data.get('noise', -100),
|
||||
'in_use': ch_data.get('in-use', False)
|
||||
}
|
||||
|
||||
if 2400 <= freq <= 2500:
|
||||
channels_2_4[ch_num] = ch_info
|
||||
if ch_info['in_use']:
|
||||
in_use_2_4 = ch_num
|
||||
elif 5100 <= freq <= 5900:
|
||||
channels_5[ch_num] = ch_info
|
||||
if ch_info['in_use']:
|
||||
in_use_5 = ch_num
|
||||
|
||||
def busy_to_color(busy_pct, is_in_use=False):
|
||||
if is_in_use:
|
||||
return "#3b82f6" # Blue
|
||||
elif busy_pct >= 50:
|
||||
return "#ef4444" # Red
|
||||
elif busy_pct >= 25:
|
||||
return "#f59e0b" # Yellow/Orange
|
||||
elif busy_pct >= 10:
|
||||
return "#06b6d4" # Cyan
|
||||
else:
|
||||
return "#22c55e" # Green
|
||||
|
||||
def generate_band_svg(channels, band, freq_min, freq_max, title, non_overlap_channels=None):
|
||||
if not channels:
|
||||
return None
|
||||
|
||||
width = 900
|
||||
height = 400
|
||||
margin_left = 60
|
||||
margin_right = 40
|
||||
margin_top = 60
|
||||
margin_bottom = 80
|
||||
chart_width = width - margin_left - margin_right
|
||||
chart_height = height - margin_top - margin_bottom
|
||||
|
||||
def freq_to_x(freq):
|
||||
return margin_left + (freq - freq_min) / (freq_max - freq_min) * chart_width
|
||||
|
||||
def busy_to_height(busy_pct):
|
||||
return (busy_pct / 100) * chart_height
|
||||
|
||||
svg_parts = []
|
||||
|
||||
# SVG header
|
||||
svg_parts.append(f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {width} {height}">
|
||||
<defs>
|
||||
<style>
|
||||
.title {{ font: bold 18px sans-serif; fill: #333; }}
|
||||
.label {{ font: 12px sans-serif; fill: #666; }}
|
||||
.axis {{ font: 10px sans-serif; fill: #333; }}
|
||||
.channel-label {{ font: bold 10px sans-serif; fill: #333; }}
|
||||
.legend {{ font: 12px sans-serif; fill: #333; }}
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="{width}" height="{height}" fill="#fafafa"/>
|
||||
|
||||
<!-- Title -->
|
||||
<text x="{width/2}" y="30" text-anchor="middle" class="title">{title}</text>
|
||||
|
||||
<!-- Chart area -->
|
||||
<rect x="{margin_left}" y="{margin_top}" width="{chart_width}" height="{chart_height}" fill="#fff" stroke="#ddd"/>
|
||||
''')
|
||||
|
||||
# Draw grid lines
|
||||
for pct in [25, 50, 75, 100]:
|
||||
y = margin_top + chart_height - busy_to_height(pct)
|
||||
svg_parts.append(f' <line x1="{margin_left}" y1="{y}" x2="{margin_left + chart_width}" y2="{y}" stroke="#eee" stroke-dasharray="4,4"/>')
|
||||
svg_parts.append(f' <text x="{margin_left - 5}" y="{y + 4}" text-anchor="end" class="axis">{pct}%</text>')
|
||||
|
||||
# Y-axis label
|
||||
svg_parts.append(f' <text x="15" y="{margin_top + chart_height/2}" text-anchor="middle" transform="rotate(-90, 15, {margin_top + chart_height/2})" class="label">Channel Busy %</text>')
|
||||
|
||||
# Draw channels as bars
|
||||
for ch_num, data in sorted(channels.items()):
|
||||
center_freq = data['freq']
|
||||
busy_pct = data['busy']
|
||||
is_in_use = data['in_use']
|
||||
|
||||
# 20 MHz width: ±10 MHz from center
|
||||
x1 = freq_to_x(center_freq - 10)
|
||||
x2 = freq_to_x(center_freq + 10)
|
||||
bar_width = x2 - x1
|
||||
bar_height = busy_to_height(busy_pct)
|
||||
bar_y = margin_top + chart_height - bar_height
|
||||
|
||||
color = busy_to_color(busy_pct, is_in_use)
|
||||
opacity = 0.6 if not is_in_use else 0.8
|
||||
|
||||
# Draw the channel bar
|
||||
svg_parts.append(f' <rect x="{x1}" y="{bar_y}" width="{bar_width}" height="{bar_height}" fill="{color}" opacity="{opacity}" stroke="{color}" stroke-width="1"/>')
|
||||
|
||||
# Channel label at bottom
|
||||
label_x = freq_to_x(center_freq)
|
||||
svg_parts.append(f' <text x="{label_x}" y="{margin_top + chart_height + 15}" text-anchor="middle" class="channel-label">{ch_num}</text>')
|
||||
|
||||
# Frequency label (only for some channels to avoid clutter)
|
||||
if band == '2.4' or ch_num in [36, 52, 100, 149, 165]:
|
||||
svg_parts.append(f' <text x="{label_x}" y="{margin_top + chart_height + 28}" text-anchor="middle" class="axis">{center_freq}</text>')
|
||||
|
||||
# Busy percentage on top of bar (if tall enough)
|
||||
if bar_height > 20:
|
||||
svg_parts.append(f' <text x="{label_x}" y="{bar_y + 15}" text-anchor="middle" class="channel-label" fill="white">{busy_pct:.0f}%</text>')
|
||||
|
||||
# X-axis labels
|
||||
svg_parts.append(f' <text x="{margin_left + chart_width/2}" y="{height - 15}" text-anchor="middle" class="label">Channel (Center Frequency MHz)</text>')
|
||||
|
||||
# Legend
|
||||
legend_y = margin_top + 10
|
||||
legend_x = margin_left + chart_width - 150
|
||||
legend_items = [
|
||||
("#3b82f6", "In Use"),
|
||||
("#ef4444", "High (>50%)"),
|
||||
("#f59e0b", "Medium (25-50%)"),
|
||||
("#06b6d4", "Low (10-25%)"),
|
||||
("#22c55e", "Idle (<10%)"),
|
||||
]
|
||||
|
||||
svg_parts.append(f' <rect x="{legend_x - 10}" y="{legend_y - 5}" width="160" height="{len(legend_items) * 18 + 10}" fill="white" stroke="#ddd" rx="4"/>')
|
||||
|
||||
for i, (color, label) in enumerate(legend_items):
|
||||
y = legend_y + 10 + i * 18
|
||||
svg_parts.append(f' <rect x="{legend_x}" y="{y - 8}" width="12" height="12" fill="{color}" opacity="0.7"/>')
|
||||
svg_parts.append(f' <text x="{legend_x + 18}" y="{y + 2}" class="legend">{label}</text>')
|
||||
|
||||
# Non-overlapping channels note (for 2.4 GHz)
|
||||
if non_overlap_channels:
|
||||
svg_parts.append(f' <text x="{margin_left + 5}" y="{margin_top + chart_height + 50}" class="label">Non-overlapping: Ch {", ".join(map(str, non_overlap_channels))}</text>')
|
||||
for ch in non_overlap_channels:
|
||||
if ch in channels:
|
||||
center_freq = channels[ch]['freq']
|
||||
x = freq_to_x(center_freq)
|
||||
svg_parts.append(f' <circle cx="{x}" cy="{margin_top + chart_height + 40}" r="8" fill="none" stroke="#22c55e" stroke-width="2"/>')
|
||||
svg_parts.append(f' <text x="{x}" y="{margin_top + chart_height + 44}" text-anchor="middle" class="channel-label" fill="#22c55e">{ch}</text>')
|
||||
else:
|
||||
svg_parts.append(f' <text x="{margin_left + 5}" y="{margin_top + chart_height + 50}" class="label">All channels non-overlapping (20 MHz spacing)</text>')
|
||||
|
||||
svg_parts.append('</svg>')
|
||||
return '\n'.join(svg_parts)
|
||||
|
||||
# Generate SVGs for each band
|
||||
svg_2_4 = generate_band_svg(channels_2_4, '2.4', 2400, 2485,
|
||||
"2.4 GHz WiFi Channel Overlap & Utilization",
|
||||
[1, 6, 11])
|
||||
svg_5 = generate_band_svg(channels_5, '5', 5150, 5850,
|
||||
"5 GHz WiFi Channel Utilization",
|
||||
None)
|
||||
|
||||
# Combine or output separately
|
||||
if svg_2_4 and svg_5:
|
||||
# Combine into one SVG with both bands stacked
|
||||
combined = f'''<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 820">
|
||||
<g transform="translate(0, 0)">
|
||||
{svg_2_4.replace('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 400">', '').replace('</svg>', '')}
|
||||
</g>
|
||||
<g transform="translate(0, 410)">
|
||||
{svg_5.replace('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 400">', '').replace('</svg>', '')}
|
||||
</g>
|
||||
</svg>'''
|
||||
svg_content = combined
|
||||
elif svg_2_4:
|
||||
svg_content = svg_2_4
|
||||
elif svg_5:
|
||||
svg_content = svg_5
|
||||
else:
|
||||
return None
|
||||
|
||||
if output_file:
|
||||
with open(output_file, 'w') as f:
|
||||
f.write(svg_content)
|
||||
print(f"SVG written to: {output_file}")
|
||||
else:
|
||||
print(svg_content)
|
||||
|
||||
return svg_content
|
||||
|
||||
|
||||
def draw_recommendations(survey_data, json_output=False):
|
||||
"""Analyze channels and provide recommendations"""
|
||||
# Parse channel data
|
||||
channels = {}
|
||||
in_use_channel = None
|
||||
|
||||
for ch_data in survey_data:
|
||||
freq = ch_data.get('frequency')
|
||||
ch_num = freq_to_channel(freq)
|
||||
if ch_num:
|
||||
busy_pct = get_busy_percentage(ch_data)
|
||||
channels[ch_num] = {
|
||||
'busy': busy_pct,
|
||||
'noise': ch_data.get('noise', -100),
|
||||
'in_use': ch_data.get('in-use', False)
|
||||
}
|
||||
if ch_data.get('in-use'):
|
||||
in_use_channel = ch_num
|
||||
|
||||
# Find least congested non-overlapping channels
|
||||
best_channels = []
|
||||
for ch in [1, 6, 11]:
|
||||
if ch in channels:
|
||||
best_channels.append((ch, channels[ch]['busy']))
|
||||
|
||||
best_channels.sort(key=lambda x: x[1])
|
||||
|
||||
# JSON output
|
||||
if json_output:
|
||||
output = {
|
||||
"recommended_channels": [
|
||||
{"channel": ch, "busy_percent": round(busy, 1)}
|
||||
for ch, busy in best_channels
|
||||
]
|
||||
}
|
||||
if in_use_channel:
|
||||
output["current_channel"] = in_use_channel
|
||||
output["current_busy_percent"] = round(channels.get(in_use_channel, {}).get('busy', 0), 1)
|
||||
|
||||
print(json.dumps(output, indent=2))
|
||||
return
|
||||
|
||||
# Text output
|
||||
print(f"\n{Colors.BOLD}Channel Recommendations{Colors.RESET}")
|
||||
print("=" * 80)
|
||||
|
||||
if in_use_channel:
|
||||
print(f"Current channel: {Colors.BOLD}{in_use_channel}{Colors.RESET}")
|
||||
current_busy = channels.get(in_use_channel, {}).get('busy', 0)
|
||||
if current_busy > 50:
|
||||
print(f" {Colors.RED}⚠{Colors.RESET} High congestion detected ({current_busy:.1f}% busy)")
|
||||
elif current_busy > 25:
|
||||
print(f" {Colors.YELLOW}⚠{Colors.RESET} Moderate congestion ({current_busy:.1f}% busy)")
|
||||
else:
|
||||
print(f" {Colors.GREEN}✓{Colors.RESET} Good channel utilization ({current_busy:.1f}% busy)")
|
||||
|
||||
print(f"\nRecommended non-overlapping channels (2.4 GHz):")
|
||||
for i, (ch, busy) in enumerate(best_channels[:3], 1):
|
||||
color = get_utilization_color(busy)
|
||||
marker = "★" if i == 1 else " "
|
||||
print(f" {marker} Channel {ch:2d}: {color}{busy:5.1f}% busy{Colors.RESET}")
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Visualize WiFi channel overlap and utilization',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog='''
|
||||
Examples:
|
||||
# Read from yanger output (show all sections)
|
||||
yanger -x "ixll -A ssh host sudo" ietf-hardware | %(prog)s
|
||||
|
||||
# Read from file
|
||||
%(prog)s survey_data.json
|
||||
|
||||
# Show only list view
|
||||
%(prog)s --list survey_data.json
|
||||
|
||||
# Show only specific sections
|
||||
%(prog)s --overlap survey_data.json
|
||||
%(prog)s --pie survey_data.json
|
||||
%(prog)s --utilization survey_data.json
|
||||
%(prog)s --recommendations survey_data.json
|
||||
%(prog)s --overlap --pie survey_data.json
|
||||
|
||||
# Output recommendations as JSON
|
||||
%(prog)s --recommendations --json survey_data.json
|
||||
|
||||
# Generate SVG image
|
||||
%(prog)s --svg /tmp/wifi-channels.svg survey_data.json
|
||||
%(prog)s --svg - survey_data.json > output.svg
|
||||
'''
|
||||
)
|
||||
parser.add_argument('file', nargs='?', help='JSON file with hardware data (default: stdin)')
|
||||
parser.add_argument('--list', action='store_true', help='Show simple list view instead of overlap graph')
|
||||
parser.add_argument('--no-color', action='store_true', help='Disable colors')
|
||||
parser.add_argument('--json', action='store_true', help='Output recommendations in JSON format')
|
||||
parser.add_argument('--svg', metavar='FILE', help='Generate SVG image to FILE (use - for stdout)')
|
||||
|
||||
# Section filters
|
||||
parser.add_argument('--overlap', action='store_true', help='Show only channel overlap visualization (2.4 GHz)')
|
||||
parser.add_argument('--pie', action='store_true', help='Show only channel group pie chart (2.4 GHz)')
|
||||
parser.add_argument('--utilization', action='store_true', help='Show only channel utilization list')
|
||||
parser.add_argument('--recommendations', action='store_true', help='Show only channel recommendations')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Disable colors if requested
|
||||
if args.no_color:
|
||||
for attr in dir(Colors):
|
||||
if not attr.startswith('_'):
|
||||
setattr(Colors, attr, '')
|
||||
|
||||
# Read input
|
||||
if args.file:
|
||||
with open(args.file, 'r') as f:
|
||||
data = json.load(f)
|
||||
else:
|
||||
data = json.load(sys.stdin)
|
||||
|
||||
# Extract survey data from hardware components
|
||||
survey_data = []
|
||||
hardware = data.get('ietf-hardware:hardware', {})
|
||||
components = hardware.get('component', [])
|
||||
|
||||
for component in components:
|
||||
if component.get('class') == 'infix-hardware:wifi':
|
||||
wifi_radio = component.get('infix-hardware:wifi-radio', {})
|
||||
survey = wifi_radio.get('survey', {})
|
||||
channels = survey.get('channel', [])
|
||||
if channels:
|
||||
survey_data.extend(channels)
|
||||
|
||||
if not survey_data:
|
||||
print("No WiFi survey data found in input", file=sys.stderr)
|
||||
print("Expected format: yanger ietf-hardware output with wifi-radio survey data", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Generate SVG if requested (exclusive mode)
|
||||
if args.svg:
|
||||
output_file = None if args.svg == '-' else args.svg
|
||||
generate_svg(survey_data, output_file)
|
||||
return
|
||||
|
||||
# Determine which sections to show
|
||||
# If no section flags are set, show all sections
|
||||
show_all = not (args.overlap or args.pie or args.utilization or args.recommendations)
|
||||
|
||||
show_overlap = show_all or args.overlap
|
||||
show_pie = show_all or args.pie
|
||||
show_utilization = show_all or args.utilization
|
||||
show_recommendations_section = show_all or args.recommendations
|
||||
|
||||
# Draw visualization
|
||||
freqs = [ch.get('frequency', 0) for ch in survey_data]
|
||||
has_2_4ghz = any(2400 <= f <= 2500 for f in freqs)
|
||||
|
||||
if show_overlap and has_2_4ghz and not args.list:
|
||||
draw_channel_graph_2_4ghz(survey_data)
|
||||
|
||||
if show_pie and has_2_4ghz:
|
||||
draw_overlap_pie(survey_data)
|
||||
|
||||
if show_utilization:
|
||||
draw_channel_list(survey_data)
|
||||
|
||||
if show_recommendations_section:
|
||||
draw_recommendations(survey_data, json_output=args.json)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -6,6 +6,20 @@ All notable changes to the project are documented in this file.
|
||||
[v26.01.0][UNRELEASED]
|
||||
-------------------------
|
||||
|
||||
> [!WARNING]
|
||||
> **BREAKING CHANGES:** This release includes breaking changes to WiFi configuration:
|
||||
>
|
||||
> - WiFi station/client configuration has been restructured. The `wifi` container
|
||||
> now requires a `radio` reference, and station configuration has moved under a
|
||||
> `wifi/station` container. Existing WiFi configurations must be manually updated.
|
||||
> - WiFi radios are now configured via `ietf-hardware` instead of the interfaces module.
|
||||
|
||||
> [!NOTE]
|
||||
> Noteworthy changes and additions in this release:
|
||||
>
|
||||
> - WiFi Access Point (AP) mode support with multi-SSID capability
|
||||
> - RIPv2 routing support
|
||||
|
||||
### Changes
|
||||
|
||||
- Upgrade Linux kernel to 6.12.63 (LTS)
|
||||
@@ -26,11 +40,20 @@ All notable changes to the project are documented in this file.
|
||||
policy, keeping snapshots from every 5 minutes (recent) to yearly (historical)
|
||||
- Add support data collection script, useful when troubleshooting issues on
|
||||
deployed systems. Gathers system information, logs, and more. Issue #1287
|
||||
- Add WiFi Access Point (AP) mode with multi-SSID support and WPA2/WPA3 security.
|
||||
**BREAKING:** WiFi architecture refactored with radios configured via
|
||||
`ietf-hardware` and interfaces requiring `radio` reference. Station config
|
||||
moved to `wifi/station` container. Existing Wi-Fi interfaces will be
|
||||
removed during upgrade (for the rest of the configuration to apply)
|
||||
and you need to reconfigure them again. See [wifi.md](wifi.md) for details
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix #1314: Raspberry Pi 4B with 1 or 8 GiB RAM does not boot. This was due
|
||||
newer EEPROM firmware in newer boards require a newer rpi-firmware package
|
||||
- Fix #1082: Wi-Fi interfaces always scanned, introduce a `scan-mode`
|
||||
to the Wi-Fi concept in Infix.
|
||||
|
||||
|
||||
[v25.11.0][] - 2025-12-02
|
||||
-------------------------
|
||||
|
||||
+367
-84
@@ -1,14 +1,29 @@
|
||||
# Wi-Fi (Wireless LAN)
|
||||
|
||||
Infix includes built-in Wi-Fi client support for connecting to
|
||||
wireless networks. When a compatible Wi-Fi adapter is detected, the
|
||||
system automatically begins scanning for available networks.
|
||||
Infix includes comprehensive Wi-Fi support for both client (Station) and
|
||||
Access Point modes. When a compatible Wi-Fi adapter is detected, the system
|
||||
automatically creates a WiFi radio (PHY) in factory-config, that can
|
||||
host virtual interfaces.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
Infix uses a two-layer WiFi architecture:
|
||||
|
||||
1. **WiFi Radio (PHY layer)**: Represents the physical wireless hardware
|
||||
- Configured via `ietf-hardware` module
|
||||
- Controls channel, transmit power, regulatory domain
|
||||
- One radio can host multiple virtual interfaces
|
||||
|
||||
2. **WiFi Interface (Network layer)**: Virtual interface on a radio
|
||||
- Configured via `infix-interfaces` module
|
||||
- Can operate in Station (client) or Access Point mode
|
||||
- Each interface references a parent radio
|
||||
|
||||
## Current Limitations
|
||||
|
||||
- Only client mode is supported (no access point functionality)
|
||||
- USB hotplug is not supported - adapters must be present at boot
|
||||
- Interface naming may be inconsistent with multiple USB Wi-Fi adapters
|
||||
- AP and Station modes cannot be mixed on the same radio
|
||||
|
||||
## Supported Wi-Fi Adapters
|
||||
|
||||
@@ -16,6 +31,8 @@ Wi-Fi support is primarily tested with Realtek chipset-based adapters.
|
||||
|
||||
### Known Working Chipsets
|
||||
|
||||
- Built-in Wi-Fi on Banana Pi r3
|
||||
- Built-in Wi-Fi on Raspberry Pi 4/CM4
|
||||
- RTL8821CU
|
||||
- Other Realtek chipsets may work but are not guaranteed
|
||||
|
||||
@@ -24,35 +41,128 @@ Wi-Fi support is primarily tested with Realtek chipset-based adapters.
|
||||
> Firmware requirements vary by chipset
|
||||
> Check kernel logs if your adapter is not detected
|
||||
|
||||
## Configuration
|
||||
## Radio Configuration
|
||||
|
||||
Add a supported Wi-Fi network device. To verify that it has been
|
||||
detected, look for `wifi0` in `show interface`
|
||||
Before configuring WiFi interfaces, you must first configure the WiFi radio.
|
||||
Radios are automatically discovered and named `radio0`, `radio1`, etc.
|
||||
|
||||
### Country Code and Regulatory Compliance
|
||||
|
||||
> [!IMPORTANT]
|
||||
> The `country-code` setting is **legally required** and determines which WiFi channels and power levels are permitted in your location. Using an incorrect country code may violate local wireless regulations.
|
||||
|
||||
**Factory default**: Systems may ship with a default country code (typically "DE" for Germany in European builds or "00" for World domain). **You must configure the correct country code for your deployment location.**
|
||||
|
||||
**Common country codes**:
|
||||
- Europe: DE (Germany), SE (Sweden), GB (UK), FR (France), ES (Spain)
|
||||
- Americas: US (United States), CA (Canada), BR (Brazil)
|
||||
- Asia-Pacific: JP (Japan), AU (Australia), CN (China)
|
||||
|
||||
See [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) for the complete list.
|
||||
|
||||
### Basic Radio Setup
|
||||
|
||||
Configure the radio with channel, power, and regulatory domain.
|
||||
|
||||
**For Station (client) mode:**
|
||||
```
|
||||
admin@example:/> configure
|
||||
admin@example:/config/> edit hardware component radio0 wifi-radio
|
||||
admin@example:/config/hardware/component/radio0/wifi-radio/> set country-code DE
|
||||
admin@example:/config/hardware/component/radio0/wifi-radio/> leave
|
||||
```
|
||||
|
||||
**For Access Point mode:**
|
||||
```
|
||||
admin@example:/> configure
|
||||
admin@example:/config/> edit hardware component radio0 wifi-radio
|
||||
admin@example:/config/hardware/component/radio0/wifi-radio/> set country-code DE
|
||||
admin@example:/config/hardware/component/radio0/wifi-radio/> set band 5GHz
|
||||
admin@example:/config/hardware/component/radio0/wifi-radio/> set channel 36
|
||||
admin@example:/config/hardware/component/radio0/wifi-radio/> leave
|
||||
```
|
||||
|
||||
**Key radio parameters:**
|
||||
- `country-code`: Two-letter ISO 3166-1 code - determines allowed channels and maximum power. Examples: US, DE, GB, SE, FR, JP. **Must match your physical location for legal compliance.**
|
||||
- `band`: 2.4GHz, 5GHz, or 6GHz (required for AP mode). Band selection automatically enables appropriate WiFi standards (2.4GHz: 802.11n, 5GHz: 802.11n/ac, 6GHz: 802.11n/ac/ax)
|
||||
- `channel`: Channel number (1-196) or "auto" (required for AP mode). When set to "auto", defaults to channel 6 for 2.4GHz, channel 36 for 5GHz, or channel 109 for 6GHz
|
||||
- `enable-wifi6`: Boolean (default: false). Opt-in to enable WiFi 6 (802.11ax) on 2.4GHz and 5GHz bands. The 6GHz band always uses WiFi 6 regardless of this setting
|
||||
|
||||
> [!NOTE]
|
||||
> TX power and channel width are automatically determined by the driver based on regulatory constraints, PHY mode, and hardware capabilities.
|
||||
|
||||
### WiFi 6 (802.11ax) Support
|
||||
|
||||
WiFi 6 (802.11ax) provides improved performance in congested environments through
|
||||
features like OFDMA, Target Wake Time, and BSS Coloring. By default, WiFi 6 is
|
||||
only enabled on the 6GHz band (WiFi 6E requirement).
|
||||
|
||||
To enable WiFi 6 on 2.4GHz or 5GHz bands:
|
||||
|
||||
```
|
||||
admin@example:/> show interface
|
||||
INTERFACE PROTOCOL STATE DATA
|
||||
lo loopback UP
|
||||
ipv4 127.0.0.1/8 (static)
|
||||
ipv6 ::1/128 (static)
|
||||
e1 ethernet UP 02:00:00:00:00:01
|
||||
ipv6 fe80::ff:fe00:1/64 (link-layer)
|
||||
ipv6 fec0::ff:fe00:1/64 (link-layer)
|
||||
wifi0 ethernet DOWN f0:09:0d:36:5f:86
|
||||
wifi ssid: ------, signal: ------
|
||||
admin@example:/> configure
|
||||
admin@example:/config/> edit hardware component radio0 wifi-radio
|
||||
admin@example:/config/hardware/component/radio0/wifi-radio/> set country-code DE
|
||||
admin@example:/config/hardware/component/radio0/wifi-radio/> set band 5GHz
|
||||
admin@example:/config/hardware/component/radio0/wifi-radio/> set channel 36
|
||||
admin@example:/config/hardware/component/radio0/wifi-radio/> set enable-wifi6 true
|
||||
admin@example:/config/hardware/component/radio0/wifi-radio/> leave
|
||||
```
|
||||
|
||||
**WiFi 6 Benefits:**
|
||||
- **OFDMA**: Better multi-user efficiency in dense environments
|
||||
- **Target Wake Time**: Improved battery life for client devices
|
||||
- **1024-QAM**: Higher throughput with strong signal conditions
|
||||
- **BSS Coloring**: Reduced interference from neighboring networks
|
||||
|
||||
**Requirements:**
|
||||
- Hardware must support 802.11ax
|
||||
- Client devices must support WiFi 6 for full benefits
|
||||
- Older WiFi 5/4 clients can still connect but won't use WiFi 6 features
|
||||
|
||||
> [!NOTE]
|
||||
> The 6GHz band always uses WiFi 6 (802.11ax) regardless of the `enable-wifi6`
|
||||
> setting, as WiFi 6E requires 802.11ax support.
|
||||
|
||||
## Discovering Available Networks (Scanning)
|
||||
|
||||
Before connecting to a WiFi network, you need to discover which networks
|
||||
are available. Infix automatically scans for networks when a WiFi interface
|
||||
is created with a radio reference.
|
||||
|
||||
### Enable Background Scanning
|
||||
|
||||
To enable scanning without connecting, configure the radio and create a WiFi
|
||||
interface referencing it:
|
||||
|
||||
**Step 1: Configure the radio**
|
||||
|
||||
```
|
||||
Add the new Wi-Fi interface to the configuration to start scanning.
|
||||
admin@example:/> configure
|
||||
admin@example:/config/> edit hardware component radio0 wifi-radio
|
||||
admin@example:/config/hardware/component/radio0/wifi-radio/> set country-code DE
|
||||
admin@example:/config/hardware/component/radio0/wifi-radio/> leave
|
||||
```
|
||||
admin@example:/config/> set interface wifi0
|
||||
admin@example:/config/> leave
|
||||
```
|
||||
Now the system will now start scanning in the background. To
|
||||
see the result read the operational datastore for interface `wifi0` or
|
||||
use the CLI
|
||||
|
||||
**Step 2: Create WiFi interface with radio reference only**
|
||||
|
||||
```
|
||||
admin@infix-00-00-00:/> show interface wifi0
|
||||
admin@example:/> configure
|
||||
admin@example:/config/> edit interface wifi0
|
||||
admin@example:/config/interface/wifi0/> set wifi radio radio0
|
||||
admin@example:/config/interface/wifi0/> leave
|
||||
```
|
||||
|
||||
The system will now start scanning in the background. The interface will
|
||||
operate in scan-only mode until you configure a specific mode (station or
|
||||
access-point).
|
||||
|
||||
### View Available Networks
|
||||
|
||||
Use `show interface` to see discovered networks and their signal strength:
|
||||
|
||||
```
|
||||
admin@example:/> show interface wifi0
|
||||
name : wifi0
|
||||
type : wifi
|
||||
index : 3
|
||||
@@ -64,89 +174,262 @@ ipv6 addresses :
|
||||
SSID : ----
|
||||
Signal : ----
|
||||
|
||||
SSID ENCRYPTION SIGNAL
|
||||
ssid1 WPA2-Personal excellent
|
||||
ssid2 WPA2-Personal excellent
|
||||
ssid3 WPA2-Personal excellent
|
||||
ssid4 WPA2-Personal good
|
||||
ssid5 WPA2-Personal good
|
||||
ssid6 WPA2-Personal good
|
||||
SSID SECURITY SIGNAL
|
||||
MyNetwork WPA2-Personal excellent
|
||||
GuestWiFi WPA2-WPA3-Personal good
|
||||
CoffeeShop Open fair
|
||||
IoT-Devices WPA2-Personal good
|
||||
```
|
||||
|
||||
In the CLI, signal strength is reported as: excellent, good, poor or
|
||||
bad. For precise values, use NETCONF or RESTCONF, where the RSSI (in
|
||||
dBm) is available in the operational datastore.
|
||||
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.
|
||||
|
||||
Configure your Wi-Fi secret in the keystore, it should be between 8
|
||||
and 63 characters
|
||||
### Connect to a Network
|
||||
|
||||
Once you've identified the desired network from the scan results, configure
|
||||
station mode with the SSID and credentials. First, store your WiFi password
|
||||
in the keystore:
|
||||
|
||||
```
|
||||
admin@example:/> configure
|
||||
admin@example:/config/> edit keystore symmetric-key example
|
||||
admin@example:/config/keystore/…/example/> set key-format wifi-preshared-key-format
|
||||
admin@example:/config/keystore/…/example/> set cleartext-symmetric-key mysecret
|
||||
admin@example:/config/keystore/…/example/> leave
|
||||
admin@example:/>
|
||||
admin@example:/config/> edit keystore symmetric-key my-wifi-key
|
||||
admin@example:/config/keystore/…/my-wifi-key/> set key-format wifi-preshared-key-format
|
||||
admin@example:/config/keystore/…/my-wifi-key/> set symmetric-key YourWiFiPassword
|
||||
admin@example:/config/keystore/…/my-wifi-key/> leave
|
||||
```
|
||||
|
||||
Configure the Wi-Fi settings, set secret to the name selected above
|
||||
for the symmetric key, in this case `example`.
|
||||
|
||||
WPA2 or WPA3 encryption will be automatically selected based on what
|
||||
the access point supports. No manual selection is required unless
|
||||
connecting to an open network. No support for certificate based
|
||||
authentication yet.
|
||||
|
||||
Unencrypted network is also supported, to connect to an unencrypted
|
||||
network (generally not recommended):
|
||||
```
|
||||
admin@example:/config/interface/wifi0/> set wifi encryption disabled
|
||||
```
|
||||
|
||||
A valid `country-code` is also required for regulatory compliance, the
|
||||
valid codes are documented in the YANG model `infix-wifi-country-codes`
|
||||
|
||||
Then configure the WiFi interface for station mode:
|
||||
|
||||
```
|
||||
admin@example:/> configure
|
||||
admin@example:/config/> edit interface wifi0
|
||||
admin@example:/config/interface/wifi0/>
|
||||
admin@example:/config/interface/wifi0/> set wifi ssid ssid1
|
||||
admin@example:/config/interface/wifi0/> set wifi secret example
|
||||
admin@example:/config/interface/wifi0/> set wifi country-code SE
|
||||
admin@example:/config/interface/wifi0/> set wifi station ssid MyNetwork
|
||||
admin@example:/config/interface/wifi0/> set wifi station security secret my-wifi-key
|
||||
admin@example:/config/interface/wifi0/> leave
|
||||
```
|
||||
|
||||
The Wi-Fi negotiation should now start immediately, provided that the
|
||||
SSID and pre-shared key are correct. You can verify the connection by
|
||||
running `show interface` again.
|
||||
The interface will transition from scan-only mode to station mode and
|
||||
attempt to connect to the specified network.
|
||||
|
||||
## Station Mode (Client)
|
||||
|
||||
Station mode connects to an existing Wi-Fi network. Before configuring station
|
||||
mode, follow the "Discovering Available Networks (Scanning)" section above to
|
||||
scan for available networks and identify the SSID you want to connect to.
|
||||
|
||||
### Step 1: Configure WiFi Password
|
||||
|
||||
Create a keystore entry for your WiFi password (8-63 characters):
|
||||
|
||||
```
|
||||
admin@example:/> show interface
|
||||
INTERFACE PROTOCOL STATE DATA
|
||||
lo loopback UP
|
||||
ipv4 127.0.0.1/8 (static)
|
||||
ipv6 ::1/128 (static)
|
||||
e1 ethernet UP 02:00:00:00:00:01
|
||||
ipv6 fe80::ff:fe00:1/64 (link-layer)
|
||||
ipv6 fec0::ff:fe00:1/64 (link-layer)
|
||||
wifi0 ethernet UP f0:09:0d:36:5f:86
|
||||
wifi ssid: ssid1, signal: excellent
|
||||
admin@example:/> configure
|
||||
admin@example:/config/> edit keystore symmetric-key my-wifi-key
|
||||
admin@example:/config/keystore/…/my-wifi-key/> set key-format wifi-preshared-key-format
|
||||
admin@example:/config/keystore/…/my-wifi-key/> set symmetric-key MyPassword123
|
||||
admin@example:/config/keystore/…/my-wifi-key/> leave
|
||||
```
|
||||
|
||||
admin@example:/>
|
||||
### Step 2: Connect to Network
|
||||
|
||||
Configure station mode with the SSID and password to connect:
|
||||
|
||||
```
|
||||
admin@example:/> configure
|
||||
admin@example:/config/> edit interface wifi0
|
||||
admin@example:/config/interface/wifi0/> set wifi station ssid MyHomeNetwork
|
||||
admin@example:/config/interface/wifi0/> set wifi station security secret my-wifi-key
|
||||
admin@example:/config/interface/wifi0/> leave
|
||||
```
|
||||
|
||||
The connection attempt will start immediately. You can verify the connection status:
|
||||
|
||||
```
|
||||
admin@example:/> show interface wifi0
|
||||
name : wifi0
|
||||
type : wifi
|
||||
operational status : up
|
||||
physical address : f0:09:0d:36:5f:86
|
||||
SSID : MyHomeNetwork
|
||||
Signal : excellent
|
||||
```
|
||||
|
||||
**Station configuration parameters:**
|
||||
- `radio`: Reference to the WiFi radio (mandatory) - already set during scanning
|
||||
- `station ssid`: Network name to connect to (mandatory)
|
||||
- `station security mode`: `auto` (default, WPA2/WPA3 auto-negotiation) or `disabled` (open network)
|
||||
- `station security secret`: Reference to keystore entry (required unless mode is `disabled`)
|
||||
|
||||
> [!NOTE]
|
||||
> The `auto` security mode automatically selects WPA3-SAE or WPA2-PSK based on
|
||||
> what the access point supports, prioritizing WPA3 for better security.
|
||||
> Certificate-based authentication (802.1X/EAP) is not yet supported.
|
||||
|
||||
## Access Point Mode
|
||||
|
||||
Access Point (AP) mode allows your device to create a WiFi network that
|
||||
other devices can connect to. APs are configured as virtual interfaces on
|
||||
a WiFi radio.
|
||||
|
||||
### Basic AP Configuration
|
||||
|
||||
First, ensure the radio is configured (see Radio Configuration above). Then
|
||||
create an AP interface:
|
||||
|
||||
```
|
||||
admin@example:/> configure
|
||||
admin@example:/config/> edit interface wifi0
|
||||
admin@example:/config/interface/wifi0/> set wifi radio radio0
|
||||
admin@example:/config/interface/wifi0/> set wifi access-point ssid MyNetwork
|
||||
admin@example:/config/interface/wifi0/> set wifi access-point security mode wpa2-personal
|
||||
admin@example:/config/interface/wifi0/> set wifi access-point security secret example
|
||||
admin@example:/config/interface/wifi0/> leave
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Using `wifiN` as the interface name automatically sets the type to WiFi.
|
||||
> Alternatively, you can use any name and explicitly set `type wifi`.
|
||||
|
||||
**Access Point configuration parameters:**
|
||||
- `radio`: Reference to the WiFi radio (mandatory)
|
||||
- `access-point ssid`: Network name (SSID) to broadcast
|
||||
- `access-point hidden`: Set to `true` to hide SSID (optional, default: false)
|
||||
- `access-point security mode`: Security mode (see below)
|
||||
- `access-point security secret`: Reference to keystore entry (for secured networks)
|
||||
|
||||
**Security modes:**
|
||||
- `open`: No encryption (not recommended)
|
||||
- `wpa2-personal`: WPA2-PSK (most compatible)
|
||||
- `wpa3-personal`: WPA3-SAE (more secure, requires WPA3-capable clients)
|
||||
- `wpa2-wpa3-personal`: Mixed mode (maximum compatibility)
|
||||
|
||||
### Hidden Network (SSID Hiding)
|
||||
|
||||
To create a hidden network that doesn't broadcast its SSID:
|
||||
|
||||
```
|
||||
admin@example:/config/interface/wifi0/> set wifi access-point hidden true
|
||||
```
|
||||
|
||||
### Multi-SSID Configuration
|
||||
|
||||
Multiple AP interfaces on the same radio allow broadcasting multiple SSIDs,
|
||||
each with independent security settings. This is useful for guest networks,
|
||||
IoT devices, or segregating traffic into different VLANs.
|
||||
|
||||
**Step 1: Configure the radio** (shared by all APs)
|
||||
|
||||
```
|
||||
admin@example:/> configure
|
||||
admin@example:/config/> edit hardware component radio0 wifi-radio
|
||||
admin@example:/config/hardware/component/radio0/wifi-radio/> set country-code DE
|
||||
admin@example:/config/hardware/component/radio0/wifi-radio/> set band 5GHz
|
||||
admin@example:/config/hardware/component/radio0/wifi-radio/> set channel 36
|
||||
admin@example:/config/hardware/component/radio0/wifi-radio/> leave
|
||||
```
|
||||
|
||||
**Step 2: Configure keystore secrets**
|
||||
|
||||
```
|
||||
admin@example:/> configure
|
||||
admin@example:/config/> edit keystore symmetric-key main-secret
|
||||
admin@example:/config/keystore/…/main-secret/> set key-format wifi-preshared-key-format
|
||||
admin@example:/config/keystore/…/main-secret/> set symmetric-key MyMainPassword
|
||||
admin@example:/config/> edit keystore symmetric-key guest-secret
|
||||
admin@example:/config/keystore/…/guest-secret/> set key-format wifi-preshared-key-format
|
||||
admin@example:/config/keystore/…/guest-secret/> set symmetric-key GuestPassword123
|
||||
admin@example:/config/> edit keystore symmetric-key iot-secret
|
||||
admin@example:/config/keystore/…/iot-secret/> set key-format wifi-preshared-key-format
|
||||
admin@example:/config/keystore/…/iot-secret/> set symmetric-key IoTDevices2025
|
||||
admin@example:/config/keystore/…/iot-secret/> leave
|
||||
```
|
||||
|
||||
**Step 3: Create multiple AP interfaces** (all on radio0)
|
||||
|
||||
```
|
||||
admin@example:/> configure
|
||||
# Primary AP - Main network (WPA3 for maximum security)
|
||||
admin@example:/config/> edit interface wifi0
|
||||
admin@example:/config/interface/wifi0/> set wifi radio radio0
|
||||
admin@example:/config/interface/wifi0/> set wifi access-point ssid MainNetwork
|
||||
admin@example:/config/interface/wifi0/> set wifi access-point security mode wpa3-personal
|
||||
admin@example:/config/interface/wifi0/> set wifi access-point security secret main-secret
|
||||
|
||||
# Guest AP - Guest network (WPA2/WPA3 mixed for compatibility)
|
||||
admin@example:/config/> edit interface wifi1
|
||||
admin@example:/config/interface/wifi1/> set wifi radio radio0
|
||||
admin@example:/config/interface/wifi1/> set wifi access-point ssid GuestNetwork
|
||||
admin@example:/config/interface/wifi1/> set wifi access-point security mode wpa2-wpa3-personal
|
||||
admin@example:/config/interface/wifi1/> set wifi access-point security secret guest-secret
|
||||
admin@example:/config/interface/wifi1/> set custom-phys-address static 00:0c:43:26:60:01
|
||||
|
||||
# IoT AP - IoT devices (WPA2 for older device compatibility)
|
||||
admin@example:/config/> edit interface wifi2
|
||||
admin@example:/config/interface/wifi2/> set wifi radio radio0
|
||||
admin@example:/config/interface/wifi2/> set wifi access-point ssid IoT-Devices
|
||||
admin@example:/config/interface/wifi2/> set wifi access-point security mode wpa2-personal
|
||||
admin@example:/config/interface/wifi2/> set wifi access-point security secret iot-secret
|
||||
admin@example:/config/interface/wifi2/> set custom-phys-address static 00:0c:43:26:60:02
|
||||
admin@example:/config/interface/wifi2/> leave
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **MAC Address Requirement for Multi-SSID:**
|
||||
> When creating multiple AP interfaces on the same radio, you **must** configure
|
||||
> a unique MAC address for each secondary interface (wifi1, wifi2, etc.) using
|
||||
> `set custom-phys-address static <MAC>`. All interfaces on the same radio inherit
|
||||
> the radio's hardware MAC address by default, which causes network conflicts. Only
|
||||
> the primary interface (alphabetically first, e.g., wifi0) should use the default
|
||||
> hardware MAC address.
|
||||
>
|
||||
> Choose MAC addresses from the same locally-administered range:
|
||||
> - Primary (wifi0): Uses hardware MAC (e.g., `00:0c:43:26:60:00`)
|
||||
> - Secondary (wifi1): `00:0c:43:26:60:01` (increment last octet)
|
||||
> - Tertiary (wifi2): `00:0c:43:26:60:02` (increment last octet)
|
||||
|
||||
**Result:** Three SSIDs broadcasting simultaneously on radio0:
|
||||
- `MainNetwork` (WPA3, most secure)
|
||||
- `GuestNetwork` (WPA2/WPA3 mixed mode)
|
||||
- `IoT-Devices` (WPA2 for compatibility)
|
||||
|
||||
All APs on the same radio share the same channel and physical layer settings
|
||||
(configured at the radio level). Each AP can have its own:
|
||||
- SSID (network name)
|
||||
- Security mode and passphrase
|
||||
- Hidden/visible SSID setting
|
||||
- Bridge membership
|
||||
|
||||
You can verify the configuration with `show hardware component radio0` to see
|
||||
radio settings, and `show interface` to see all active AP interfaces.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> AP and Station modes cannot be mixed on the same radio. All virtual interfaces
|
||||
> on a radio must be the same mode (all APs or all Stations).
|
||||
|
||||
### AP as Bridge Port
|
||||
|
||||
WiFi AP interfaces can be added to bridges to integrate wireless devices
|
||||
into your LAN:
|
||||
|
||||
```
|
||||
admin@example:/> configure
|
||||
admin@example:/config/> edit interface br0
|
||||
admin@example:/config/interface/br0/> set type bridge
|
||||
|
||||
admin@example:/config/> edit interface wifi0
|
||||
admin@example:/config/interface/wifi0/> set bridge-port bridge br0
|
||||
admin@example:/config/interface/wifi0/> leave
|
||||
```
|
||||
|
||||
## Troubleshooting Connection Issues
|
||||
|
||||
Use `show wifi scan wifi0` and `show interface` to verify signal strength
|
||||
and connection status. If issues arise, try the following
|
||||
troubleshooting steps:
|
||||
Use `show interface wifi0` to verify signal strength and connection status.
|
||||
If issues arise, try the following troubleshooting steps:
|
||||
|
||||
1. **Verify signal strength**: Check that the target network shows "good" or "excellent" signal
|
||||
2. **Check credentials**: Verify the preshared key in `ietf-keystore`
|
||||
1. **Verify signal strength**: Check that the target network shows "good" or "excellent" signal in scan results
|
||||
2. **Check credentials**: Verify the preshared key in the keystore matches the network password
|
||||
3. **Review logs**: Check system logs with `show log` for Wi-Fi related errors
|
||||
4. **Regulatory compliance**: Ensure the country-code matches your location
|
||||
5. **Hardware detection**: Confirm the adapter appears in `show interface`
|
||||
4. **Regulatory compliance**: Ensure the country-code on the radio matches your location
|
||||
5. **Hardware detection**: Confirm the WiFi radio appears in `show hardware`
|
||||
|
||||
If issues persist, check the system log for specific error messages that can help identify the root cause.
|
||||
|
||||
@@ -6,6 +6,10 @@ config BR2_PACKAGE_FEATURE_WIFI
|
||||
select BR2_PACKAGE_WPA_SUPPLICANT_AUTOSCAN
|
||||
select BR2_PACKAGE_WPA_SUPPLICANT_CLI
|
||||
select BR2_PACKAGE_WIRELESS_REGDB
|
||||
select BR2_PACKAGE_HOSTAPD
|
||||
select BR2_PACKAGE_HOSTAPD_DRIVER_NL80211
|
||||
select BR2_PACKAGE_HOSTAPD_WPA3
|
||||
select BR2_PACKAGE_HOSTAPD_WPS
|
||||
select BR2_PACKAGE_IW
|
||||
help
|
||||
Enables WiFi in Infix. Enables all requried applications.
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
#
|
||||
########################################################################
|
||||
# The system factory-config, failure-config and test-config are derived
|
||||
# from default settings snippets, from /usr/share/confd/factory.d, and
|
||||
# some generated snippets, e.g., hostname (based on base MAC address)
|
||||
# from default settings snippets, from /usr/share/confd/factory.d, and
|
||||
# some generated snippets, e.g., hostname (based on base MAC address)
|
||||
# and number of interfaces.
|
||||
#
|
||||
# The resulting factory-config is used to create the syrepo db (below)
|
||||
@@ -147,7 +147,7 @@ gen_test_cfg()
|
||||
|
||||
# Both factory-config and failure-config are generated every boot
|
||||
# regardless if there is a static /etc/factory-config.cfg or not.
|
||||
gen_factory_cfg
|
||||
gen_factory_cfg
|
||||
gen_failure_cfg
|
||||
|
||||
if [ -f "/mnt/aux/test-mode" ]; then
|
||||
|
||||
@@ -6,6 +6,8 @@ if jq -e '.["usb-ports"]' /run/system.json > /dev/null; then
|
||||
else
|
||||
usb_ports=""
|
||||
fi
|
||||
wifi_radios=$(/usr/libexec/infix/iw.py list 2>/dev/null | jq -r '.[]' || echo "")
|
||||
|
||||
|
||||
gen_port()
|
||||
{
|
||||
@@ -20,6 +22,43 @@ gen_port()
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
gen_radio()
|
||||
{
|
||||
radio="$1"
|
||||
|
||||
# Detect supported bands from iw.py info JSON output
|
||||
phy_info=$(/usr/libexec/infix/iw.py info "$radio" 2>/dev/null || echo '{"bands":[]}')
|
||||
# Check if 2.4GHz band exists (band name "2.4GHz")
|
||||
has_2ghz=$(echo "$phy_info" | jq '[.bands[] | select(.name == "2.4GHz")] | length')
|
||||
# Check if 5GHz band exists (band name "5GHz")
|
||||
has_5ghz=$(echo "$phy_info" | jq '[.bands[] | select(.name == "5GHz")] | length')
|
||||
|
||||
# Determine band setting
|
||||
# If both bands supported, prefer 5GHz for better performance
|
||||
if [ "$has_2ghz" -gt 0 ] && [ "$has_5ghz" -gt 0 ]; then
|
||||
band="5GHz"
|
||||
elif [ "$has_5ghz" -gt 0 ]; then
|
||||
band="5GHz"
|
||||
elif [ "$has_2ghz" -gt 0 ]; then
|
||||
band="2.4GHz"
|
||||
else
|
||||
band="2.4GHz" # Fallback to 2.4GHz for maximum compatibility
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"name": "$radio",
|
||||
"class": "infix-hardware:wifi",
|
||||
"infix-hardware:wifi-radio": {
|
||||
"country-code": "00",
|
||||
"band": "$band",
|
||||
"channel": "auto"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
first=1
|
||||
cat <<EOF
|
||||
{
|
||||
@@ -33,6 +72,13 @@ for port in $usb_ports; do
|
||||
first=0;
|
||||
gen_port "$port"
|
||||
done
|
||||
for radio in $wifi_radios; do
|
||||
if [ $first -eq 0 ]; then
|
||||
echo -n ','
|
||||
fi
|
||||
first=0;
|
||||
gen_radio $radio
|
||||
done
|
||||
cat <<EOF
|
||||
]}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
#!/bin/sh
|
||||
# Migrate keystore symmetric key syntax to new YANG schema (RFC 9643)
|
||||
# The 'cleartext-key' leaf has been renamed to 'cleartext-symmetric-key'
|
||||
# Old: symmetric-key[]/key-type/cleartext-key
|
||||
# New: symmetric-key[]/key-type/cleartext-symmetric-key
|
||||
# Rename cleartext-symmetric-key to symmetric-key
|
||||
|
||||
file=$1
|
||||
temp=${file}.tmp
|
||||
@@ -14,7 +11,7 @@ if .["ietf-keystore:keystore"]?."symmetric-keys"?."symmetric-key" then
|
||||
# Rename cleartext-key to cleartext-symmetric-key
|
||||
."key-type"."cleartext-key" as $key_value |
|
||||
."key-type" |= (del(."cleartext-key") | . + {
|
||||
"cleartext-symmetric-key": $key_value
|
||||
"symmetric-key": $key_value
|
||||
})
|
||||
else
|
||||
.
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
# Remove interfaces of type infix-if-type:wifi
|
||||
# Wi-Fi support has been refactored and all radio
|
||||
# settings have been moved to ietf-hardware.
|
||||
file=$1
|
||||
temp=${file}.tmp
|
||||
|
||||
jq '
|
||||
if .["ietf-interfaces:interfaces"]?.interface then
|
||||
.["ietf-interfaces:interfaces"].interface |= map(
|
||||
select(.type != "infix-if-type:wifi")
|
||||
)
|
||||
else
|
||||
.
|
||||
end
|
||||
' "$file" > "$temp" && mv "$temp" "$file"
|
||||
@@ -1,3 +1,4 @@
|
||||
migratedir = $(pkgdatadir)/migrate/1.7
|
||||
dist_migrate_DATA = 10-netconf-server-tcp-params.sh \
|
||||
20-keystore-cleartext-key-rename.sh
|
||||
20-keystore-cleartext-key-rename.sh \
|
||||
30-remove-wifi-interfaces.sh
|
||||
|
||||
@@ -138,6 +138,128 @@ static confd_dependency_t handle_dependencies(struct lyd_node **diff, struct lyd
|
||||
}
|
||||
}
|
||||
|
||||
/* When WiFi interfaces change, add their radios to the diff */
|
||||
struct lyd_node *difs = lydx_get_descendant(*diff, "interfaces", "interface", NULL);
|
||||
if (difs) {
|
||||
struct lyd_node *dif;
|
||||
|
||||
LYX_LIST_FOR_EACH(difs, dif, "interface") {
|
||||
struct lyd_node *dwifi, *radio_node;
|
||||
const char *ifname, *radio_name;
|
||||
char xpath[256];
|
||||
|
||||
/* Check if this interface has a wifi container in the diff */
|
||||
dwifi = lydx_get_child(dif, "wifi");
|
||||
if (!dwifi)
|
||||
continue;
|
||||
|
||||
/* Get the interface name */
|
||||
ifname = lydx_get_cattr(dif, "name");
|
||||
if (!ifname)
|
||||
continue;
|
||||
|
||||
/* Get radio reference from config tree using xpath */
|
||||
radio_node = lydx_get_xpathf(config,
|
||||
"/ietf-interfaces:interfaces/interface[name='%s']/infix-interfaces:wifi/radio",
|
||||
ifname);
|
||||
if (!radio_node)
|
||||
continue;
|
||||
|
||||
radio_name = lyd_get_value(radio_node);
|
||||
if (!radio_name)
|
||||
continue;
|
||||
|
||||
/* Add the radio to the diff */
|
||||
snprintf(xpath, sizeof(xpath), "/ietf-hardware:hardware/component[name='%s']/infix-hardware:wifi-radio", radio_name);
|
||||
result = add_dependencies(diff, xpath, radio_name);
|
||||
if (result == CONFD_DEP_ERROR) {
|
||||
ERROR("Failed to add radio %s to diff for WiFi interface %s", radio_name, ifname);
|
||||
return result;
|
||||
}
|
||||
|
||||
DEBUG("Added radio %s to diff for WiFi interface %s", radio_name, ifname);
|
||||
}
|
||||
}
|
||||
|
||||
/* When WiFi radios change, add all interfaces using that radio to the diff */
|
||||
struct ly_set *radios = lydx_find_xpathf(*diff,
|
||||
"/ietf-hardware:hardware/component[infix-hardware:wifi-radio]");
|
||||
if (radios && radios->count > 0) {
|
||||
for (uint32_t i = 0; i < radios->count; i++) {
|
||||
struct lyd_node *hradio = radios->dnodes[i];
|
||||
const char *radio_name = lydx_get_cattr(hradio, "name");
|
||||
struct ly_set *ifaces;
|
||||
uint32_t j;
|
||||
char xpath[256];
|
||||
|
||||
if (!radio_name)
|
||||
continue;
|
||||
|
||||
/* Find all interfaces that reference this radio */
|
||||
ifaces = lydx_find_xpathf(config,
|
||||
"/ietf-interfaces:interfaces/interface[infix-interfaces:wifi/radio='%s']",
|
||||
radio_name);
|
||||
if (ifaces && ifaces->count > 0) {
|
||||
for (j = 0; j < ifaces->count; j++) {
|
||||
const char *ifname = lydx_get_cattr(ifaces->dnodes[j], "name");
|
||||
|
||||
/* Add the wifi container */
|
||||
snprintf(xpath, sizeof(xpath),
|
||||
"/ietf-interfaces:interfaces/interface[name='%s']/infix-interfaces:wifi",
|
||||
ifname);
|
||||
result = add_dependencies(diff, xpath, ifname);
|
||||
if (result == CONFD_DEP_ERROR) {
|
||||
ERROR("Failed to add interface wifi for %s (radio %s)", ifname, radio_name);
|
||||
ly_set_free(ifaces, NULL);
|
||||
ly_set_free(radios, NULL);
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Add the radio leaf */
|
||||
snprintf(xpath, sizeof(xpath),
|
||||
"/ietf-interfaces:interfaces/interface[name='%s']/infix-interfaces:wifi/radio",
|
||||
ifname);
|
||||
result = add_dependencies(diff, xpath, radio_name);
|
||||
if (result == CONFD_DEP_ERROR) {
|
||||
ERROR("Failed to add radio leaf for interface %s (radio %s)", ifname, radio_name);
|
||||
ly_set_free(ifaces, NULL);
|
||||
ly_set_free(radios, NULL);
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Add station or access-point container depending on mode */
|
||||
if (lydx_get_descendant(ifaces->dnodes[j], "interface", "wifi", "station", NULL)) {
|
||||
snprintf(xpath, sizeof(xpath),
|
||||
"/ietf-interfaces:interfaces/interface[name='%s']/infix-interfaces:wifi/station",
|
||||
ifname);
|
||||
result = add_dependencies(diff, xpath, ifname);
|
||||
if (result == CONFD_DEP_ERROR) {
|
||||
ERROR("Failed to add station for interface %s (radio %s)", ifname, radio_name);
|
||||
ly_set_free(ifaces, NULL);
|
||||
ly_set_free(radios, NULL);
|
||||
return result;
|
||||
}
|
||||
} else if (lydx_get_descendant(ifaces->dnodes[j], "interface", "wifi", "access-point", NULL)) {
|
||||
snprintf(xpath, sizeof(xpath),
|
||||
"/ietf-interfaces:interfaces/interface[name='%s']/infix-interfaces:wifi/access-point",
|
||||
ifname);
|
||||
result = add_dependencies(diff, xpath, ifname);
|
||||
if (result == CONFD_DEP_ERROR) {
|
||||
ERROR("Failed to add access-point for interface %s (radio %s)", ifname, radio_name);
|
||||
ly_set_free(ifaces, NULL);
|
||||
ly_set_free(radios, NULL);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
DEBUG("Added interface %s to diff for radio %s", ifname, radio_name);
|
||||
}
|
||||
ly_set_free(ifaces, NULL);
|
||||
}
|
||||
}
|
||||
ly_set_free(radios, NULL);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
+606
-17
@@ -10,8 +10,14 @@
|
||||
#include <limits.h>
|
||||
|
||||
#include "core.h"
|
||||
#include "interfaces.h"
|
||||
#include "dagger.h"
|
||||
|
||||
#define XPATH_BASE_ "/ietf-hardware:hardware"
|
||||
#define XPATH_BASE_ "/ietf-hardware:hardware"
|
||||
#define HOSTAPD_CONF "/etc/hostapd-%s.conf"
|
||||
#define HOSTAPD_CONF_NEXT HOSTAPD_CONF"+"
|
||||
#define WPA_SUPPLICANT_CONF "/etc/wpa_supplicant-%s.conf"
|
||||
#define WPA_SUPPLICANT_CONF_NEXT WPA_SUPPLICANT_CONF"+"
|
||||
|
||||
static int dir_cb(const char *fpath, const struct stat *sb,
|
||||
int typeflag, struct FTW *ftwbuf)
|
||||
@@ -150,6 +156,452 @@ out_free_xpath:
|
||||
free(xpath);
|
||||
return err;
|
||||
}
|
||||
|
||||
|
||||
static int wifi_find_interfaces_on_radio(struct lyd_node *ifs, const char *radio_name,
|
||||
struct lyd_node ***iface_list, int *count)
|
||||
{
|
||||
struct lyd_node *iface, *wifi;
|
||||
const char *radio;
|
||||
struct lyd_node **list = NULL;
|
||||
int n = 0;
|
||||
|
||||
if (!ifs)
|
||||
return 0;
|
||||
|
||||
LYX_LIST_FOR_EACH(ifs, iface, "interface") {
|
||||
wifi = lydx_get_child(iface, "wifi");
|
||||
if (!wifi)
|
||||
continue;
|
||||
|
||||
radio = lydx_get_cattr(wifi, "radio");
|
||||
if (!radio || strcmp(radio, radio_name))
|
||||
continue;
|
||||
|
||||
if (lydx_get_op(iface) == LYDX_OP_DELETE)
|
||||
continue;
|
||||
list = realloc(list, sizeof(struct lyd_node *) * n + 1);
|
||||
list[n++] = iface;
|
||||
}
|
||||
|
||||
*iface_list = list;
|
||||
*count = n;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int wifi_gen_station(const char *ifname, struct lyd_node *station,
|
||||
const char *radio, struct lyd_node *config)
|
||||
{
|
||||
const char *ssid, *secret_name, *secret, *security_mode;
|
||||
struct lyd_node *security, *secret_node, *radio_node;
|
||||
FILE *wpa_supplicant = NULL;
|
||||
char *security_str = NULL;
|
||||
const char *country;
|
||||
int rc = SR_ERR_OK;
|
||||
|
||||
/* If station is NULL, we're in scan-only mode (no station container) */
|
||||
if (station) {
|
||||
ssid = lydx_get_cattr(station, "ssid");
|
||||
security = lydx_get_child(station, "security");
|
||||
security_mode = lydx_get_cattr(security, "mode");
|
||||
secret_name = lydx_get_cattr(security, "secret");
|
||||
} else {
|
||||
ssid = NULL;
|
||||
security = NULL;
|
||||
security_mode = "disabled";
|
||||
secret_name = NULL;
|
||||
}
|
||||
|
||||
radio_node = lydx_get_xpathf(config,
|
||||
"/hardware/component[name='%s']/wifi-radio", radio);
|
||||
country = radio_node ? lydx_get_cattr(radio_node, "country-code") : NULL;
|
||||
|
||||
if (secret_name && strcmp(security_mode, "disabled") != 0) {
|
||||
secret_node = lydx_get_xpathf(config,
|
||||
"/keystore/symmetric-keys/symmetric-key[name='%s']/symmetric-key",
|
||||
secret_name);
|
||||
secret = secret_node ? lyd_get_value(secret_node) : NULL;
|
||||
} else {
|
||||
secret = NULL;
|
||||
}
|
||||
|
||||
wpa_supplicant = fopenf("w", WPA_SUPPLICANT_CONF_NEXT, ifname);
|
||||
if (!wpa_supplicant) {
|
||||
rc = SR_ERR_INTERNAL;
|
||||
goto out;
|
||||
}
|
||||
|
||||
fprintf(wpa_supplicant,
|
||||
"ctrl_interface=/run/wpa_supplicant\n"
|
||||
"autoscan=periodic:10\n"
|
||||
"ap_scan=1\n");
|
||||
|
||||
if (country)
|
||||
fprintf(wpa_supplicant, "country=%s\n", country);
|
||||
|
||||
/* If SSID is present, create network block. Otherwise, scan-only mode */
|
||||
if (ssid) {
|
||||
/* Station mode with network configured */
|
||||
if (!strcmp(security_mode, "disabled")) {
|
||||
asprintf(&security_str, "key_mgmt=NONE");
|
||||
} else if (secret) {
|
||||
asprintf(&security_str, "key_mgmt=SAE WPA-PSK\npsk=\"%s\"", secret);
|
||||
}
|
||||
fprintf(wpa_supplicant,
|
||||
"network={\n"
|
||||
"bgscan=\"simple: 30:-45:300\"\n"
|
||||
"ssid=\"%s\"\n"
|
||||
"%s\n"
|
||||
"}\n", ssid, security_str);
|
||||
free(security_str);
|
||||
} else {
|
||||
/* Scan-only mode - no station container configured */
|
||||
fprintf(wpa_supplicant, "# Scan-only mode - no network configured\n");
|
||||
}
|
||||
|
||||
out:
|
||||
if (wpa_supplicant)
|
||||
fclose(wpa_supplicant);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Helper: Find all AP interfaces on a specific radio */
|
||||
static int wifi_find_radio_aps(struct lyd_node *cifs, const char *radio_name,
|
||||
char ***ap_list, int *count)
|
||||
{
|
||||
struct lyd_node *cif, *wifi, *ap;
|
||||
const char *ifname, *radio;
|
||||
char **list = NULL;
|
||||
int n = 0;
|
||||
|
||||
LYX_LIST_FOR_EACH(cifs, cif, "interface") {
|
||||
wifi = lydx_get_child(cif, "wifi");
|
||||
if (!wifi)
|
||||
continue;
|
||||
|
||||
radio = lydx_get_cattr(wifi, "radio");
|
||||
if (!radio || strcmp(radio, radio_name))
|
||||
continue;
|
||||
|
||||
ap = lydx_get_child(wifi, "access-point");
|
||||
if (!ap)
|
||||
continue;
|
||||
list = realloc(list, sizeof(char *) *n+1);
|
||||
|
||||
|
||||
ifname = lydx_get_cattr(cif, "name");
|
||||
list[n++] = strdup(ifname);
|
||||
}
|
||||
|
||||
/* Sort alphabetically for consistent primary selection */
|
||||
for (int i = 0; i < n - 1; i++) {
|
||||
for (int j = i + 1; j < n; j++) {
|
||||
if (strcmp(list[i], list[j]) > 0) {
|
||||
char *tmp = list[i];
|
||||
list[i] = list[j];
|
||||
list[j] = tmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*ap_list = list;
|
||||
*count = n;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Generate BSS section for secondary AP (multi-SSID) */
|
||||
static int wifi_gen_bss_section(FILE *hostapd, struct lyd_node *cifs, const char *ifname, struct lyd_node *config)
|
||||
{
|
||||
const char *ssid, *hidden, *security_mode, *secret_name, *secret;
|
||||
struct lyd_node *cif, *wifi, *ap, *security, *secret_node;
|
||||
|
||||
/* Find the interface node for this BSS */
|
||||
LYX_LIST_FOR_EACH(cifs, cif, "interface") {
|
||||
const char *name = lydx_get_cattr(cif, "name");
|
||||
if (strcmp(name, ifname) == 0)
|
||||
break;
|
||||
}
|
||||
|
||||
if (!cif) {
|
||||
ERROR("Failed to find interface %s for BSS section", ifname);
|
||||
return SR_ERR_INVAL_ARG;
|
||||
}
|
||||
|
||||
wifi = lydx_get_child(cif, "wifi");
|
||||
ap = lydx_get_child(wifi, "access-point");
|
||||
|
||||
fprintf(hostapd, "\n# BSS %s\n", ifname);
|
||||
fprintf(hostapd, "bss=%s\n", ifname);
|
||||
|
||||
/* SSID configuration */
|
||||
ssid = lydx_get_cattr(ap, "ssid");
|
||||
hidden = lydx_get_cattr(ap, "hidden");
|
||||
|
||||
if (ssid)
|
||||
fprintf(hostapd, "ssid=%s\n", ssid);
|
||||
if (hidden && !strcmp(hidden, "true"))
|
||||
fprintf(hostapd, "ignore_broadcast_ssid=1\n");
|
||||
|
||||
/* Security configuration */
|
||||
security = lydx_get_child(ap, "security");
|
||||
security_mode = lydx_get_cattr(security, "mode");
|
||||
|
||||
if (!security_mode)
|
||||
security_mode = "open";
|
||||
|
||||
/* Get secret from keystore if needed */
|
||||
secret = NULL;
|
||||
if (strcmp(security_mode, "open") != 0) {
|
||||
secret_name = lydx_get_cattr(security, "secret");
|
||||
if (secret_name) {
|
||||
secret_node = lydx_get_xpathf(config,
|
||||
"/keystore/symmetric-keys/symmetric-key[name='%s']/symmetric-key",
|
||||
secret_name);
|
||||
if (secret_node)
|
||||
secret = lyd_get_value(secret_node);
|
||||
}
|
||||
}
|
||||
|
||||
if (!strcmp(security_mode, "open")) {
|
||||
fprintf(hostapd, "# Open network\n");
|
||||
fprintf(hostapd, "auth_algs=1\n");
|
||||
} else if (!strcmp(security_mode, "wpa2-personal")) {
|
||||
fprintf(hostapd, "# WPA2-Personal\n");
|
||||
fprintf(hostapd, "wpa=2\n");
|
||||
fprintf(hostapd, "wpa_key_mgmt=WPA-PSK\n");
|
||||
fprintf(hostapd, "wpa_pairwise=CCMP\n");
|
||||
if (secret)
|
||||
fprintf(hostapd, "wpa_passphrase=%s\n", secret);
|
||||
} else if (!strcmp(security_mode, "wpa3-personal")) {
|
||||
fprintf(hostapd, "# WPA3-Personal\n");
|
||||
fprintf(hostapd, "wpa=2\n");
|
||||
fprintf(hostapd, "wpa_key_mgmt=SAE\n");
|
||||
fprintf(hostapd, "rsn_pairwise=CCMP\n");
|
||||
if (secret)
|
||||
fprintf(hostapd, "sae_password=%s\n", secret);
|
||||
fprintf(hostapd, "ieee80211w=2\n");
|
||||
} else if (!strcmp(security_mode, "wpa2-wpa3-personal")) {
|
||||
fprintf(hostapd, "# WPA2/WPA3 Mixed\n");
|
||||
fprintf(hostapd, "wpa=2\n");
|
||||
fprintf(hostapd, "wpa_key_mgmt=WPA-PSK SAE\n");
|
||||
fprintf(hostapd, "rsn_pairwise=CCMP\n");
|
||||
if (secret) {
|
||||
fprintf(hostapd, "wpa_passphrase=%s\n", secret);
|
||||
fprintf(hostapd, "sae_password=%s\n", secret);
|
||||
}
|
||||
fprintf(hostapd, "ieee80211w=1\n");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Generate hostapd config for all APs on a radio (multi-SSID support) */
|
||||
static int wifi_gen_aps_on_radio(const char *radio_name, struct lyd_node *cifs,
|
||||
struct lyd_node *radio_node, struct lyd_node *config)
|
||||
{
|
||||
const char *ssid, *hidden, *security_mode, *secret_name, *secret;
|
||||
struct lyd_node *primary_cif, *cif;
|
||||
struct lyd_node *primary_wifi, *primary_ap;
|
||||
struct lyd_node *security, *secret_node;
|
||||
const char *country, *channel, *band;
|
||||
const char *primary_ifname;
|
||||
char hostapd_conf[256];
|
||||
char **ap_list = NULL;
|
||||
FILE *hostapd = NULL;
|
||||
bool wifi6_enabled;
|
||||
int ap_count = 0;
|
||||
int i;
|
||||
|
||||
int rc = SR_ERR_OK;
|
||||
|
||||
wifi_find_radio_aps(cifs, radio_name, &ap_list, &ap_count);
|
||||
|
||||
if (ap_count == 0) {
|
||||
DEBUG("No APs found on radio %s", radio_name);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
DEBUG("Generating hostapd config for radio %s (%d APs)", radio_name, ap_count);
|
||||
|
||||
primary_ifname = ap_list[0];
|
||||
primary_cif = NULL;
|
||||
LYX_LIST_FOR_EACH(cifs, cif, "interface") {
|
||||
if (!strcmp(lydx_get_cattr(cif, "name"), primary_ifname)) {
|
||||
primary_cif = cif;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!primary_cif) {
|
||||
ERROR("Failed to find primary AP interface %s", primary_ifname);
|
||||
rc = SR_ERR_INVAL_ARG;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
primary_wifi = lydx_get_child(primary_cif, "wifi");
|
||||
primary_ap = lydx_get_child(primary_wifi, "access-point");
|
||||
|
||||
/* Get AP configuration */
|
||||
ssid = lydx_get_cattr(primary_ap, "ssid");
|
||||
hidden = lydx_get_cattr(primary_ap, "hidden");
|
||||
security = lydx_get_child(primary_ap, "security");
|
||||
security_mode = lydx_get_cattr(security, "mode");
|
||||
secret_name = lydx_get_cattr(security, "secret");
|
||||
secret = NULL;
|
||||
|
||||
/* Get radio configuration */
|
||||
country = lydx_get_cattr(radio_node, "country-code");
|
||||
band = lydx_get_cattr(radio_node, "band");
|
||||
channel = lydx_get_cattr(radio_node, "channel");
|
||||
wifi6_enabled = lydx_get_bool(radio_node, "enable_wifi6");
|
||||
|
||||
/* Get secret from keystore if not open network */
|
||||
if (secret_name && strcmp(security_mode, "open") != 0) {
|
||||
secret_node = lydx_get_xpathf(config,
|
||||
"/keystore/symmetric-keys/symmetric-key[name='%s']/symmetric-key",
|
||||
secret_name);
|
||||
if (secret_node) {
|
||||
secret = lyd_get_value(secret_node);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
snprintf(hostapd_conf, sizeof(hostapd_conf), HOSTAPD_CONF_NEXT, radio_name);
|
||||
|
||||
hostapd = fopen(hostapd_conf, "w");
|
||||
if (!hostapd) {
|
||||
ERROR("Failed to create hostapd config: %s", hostapd_conf);
|
||||
rc = SR_ERR_INTERNAL;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
fprintf(hostapd, "# Generated by Infix confd - WiFi Radio %s\n", radio_name);
|
||||
fprintf(hostapd, "# Primary BSS: %s", primary_ifname);
|
||||
if (ap_count > 1)
|
||||
fprintf(hostapd, " (%d total APs)\n\n", ap_count);
|
||||
else
|
||||
fprintf(hostapd, "\n\n");
|
||||
|
||||
fprintf(hostapd, "interface=%s\n", primary_ifname);
|
||||
fprintf(hostapd, "driver=nl80211\n");
|
||||
fprintf(hostapd, "ctrl_interface=/run/hostapd\n\n");
|
||||
|
||||
fprintf(hostapd, "ssid=%s\n", ssid);
|
||||
if (hidden && !strcmp(hidden, "true"))
|
||||
fprintf(hostapd, "ignore_broadcast_ssid=1\n");
|
||||
fprintf(hostapd, "\n");
|
||||
|
||||
if (country)
|
||||
fprintf(hostapd, "country_code=%s\n", country);
|
||||
|
||||
/* Enable 802.11d (regulatory domain) and 802.11h (spectrum management/DFS) */
|
||||
fprintf(hostapd, "ieee80211d=1\n");
|
||||
fprintf(hostapd, "ieee80211h=1\n");
|
||||
|
||||
/* Band and channel configuration */
|
||||
if (band) {
|
||||
/* Set hardware mode based on band */
|
||||
if (!strcmp(band, "2.4GHz")) {
|
||||
fprintf(hostapd, "hw_mode=g\n");
|
||||
} else if (!strcmp(band, "5GHz") || !strcmp(band, "6GHz")) {
|
||||
fprintf(hostapd, "hw_mode=a\n");
|
||||
}
|
||||
|
||||
/* Set channel */
|
||||
if (channel) {
|
||||
if (strcmp(channel, "auto") == 0) {
|
||||
/*
|
||||
Use default channels: 6 for 2.4GHz, 36 for 5GHz, 109 for 6GHz, this
|
||||
is a temporary hack, replace with logic for finding best free channel.
|
||||
*/
|
||||
if (!strcmp(band, "2.4GHz")) {
|
||||
fprintf(hostapd, "channel=6\n");
|
||||
} else if (!strcmp(band, "5GHz")) {
|
||||
fprintf(hostapd, "channel=36\n");
|
||||
} else if (!strcmp(band, "6GHz")) {
|
||||
fprintf(hostapd, "channel=109\n");
|
||||
} else {
|
||||
/* Unknown band - use ACS */
|
||||
fprintf(hostapd, "channel=0\n");
|
||||
}
|
||||
} else {
|
||||
fprintf(hostapd, "channel=%s\n", channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (band) {
|
||||
if (!strcmp(band, "2.4GHz")) {
|
||||
/* 2.4GHz: Enable 802.11n (HT), optionally WiFi 6 */
|
||||
fprintf(hostapd, "ieee80211n=1\n");
|
||||
if (wifi6_enabled) {
|
||||
fprintf(hostapd, "ieee80211ax=1\n");
|
||||
}
|
||||
} else if (!strcmp(band, "5GHz")) {
|
||||
/* 5GHz: Enable 802.11n and 802.11ac, optionally WiFi 6 */
|
||||
fprintf(hostapd, "ieee80211n=1\n");
|
||||
fprintf(hostapd, "ieee80211ac=1\n");
|
||||
if (wifi6_enabled) {
|
||||
fprintf(hostapd, "ieee80211ax=1\n");
|
||||
}
|
||||
} else if (!strcmp(band, "6GHz")) {
|
||||
/* 6GHz: Enable 802.11ax (WiFi 6E required) */
|
||||
fprintf(hostapd, "ieee80211n=1\n");
|
||||
fprintf(hostapd, "ieee80211ac=1\n");
|
||||
fprintf(hostapd, "ieee80211ax=1\n");
|
||||
}
|
||||
}
|
||||
fprintf(hostapd, "\n");
|
||||
|
||||
/* Security configuration */
|
||||
if (!strcmp(security_mode, "open")) {
|
||||
fprintf(hostapd, "# Open network (no encryption)\n");
|
||||
fprintf(hostapd, "auth_algs=1\n");
|
||||
} else if (!strcmp(security_mode, "wpa2-personal")) {
|
||||
fprintf(hostapd, "# WPA2-Personal\n");
|
||||
fprintf(hostapd, "wpa=2\n");
|
||||
fprintf(hostapd, "wpa_key_mgmt=WPA-PSK\n");
|
||||
fprintf(hostapd, "wpa_pairwise=CCMP\n");
|
||||
fprintf(hostapd, "wpa_passphrase=%s\n", secret);
|
||||
} else if (!strcmp(security_mode, "wpa3-personal")) {
|
||||
fprintf(hostapd, "# WPA3-Personal\n");
|
||||
fprintf(hostapd, "wpa=2\n");
|
||||
fprintf(hostapd, "wpa_key_mgmt=SAE\n");
|
||||
fprintf(hostapd, "rsn_pairwise=CCMP\n");
|
||||
fprintf(hostapd, "sae_password=%s\n", secret);
|
||||
fprintf(hostapd, "ieee80211w=2\n");
|
||||
} else if (!strcmp(security_mode, "wpa2-wpa3-personal")) {
|
||||
fprintf(hostapd, "# WPA2/WPA3 Mixed Mode\n");
|
||||
fprintf(hostapd, "wpa=2\n");
|
||||
fprintf(hostapd, "wpa_key_mgmt=WPA-PSK SAE\n");
|
||||
fprintf(hostapd, "rsn_pairwise=CCMP\n");
|
||||
fprintf(hostapd, "wpa_passphrase=%s\n", secret);
|
||||
fprintf(hostapd, "sae_password=%s\n", secret);
|
||||
fprintf(hostapd, "ieee80211w=1\n");
|
||||
}
|
||||
|
||||
/* Add BSS sections for secondary APs (multi-SSID) */
|
||||
for (i = 1; i < ap_count; i++) {
|
||||
DEBUG("Adding BSS section for secondary AP %s", ap_list[i]);
|
||||
rc = wifi_gen_bss_section(hostapd, cifs, ap_list[i], config);
|
||||
if (rc != SR_ERR_OK) {
|
||||
ERROR("Failed to generate BSS section for %s", ap_list[i]);
|
||||
fclose(hostapd);
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
fclose(hostapd);
|
||||
|
||||
cleanup:
|
||||
for (i = 0; i < ap_count; i++)
|
||||
free(ap_list[i]);
|
||||
free(ap_list);
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int hardware_cand(sr_session_ctx_t *session, uint32_t sub_id, const char *module,
|
||||
const char *xpath, sr_event_t event, unsigned request_id, void *priv)
|
||||
{
|
||||
@@ -187,49 +639,186 @@ static int hardware_cand(sr_session_ctx_t *session, uint32_t sub_id, const char
|
||||
|
||||
int hardware_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd)
|
||||
{
|
||||
struct lyd_node *difs = NULL, *dif = NULL, *cifs = NULL, *cif = NULL;
|
||||
struct lyd_node *difs = NULL, *dif = NULL;
|
||||
int rc = SR_ERR_OK;
|
||||
|
||||
if (event != SR_EV_DONE || !lydx_find_xpathf(diff, XPATH_BASE_))
|
||||
if (!lydx_find_xpathf(diff, XPATH_BASE_))
|
||||
return SR_ERR_OK;
|
||||
|
||||
cifs = lydx_get_descendant(config, "hardware", "component", NULL);
|
||||
difs = lydx_get_descendant(diff, "hardware", "component", NULL);
|
||||
|
||||
LYX_LIST_FOR_EACH(difs, dif, "component") {
|
||||
enum lydx_op op;
|
||||
struct lyd_node *state;
|
||||
struct lyd_node *state, *cif;
|
||||
const char *admin_state;
|
||||
const char *class, *name;
|
||||
|
||||
op = lydx_get_op(dif);
|
||||
name = lydx_get_cattr(dif, "name");
|
||||
if (op == LYDX_OP_DELETE) {
|
||||
if (usb_authorize(confd->root, name, 0)) {
|
||||
rc = SR_ERR_INTERNAL;
|
||||
goto err;;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
LYX_LIST_FOR_EACH(cifs, cif, "component") {
|
||||
if (strcmp(name, lydx_get_cattr(cif, "name")))
|
||||
/* Get the current config node for this component */
|
||||
cif = lydx_get_xpathf(config, "/hardware/component[name='%s']", name);
|
||||
if (!cif)
|
||||
continue;
|
||||
|
||||
class = lydx_get_cattr(cif, "class");
|
||||
|
||||
/* Handle USB components */
|
||||
if (!strcmp(class, "infix-hardware:usb")) {
|
||||
if (event != SR_EV_DONE)
|
||||
continue;
|
||||
|
||||
class = lydx_get_cattr(cif, "class");
|
||||
if (strcmp(class, "infix-hardware:usb")) {
|
||||
if (op == LYDX_OP_DELETE) {
|
||||
/* Handle USB deletion */
|
||||
if (usb_authorize(confd->root, name, 0)) {
|
||||
rc = SR_ERR_INTERNAL;
|
||||
goto err;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
state = lydx_get_child(dif, "state");
|
||||
admin_state = lydx_get_cattr(state, "admin-state");
|
||||
if (usb_authorize(confd->root, name, !strcmp(admin_state, "unlocked"))) {
|
||||
rc = SR_ERR_INTERNAL;
|
||||
goto err;;
|
||||
goto err;
|
||||
}
|
||||
} else if (!strcmp(class, "infix-hardware:wifi")) {
|
||||
struct lyd_node *interfaces_config, *interfaces_diff;
|
||||
struct lyd_node **wifi_iface_list = NULL;
|
||||
struct lyd_node *station, *ap;
|
||||
struct lyd_node *cwifi_radio;
|
||||
int wifi_iface_count = 0;
|
||||
char src[40], dst[40];
|
||||
int ap_interfaces = 0;
|
||||
|
||||
switch (event) {
|
||||
case SR_EV_ABORT:
|
||||
continue;
|
||||
case SR_EV_CHANGE:
|
||||
break;
|
||||
case SR_EV_DONE:
|
||||
interfaces_diff = lydx_get_descendant(diff, "interfaces", "interface", NULL);
|
||||
|
||||
wifi_find_interfaces_on_radio(interfaces_diff, name,
|
||||
&wifi_iface_list, &wifi_iface_count);
|
||||
if (wifi_iface_count > 0) {
|
||||
bool running, enabled;
|
||||
station = lydx_get_descendant(wifi_iface_list[0], "interface", "wifi", "station", NULL);
|
||||
ap = lydx_get_descendant(wifi_iface_list[0], "interface", "wifi", "access-point", NULL);
|
||||
|
||||
if (station || !ap || lydx_get_op(ap) == LYDX_OP_DELETE) {
|
||||
const char *ifname = lydx_get_cattr(wifi_iface_list[0], "name");
|
||||
running = !systemf("initctl -bfq status wpa_supplicant:%s", ifname);
|
||||
if (lydx_get_op(station) == LYDX_OP_DELETE) {
|
||||
erasef(WPA_SUPPLICANT_CONF, ifname);
|
||||
erasef(WPA_SUPPLICANT_CONF_NEXT, ifname);
|
||||
systemf("initctl -bfq disable wifi@%s", ifname);
|
||||
} else {
|
||||
snprintf(src, sizeof(src), WPA_SUPPLICANT_CONF_NEXT, ifname);
|
||||
snprintf(dst, sizeof(dst), WPA_SUPPLICANT_CONF, ifname);
|
||||
running = !systemf("initctl -bfq status wpa_supplicant:%s", ifname);
|
||||
enabled = fexistf(WPA_SUPPLICANT_CONF_NEXT, ifname);
|
||||
|
||||
if (enabled) {
|
||||
(void)rename(src, dst);
|
||||
if (running)
|
||||
systemf("initctl -bfq touch wifi@%s", ifname);
|
||||
else
|
||||
systemf("initctl -bfq enable wifi@%s", ifname);
|
||||
}
|
||||
}
|
||||
} else if (wifi_iface_count > 0) {
|
||||
/* AP mode - activate hostapd for radio */
|
||||
snprintf(src, sizeof(src), HOSTAPD_CONF_NEXT, name);
|
||||
snprintf(dst, sizeof(dst), HOSTAPD_CONF, name);
|
||||
|
||||
running = !systemf("initctl -bfq status hostapd:%s", name);
|
||||
enabled = fexistf(HOSTAPD_CONF_NEXT, name);
|
||||
|
||||
if (enabled) {
|
||||
(void)rename(src, dst);
|
||||
ap_interfaces++;
|
||||
|
||||
if (running)
|
||||
systemf("initctl -bfq touch hostapd@%s", name);
|
||||
else
|
||||
systemf("initctl -bfq enable hostapd@%s", name);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!ap_interfaces) {
|
||||
systemf("initctl -bfq disable hostapd@%s", name);
|
||||
erasef(HOSTAPD_CONF, name);
|
||||
erasef(HOSTAPD_CONF_NEXT, name);
|
||||
}
|
||||
free(wifi_iface_list);
|
||||
continue;
|
||||
default:
|
||||
continue;
|
||||
|
||||
}
|
||||
|
||||
cwifi_radio = lydx_get_child(cif, "wifi-radio");
|
||||
|
||||
interfaces_config = lydx_get_descendant(config, "interfaces", "interface", NULL);
|
||||
|
||||
wifi_find_interfaces_on_radio(interfaces_config, name,
|
||||
&wifi_iface_list, &wifi_iface_count);
|
||||
|
||||
|
||||
if (!wifi_iface_count)
|
||||
continue;
|
||||
/*
|
||||
* A radio operates in one of three modes:
|
||||
* 1. Station mode: One station interface (client mode)
|
||||
* 2. AP mode: One or more AP interfaces (hostapd multi-SSID)
|
||||
* 3. Scan-only mode: WiFi interface with radio but no mode configured
|
||||
*
|
||||
* Check for station first - there can be at most one per radio.
|
||||
* If no station or AP is configured, default to scan-only mode.
|
||||
*/
|
||||
station = lydx_get_descendant(wifi_iface_list[0], "interface", "wifi", "station", NULL);
|
||||
ap = lydx_get_descendant(wifi_iface_list[0], "interface", "wifi", "access-point", NULL);
|
||||
if (wifi_iface_count == 1 && station) {
|
||||
/* Station mode (with or without SSID for scan-only) */
|
||||
struct lyd_node *iface = wifi_iface_list[0];
|
||||
if (lydx_is_enabled(iface, "enabled")) {
|
||||
const char *ifname = lydx_get_cattr(iface, "name");
|
||||
rc = wifi_gen_station(ifname, station, name, config);
|
||||
if (rc != SR_ERR_OK) {
|
||||
ERROR("Failed to generate station config for %s", ifname);
|
||||
goto next;
|
||||
}
|
||||
}
|
||||
} else if (!station && !ap) {
|
||||
/* No station/AP configured - default to scan-only mode */
|
||||
struct lyd_node *iface = wifi_iface_list[0];
|
||||
if (lydx_is_enabled(iface, "enabled")) {
|
||||
const char *ifname = lydx_get_cattr(iface, "name");
|
||||
rc = wifi_gen_station(ifname, NULL, name, config);
|
||||
if (rc != SR_ERR_OK) {
|
||||
ERROR("Failed to generate scan-only config for %s", ifname);
|
||||
goto next;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* Multiple interfaces or APs */
|
||||
rc = wifi_gen_aps_on_radio(name, interfaces_config, cwifi_radio, config);
|
||||
if (rc != SR_ERR_OK) {
|
||||
ERROR("Failed to generate AP config for radio %s", name);
|
||||
goto next;
|
||||
}
|
||||
}
|
||||
next:
|
||||
/* Free the interface list */
|
||||
free(wifi_iface_list);
|
||||
wifi_iface_list = NULL;
|
||||
wifi_iface_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
err:
|
||||
|
||||
return rc;
|
||||
}
|
||||
int hardware_candidate_init(struct confd *confd)
|
||||
|
||||
+97
-84
@@ -3,108 +3,121 @@
|
||||
|
||||
#include "interfaces.h"
|
||||
|
||||
#define WPA_SUPPLICANT_FINIT_CONF "/etc/finit.d/available/wpa_supplicant-%s.conf"
|
||||
#define WPA_SUPPLICANT_CONF "/etc/wpa_supplicant-%s.conf"
|
||||
/*
|
||||
* WiFi Interface Management
|
||||
*
|
||||
* This file handles only virtual WiFi interface creation/deletion.
|
||||
* WiFi daemon configuration (hostapd/wpa_supplicant) is handled by
|
||||
* hardware.c when the WiFi radio (phy) is configured.
|
||||
*/
|
||||
|
||||
static int wifi_gen_config(const char *ifname, const char *ssid, const char *country, const char *secret, const char* encryption, struct dagger *net)
|
||||
/*
|
||||
* 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)
|
||||
{
|
||||
FILE *wpa_supplicant = NULL, *wpa = NULL;
|
||||
char *encryption_str;
|
||||
if (lydx_get_child(wifi, "access-point"))
|
||||
return wifi_ap;
|
||||
else
|
||||
return wifi_station; /* Need to return station even if "station" also is false, since that is the default scanning mode */
|
||||
}
|
||||
|
||||
int wifi_mode_changed(struct lyd_node *wifi)
|
||||
{
|
||||
struct lyd_node *station, *ap;
|
||||
enum lydx_op station_op, ap_op;
|
||||
|
||||
if (!wifi)
|
||||
return 0;
|
||||
station = lydx_get_child(wifi, "station");
|
||||
ap = lydx_get_child(wifi, "access-point");
|
||||
if (station)
|
||||
station_op = lydx_get_op(station);
|
||||
if (ap)
|
||||
ap_op = lydx_get_op(ap);
|
||||
|
||||
return ((station && station_op == LYDX_OP_DELETE) || (ap && ap_op == LYDX_OP_DELETE));
|
||||
}
|
||||
/*
|
||||
* Add WiFi virtual interface using iw
|
||||
*/
|
||||
int wifi_add_iface(struct lyd_node *cif, struct dagger *net)
|
||||
{
|
||||
const char *ifname, *radio;
|
||||
struct lyd_node *wifi;
|
||||
wifi_mode_t mode;
|
||||
FILE *iw;
|
||||
int rc = SR_ERR_OK;
|
||||
|
||||
if (!secret && (ssid && country && encryption)) {
|
||||
/* Not an error, updated from two ways, interface cb and keystore cb. */
|
||||
return 0;
|
||||
ifname = lydx_get_cattr(cif, "name");
|
||||
wifi = lydx_get_child(cif, "wifi");
|
||||
|
||||
if (!wifi) {
|
||||
ERROR("WiFi interface %s: no wifi container", ifname);
|
||||
return SR_ERR_INVAL_ARG;
|
||||
}
|
||||
|
||||
wpa = dagger_fopen_net_init(net, ifname, NETDAG_INIT_POST, "wpa_supplicant.sh");
|
||||
if (!wpa) {
|
||||
rc = SR_ERR_INTERNAL;
|
||||
radio = lydx_get_cattr(wifi, "radio");
|
||||
if (!radio) {
|
||||
ERROR("WiFi interface %s: missing radio reference", ifname);
|
||||
return SR_ERR_INVAL_ARG;
|
||||
}
|
||||
|
||||
iw = dagger_fopen_net_init(net, ifname, NETDAG_INIT_PRE, "wifi-iface.sh");
|
||||
if (!iw) {
|
||||
ERROR("Failed to open dagger file for WiFi interface creation");
|
||||
return SR_ERR_INTERNAL;
|
||||
}
|
||||
|
||||
mode = wifi_get_mode(wifi);
|
||||
|
||||
fprintf(iw, "# Generated by Infix confd - WiFi Interface Creation\n");
|
||||
fprintf(iw, "# Create %s interface %s on radio %s\n",
|
||||
mode == wifi_station ? "station" : "access point", ifname, radio);
|
||||
|
||||
switch(mode) {
|
||||
case wifi_station:
|
||||
fprintf(iw, "iw phy %s interface add %s type managed\n", radio, ifname);
|
||||
break;
|
||||
case wifi_ap:
|
||||
fprintf(iw, "iw phy %s interface add %s type __ap\n", radio, ifname);
|
||||
break;
|
||||
default:
|
||||
ERROR("WiFi mode %d unknown", mode);
|
||||
rc = SR_ERR_INVAL_ARG;
|
||||
goto out;
|
||||
}
|
||||
|
||||
fprintf(wpa, "# Generated by Infix confd\n");
|
||||
|
||||
fprintf(wpa, "if [ -f '/etc/finit.d/enabled/wifi@%s.conf' ];then\n", ifname);
|
||||
fprintf(wpa, "initctl -bfqn touch wifi@%s\n", ifname);
|
||||
fprintf(wpa, "else\n");
|
||||
fprintf(wpa, "initctl -bfqn enable wifi@%s\n", ifname);
|
||||
fprintf(wpa, "fi\n");
|
||||
fclose(wpa);
|
||||
|
||||
wpa_supplicant = fopenf("w", WPA_SUPPLICANT_CONF, ifname);
|
||||
if (!wpa_supplicant) {
|
||||
rc = SR_ERR_INTERNAL;
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (!secret || !ssid || !country || !encryption) {
|
||||
fprintf(wpa_supplicant,
|
||||
"ctrl_interface=/run/wpa_supplicant\n"
|
||||
"autoscan=periodic:10\n"
|
||||
"ap_scan=1\n");
|
||||
} else {
|
||||
if (!strcmp(encryption, "disabled")) {
|
||||
asprintf(&encryption_str, "key_mgmt=NONE");
|
||||
} else {
|
||||
asprintf(&encryption_str, "key_mgmt=SAE WPA-PSK\npsk=\"%s\"", secret);
|
||||
}
|
||||
fprintf(wpa_supplicant,
|
||||
"country=%s\n"
|
||||
"ctrl_interface=/run/wpa_supplicant\n"
|
||||
"autoscan=periodic:10\n"
|
||||
"ap_scan=1\n"
|
||||
"network={\n"
|
||||
"bgscan=\"simple: 30:-45:300\"\n"
|
||||
"ssid=\"%s\"\n"
|
||||
"%s\n"
|
||||
"}\n", country, ssid, encryption_str);
|
||||
free(encryption_str);
|
||||
}
|
||||
fclose(wpa_supplicant);
|
||||
|
||||
out:
|
||||
fclose(iw);
|
||||
return rc;
|
||||
|
||||
}
|
||||
int wifi_gen(struct lyd_node *dif, struct lyd_node *cif, struct dagger *net)
|
||||
|
||||
/*
|
||||
* Delete WiFi virtual interface using iw
|
||||
*/
|
||||
int wifi_del_iface(struct lyd_node *dif, struct dagger *net)
|
||||
{
|
||||
const char *ssid, *secret_name, *secret, *ifname, *country, *encryption;
|
||||
struct lyd_node *wifi, *secret_node;
|
||||
const char *ifname;
|
||||
FILE *iw;
|
||||
|
||||
bool enabled;
|
||||
ifname = lydx_get_cattr(cif, "name");
|
||||
ifname = lydx_get_cattr(dif, "name");
|
||||
|
||||
if (cif && !lydx_get_child(cif, "wifi")) {
|
||||
return wifi_gen_config(ifname, NULL, NULL, NULL, NULL, net);
|
||||
iw = dagger_fopen_net_exit(net, ifname, NETDAG_EXIT_POST, "wifi-iface.sh");
|
||||
if (!iw) {
|
||||
ERROR("Failed to open dagger file for WiFi interface deletion");
|
||||
return SR_ERR_INTERNAL;
|
||||
}
|
||||
|
||||
enabled = lydx_get_bool(cif, "enabled");
|
||||
wifi = lydx_get_child(cif, "wifi");
|
||||
|
||||
ssid = lydx_get_cattr(wifi, "ssid");
|
||||
secret_name = lydx_get_cattr(wifi, "secret");
|
||||
country = lydx_get_cattr(wifi, "country-code");
|
||||
encryption = lydx_get_cattr(wifi, "encryption");
|
||||
secret_node = lydx_get_xpathf(cif, "../../keystore/symmetric-keys/symmetric-key[name='%s']", secret_name);
|
||||
secret = lydx_get_cattr(secret_node, "cleartext-symmetric-key");
|
||||
|
||||
if (!enabled)
|
||||
return wifi_gen_del(cif, net);
|
||||
|
||||
return wifi_gen_config(ifname, ssid, country, secret, encryption, net);
|
||||
}
|
||||
|
||||
int wifi_gen_del(struct lyd_node *dif, struct dagger *net)
|
||||
{
|
||||
const char *ifname = lydx_get_cattr(dif, "name");
|
||||
FILE *iw = dagger_fopen_net_exit(net, ifname, NETDAG_EXIT_PRE, "iw.sh");
|
||||
|
||||
fprintf(iw, "# Generated by Infix confd\n");
|
||||
fprintf(iw, "# Generated by Infix confd - WiFi Interface Deletion\n");
|
||||
fprintf(iw, "ip link set %s down\n", ifname); /* Required to change modes. */
|
||||
fprintf(iw, "iw dev %s disconnect\n", ifname);
|
||||
fprintf(iw, "initctl -bfqn disable wifi@%s\n", ifname);
|
||||
fprintf(iw, "iw dev %s del\n", ifname);
|
||||
fclose(iw);
|
||||
erasef(WPA_SUPPLICANT_CONF, ifname);
|
||||
|
||||
return SR_ERR_OK;
|
||||
}
|
||||
|
||||
+12
-10
@@ -418,7 +418,7 @@ static int netdag_gen_afspec_add(sr_session_ctx_t *session, struct dagger *net,
|
||||
case IFT_VXLAN:
|
||||
return vxlan_gen(NULL, cif, ip);
|
||||
case IFT_WIFI:
|
||||
return wifi_gen(NULL, cif, net);
|
||||
return wifi_add_iface(cif, net);
|
||||
case IFT_ETH:
|
||||
return netdag_gen_ethtool(net, cif, dif);
|
||||
case IFT_LO:
|
||||
@@ -448,7 +448,10 @@ 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:
|
||||
return wifi_gen(dif, cif, net);
|
||||
/* 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(). */
|
||||
return 0;
|
||||
case IFT_DUMMY:
|
||||
case IFT_GRE:
|
||||
case IFT_GRETAP:
|
||||
@@ -472,9 +475,10 @@ static bool netdag_must_del(struct lyd_node *dif, struct lyd_node *cif)
|
||||
case IFT_BRIDGE:
|
||||
case IFT_DUMMY:
|
||||
case IFT_LO:
|
||||
case IFT_WIFI:
|
||||
return lydx_get_child(dif, "custom-phys-address") || lydx_get_descendant(dif, "wifi", "radio", NULL) || wifi_mode_changed(lydx_get_child(dif, "wifi"));
|
||||
break;
|
||||
|
||||
case IFT_WIFI:
|
||||
case IFT_ETH:
|
||||
return lydx_get_child(dif, "custom-phys-address");
|
||||
|
||||
@@ -562,13 +566,12 @@ static int netdag_gen_iface_del(struct dagger *net, struct lyd_node *dif,
|
||||
case IFT_LO:
|
||||
eth_gen_del(dif, ip);
|
||||
break;
|
||||
case IFT_WIFI:
|
||||
eth_gen_del(dif, ip);
|
||||
wifi_gen_del(dif, net);
|
||||
break;
|
||||
case IFT_VETH:
|
||||
veth_gen_del(dif, ip);
|
||||
break;
|
||||
case IFT_WIFI:
|
||||
wifi_del_iface(dif, net);
|
||||
break;
|
||||
case IFT_BRIDGE:
|
||||
case IFT_DUMMY:
|
||||
case IFT_GRE:
|
||||
@@ -587,7 +590,7 @@ static int netdag_gen_iface_del(struct dagger *net, struct lyd_node *dif,
|
||||
|
||||
static sr_error_t netdag_gen_iface_timeout(struct dagger *net, const char *ifname, const char *iftype)
|
||||
{
|
||||
if (!strcmp(iftype, "infix-if-type:ethernet") || !strcmp(iftype, "infix-if-type:wifi")) {
|
||||
if (!strcmp(iftype, "infix-if-type:ethernet")) {
|
||||
FILE *wait = dagger_fopen_net_init(net, ifname, NETDAG_INIT_TIMEOUT, "wait-interface.sh");
|
||||
if (!wait) {
|
||||
return -EIO;
|
||||
@@ -734,10 +737,9 @@ static int netdag_init_iface(struct lyd_node *cif)
|
||||
return vlan_add_deps(cif);
|
||||
case IFT_VETH:
|
||||
return veth_add_deps(cif);
|
||||
|
||||
case IFT_WIFI:
|
||||
case IFT_DUMMY:
|
||||
case IFT_ETH:
|
||||
case IFT_WIFI:
|
||||
case IFT_GRE:
|
||||
case IFT_GRETAP:
|
||||
case IFT_LO:
|
||||
|
||||
@@ -25,15 +25,15 @@
|
||||
_map(IFT_BRIDGE, "infix-if-type:bridge") \
|
||||
_map(IFT_DUMMY, "infix-if-type:dummy") \
|
||||
_map(IFT_ETH, "infix-if-type:ethernet") \
|
||||
_map(IFT_WIFI, "infix-if-type:wifi") \
|
||||
_map(IFT_GRE, "infix-if-type:gre") \
|
||||
_map(IFT_GRETAP, "infix-if-type:gretap") \
|
||||
_map(IFT_LAG, "infix-if-type:lag") \
|
||||
_map(IFT_LAG, "infix-if-type:lag") \
|
||||
_map(IFT_LO, "infix-if-type:loopback") \
|
||||
_map(IFT_VETH, "infix-if-type:veth") \
|
||||
_map(IFT_VLAN, "infix-if-type:vlan") \
|
||||
_map(IFT_VXLAN, "infix-if-type:vxlan") \
|
||||
/* */
|
||||
_map(IFT_WIFI, "infix-if-type:wifi") \
|
||||
/* */
|
||||
|
||||
enum iftype {
|
||||
#define ift_enum(_enum, _str) _enum,
|
||||
@@ -122,8 +122,9 @@ 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 */
|
||||
int wifi_gen(struct lyd_node *dif, struct lyd_node *cif, struct dagger *net);
|
||||
int wifi_gen_del(struct lyd_node *dif, struct dagger *net);
|
||||
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);
|
||||
|
||||
/* if-gre.c */
|
||||
int gre_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip);
|
||||
|
||||
@@ -23,7 +23,7 @@ MODULES=(
|
||||
"infix-syslog@2025-11-17.yang"
|
||||
"iana-hardware@2018-03-13.yang"
|
||||
"ietf-hardware@2018-03-13.yang -e hardware-state -e hardware-sensor"
|
||||
"infix-hardware@2025-10-30.yang"
|
||||
"infix-hardware@2025-12-04.yang"
|
||||
"ieee802-dot1q-types@2022-10-29.yang"
|
||||
"infix-ip@2025-11-02.yang"
|
||||
"infix-if-type@2025-02-12.yang"
|
||||
|
||||
@@ -13,10 +13,18 @@ module infix-hardware {
|
||||
prefix yang;
|
||||
}
|
||||
|
||||
import infix-wifi-country-codes {
|
||||
prefix iwcc;
|
||||
}
|
||||
|
||||
organization "KernelKit";
|
||||
contact "kernelkit@googlegroups.com";
|
||||
description "Vital Product Data augmentation of ieee-hardware and deviations.";
|
||||
|
||||
revision 2025-12-04 {
|
||||
description "Add WiFi radio survey container for channel utilization data.";
|
||||
reference "internal";
|
||||
}
|
||||
revision 2025-10-30 {
|
||||
description "Add phys-address leaf for hardware components and enable sensor support.";
|
||||
reference "internal";
|
||||
@@ -38,6 +46,38 @@ module infix-hardware {
|
||||
description "A two-letter country code.";
|
||||
}
|
||||
|
||||
/*
|
||||
* WiFi-specific typedefs
|
||||
*/
|
||||
|
||||
typedef wifi-radio-ref {
|
||||
type leafref {
|
||||
path "/iehw:hardware/iehw:component/iehw:name";
|
||||
}
|
||||
description
|
||||
"Reference to a WiFi radio hardware component.
|
||||
WiFi radios are hardware components with class 'ih:wifi'.";
|
||||
}
|
||||
|
||||
typedef wifi-band {
|
||||
type enumeration {
|
||||
enum "2.4GHz" {
|
||||
description "2.4 GHz band (channels 1-14, maximum compatibility)";
|
||||
}
|
||||
enum "5GHz" {
|
||||
description "5 GHz band (less congestion, higher throughput, recommended)";
|
||||
}
|
||||
enum "6GHz" {
|
||||
description "6 GHz band (WiFi 6E, requires compatible hardware)";
|
||||
}
|
||||
}
|
||||
description "WiFi frequency band selection.";
|
||||
}
|
||||
|
||||
/*
|
||||
* Hardware class identities
|
||||
*/
|
||||
|
||||
identity usb {
|
||||
base iahw:hardware-class;
|
||||
description "This identity is used to describe a USB port";
|
||||
@@ -143,5 +183,335 @@ module infix-hardware {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* WiFi Radio configuration (when class = 'ih:wifi')
|
||||
*/
|
||||
|
||||
container wifi-radio {
|
||||
when "derived-from-or-self(../iehw:class, 'ih:wifi')";
|
||||
presence "WiFi radio configuration";
|
||||
description
|
||||
"WiFi radio/PHY configuration and operational data.
|
||||
|
||||
This container is present when the hardware component represents
|
||||
a WiFi radio (class 'ih:wifi'). WiFi radios are physical devices
|
||||
that can host multiple virtual WiFi interfaces (APs or Stations).";
|
||||
|
||||
leaf country-code {
|
||||
type iwcc:country-code;
|
||||
mandatory true;
|
||||
description
|
||||
"Two-letter ISO 3166-1 country code for regulatory compliance.
|
||||
|
||||
Sets the regulatory domain for this radio, determining:
|
||||
- Allowed channels and frequencies
|
||||
- Maximum transmit power
|
||||
- DFS (Dynamic Frequency Selection) requirements
|
||||
|
||||
Examples: 'US', 'DE', 'JP'.
|
||||
|
||||
WARNING: Incorrect values may violate local laws and regulations.";
|
||||
}
|
||||
|
||||
leaf channel {
|
||||
type union {
|
||||
type uint16 {
|
||||
range "1..196";
|
||||
}
|
||||
type enumeration {
|
||||
enum "auto" {
|
||||
description "Automatic channel selection (ACS)";
|
||||
}
|
||||
}
|
||||
}
|
||||
default "auto";
|
||||
description
|
||||
"Operating channel number.
|
||||
|
||||
Required for Access Point mode.
|
||||
Not used in Station mode (station uses AP's channel).
|
||||
|
||||
Channel availability depends on:
|
||||
- Configured band (2.4/5/6 GHz)
|
||||
- Regulatory domain (country-code)
|
||||
- Hardware capabilities
|
||||
|
||||
Common channels:
|
||||
- 2.4 GHz: 1-14 (channels 12-14 restricted in some countries)
|
||||
- 5 GHz: 36, 40, 44, 48, 149, 153, 157, 161, 165 (varies by region)
|
||||
- 6 GHz: 1-233 (WiFi 6E, where permitted)
|
||||
|
||||
Set to 'auto' for automatic channel selection.";
|
||||
}
|
||||
|
||||
leaf band {
|
||||
type wifi-band;
|
||||
description
|
||||
"Frequency band selection.
|
||||
|
||||
Required for Access Point mode.
|
||||
Not used in Station mode (station uses AP's band).
|
||||
|
||||
Constraints:
|
||||
- Hardware must support the selected band
|
||||
- Regulatory domain affects channel availability
|
||||
- PHY mode must be compatible with selected band
|
||||
|
||||
Recommendation: Use 5GHz for better performance and less
|
||||
congestion in most environments.";
|
||||
}
|
||||
|
||||
leaf enable-wifi6 {
|
||||
type boolean;
|
||||
default false;
|
||||
description
|
||||
"Enable WiFi 6 (802.11ax) on 2.4GHz and 5GHz bands.
|
||||
|
||||
By default, WiFi 6 is enabled only on 6GHz (WiFi 6E).
|
||||
Set to 'true' to enable WiFi 6 on 2.4GHz and 5GHz bands.
|
||||
|
||||
WiFi 6 provides:
|
||||
- OFDMA (better multi-user efficiency)
|
||||
- Target Wake Time (better battery life)
|
||||
- 1024-QAM (higher throughput)
|
||||
- BSS Coloring (reduced interference)
|
||||
|
||||
Requires:
|
||||
- Hardware support for 802.11ax
|
||||
- Compatible clients for full benefits
|
||||
|
||||
Note: 6GHz band always uses WiFi 6 regardless of this setting.";
|
||||
}
|
||||
|
||||
/*
|
||||
* Operational state
|
||||
*/
|
||||
|
||||
leaf frequency {
|
||||
config false;
|
||||
type uint32;
|
||||
units "MHz";
|
||||
description
|
||||
"Current operating frequency in MHz.
|
||||
|
||||
Derived from the configured channel and band.
|
||||
|
||||
Example values:
|
||||
- 2412 MHz (channel 1, 2.4 GHz)
|
||||
- 5180 MHz (channel 36, 5 GHz)
|
||||
- 5955 MHz (channel 1, 6 GHz)";
|
||||
}
|
||||
|
||||
leaf noise {
|
||||
config false;
|
||||
type int16;
|
||||
units "dBm";
|
||||
description
|
||||
"Background noise level on current channel in dBm.
|
||||
|
||||
Lower (more negative) values indicate a cleaner RF environment.
|
||||
|
||||
Typical values:
|
||||
- -95 to -100 dBm: Very low noise (excellent)
|
||||
- -85 to -95 dBm: Low noise (good)
|
||||
- -75 to -85 dBm: Moderate noise
|
||||
- -65 to -75 dBm: High noise (congested)";
|
||||
}
|
||||
|
||||
leaf-list supported-channels {
|
||||
config false;
|
||||
type uint16;
|
||||
description
|
||||
"List of channels supported by this radio in the current
|
||||
regulatory domain.
|
||||
|
||||
Channels depend on:
|
||||
- Hardware capabilities
|
||||
- Configured country-code
|
||||
- Band selection
|
||||
|
||||
This list reflects actual usable channels after applying
|
||||
regulatory constraints.";
|
||||
}
|
||||
|
||||
leaf max-txpower {
|
||||
config false;
|
||||
type uint8;
|
||||
units "dBm";
|
||||
description
|
||||
"Maximum transmit power allowed by the regulatory domain
|
||||
for the current channel.
|
||||
|
||||
This is the regulatory limit for the current band and channel.";
|
||||
}
|
||||
|
||||
leaf num-virtual-interfaces {
|
||||
config false;
|
||||
type uint8;
|
||||
description
|
||||
"Number of virtual interfaces (AP/Station) currently
|
||||
configured on this radio.";
|
||||
}
|
||||
|
||||
leaf driver {
|
||||
config false;
|
||||
type string;
|
||||
description
|
||||
"WiFi driver name (e.g., mt798x-wmac, ath10k).";
|
||||
}
|
||||
|
||||
container max-interfaces {
|
||||
config false;
|
||||
description
|
||||
"Maximum number of virtual interfaces supported by this radio.";
|
||||
|
||||
leaf ap {
|
||||
type uint8;
|
||||
description
|
||||
"Maximum number of AP interfaces.";
|
||||
}
|
||||
|
||||
leaf station {
|
||||
type uint8;
|
||||
description
|
||||
"Maximum number of station interfaces.";
|
||||
}
|
||||
}
|
||||
|
||||
list bands {
|
||||
config false;
|
||||
key "band";
|
||||
description
|
||||
"Supported frequency bands and their capabilities.";
|
||||
|
||||
leaf band {
|
||||
type string;
|
||||
description
|
||||
"Band identifier from iw (e.g., '1' for 2.4GHz, '2' for 5GHz).";
|
||||
}
|
||||
|
||||
leaf name {
|
||||
type string;
|
||||
description
|
||||
"Human-readable band name (e.g., '2.4GHz', '5GHz', '6GHz').";
|
||||
}
|
||||
|
||||
leaf ht-capable {
|
||||
type boolean;
|
||||
description
|
||||
"High Throughput (802.11n) support.";
|
||||
}
|
||||
|
||||
leaf vht-capable {
|
||||
type boolean;
|
||||
description
|
||||
"Very High Throughput (802.11ac) support.";
|
||||
}
|
||||
|
||||
leaf he-capable {
|
||||
type boolean;
|
||||
description
|
||||
"High Efficiency (802.11ax/WiFi 6) support.";
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Channel survey data (operational state)
|
||||
*/
|
||||
|
||||
container survey {
|
||||
config false;
|
||||
description
|
||||
"WiFi channel survey data providing channel utilization
|
||||
and interference information.
|
||||
|
||||
This data is collected from the WiFi driver and provides
|
||||
insights into channel occupancy, noise levels, and RF activity.";
|
||||
|
||||
list channel {
|
||||
key "frequency";
|
||||
description
|
||||
"Per-channel survey information.
|
||||
|
||||
Includes utilization metrics for all channels scanned by
|
||||
the radio, not just the currently active channel.";
|
||||
|
||||
leaf frequency {
|
||||
type uint32;
|
||||
units "MHz";
|
||||
description
|
||||
"Channel center frequency in MHz.
|
||||
|
||||
Examples:
|
||||
- 2412 MHz (2.4 GHz channel 1)
|
||||
- 5180 MHz (5 GHz channel 36)
|
||||
- 5955 MHz (6 GHz channel 1)";
|
||||
}
|
||||
|
||||
leaf in-use {
|
||||
type boolean;
|
||||
description
|
||||
"Whether this channel is currently in use by the radio.
|
||||
|
||||
Only one channel will have this set to true at a time.";
|
||||
}
|
||||
|
||||
leaf noise {
|
||||
type int16;
|
||||
units "dBm";
|
||||
description
|
||||
"Background noise level on this channel in dBm.
|
||||
|
||||
Lower (more negative) values indicate cleaner RF environment.
|
||||
|
||||
Typical values:
|
||||
- -95 to -100 dBm: Very low noise (excellent)
|
||||
- -85 to -95 dBm: Low noise (good)
|
||||
- -75 to -85 dBm: Moderate noise
|
||||
- -65 to -75 dBm: High noise (congested)";
|
||||
}
|
||||
|
||||
leaf active-time {
|
||||
type uint32;
|
||||
units "milliseconds";
|
||||
description
|
||||
"Total time the radio was active on this channel.
|
||||
|
||||
This is the survey measurement period for this channel.";
|
||||
}
|
||||
|
||||
leaf busy-time {
|
||||
type uint32;
|
||||
units "milliseconds";
|
||||
description
|
||||
"Time the channel was detected as busy.
|
||||
|
||||
Includes time spent receiving frames, transmitting frames,
|
||||
and time the channel was busy due to other sources.
|
||||
|
||||
Channel utilization = (busy-time / active-time) * 100%";
|
||||
}
|
||||
|
||||
leaf receive-time {
|
||||
type uint32;
|
||||
units "milliseconds";
|
||||
description
|
||||
"Time spent receiving frames on this channel.
|
||||
|
||||
Subset of busy-time spent on frame reception.";
|
||||
}
|
||||
|
||||
leaf transmit-time {
|
||||
type uint32;
|
||||
units "milliseconds";
|
||||
description
|
||||
"Time spent transmitting frames on this channel.
|
||||
|
||||
Subset of busy-time spent on frame transmission.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -928,6 +928,9 @@ submodule infix-if-bridge {
|
||||
must "not(../ip:ipv4/ip:address or ../ip:ipv6/ip:address)" {
|
||||
error-message "Bridge ports cannot have IP addresses configured.";
|
||||
}
|
||||
must "not(derived-from-or-self(../if:type, 'infix-ift:wifi')) or ../infix-if:wifi/infix-if:access-point" {
|
||||
error-message "WiFi interfaces can only be bridge ports when configured as Access Points.";
|
||||
}
|
||||
description "Bridge association and port specific settings.";
|
||||
uses bridge-port-common;
|
||||
uses bridge-port-lower {
|
||||
|
||||
@@ -110,6 +110,6 @@ module infix-if-type {
|
||||
if-feature wifi;
|
||||
base infix-interface-type;
|
||||
base ianaift:ieee80211;
|
||||
description "WiFi interface";
|
||||
description "WiFi (802.11) interface";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,11 @@ submodule infix-if-wifi {
|
||||
import infix-if-type {
|
||||
prefix infixift;
|
||||
}
|
||||
import infix-wifi-country-codes {
|
||||
prefix iwcc;
|
||||
import ietf-hardware {
|
||||
prefix iehw;
|
||||
}
|
||||
import infix-hardware {
|
||||
prefix ih;
|
||||
}
|
||||
|
||||
organization "KernelKit";
|
||||
@@ -30,12 +33,30 @@ submodule infix-if-wifi {
|
||||
description
|
||||
"WiFi-specific extensions to the standard IETF interfaces model.
|
||||
|
||||
This submodule defines configuration and operational data relevant to
|
||||
WiFi interfaces, including security settings, network
|
||||
discovery, and regulatory compliance.
|
||||
This submodule defines configuration and operational data for WiFi
|
||||
virtual interfaces, supporting both Access Point (AP) and Station
|
||||
(client) modes.
|
||||
|
||||
It supports WiFi client mode and enables comprehensive management of
|
||||
wireless connections, including encryption, country codes, and scanning.";
|
||||
WiFi virtual interfaces are created on top of WiFi radios (PHYs)
|
||||
defined in the infix-wifi-radio module. The radio provides physical
|
||||
layer configuration (channel, power, PHY mode), while virtual
|
||||
interfaces provide network-layer configuration (SSID, security).
|
||||
|
||||
Key features:
|
||||
- Dual mode support: AP and Station
|
||||
- Multi-SSID: Multiple APs on same radio
|
||||
- Security: WPA2/WPA3 with keystore integration
|
||||
- Operational state: Connection status, RSSI, client lists";
|
||||
|
||||
revision 2025-12-17 {
|
||||
description
|
||||
"Major refactoring for AP mode support (BREAKING CHANGE):
|
||||
- Added radio reference (parent PHY)
|
||||
- Added wifi-mode choice (AP vs Station)
|
||||
- Reorganized configuration hierarchy
|
||||
- Old configurations must be migrated manually";
|
||||
reference "internal";
|
||||
}
|
||||
|
||||
revision 2025-12-12 {
|
||||
description "Adapt to new revision of model ietf-keystore.";
|
||||
@@ -43,7 +64,7 @@ submodule infix-if-wifi {
|
||||
}
|
||||
|
||||
revision 2025-05-27 {
|
||||
description "Initial revision.";
|
||||
description "Initial revision (Station mode only).";
|
||||
reference "internal";
|
||||
}
|
||||
|
||||
@@ -51,29 +72,6 @@ submodule infix-if-wifi {
|
||||
description "WiFi support is an optional build-time feature in Infix.";
|
||||
}
|
||||
|
||||
typedef encryption {
|
||||
type enumeration {
|
||||
enum auto {
|
||||
description
|
||||
"Enables WPA/WPA2/WPA3 encryption with automatic protocol
|
||||
negotiation. The system uses the strongest supported variant supported by Access Point.";
|
||||
}
|
||||
enum disabled {
|
||||
description
|
||||
"Disables encryption for an open network.
|
||||
|
||||
WARNING: Open networks transmit data unencrypted and should only
|
||||
be used in trusted environments.";
|
||||
}
|
||||
}
|
||||
description
|
||||
"Encryption modes available for WiFi connections.
|
||||
|
||||
- auto: Secure connection using WPA3/WPA2/WPA (auto-selected)
|
||||
- disabled: Open network (unencrypted)";
|
||||
}
|
||||
|
||||
|
||||
augment "/if:interfaces/if:interface" {
|
||||
when "derived-from-or-self(if:type, 'infixift:wifi')" {
|
||||
description
|
||||
@@ -82,107 +80,370 @@ submodule infix-if-wifi {
|
||||
|
||||
container wifi {
|
||||
if-feature wifi;
|
||||
presence "Configure Wi-Fi settings";
|
||||
presence "Configure Wi-Fi virtual interface";
|
||||
|
||||
description
|
||||
"WiFi-specific configuration and operational data.";
|
||||
"WiFi virtual interface configuration.
|
||||
|
||||
leaf country-code {
|
||||
type iwcc:country-code;
|
||||
mandatory true;
|
||||
description
|
||||
"Two-letter ISO 3166-1 country code for regulatory compliance.
|
||||
Each WiFi interface represents a virtual interface (VAP - Virtual
|
||||
Access Point, or Station) created on a physical radio.
|
||||
|
||||
Examples: 'US', 'DE', 'JP'.
|
||||
The interface must reference a radio defined in infix-wifi-radio
|
||||
module, which provides the physical layer configuration.";
|
||||
|
||||
WARNING: Incorrect values may violate local laws.";
|
||||
|
||||
|
||||
}
|
||||
|
||||
leaf encryption {
|
||||
default auto;
|
||||
type encryption;
|
||||
|
||||
description
|
||||
"WiFi encryption method.
|
||||
|
||||
- auto (default): Enables WPA2/WPA3 auto-negotiation
|
||||
- disabled: Disables encryption (open network)";
|
||||
}
|
||||
|
||||
leaf ssid {
|
||||
type string {
|
||||
length "1..32";
|
||||
leaf radio {
|
||||
type leafref {
|
||||
path "/iehw:hardware/iehw:component/iehw:name";
|
||||
}
|
||||
mandatory true;
|
||||
must "derived-from-or-self(/iehw:hardware/iehw:component[iehw:name=current()]/iehw:class, 'ih:wifi')" {
|
||||
error-message "Referenced hardware component must be a WiFi radio (class 'ih:wifi')";
|
||||
}
|
||||
|
||||
description
|
||||
"WiFi network name (SSID).
|
||||
"Reference to parent WiFi radio (PHY).
|
||||
|
||||
Case-sensitive, must match the target network.
|
||||
References a hardware component with class 'ih:wifi'.
|
||||
The radio must exist and be configured before creating
|
||||
virtual interfaces.
|
||||
|
||||
Length: 1–32 characters.";
|
||||
Example: 'phy0' for the first WiFi radio.
|
||||
|
||||
All physical layer settings (channel, power, regulatory)
|
||||
are inherited from the radio configuration.";
|
||||
}
|
||||
|
||||
leaf secret {
|
||||
type ks:central-symmetric-key-ref;
|
||||
mandatory true;
|
||||
must "../encryption != 'disabled'" {
|
||||
error-message
|
||||
"Pre-shared key required unless encryption is disabled.";
|
||||
}
|
||||
|
||||
choice wifi-mode {
|
||||
description
|
||||
"Pre-shared key (PSK) for WPA-secured networks.";
|
||||
}
|
||||
"WiFi interface operating mode.
|
||||
|
||||
leaf rssi {
|
||||
config false;
|
||||
type int16;
|
||||
units "dBm";
|
||||
description
|
||||
"Current received signal strength (RSSI) in dBm.
|
||||
When no mode is configured, the interface operates in scan-only
|
||||
mode, allowing discovery of available WiFi networks.
|
||||
|
||||
Lower (more negative) values indicate stronger signals.";
|
||||
}
|
||||
Once you've identified a network, configure either:
|
||||
- Station mode: Connect to an existing WiFi network
|
||||
- Access Point mode: Create a WiFi network for clients
|
||||
|
||||
list scan-results {
|
||||
config false;
|
||||
key ssid;
|
||||
description
|
||||
"List of discovered networks.";
|
||||
Note: A radio can host either:
|
||||
- Multiple AP interfaces (multi-SSID), OR
|
||||
- A single Station interface
|
||||
|
||||
leaf ssid {
|
||||
type string;
|
||||
description
|
||||
"SSID of the discovered network.";
|
||||
Mixing AP and Station on the same radio is not supported.";
|
||||
|
||||
case station {
|
||||
container station {
|
||||
presence "Configure WiFi station (client) mode";
|
||||
|
||||
description
|
||||
"WiFi Station mode configuration.
|
||||
|
||||
In station mode, the interface acts as a WiFi client,
|
||||
connecting to an existing Access Point.
|
||||
|
||||
Only one station interface is allowed per radio.
|
||||
|
||||
Example use case: Connect to upstream WiFi network.";
|
||||
|
||||
leaf ssid {
|
||||
type string {
|
||||
length "1..32";
|
||||
}
|
||||
mandatory true;
|
||||
description
|
||||
"WiFi network name (SSID) to connect to.
|
||||
|
||||
Case-sensitive, must match the target network exactly.
|
||||
|
||||
Length: 1–32 characters.";
|
||||
}
|
||||
|
||||
container security {
|
||||
description
|
||||
"WiFi security configuration.";
|
||||
|
||||
leaf mode {
|
||||
type enumeration {
|
||||
enum auto {
|
||||
description
|
||||
"Automatic security negotiation.
|
||||
Tries WPA3-SAE, then WPA2-PSK, in that order.
|
||||
Recommended for maximum compatibility and security.";
|
||||
}
|
||||
enum disabled {
|
||||
description
|
||||
"Open network (no security).
|
||||
|
||||
WARNING: All traffic is transmitted unencrypted!
|
||||
Only use in trusted environments.";
|
||||
}
|
||||
}
|
||||
default auto;
|
||||
description
|
||||
"Security mode for WiFi connection.
|
||||
|
||||
- auto (default): WPA3/WPA2 auto-negotiation (secure)
|
||||
- disabled: Open network (insecure)";
|
||||
}
|
||||
|
||||
leaf secret {
|
||||
when "../mode != 'disabled'";
|
||||
type ks:central-symmetric-key-ref;
|
||||
mandatory true;
|
||||
description
|
||||
"Pre-shared key (PSK) reference.
|
||||
|
||||
References a symmetric key in the keystore.
|
||||
|
||||
For WPA2/WPA3 networks, this is the WiFi password.";
|
||||
}
|
||||
}
|
||||
|
||||
/* Operational state */
|
||||
|
||||
leaf rssi {
|
||||
config false;
|
||||
type int16;
|
||||
units "dBm";
|
||||
description
|
||||
"Current received signal strength indication (RSSI) in dBm.
|
||||
|
||||
More negative values indicate weaker signal.
|
||||
|
||||
Typical values:
|
||||
- -30 to -50 dBm: Excellent
|
||||
- -50 to -60 dBm: Good
|
||||
- -60 to -70 dBm: Fair
|
||||
- -70 to -80 dBm: Weak
|
||||
- Below -80 dBm: Very weak";
|
||||
}
|
||||
|
||||
list scan-results {
|
||||
config false;
|
||||
key ssid;
|
||||
description
|
||||
"List of discovered WiFi networks.
|
||||
|
||||
Updated periodically by background scanning.";
|
||||
|
||||
leaf ssid {
|
||||
type string;
|
||||
description "SSID of the discovered network.";
|
||||
}
|
||||
|
||||
leaf bssid {
|
||||
type yang:mac-address;
|
||||
description "BSSID (MAC address) of the AP.";
|
||||
}
|
||||
|
||||
leaf rssi {
|
||||
type int16;
|
||||
units "dBm";
|
||||
description "Signal strength of the network.";
|
||||
}
|
||||
|
||||
leaf channel {
|
||||
type uint16;
|
||||
description "Channel on which the network was detected.";
|
||||
}
|
||||
|
||||
leaf-list encryption {
|
||||
ordered-by user;
|
||||
type string;
|
||||
description
|
||||
"Human-readable security information.
|
||||
|
||||
Examples: 'WPA2-Personal', 'WPA3-SAE', 'Open'";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
leaf bssid {
|
||||
type string;
|
||||
description
|
||||
"BSSID of the discovered network.";
|
||||
}
|
||||
case access-point {
|
||||
container access-point {
|
||||
presence "Configure WiFi Access Point mode";
|
||||
|
||||
leaf rssi {
|
||||
type int16;
|
||||
units "dBm";
|
||||
description
|
||||
"Signal strength of the network.";
|
||||
}
|
||||
description
|
||||
"WiFi Access Point mode configuration.
|
||||
|
||||
leaf channel {
|
||||
type int16;
|
||||
description
|
||||
"Channel on which the network was detected.";
|
||||
}
|
||||
In AP mode, the interface provides a WiFi network that
|
||||
clients can connect to.
|
||||
|
||||
leaf-list encryption {
|
||||
ordered-by user;
|
||||
type string;
|
||||
description
|
||||
"Human-readable description of the detected security.";
|
||||
Multiple AP interfaces can be created on the same radio
|
||||
for multi-SSID support (Guest network, IoT network, etc.).
|
||||
|
||||
Example use case: Create WiFi hotspot.";
|
||||
|
||||
must "/iehw:hardware/iehw:component[iehw:name = current()/../radio]/ih:wifi-radio/ih:band" {
|
||||
error-message "Parent radio must have 'band' configured for Access Point mode";
|
||||
}
|
||||
|
||||
must "/iehw:hardware/iehw:component[iehw:name = current()/../radio]/ih:wifi-radio/ih:channel" {
|
||||
error-message "Parent radio must have 'channel' configured for Access Point mode";
|
||||
}
|
||||
|
||||
must "/iehw:hardware/iehw:component[iehw:name = current()/../radio]/ih:wifi-radio/ih:country-code != '00'" {
|
||||
error-message "Country code '00' (world regulatory domain) is not allowed for Access Point mode. Please configure a specific country code on the radio.";
|
||||
}
|
||||
|
||||
leaf ssid {
|
||||
type string {
|
||||
length "1..32";
|
||||
}
|
||||
mandatory true;
|
||||
description
|
||||
"WiFi network name (SSID) to broadcast.
|
||||
|
||||
This is the network name that clients will see when
|
||||
scanning for WiFi networks.
|
||||
|
||||
Length: 1–32 characters.";
|
||||
}
|
||||
|
||||
leaf hidden {
|
||||
type boolean;
|
||||
default false;
|
||||
description
|
||||
"Hide the SSID from broadcast beacons.
|
||||
|
||||
When true, the network will not appear in WiFi scans.
|
||||
Clients must know the exact SSID to connect.
|
||||
|
||||
Note: This provides minimal security benefit and may
|
||||
cause compatibility issues with some clients.";
|
||||
}
|
||||
|
||||
container security {
|
||||
description
|
||||
"WiFi security configuration.";
|
||||
|
||||
leaf mode {
|
||||
type enumeration {
|
||||
enum open {
|
||||
description
|
||||
"Open network (no encryption).
|
||||
|
||||
WARNING: All client traffic is unencrypted!
|
||||
Only use in controlled environments (captive portal, etc.).";
|
||||
}
|
||||
enum wpa2-personal {
|
||||
description
|
||||
"WPA2-Personal (WPA2-PSK).
|
||||
Widely compatible, secure for most use cases.";
|
||||
}
|
||||
enum wpa3-personal {
|
||||
description
|
||||
"WPA3-Personal (WPA3-SAE).
|
||||
Enhanced security with forward secrecy.
|
||||
Requires WPA3-capable clients.";
|
||||
}
|
||||
enum wpa2-wpa3-personal {
|
||||
description
|
||||
"WPA2/WPA3 transitional mode.
|
||||
Accepts both WPA2 and WPA3 clients.
|
||||
Recommended for maximum compatibility + security.";
|
||||
}
|
||||
}
|
||||
default wpa2-wpa3-personal;
|
||||
description
|
||||
"WiFi security mode.
|
||||
|
||||
Determines authentication and encryption methods.
|
||||
|
||||
Recommended: wpa2-wpa3-personal for best security
|
||||
and compatibility.";
|
||||
}
|
||||
|
||||
leaf secret {
|
||||
when "../mode != 'open'";
|
||||
type ks:central-symmetric-key-ref;
|
||||
mandatory true;
|
||||
description
|
||||
"Pre-shared key (PSK) reference.
|
||||
|
||||
References a symmetric key in the keystore.
|
||||
|
||||
This is the WiFi password that clients must provide
|
||||
to connect to the network.
|
||||
|
||||
Requirements:
|
||||
- WPA2/WPA3: 8-63 characters (configured in keystore)";
|
||||
}
|
||||
}
|
||||
|
||||
/* Operational state */
|
||||
|
||||
container stations {
|
||||
list station {
|
||||
config false;
|
||||
key mac-address;
|
||||
description
|
||||
"List of currently connected clients (stations).";
|
||||
|
||||
leaf mac-address {
|
||||
type yang:mac-address;
|
||||
description "Client MAC address.";
|
||||
}
|
||||
|
||||
leaf rssi {
|
||||
type int16;
|
||||
units "dBm";
|
||||
description "Client signal strength in dBm.";
|
||||
}
|
||||
|
||||
leaf connected-time {
|
||||
type uint32;
|
||||
units "seconds";
|
||||
description "Time since client connected, in seconds.";
|
||||
}
|
||||
|
||||
leaf rx-packets {
|
||||
type uint32;
|
||||
description "Packets received from this client.";
|
||||
}
|
||||
|
||||
leaf tx-packets {
|
||||
type uint32;
|
||||
description "Packets transmitted to this client.";
|
||||
}
|
||||
|
||||
leaf rx-bytes {
|
||||
type uint32;
|
||||
units "octets";
|
||||
description "Bytes received from this client.";
|
||||
}
|
||||
|
||||
leaf tx-bytes {
|
||||
type uint32;
|
||||
units "octets";
|
||||
description "Bytes transmitted to this client.";
|
||||
}
|
||||
|
||||
leaf rx-speed {
|
||||
type uint32;
|
||||
units "100 kbit/s";
|
||||
description
|
||||
"Last received data rate from this client in 100 kbit/s.
|
||||
|
||||
Examples:
|
||||
- 10 = 1 Mbit/s
|
||||
- 65 = 6.5 Mbit/s
|
||||
- 866 = 86.6 Mbit/s";
|
||||
}
|
||||
|
||||
leaf tx-speed {
|
||||
type uint32;
|
||||
units "100 kbit/s";
|
||||
description
|
||||
"Last transmitted data rate to this client in 100 kbit/s.
|
||||
|
||||
Examples:
|
||||
- 10 = 1 Mbit/s
|
||||
- 65 = 6.5 Mbit/s
|
||||
- 866 = 86.6 Mbit/s";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,12 @@ module infix-interfaces {
|
||||
import infix-if-type {
|
||||
prefix infix-ift;
|
||||
}
|
||||
import ietf-hardware {
|
||||
prefix iehw;
|
||||
}
|
||||
import infix-hardware {
|
||||
prefix ih;
|
||||
}
|
||||
|
||||
include infix-if-base;
|
||||
include infix-if-bridge;
|
||||
|
||||
@@ -55,9 +55,9 @@ module infix-keystore {
|
||||
}
|
||||
augment "/ks:keystore/ks:symmetric-keys/ks:symmetric-key/ks:key-type" {
|
||||
case cleartext-symmetric-key {
|
||||
leaf cleartext-symmetric-key {
|
||||
leaf symmetric-key {
|
||||
type string;
|
||||
must "../infix-ks:key-format != 'infix-ct:wifi-preshared-key-format' or " +
|
||||
must "../../key-format != 'infix-ct:wifi-preshared-key-format' or " +
|
||||
"(string-length(.) >= 8 and string-length(.) <= 63)" {
|
||||
error-message "WiFi pre-shared key must be 8-63 characters long";
|
||||
}
|
||||
|
||||
@@ -16,7 +16,12 @@ module infix-wifi-country-codes {
|
||||
|
||||
The regulatory domain configuration follows the principles
|
||||
established in IETF RFCs for wireless access point management.";
|
||||
|
||||
revision 2025-11-28 {
|
||||
description
|
||||
"Add support for 00 - World regulatory domain.";
|
||||
reference
|
||||
"Internal";
|
||||
}
|
||||
revision 2025-06-02 {
|
||||
description
|
||||
"Initial revision for WiFi country code support.";
|
||||
@@ -27,6 +32,7 @@ module infix-wifi-country-codes {
|
||||
|
||||
typedef country-code {
|
||||
type enumeration {
|
||||
enum "00" { description "World regulatory domain (no country restrictions)"; }
|
||||
enum "AD" { description "Andorra"; }
|
||||
enum "AE" { description "United Arab Emirates"; }
|
||||
enum "AF" { description "Afghanistan"; }
|
||||
|
||||
+1
-15
@@ -422,19 +422,6 @@ def lldp(args: List[str]):
|
||||
return
|
||||
cli_pretty(data, "show-lldp")
|
||||
|
||||
def wifi(args: List[str]):
|
||||
iface = args[0]
|
||||
if len(args) == 0:
|
||||
print("Illigal usage")
|
||||
return
|
||||
if is_valid_interface_name(iface):
|
||||
if not os.path.exists(f"/sys/class/net/{iface}/wireless"):
|
||||
print("Not a Wi-Fi interface")
|
||||
return
|
||||
data = run_sysrepocfg("/ietf-interfaces:interfaces")
|
||||
cli_pretty(data, "show-wifi-scan", "-n", iface)
|
||||
else:
|
||||
print(f"Invalid interface name: {iface}")
|
||||
|
||||
def system(args: List[str]) -> None:
|
||||
# Get system state from sysrepo
|
||||
@@ -556,8 +543,7 @@ def execute_command(command: str, args: List[str]):
|
||||
'services': services,
|
||||
'software': software,
|
||||
'stp': stp,
|
||||
'system': system,
|
||||
'wifi': wifi
|
||||
'system': system
|
||||
}
|
||||
|
||||
if command in command_mapping:
|
||||
|
||||
@@ -159,18 +159,6 @@ class PadDhcpServer:
|
||||
exp = 10
|
||||
|
||||
|
||||
class PadUsbPort:
|
||||
title = 30
|
||||
name = 20
|
||||
state = 10
|
||||
oper = 10
|
||||
|
||||
@classmethod
|
||||
def table_width(cls):
|
||||
"""Total width of USB port table"""
|
||||
return cls.name + cls.state + cls.oper
|
||||
|
||||
|
||||
class PadSensor:
|
||||
name = 30
|
||||
value = 20
|
||||
@@ -190,12 +178,6 @@ class PadNtpSource:
|
||||
poll = 14
|
||||
|
||||
|
||||
class PadWifiScan:
|
||||
ssid = 40
|
||||
encryption = 30
|
||||
signal = 9
|
||||
|
||||
|
||||
class PadLldp:
|
||||
interface = 16
|
||||
rem_idx = 10
|
||||
@@ -544,7 +526,7 @@ class Decore():
|
||||
|
||||
@staticmethod
|
||||
def bright_green(txt):
|
||||
return Decore.decorate("1;32", txt, "39")
|
||||
return Decore.decorate("1;32", txt, "0")
|
||||
|
||||
@staticmethod
|
||||
def yellow(txt):
|
||||
@@ -847,12 +829,6 @@ class USBport:
|
||||
self.state = get_json_data('', self.data, 'state', 'admin-state')
|
||||
self.oper = get_json_data('', self.data, 'state', 'oper-state')
|
||||
|
||||
def print(self):
|
||||
row = f"{self.name:<{PadUsbPort.name}}"
|
||||
row += f"{self.state:<{PadUsbPort.state}}"
|
||||
row += f"{self.oper:<{PadUsbPort.oper}}"
|
||||
print(row)
|
||||
|
||||
|
||||
class Sensor:
|
||||
def __init__(self, data):
|
||||
@@ -1248,20 +1224,73 @@ class Iface:
|
||||
print(row)
|
||||
|
||||
def pr_wifi_ssids(self):
|
||||
hdr = (f"{'SSID':<{PadWifiScan.ssid}}"
|
||||
f"{'ENCRYPTION':<{PadWifiScan.encryption}}"
|
||||
f"{'SIGNAL':<{PadWifiScan.signal}}")
|
||||
print("\nAVAILABLE NETWORKS:")
|
||||
ssid_table = SimpleTable([
|
||||
Column('SSID'),
|
||||
Column('SECURITY'),
|
||||
Column('SIGNAL'),
|
||||
Column('CHANNEL')
|
||||
])
|
||||
|
||||
print(Decore.invert(hdr))
|
||||
results = self.wifi.get("scan-results", {})
|
||||
station = self.wifi.get("station", {})
|
||||
results = station.get("scan-results", {})
|
||||
for result in results:
|
||||
encstr = ", ".join(result["encryption"])
|
||||
status = rssi_to_status(result["rssi"])
|
||||
row = f"{result['ssid']:<{PadWifiScan.ssid}}"
|
||||
row += f"{encstr:<{PadWifiScan.encryption}}"
|
||||
row += f"{status:<{PadWifiScan.signal}}"
|
||||
encstr = ", ".join(result.get("encryption", ["Unknown"]))
|
||||
status = rssi_to_status(result.get("rssi", -100))
|
||||
channel = result.get("channel", "?")
|
||||
|
||||
print(row)
|
||||
ssid_table.row(result.get('ssid', 'Hidden'), encstr, status, channel)
|
||||
ssid_table.print()
|
||||
|
||||
def pr_wifi_stations(self):
|
||||
"""Display connected stations for AP mode"""
|
||||
if not self.wifi:
|
||||
return
|
||||
|
||||
# Get stations from access-point container
|
||||
ap = self.wifi.get("access-point", {})
|
||||
stations_data = ap.get("stations", {})
|
||||
stations = stations_data.get("station", [])
|
||||
|
||||
if not stations:
|
||||
return
|
||||
|
||||
print("\nCONNECTED STATIONS:")
|
||||
stations_table = SimpleTable([
|
||||
Column('MAC'),
|
||||
Column('SIGNAL'),
|
||||
Column('TIME'),
|
||||
Column('RX PKT'),
|
||||
Column('TX PKT'),
|
||||
Column('RX BYTES'),
|
||||
Column('TX BYTES'),
|
||||
Column('RX SPEED'),
|
||||
Column('TX SPEED')
|
||||
])
|
||||
|
||||
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 "------"
|
||||
|
||||
conn_time = station.get("connected-time", 0)
|
||||
time_str = f"{conn_time}s"
|
||||
|
||||
rx_pkt = station.get("rx-packets", 0)
|
||||
tx_pkt = station.get("tx-packets", 0)
|
||||
rx_bytes = station.get("rx-bytes", 0)
|
||||
tx_bytes = station.get("tx-bytes", 0)
|
||||
|
||||
# Speed in 100 kbit/s units, convert to Mbps for display
|
||||
rx_speed = station.get("rx-speed", 0)
|
||||
tx_speed = station.get("tx-speed", 0)
|
||||
rx_speed_str = f"{rx_speed / 10:.1f}" if rx_speed else "-"
|
||||
tx_speed_str = f"{tx_speed / 10:.1f}" if tx_speed else "-"
|
||||
|
||||
stations_table.row(mac, signal_str, time_str, rx_pkt, tx_pkt,
|
||||
rx_bytes, tx_bytes, rx_speed_str, tx_speed_str)
|
||||
|
||||
stations_table.print()
|
||||
|
||||
|
||||
def pr_proto_wifi(self, pipe=''):
|
||||
@@ -1269,20 +1298,35 @@ class Iface:
|
||||
print(row)
|
||||
ssid = None
|
||||
rssi = None
|
||||
mode = None
|
||||
|
||||
if self.wifi:
|
||||
rssi=self.wifi.get("rssi")
|
||||
ssid=self.wifi.get("ssid")
|
||||
if ssid is None:
|
||||
ssid="------"
|
||||
|
||||
if rssi is None:
|
||||
signal="------"
|
||||
# Detect mode: AP has "stations", Station has "rssi" or "scan-results"
|
||||
ap=self.wifi.get("access-point", {})
|
||||
if ap:
|
||||
ssid = ap.get("ssid", "------")
|
||||
mode = "AP"
|
||||
stations_data = ap.get("stations", {})
|
||||
stations = stations_data.get("station", [])
|
||||
station_count = len(stations)
|
||||
data_str = f"{mode}, ssid: {ssid}, stations: {station_count}"
|
||||
else:
|
||||
station=self.wifi.get("station", {})
|
||||
ssid = station.get("ssid", "------")
|
||||
rssi = station.get("rssi")
|
||||
mode = "Station"
|
||||
if rssi is not None:
|
||||
signal = rssi_to_status(rssi)
|
||||
data_str = f"{mode}, ssid: {ssid}, signal: {signal}"
|
||||
else:
|
||||
data_str = f"{mode}, ssid: {ssid}"
|
||||
else:
|
||||
signal=rssi_to_status(rssi)
|
||||
data_str = f"ssid: {ssid}, signal: {signal}"
|
||||
data_str = "ssid: ------"
|
||||
|
||||
row = f"{'':<{Pad.iface}}"
|
||||
row = f"{'':<{Pad.flags}}"
|
||||
row += f"{pipe:<{Pad.iface}}"
|
||||
row = f"{'':<{Pad.flags}}"
|
||||
row += f"{pipe:<{Pad.iface}}"
|
||||
row += f"{'wifi':<{Pad.proto}}"
|
||||
row += f"{'':<{Pad.state}}{data_str}"
|
||||
print(row)
|
||||
@@ -1550,13 +1594,37 @@ class Iface:
|
||||
else:
|
||||
print(f"{'ipv6 addresses':<{20}}:")
|
||||
|
||||
if self.in_octets and self.out_octets:
|
||||
print(f"{'in-octets':<{20}}: {self.in_octets}")
|
||||
print(f"{'out-octets':<{20}}: {self.out_octets}")
|
||||
|
||||
frame = get_json_data([], self.data,'ieee802-ethernet-interface:ethernet',
|
||||
'statistics', 'frame')
|
||||
|
||||
if self.wifi:
|
||||
ssid=self.wifi.get('ssid', "----")
|
||||
rssi=self.wifi.get('rssi', "----")
|
||||
print(f"{'SSID':<{20}}: {ssid}")
|
||||
print(f"{'Signal':<{20}}: {rssi}")
|
||||
print("")
|
||||
self.pr_wifi_ssids()
|
||||
# Detect mode: AP has "stations", Station has "rssi" or "scan-results"
|
||||
ap = self.wifi.get('access-point')
|
||||
if ap:
|
||||
mode = "access-point"
|
||||
ssid = ap.get('ssid', "----")
|
||||
stations_data = ap.get("stations", {})
|
||||
stations = stations_data.get("station", [])
|
||||
print(f"{'mode':<{20}}: {mode}")
|
||||
print(f"{'ssid':<{20}}: {ssid}")
|
||||
print(f"{'connected stations':<{20}}: {len(stations)}")
|
||||
self.pr_wifi_stations()
|
||||
else:
|
||||
mode = "station"
|
||||
station = self.wifi.get('station', {})
|
||||
rssi = station.get('rssi')
|
||||
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 "scan-results" in station:
|
||||
self.pr_wifi_ssids()
|
||||
|
||||
if self.gre:
|
||||
print(f"{'local address':<{20}}: {self.gre['local']}")
|
||||
@@ -1567,12 +1635,6 @@ class Iface:
|
||||
print(f"{'remote address':<{20}}: {self.vxlan['remote']}")
|
||||
print(f"{'VxLAN id':<{20}}: {self.vxlan['vni']}")
|
||||
|
||||
if self.in_octets and self.out_octets:
|
||||
print(f"{'in-octets':<{20}}: {self.in_octets}")
|
||||
print(f"{'out-octets':<{20}}: {self.out_octets}")
|
||||
|
||||
frame = get_json_data([], self.data,'ieee802-ethernet-interface:ethernet',
|
||||
'statistics', 'frame')
|
||||
if frame:
|
||||
print("")
|
||||
for key, val in frame.items():
|
||||
@@ -1953,9 +2015,6 @@ def show_services(json):
|
||||
services_data = get_json_data({}, json, 'ietf-system:system-state', 'infix-system:services')
|
||||
services = services_data.get("service", [])
|
||||
|
||||
# This is the first usage of simple table. I assume this will be
|
||||
# copied so I left a lot of comments. If you copy it feel free
|
||||
# to be less verbose..
|
||||
service_table = SimpleTable([
|
||||
Column('NAME'),
|
||||
Column('STATUS'),
|
||||
@@ -2005,9 +2064,9 @@ def show_hardware(json):
|
||||
motherboard = [c for c in components if c.get("class") == "iana-hardware:chassis"]
|
||||
usb_ports = [c for c in components if c.get("class") == "infix-hardware:usb"]
|
||||
sensors = [c for c in components if c.get("class") == "iana-hardware:sensor"]
|
||||
wifi_radios = [c for c in components if c.get("class") == "infix-hardware:wifi"]
|
||||
|
||||
# Determine overall width (use the wider of the two sections)
|
||||
width = max(PadUsbPort.table_width(), PadSensor.table_width())
|
||||
width = max(PadSensor.table_width(), 100)
|
||||
|
||||
# Display full-width inverted heading
|
||||
print(Decore.invert(f"{'HARDWARE COMPONENTS':<{width}}"))
|
||||
@@ -2027,18 +2086,70 @@ def show_hardware(json):
|
||||
if board.get("hardware-rev"):
|
||||
print(f"Hardware Revision : {board['hardware-rev']}")
|
||||
|
||||
if wifi_radios:
|
||||
Decore.title("WiFi radios", width)
|
||||
|
||||
radios_table = SimpleTable([
|
||||
Column('NAME'),
|
||||
Column('MANUFACTURER'),
|
||||
Column('BANDS', 'right'),
|
||||
Column('STANDARDS', 'right'),
|
||||
Column('MAX AP', 'right')
|
||||
])
|
||||
|
||||
for component in wifi_radios:
|
||||
phy = component.get("name", "")
|
||||
manufacturer = component.get("mfg-name", "Unknown")
|
||||
|
||||
radio_data = component.get("infix-hardware:wifi-radio", {})
|
||||
|
||||
bands = radio_data.get("bands", [])
|
||||
band_names = []
|
||||
has_ht = False
|
||||
has_vht = False
|
||||
has_he = False
|
||||
|
||||
for band in bands:
|
||||
if band.get("name"):
|
||||
band_names.append(band["name"])
|
||||
if band.get("ht-capable"):
|
||||
has_ht = True
|
||||
if band.get("vht-capable"):
|
||||
has_vht = True
|
||||
if band.get("he-capable"):
|
||||
has_he = True
|
||||
|
||||
bands_str = "/".join(band_names) if band_names else "Unknown"
|
||||
|
||||
standards = []
|
||||
if has_ht:
|
||||
standards.append("11n")
|
||||
if has_vht:
|
||||
standards.append("11ac")
|
||||
if has_he:
|
||||
standards.append("11ax")
|
||||
standard_str = "/".join(standards) if standards else "Unknown"
|
||||
|
||||
max_if = radio_data.get("max-interfaces", {})
|
||||
max_ap = max_if.get('ap', 'N/A') if max_if else 'N/A'
|
||||
|
||||
radios_table.row(phy, manufacturer, bands_str, standard_str, max_ap)
|
||||
radios_table.print()
|
||||
|
||||
if usb_ports:
|
||||
Decore.title("USB Ports", width)
|
||||
hdr = (f"{'NAME':<{PadUsbPort.name}}"
|
||||
f"{'STATE':<{PadUsbPort.state}}"
|
||||
f"{'OPER':<{PadUsbPort.oper}}")
|
||||
# Pad header to full width
|
||||
hdr = f"{hdr:<{width}}"
|
||||
print(Decore.invert(hdr))
|
||||
|
||||
usb_table = SimpleTable([
|
||||
Column('NAME'),
|
||||
Column('STATE'),
|
||||
Column('OPER')
|
||||
])
|
||||
|
||||
for component in usb_ports:
|
||||
port = USBport(component)
|
||||
port.print()
|
||||
usb_table.row(port.name, port.state, port.oper)
|
||||
|
||||
usb_table.print()
|
||||
|
||||
if sensors:
|
||||
Decore.title("Sensors", width)
|
||||
@@ -4342,6 +4453,8 @@ def main():
|
||||
show_firewall_logs(args.limit)
|
||||
elif args.command == "show-ntp":
|
||||
show_ntp(json_data)
|
||||
elif args.command == "show-wifi-radio":
|
||||
show_wifi_radio(json_data)
|
||||
elif args.command == "show-bfd":
|
||||
show_bfd(json_data)
|
||||
elif args.command == "show-bfd-status":
|
||||
|
||||
@@ -87,6 +87,9 @@ def main():
|
||||
elif args.model == 'ietf-bfd-ip-sh':
|
||||
from . import ietf_bfd_ip_sh
|
||||
yang_data = ietf_bfd_ip_sh.operational()
|
||||
elif args.model == 'infix-wifi-radio':
|
||||
from . import infix_wifi_radio
|
||||
yang_data = infix_wifi_radio.operational()
|
||||
else:
|
||||
common.LOG.warning("Unsupported model %s", args.model)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import datetime
|
||||
import os
|
||||
import glob
|
||||
import re
|
||||
import sys
|
||||
|
||||
from .common import insert, YangDate
|
||||
from .host import HOST
|
||||
@@ -150,65 +151,44 @@ def normalize_sensor_name(name):
|
||||
|
||||
def get_wifi_phy_info():
|
||||
"""
|
||||
Discover WiFi PHYs and map them to bands and interface names.
|
||||
Discover WiFi PHYs using iw list command.
|
||||
Returns dict: {phy_name: {band: str, iface: str, description: str}}
|
||||
|
||||
Example: {"phy0": {"band": "2.4 GHz", "iface": "wlan0", "description": "WiFi Radio (2.4 GHz)"}}
|
||||
Example: {"radio0": {"band": "2.4 GHz", "iface": "wlan0", "description": "WiFi Radio (2.4 GHz)"}}
|
||||
"""
|
||||
phy_info = {}
|
||||
|
||||
try:
|
||||
# Enumerate PHYs from /sys/class/ieee80211/
|
||||
ieee80211_path = "/sys/class/ieee80211"
|
||||
if not os.path.exists(ieee80211_path):
|
||||
# Use iw.py to list all PHYs
|
||||
phys = HOST.run_json(("/usr/libexec/infix/iw.py", "list"), default=[])
|
||||
if not phys:
|
||||
return phy_info
|
||||
|
||||
for phy in os.listdir(ieee80211_path):
|
||||
if not phy.startswith("phy"):
|
||||
continue
|
||||
# Initialize PHY info for each PHY
|
||||
for phy in phys:
|
||||
phy_info[phy] = {"band": "Unknown", "iface": None, "description": None}
|
||||
|
||||
phy_path = os.path.join(ieee80211_path, phy)
|
||||
info = {"band": "Unknown", "iface": None, "description": None}
|
||||
# Create a mapping from PHY number to PHY name
|
||||
phy_num_to_name = {}
|
||||
for phy_name in phy_info.keys():
|
||||
# Extract number from radio/phy name (e.g., "0" from "radio0" or "phy0")
|
||||
num_match = re.search(r'(\d+)$', phy_name)
|
||||
if num_match:
|
||||
phy_num = num_match.group(1)
|
||||
phy_num_to_name[phy_num] = phy_name
|
||||
|
||||
# Try to determine band from device path or hwmon name
|
||||
# The hwmon device usually tells us: mt7915_phy0, mt7915_phy1, etc.
|
||||
# We'll check supported frequencies to determine band
|
||||
try:
|
||||
# Read supported bands - check if device supports 5 GHz
|
||||
# Most dual-band chips expose phy0 as 2.4 GHz and phy1 as 5 GHz
|
||||
device_path = os.path.join(phy_path, "device")
|
||||
if os.path.exists(device_path):
|
||||
# Simple heuristic: phy0 is usually 2.4 GHz, phy1 is 5 GHz
|
||||
# This works for most MediaTek chips (mt7915, mt7921, etc.)
|
||||
if phy == "phy0":
|
||||
info["band"] = "2.4 GHz"
|
||||
elif phy == "phy1":
|
||||
info["band"] = "5 GHz"
|
||||
elif phy == "phy2":
|
||||
info["band"] = "6 GHz" # WiFi 6E
|
||||
except:
|
||||
pass
|
||||
# Find associated virtual interfaces using iw.py dev
|
||||
dev_map = HOST.run_json(("/usr/libexec/infix/iw.py", "dev"), default={})
|
||||
|
||||
# Find associated interface by checking which interface has a phy80211 link to this PHY
|
||||
try:
|
||||
net_path = "/sys/class/net"
|
||||
if os.path.exists(net_path):
|
||||
for iface in os.listdir(net_path):
|
||||
phy_link = os.path.join(net_path, iface, "phy80211")
|
||||
if os.path.islink(phy_link):
|
||||
# Read the link target and extract PHY name
|
||||
try:
|
||||
link_target = os.readlink(phy_link)
|
||||
linked_phy = os.path.basename(link_target)
|
||||
if linked_phy == phy:
|
||||
info["iface"] = iface
|
||||
break
|
||||
except:
|
||||
continue
|
||||
except:
|
||||
pass
|
||||
# dev_map is a dict mapping PHY numbers to list of interfaces
|
||||
for phy_num, interfaces in dev_map.items():
|
||||
phy_name = phy_num_to_name.get(phy_num)
|
||||
if phy_name and phy_name in phy_info and interfaces:
|
||||
# Use the first interface
|
||||
phy_info[phy_name]["iface"] = interfaces[0]
|
||||
|
||||
# Build description
|
||||
# Build descriptions
|
||||
for phy, info in phy_info.items():
|
||||
if info["iface"] and info["band"] != "Unknown":
|
||||
info["description"] = f"WiFi Radio {info['iface']} ({info['band']})"
|
||||
elif info["band"] != "Unknown":
|
||||
@@ -218,8 +198,6 @@ def get_wifi_phy_info():
|
||||
else:
|
||||
info["description"] = "WiFi Radio"
|
||||
|
||||
phy_info[phy] = info
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -248,7 +226,8 @@ def hwmon_sensor_components():
|
||||
device_sensors[base_name].append(sensor_component)
|
||||
|
||||
try:
|
||||
hwmon_devices = glob.glob("/sys/class/hwmon/hwmon*")
|
||||
hwmon_entries = HOST.run(("ls", "/sys/class/hwmon"), default="").split()
|
||||
hwmon_devices = [os.path.join("/sys/class/hwmon", entry) for entry in hwmon_entries if entry.startswith("hwmon")]
|
||||
|
||||
for hwmon_path in hwmon_devices:
|
||||
try:
|
||||
@@ -257,6 +236,12 @@ def hwmon_sensor_components():
|
||||
continue
|
||||
|
||||
device_name = HOST.read(name_path).strip()
|
||||
|
||||
# Check if device/name exists (e.g., for WiFi radios) and use that instead
|
||||
device_name_path = os.path.join(hwmon_path, "device", "name")
|
||||
if HOST.exists(device_name_path):
|
||||
device_name = HOST.read(device_name_path).strip()
|
||||
|
||||
base_name = normalize_sensor_name(device_name)
|
||||
|
||||
# Helper to create sensor component with human-readable description
|
||||
@@ -281,7 +266,9 @@ def hwmon_sensor_components():
|
||||
return component
|
||||
|
||||
# Temperature sensors
|
||||
for temp_file in glob.glob(os.path.join(hwmon_path, "temp*_input")):
|
||||
temp_entries = HOST.run(("ls", hwmon_path), default="").split()
|
||||
temp_files = [os.path.join(hwmon_path, e) for e in temp_entries if e.startswith("temp") and e.endswith("_input")]
|
||||
for temp_file in temp_files:
|
||||
try:
|
||||
sensor_num = os.path.basename(temp_file).split('_')[0].replace('temp', '')
|
||||
value = int(HOST.read(temp_file).strip())
|
||||
@@ -298,7 +285,9 @@ def hwmon_sensor_components():
|
||||
continue
|
||||
|
||||
# Fan sensors (RPM from tachometer)
|
||||
for fan_file in glob.glob(os.path.join(hwmon_path, "fan*_input")):
|
||||
fan_entries = HOST.run(("ls", hwmon_path), default="").split()
|
||||
fan_files = [os.path.join(hwmon_path, e) for e in fan_entries if e.startswith("fan") and e.endswith("_input")]
|
||||
for fan_file in fan_files:
|
||||
try:
|
||||
sensor_num = os.path.basename(fan_file).split('_')[0].replace('fan', '')
|
||||
value = int(HOST.read(fan_file).strip())
|
||||
@@ -316,9 +305,11 @@ def hwmon_sensor_components():
|
||||
|
||||
# PWM fan sensors (duty cycle percentage)
|
||||
# Only add if no fan*_input exists for this device (avoid duplicates)
|
||||
has_rpm_sensor = bool(glob.glob(os.path.join(hwmon_path, "fan*_input")))
|
||||
has_rpm_sensor = bool(fan_files)
|
||||
if not has_rpm_sensor:
|
||||
for pwm_file in glob.glob(os.path.join(hwmon_path, "pwm[0-9]*")):
|
||||
pwm_entries = HOST.run(("ls", hwmon_path), default="").split()
|
||||
pwm_files = [os.path.join(hwmon_path, e) for e in pwm_entries if e.startswith("pwm") and e[3:].replace('_', '').isdigit() if len(e) > 3]
|
||||
for pwm_file in pwm_files:
|
||||
# Skip pwm*_enable, pwm*_mode, etc. - only process pwm1, pwm2, etc.
|
||||
pwm_basename = os.path.basename(pwm_file)
|
||||
if not pwm_basename.replace('pwm', '').isdigit():
|
||||
@@ -345,7 +336,9 @@ def hwmon_sensor_components():
|
||||
continue
|
||||
|
||||
# Voltage sensors
|
||||
for voltage_file in glob.glob(os.path.join(hwmon_path, "in*_input")):
|
||||
voltage_entries = HOST.run(("ls", hwmon_path), default="").split()
|
||||
voltage_files = [os.path.join(hwmon_path, e) for e in voltage_entries if e.startswith("in") and e.endswith("_input")]
|
||||
for voltage_file in voltage_files:
|
||||
try:
|
||||
sensor_num = os.path.basename(voltage_file).split('_')[0].replace('in', '')
|
||||
value = int(HOST.read(voltage_file).strip())
|
||||
@@ -363,7 +356,9 @@ def hwmon_sensor_components():
|
||||
continue
|
||||
|
||||
# Current sensors
|
||||
for current_file in glob.glob(os.path.join(hwmon_path, "curr*_input")):
|
||||
current_entries = HOST.run(("ls", hwmon_path), default="").split()
|
||||
current_files = [os.path.join(hwmon_path, e) for e in current_entries if e.startswith("curr") and e.endswith("_input")]
|
||||
for current_file in current_files:
|
||||
try:
|
||||
sensor_num = os.path.basename(current_file).split('_')[0].replace('curr', '')
|
||||
value = int(HOST.read(current_file).strip())
|
||||
@@ -381,7 +376,9 @@ def hwmon_sensor_components():
|
||||
continue
|
||||
|
||||
# Power sensors
|
||||
for power_file in glob.glob(os.path.join(hwmon_path, "power*_input")):
|
||||
power_entries = HOST.run(("ls", hwmon_path), default="").split()
|
||||
power_files = [os.path.join(hwmon_path, e) for e in power_entries if e.startswith("power") and e.endswith("_input")]
|
||||
for power_file in power_files:
|
||||
try:
|
||||
sensor_num = os.path.basename(power_file).split('_')[0].replace('power', '')
|
||||
value = int(HOST.read(power_file).strip())
|
||||
@@ -426,8 +423,8 @@ def hwmon_sensor_components():
|
||||
wifi_info = get_wifi_phy_info()
|
||||
for component in components:
|
||||
name = component.get("name", "")
|
||||
# Match phy0, phy1, etc. sensors
|
||||
if name.startswith("phy") and name in wifi_info:
|
||||
# Match radio0, radio1, etc. sensors
|
||||
if name.startswith("radio") and name in wifi_info:
|
||||
phy = wifi_info[name]
|
||||
# Add WiFi-specific description
|
||||
component["description"] = phy["description"]
|
||||
@@ -448,7 +445,8 @@ def thermal_sensor_components():
|
||||
|
||||
try:
|
||||
# Find all thermal zones
|
||||
thermal_zones = glob.glob("/sys/class/thermal/thermal_zone*")
|
||||
thermal_entries = HOST.run(("ls", "/sys/class/thermal"), default="").split()
|
||||
thermal_zones = [os.path.join("/sys/class/thermal", entry) for entry in thermal_entries if entry.startswith("thermal_zone")]
|
||||
|
||||
for zone_path in thermal_zones:
|
||||
try:
|
||||
@@ -496,6 +494,179 @@ def thermal_sensor_components():
|
||||
return components
|
||||
|
||||
|
||||
def get_survey_data(ifname):
|
||||
"""Get channel survey data using iw.py script"""
|
||||
channels = []
|
||||
|
||||
try:
|
||||
survey_data = HOST.run_json(("/usr/libexec/infix/iw.py", "survey", ifname), default=[])
|
||||
|
||||
for entry in survey_data:
|
||||
channel = {
|
||||
"frequency": entry.get("frequency"),
|
||||
"in-use": entry.get("in_use", False)
|
||||
}
|
||||
|
||||
# Add optional fields if present
|
||||
if "noise" in entry:
|
||||
channel["noise"] = entry["noise"]
|
||||
if "active_time" in entry:
|
||||
channel["active-time"] = entry["active_time"]
|
||||
if "busy_time" in entry:
|
||||
channel["busy-time"] = entry["busy_time"]
|
||||
if "receive_time" in entry:
|
||||
channel["receive-time"] = entry["receive_time"]
|
||||
if "transmit_time" in entry:
|
||||
channel["transmit-time"] = entry["transmit_time"]
|
||||
|
||||
channels.append(channel)
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return channels
|
||||
|
||||
|
||||
def get_phy_info(phy_name):
|
||||
"""Get complete PHY information using iw.py script"""
|
||||
try:
|
||||
return HOST.run_json(("/usr/libexec/infix/iw.py", "info", phy_name), default={})
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def convert_iw_phy_info_for_yanger(phy_info):
|
||||
"""
|
||||
Convert iw.py phy_info format to yanger format.
|
||||
Input: iw.py format with 'bands', 'driver', 'manufacturer', 'interface_combinations'
|
||||
Output: yanger format with renamed/restructured fields
|
||||
"""
|
||||
result = {"bands": [], "driver": None, "manufacturer": "Unknown", "max-interfaces": {}}
|
||||
|
||||
# Convert bands - iw.py already uses snake_case for capabilities
|
||||
for band in phy_info.get("bands", []):
|
||||
band_data = {
|
||||
"band": str(band.get("band", 0)),
|
||||
"name": band.get("name", "Unknown")
|
||||
}
|
||||
|
||||
# Add capability flags (iw.py uses snake_case: ht_capable, vht_capable, he_capable)
|
||||
if band.get("ht_capable"):
|
||||
band_data["ht-capable"] = True
|
||||
if band.get("vht_capable"):
|
||||
band_data["vht-capable"] = True
|
||||
if band.get("he_capable"):
|
||||
band_data["he-capable"] = True
|
||||
|
||||
result["bands"].append(band_data)
|
||||
|
||||
# Copy driver and manufacturer
|
||||
if phy_info.get("driver"):
|
||||
result["driver"] = phy_info["driver"]
|
||||
if phy_info.get("manufacturer"):
|
||||
result["manufacturer"] = phy_info["manufacturer"]
|
||||
|
||||
# Convert interface combinations to max-interfaces
|
||||
# Find max AP interfaces from combinations
|
||||
for comb in phy_info.get("interface_combinations", []):
|
||||
for limit in comb.get("limits", []):
|
||||
if "AP" in limit.get("types", []):
|
||||
ap_max = limit.get("max", 0)
|
||||
if "ap" not in result["max-interfaces"] or ap_max > result["max-interfaces"]["ap"]:
|
||||
result["max-interfaces"]["ap"] = ap_max
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def wifi_radio_components():
|
||||
"""
|
||||
Create WiFi radio components with complete operational data.
|
||||
Returns a list of hardware components for WiFi radios.
|
||||
"""
|
||||
components = []
|
||||
wifi_info = get_wifi_phy_info()
|
||||
|
||||
for phy_name, phy_data in wifi_info.items():
|
||||
component = {
|
||||
"name": phy_name,
|
||||
"class": "infix-hardware:wifi",
|
||||
"description": phy_data.get("description", "WiFi Radio")
|
||||
}
|
||||
|
||||
# Initialize wifi-radio data structure
|
||||
wifi_radio_data = {}
|
||||
|
||||
# Get complete PHY information from iw.py script
|
||||
iw_info = get_phy_info(phy_name)
|
||||
|
||||
# Convert iw.py format to yanger format
|
||||
phy_details = convert_iw_phy_info_for_yanger(iw_info)
|
||||
|
||||
# Add manufacturer to component
|
||||
if phy_details.get("manufacturer") and phy_details["manufacturer"] != "Unknown":
|
||||
component["mfg-name"] = phy_details["manufacturer"]
|
||||
|
||||
# Add bands
|
||||
if phy_details.get("bands"):
|
||||
wifi_radio_data["bands"] = phy_details["bands"]
|
||||
|
||||
# Add driver
|
||||
if phy_details.get("driver"):
|
||||
wifi_radio_data["driver"] = phy_details["driver"]
|
||||
|
||||
# Add max-interfaces
|
||||
if phy_details.get("max-interfaces"):
|
||||
wifi_radio_data["max-interfaces"] = phy_details["max-interfaces"]
|
||||
|
||||
# Add max TX power from iw info
|
||||
if iw_info.get("max_txpower"):
|
||||
wifi_radio_data["max-txpower"] = iw_info["max_txpower"]
|
||||
|
||||
# Add supported channels from band frequencies
|
||||
supported_channels = []
|
||||
for band in iw_info.get("bands", []):
|
||||
for freq in band.get("frequencies", []):
|
||||
# Convert frequency to channel number
|
||||
if 2412 <= freq <= 2484:
|
||||
channel = (freq - 2407) // 5
|
||||
elif 5170 <= freq <= 5825:
|
||||
channel = (freq - 5000) // 5
|
||||
elif 5955 <= freq <= 7115:
|
||||
channel = (freq - 5950) // 5
|
||||
else:
|
||||
continue
|
||||
supported_channels.append(channel)
|
||||
|
||||
if supported_channels:
|
||||
wifi_radio_data['supported-channels'] = sorted(set(supported_channels))
|
||||
|
||||
# Count virtual interfaces from iw info
|
||||
num_ifaces = iw_info.get('num_virtual_interfaces', 0)
|
||||
wifi_radio_data['num-virtual-interfaces'] = num_ifaces
|
||||
|
||||
# Get survey data if we have an interface
|
||||
iface = phy_data.get("iface")
|
||||
if iface:
|
||||
try:
|
||||
channels = get_survey_data(iface)
|
||||
|
||||
if channels:
|
||||
wifi_radio_data["survey"] = {
|
||||
"channel": channels
|
||||
}
|
||||
except Exception:
|
||||
# If survey fails, continue without survey data
|
||||
pass
|
||||
|
||||
# Add wifi-radio data to component
|
||||
if wifi_radio_data:
|
||||
component["infix-hardware:wifi-radio"] = wifi_radio_data
|
||||
|
||||
components.append(component)
|
||||
|
||||
return components
|
||||
|
||||
|
||||
def operational():
|
||||
systemjson = HOST.read_json("/run/system.json")
|
||||
|
||||
@@ -507,6 +678,7 @@ def operational():
|
||||
usb_port_components(systemjson) +
|
||||
hwmon_sensor_components() +
|
||||
thermal_sensor_components() +
|
||||
wifi_radio_components() +
|
||||
[],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,38 +2,213 @@ from ..host import HOST
|
||||
import json
|
||||
import re
|
||||
|
||||
def wifi(ifname):
|
||||
wifi_data={}
|
||||
|
||||
def detect_wifi_mode(ifname):
|
||||
"""Detect if interface is in AP or Station mode"""
|
||||
try:
|
||||
output = HOST.run(tuple(f"iw dev {ifname} info".split()), default="")
|
||||
for line in output.splitlines():
|
||||
if 'type' in line.lower():
|
||||
if 'ap' in line.lower():
|
||||
return 'ap'
|
||||
else:
|
||||
return 'station'
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Default to station mode
|
||||
return 'station'
|
||||
|
||||
|
||||
def find_primary_interface_from_config(ifname):
|
||||
"""Find primary interface by reading hostapd config files"""
|
||||
try:
|
||||
file_list = HOST.run(tuple("ls /etc/hostapd-*.conf".split()), default="")
|
||||
if not file_list:
|
||||
return None
|
||||
|
||||
for config_file in file_list.splitlines():
|
||||
config_file = config_file.strip()
|
||||
if not config_file:
|
||||
continue
|
||||
|
||||
try:
|
||||
content = HOST.run(tuple(f"cat {config_file}".split()), default="")
|
||||
if not content:
|
||||
continue
|
||||
|
||||
if f"interface={ifname}" in content or f"bss={ifname}" in content:
|
||||
for line in content.splitlines():
|
||||
if line.startswith("interface="):
|
||||
return line.split("=", 1)[1].strip()
|
||||
except Exception:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def wifi_ap(ifname):
|
||||
"""Get operational data for AP mode using hostapd_cli"""
|
||||
ap_data = {}
|
||||
|
||||
try:
|
||||
data=HOST.run(tuple(f"wpa_cli -i {ifname} status".split()), default="")
|
||||
primary_if = find_primary_interface_from_config(ifname)
|
||||
if not primary_if:
|
||||
return {}
|
||||
|
||||
data = HOST.run(tuple(f"hostapd_cli -i {primary_if} status".split()), default="")
|
||||
if not data:
|
||||
return {}
|
||||
|
||||
# Find our interface's SSID, different for bss and primary, because it is
|
||||
if ifname == primary_if:
|
||||
# Primary interface - get ssid[0] or ssid
|
||||
for line in data.splitlines():
|
||||
if "=" in line:
|
||||
try:
|
||||
k, v = line.split("=", 1)
|
||||
if k in ("ssid[0]", "ssid"):
|
||||
ap_data["ssid"] = v
|
||||
break
|
||||
except ValueError:
|
||||
continue
|
||||
else:
|
||||
# Secondary BSS - find in BSS array
|
||||
bss_idx = None
|
||||
for line in data.splitlines():
|
||||
if "=" in line:
|
||||
try:
|
||||
k, v = line.split("=", 1)
|
||||
if v == ifname and k.startswith("bss["):
|
||||
bss_idx = k[4:-1] # Extract index from bss[N]
|
||||
break
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
if bss_idx:
|
||||
for line in data.splitlines():
|
||||
if "=" in line:
|
||||
try:
|
||||
k, v = line.split("=", 1)
|
||||
if k == f"ssid[{bss_idx}]":
|
||||
ap_data["ssid"] = v
|
||||
break
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
stations_data = HOST.run(tuple(f"iw dev {ifname} station dump".split()), default="")
|
||||
stations = parse_iw_stations(stations_data)
|
||||
|
||||
if stations:
|
||||
ap_data["stations"] = {
|
||||
"station": stations
|
||||
}
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Nest data inside access-point container to match YANG schema
|
||||
return {
|
||||
"access-point": ap_data
|
||||
} if ap_data else {}
|
||||
|
||||
|
||||
def parse_iw_stations(output):
|
||||
"""Parse iw station dump output to get connected stations"""
|
||||
stations = []
|
||||
current_station = None
|
||||
|
||||
for line in output.splitlines():
|
||||
line = line.strip()
|
||||
|
||||
# Station line: "Station aa:bb:cc:dd:ee:ff (on wifiX)"
|
||||
if line.startswith("Station "):
|
||||
if current_station:
|
||||
stations.append(current_station)
|
||||
# Extract MAC address
|
||||
parts = line.split()
|
||||
if len(parts) >= 2:
|
||||
current_station = {
|
||||
"mac-address": parts[1].lower()
|
||||
}
|
||||
elif current_station:
|
||||
# Parse station attributes
|
||||
try:
|
||||
# Lines are in format "key: value" with tabs
|
||||
if ":" not in line:
|
||||
continue
|
||||
|
||||
parts = line.split(":", 1)
|
||||
key = parts[0].strip()
|
||||
value = parts[1].strip()
|
||||
|
||||
if key == "signal":
|
||||
# Format: "-42 dBm" or "-42 [-44] dBm"
|
||||
rssi = int(value.split()[0])
|
||||
current_station["rssi"] = rssi
|
||||
elif key == "connected time":
|
||||
# Format: "123 seconds"
|
||||
seconds = int(value.split()[0])
|
||||
current_station["connected-time"] = seconds
|
||||
elif key == "rx packets":
|
||||
current_station["rx-packets"] = int(value)
|
||||
elif key == "tx packets":
|
||||
current_station["tx-packets"] = int(value)
|
||||
elif key == "rx bytes":
|
||||
current_station["rx-bytes"] = int(value)
|
||||
elif key == "tx bytes":
|
||||
current_station["tx-bytes"] = int(value)
|
||||
elif key == "tx bitrate":
|
||||
# Format: "866.7 MBit/s ..." - extract speed and convert to 100kbit/s units
|
||||
speed_mbps = float(value.split()[0])
|
||||
current_station["tx-speed"] = int(speed_mbps * 10)
|
||||
elif key == "rx bitrate":
|
||||
# Format: "780.0 MBit/s ..." - extract speed and convert to 100kbit/s units
|
||||
speed_mbps = float(value.split()[0])
|
||||
current_station["rx-speed"] = int(speed_mbps * 10)
|
||||
except (ValueError, KeyError, IndexError):
|
||||
# Skip invalid values
|
||||
continue
|
||||
|
||||
# Add last station
|
||||
if current_station:
|
||||
stations.append(current_station)
|
||||
|
||||
return stations
|
||||
|
||||
|
||||
def wifi_station(ifname):
|
||||
"""Get operational data for Station mode using wpa_cli"""
|
||||
station_data = {}
|
||||
|
||||
try:
|
||||
data = HOST.run(tuple(f"wpa_cli -i {ifname} status".split()), default="")
|
||||
|
||||
if data != "":
|
||||
for line in data.splitlines():
|
||||
try:
|
||||
if "=" not in line:
|
||||
continue
|
||||
k,v = line.split("=", 1)
|
||||
k, v = line.split("=", 1)
|
||||
if k == "ssid":
|
||||
wifi_data["ssid"] = v
|
||||
if k == "wpa_state" and v == "DISCONNECTED": # wpa_suppicant has most likely restarted, restart scanning
|
||||
HOST.run(tuple(f"wpa_cli -i {ifname} scan".split()), default="")
|
||||
station_data["ssid"] = v
|
||||
except ValueError:
|
||||
# Skip malformed lines
|
||||
continue
|
||||
|
||||
try:
|
||||
data=HOST.run(tuple(f"wpa_cli -i {ifname} signal_poll".split()), default="FAIL")
|
||||
data = HOST.run(tuple(f"wpa_cli -i {ifname} signal_poll".split()), default="FAIL")
|
||||
|
||||
# signal_poll return FAIL not connected
|
||||
# signal_poll return FAIL if not connected
|
||||
if data.strip() != "FAIL":
|
||||
for line in data.splitlines():
|
||||
try:
|
||||
if "=" not in line:
|
||||
continue
|
||||
k,v = line.strip().split("=", 1)
|
||||
k, v = line.strip().split("=", 1)
|
||||
if k == "RSSI":
|
||||
wifi_data["rssi"]=int(v)
|
||||
station_data["rssi"] = int(v)
|
||||
except (ValueError, KeyError):
|
||||
# Skip malformed lines or invalid integers
|
||||
continue
|
||||
@@ -45,14 +220,28 @@ def wifi(ifname):
|
||||
pass
|
||||
|
||||
try:
|
||||
data=HOST.run(tuple(f"wpa_cli -i {ifname} scan_result".split()), default="FAIL")
|
||||
data = HOST.run(tuple(f"wpa_cli -i {ifname} scan_result".split()), default="FAIL")
|
||||
if data != "FAIL":
|
||||
wifi_data["scan-results"] = parse_wpa_scan_result(data)
|
||||
scan_results = parse_wpa_scan_result(data)
|
||||
if scan_results:
|
||||
station_data["scan-results"] = scan_results
|
||||
except Exception:
|
||||
# If scan results fail, just omit them
|
||||
pass
|
||||
|
||||
return wifi_data
|
||||
# Always nest data inside station container to match YANG schema
|
||||
# In scan-only mode, this will be just scan-results with no ssid/rssi
|
||||
return {"station": station_data} if station_data else {}
|
||||
|
||||
|
||||
def wifi(ifname):
|
||||
"""Main entry point - detect mode and return appropriate data"""
|
||||
mode = detect_wifi_mode(ifname)
|
||||
|
||||
if mode == 'ap':
|
||||
return wifi_ap(ifname)
|
||||
else:
|
||||
return wifi_station(ifname)
|
||||
|
||||
|
||||
def parse_wpa_scan_result(scan_output):
|
||||
@@ -106,7 +295,7 @@ def parse_wpa_scan_result(scan_output):
|
||||
|
||||
# Convert to list and sort by RSSI (best first)
|
||||
result = list(networks.values())
|
||||
result.sort(key=lambda x: x['rssi'], reverse=False)
|
||||
result.sort(key=lambda x: x['rssi'], reverse=True)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
[7mHARDWARE COMPONENTS [0m
|
||||
────────────────────────────────────────────────────────────
|
||||
[7mHARDWARE COMPONENTS [0m
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
[1mBoard Information[0m
|
||||
Model : VM
|
||||
Model : Standard PC (i440FX + PIIX, 1996)
|
||||
Manufacturer : QEMU
|
||||
────────────────────────────────────────────────────────────
|
||||
Base MAC Address : 00:a0:85:00:03:00
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
[1mUSB Ports[0m
|
||||
[7mNAME STATE OPER [0m
|
||||
USB locked enabled
|
||||
USB2 locked enabled
|
||||
[7mNAME STATE OPER [0m
|
||||
USB1 locked enabled
|
||||
USB2 locked enabled
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"name": "mainboard",
|
||||
"class": "iana-hardware:chassis",
|
||||
"mfg-name": "QEMU",
|
||||
"model-name": "VM",
|
||||
"model-name": "Standard PC (i440FX + PIIX, 1996)",
|
||||
"infix-hardware:phys-address": "00:a0:85:00:03:00",
|
||||
"state": {
|
||||
"admin-state": "unknown",
|
||||
@@ -21,7 +21,7 @@
|
||||
"admin-state": "locked",
|
||||
"oper-state": "enabled"
|
||||
},
|
||||
"name": "USB",
|
||||
"name": "USB1",
|
||||
"class": "infix-hardware:usb"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -120,13 +120,13 @@
|
||||
"search": []
|
||||
},
|
||||
"clock": {
|
||||
"boot-datetime": "2025-04-30T09:47:32+00:00",
|
||||
"current-datetime": "2025-04-30T09:48:01+00:00"
|
||||
"boot-datetime": "2026-01-02T20:42:11+00:00",
|
||||
"current-datetime": "2026-01-02T20:43:23+00:00"
|
||||
},
|
||||
"platform": {
|
||||
"os-name": "Infix",
|
||||
"os-version": "v25.04.0-rc1-3-g8daf1571-dirty",
|
||||
"os-release": "v25.04.0-rc1-3-g8daf1571-dirty",
|
||||
"os-version": "v25.11.0-135-g44d2af57-dirty",
|
||||
"os-release": "v25.11.0-135-g44d2af57-dirty",
|
||||
"machine": "x86_64"
|
||||
},
|
||||
"infix-system:services": {
|
||||
@@ -385,6 +385,38 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"infix-system:resource-usage": {
|
||||
"memory": {
|
||||
"total": "355076",
|
||||
"free": "134936",
|
||||
"available": "225652"
|
||||
},
|
||||
"load-average": {
|
||||
"load-1min": "0.12",
|
||||
"load-5min": "0.05",
|
||||
"load-15min": "0.02"
|
||||
},
|
||||
"filesystem": [
|
||||
{
|
||||
"mount-point": "/",
|
||||
"size": "142720",
|
||||
"used": "142720",
|
||||
"available": "0"
|
||||
},
|
||||
{
|
||||
"mount-point": "/var",
|
||||
"size": "86427",
|
||||
"used": "281",
|
||||
"available": "79270"
|
||||
},
|
||||
{
|
||||
"mount-point": "/cfg",
|
||||
"size": "14073",
|
||||
"used": "67",
|
||||
"available": "12860"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
"ietf-hardware:hardware": {
|
||||
"component": [
|
||||
{
|
||||
"name": "mainboard",
|
||||
"class": "iana-hardware:chassis",
|
||||
"infix-hardware:phys-address": "00:a0:85:00:03:00",
|
||||
"mfg-name": "QEMU",
|
||||
"model-name": "VM",
|
||||
"model-name": "Standard PC (i440FX + PIIX, 1996)",
|
||||
"name": "mainboard",
|
||||
"state": {
|
||||
"admin-state": "unknown",
|
||||
"oper-state": "enabled"
|
||||
@@ -17,7 +18,7 @@
|
||||
},
|
||||
{
|
||||
"class": "infix-hardware:usb",
|
||||
"name": "USB",
|
||||
"name": "USB1",
|
||||
"state": {
|
||||
"admin-state": "locked",
|
||||
"oper-state": "enabled"
|
||||
@@ -49,8 +50,8 @@
|
||||
},
|
||||
"ietf-system:system-state": {
|
||||
"clock": {
|
||||
"boot-datetime": "2025-04-30T09:47:32+00:00",
|
||||
"current-datetime": "2025-04-30T09:48:01+00:00"
|
||||
"boot-datetime": "2026-01-02T20:42:11+00:00",
|
||||
"current-datetime": "2026-01-02T20:43:23+00:00"
|
||||
},
|
||||
"infix-system:dns-resolver": {
|
||||
"options": {},
|
||||
@@ -95,30 +96,30 @@
|
||||
{
|
||||
"available": "0",
|
||||
"mount-point": "/",
|
||||
"size": "70912",
|
||||
"used": "70912"
|
||||
"size": "142720",
|
||||
"used": "142720"
|
||||
},
|
||||
{
|
||||
"available": "79314",
|
||||
"available": "79270",
|
||||
"mount-point": "/var",
|
||||
"size": "86459",
|
||||
"used": "267"
|
||||
"size": "86427",
|
||||
"used": "281"
|
||||
},
|
||||
{
|
||||
"available": "12861",
|
||||
"available": "12860",
|
||||
"mount-point": "/cfg",
|
||||
"size": "14073",
|
||||
"used": "66"
|
||||
"used": "67"
|
||||
}
|
||||
],
|
||||
"load-average": {
|
||||
"load-15min": "0.01",
|
||||
"load-1min": "0.16",
|
||||
"load-5min": "0.03"
|
||||
"load-15min": "0.02",
|
||||
"load-1min": "0.12",
|
||||
"load-5min": "0.05"
|
||||
},
|
||||
"memory": {
|
||||
"available": "259640",
|
||||
"free": "187776",
|
||||
"available": "225652",
|
||||
"free": "134936",
|
||||
"total": "355076"
|
||||
}
|
||||
},
|
||||
@@ -449,8 +450,8 @@
|
||||
"platform": {
|
||||
"machine": "x86_64",
|
||||
"os-name": "Infix",
|
||||
"os-release": "v25.04.0-rc1-3-g8daf1571-dirty",
|
||||
"os-version": "v25.04.0-rc1-3-g8daf1571-dirty"
|
||||
"os-release": "v25.11.0-135-g44d2af57-dirty",
|
||||
"os-version": "v25.11.0-135-g44d2af57-dirty"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
NAME="Infix"
|
||||
ID=infix
|
||||
PRETTY_NAME="Infix OS — Immutable.Friendly.Secure v25.04.0-rc1-3-g8daf1571-dirty"
|
||||
PRETTY_NAME="Infix OS — Immutable.Friendly.Secure v25.11.0-135-g44d2af57-dirty"
|
||||
ID_LIKE="buildroot"
|
||||
DEFAULT_HOSTNAME=ix
|
||||
VERSION="v25.04.0-rc1-3-g8daf1571-dirty"
|
||||
VERSION_ID=v25.04.0-rc1-3-g8daf1571-dirty
|
||||
BUILD_ID="v25.04.0-rc1-3-g8daf1571-dirty"
|
||||
DEFAULT_HOSTNAME=infix
|
||||
VERSION="v25.11.0-135-g44d2af57-dirty"
|
||||
VERSION_ID=v25.11.0-135-g44d2af57-dirty
|
||||
BUILD_ID="v25.11.0-135-g44d2af57-dirty"
|
||||
IMAGE_ID="infix-x86_64"
|
||||
ARCHITECTURE="x86_64"
|
||||
HOME_URL=https://github.com/kernelkit/infix/
|
||||
VENDOR_NAME="KernelKit"
|
||||
VENDOR_HOME="https://github.com/kernelkit"
|
||||
DOCUMENTATION_URL="https://github.com/kernelkit/infix/tree/main/doc"
|
||||
VENDOR_HOME="https://kernelkit.org"
|
||||
DOCUMENTATION_URL="https://kernelkit.org/infix/"
|
||||
SUPPORT_URL="mailto:kernelkit@googlegroups.com"
|
||||
INFIX_DESC="Infix is an operating system based on Linux and modeled with YANG. It can be set up both as a switch, with offloading using switchdev, a router with firewalling, or a secure end device. All while supporting advanced networking scenarios and running Docker containers."
|
||||
INFIX_DESC="Infix is an immutable, friendly, and secure operating system that turns any ARM or x86 device into a powerful, manageable network appliance. Deploy on anything from 5 Raspberry Pi boards to enterprise switches as routers, IoT gateways, or edge devices. Infix models Linux networking features using YANG so you can manage your devices using NETCONF/RESTCONF APIs and focus on your business logic running in isolated containers."
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
0.12 0.05 0.02 2/117 4690
|
||||
@@ -0,0 +1,40 @@
|
||||
MemTotal: 355076 kB
|
||||
MemFree: 134936 kB
|
||||
MemAvailable: 225652 kB
|
||||
Buffers: 4252 kB
|
||||
Cached: 91640 kB
|
||||
SwapCached: 0 kB
|
||||
Active: 34012 kB
|
||||
Inactive: 136744 kB
|
||||
Active(anon): 1588 kB
|
||||
Inactive(anon): 75512 kB
|
||||
Active(file): 32424 kB
|
||||
Inactive(file): 61232 kB
|
||||
Unevictable: 0 kB
|
||||
Mlocked: 0 kB
|
||||
SwapTotal: 0 kB
|
||||
SwapFree: 0 kB
|
||||
Dirty: 40 kB
|
||||
Writeback: 0 kB
|
||||
AnonPages: 74952 kB
|
||||
Mapped: 41652 kB
|
||||
Shmem: 2232 kB
|
||||
KReclaimable: 8072 kB
|
||||
Slab: 30180 kB
|
||||
SReclaimable: 8072 kB
|
||||
SUnreclaim: 22108 kB
|
||||
KernelStack: 1872 kB
|
||||
PageTables: 2372 kB
|
||||
SecPageTables: 0 kB
|
||||
NFS_Unstable: 0 kB
|
||||
Bounce: 0 kB
|
||||
WritebackTmp: 0 kB
|
||||
CommitLimit: 177536 kB
|
||||
Committed_AS: 278488 kB
|
||||
VmallocTotal: 34359738367 kB
|
||||
VmallocUsed: 9564 kB
|
||||
VmallocChunk: 0 kB
|
||||
Percpu: 304 kB
|
||||
DirectMap4k: 41872 kB
|
||||
DirectMap2M: 346112 kB
|
||||
DirectMap1G: 0 kB
|
||||
@@ -1 +1 @@
|
||||
28.16 15.60
|
||||
71.22 64.17
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"vendor": "QEMU", "product-name": "VM", "part-number": null, "serial-number": null, "mac-address": "00:a0:85:00:03:00", "factory-password-hash": "$5$mI/zpOAqZYKLC2WU$i7iPzZiIjOjrBF3NyftS9CCq8dfYwHwrmUK097Jca9A", "vpd": {"product": {"board": "product", "available": false, "trusted": true, "data": {}}}, "usb-ports": [{"name": "USB", "path": "/sys/bus/usb/devices/usb1"}, {"name": "USB2", "path": "/sys/bus/usb/devices/usb2"}]}
|
||||
{"vendor": "QEMU", "product-name": "Standard PC (i440FX + PIIX, 1996)", "product-version": "pc-i440fx-8.0", "part-number": null, "serial-number": null, "mac-address": "00:a0:85:00:03:00", "factory-password-hash": "$5$mI/zpOAqZYKLC2WU$i7iPzZiIjOjrBF3NyftS9CCq8dfYwHwrmUK097Jca9A", "vpd": {"product": {"board": "product", "available": false, "trusted": true, "data": {}}}, "usb-ports": [{"name": "USB1", "path": "/sys/bus/usb/devices/usb1"}, {"name": "USB2", "path": "/sys/bus/usb/devices/usb2"}]}
|
||||
@@ -1 +1 @@
|
||||
0
|
||||
2
|
||||
|
||||
@@ -1 +1 @@
|
||||
0
|
||||
2
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1,2 @@
|
||||
Filesystem 1K-blocks Used Available Use% Mounted on
|
||||
/dev/root 142720 142720 0 100% /
|
||||
@@ -0,0 +1,2 @@
|
||||
Filesystem 1K-blocks Used Available Use% Mounted on
|
||||
cfg-overlay 14073 67 12860 1% /cfg
|
||||
@@ -0,0 +1,2 @@
|
||||
Filesystem 1K-blocks Used Available Use% Mounted on
|
||||
/dev/vda6 86427 281 79270 0% /mnt/var
|
||||
@@ -0,0 +1 @@
|
||||
cooling_device0
|
||||
@@ -1,2 +1,2 @@
|
||||
1746006481
|
||||
1767386603
|
||||
|
||||
|
||||
Reference in New Issue
Block a user