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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
240 changes: 240 additions & 0 deletions renderer/halow_channels.uc

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion renderer/templates/interface/ssid.uc
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,6 @@
uci_comment(output, '### generate HaLow mesh specific configuration');
uci_named_section(output, 'wireless.halowmesh', 'wifi-iface');
uci_set_string(output, 'wireless.halowmesh.device', phy.section);
uci_set_string(output, 'wireless.halowmesh.ifname', 'halow_mesh');
uci_set_string(output, 'wireless.halowmesh.disabled', '0');
uci_set_string(output, 'wireless.halowmesh.beacon_int', '1000');
uci_set_string(output, 'wireless.halowmesh.wds', '0');
Expand Down
109 changes: 105 additions & 4 deletions renderer/templates/radio.uc
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{%
let halow = require('halow_channels');
let phys = wiphy.lookup_by_band(radio.band);

if (!length(phys)) {
Expand Down Expand Up @@ -54,7 +55,84 @@
if (!length(radio.valid_channels))
radio.valid_channels = phys[0].channels;

radio.country ??= default_config.country;
// HaLow country resolution. The MORSE chip only supports the regulatory
// domains AU, CA, EU, JP and US. Resolve as follows:
// 1. When HaLow has no country of its own, follow the 2.4/5G peer radio
// (e.g. 5G=JP means HaLow must also be JP, not the default).
// 2. Otherwise inherit the peer/default country.
// 3. Normalize European ISO codes to EU.
// 4. If any radio country is outside the supported set, fall back ALL
// radios to US; if HaLow and the peer explicitly disagree, reject.
let peer_country = null;
for (let r in state.radios) {
if (r.band != "HaLow" && r.country) {
peer_country = r.country;
break;
}
}

if (radio.band == "HaLow")
radio.country ??= peer_country || default_config.country || 'US';
else
radio.country ??= default_config.country;

// EdgeCore: AL (Albania) and BA (Bosnia) are aliased to EU below, but
// their local regulation only permits 1 MHz operation. Remember the
// original code so match_channel() can downgrade any 2 MHz+ channel to
// the 1 MHz default (with a warning, not a reject).
let halow_1mhz_only = (radio.band == "HaLow") &&
(radio.country == "AL" || radio.country == "BA");

let eu_alias = {
AD: true, AL: true, AT: true, BA: true, BE: true, BG: true, BY: true,
CH: true, CY: true, CZ: true, DD: true, DE: true, DK: true, EE: true,
ES: true, FI: true, FL: true, FR: true, GB: true, GR: true, HR: true,
HU: true, IE: true, IS: true, IT: true, LI: true, LT: true, LU: true,
LV: true, MC: true, MD: true, ME: true, MK: true, MT: true, NL: true,
NO: true, PL: true, PT: true, RO: true, RS: true, SE: true, SI: true,
SK: true, SM: true, TR: true, UA: true, UK: true, VA: true, XK: true
};
Comment thread
ian77chen marked this conversation as resolved.
if (radio.band == "HaLow" && radio.country && eu_alias[radio.country])
radio.country = "EU";
if (peer_country && eu_alias[peer_country])
peer_country = "EU";

// If any radio country is unsupported by the MORSE chip, all radios
// (2.4G/5G/HaLow) fall back to US.
let morse_supported = { AU: true, CA: true, EU: true, JP: true, US: true };
let need_us = false;
for (let r in state.radios) {
let c = r.country;
if (c && eu_alias[c])
c = "EU";
if (c && !morse_supported[c])
need_us = true;
}

if (need_us) {
if (radio.band == "HaLow" && peer_country && radio.country != peer_country) {
warn(sprintf("HaLow country '%s' conflicts with 2G/5G country '%s'",
radio.country, peer_country));
die(sprintf("HaLow country '%s' conflicts with 2G/5G country '%s'",
radio.country, peer_country));
}
radio.country = "US";
if (peer_country)
peer_country = "US";
} else if (radio.band == "HaLow" && peer_country && radio.country != peer_country) {
warn(sprintf("HaLow country '%s' conflicts with 2G/5G country '%s'",
radio.country, peer_country));
die(sprintf("HaLow country '%s' conflicts with 2G/5G country '%s'",
radio.country, peer_country));
}

// HaLow S1G channel/op_class/frequency are per-country table values, not
// derivable by the generic 5G algorithm. Resolve against the map and seed
// valid_channels from it.
let halow_country = (radio.band == "HaLow")
? halow.halow_resolve_country(radio.country, peer_country || default_config.country) : null;
if (radio.band == "HaLow")
radio.valid_channels = halow.halow_valid_channels(halow_country);

if (length(restrict.country) && !(radio.country in restrict.country)) {
warn("Country code is restricted");
Expand All @@ -78,8 +156,30 @@
function match_channel(phy, radio) {
let wanted_channel = radio.channel;

if (radio.band == "HaLow" && !wanted_channel)
return 12;
if (radio.band == "HaLow") {
let chan = !wanted_channel
? halow.halow_default_channel(halow_country)
: halow.halow_resolve_channel(halow_country, wanted_channel);
// AL/BA: only 1 MHz allowed. If the resolved channel is
// wider (2 MHz+), fall back to a 1 MHz channel. The country
// default_channel may itself be 2 MHz+ (e.g. EU default is
// ch2 @ 2 MHz), so pick the smallest-numbered 1 MHz channel
// from the map rather than default_channel.
if (halow_1mhz_only) {
let row = halow.CHANNEL_MAP[halow_country]?.channels[sprintf("%d", chan)];
if (row && row.bw > 1) {
let dflt = null;
let chans = halow.CHANNEL_MAP[halow_country]?.channels;
for (let k, v in chans)
if (v.bw == 1 && (dflt == null || v.s1g_chan < dflt))
dflt = v.s1g_chan;
warn("HaLow: country '%s' (via AL/BA) allows 1 MHz only, channel %d is %d MHz, falling back to %d",
halow_country, chan, row.bw, dflt);
chan = dflt;
}
}
return chan;
}

if (!wanted_channel || wanted_channel == "auto")
return 0;
Expand Down Expand Up @@ -261,7 +361,8 @@ set wireless.{{ phy.section }}.type='morse'
set wireless.{{ phy.section }}.band='s1g'
set wireless.{{ phy.section }}.hwmode='a'
set wireless.{{ phy.section }}.s1g_prim_1mhz_chan_index='auto'
set wireless.{{ phy.section }}.op_class={{ get_s1g_op_class(match_channel(phy, radio)) }}
set wireless.{{ phy.section }}.op_class={{ halow.halow_lookup_op_class(halow_country, match_channel(phy, radio)) }}
set wireless.{{ phy.section }}.frequency={{ halow.halow_centre_freq(halow_country, match_channel(phy, radio)) }}
{% endif %}
{% if (afc): %}
add wireless afc-server
Expand Down
77 changes: 76 additions & 1 deletion renderer/wifi/phy.uc
Original file line number Diff line number Diff line change
Expand Up @@ -219,10 +219,85 @@ function map5GToS1G() {
return mappingTable;
}

function scan_morse_phys() {
lookup_paths();

let phys = phy_get();
let ret = {};
let s1gMapping = map5GToS1G();

for (let phy in phys) {
if (!exists(phy, 'wiphy'))
continue;
let phyname = 'phy' + phy.wiphy;
let path = paths[phyname];
if (!path)
continue;

// Only process Morse PHYs
let morsePath = '/sys/kernel/debug/ieee80211/' + phyname + '/morse';
if (!fs.stat(morsePath))
continue;

let p = {};
p.is_morse_phy = true;

let temp = get_hwmon('phy' + phy.wiphy);
if (temp)
p.temperature = temp / 1000;

p.tx_ant = phy.wiphy_antenna_tx;
p.rx_ant = phy.wiphy_antenna_rx;
p.tx_ant_avail = phy.wiphy_antenna_avail_tx;
p.rx_ant_avail = phy.wiphy_antenna_avail_rx;
p.frequencies = [];
p.channels = [];
p.dfs_channels = [];
p.htmode = [];
p.band = [];
for (let band in phy.wiphy_bands) {
for (let freq in band?.freqs) {
if (freq.disabled)
continue;

let channel = freq2channel(freq.freq);
if (freq.freq >= 5160 && freq.freq <= 5885) {
let ch = "" + channel;
if (ch in s1gMapping) {
channel = s1gMapping[ch].s1g_channel;
push(p.channels, channel);
push(p.frequencies, s1gMapping[ch].s1g_freq);
} else {
push(p.channels, channel);
push(p.frequencies, freq.freq);
}
} else {
push(p.channels, channel);
push(p.frequencies, freq.freq);
}

if (freq.freq < 1000 || (freq.freq >= 5160 && freq.freq <= 5885))
push(p.band, 'HaLow');
}
}

p.band = uniq(p.band);
if (length(p.band))
ret[path] = p;
}
return ret;
}

function lookup_phys() {
let ret = lookup_board();
if (ret)
if (ret) {
// board.json declares the PCIe wlan, but the USB HaLow (Morse) dongle
// is not in board.json - scan for it and merge it in.
let morse = scan_morse_phys();
for (let path, phy in morse)
ret[path] = phy;
return ret;
}
lookup_paths();

let phys = phy_get();
Expand Down
9 changes: 8 additions & 1 deletion renderer/wifi/survey.uc
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,18 @@ function wif_get(wdev) {
function lookup_survey() {
let wifs = wif_get();

// Tag every survey entry with its owning interface + wiphy. The morse
// HaLow phy uses borrowed 5 GHz shim frequencies that collide with the
// real 5 GHz radio, so frequency alone cannot attribute a survey row to a
// radio; the consumer filters by ifname/wiphy.
for (let wif in wifs)
for (let survey in survey_get(wif.ifname))
if (!frequency || survey.survey_info.frequency == frequency)
if (survey.survey_info?.time)
if (survey.survey_info?.time) {
survey.survey_info.ifname = wif.ifname;
survey.survey_info.wiphy = wif.wiphy;
push(rv.survey, survey.survey_info);
}
}

lookup_survey();
Expand Down
26 changes: 24 additions & 2 deletions system/state/wifi.uc
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,36 @@ export function collect_wifi_radios(state) {
if (_chanUtil > 0)
radio.chanUtil = _chanUtil;

// Pre-scope the survey to THIS radio's own interfaces before anyone
// consumes it. The morse HaLow phy borrows 5 GHz shim frequencies that
// collide with the real 5 GHz radio, so a frequency-only match would
// cross-contaminate the two radios' telemetry. survey rows are tagged
// with ifname (renderer/wifi/survey.uc); keep only rows whose ifname
// belongs to this radio (untagged rows are left for the frequency
// filter).
let radio_ifnames = {};
for (let wif in data.interfaces)
if (wif.ifname)
radio_ifnames[wif.ifname] = true;
let radio_survey = { survey: [] };
for (let k, v in survey.survey)
if (v.ifname == null || radio_ifnames[v.ifname])
push(radio_survey.survey, v);

// Check if this is a HaLow device and process accordingly
if (!halow_module.process_halow_radio(radio, survey)) {
if (!halow_module.process_halow_radio(radio, radio_survey)) {
// Non-HaLow device, process survey normally
radio.survey = [];
for (let k, v in survey.survey)
for (let k, v in radio_survey.survey)
if (v.frequency in radio.frequency)
push(radio.survey, v);
}
// Drop the internal attribution tags so the uploaded telemetry keeps
// the exact original survey shape (no extra ifname/wiphy keys).
for (let s in radio.survey) {
delete s.ifname;
delete s.wiphy;
}
delete radio.in_use;
push(radios, radio);
}
Expand Down