Merge pull request #1329 from mweinelt/outdoor_mode

gluon-core: add outdoor channel support for 5 ghz radios
This commit is contained in:
Matthias Schiffer 2019-04-28 18:52:10 +02:00 committed by GitHub
commit ddb11ddd69
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 347 additions and 30 deletions

View File

@ -61,6 +61,7 @@
-- for channel.
wifi5 = {
channel = 44,
outdoor_chanlist = '100-140',
ap = {
ssid = 'alpha-centauri.freifunk.net',
},

View File

@ -166,6 +166,25 @@ wifi24 \: optional
wifi5 \: optional
Same as `wifi24` but for the 5Ghz radio.
Additionally a range of channels that are safe to use outsides on the 5 GHz band can
be set up through ``outdoor_chanlist``, which allows for a space-seperated list of
channels and channel ranges, seperated by a hyphen.
When set this offers the outdoor mode flag for 5 GHz radios in the config mode which
reconfigures the AP to select its channel from outdoor chanlist, while respecting
regulatory specifications, and disables mesh on that radio.
The ``outdoors`` option in turn allows to configure when outdoor mode will be enabled.
When set to ``true`` all 5 GHz radios will use outdoor channels, while on ``false``
the outdoor mode will be completely disabled. The default setting is ``'preset'``,
which will enable outdoor mode automatically on outdoor-capable devices.
::
wifi5 = {
channel = 44,
outdoor_chanlist = "100-140",
[...]
},
next_node \: package
Configuration of the local node feature of Gluon
::

View File

@ -3,7 +3,8 @@ nodefault 'web-wizard'
packages 'web-wizard' \
'gluon-config-mode-hostname' \
'gluon-config-mode-geo-location' \
'gluon-config-mode-contact-info'
'gluon-config-mode-contact-info' \
'gluon-config-mode-outdoor'
packages 'web-wizard & autoupdater' \
'gluon-config-mode-autoupdater'

View File

@ -0,0 +1,13 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=gluon-config-mode-outdoor
PKG_VERSION:=1
include ../gluon.mk
define Package/gluon-config-mode-outdoor
TITLE:=UI for displaying & changing the outdoor mode flag in the wizard
DEPENDS:=+gluon-config-mode-core
endef
$(eval $(call BuildPackageGluon,gluon-config-mode-outdoor))

View File

@ -0,0 +1,9 @@
msgid ""
"Please enable this option in case the node is to be installed outdoors "
"to comply with local frequency regulations."
msgstr ""
"Wenn der Knoten im Freien aufgestellt werden soll, dann aktiviere bitte "
"diese Option um den örtlichen Frequenzbestimmungen zu entsprechen."
msgid "Node will be installed outdoors"
msgstr "Knoten wird im Außenbereich betrieben"

View File

@ -0,0 +1,7 @@
msgid ""
"Please enable this option in case the node is to be installed outdoors "
"to comply with local frequency regulations."
msgstr ""
msgid "Node will be installed outdoors"
msgstr ""

View File

@ -0,0 +1,28 @@
return function(form, uci)
local platform_info = require 'platform_info'
if not platform_info.is_outdoor_device() then
-- only visible on wizard for outdoor devices
return
end
local pkg_i18n = i18n 'gluon-config-mode-outdoor'
local section = form:section(Section, nil, pkg_i18n.translate(
"Please enable this option in case the node is to be installed outdoors "
.. "to comply with local frequency regulations."
))
local outdoor = section:option(Flag, 'outdoor', pkg_i18n.translate("Node will be installed outdoors"))
outdoor.default = outdoor_mode
function outdoor:write(data)
if data ~= outdoor_mode then
uci:set('gluon', 'wireless', 'outdoor', data)
uci:save('gluon')
os.execute('/lib/gluon/upgrade/200-wireless')
end
end
return {'gluon', 'wireless'}
end

View File

@ -33,7 +33,19 @@ for _, config in ipairs({'wifi24', 'wifi5'}) do
if need_table({config}, nil, false) then
need_string(in_site({'regdom'})) -- regdom is only required when wifi24 or wifi5 is configured
need_number({config, 'channel'})
if config == "wifi24" then
local channels = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}
need_one_of({config, 'channel'}, channels)
elseif config == 'wifi5' then
local channels = {
34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62,
64, 96, 100, 102, 104, 106, 108, 110, 112, 114, 116, 118,
120, 122, 124, 126, 128, 132, 134, 136, 138, 140, 142, 144,
149, 151, 153, 155, 157, 159, 161, 165, 169, 173 }
need_one_of({config, 'channel'}, channels)
need_chanlist({config, 'outdoor_chanlist'}, channels, false)
need_one_of({config, 'outdoors'}, {true, false, 'preset'}, false)
end
obsolete({config, 'supported_rates'}, '802.11b rates are disabled by default.')
obsolete({config, 'basic_rate'}, '802.11b rates are disabled by default.')

View File

@ -0,0 +1,35 @@
#!/usr/bin/lua
-- This script needs to be sorted before 200-wireless as it affects
-- wireless channel selection and wireless mesh configuration.
local uci = require('simple-uci').cursor()
local site = require 'gluon.site'
if uci:get('gluon', 'wireless', 'outdoor') ~= nil then
-- don't overwrite existing configuration
os.exit(0)
end
local sysconfig = require 'gluon.sysconfig'
local platform_info = require 'platform_info'
local config = site.wifi5.outdoor_preset('preset')
local outdoor = false
if sysconfig.gluon_version then
-- don't enable the outdoor mode after an upgrade
outdoor = false
elseif config == 'preset' then
-- enable outdoor mode through presets on new installs
outdoor = platform_info.is_outdoor_device()
else
-- enable/disable outdoor mode unconditionally on new installs
outdoor = config
end
uci:section('gluon', 'wireless', 'wireless', {
outdoor = outdoor
})
uci:save('gluon')

View File

@ -49,22 +49,37 @@ if not sysconfig.gluon_version then
end)
end
local function is_outdoor()
return uci:get_bool('gluon', 'wireless', 'outdoor')
end
local function get_channel(radio, config)
local channel
if uci:get_first('gluon-core', 'wireless', 'preserve_channels') then
-- preserved channel always wins
channel = radio.channel
elseif (radio.hwmode == '11a' or radio.hwmode == '11na') and is_outdoor() then
-- actual channel will be picked and probed from chanlist
channel = 'auto'
end
return channel or config.channel()
end
local function get_htmode(radio)
if (radio.hwmode == '11a' or radio.hwmode == '11na') and is_outdoor() then
local outdoor_htmode = uci:get('gluon', 'wireless', 'outdoor_' .. radio['.name'] .. '_htmode')
if outdoor_htmode ~= nil then
return outdoor_htmode
end
end
local phy = util.find_phy(radio)
if iwinfo.nl80211.hwmodelist(phy).ac then
return 'VHT20'
else
return 'HT20'
end
return 'HT20'
end
local function is_disabled(name)
@ -207,13 +222,29 @@ util.foreach_radio(uci, function(radio, index, config)
uci:set('wireless', radio_name, 'htmode', htmode)
uci:set('wireless', radio_name, 'country', site.regdom())
uci:delete('wireless', radio_name, 'supported_rates')
uci:delete('wireless', radio_name, 'basic_rate')
local hwmode = radio.hwmode
if hwmode == '11g' or hwmode == '11ng' then
uci:set('wireless', radio_name, 'legacy_rates', false)
end
elseif (hwmode == '11a' or hwmode == '11na') then
if is_outdoor() then
uci:set('wireless', radio_name, 'channels', config.outdoor_chanlist())
uci:delete('wireless', radio_name, 'supported_rates')
uci:delete('wireless', radio_name, 'basic_rate')
-- enforce outdoor channels by filtering the regdom for outdoor channels
local hostapd_options = uci:get_list('wireless', radio_name, 'hostapd_options')
util.add_to_set(hostapd_options, 'country3=0x4f')
uci:set_list('wireless', radio_name, 'hostapd_options', hostapd_options)
uci:delete('wireless', 'ibss_' .. radio_name)
uci:delete('wireless', 'mesh_' .. radio_name)
else
uci:delete('wireless', radio_name, 'channels')
local hostapd_options = uci:get_list('wireless', radio_name, 'hostapd_options')
util.remove_from_set(hostapd_options, 'country3=0x4f')
uci:set_list('wireless', radio_name, 'hostapd_options', hostapd_options)
local ibss_disabled = is_disabled('ibss_' .. radio_name)
local mesh_disabled = is_disabled('mesh_' .. radio_name)
@ -232,6 +263,8 @@ util.foreach_radio(uci, function(radio, index, config)
config.mesh.disabled(false)
)
)
end
end
fixup_wan(radio, index)
end)

View File

@ -27,3 +27,23 @@ function match(target, subtarget, boards)
return true
end
function is_outdoor_device()
if match('ar71xx', 'generic', {
'cpe510-520-v1',
'ubnt-nano-m',
'ubnt-nano-m-xw',
}) then
return true
elseif match('ar71xx', 'generic', {'unifiac-lite'}) and
get_model() == 'Ubiquiti UniFi-AC-MESH' then
return true
elseif match('ar71xx', 'generic', {'unifiac-pro'}) and
get_model() == 'Ubiquiti UniFi-AC-MESH-PRO' then
return true
end
return false
end

View File

@ -49,3 +49,25 @@ msgstr ""
"werden. Wenn möglich, ist in den Werten der Sendeleistung der Antennengewinn "
"enthalten; diese Werte sind allerdings für viele Geräte nicht verfügbar oder "
"fehlerhaft."
msgid "Outdoor installation"
msgstr "Outdoor-Installation"
msgid "Node will be installed outdoors"
msgstr "Knoten wird im Außenbereich betrieben"
msgid ""
"Configuring the node for outdoor use tunes the 5 GHz radio to a frequency "
"and transmission power that conforms with the local regulatory requirements. "
"It also enables dynamic frequency selection (DFS; radar detection). At the "
"same time, mesh functionality is disabled as it requires neighbouring nodes "
"to stay on the same channel permanently."
msgstr ""
"Ist der Knoten für den Einsatz im Freien konfiguriert, wird ein WLAN-Kanal auf "
"dem 5-GHz-Band sowie eine Sendeleistung entsprechend den gesetzlichen "
"Frequenzregulatorien gewählt. Gleichzeitig wird die dynamische Frequenzwahl "
"(DFS; Radarerkennung) aktiviert und die Mesh-Funktionalität deaktiviert, da "
"sich Nachbarknoten dauerhaft auf demselben Kanal befinden müssen."
msgid "HT Mode"
msgstr "HT-Modus"

View File

@ -46,3 +46,9 @@ msgstr ""
"<br /><br />Ici vous pouvez aussi configurer la puissance d'émmission se votre Wi-Fi. "
"Prenez note que les valeurs fournies pour la puissance de transmission prennent "
"en compte les gains fournis par l'antenne, et que ces valeurs ne sont pas toujours disponibles ou exactes."
msgid "Outdoor installation"
msgstr "Installation extérieure"
msgid "HT Mode"
msgstr "Mode HT"

View File

@ -33,3 +33,20 @@ msgid ""
"values include the antenna gain where available, but there are many devices "
"for which the gain is unavailable or inaccurate."
msgstr ""
msgid "Outdoor installation"
msgstr ""
msgid "Node will be installed outdoors"
msgstr ""
msgid ""
"Configuring the node for outdoor use tunes the 5 GHz radio to a frequency "
"and transmission power that conforms with the local regulatory requirements. "
"It also enables dynamic frequency selection (DFS; radar detection). At the "
"same time, mesh functionality is disabled as it requires neighbouring nodes "
"to stay on the same channel permanently."
msgstr ""
msgid "HT Mode"
msgstr ""

View File

@ -1,4 +1,5 @@
local iwinfo = require 'iwinfo'
local site = require 'gluon.site'
local uci = require("simple-uci").cursor()
local util = require 'gluon.util'
@ -8,7 +9,6 @@ local function txpower_list(phy)
local off = tonumber(iwinfo.nl80211.txpower_offset(phy)) or 0
local new = { }
local prev = -1
local _, val
for _, val in ipairs(list) do
local dbm = val.dbm + off
local mw = math.floor(10 ^ (dbm / 10))
@ -24,6 +24,17 @@ local function txpower_list(phy)
return new
end
local function has_5ghz_radio()
local result = false
uci:foreach('wireless', 'wifi-device', function(config)
local radio = config['.name']
local hwmode = uci:get('wireless', radio, 'hwmode')
result = result or (hwmode == '11a' or hwmode == '11na')
end)
return result
end
local f = Form(translate("WLAN"))
@ -97,7 +108,57 @@ uci:foreach('wireless', 'wifi-device', function(config)
end
end)
if has_5ghz_radio() then
local r = f:section(Section, translate("Outdoor Installation"), translate(
"Configuring the node for outdoor use tunes the 5 GHz radio to a frequency "
.. "and transmission power that conforms with the local regulatory requirements. "
.. "It also enables dynamic frequency selection (DFS; radar detection). At the "
.. "same time, mesh functionality is disabled as it requires neighbouring nodes "
.. "to stay on the same channel permanently."
))
local outdoor = r:option(Flag, 'outdoor', translate("Node will be installed outdoors"))
outdoor.default = uci:get_bool('gluon', 'wireless', 'outdoor')
function outdoor:write(data)
uci:set('gluon', 'wireless', 'outdoor', data)
end
uci:foreach('wireless', 'wifi-device', function(config)
local radio = config['.name']
local hwmode = uci:get('wireless', radio, 'hwmode')
if hwmode ~= '11a' and hwmode ~= '11na' then
return
end
local phy = util.find_phy(uci:get_all('wireless', radio))
local ht = r:option(ListValue, 'outdoor_htmode', translate('HT Mode') .. ' (' .. radio .. ')')
ht:depends(outdoor, true)
ht.default = uci.get('gluon', 'wireless', 'outdoor_' .. radio .. '_htmode') or 'default'
ht:value('default', translate("(default)"))
for mode, available in pairs(iwinfo.nl80211.htmodelist(phy)) do
if available then
ht:value(mode, mode)
end
end
function ht:write(data)
if data == 'default' then
data = nil
end
uci:set('gluon', 'wireless', 'outdoor_' .. radio .. '_htmode', data)
end
end)
end
function f:write()
uci:commit('gluon')
os.execute('/lib/gluon/upgrade/200-wireless')
uci:commit('wireless')
end

View File

@ -203,6 +203,33 @@ function alternatives(...)
end
local function check_chanlist(channels)
local is_valid_channel = check_one_of(channels)
return function(chanlist)
for group in chanlist:gmatch("%S+") do
if group:match("^%d+$") then
channel = tonumber(group)
if not is_valid_channel(channel) then
return false
end
elseif group:match("^%d+-%d+$") then
from, to = group:match("^(%d+)-(%d+)$")
from = tonumber(from)
to = tonumber(to)
if from >= to then
return false
end
if not is_valid_channel(from) or not is_valid_channel(to) then
return false
end
else
return false
end
end
return true
end
end
function need(path, check, required, msg)
local val = loadvar(path)
if required == false and val == nil then
@ -307,6 +334,12 @@ function need_array_of(path, array, required)
return need_array(path, function(e) need_one_of(e, array) end, required)
end
function need_chanlist(path, channels, required)
local valid_chanlist = check_chanlist(channels)
return need(path, valid_chanlist, required, 'be a space-separated list of WiFi channels or channel-ranges (separated by a hyphen). ' ..
'Valid channels are: ' .. array_to_string(channels))
end
function need_domain_name(path)
need_string(path)
need(path, function(domain_name)