From bf5524915937f1511bc213d380698d801f4c5b73 Mon Sep 17 00:00:00 2001
From: Martin Weinelt <martin@darmstadt.freifunk.net>
Date: Sun, 11 Feb 2018 13:54:06 +0100
Subject: [PATCH] gluon-core: add outdoor support for 5 ghz radios

Add the `wifi5.outdoor_chanlist` site configuration that
allows specifying an outdoor channel range that can be
switched to for regulatory compliance.

Upon enabling the outdoor option the device will
 - configure the `outdoor_chanlist` on all 5 GHz radios
 - which may enable DFS/TPC, based on the regulatory domain
 - disable ibss/mesh on the 5 GHz radio, as DFS *will*
   break mesh connections
 - allow for htmode reconfiguration on 5 GHz radios

The outdoor option can be toggled from
 - Advanced Settings
   - W-LAN
     - Outdoor Installation

The `preserve_channel` flag overrules the outdoor channel
selection.
---
 docs/site-example/site.conf                   |  1 +
 docs/user/site.rst                            | 15 ++++
 package/gluon-core/check_site.lua             |  3 +
 .../luasrc/lib/gluon/upgrade/200-wireless     | 87 +++++++++++++------
 package/gluon-web-wifi-config/i18n/de.po      | 22 +++++
 package/gluon-web-wifi-config/i18n/fr.po      |  6 ++
 .../i18n/gluon-web-wifi-config.pot            | 17 ++++
 .../config-mode/model/admin/wifi-config.lua   | 63 +++++++++++++-
 8 files changed, 186 insertions(+), 28 deletions(-)

diff --git a/docs/site-example/site.conf b/docs/site-example/site.conf
index 2df60661e..e518ff9e4 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 4f4e9d453..88859a299 100644
--- a/docs/user/site.rst
+++ b/docs/user/site.rst
@@ -166,6 +166,21 @@ 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.
+    ::
+
+      wifi5 = {
+        channel = 44,
+        outdoor_chanlist = "100-140",
+
+        [...]
+      },
+
 next_node \: package
     Configuration of the local node feature of Gluon
     ::
diff --git a/package/gluon-core/check_site.lua b/package/gluon-core/check_site.lua
index 51791e0b6..a18aee43a 100644
--- a/package/gluon-core/check_site.lua
+++ b/package/gluon-core/check_site.lua
@@ -34,6 +34,9 @@ for _, config in ipairs({'wifi24', 'wifi5'}) do
 		need_string(in_site({'regdom'})) -- regdom is only required when wifi24 or wifi5 is configured
 
 		need_number({config, 'channel'})
+		if config == 'wifi5' then
+			need_string_match({config, 'outdoor_chanlist'}, '^[%d%s-]+$', 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/200-wireless b/package/gluon-core/luasrc/lib/gluon/upgrade/200-wireless
index adc1b17ca..ccf485455 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,32 +222,50 @@ 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)
+	elseif (hwmode == '11a' or hwmode == '11na') then
+		if is_outdoor() then
+			uci:set('wireless', radio_name, 'channels', config.outdoor_chanlist())
+
+			-- 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
 
-	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)
-
-	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)
-		)
-	)
-
 	fixup_wan(radio, index)
 end)
 
diff --git a/package/gluon-web-wifi-config/i18n/de.po b/package/gluon-web-wifi-config/i18n/de.po
index 099e79b59..d3ba4e2bd 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 b019e2fec..faeb01ab7 100644
--- a/package/gluon-web-wifi-config/i18n/fr.po
+++ b/package/gluon-web-wifi-config/i18n/fr.po
@@ -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"
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 9b1c86440..e73666158 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 bfb5b9cf0..fc3b23197 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
 
-- 
GitLab