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. -- for channel.
wifi5 = { wifi5 = {
channel = 44, channel = 44,
outdoor_chanlist = '100-140',
ap = { ap = {
ssid = 'alpha-centauri.freifunk.net', ssid = 'alpha-centauri.freifunk.net',
}, },

View File

@ -166,6 +166,25 @@ wifi24 \: optional
wifi5 \: optional wifi5 \: optional
Same as `wifi24` but for the 5Ghz radio. 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 next_node \: package
Configuration of the local node feature of Gluon Configuration of the local node feature of Gluon
:: ::

View File

@ -3,7 +3,8 @@ nodefault 'web-wizard'
packages 'web-wizard' \ packages 'web-wizard' \
'gluon-config-mode-hostname' \ 'gluon-config-mode-hostname' \
'gluon-config-mode-geo-location' \ 'gluon-config-mode-geo-location' \
'gluon-config-mode-contact-info' 'gluon-config-mode-contact-info' \
'gluon-config-mode-outdoor'
packages 'web-wizard & autoupdater' \ packages 'web-wizard & autoupdater' \
'gluon-config-mode-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 if need_table({config}, nil, false) then
need_string(in_site({'regdom'})) -- regdom is only required when wifi24 or wifi5 is configured 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, 'supported_rates'}, '802.11b rates are disabled by default.')
obsolete({config, 'basic_rate'}, '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)
end end
local function is_outdoor()
return uci:get_bool('gluon', 'wireless', 'outdoor')
end
local function get_channel(radio, config) local function get_channel(radio, config)
local channel local channel
if uci:get_first('gluon-core', 'wireless', 'preserve_channels') then if uci:get_first('gluon-core', 'wireless', 'preserve_channels') then
-- preserved channel always wins
channel = radio.channel 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 end
return channel or config.channel() return channel or config.channel()
end end
local function get_htmode(radio) 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) local phy = util.find_phy(radio)
if iwinfo.nl80211.hwmodelist(phy).ac then if iwinfo.nl80211.hwmodelist(phy).ac then
return 'VHT20' return 'VHT20'
else
return 'HT20'
end end
return 'HT20'
end end
local function is_disabled(name) 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, 'htmode', htmode)
uci:set('wireless', radio_name, 'country', site.regdom()) 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 local hwmode = radio.hwmode
if hwmode == '11g' or hwmode == '11ng' then if hwmode == '11g' or hwmode == '11ng' then
uci:set('wireless', radio_name, 'legacy_rates', false) 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') -- enforce outdoor channels by filtering the regdom for outdoor channels
uci:delete('wireless', radio_name, 'basic_rate') 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 ibss_disabled = is_disabled('ibss_' .. radio_name)
local mesh_disabled = is_disabled('mesh_' .. 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) config.mesh.disabled(false)
) )
) )
end
end
fixup_wan(radio, index) fixup_wan(radio, index)
end) end)

View File

@ -27,3 +27,23 @@ function match(target, subtarget, boards)
return true return true
end 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 " "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 " "enthalten; diese Werte sind allerdings für viele Geräte nicht verfügbar oder "
"fehlerhaft." "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. " "<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 " "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." "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 " "values include the antenna gain where available, but there are many devices "
"for which the gain is unavailable or inaccurate." "for which the gain is unavailable or inaccurate."
msgstr "" 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 iwinfo = require 'iwinfo'
local site = require 'gluon.site'
local uci = require("simple-uci").cursor() local uci = require("simple-uci").cursor()
local util = require 'gluon.util' 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 off = tonumber(iwinfo.nl80211.txpower_offset(phy)) or 0
local new = { } local new = { }
local prev = -1 local prev = -1
local _, val
for _, val in ipairs(list) do for _, val in ipairs(list) do
local dbm = val.dbm + off local dbm = val.dbm + off
local mw = math.floor(10 ^ (dbm / 10)) local mw = math.floor(10 ^ (dbm / 10))
@ -24,6 +24,17 @@ local function txpower_list(phy)
return new return new
end 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")) local f = Form(translate("WLAN"))
@ -97,7 +108,57 @@ uci:foreach('wireless', 'wifi-device', function(config)
end end
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() function f:write()
uci:commit('gluon')
os.execute('/lib/gluon/upgrade/200-wireless')
uci:commit('wireless') uci:commit('wireless')
end end

View File

@ -203,6 +203,33 @@ function alternatives(...)
end 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) function need(path, check, required, msg)
local val = loadvar(path) local val = loadvar(path)
if required == false and val == nil then 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) return need_array(path, function(e) need_one_of(e, array) end, required)
end 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) function need_domain_name(path)
need_string(path) need_string(path)
need(path, function(domain_name) need(path, function(domain_name)