mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-28 11:43:01 +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": {
|
"state": {
|
||||||
"admin-state": "unlocked"
|
"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": {
|
"ietf-interfaces:interfaces": {
|
||||||
@@ -150,12 +168,36 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "wifi0",
|
"name": "wifi0-ap",
|
||||||
"type": "infix-if-type:wifi"
|
"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",
|
"name": "wifi1-ap",
|
||||||
"type": "infix-if-type:wifi"
|
"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": {}
|
"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": {
|
"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:/> configure
|
||||||
admin@infix:/config/> edit keystore symmetric-key mywifi
|
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 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
|
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 \
|
service name:wpa_supplicant :%i \
|
||||||
[2345] wpa_supplicant -s -i %i -c /etc/wpa_supplicant-%i.conf -P/var/run/wpa_supplicant-%i.pid \
|
[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]
|
[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
|
### Changes
|
||||||
|
|
||||||
- Upgrade Linux kernel to 6.12.63 (LTS)
|
- 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)
|
policy, keeping snapshots from every 5 minutes (recent) to yearly (historical)
|
||||||
- Add support data collection script, useful when troubleshooting issues on
|
- Add support data collection script, useful when troubleshooting issues on
|
||||||
deployed systems. Gathers system information, logs, and more. Issue #1287
|
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
|
### Fixes
|
||||||
|
|
||||||
- Fix #1314: Raspberry Pi 4B with 1 or 8 GiB RAM does not boot. This was due
|
- 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
|
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
|
[v25.11.0][] - 2025-12-02
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|||||||
+367
-84
@@ -1,14 +1,29 @@
|
|||||||
# Wi-Fi (Wireless LAN)
|
# Wi-Fi (Wireless LAN)
|
||||||
|
|
||||||
Infix includes built-in Wi-Fi client support for connecting to
|
Infix includes comprehensive Wi-Fi support for both client (Station) and
|
||||||
wireless networks. When a compatible Wi-Fi adapter is detected, the
|
Access Point modes. When a compatible Wi-Fi adapter is detected, the system
|
||||||
system automatically begins scanning for available networks.
|
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
|
## Current Limitations
|
||||||
|
|
||||||
- Only client mode is supported (no access point functionality)
|
|
||||||
- USB hotplug is not supported - adapters must be present at boot
|
- USB hotplug is not supported - adapters must be present at boot
|
||||||
- Interface naming may be inconsistent with multiple USB Wi-Fi adapters
|
- 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
|
## Supported Wi-Fi Adapters
|
||||||
|
|
||||||
@@ -16,6 +31,8 @@ Wi-Fi support is primarily tested with Realtek chipset-based adapters.
|
|||||||
|
|
||||||
### Known Working Chipsets
|
### Known Working Chipsets
|
||||||
|
|
||||||
|
- Built-in Wi-Fi on Banana Pi r3
|
||||||
|
- Built-in Wi-Fi on Raspberry Pi 4/CM4
|
||||||
- RTL8821CU
|
- RTL8821CU
|
||||||
- Other Realtek chipsets may work but are not guaranteed
|
- 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
|
> Firmware requirements vary by chipset
|
||||||
> Check kernel logs if your adapter is not detected
|
> 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
|
Before configuring WiFi interfaces, you must first configure the WiFi radio.
|
||||||
detected, look for `wifi0` in `show interface`
|
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
|
admin@example:/> configure
|
||||||
INTERFACE PROTOCOL STATE DATA
|
admin@example:/config/> edit hardware component radio0 wifi-radio
|
||||||
lo loopback UP
|
admin@example:/config/hardware/component/radio0/wifi-radio/> set country-code DE
|
||||||
ipv4 127.0.0.1/8 (static)
|
admin@example:/config/hardware/component/radio0/wifi-radio/> set band 5GHz
|
||||||
ipv6 ::1/128 (static)
|
admin@example:/config/hardware/component/radio0/wifi-radio/> set channel 36
|
||||||
e1 ethernet UP 02:00:00:00:00:01
|
admin@example:/config/hardware/component/radio0/wifi-radio/> set enable-wifi6 true
|
||||||
ipv6 fe80::ff:fe00:1/64 (link-layer)
|
admin@example:/config/hardware/component/radio0/wifi-radio/> leave
|
||||||
ipv6 fec0::ff:fe00:1/64 (link-layer)
|
```
|
||||||
wifi0 ethernet DOWN f0:09:0d:36:5f:86
|
|
||||||
wifi ssid: ------, signal: ------
|
**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
|
**Step 2: Create WiFi interface with radio reference only**
|
||||||
```
|
|
||||||
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
|
|
||||||
|
|
||||||
```
|
```
|
||||||
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
|
name : wifi0
|
||||||
type : wifi
|
type : wifi
|
||||||
index : 3
|
index : 3
|
||||||
@@ -64,89 +174,262 @@ ipv6 addresses :
|
|||||||
SSID : ----
|
SSID : ----
|
||||||
Signal : ----
|
Signal : ----
|
||||||
|
|
||||||
SSID ENCRYPTION SIGNAL
|
SSID SECURITY SIGNAL
|
||||||
ssid1 WPA2-Personal excellent
|
MyNetwork WPA2-Personal excellent
|
||||||
ssid2 WPA2-Personal excellent
|
GuestWiFi WPA2-WPA3-Personal good
|
||||||
ssid3 WPA2-Personal excellent
|
CoffeeShop Open fair
|
||||||
ssid4 WPA2-Personal good
|
IoT-Devices WPA2-Personal good
|
||||||
ssid5 WPA2-Personal good
|
|
||||||
ssid6 WPA2-Personal good
|
|
||||||
```
|
```
|
||||||
|
|
||||||
In the CLI, signal strength is reported as: excellent, good, poor or
|
In the CLI, signal strength is reported as: excellent, good, fair or bad.
|
||||||
bad. For precise values, use NETCONF or RESTCONF, where the RSSI (in
|
For precise RSSI values in dBm, use NETCONF or RESTCONF to access the
|
||||||
dBm) is available in the operational datastore.
|
operational datastore directly.
|
||||||
|
|
||||||
Configure your Wi-Fi secret in the keystore, it should be between 8
|
### Connect to a Network
|
||||||
and 63 characters
|
|
||||||
|
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:/> configure
|
||||||
admin@example:/config/> edit keystore symmetric-key example
|
admin@example:/config/> edit keystore symmetric-key my-wifi-key
|
||||||
admin@example:/config/keystore/…/example/> set key-format wifi-preshared-key-format
|
admin@example:/config/keystore/…/my-wifi-key/> set key-format wifi-preshared-key-format
|
||||||
admin@example:/config/keystore/…/example/> set cleartext-symmetric-key mysecret
|
admin@example:/config/keystore/…/my-wifi-key/> set symmetric-key YourWiFiPassword
|
||||||
admin@example:/config/keystore/…/example/> leave
|
admin@example:/config/keystore/…/my-wifi-key/> leave
|
||||||
admin@example:/>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Configure the Wi-Fi settings, set secret to the name selected above
|
Then configure the WiFi interface for station mode:
|
||||||
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`
|
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
admin@example:/> configure
|
admin@example:/> configure
|
||||||
admin@example:/config/> edit interface wifi0
|
admin@example:/config/> edit interface wifi0
|
||||||
admin@example:/config/interface/wifi0/>
|
admin@example:/config/interface/wifi0/> set wifi station ssid MyNetwork
|
||||||
admin@example:/config/interface/wifi0/> set wifi ssid ssid1
|
admin@example:/config/interface/wifi0/> set wifi station security secret my-wifi-key
|
||||||
admin@example:/config/interface/wifi0/> set wifi secret example
|
|
||||||
admin@example:/config/interface/wifi0/> set wifi country-code SE
|
|
||||||
admin@example:/config/interface/wifi0/> leave
|
admin@example:/config/interface/wifi0/> leave
|
||||||
```
|
```
|
||||||
|
|
||||||
The Wi-Fi negotiation should now start immediately, provided that the
|
The interface will transition from scan-only mode to station mode and
|
||||||
SSID and pre-shared key are correct. You can verify the connection by
|
attempt to connect to the specified network.
|
||||||
running `show interface` again.
|
|
||||||
|
|
||||||
|
## 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
|
admin@example:/> configure
|
||||||
INTERFACE PROTOCOL STATE DATA
|
admin@example:/config/> edit keystore symmetric-key my-wifi-key
|
||||||
lo loopback UP
|
admin@example:/config/keystore/…/my-wifi-key/> set key-format wifi-preshared-key-format
|
||||||
ipv4 127.0.0.1/8 (static)
|
admin@example:/config/keystore/…/my-wifi-key/> set symmetric-key MyPassword123
|
||||||
ipv6 ::1/128 (static)
|
admin@example:/config/keystore/…/my-wifi-key/> leave
|
||||||
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:/>
|
### 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
|
## Troubleshooting Connection Issues
|
||||||
|
|
||||||
Use `show wifi scan wifi0` and `show interface` to verify signal strength
|
Use `show interface wifi0` to verify signal strength and connection status.
|
||||||
and connection status. If issues arise, try the following
|
If issues arise, try the following troubleshooting steps:
|
||||||
troubleshooting steps:
|
|
||||||
|
|
||||||
1. **Verify signal strength**: Check that the target network shows "good" or "excellent" signal
|
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 `ietf-keystore`
|
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
|
3. **Review logs**: Check system logs with `show log` for Wi-Fi related errors
|
||||||
4. **Regulatory compliance**: Ensure the country-code matches your location
|
4. **Regulatory compliance**: Ensure the country-code on the radio matches your location
|
||||||
5. **Hardware detection**: Confirm the adapter appears in `show interface`
|
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.
|
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_AUTOSCAN
|
||||||
select BR2_PACKAGE_WPA_SUPPLICANT_CLI
|
select BR2_PACKAGE_WPA_SUPPLICANT_CLI
|
||||||
select BR2_PACKAGE_WIRELESS_REGDB
|
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
|
select BR2_PACKAGE_IW
|
||||||
help
|
help
|
||||||
Enables WiFi in Infix. Enables all requried applications.
|
Enables WiFi in Infix. Enables all requried applications.
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
#
|
#
|
||||||
########################################################################
|
########################################################################
|
||||||
# The system factory-config, failure-config and test-config are derived
|
# The system factory-config, failure-config and test-config are derived
|
||||||
# from default settings snippets, from /usr/share/confd/factory.d, and
|
# from default settings snippets, from /usr/share/confd/factory.d, and
|
||||||
# some generated snippets, e.g., hostname (based on base MAC address)
|
# some generated snippets, e.g., hostname (based on base MAC address)
|
||||||
# and number of interfaces.
|
# and number of interfaces.
|
||||||
#
|
#
|
||||||
# The resulting factory-config is used to create the syrepo db (below)
|
# 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
|
# Both factory-config and failure-config are generated every boot
|
||||||
# regardless if there is a static /etc/factory-config.cfg or not.
|
# regardless if there is a static /etc/factory-config.cfg or not.
|
||||||
gen_factory_cfg
|
gen_factory_cfg
|
||||||
gen_failure_cfg
|
gen_failure_cfg
|
||||||
|
|
||||||
if [ -f "/mnt/aux/test-mode" ]; then
|
if [ -f "/mnt/aux/test-mode" ]; then
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ if jq -e '.["usb-ports"]' /run/system.json > /dev/null; then
|
|||||||
else
|
else
|
||||||
usb_ports=""
|
usb_ports=""
|
||||||
fi
|
fi
|
||||||
|
wifi_radios=$(/usr/libexec/infix/iw.py list 2>/dev/null | jq -r '.[]' || echo "")
|
||||||
|
|
||||||
|
|
||||||
gen_port()
|
gen_port()
|
||||||
{
|
{
|
||||||
@@ -20,6 +22,43 @@ gen_port()
|
|||||||
}
|
}
|
||||||
EOF
|
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
|
first=1
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
{
|
{
|
||||||
@@ -33,6 +72,13 @@ for port in $usb_ports; do
|
|||||||
first=0;
|
first=0;
|
||||||
gen_port "$port"
|
gen_port "$port"
|
||||||
done
|
done
|
||||||
|
for radio in $wifi_radios; do
|
||||||
|
if [ $first -eq 0 ]; then
|
||||||
|
echo -n ','
|
||||||
|
fi
|
||||||
|
first=0;
|
||||||
|
gen_radio $radio
|
||||||
|
done
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
]}
|
]}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# Migrate keystore symmetric key syntax to new YANG schema (RFC 9643)
|
# Rename cleartext-symmetric-key to symmetric-key
|
||||||
# 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
|
|
||||||
|
|
||||||
file=$1
|
file=$1
|
||||||
temp=${file}.tmp
|
temp=${file}.tmp
|
||||||
@@ -14,7 +11,7 @@ if .["ietf-keystore:keystore"]?."symmetric-keys"?."symmetric-key" then
|
|||||||
# Rename cleartext-key to cleartext-symmetric-key
|
# Rename cleartext-key to cleartext-symmetric-key
|
||||||
."key-type"."cleartext-key" as $key_value |
|
."key-type"."cleartext-key" as $key_value |
|
||||||
."key-type" |= (del(."cleartext-key") | . + {
|
."key-type" |= (del(."cleartext-key") | . + {
|
||||||
"cleartext-symmetric-key": $key_value
|
"symmetric-key": $key_value
|
||||||
})
|
})
|
||||||
else
|
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
|
migratedir = $(pkgdatadir)/migrate/1.7
|
||||||
dist_migrate_DATA = 10-netconf-server-tcp-params.sh \
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+606
-17
@@ -10,8 +10,14 @@
|
|||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
|
|
||||||
#include "core.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,
|
static int dir_cb(const char *fpath, const struct stat *sb,
|
||||||
int typeflag, struct FTW *ftwbuf)
|
int typeflag, struct FTW *ftwbuf)
|
||||||
@@ -150,6 +156,452 @@ out_free_xpath:
|
|||||||
free(xpath);
|
free(xpath);
|
||||||
return err;
|
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,
|
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)
|
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)
|
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;
|
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;
|
return SR_ERR_OK;
|
||||||
|
|
||||||
cifs = lydx_get_descendant(config, "hardware", "component", NULL);
|
|
||||||
difs = lydx_get_descendant(diff, "hardware", "component", NULL);
|
difs = lydx_get_descendant(diff, "hardware", "component", NULL);
|
||||||
|
|
||||||
LYX_LIST_FOR_EACH(difs, dif, "component") {
|
LYX_LIST_FOR_EACH(difs, dif, "component") {
|
||||||
enum lydx_op op;
|
enum lydx_op op;
|
||||||
struct lyd_node *state;
|
struct lyd_node *state, *cif;
|
||||||
const char *admin_state;
|
const char *admin_state;
|
||||||
const char *class, *name;
|
const char *class, *name;
|
||||||
|
|
||||||
op = lydx_get_op(dif);
|
op = lydx_get_op(dif);
|
||||||
name = lydx_get_cattr(dif, "name");
|
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") {
|
/* Get the current config node for this component */
|
||||||
if (strcmp(name, lydx_get_cattr(cif, "name")))
|
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;
|
continue;
|
||||||
|
|
||||||
class = lydx_get_cattr(cif, "class");
|
if (op == LYDX_OP_DELETE) {
|
||||||
if (strcmp(class, "infix-hardware:usb")) {
|
/* Handle USB deletion */
|
||||||
|
if (usb_authorize(confd->root, name, 0)) {
|
||||||
|
rc = SR_ERR_INTERNAL;
|
||||||
|
goto err;
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
state = lydx_get_child(dif, "state");
|
state = lydx_get_child(dif, "state");
|
||||||
admin_state = lydx_get_cattr(state, "admin-state");
|
admin_state = lydx_get_cattr(state, "admin-state");
|
||||||
if (usb_authorize(confd->root, name, !strcmp(admin_state, "unlocked"))) {
|
if (usb_authorize(confd->root, name, !strcmp(admin_state, "unlocked"))) {
|
||||||
rc = SR_ERR_INTERNAL;
|
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:
|
err:
|
||||||
|
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
int hardware_candidate_init(struct confd *confd)
|
int hardware_candidate_init(struct confd *confd)
|
||||||
|
|||||||
+97
-84
@@ -3,108 +3,121 @@
|
|||||||
|
|
||||||
#include "interfaces.h"
|
#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;
|
if (lydx_get_child(wifi, "access-point"))
|
||||||
char *encryption_str;
|
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;
|
int rc = SR_ERR_OK;
|
||||||
|
|
||||||
if (!secret && (ssid && country && encryption)) {
|
ifname = lydx_get_cattr(cif, "name");
|
||||||
/* Not an error, updated from two ways, interface cb and keystore cb. */
|
wifi = lydx_get_child(cif, "wifi");
|
||||||
return 0;
|
|
||||||
|
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");
|
radio = lydx_get_cattr(wifi, "radio");
|
||||||
if (!wpa) {
|
if (!radio) {
|
||||||
rc = SR_ERR_INTERNAL;
|
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;
|
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:
|
out:
|
||||||
|
fclose(iw);
|
||||||
return rc;
|
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;
|
const char *ifname;
|
||||||
struct lyd_node *wifi, *secret_node;
|
FILE *iw;
|
||||||
|
|
||||||
bool enabled;
|
ifname = lydx_get_cattr(dif, "name");
|
||||||
ifname = lydx_get_cattr(cif, "name");
|
|
||||||
|
|
||||||
if (cif && !lydx_get_child(cif, "wifi")) {
|
iw = dagger_fopen_net_exit(net, ifname, NETDAG_EXIT_POST, "wifi-iface.sh");
|
||||||
return wifi_gen_config(ifname, NULL, NULL, NULL, NULL, net);
|
if (!iw) {
|
||||||
|
ERROR("Failed to open dagger file for WiFi interface deletion");
|
||||||
|
return SR_ERR_INTERNAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
enabled = lydx_get_bool(cif, "enabled");
|
fprintf(iw, "# Generated by Infix confd - WiFi Interface Deletion\n");
|
||||||
wifi = lydx_get_child(cif, "wifi");
|
fprintf(iw, "ip link set %s down\n", ifname); /* Required to change modes. */
|
||||||
|
|
||||||
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, "iw dev %s disconnect\n", ifname);
|
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);
|
fclose(iw);
|
||||||
erasef(WPA_SUPPLICANT_CONF, ifname);
|
|
||||||
|
|
||||||
return SR_ERR_OK;
|
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:
|
case IFT_VXLAN:
|
||||||
return vxlan_gen(NULL, cif, ip);
|
return vxlan_gen(NULL, cif, ip);
|
||||||
case IFT_WIFI:
|
case IFT_WIFI:
|
||||||
return wifi_gen(NULL, cif, net);
|
return wifi_add_iface(cif, net);
|
||||||
case IFT_ETH:
|
case IFT_ETH:
|
||||||
return netdag_gen_ethtool(net, cif, dif);
|
return netdag_gen_ethtool(net, cif, dif);
|
||||||
case IFT_LO:
|
case IFT_LO:
|
||||||
@@ -448,7 +448,10 @@ static int netdag_gen_afspec_set(sr_session_ctx_t *session, struct dagger *net,
|
|||||||
case IFT_ETH:
|
case IFT_ETH:
|
||||||
return netdag_gen_ethtool(net, cif, dif);
|
return netdag_gen_ethtool(net, cif, dif);
|
||||||
case IFT_WIFI:
|
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_DUMMY:
|
||||||
case IFT_GRE:
|
case IFT_GRE:
|
||||||
case IFT_GRETAP:
|
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_BRIDGE:
|
||||||
case IFT_DUMMY:
|
case IFT_DUMMY:
|
||||||
case IFT_LO:
|
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;
|
break;
|
||||||
|
|
||||||
case IFT_WIFI:
|
|
||||||
case IFT_ETH:
|
case IFT_ETH:
|
||||||
return lydx_get_child(dif, "custom-phys-address");
|
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:
|
case IFT_LO:
|
||||||
eth_gen_del(dif, ip);
|
eth_gen_del(dif, ip);
|
||||||
break;
|
break;
|
||||||
case IFT_WIFI:
|
|
||||||
eth_gen_del(dif, ip);
|
|
||||||
wifi_gen_del(dif, net);
|
|
||||||
break;
|
|
||||||
case IFT_VETH:
|
case IFT_VETH:
|
||||||
veth_gen_del(dif, ip);
|
veth_gen_del(dif, ip);
|
||||||
break;
|
break;
|
||||||
|
case IFT_WIFI:
|
||||||
|
wifi_del_iface(dif, net);
|
||||||
|
break;
|
||||||
case IFT_BRIDGE:
|
case IFT_BRIDGE:
|
||||||
case IFT_DUMMY:
|
case IFT_DUMMY:
|
||||||
case IFT_GRE:
|
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)
|
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");
|
FILE *wait = dagger_fopen_net_init(net, ifname, NETDAG_INIT_TIMEOUT, "wait-interface.sh");
|
||||||
if (!wait) {
|
if (!wait) {
|
||||||
return -EIO;
|
return -EIO;
|
||||||
@@ -734,10 +737,9 @@ static int netdag_init_iface(struct lyd_node *cif)
|
|||||||
return vlan_add_deps(cif);
|
return vlan_add_deps(cif);
|
||||||
case IFT_VETH:
|
case IFT_VETH:
|
||||||
return veth_add_deps(cif);
|
return veth_add_deps(cif);
|
||||||
|
case IFT_WIFI:
|
||||||
case IFT_DUMMY:
|
case IFT_DUMMY:
|
||||||
case IFT_ETH:
|
case IFT_ETH:
|
||||||
case IFT_WIFI:
|
|
||||||
case IFT_GRE:
|
case IFT_GRE:
|
||||||
case IFT_GRETAP:
|
case IFT_GRETAP:
|
||||||
case IFT_LO:
|
case IFT_LO:
|
||||||
|
|||||||
@@ -25,15 +25,15 @@
|
|||||||
_map(IFT_BRIDGE, "infix-if-type:bridge") \
|
_map(IFT_BRIDGE, "infix-if-type:bridge") \
|
||||||
_map(IFT_DUMMY, "infix-if-type:dummy") \
|
_map(IFT_DUMMY, "infix-if-type:dummy") \
|
||||||
_map(IFT_ETH, "infix-if-type:ethernet") \
|
_map(IFT_ETH, "infix-if-type:ethernet") \
|
||||||
_map(IFT_WIFI, "infix-if-type:wifi") \
|
|
||||||
_map(IFT_GRE, "infix-if-type:gre") \
|
_map(IFT_GRE, "infix-if-type:gre") \
|
||||||
_map(IFT_GRETAP, "infix-if-type:gretap") \
|
_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_LO, "infix-if-type:loopback") \
|
||||||
_map(IFT_VETH, "infix-if-type:veth") \
|
_map(IFT_VETH, "infix-if-type:veth") \
|
||||||
_map(IFT_VLAN, "infix-if-type:vlan") \
|
_map(IFT_VLAN, "infix-if-type:vlan") \
|
||||||
_map(IFT_VXLAN, "infix-if-type:vxlan") \
|
_map(IFT_VXLAN, "infix-if-type:vxlan") \
|
||||||
/* */
|
_map(IFT_WIFI, "infix-if-type:wifi") \
|
||||||
|
/* */
|
||||||
|
|
||||||
enum iftype {
|
enum iftype {
|
||||||
#define ift_enum(_enum, _str) _enum,
|
#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);
|
int bridge_port_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip);
|
||||||
|
|
||||||
/* if-wifi.c */
|
/* if-wifi.c */
|
||||||
int wifi_gen(struct lyd_node *dif, struct lyd_node *cif, struct dagger *net);
|
int wifi_add_iface(struct lyd_node *cif, struct dagger *net);
|
||||||
int wifi_gen_del(struct lyd_node *dif, 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 */
|
/* if-gre.c */
|
||||||
int gre_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip);
|
int gre_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ MODULES=(
|
|||||||
"infix-syslog@2025-11-17.yang"
|
"infix-syslog@2025-11-17.yang"
|
||||||
"iana-hardware@2018-03-13.yang"
|
"iana-hardware@2018-03-13.yang"
|
||||||
"ietf-hardware@2018-03-13.yang -e hardware-state -e hardware-sensor"
|
"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"
|
"ieee802-dot1q-types@2022-10-29.yang"
|
||||||
"infix-ip@2025-11-02.yang"
|
"infix-ip@2025-11-02.yang"
|
||||||
"infix-if-type@2025-02-12.yang"
|
"infix-if-type@2025-02-12.yang"
|
||||||
|
|||||||
@@ -13,10 +13,18 @@ module infix-hardware {
|
|||||||
prefix yang;
|
prefix yang;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import infix-wifi-country-codes {
|
||||||
|
prefix iwcc;
|
||||||
|
}
|
||||||
|
|
||||||
organization "KernelKit";
|
organization "KernelKit";
|
||||||
contact "kernelkit@googlegroups.com";
|
contact "kernelkit@googlegroups.com";
|
||||||
description "Vital Product Data augmentation of ieee-hardware and deviations.";
|
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 {
|
revision 2025-10-30 {
|
||||||
description "Add phys-address leaf for hardware components and enable sensor support.";
|
description "Add phys-address leaf for hardware components and enable sensor support.";
|
||||||
reference "internal";
|
reference "internal";
|
||||||
@@ -38,6 +46,38 @@ module infix-hardware {
|
|||||||
description "A two-letter country code.";
|
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 {
|
identity usb {
|
||||||
base iahw:hardware-class;
|
base iahw:hardware-class;
|
||||||
description "This identity is used to describe a USB port";
|
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)" {
|
must "not(../ip:ipv4/ip:address or ../ip:ipv6/ip:address)" {
|
||||||
error-message "Bridge ports cannot have IP addresses configured.";
|
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.";
|
description "Bridge association and port specific settings.";
|
||||||
uses bridge-port-common;
|
uses bridge-port-common;
|
||||||
uses bridge-port-lower {
|
uses bridge-port-lower {
|
||||||
|
|||||||
@@ -110,6 +110,6 @@ module infix-if-type {
|
|||||||
if-feature wifi;
|
if-feature wifi;
|
||||||
base infix-interface-type;
|
base infix-interface-type;
|
||||||
base ianaift:ieee80211;
|
base ianaift:ieee80211;
|
||||||
description "WiFi interface";
|
description "WiFi (802.11) interface";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,11 @@ submodule infix-if-wifi {
|
|||||||
import infix-if-type {
|
import infix-if-type {
|
||||||
prefix infixift;
|
prefix infixift;
|
||||||
}
|
}
|
||||||
import infix-wifi-country-codes {
|
import ietf-hardware {
|
||||||
prefix iwcc;
|
prefix iehw;
|
||||||
|
}
|
||||||
|
import infix-hardware {
|
||||||
|
prefix ih;
|
||||||
}
|
}
|
||||||
|
|
||||||
organization "KernelKit";
|
organization "KernelKit";
|
||||||
@@ -30,12 +33,30 @@ submodule infix-if-wifi {
|
|||||||
description
|
description
|
||||||
"WiFi-specific extensions to the standard IETF interfaces model.
|
"WiFi-specific extensions to the standard IETF interfaces model.
|
||||||
|
|
||||||
This submodule defines configuration and operational data relevant to
|
This submodule defines configuration and operational data for WiFi
|
||||||
WiFi interfaces, including security settings, network
|
virtual interfaces, supporting both Access Point (AP) and Station
|
||||||
discovery, and regulatory compliance.
|
(client) modes.
|
||||||
|
|
||||||
It supports WiFi client mode and enables comprehensive management of
|
WiFi virtual interfaces are created on top of WiFi radios (PHYs)
|
||||||
wireless connections, including encryption, country codes, and scanning.";
|
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 {
|
revision 2025-12-12 {
|
||||||
description "Adapt to new revision of model ietf-keystore.";
|
description "Adapt to new revision of model ietf-keystore.";
|
||||||
@@ -43,7 +64,7 @@ submodule infix-if-wifi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
revision 2025-05-27 {
|
revision 2025-05-27 {
|
||||||
description "Initial revision.";
|
description "Initial revision (Station mode only).";
|
||||||
reference "internal";
|
reference "internal";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,29 +72,6 @@ submodule infix-if-wifi {
|
|||||||
description "WiFi support is an optional build-time feature in Infix.";
|
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" {
|
augment "/if:interfaces/if:interface" {
|
||||||
when "derived-from-or-self(if:type, 'infixift:wifi')" {
|
when "derived-from-or-self(if:type, 'infixift:wifi')" {
|
||||||
description
|
description
|
||||||
@@ -82,107 +80,370 @@ submodule infix-if-wifi {
|
|||||||
|
|
||||||
container wifi {
|
container wifi {
|
||||||
if-feature wifi;
|
if-feature wifi;
|
||||||
presence "Configure Wi-Fi settings";
|
presence "Configure Wi-Fi virtual interface";
|
||||||
|
|
||||||
description
|
description
|
||||||
"WiFi-specific configuration and operational data.";
|
"WiFi virtual interface configuration.
|
||||||
|
|
||||||
leaf country-code {
|
Each WiFi interface represents a virtual interface (VAP - Virtual
|
||||||
type iwcc:country-code;
|
Access Point, or Station) created on a physical radio.
|
||||||
mandatory true;
|
|
||||||
description
|
|
||||||
"Two-letter ISO 3166-1 country code for regulatory compliance.
|
|
||||||
|
|
||||||
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 radio {
|
||||||
|
type leafref {
|
||||||
|
path "/iehw:hardware/iehw:component/iehw:name";
|
||||||
}
|
|
||||||
|
|
||||||
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";
|
|
||||||
}
|
}
|
||||||
mandatory true;
|
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
|
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 {
|
choice wifi-mode {
|
||||||
type ks:central-symmetric-key-ref;
|
|
||||||
mandatory true;
|
|
||||||
must "../encryption != 'disabled'" {
|
|
||||||
error-message
|
|
||||||
"Pre-shared key required unless encryption is disabled.";
|
|
||||||
}
|
|
||||||
|
|
||||||
description
|
description
|
||||||
"Pre-shared key (PSK) for WPA-secured networks.";
|
"WiFi interface operating mode.
|
||||||
}
|
|
||||||
|
|
||||||
leaf rssi {
|
When no mode is configured, the interface operates in scan-only
|
||||||
config false;
|
mode, allowing discovery of available WiFi networks.
|
||||||
type int16;
|
|
||||||
units "dBm";
|
|
||||||
description
|
|
||||||
"Current received signal strength (RSSI) in dBm.
|
|
||||||
|
|
||||||
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 {
|
Note: A radio can host either:
|
||||||
config false;
|
- Multiple AP interfaces (multi-SSID), OR
|
||||||
key ssid;
|
- A single Station interface
|
||||||
description
|
|
||||||
"List of discovered networks.";
|
|
||||||
|
|
||||||
leaf ssid {
|
Mixing AP and Station on the same radio is not supported.";
|
||||||
type string;
|
|
||||||
description
|
case station {
|
||||||
"SSID of the discovered network.";
|
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 {
|
case access-point {
|
||||||
type string;
|
container access-point {
|
||||||
description
|
presence "Configure WiFi Access Point mode";
|
||||||
"BSSID of the discovered network.";
|
|
||||||
}
|
|
||||||
|
|
||||||
leaf rssi {
|
description
|
||||||
type int16;
|
"WiFi Access Point mode configuration.
|
||||||
units "dBm";
|
|
||||||
description
|
|
||||||
"Signal strength of the network.";
|
|
||||||
}
|
|
||||||
|
|
||||||
leaf channel {
|
In AP mode, the interface provides a WiFi network that
|
||||||
type int16;
|
clients can connect to.
|
||||||
description
|
|
||||||
"Channel on which the network was detected.";
|
|
||||||
}
|
|
||||||
|
|
||||||
leaf-list encryption {
|
Multiple AP interfaces can be created on the same radio
|
||||||
ordered-by user;
|
for multi-SSID support (Guest network, IoT network, etc.).
|
||||||
type string;
|
|
||||||
description
|
Example use case: Create WiFi hotspot.";
|
||||||
"Human-readable description of the detected security.";
|
|
||||||
|
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 {
|
import infix-if-type {
|
||||||
prefix infix-ift;
|
prefix infix-ift;
|
||||||
}
|
}
|
||||||
|
import ietf-hardware {
|
||||||
|
prefix iehw;
|
||||||
|
}
|
||||||
|
import infix-hardware {
|
||||||
|
prefix ih;
|
||||||
|
}
|
||||||
|
|
||||||
include infix-if-base;
|
include infix-if-base;
|
||||||
include infix-if-bridge;
|
include infix-if-bridge;
|
||||||
|
|||||||
@@ -55,9 +55,9 @@ module infix-keystore {
|
|||||||
}
|
}
|
||||||
augment "/ks:keystore/ks:symmetric-keys/ks:symmetric-key/ks:key-type" {
|
augment "/ks:keystore/ks:symmetric-keys/ks:symmetric-key/ks:key-type" {
|
||||||
case cleartext-symmetric-key {
|
case cleartext-symmetric-key {
|
||||||
leaf cleartext-symmetric-key {
|
leaf symmetric-key {
|
||||||
type string;
|
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)" {
|
"(string-length(.) >= 8 and string-length(.) <= 63)" {
|
||||||
error-message "WiFi pre-shared key must be 8-63 characters long";
|
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
|
The regulatory domain configuration follows the principles
|
||||||
established in IETF RFCs for wireless access point management.";
|
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 {
|
revision 2025-06-02 {
|
||||||
description
|
description
|
||||||
"Initial revision for WiFi country code support.";
|
"Initial revision for WiFi country code support.";
|
||||||
@@ -27,6 +32,7 @@ module infix-wifi-country-codes {
|
|||||||
|
|
||||||
typedef country-code {
|
typedef country-code {
|
||||||
type enumeration {
|
type enumeration {
|
||||||
|
enum "00" { description "World regulatory domain (no country restrictions)"; }
|
||||||
enum "AD" { description "Andorra"; }
|
enum "AD" { description "Andorra"; }
|
||||||
enum "AE" { description "United Arab Emirates"; }
|
enum "AE" { description "United Arab Emirates"; }
|
||||||
enum "AF" { description "Afghanistan"; }
|
enum "AF" { description "Afghanistan"; }
|
||||||
|
|||||||
+1
-15
@@ -422,19 +422,6 @@ def lldp(args: List[str]):
|
|||||||
return
|
return
|
||||||
cli_pretty(data, "show-lldp")
|
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:
|
def system(args: List[str]) -> None:
|
||||||
# Get system state from sysrepo
|
# Get system state from sysrepo
|
||||||
@@ -556,8 +543,7 @@ def execute_command(command: str, args: List[str]):
|
|||||||
'services': services,
|
'services': services,
|
||||||
'software': software,
|
'software': software,
|
||||||
'stp': stp,
|
'stp': stp,
|
||||||
'system': system,
|
'system': system
|
||||||
'wifi': wifi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if command in command_mapping:
|
if command in command_mapping:
|
||||||
|
|||||||
@@ -159,18 +159,6 @@ class PadDhcpServer:
|
|||||||
exp = 10
|
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:
|
class PadSensor:
|
||||||
name = 30
|
name = 30
|
||||||
value = 20
|
value = 20
|
||||||
@@ -190,12 +178,6 @@ class PadNtpSource:
|
|||||||
poll = 14
|
poll = 14
|
||||||
|
|
||||||
|
|
||||||
class PadWifiScan:
|
|
||||||
ssid = 40
|
|
||||||
encryption = 30
|
|
||||||
signal = 9
|
|
||||||
|
|
||||||
|
|
||||||
class PadLldp:
|
class PadLldp:
|
||||||
interface = 16
|
interface = 16
|
||||||
rem_idx = 10
|
rem_idx = 10
|
||||||
@@ -544,7 +526,7 @@ class Decore():
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def bright_green(txt):
|
def bright_green(txt):
|
||||||
return Decore.decorate("1;32", txt, "39")
|
return Decore.decorate("1;32", txt, "0")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def yellow(txt):
|
def yellow(txt):
|
||||||
@@ -847,12 +829,6 @@ class USBport:
|
|||||||
self.state = get_json_data('', self.data, 'state', 'admin-state')
|
self.state = get_json_data('', self.data, 'state', 'admin-state')
|
||||||
self.oper = get_json_data('', self.data, 'state', 'oper-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:
|
class Sensor:
|
||||||
def __init__(self, data):
|
def __init__(self, data):
|
||||||
@@ -1248,20 +1224,73 @@ class Iface:
|
|||||||
print(row)
|
print(row)
|
||||||
|
|
||||||
def pr_wifi_ssids(self):
|
def pr_wifi_ssids(self):
|
||||||
hdr = (f"{'SSID':<{PadWifiScan.ssid}}"
|
print("\nAVAILABLE NETWORKS:")
|
||||||
f"{'ENCRYPTION':<{PadWifiScan.encryption}}"
|
ssid_table = SimpleTable([
|
||||||
f"{'SIGNAL':<{PadWifiScan.signal}}")
|
Column('SSID'),
|
||||||
|
Column('SECURITY'),
|
||||||
|
Column('SIGNAL'),
|
||||||
|
Column('CHANNEL')
|
||||||
|
])
|
||||||
|
|
||||||
print(Decore.invert(hdr))
|
station = self.wifi.get("station", {})
|
||||||
results = self.wifi.get("scan-results", {})
|
results = station.get("scan-results", {})
|
||||||
for result in results:
|
for result in results:
|
||||||
encstr = ", ".join(result["encryption"])
|
encstr = ", ".join(result.get("encryption", ["Unknown"]))
|
||||||
status = rssi_to_status(result["rssi"])
|
status = rssi_to_status(result.get("rssi", -100))
|
||||||
row = f"{result['ssid']:<{PadWifiScan.ssid}}"
|
channel = result.get("channel", "?")
|
||||||
row += f"{encstr:<{PadWifiScan.encryption}}"
|
|
||||||
row += f"{status:<{PadWifiScan.signal}}"
|
|
||||||
|
|
||||||
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=''):
|
def pr_proto_wifi(self, pipe=''):
|
||||||
@@ -1269,20 +1298,35 @@ class Iface:
|
|||||||
print(row)
|
print(row)
|
||||||
ssid = None
|
ssid = None
|
||||||
rssi = None
|
rssi = None
|
||||||
|
mode = None
|
||||||
|
|
||||||
if self.wifi:
|
if self.wifi:
|
||||||
rssi=self.wifi.get("rssi")
|
# Detect mode: AP has "stations", Station has "rssi" or "scan-results"
|
||||||
ssid=self.wifi.get("ssid")
|
ap=self.wifi.get("access-point", {})
|
||||||
if ssid is None:
|
if ap:
|
||||||
ssid="------"
|
ssid = ap.get("ssid", "------")
|
||||||
|
mode = "AP"
|
||||||
if rssi is None:
|
stations_data = ap.get("stations", {})
|
||||||
signal="------"
|
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:
|
else:
|
||||||
signal=rssi_to_status(rssi)
|
data_str = "ssid: ------"
|
||||||
data_str = f"ssid: {ssid}, signal: {signal}"
|
|
||||||
|
|
||||||
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"{'wifi':<{Pad.proto}}"
|
||||||
row += f"{'':<{Pad.state}}{data_str}"
|
row += f"{'':<{Pad.state}}{data_str}"
|
||||||
print(row)
|
print(row)
|
||||||
@@ -1550,13 +1594,37 @@ class Iface:
|
|||||||
else:
|
else:
|
||||||
print(f"{'ipv6 addresses':<{20}}:")
|
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:
|
if self.wifi:
|
||||||
ssid=self.wifi.get('ssid', "----")
|
# Detect mode: AP has "stations", Station has "rssi" or "scan-results"
|
||||||
rssi=self.wifi.get('rssi', "----")
|
ap = self.wifi.get('access-point')
|
||||||
print(f"{'SSID':<{20}}: {ssid}")
|
if ap:
|
||||||
print(f"{'Signal':<{20}}: {rssi}")
|
mode = "access-point"
|
||||||
print("")
|
ssid = ap.get('ssid', "----")
|
||||||
self.pr_wifi_ssids()
|
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:
|
if self.gre:
|
||||||
print(f"{'local address':<{20}}: {self.gre['local']}")
|
print(f"{'local address':<{20}}: {self.gre['local']}")
|
||||||
@@ -1567,12 +1635,6 @@ class Iface:
|
|||||||
print(f"{'remote address':<{20}}: {self.vxlan['remote']}")
|
print(f"{'remote address':<{20}}: {self.vxlan['remote']}")
|
||||||
print(f"{'VxLAN id':<{20}}: {self.vxlan['vni']}")
|
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:
|
if frame:
|
||||||
print("")
|
print("")
|
||||||
for key, val in frame.items():
|
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_data = get_json_data({}, json, 'ietf-system:system-state', 'infix-system:services')
|
||||||
services = services_data.get("service", [])
|
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([
|
service_table = SimpleTable([
|
||||||
Column('NAME'),
|
Column('NAME'),
|
||||||
Column('STATUS'),
|
Column('STATUS'),
|
||||||
@@ -2005,9 +2064,9 @@ def show_hardware(json):
|
|||||||
motherboard = [c for c in components if c.get("class") == "iana-hardware:chassis"]
|
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"]
|
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"]
|
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(PadSensor.table_width(), 100)
|
||||||
width = max(PadUsbPort.table_width(), PadSensor.table_width())
|
|
||||||
|
|
||||||
# Display full-width inverted heading
|
# Display full-width inverted heading
|
||||||
print(Decore.invert(f"{'HARDWARE COMPONENTS':<{width}}"))
|
print(Decore.invert(f"{'HARDWARE COMPONENTS':<{width}}"))
|
||||||
@@ -2027,18 +2086,70 @@ def show_hardware(json):
|
|||||||
if board.get("hardware-rev"):
|
if board.get("hardware-rev"):
|
||||||
print(f"Hardware Revision : {board['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:
|
if usb_ports:
|
||||||
Decore.title("USB Ports", width)
|
Decore.title("USB Ports", width)
|
||||||
hdr = (f"{'NAME':<{PadUsbPort.name}}"
|
|
||||||
f"{'STATE':<{PadUsbPort.state}}"
|
usb_table = SimpleTable([
|
||||||
f"{'OPER':<{PadUsbPort.oper}}")
|
Column('NAME'),
|
||||||
# Pad header to full width
|
Column('STATE'),
|
||||||
hdr = f"{hdr:<{width}}"
|
Column('OPER')
|
||||||
print(Decore.invert(hdr))
|
])
|
||||||
|
|
||||||
for component in usb_ports:
|
for component in usb_ports:
|
||||||
port = USBport(component)
|
port = USBport(component)
|
||||||
port.print()
|
usb_table.row(port.name, port.state, port.oper)
|
||||||
|
|
||||||
|
usb_table.print()
|
||||||
|
|
||||||
if sensors:
|
if sensors:
|
||||||
Decore.title("Sensors", width)
|
Decore.title("Sensors", width)
|
||||||
@@ -4342,6 +4453,8 @@ def main():
|
|||||||
show_firewall_logs(args.limit)
|
show_firewall_logs(args.limit)
|
||||||
elif args.command == "show-ntp":
|
elif args.command == "show-ntp":
|
||||||
show_ntp(json_data)
|
show_ntp(json_data)
|
||||||
|
elif args.command == "show-wifi-radio":
|
||||||
|
show_wifi_radio(json_data)
|
||||||
elif args.command == "show-bfd":
|
elif args.command == "show-bfd":
|
||||||
show_bfd(json_data)
|
show_bfd(json_data)
|
||||||
elif args.command == "show-bfd-status":
|
elif args.command == "show-bfd-status":
|
||||||
|
|||||||
@@ -87,6 +87,9 @@ def main():
|
|||||||
elif args.model == 'ietf-bfd-ip-sh':
|
elif args.model == 'ietf-bfd-ip-sh':
|
||||||
from . import ietf_bfd_ip_sh
|
from . import ietf_bfd_ip_sh
|
||||||
yang_data = ietf_bfd_ip_sh.operational()
|
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:
|
else:
|
||||||
common.LOG.warning("Unsupported model %s", args.model)
|
common.LOG.warning("Unsupported model %s", args.model)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
import glob
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
from .common import insert, YangDate
|
from .common import insert, YangDate
|
||||||
from .host import HOST
|
from .host import HOST
|
||||||
@@ -150,65 +151,44 @@ def normalize_sensor_name(name):
|
|||||||
|
|
||||||
def get_wifi_phy_info():
|
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}}
|
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 = {}
|
phy_info = {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Enumerate PHYs from /sys/class/ieee80211/
|
# Use iw.py to list all PHYs
|
||||||
ieee80211_path = "/sys/class/ieee80211"
|
phys = HOST.run_json(("/usr/libexec/infix/iw.py", "list"), default=[])
|
||||||
if not os.path.exists(ieee80211_path):
|
if not phys:
|
||||||
return phy_info
|
return phy_info
|
||||||
|
|
||||||
for phy in os.listdir(ieee80211_path):
|
# Initialize PHY info for each PHY
|
||||||
if not phy.startswith("phy"):
|
for phy in phys:
|
||||||
continue
|
phy_info[phy] = {"band": "Unknown", "iface": None, "description": None}
|
||||||
|
|
||||||
phy_path = os.path.join(ieee80211_path, phy)
|
# Create a mapping from PHY number to PHY name
|
||||||
info = {"band": "Unknown", "iface": None, "description": None}
|
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
|
# Find associated virtual interfaces using iw.py dev
|
||||||
# The hwmon device usually tells us: mt7915_phy0, mt7915_phy1, etc.
|
dev_map = HOST.run_json(("/usr/libexec/infix/iw.py", "dev"), default={})
|
||||||
# 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 interface by checking which interface has a phy80211 link to this PHY
|
# dev_map is a dict mapping PHY numbers to list of interfaces
|
||||||
try:
|
for phy_num, interfaces in dev_map.items():
|
||||||
net_path = "/sys/class/net"
|
phy_name = phy_num_to_name.get(phy_num)
|
||||||
if os.path.exists(net_path):
|
if phy_name and phy_name in phy_info and interfaces:
|
||||||
for iface in os.listdir(net_path):
|
# Use the first interface
|
||||||
phy_link = os.path.join(net_path, iface, "phy80211")
|
phy_info[phy_name]["iface"] = interfaces[0]
|
||||||
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
|
|
||||||
|
|
||||||
# Build description
|
# Build descriptions
|
||||||
|
for phy, info in phy_info.items():
|
||||||
if info["iface"] and info["band"] != "Unknown":
|
if info["iface"] and info["band"] != "Unknown":
|
||||||
info["description"] = f"WiFi Radio {info['iface']} ({info['band']})"
|
info["description"] = f"WiFi Radio {info['iface']} ({info['band']})"
|
||||||
elif info["band"] != "Unknown":
|
elif info["band"] != "Unknown":
|
||||||
@@ -218,8 +198,6 @@ def get_wifi_phy_info():
|
|||||||
else:
|
else:
|
||||||
info["description"] = "WiFi Radio"
|
info["description"] = "WiFi Radio"
|
||||||
|
|
||||||
phy_info[phy] = info
|
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -248,7 +226,8 @@ def hwmon_sensor_components():
|
|||||||
device_sensors[base_name].append(sensor_component)
|
device_sensors[base_name].append(sensor_component)
|
||||||
|
|
||||||
try:
|
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:
|
for hwmon_path in hwmon_devices:
|
||||||
try:
|
try:
|
||||||
@@ -257,6 +236,12 @@ def hwmon_sensor_components():
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
device_name = HOST.read(name_path).strip()
|
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)
|
base_name = normalize_sensor_name(device_name)
|
||||||
|
|
||||||
# Helper to create sensor component with human-readable description
|
# Helper to create sensor component with human-readable description
|
||||||
@@ -281,7 +266,9 @@ def hwmon_sensor_components():
|
|||||||
return component
|
return component
|
||||||
|
|
||||||
# Temperature sensors
|
# 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:
|
try:
|
||||||
sensor_num = os.path.basename(temp_file).split('_')[0].replace('temp', '')
|
sensor_num = os.path.basename(temp_file).split('_')[0].replace('temp', '')
|
||||||
value = int(HOST.read(temp_file).strip())
|
value = int(HOST.read(temp_file).strip())
|
||||||
@@ -298,7 +285,9 @@ def hwmon_sensor_components():
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Fan sensors (RPM from tachometer)
|
# 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:
|
try:
|
||||||
sensor_num = os.path.basename(fan_file).split('_')[0].replace('fan', '')
|
sensor_num = os.path.basename(fan_file).split('_')[0].replace('fan', '')
|
||||||
value = int(HOST.read(fan_file).strip())
|
value = int(HOST.read(fan_file).strip())
|
||||||
@@ -316,9 +305,11 @@ def hwmon_sensor_components():
|
|||||||
|
|
||||||
# PWM fan sensors (duty cycle percentage)
|
# PWM fan sensors (duty cycle percentage)
|
||||||
# Only add if no fan*_input exists for this device (avoid duplicates)
|
# 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:
|
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.
|
# Skip pwm*_enable, pwm*_mode, etc. - only process pwm1, pwm2, etc.
|
||||||
pwm_basename = os.path.basename(pwm_file)
|
pwm_basename = os.path.basename(pwm_file)
|
||||||
if not pwm_basename.replace('pwm', '').isdigit():
|
if not pwm_basename.replace('pwm', '').isdigit():
|
||||||
@@ -345,7 +336,9 @@ def hwmon_sensor_components():
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Voltage sensors
|
# 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:
|
try:
|
||||||
sensor_num = os.path.basename(voltage_file).split('_')[0].replace('in', '')
|
sensor_num = os.path.basename(voltage_file).split('_')[0].replace('in', '')
|
||||||
value = int(HOST.read(voltage_file).strip())
|
value = int(HOST.read(voltage_file).strip())
|
||||||
@@ -363,7 +356,9 @@ def hwmon_sensor_components():
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Current sensors
|
# 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:
|
try:
|
||||||
sensor_num = os.path.basename(current_file).split('_')[0].replace('curr', '')
|
sensor_num = os.path.basename(current_file).split('_')[0].replace('curr', '')
|
||||||
value = int(HOST.read(current_file).strip())
|
value = int(HOST.read(current_file).strip())
|
||||||
@@ -381,7 +376,9 @@ def hwmon_sensor_components():
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Power sensors
|
# 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:
|
try:
|
||||||
sensor_num = os.path.basename(power_file).split('_')[0].replace('power', '')
|
sensor_num = os.path.basename(power_file).split('_')[0].replace('power', '')
|
||||||
value = int(HOST.read(power_file).strip())
|
value = int(HOST.read(power_file).strip())
|
||||||
@@ -426,8 +423,8 @@ def hwmon_sensor_components():
|
|||||||
wifi_info = get_wifi_phy_info()
|
wifi_info = get_wifi_phy_info()
|
||||||
for component in components:
|
for component in components:
|
||||||
name = component.get("name", "")
|
name = component.get("name", "")
|
||||||
# Match phy0, phy1, etc. sensors
|
# Match radio0, radio1, etc. sensors
|
||||||
if name.startswith("phy") and name in wifi_info:
|
if name.startswith("radio") and name in wifi_info:
|
||||||
phy = wifi_info[name]
|
phy = wifi_info[name]
|
||||||
# Add WiFi-specific description
|
# Add WiFi-specific description
|
||||||
component["description"] = phy["description"]
|
component["description"] = phy["description"]
|
||||||
@@ -448,7 +445,8 @@ def thermal_sensor_components():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Find all thermal zones
|
# 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:
|
for zone_path in thermal_zones:
|
||||||
try:
|
try:
|
||||||
@@ -496,6 +494,179 @@ def thermal_sensor_components():
|
|||||||
return 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():
|
def operational():
|
||||||
systemjson = HOST.read_json("/run/system.json")
|
systemjson = HOST.read_json("/run/system.json")
|
||||||
|
|
||||||
@@ -507,6 +678,7 @@ def operational():
|
|||||||
usb_port_components(systemjson) +
|
usb_port_components(systemjson) +
|
||||||
hwmon_sensor_components() +
|
hwmon_sensor_components() +
|
||||||
thermal_sensor_components() +
|
thermal_sensor_components() +
|
||||||
|
wifi_radio_components() +
|
||||||
[],
|
[],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,38 +2,213 @@ from ..host import HOST
|
|||||||
import json
|
import json
|
||||||
import re
|
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:
|
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 != "":
|
if data != "":
|
||||||
for line in data.splitlines():
|
for line in data.splitlines():
|
||||||
try:
|
try:
|
||||||
if "=" not in line:
|
if "=" not in line:
|
||||||
continue
|
continue
|
||||||
k,v = line.split("=", 1)
|
k, v = line.split("=", 1)
|
||||||
if k == "ssid":
|
if k == "ssid":
|
||||||
wifi_data["ssid"] = v
|
station_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="")
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# Skip malformed lines
|
# Skip malformed lines
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
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":
|
if data.strip() != "FAIL":
|
||||||
for line in data.splitlines():
|
for line in data.splitlines():
|
||||||
try:
|
try:
|
||||||
if "=" not in line:
|
if "=" not in line:
|
||||||
continue
|
continue
|
||||||
k,v = line.strip().split("=", 1)
|
k, v = line.strip().split("=", 1)
|
||||||
if k == "RSSI":
|
if k == "RSSI":
|
||||||
wifi_data["rssi"]=int(v)
|
station_data["rssi"] = int(v)
|
||||||
except (ValueError, KeyError):
|
except (ValueError, KeyError):
|
||||||
# Skip malformed lines or invalid integers
|
# Skip malformed lines or invalid integers
|
||||||
continue
|
continue
|
||||||
@@ -45,14 +220,28 @@ def wifi(ifname):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
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":
|
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:
|
except Exception:
|
||||||
# If scan results fail, just omit them
|
# If scan results fail, just omit them
|
||||||
pass
|
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):
|
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)
|
# Convert to list and sort by RSSI (best first)
|
||||||
result = list(networks.values())
|
result = list(networks.values())
|
||||||
result.sort(key=lambda x: x['rssi'], reverse=False)
|
result.sort(key=lambda x: x['rssi'], reverse=True)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
[7mHARDWARE COMPONENTS [0m
|
[7mHARDWARE COMPONENTS [0m
|
||||||
────────────────────────────────────────────────────────────
|
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
[1mBoard Information[0m
|
[1mBoard Information[0m
|
||||||
Model : VM
|
Model : Standard PC (i440FX + PIIX, 1996)
|
||||||
Manufacturer : QEMU
|
Manufacturer : QEMU
|
||||||
────────────────────────────────────────────────────────────
|
Base MAC Address : 00:a0:85:00:03:00
|
||||||
|
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
[1mUSB Ports[0m
|
[1mUSB Ports[0m
|
||||||
[7mNAME STATE OPER [0m
|
[7mNAME STATE OPER [0m
|
||||||
USB locked enabled
|
USB1 locked enabled
|
||||||
USB2 locked enabled
|
USB2 locked enabled
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"name": "mainboard",
|
"name": "mainboard",
|
||||||
"class": "iana-hardware:chassis",
|
"class": "iana-hardware:chassis",
|
||||||
"mfg-name": "QEMU",
|
"mfg-name": "QEMU",
|
||||||
"model-name": "VM",
|
"model-name": "Standard PC (i440FX + PIIX, 1996)",
|
||||||
"infix-hardware:phys-address": "00:a0:85:00:03:00",
|
"infix-hardware:phys-address": "00:a0:85:00:03:00",
|
||||||
"state": {
|
"state": {
|
||||||
"admin-state": "unknown",
|
"admin-state": "unknown",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"admin-state": "locked",
|
"admin-state": "locked",
|
||||||
"oper-state": "enabled"
|
"oper-state": "enabled"
|
||||||
},
|
},
|
||||||
"name": "USB",
|
"name": "USB1",
|
||||||
"class": "infix-hardware:usb"
|
"class": "infix-hardware:usb"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -120,13 +120,13 @@
|
|||||||
"search": []
|
"search": []
|
||||||
},
|
},
|
||||||
"clock": {
|
"clock": {
|
||||||
"boot-datetime": "2025-04-30T09:47:32+00:00",
|
"boot-datetime": "2026-01-02T20:42:11+00:00",
|
||||||
"current-datetime": "2025-04-30T09:48:01+00:00"
|
"current-datetime": "2026-01-02T20:43:23+00:00"
|
||||||
},
|
},
|
||||||
"platform": {
|
"platform": {
|
||||||
"os-name": "Infix",
|
"os-name": "Infix",
|
||||||
"os-version": "v25.04.0-rc1-3-g8daf1571-dirty",
|
"os-version": "v25.11.0-135-g44d2af57-dirty",
|
||||||
"os-release": "v25.04.0-rc1-3-g8daf1571-dirty",
|
"os-release": "v25.11.0-135-g44d2af57-dirty",
|
||||||
"machine": "x86_64"
|
"machine": "x86_64"
|
||||||
},
|
},
|
||||||
"infix-system:services": {
|
"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": {
|
"ietf-hardware:hardware": {
|
||||||
"component": [
|
"component": [
|
||||||
{
|
{
|
||||||
"name": "mainboard",
|
|
||||||
"class": "iana-hardware:chassis",
|
"class": "iana-hardware:chassis",
|
||||||
|
"infix-hardware:phys-address": "00:a0:85:00:03:00",
|
||||||
"mfg-name": "QEMU",
|
"mfg-name": "QEMU",
|
||||||
"model-name": "VM",
|
"model-name": "Standard PC (i440FX + PIIX, 1996)",
|
||||||
|
"name": "mainboard",
|
||||||
"state": {
|
"state": {
|
||||||
"admin-state": "unknown",
|
"admin-state": "unknown",
|
||||||
"oper-state": "enabled"
|
"oper-state": "enabled"
|
||||||
@@ -17,7 +18,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"class": "infix-hardware:usb",
|
"class": "infix-hardware:usb",
|
||||||
"name": "USB",
|
"name": "USB1",
|
||||||
"state": {
|
"state": {
|
||||||
"admin-state": "locked",
|
"admin-state": "locked",
|
||||||
"oper-state": "enabled"
|
"oper-state": "enabled"
|
||||||
@@ -49,8 +50,8 @@
|
|||||||
},
|
},
|
||||||
"ietf-system:system-state": {
|
"ietf-system:system-state": {
|
||||||
"clock": {
|
"clock": {
|
||||||
"boot-datetime": "2025-04-30T09:47:32+00:00",
|
"boot-datetime": "2026-01-02T20:42:11+00:00",
|
||||||
"current-datetime": "2025-04-30T09:48:01+00:00"
|
"current-datetime": "2026-01-02T20:43:23+00:00"
|
||||||
},
|
},
|
||||||
"infix-system:dns-resolver": {
|
"infix-system:dns-resolver": {
|
||||||
"options": {},
|
"options": {},
|
||||||
@@ -95,30 +96,30 @@
|
|||||||
{
|
{
|
||||||
"available": "0",
|
"available": "0",
|
||||||
"mount-point": "/",
|
"mount-point": "/",
|
||||||
"size": "70912",
|
"size": "142720",
|
||||||
"used": "70912"
|
"used": "142720"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"available": "79314",
|
"available": "79270",
|
||||||
"mount-point": "/var",
|
"mount-point": "/var",
|
||||||
"size": "86459",
|
"size": "86427",
|
||||||
"used": "267"
|
"used": "281"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"available": "12861",
|
"available": "12860",
|
||||||
"mount-point": "/cfg",
|
"mount-point": "/cfg",
|
||||||
"size": "14073",
|
"size": "14073",
|
||||||
"used": "66"
|
"used": "67"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"load-average": {
|
"load-average": {
|
||||||
"load-15min": "0.01",
|
"load-15min": "0.02",
|
||||||
"load-1min": "0.16",
|
"load-1min": "0.12",
|
||||||
"load-5min": "0.03"
|
"load-5min": "0.05"
|
||||||
},
|
},
|
||||||
"memory": {
|
"memory": {
|
||||||
"available": "259640",
|
"available": "225652",
|
||||||
"free": "187776",
|
"free": "134936",
|
||||||
"total": "355076"
|
"total": "355076"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -449,8 +450,8 @@
|
|||||||
"platform": {
|
"platform": {
|
||||||
"machine": "x86_64",
|
"machine": "x86_64",
|
||||||
"os-name": "Infix",
|
"os-name": "Infix",
|
||||||
"os-release": "v25.04.0-rc1-3-g8daf1571-dirty",
|
"os-release": "v25.11.0-135-g44d2af57-dirty",
|
||||||
"os-version": "v25.04.0-rc1-3-g8daf1571-dirty"
|
"os-version": "v25.11.0-135-g44d2af57-dirty"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
NAME="Infix"
|
NAME="Infix"
|
||||||
ID=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"
|
ID_LIKE="buildroot"
|
||||||
DEFAULT_HOSTNAME=ix
|
DEFAULT_HOSTNAME=infix
|
||||||
VERSION="v25.04.0-rc1-3-g8daf1571-dirty"
|
VERSION="v25.11.0-135-g44d2af57-dirty"
|
||||||
VERSION_ID=v25.04.0-rc1-3-g8daf1571-dirty
|
VERSION_ID=v25.11.0-135-g44d2af57-dirty
|
||||||
BUILD_ID="v25.04.0-rc1-3-g8daf1571-dirty"
|
BUILD_ID="v25.11.0-135-g44d2af57-dirty"
|
||||||
IMAGE_ID="infix-x86_64"
|
IMAGE_ID="infix-x86_64"
|
||||||
ARCHITECTURE="x86_64"
|
ARCHITECTURE="x86_64"
|
||||||
HOME_URL=https://github.com/kernelkit/infix/
|
HOME_URL=https://github.com/kernelkit/infix/
|
||||||
VENDOR_NAME="KernelKit"
|
VENDOR_NAME="KernelKit"
|
||||||
VENDOR_HOME="https://github.com/kernelkit"
|
VENDOR_HOME="https://kernelkit.org"
|
||||||
DOCUMENTATION_URL="https://github.com/kernelkit/infix/tree/main/doc"
|
DOCUMENTATION_URL="https://kernelkit.org/infix/"
|
||||||
SUPPORT_URL="mailto:kernelkit@googlegroups.com"
|
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