diff --git a/docs/site-example/site.conf b/docs/site-example/site.conf
index 2df60661..e518ff9e 100644
--- a/docs/site-example/site.conf
+++ b/docs/site-example/site.conf
@@ -61,6 +61,7 @@
-- for channel.
wifi5 = {
channel = 44,
+ outdoor_chanlist = '100-140',
ap = {
ssid = 'alpha-centauri.freifunk.net',
},
diff --git a/docs/user/site.rst b/docs/user/site.rst
index 4f4e9d45..b1549b23 100644
--- a/docs/user/site.rst
+++ b/docs/user/site.rst
@@ -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
::
diff --git a/package/features b/package/features
index 8b44c82e..5de9e424 100644
--- a/package/features
+++ b/package/features
@@ -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'
diff --git a/package/gluon-config-mode-outdoor/Makefile b/package/gluon-config-mode-outdoor/Makefile
new file mode 100644
index 00000000..2f3acfcd
--- /dev/null
+++ b/package/gluon-config-mode-outdoor/Makefile
@@ -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))
diff --git a/package/gluon-config-mode-outdoor/i18n/de.po b/package/gluon-config-mode-outdoor/i18n/de.po
new file mode 100644
index 00000000..55cdb230
--- /dev/null
+++ b/package/gluon-config-mode-outdoor/i18n/de.po
@@ -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"
diff --git a/package/gluon-config-mode-outdoor/i18n/gluon-config-mode-outdoor.pot b/package/gluon-config-mode-outdoor/i18n/gluon-config-mode-outdoor.pot
new file mode 100644
index 00000000..d7c04908
--- /dev/null
+++ b/package/gluon-config-mode-outdoor/i18n/gluon-config-mode-outdoor.pot
@@ -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 ""
diff --git a/package/gluon-config-mode-outdoor/luasrc/lib/gluon/config-mode/wizard/0250-outdoor.lua b/package/gluon-config-mode-outdoor/luasrc/lib/gluon/config-mode/wizard/0250-outdoor.lua
new file mode 100644
index 00000000..edfe839f
--- /dev/null
+++ b/package/gluon-config-mode-outdoor/luasrc/lib/gluon/config-mode/wizard/0250-outdoor.lua
@@ -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
diff --git a/package/gluon-core/check_site.lua b/package/gluon-core/check_site.lua
index 51791e0b..74a6c3f9 100644
--- a/package/gluon-core/check_site.lua
+++ b/package/gluon-core/check_site.lua
@@ -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.')
diff --git a/package/gluon-core/luasrc/lib/gluon/upgrade/180-outdoors b/package/gluon-core/luasrc/lib/gluon/upgrade/180-outdoors
new file mode 100755
index 00000000..3acaa63c
--- /dev/null
+++ b/package/gluon-core/luasrc/lib/gluon/upgrade/180-outdoors
@@ -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')
diff --git a/package/gluon-core/luasrc/lib/gluon/upgrade/200-wireless b/package/gluon-core/luasrc/lib/gluon/upgrade/200-wireless
index adc1b17c..ccf48545 100755
--- a/package/gluon-core/luasrc/lib/gluon/upgrade/200-wireless
+++ b/package/gluon-core/luasrc/lib/gluon/upgrade/200-wireless
@@ -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)
- local phy = util.find_phy(radio)
- if iwinfo.nl80211.hwmodelist(phy).ac then
- return 'VHT20'
- else
- return 'HT20'
- end
+ 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'
+ end
+
+ return 'HT20'
end
local function is_disabled(name)
@@ -207,31 +222,49 @@ util.foreach_radio(uci, function(radio, index, config)
uci:set('wireless', radio_name, 'htmode', htmode)
uci:set('wireless', radio_name, 'country', site.regdom())
- local hwmode = radio.hwmode
- if hwmode == '11g' or hwmode == '11ng' then
- uci:set('wireless', radio_name, 'legacy_rates', false)
- end
-
uci:delete('wireless', radio_name, 'supported_rates')
uci:delete('wireless', radio_name, 'basic_rate')
- local ibss_disabled = is_disabled('ibss_' .. radio_name)
- local mesh_disabled = is_disabled('mesh_' .. radio_name)
+ local hwmode = radio.hwmode
+ if hwmode == '11g' or hwmode == '11ng' then
+ uci:set('wireless', radio_name, 'legacy_rates', false)
+ elseif (hwmode == '11a' or hwmode == '11na') then
+ if is_outdoor() then
+ uci:set('wireless', radio_name, 'channels', config.outdoor_chanlist())
- configure_ibss(config.ibss(), radio, index, suffix,
- first_non_nil(
- ibss_disabled,
- mesh_disabled,
- config.ibss.disabled(false)
- )
- )
- configure_mesh(config.mesh(), radio, index, suffix,
- first_non_nil(
- mesh_disabled,
- ibss_disabled,
- config.mesh.disabled(false)
- )
- )
+ -- 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)
+
+ configure_ibss(config.ibss(), radio, index, suffix,
+ first_non_nil(
+ ibss_disabled,
+ mesh_disabled,
+ config.ibss.disabled(false)
+ )
+ )
+ configure_mesh(config.mesh(), radio, index, suffix,
+ first_non_nil(
+ mesh_disabled,
+ ibss_disabled,
+ config.mesh.disabled(false)
+ )
+ )
+ end
+ end
fixup_wan(radio, index)
end)
diff --git a/package/gluon-core/luasrc/usr/lib/lua/gluon/platform.lua b/package/gluon-core/luasrc/usr/lib/lua/gluon/platform.lua
index 5b4f559a..17d881f6 100644
--- a/package/gluon-core/luasrc/usr/lib/lua/gluon/platform.lua
+++ b/package/gluon-core/luasrc/usr/lib/lua/gluon/platform.lua
@@ -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
diff --git a/package/gluon-web-wifi-config/i18n/de.po b/package/gluon-web-wifi-config/i18n/de.po
index 099e79b5..d3ba4e2b 100644
--- a/package/gluon-web-wifi-config/i18n/de.po
+++ b/package/gluon-web-wifi-config/i18n/de.po
@@ -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"
diff --git a/package/gluon-web-wifi-config/i18n/fr.po b/package/gluon-web-wifi-config/i18n/fr.po
index b019e2fe..faeb01ab 100644
--- a/package/gluon-web-wifi-config/i18n/fr.po
+++ b/package/gluon-web-wifi-config/i18n/fr.po
@@ -46,3 +46,9 @@ msgstr ""
"
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"
diff --git a/package/gluon-web-wifi-config/i18n/gluon-web-wifi-config.pot b/package/gluon-web-wifi-config/i18n/gluon-web-wifi-config.pot
index 9b1c8644..e7366615 100644
--- a/package/gluon-web-wifi-config/i18n/gluon-web-wifi-config.pot
+++ b/package/gluon-web-wifi-config/i18n/gluon-web-wifi-config.pot
@@ -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 ""
diff --git a/package/gluon-web-wifi-config/luasrc/lib/gluon/config-mode/model/admin/wifi-config.lua b/package/gluon-web-wifi-config/luasrc/lib/gluon/config-mode/model/admin/wifi-config.lua
index bfb5b9cf..fc3b2319 100644
--- a/package/gluon-web-wifi-config/luasrc/lib/gluon/config-mode/model/admin/wifi-config.lua
+++ b/package/gluon-web-wifi-config/luasrc/lib/gluon/config-mode/model/admin/wifi-config.lua
@@ -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
diff --git a/scripts/check_site.lua b/scripts/check_site.lua
index 9d65bb69..507fce83 100644
--- a/scripts/check_site.lua
+++ b/scripts/check_site.lua
@@ -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)