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)