mirror of https://github.com/aredn/aredn.git
Link Quality Management (#360)
* Link Quality Management experiment (built in) * Protect LQM pages * Omit "empty" mac addresses * Integrate LQM v0.2 Includes proposed UI if this were built-in. When LQM is enabled (advanced settings) the usual distance inputs are replaced with "min snr' and "max distance" inputs which are the major ones you might tweak, as well as a link to the LQM status page. Other controls are now available (so protected) in advanced settings. * Improve LQM updating * Use running snr averages * Merge app changes * AREDN-ize the UI * Improve status language * Improved DtD detection * Improve quality reporting * Link Quality category * Enable by default * Better intergration * Link => Neighbor * Formatting * Make sure initial page is populated without extra fetch * Handle empty lqm.info * Update with latest experiment algorithm changes * Validate LQM settings before applying them * Algorithm updates * Improve quality reporting * %% -> % * Default max distance now 50 miles * Get actual noise if radio will provide it * low_snr => min_snr * Dont print node description if we dont have one * Remove properties duplicated from setup page * Localize max distance. Miles in GB and US, Kilometers everywhere else. * Ping link quality testing * UDP 'ping' for quality check * Change Active Settings title * Expand ping test * Improve messaging * Add a ping penalty for neighbors which cannot be contacted in a timely manner. * Remove user_blocks config option. No one needs to use this anymore. * Localize distances on lqm page * Improve status reporting * First run emergency node setup. When a node first runs LQM, if the default settings fail to connect to a node we will now adjust them so that at least one node is viable. * Restore blocking of mac addresses * LQM now off by default fixed #47
This commit is contained in:
parent
276d1411f1
commit
b23ab5ee8a
|
@ -22,3 +22,13 @@ config location
|
||||||
config tunnel
|
config tunnel
|
||||||
option maxclients '10'
|
option maxclients '10'
|
||||||
option maxservers '10'
|
option maxservers '10'
|
||||||
|
|
||||||
|
config lqm
|
||||||
|
option enable '0'
|
||||||
|
option min_snr '15'
|
||||||
|
option margin_snr '1'
|
||||||
|
option min_distance '0'
|
||||||
|
option max_distance '80467'
|
||||||
|
option min_quality '50'
|
||||||
|
option ping_penalty '10'
|
||||||
|
option margin_quality '1'
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
#!/bin/sh
|
||||||
|
<<'LICENSE'
|
||||||
|
Part of AREDN -- Used for creating Amateur Radio Emergency Data Networks
|
||||||
|
Copyright (C) 2022 Tim Wilkinson
|
||||||
|
See Contributors file for additional contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation version 3 of the License.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Additional Terms:
|
||||||
|
|
||||||
|
Additional use restrictions exist on the AREDN(TM) trademark and logo.
|
||||||
|
See AREDNLicense.txt for more info.
|
||||||
|
|
||||||
|
Attributions to the AREDN Project must be retained in the source code.
|
||||||
|
If importing this code into a new or existing project attribution
|
||||||
|
to the AREDN project must be added to the source code.
|
||||||
|
|
||||||
|
You must not misrepresent the origin of the material contained within.
|
||||||
|
|
||||||
|
Modified versions must be modified to attribute to the original source
|
||||||
|
and be marked in reasonable ways as differentiate it from the original
|
||||||
|
version.
|
||||||
|
|
||||||
|
LICENSE
|
||||||
|
|
||||||
|
/usr/sbin/iptables -F input_lqm 2> /dev/null
|
||||||
|
/usr/sbin/iptables -X input_lqm 2> /dev/null
|
||||||
|
/usr/sbin/iptables -N input_lqm 2> /dev/null
|
||||||
|
/usr/sbin/iptables -D INPUT -j input_lqm -m comment --comment 'block low quality links' 2> /dev/null
|
||||||
|
/usr/sbin/iptables -I INPUT -j input_lqm -m comment --comment 'block low quality links' 2> /dev/null
|
|
@ -0,0 +1,18 @@
|
||||||
|
#!/bin/sh
|
||||||
|
for c in /etc/config /etc/config.mesh
|
||||||
|
do
|
||||||
|
if [ "$(/sbin/uci -c $c -q get aredn.@lqm[0].enable)" = "" ]; then
|
||||||
|
/sbin/uci -c $c -m import aredn <<__EOF__
|
||||||
|
config lqm
|
||||||
|
option enable '0'
|
||||||
|
option min_snr '15'
|
||||||
|
option margin_snr '1'
|
||||||
|
option min_distance '0'
|
||||||
|
option max_distance '80467'
|
||||||
|
option min_quality '50'
|
||||||
|
option ping_penalty '10'
|
||||||
|
option margin_quality '1'
|
||||||
|
__EOF__
|
||||||
|
/sbin/uci -c $c commit aredn
|
||||||
|
fi
|
||||||
|
done
|
|
@ -0,0 +1,603 @@
|
||||||
|
--[[
|
||||||
|
|
||||||
|
Copyright (C) 2022 Tim Wilkinson
|
||||||
|
See Contributors file for additional contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation version 3 of the License.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Additional Terms:
|
||||||
|
|
||||||
|
Additional use restrictions exist on the AREDN(TM) trademark and logo.
|
||||||
|
See AREDNLicense.txt for more info.
|
||||||
|
|
||||||
|
Attributions to the AREDN Project must be retained in the source code.
|
||||||
|
If importing this code into a new or existing project attribution
|
||||||
|
to the AREDN project must be added to the source code.
|
||||||
|
|
||||||
|
You must not misrepresent the origin of the material contained within.
|
||||||
|
|
||||||
|
Modified versions must be modified to attribute to the original source
|
||||||
|
and be marked in reasonable ways as differentiate it from the original
|
||||||
|
version
|
||||||
|
|
||||||
|
--]]
|
||||||
|
|
||||||
|
local json = require("luci.jsonc")
|
||||||
|
local ip = require("luci.ip")
|
||||||
|
local info = require("aredn.info")
|
||||||
|
|
||||||
|
local refresh_timeout = 15 * 60 -- refresh high cost data every 15 minutes
|
||||||
|
local pending_timeout = 5 * 60 -- pending node wait 5 minutes before they are included
|
||||||
|
local first_run_timeout = 4 * 60 -- first ever run can adjust the config to make sure we dont ignore evereyone
|
||||||
|
local lastseen_timeout = 60 * 60 -- age out nodes we've not seen for 1 hour
|
||||||
|
local snr_run_avg = 0.8 -- snr running average
|
||||||
|
local quality_min_packets = 100 -- minimum number of tx packets before we can safely calculate the link quality
|
||||||
|
local quality_injection_max = 10 -- number of packets to inject into poor links to update quality
|
||||||
|
local quality_run_avg = 0.8 -- quality running average
|
||||||
|
local ping_timeout = 1.0 -- timeout before ping gives a qualtiy penalty
|
||||||
|
|
||||||
|
local myhostname = (info.get_nvram("node") or "localnode"):lower()
|
||||||
|
local now = 0
|
||||||
|
|
||||||
|
function get_config()
|
||||||
|
local c = uci.cursor() -- each time as /etc/config/aredn may have changed
|
||||||
|
return {
|
||||||
|
margin = tonumber(c:get("aredn", "@lqm[0]", "margin_snr")),
|
||||||
|
low = tonumber(c:get("aredn", "@lqm[0]", "min_snr")),
|
||||||
|
min_distance = tonumber(c:get("aredn", "@lqm[0]", "min_distance")),
|
||||||
|
max_distance = tonumber(c:get("aredn", "@lqm[0]", "max_distance")),
|
||||||
|
min_quality = tonumber(c:get("aredn", "@lqm[0]", "min_quality")),
|
||||||
|
margin_quality = tonumber(c:get("aredn", "@lqm[0]", "margin_quality")),
|
||||||
|
ping_penalty = tonumber(c:get("aredn", "@lqm[0]", "ping_penalty")),
|
||||||
|
user_blocks = c:get("aredn", "@lqm[0]", "user_blocks") or ""
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function should_block(track)
|
||||||
|
if now > track.pending then
|
||||||
|
return track.blocks.dtd or track.blocks.signal or track.blocks.distance or track.blocks.user or track.blocks.dup or track.blocks.quality
|
||||||
|
else
|
||||||
|
return track.blocks.dtd or track.blocks.user
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function should_nonpair_block(track)
|
||||||
|
return track.blocks.dtd or track.blocks.signal or track.blocks.distance or track.blocks.user or track.blocks.quality
|
||||||
|
end
|
||||||
|
|
||||||
|
function only_quality_block(track)
|
||||||
|
return track.blocked and track.blocks.quality and not (
|
||||||
|
track.blocks.dtd or track.blocks.signal or track.blocks.distance or track.blocks.user or track.blocks.dup
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
function update_block(track)
|
||||||
|
if should_block(track) then
|
||||||
|
if not track.blocked then
|
||||||
|
track.blocked = true
|
||||||
|
os.execute("/usr/sbin/iptables -D input_lqm -p udp --destination-port 698 -m mac --mac-source " .. track.mac .. " -j DROP 2> /dev/null")
|
||||||
|
os.execute("/usr/sbin/iptables -I input_lqm -p udp --destination-port 698 -m mac --mac-source " .. track.mac .. " -j DROP 2> /dev/null")
|
||||||
|
return "blocked"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if track.blocked then
|
||||||
|
track.blocked = false
|
||||||
|
os.execute("/usr/sbin/iptables -D input_lqm -p udp --destination-port 698 -m mac --mac-source " .. track.mac .. " -j DROP 2> /dev/null")
|
||||||
|
return "unblocked"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return "unchanged"
|
||||||
|
end
|
||||||
|
|
||||||
|
function calcDistance(lat1, lon1, lat2, lon2)
|
||||||
|
local r2 = 12742000 -- diameter earth (meters)
|
||||||
|
local p = 0.017453292519943295 -- Math.PI / 180
|
||||||
|
local v = 0.5 - math.cos((lat2 - lat1) * p) / 2 + math.cos(lat1 * p) * math.cos(lat2 * p) * (1 - math.cos((lon2 - lon1) * p)) / 2
|
||||||
|
return math.floor(r2 * math.asin(math.sqrt(v)))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Clear old data
|
||||||
|
local f = io.open("/tmp/lqm.info", "w")
|
||||||
|
f:write("{}")
|
||||||
|
f:close()
|
||||||
|
|
||||||
|
local cursor = uci.cursor()
|
||||||
|
|
||||||
|
-- Get radio
|
||||||
|
local radioname = "radio0"
|
||||||
|
for i = 0,2
|
||||||
|
do
|
||||||
|
if cursor:get("wireless","@wifi-iface[" .. i .. "]", "network") == "wifi" then
|
||||||
|
radioname = cursor:get("wireless","@wifi-iface[" .. i .. "]", "device")
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local phy = "phy" .. radioname:match("radio(%d+)")
|
||||||
|
local wlan = get_ifname("wifi")
|
||||||
|
|
||||||
|
function lqm()
|
||||||
|
|
||||||
|
-- Let things startup for a while before we begin
|
||||||
|
wait_for_ticks(math.max(1, 30 - nixio.sysinfo().uptime))
|
||||||
|
|
||||||
|
-- Create filters (cannot create during install as they disappear on reboot)
|
||||||
|
os.execute("/usr/sbin/iptables -F input_lqm 2> /dev/null")
|
||||||
|
os.execute("/usr/sbin/iptables -X input_lqm 2> /dev/null")
|
||||||
|
os.execute("/usr/sbin/iptables -N input_lqm 2> /dev/null")
|
||||||
|
os.execute("/usr/sbin/iptables -D INPUT -j input_lqm -m comment --comment 'block low quality links' 2> /dev/null")
|
||||||
|
os.execute("/usr/sbin/iptables -I INPUT -j input_lqm -m comment --comment 'block low quality links' 2> /dev/null")
|
||||||
|
|
||||||
|
-- We dont know any distances yet
|
||||||
|
os.execute("iw " .. phy .. " set distance auto")
|
||||||
|
|
||||||
|
-- Setup a first_run timeout if this is our first every run
|
||||||
|
if cursor:get("aredn", "@lqm[0]", "first_run") == "0" then
|
||||||
|
first_run_timeout = 0
|
||||||
|
else
|
||||||
|
first_run_timeout = first_run_timeout + nixio.sysinfo().uptime
|
||||||
|
end
|
||||||
|
|
||||||
|
local tracker = {}
|
||||||
|
while true
|
||||||
|
do
|
||||||
|
now = nixio.sysinfo().uptime
|
||||||
|
|
||||||
|
local config = get_config()
|
||||||
|
|
||||||
|
local lat = tonumber(cursor:get("aredn", "@location[0]", "lat"))
|
||||||
|
local lon = tonumber(cursor:get("aredn", "@location[0]", "lon"))
|
||||||
|
|
||||||
|
local arps = {}
|
||||||
|
arptable(
|
||||||
|
function (entry)
|
||||||
|
arps[entry["HW address"]:upper()] = entry
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
local kv = {
|
||||||
|
["signal avg:"] = "signal",
|
||||||
|
["tx packets:"] = "tx_packets",
|
||||||
|
["tx retries:"] = "tx_retries",
|
||||||
|
["tx failed:"] = "tx_fail",
|
||||||
|
["tx bitrate:"] = "tx_rate"
|
||||||
|
}
|
||||||
|
local stations = {}
|
||||||
|
local station = {}
|
||||||
|
local noise = iwinfo.nl80211.noise(wlan) or -95
|
||||||
|
for line in io.popen("iw " .. wlan .. " station dump"):lines()
|
||||||
|
do
|
||||||
|
local mac = line:match("^Station ([0-9a-f:]+) ")
|
||||||
|
if mac then
|
||||||
|
station = {
|
||||||
|
signal = 0,
|
||||||
|
noise = noise,
|
||||||
|
}
|
||||||
|
stations[mac:upper()] = station
|
||||||
|
else
|
||||||
|
for k, v in pairs(kv)
|
||||||
|
do
|
||||||
|
local val = line:match(k .. "%s*([%d%-]+)")
|
||||||
|
if val then
|
||||||
|
station[v] = tonumber(val)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for mac, station in pairs(stations)
|
||||||
|
do
|
||||||
|
if station.signal ~= 0 then
|
||||||
|
local snr = station.signal - station.noise
|
||||||
|
if not tracker[mac] then
|
||||||
|
tracker[mac] = {
|
||||||
|
pending = now + pending_timeout,
|
||||||
|
refresh = 0,
|
||||||
|
mac = mac,
|
||||||
|
station = nil,
|
||||||
|
ip = nil,
|
||||||
|
hostname = nil,
|
||||||
|
lat = nil,
|
||||||
|
lon = nil,
|
||||||
|
distance = nil,
|
||||||
|
blocks = {
|
||||||
|
dtd = false,
|
||||||
|
signal = false,
|
||||||
|
distance = false,
|
||||||
|
pair = false,
|
||||||
|
quality = false
|
||||||
|
},
|
||||||
|
blocked = false,
|
||||||
|
snr = snr,
|
||||||
|
rev_snr = nil,
|
||||||
|
avg_snr = 0,
|
||||||
|
links = {},
|
||||||
|
tx_rate = 0,
|
||||||
|
last_tx = nil,
|
||||||
|
last_tx_total = nil
|
||||||
|
}
|
||||||
|
end
|
||||||
|
local track = tracker[mac]
|
||||||
|
|
||||||
|
-- If we have a direct dtd connection to this device, make sure we use that
|
||||||
|
local entry = arps[mac]
|
||||||
|
if entry then
|
||||||
|
track.ip = entry["IP address"]
|
||||||
|
local a, b, c = mac:match("^(..:..:..:)(..)(:..:..)$")
|
||||||
|
local dtd = arps[string.format("%s%02x%s", a, tonumber(b, 16) + 1, c):upper()]
|
||||||
|
if dtd and dtd.Device:match("%.2$") then
|
||||||
|
track.blocks.dtd = true
|
||||||
|
end
|
||||||
|
local hostname = nixio.getnameinfo(track.ip)
|
||||||
|
if hostname then
|
||||||
|
track.hostname = hostname:lower():match("^(.*)%.local%.mesh$")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Running average SNR
|
||||||
|
track.snr = math.ceil(snr_run_avg * track.snr + (1 - snr_run_avg) * snr)
|
||||||
|
|
||||||
|
-- Running average estimate of link quality
|
||||||
|
local tx = station.tx_packets
|
||||||
|
local tx_total = station.tx_packets + station.tx_fail + station.tx_retries
|
||||||
|
if not track.last_tx then
|
||||||
|
track.last_tx = tx
|
||||||
|
track.last_tx_total = tx_total
|
||||||
|
track.tx_quality = 100
|
||||||
|
elseif tx_total >= track.last_tx_total + quality_min_packets then
|
||||||
|
local tx_quality = 100 * (tx - track.last_tx) / (tx_total - track.last_tx_total)
|
||||||
|
track.last_tx = tx
|
||||||
|
track.last_tx_total = tx_total
|
||||||
|
track.last_quality = tx_quality
|
||||||
|
track.tx_quality = math.ceil(quality_run_avg * track.tx_quality + (1 - quality_run_avg) * tx_quality)
|
||||||
|
end
|
||||||
|
|
||||||
|
track.tx_rate = station.tx_rate
|
||||||
|
|
||||||
|
track.lastseen = now
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local distance = -1
|
||||||
|
local alt_distance = -1
|
||||||
|
local coverage = -1
|
||||||
|
|
||||||
|
-- Update link tracking state
|
||||||
|
for _, track in pairs(tracker)
|
||||||
|
do
|
||||||
|
-- Clear snr when we've not seen the node this time (disconnected)
|
||||||
|
if track.lastseen < now then
|
||||||
|
track.snr = 0
|
||||||
|
track.rev_snr = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Only refresh remote attributes periodically
|
||||||
|
if track.ip and (now > track.refresh or track.pending > now) then
|
||||||
|
track.refresh = now + refresh_timeout
|
||||||
|
local info = json.parse(luci.sys.httpget("http://" .. track.ip .. ":8080/cgi-bin/sysinfo.json?link_info=1&lqm=1"))
|
||||||
|
if info then
|
||||||
|
if tonumber(info.lat) and tonumber(info.lon) then
|
||||||
|
track.lat = tonumber(info.lat)
|
||||||
|
track.lon = tonumber(info.lon)
|
||||||
|
if lat and lon then
|
||||||
|
track.distance = calcDistance(lat, lon, track.lat, track.lon)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local old_rev_snr = track.rev_snr
|
||||||
|
track.links = {}
|
||||||
|
-- Note: We cannot assume a missing link means no wifi connection
|
||||||
|
track.rev_snr = null
|
||||||
|
if info.lqm and info.lqm.enabled then
|
||||||
|
for _, rtrack in pairs(info.lqm.info.trackers)
|
||||||
|
do
|
||||||
|
if rtrack.hostname then
|
||||||
|
local hostname = rtrack.hostname:lower():gsub("^dtdlink%.","")
|
||||||
|
track.links[hostname] = {
|
||||||
|
type = "RF",
|
||||||
|
snr = rtrack.snr
|
||||||
|
}
|
||||||
|
if myhostname == hostname then
|
||||||
|
if not old_rev_snr or not rtrack.snr then
|
||||||
|
track.rev_snr = rtrack.snr
|
||||||
|
else
|
||||||
|
track.rev_snr = math.ceil(snr_run_avg * old_rev_snr + (1 - snr_run_avg) * rtrack.snr)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for ip, link in pairs(info.link_info)
|
||||||
|
do
|
||||||
|
if link.hostname and link.linkType == "DTD" then
|
||||||
|
track.links[link.hostname:lower()] = { type = "DTD" }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- If there's no LQM information we fallback on using link information.
|
||||||
|
for ip, link in pairs(info.link_info)
|
||||||
|
do
|
||||||
|
if link.hostname then
|
||||||
|
local hostname = link.hostname:lower():gsub("^dtdlink%.","")
|
||||||
|
if link.linkType == "DTD" then
|
||||||
|
track.links[hostname] = { type = link.linkType }
|
||||||
|
elseif link.linkType == "RF" and link.signal and link.noise then
|
||||||
|
local snr = link.signal - link.noise
|
||||||
|
if not track.links[hostname] then
|
||||||
|
track.links[hostname] = {
|
||||||
|
type = link.linkType,
|
||||||
|
snr = snr
|
||||||
|
}
|
||||||
|
end
|
||||||
|
if myhostname == hostname then
|
||||||
|
if not old_rev_snr then
|
||||||
|
track.rev_snr = snr
|
||||||
|
else
|
||||||
|
track.rev_snr = math.ceil(snr_run_avg * old_rev_snr + (1 - snr_run_avg) * snr)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- Clear these if we cannot talk to the other end, so we dont use stale values
|
||||||
|
track.links = {}
|
||||||
|
track.rev_snr = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Update avg snr using both ends (if we have them)
|
||||||
|
track.avg_snr = (track.snr + (track.rev_snr or track.snr)) / 2
|
||||||
|
|
||||||
|
-- Routable
|
||||||
|
local rt = track.ip and ip.route(track.ip) or nil
|
||||||
|
if rt and tostring(rt.gw) == track.ip then
|
||||||
|
track.routable = true
|
||||||
|
else
|
||||||
|
track.routable = false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Ping addresses and penalize quality for excessively slow links
|
||||||
|
if track.ip and (not track.blocked or only_quality_block(track)) then
|
||||||
|
local sigsock = nixio.socket("inet", "dgram")
|
||||||
|
sigsock:setopt("socket", "bindtodevice", wlan)
|
||||||
|
sigsock:setopt("socket", "dontroute", 1)
|
||||||
|
sigsock:setopt("socket", "rcvtimeo", ping_timeout)
|
||||||
|
sigsock:connect(track.ip, 8080)
|
||||||
|
sigsock:send("")
|
||||||
|
-- There's no actual UDP server at the other end so recv will either timeout and return 'false' if the link is slow,
|
||||||
|
-- or will error and return 'nil' if there is a node and it send back an ICMP error quickly (which for our purposes is a positive)
|
||||||
|
if sigsock:recv(0) == false then
|
||||||
|
track.tx_quality = math.max(0, math.ceil(track.tx_quality - config.ping_penalty))
|
||||||
|
end
|
||||||
|
sigsock:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Inject traffic into links with poor quality
|
||||||
|
-- We do this so we can keep measuring the current link quality otherwise, once it becomes
|
||||||
|
-- bad, it wont be used and we can never tell if it becomes good again. Beware injecting too
|
||||||
|
-- much traffic because, on very poor links, this can generate multiple retries per packet, flooding
|
||||||
|
-- the wifi channel
|
||||||
|
if track.ip and only_quality_block(track) then
|
||||||
|
-- Create socket we use to inject traffic into degraded links
|
||||||
|
-- This is setup so it ignores routing and will always send to the correct wifi station
|
||||||
|
local sigsock = nixio.socket("inet", "dgram")
|
||||||
|
sigsock:setopt("socket", "bindtodevice", wlan)
|
||||||
|
sigsock:setopt("socket", "dontroute", 1)
|
||||||
|
for _ = 1,quality_injection_max
|
||||||
|
do
|
||||||
|
sigsock:sendto("", track.ip, 8080)
|
||||||
|
end
|
||||||
|
sigsock:close()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- First run handling (emergency node)
|
||||||
|
-- If this is the very first time this has even been run, either because this is an upgrade or a new install,
|
||||||
|
-- we make sure we can talk to *something* by adjusting config options so that's possible and we don't
|
||||||
|
-- accidentally isolate the node.
|
||||||
|
if first_run_timeout ~= 0 and now >= first_run_timeout then
|
||||||
|
local changes = {
|
||||||
|
snr = -1,
|
||||||
|
distance = nil,
|
||||||
|
tx_quality = nil
|
||||||
|
}
|
||||||
|
-- Scan through the list of nodes we're tracking and select the node with the best SNR then
|
||||||
|
-- adjust our settings so that this node is valid
|
||||||
|
for _, track in pairs(tracker)
|
||||||
|
do
|
||||||
|
local snr = track.snr
|
||||||
|
if track.rev_snr and track.rev_snr ~= 0 and track.rev_snr < snr then
|
||||||
|
snr = track.rev_snr
|
||||||
|
end
|
||||||
|
if snr > changes.snr then
|
||||||
|
changes.snr = snr
|
||||||
|
changes.distance = track.distance
|
||||||
|
changes.tx_quality = track.tx_quality
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local cursorb = uci.cursor("/etc/config.mesh")
|
||||||
|
if changes.snr > -1 then
|
||||||
|
if changes.snr < config.low then
|
||||||
|
cursor:set("aredn", "@lqm[0]", "min_snr", math.max(1, changes.snr - 3))
|
||||||
|
cursorb:set("aredn", "@lqm[0]", "min_snr", math.max(1, changes.snr - 3))
|
||||||
|
end
|
||||||
|
if changes.distance and changes.distance > config.max_distance then
|
||||||
|
cursor:set("aredn", "@lqm[0]", "max_distance", changes.distance)
|
||||||
|
cursorb:set("aredn", "@lqm[0]", "max_distance", changes.distance)
|
||||||
|
end
|
||||||
|
if changes.tx_quality and changes.tx_quality < config.min_quality then
|
||||||
|
cursor:set("aredn", "@lqm[0]", "min_quality", math.max(0, math.floor(changes.tx_quality - 20)))
|
||||||
|
cursorb:set("aredn", "@lqm[0]", "min_quality", math.max(0, math.floor(changes.tx_quality - 20)))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
cursor:set("aredn", "@lqm[0]", "first_run", "0")
|
||||||
|
cursorb:set("aredn", "@lqm[0]", "first_run", "0")
|
||||||
|
cursor:commit("aredn")
|
||||||
|
cursorb:commit("aredn")
|
||||||
|
first_run_timeout = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Work out what to block, unblock and limit
|
||||||
|
for _, track in pairs(tracker)
|
||||||
|
do
|
||||||
|
-- When unblocked link signal becomes too low, block
|
||||||
|
if not track.blocks.signal then
|
||||||
|
if track.snr < config.low or (track.rev_snr and track.rev_snr < config.low) then
|
||||||
|
track.blocks.signal = true
|
||||||
|
end
|
||||||
|
-- when blocked link becomes (low+margin) again, unblock
|
||||||
|
else
|
||||||
|
if track.snr >= config.low + config.margin and (not track.rev_snr or track.rev_snr >= config.low + config.margin) then
|
||||||
|
track.blocks.signal = false
|
||||||
|
-- When signal is good enough to unblock a link but the quality is low, artificially bump
|
||||||
|
-- it up to give the link chance to recover
|
||||||
|
if track.blocks.quality then
|
||||||
|
track.tx_quality = config.min_quality + config.margin_quality
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Block any nodes which are too distant
|
||||||
|
if not track.distance or (track.distance >= config.min_distance and track.distance <= config.max_distance) then
|
||||||
|
track.blocks.distance = false
|
||||||
|
else
|
||||||
|
track.blocks.distance = true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Block if user requested it
|
||||||
|
track.blocks.user = false
|
||||||
|
for val in string.gmatch(config.user_blocks, "([^,]+)")
|
||||||
|
do
|
||||||
|
if val == track.mac then
|
||||||
|
track.blocks.user = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Block if quality is poor
|
||||||
|
if track.tx_quality then
|
||||||
|
if not track.blocks.quality and track.tx_quality < config.min_quality then
|
||||||
|
track.blocks.quality = true
|
||||||
|
elseif track.blocks.quality and track.tx_quality >= config.min_quality + config.margin_quality then
|
||||||
|
track.blocks.quality = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Eliminate link pairs, where we might have links to multiple radios at the same site
|
||||||
|
-- Find them and select the one with the best SNR avg on both ends
|
||||||
|
for _, track in pairs(tracker)
|
||||||
|
do
|
||||||
|
if track.hostname and not should_nonpair_block(track) then
|
||||||
|
-- Get a list of radio pairs. These are radios we're associated with which are DTD'ed together
|
||||||
|
local tracklist = { track }
|
||||||
|
for _, track2 in pairs(tracker)
|
||||||
|
do
|
||||||
|
if track ~= track2 and track2.hostname and not should_nonpair_block(track2) then
|
||||||
|
local connection = track.links[track2.hostname]
|
||||||
|
if connection and connection.type == "DTD" then
|
||||||
|
tracklist[#tracklist + 1] = track2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if #tracklist == 1 then
|
||||||
|
track.blocks.dup = false
|
||||||
|
else
|
||||||
|
-- Find the link with the best average snr overall as well as unblocked
|
||||||
|
local bestany = track
|
||||||
|
local bestunblocked = nil
|
||||||
|
for _, track2 in ipairs(tracklist)
|
||||||
|
do
|
||||||
|
if track2.avg_snr > bestany.avg_snr then
|
||||||
|
bestany = track2
|
||||||
|
end
|
||||||
|
if not track2.blocks.dup and (not bestunblocked or (track2.avg_snr > bestunblocked.avg_snr)) then
|
||||||
|
bestunblocked = track2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- A new winner if it's sufficiently better than the current
|
||||||
|
if not bestunblocked or bestany.avg_snr >= bestunblocked.avg_snr + config.margin then
|
||||||
|
bestunblocked = bestany
|
||||||
|
end
|
||||||
|
for _, track2 in ipairs(tracklist)
|
||||||
|
do
|
||||||
|
if track2 == bestunblocked then
|
||||||
|
track2.blocks.dup = false
|
||||||
|
else
|
||||||
|
track2.blocks.dup = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Update the block state and calculate the routable distance
|
||||||
|
for _, track in pairs(tracker)
|
||||||
|
do
|
||||||
|
if update_block(track) == "unblocked" then
|
||||||
|
-- If the link becomes unblocked, return it to pending state
|
||||||
|
track.pending = now + pending_timeout
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Find the most distant, unblocked, routable, node
|
||||||
|
if not track.blocked and track.distance then
|
||||||
|
if now > track.pending and track.routable then
|
||||||
|
if track.distance > distance then
|
||||||
|
distance = track.distance
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if track.distance > alt_distance then
|
||||||
|
alt_distance = track.distance
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Remove any trackers which are too old
|
||||||
|
if now > track.lastseen + lastseen_timeout then
|
||||||
|
track.blocked = true;
|
||||||
|
track.blocks = {}
|
||||||
|
update_block(track)
|
||||||
|
tracker[track.mac] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
distance = distance + 1
|
||||||
|
alt_distance = alt_distance + 1
|
||||||
|
|
||||||
|
-- Update the wifi distance
|
||||||
|
if distance > 0 then
|
||||||
|
coverage = math.floor((distance * 2 * 0.0033) / 3) -- airtime
|
||||||
|
os.execute("iw " .. phy .. " set coverage " .. coverage)
|
||||||
|
elseif alt_distance > 1 then
|
||||||
|
coverage = math.floor((alt_distance * 2 * 0.0033) / 3)
|
||||||
|
os.execute("iw " .. phy .. " set coverage " .. coverage)
|
||||||
|
else
|
||||||
|
os.execute("iw " .. phy .. " set distance auto")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Save this for the UI
|
||||||
|
f = io.open("/tmp/lqm.info", "w")
|
||||||
|
if f then
|
||||||
|
f:write(json.stringify({
|
||||||
|
now = now,
|
||||||
|
trackers = tracker,
|
||||||
|
distance = distance,
|
||||||
|
coverage = coverage
|
||||||
|
}, true))
|
||||||
|
f:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
wait_for_ticks(60) -- 1 minute
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return lqm
|
|
@ -49,6 +49,7 @@ local tmpdir = "/tmp/snrlog"
|
||||||
local lastdat = "/tmp/snr.dat"
|
local lastdat = "/tmp/snr.dat"
|
||||||
local autolog = "/tmp/AutoDistReset.log"
|
local autolog = "/tmp/AutoDistReset.log"
|
||||||
local defnoise = -95
|
local defnoise = -95
|
||||||
|
local cursor = uci.cursor()
|
||||||
|
|
||||||
-- create tmp dir if needed
|
-- create tmp dir if needed
|
||||||
nixio.fs.mkdir(tmpdir)
|
nixio.fs.mkdir(tmpdir)
|
||||||
|
@ -220,7 +221,7 @@ function run_snrlog()
|
||||||
f:close()
|
f:close()
|
||||||
|
|
||||||
-- trigger auto distancing if necessary
|
-- trigger auto distancing if necessary
|
||||||
if trigger_auto_distance then
|
if trigger_auto_distance and cursor:get("aredn", "@lqm[0]", "enable") ~= "1" then
|
||||||
reset_auto_distance()
|
reset_auto_distance()
|
||||||
file_trim(autolog, MAXLINES)
|
file_trim(autolog, MAXLINES)
|
||||||
f, err = assert(io.open(autolog, "a"),"Cannot open file (autolog) to write!")
|
f, err = assert(io.open(autolog, "a"),"Cannot open file (autolog) to write!")
|
||||||
|
|
|
@ -286,6 +286,55 @@ local settings = {
|
||||||
type = "boolean",
|
type = "boolean",
|
||||||
desc = "Enable the included iperf3 client/server support",
|
desc = "Enable the included iperf3 client/server support",
|
||||||
default = "1"
|
default = "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category = "Link Quality",
|
||||||
|
key = "aredn.@lqm[0].enable",
|
||||||
|
type = "boolean",
|
||||||
|
desc = "Enable experimental link quality management",
|
||||||
|
default = "0",
|
||||||
|
postcallback = "lqm_defaults()",
|
||||||
|
needreboot = true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category = "Link Quality",
|
||||||
|
key = "aredn.@lqm[0].margin_snr",
|
||||||
|
type = "string",
|
||||||
|
desc = "Margin above minimim SNR a signal must reach to become acceptable",
|
||||||
|
default = "1",
|
||||||
|
condition = "lqm_enabled()"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category = "Link Quality",
|
||||||
|
key = "aredn.@lqm[0].min_distance",
|
||||||
|
type = "string",
|
||||||
|
desc = "Distance neightbor must be over to be acceptable",
|
||||||
|
default = "0",
|
||||||
|
condition = "lqm_enabled()"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category = "Link Quality",
|
||||||
|
key = "aredn.@lqm[0].margin_quality",
|
||||||
|
type = "string",
|
||||||
|
desc = "Quality increase before neighbor can be re-accepted",
|
||||||
|
default = "1",
|
||||||
|
condition = "lqm_enabled()"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category = "Link Quality",
|
||||||
|
key = "aredn.@lqm[0].ping_penalty",
|
||||||
|
type = "string",
|
||||||
|
desc = "Quality penalty when neighbor cannot be pinged",
|
||||||
|
default = "10",
|
||||||
|
condition = "lqm_enabled()"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category = "Link Quality",
|
||||||
|
key = "aredn.@lqm[0].user_blocks",
|
||||||
|
type = "string",
|
||||||
|
desc = "Comma separated list of blocked MACs",
|
||||||
|
default = "",
|
||||||
|
condition = "lqm_enabled()"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -432,6 +481,20 @@ function alert_purge()
|
||||||
os.remove("/tmp/local_message")
|
os.remove("/tmp/local_message")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function lqm_enabled()
|
||||||
|
return cursor_get("aredn", "@lqm[0]", "enable") == "1"
|
||||||
|
end
|
||||||
|
|
||||||
|
function lqm_defaults()
|
||||||
|
cursor_set("aredn", "@lqm[0]", "min_snr", "15")
|
||||||
|
cursor_set("aredn", "@lqm[0]", "margin_snr", "1")
|
||||||
|
cursor_set("aredn", "@lqm[0]", "min_distance", "0")
|
||||||
|
cursor_set("aredn", "@lqm[0]", "max_distance", "80467")
|
||||||
|
cursor_set("aredn", "@lqm[0]", "min_quality", "50")
|
||||||
|
cursor_set("aredn", "@lqm[0]", "ping_penalty", "10")
|
||||||
|
cursor_set("aredn", "@lqm[0]", "margin_quality", "1")
|
||||||
|
end
|
||||||
|
|
||||||
function writePackageRepo(repo)
|
function writePackageRepo(repo)
|
||||||
local uciurl = cursor_get("aredn", "@downloads[0]", "pkgs_" .. repo)
|
local uciurl = cursor_get("aredn", "@downloads[0]", "pkgs_" .. repo)
|
||||||
local disturl = capture("grep aredn_" .. repo .. " /etc/opkg/distfeeds.conf | cut -d' ' -f3")
|
local disturl = capture("grep aredn_" .. repo .. " /etc/opkg/distfeeds.conf | cut -d' ' -f3")
|
||||||
|
|
|
@ -0,0 +1,202 @@
|
||||||
|
#!/usr/bin/lua
|
||||||
|
--[[
|
||||||
|
|
||||||
|
Copyright (C) 2022 Tim Wilkinson
|
||||||
|
See Contributors file for additional contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation version 3 of the License.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Additional Terms:
|
||||||
|
|
||||||
|
Additional use restrictions exist on the AREDN(TM) trademark and logo.
|
||||||
|
See AREDNLicense.txt for more info.
|
||||||
|
|
||||||
|
Attributions to the AREDN Project must be retained in the source code.
|
||||||
|
If importing this code into a new or existing project attribution
|
||||||
|
to the AREDN project must be added to the source code.
|
||||||
|
|
||||||
|
You must not misrepresent the origin of the material contained within.
|
||||||
|
|
||||||
|
Modified versions must be modified to attribute to the original source
|
||||||
|
and be marked in reasonable ways as differentiate it from the original
|
||||||
|
version
|
||||||
|
|
||||||
|
--]]
|
||||||
|
|
||||||
|
require("uci")
|
||||||
|
require("aredn.http")
|
||||||
|
require("aredn.hardware")
|
||||||
|
local html = require("aredn.html")
|
||||||
|
local info = require("aredn.info")
|
||||||
|
|
||||||
|
local cursor = uci.cursor()
|
||||||
|
|
||||||
|
if cursor:get("aredn", "@lqm[0]", "enable") ~= "1" then
|
||||||
|
print "Content-type: text/text\r"
|
||||||
|
print "Cache-Control: no-store\r"
|
||||||
|
print "\r"
|
||||||
|
print "Disabled"
|
||||||
|
os.exit()
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local node = info.get_nvram("node")
|
||||||
|
local node_desc = cursor:get("system", "@system[0]", "description") or ""
|
||||||
|
local lat_lon = "<strong>Location Not Available</strong>"
|
||||||
|
local lat = cursor:get("aredn", "@location[0]", "lat")
|
||||||
|
local lon = cursor:get("aredn", "@location[0]", "lon")
|
||||||
|
if lat and lon then
|
||||||
|
lat_lon = string.format("<center><strong>Location: </strong> %s %s</center>", lat, lon)
|
||||||
|
end
|
||||||
|
|
||||||
|
http_header()
|
||||||
|
html.header(node .. " Neighbor Status")
|
||||||
|
html.print("<body>")
|
||||||
|
html.alert_banner()
|
||||||
|
html.print([[
|
||||||
|
<style>
|
||||||
|
.lt {
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 30px 0 4px 0;
|
||||||
|
}
|
||||||
|
#links {
|
||||||
|
padding-bottom: 16px;
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
#links > div {
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
.m, .b {
|
||||||
|
display: inline-block;
|
||||||
|
width: 190px;
|
||||||
|
}
|
||||||
|
.s {
|
||||||
|
display: inline-block;
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
.p {
|
||||||
|
display: inline-block;
|
||||||
|
width: 130px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<center>
|
||||||
|
<h1>]] .. node .. [[ neighbor status</h1>]] .. lat_lon)
|
||||||
|
if node_desc ~= "" then
|
||||||
|
html.print([[<table id='node_description_display'><tr><td>]] .. node_desc .. [[</td></tr></table>]])
|
||||||
|
end
|
||||||
|
html.print([[<hr>
|
||||||
|
<table width=750>
|
||||||
|
<tr><td>
|
||||||
|
<center>
|
||||||
|
<button type=button onClick='window.location.reload()' title='Refresh this page'>Refresh</button>
|
||||||
|
|
||||||
|
<button type=button onClick='window.location="status"' title='Return to the status page'>Quit</button>
|
||||||
|
</center>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td>
|
||||||
|
<div class="lt">
|
||||||
|
<span class="m">Neighbor</span><span class="s">SNR</span><span class="p">Distance</span><span class="s">Quality</span><span class="p">TX Estimate</span><span class="p">Status</span>
|
||||||
|
</div>
|
||||||
|
<div id="links"></div>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</center>
|
||||||
|
<script>
|
||||||
|
const meters_to_miles = 0.000621371;
|
||||||
|
const meters_to_km = 0.001;
|
||||||
|
const wifi_scale = 0.2;
|
||||||
|
const get_status = (track, data) => {
|
||||||
|
if (track.pending > data.info.now) {
|
||||||
|
return "pending";
|
||||||
|
}
|
||||||
|
if (track.blocked) {
|
||||||
|
if (track.blocks.user) {
|
||||||
|
return "blocked - user";
|
||||||
|
}
|
||||||
|
if (track.blocks.dtd) {
|
||||||
|
return "blocked - dtd";
|
||||||
|
}
|
||||||
|
if (track.blocks.distance) {
|
||||||
|
return "blocked - distance";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (track.blocks.signal) {
|
||||||
|
return "blocked - signal";
|
||||||
|
}
|
||||||
|
if (track.blocks.dup) {
|
||||||
|
return "blocked - dup";
|
||||||
|
}
|
||||||
|
if (track.blocks.quality) {
|
||||||
|
return "blocked - quality";
|
||||||
|
}
|
||||||
|
return "blocked";
|
||||||
|
}
|
||||||
|
if (track.routable) {
|
||||||
|
return "active";
|
||||||
|
}
|
||||||
|
return "idle";
|
||||||
|
}
|
||||||
|
const name = track => {
|
||||||
|
if (track.hostname || track.ip) {
|
||||||
|
return `<a href="http://${track.hostname || track.ip}:8080">${track.hostname || track.ip}</a>`;
|
||||||
|
}
|
||||||
|
return track.mac || "-";
|
||||||
|
}
|
||||||
|
let convertd = (d) => (meters_to_km * d).toFixed(1) + " km";
|
||||||
|
switch (navigator.language.split("-")[1] || "unknown") {
|
||||||
|
case "US":
|
||||||
|
case "GB":
|
||||||
|
convertd = (d) => (meters_to_miles * d).toFixed(1) + " miles";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const update = data => {
|
||||||
|
let links = "";
|
||||||
|
for (let mac in data.info.trackers) {
|
||||||
|
const track = data.info.trackers[mac];
|
||||||
|
let txspeed = "-";
|
||||||
|
let txquality = "-";
|
||||||
|
let distance = "-";
|
||||||
|
let status = get_status(track, data);
|
||||||
|
if (status === "pending" || !track.blocked) {
|
||||||
|
txspeed = (track.tx_rate * wifi_scale).toFixed(2) + " Mbps";
|
||||||
|
}
|
||||||
|
if (typeof track.tx_quality === "number" && (status === "pending" || !track.blocked || (track.blocks.quality && !(track.blocks.dtd || track.blocks.signal || track.blocks.distance || track.blocks.user || track.blocks.dup)))) {
|
||||||
|
txquality = track.tx_quality + "%";
|
||||||
|
}
|
||||||
|
if (typeof track.distance === "number") {
|
||||||
|
distance = convertd(track.distance);
|
||||||
|
}
|
||||||
|
links += `<div><span class="m">${name(track)}</span><span class="s">${track.snr}${"rev_snr" in track ? "/" + track.rev_snr : ""}</span><span class="p">${distance}</span><span class="s">${txquality}</span><span class="p">${txspeed}</span><span class="p">${status}</span></div>`;
|
||||||
|
}
|
||||||
|
document.getElementById("links").innerHTML = links;
|
||||||
|
}
|
||||||
|
const fetchAndUpdate = () => {
|
||||||
|
fetch("/cgi-bin/sysinfo.json?lqm=1").then(r => r.json()).then(data => {
|
||||||
|
update(data.lqm);
|
||||||
|
setTimeout(fetchAndUpdate, 60000);
|
||||||
|
}).catch(_ => {
|
||||||
|
setTimeout(fetchAndUpdate, 30000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setTimeout(fetchAndUpdate, 30000);
|
||||||
|
]])
|
||||||
|
local ls = nixio.fs.stat("/tmp/lqm.info")
|
||||||
|
if ls and ls.size > 0 then
|
||||||
|
html.print("update({info:" .. io.open("/tmp/lqm.info"):read("*a") .. "})")
|
||||||
|
end
|
||||||
|
html.print([[</script>]])
|
||||||
|
html.footer()
|
||||||
|
html.print("</body></html>")
|
||||||
|
http_footer()
|
|
@ -409,6 +409,12 @@ if not parms.reload then
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- lqm
|
||||||
|
local lqm_mode = false
|
||||||
|
if cursor:get("aredn", "@lqm[0]", "enable") == "1" then
|
||||||
|
lqm_mode = true
|
||||||
|
end
|
||||||
|
|
||||||
-- sanitize the active settings
|
-- sanitize the active settings
|
||||||
if not wifi_txpower or wifi_txpower > aredn.hardware.wifi_maxpower(wifi_channel) then
|
if not wifi_txpower or wifi_txpower > aredn.hardware.wifi_maxpower(wifi_channel) then
|
||||||
wifi_txpower = aredn.hardware.wifi_maxpower(wifi_channel)
|
wifi_txpower = aredn.hardware.wifi_maxpower(wifi_channel)
|
||||||
|
@ -429,11 +435,49 @@ parms.wifi_distance = wifi_distance
|
||||||
parms.wifi_txpower = wifi_txpower
|
parms.wifi_txpower = wifi_txpower
|
||||||
|
|
||||||
-- apply the wifi settings
|
-- apply the wifi settings
|
||||||
if (parms.button_apply or parms.button_save) and wifi_enable == "1" and phy then
|
if (parms.button_apply or parms.button_save) and wifi_enable == "1" then
|
||||||
if wifi_distance == 0 then
|
if not lqm_mode then
|
||||||
os.execute("iw phy " .. phy .. " set distance auto >/dev/null 2>&1")
|
if phy then
|
||||||
|
if wifi_distance == 0 then
|
||||||
|
os.execute("iw phy " .. phy .. " set distance auto >/dev/null 2>&1")
|
||||||
|
else
|
||||||
|
os.execute("iw phy " .. phy .. " set distance " .. wifi_distance .. " >/dev/null 2>&1")
|
||||||
|
end
|
||||||
|
end
|
||||||
else
|
else
|
||||||
os.execute("iw phy " .. phy .. " set distance " .. wifi_distance .. " >/dev/null 2>&1")
|
-- validate values
|
||||||
|
local lqm_min_snr = tonumber(parms.lqm_min_snr)
|
||||||
|
local lqm_max_distance = tonumber(parms.lqm_max_distance)
|
||||||
|
local lqm_distance_unit = parms.lqm_distance_unit or "mile"
|
||||||
|
local lqm_min_quality = tonumber(parms.lqm_min_quality)
|
||||||
|
if not lqm_min_snr or lqm_min_snr < 0 or lqm_min_snr > 95 then
|
||||||
|
err("ERROR: Minimum SNR out of range (0-95)")
|
||||||
|
end
|
||||||
|
local distance_scale = 1000
|
||||||
|
if lqm_distance_unit == "mile" then
|
||||||
|
distance_scale = 1609.344
|
||||||
|
if not lqm_max_distance or lqm_max_distance < 0 or lqm_max_distance > 71 then
|
||||||
|
err("ERROR: Maximum Distance out of range (0-71)")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if not lqm_max_distance or lqm_max_distance < 0 or lqm_max_distance > 114 then
|
||||||
|
err("ERROR: Maximum Distance out of range (0-114)")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not lqm_min_quality or lqm_min_quality < 0 or lqm_min_quality > 100 then
|
||||||
|
err("ERROR: Minimum Quality out of range (0-100)")
|
||||||
|
end
|
||||||
|
if #errors == 0 then
|
||||||
|
local cursorb = uci.cursor("/etc/config.mesh")
|
||||||
|
cursor:set("aredn", "@lqm[0]", "min_snr", parms.lqm_min_snr)
|
||||||
|
cursorb:set("aredn", "@lqm[0]", "min_snr", parms.lqm_min_snr)
|
||||||
|
cursor:set("aredn", "@lqm[0]", "max_distance", math.floor(parms.lqm_max_distance * distance_scale))
|
||||||
|
cursorb:set("aredn", "@lqm[0]", "max_distance", math.floor(parms.lqm_max_distance * distance_scale))
|
||||||
|
cursor:set("aredn", "@lqm[0]", "min_quality", parms.lqm_min_quality)
|
||||||
|
cursorb:set("aredn", "@lqm[0]", "min_quality", parms.lqm_min_quality)
|
||||||
|
cursor:commit("aredn")
|
||||||
|
cursorb:commit("aredn")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
os.execute("iw dev " .. wifiintf .. " set txpower fixed " .. wifi_txpower .. "00 >/dev/null 2>&1")
|
os.execute("iw dev " .. wifiintf .. " set txpower fixed " .. wifi_txpower .. "00 >/dev/null 2>&1")
|
||||||
end
|
end
|
||||||
|
@ -1035,7 +1079,11 @@ if wifi_enable == "1" then
|
||||||
|
|
||||||
hidden[#hidden + 1] = "<input type=hidden name=wifi_country value='HX'>"
|
hidden[#hidden + 1] = "<input type=hidden name=wifi_country value='HX'>"
|
||||||
|
|
||||||
html.print("<tr><td colspan=2 align=center><hr><small>Active Settings</small></td></tr>")
|
if lqm_mode then
|
||||||
|
html.print("<tr><th colspan=2 align=center><hr><small>Power & Link Quality</small></th></tr>")
|
||||||
|
else
|
||||||
|
html.print("<tr><th colspan=2 align=center><hr><small>Power & Distance</small></th></tr>")
|
||||||
|
end
|
||||||
html.print("<tr><td><nobr>Tx Power</nobr></td><td><select name=wifi_txpower>")
|
html.print("<tr><td><nobr>Tx Power</nobr></td><td><select name=wifi_txpower>")
|
||||||
local txpoweroffset = aredn.hardware.wifi_poweroffset(wifiintf)
|
local txpoweroffset = aredn.hardware.wifi_poweroffset(wifiintf)
|
||||||
for i = aredn.hardware.wifi_maxpower(wifi_channel),1,-1
|
for i = aredn.hardware.wifi_maxpower(wifi_channel),1,-1
|
||||||
|
@ -1043,18 +1091,41 @@ if wifi_enable == "1" then
|
||||||
html.print("<option value='" .. i .. "'".. (i == tonumber(wifi_txpower) and " selected" or "") .. ">" .. (txpoweroffset + i) .. " dBm</option>")
|
html.print("<option value='" .. i .. "'".. (i == tonumber(wifi_txpower) and " selected" or "") .. ">" .. (txpoweroffset + i) .. " dBm</option>")
|
||||||
end
|
end
|
||||||
html.print("</select> <a href=\"/help.html#power\" target=\"_blank\"><img src=\"/qmark.png\"></a></td></tr>")
|
html.print("</select> <a href=\"/help.html#power\" target=\"_blank\"><img src=\"/qmark.png\"></a></td></tr>")
|
||||||
html.print("<tr id='dist' class='dist-norm'><td>Distance to<br/>FARTHEST Neighbor<br/><h3>'0' is auto</h3></td>")
|
if lqm_mode then
|
||||||
|
local lqm_max_distance = tonumber(cursor:get("aredn", "@lqm[0]", "max_distance")) or 114750
|
||||||
local wifi_distance = math.floor(tonumber(wifi_distance))
|
local lqm_min_snr = cursor:get("aredn", "@lqm[0]", "min_snr") or "15"
|
||||||
local wifi_distance_disp_km = math.floor(wifi_distance / 1000)
|
local lqm_min_quality = cursor:get("aredn", "@lqm[0]", "min_quality") or "50"
|
||||||
local wifi_distance_disp_miles = string.format("%.2f", wifi_distance_disp_km * 0.621371192)
|
html.print([[
|
||||||
html.print("<td><input disabled size=6 type=text name='wifi_distance_disp_miles' value='" .. wifi_distance_disp_miles .. "' title='Distance to the farthest neighbor'> mi<br />")
|
<tr><td>Max Distance</td><td>
|
||||||
html.print("<input disabled size=6 type=text size=4 name='wifi_distance_disp_km' value='" .. wifi_distance_disp_km .. "' title='Distance to the farthest neighbor'> km<br />")
|
<script>
|
||||||
html.print("<input disabled size=6 type=text size=4 name='wifi_distance_disp_meters' value='" .. wifi_distance .."' title='Distance to the farthest neighbor'> m<br />")
|
var dm = ]] .. lqm_max_distance .. [[;
|
||||||
html.print("<input id='distance_slider' type='range' min='0' max='150' step='1' value='" .. wifi_distance_disp_km .."' oninput='updDist(this.value)' onchange='updDist(this.value)' /><br />")
|
switch (navigator.language.split("-")[1] || "unknown") {
|
||||||
html.print("<input type='hidden' size='6' name='wifi_distance' value='" .. wifi_distance .. "' />")
|
case "US":
|
||||||
html.print("</td></tr>")
|
case "GB":
|
||||||
|
document.write("<input type=hidden name='lqm_distance_unit' value='mile'><input type=text size=4 name='lqm_max_distance' value='" + (dm / 1609.344).toFixed(1) + "' title='Maximum distance to a neighbor before it will be ignored'> miles")
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
document.write("<input type=hidden name='lqm_distance_unit' value='kilometer'><input type=text size=4 name='lqm_max_distance' value='" + (dm / 1000).toFixed(1) + "' title='Maximum distance to a neighbor before it will be ignored'> km")
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</td></tr>
|
||||||
|
]])
|
||||||
|
html.print("<tr><td>Min SNR</td><td><input type=text size=4 name='lqm_min_snr' value='" .. lqm_min_snr .. "' title='Minimum SNR of neighbor before it will be accepted'></td></tr>")
|
||||||
|
html.print("<tr><td>Min Quality</td><td><input type=text size=4 name='lqm_min_quality' value='" .. lqm_min_quality .. "' title='Minimum acceptable link quality'> %</td></tr>")
|
||||||
|
else
|
||||||
|
html.print("<tr id='dist' class='dist-norm'><td>Distance to<br/>FARTHEST Neighbor<br/><h3>'0' is auto</h3></td>")
|
||||||
|
|
||||||
|
local wifi_distance = math.floor(tonumber(wifi_distance))
|
||||||
|
local wifi_distance_disp_km = math.floor(wifi_distance / 1000)
|
||||||
|
local wifi_distance_disp_miles = string.format("%.2f", wifi_distance_disp_km * 0.621371192)
|
||||||
|
html.print("<td><input disabled size=6 type=text name='wifi_distance_disp_miles' value='" .. wifi_distance_disp_miles .. "' title='Distance to the farthest neighbor'> mi<br />")
|
||||||
|
html.print("<input disabled size=6 type=text size=4 name='wifi_distance_disp_km' value='" .. wifi_distance_disp_km .. "' title='Distance to the farthest neighbor'> km<br />")
|
||||||
|
html.print("<input disabled size=6 type=text size=4 name='wifi_distance_disp_meters' value='" .. wifi_distance .."' title='Distance to the farthest neighbor'> m<br />")
|
||||||
|
html.print("<input id='distance_slider' type='range' min='0' max='150' step='1' value='" .. wifi_distance_disp_km .."' oninput='updDist(this.value)' onchange='updDist(this.value)' /><br />")
|
||||||
|
html.print("<input type='hidden' size='6' name='wifi_distance' value='" .. wifi_distance .. "' />")
|
||||||
|
html.print("</td></tr>")
|
||||||
|
end
|
||||||
html.print("<tr><td></td><td><input type=submit name=button_apply value=Apply title='Immediately use these active settings'></td></tr>")
|
html.print("<tr><td></td><td><input type=submit name=button_apply value=Apply title='Immediately use these active settings'></td></tr>")
|
||||||
else
|
else
|
||||||
hidden[#hidden + 1] = "<input type=hidden name=wifi_ssid value='" .. wifi_ssid .."'>"
|
hidden[#hidden + 1] = "<input type=hidden name=wifi_ssid value='" .. wifi_ssid .."'>"
|
||||||
|
@ -1172,7 +1243,7 @@ if (phycount > 1 and (wifi_enable ~= "1" or wifi3_enable ~= "1")) or (phycount =
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
html.print("<tr><th colspan=2>LAN Access Point</th></tr><tr><td>Enable</td><td><input type=checkbox name=wifi2_enable value=1" .. (wifi2_enable == "1" and " checked" or "") .. "></td></tr>")
|
html.print("<tr><th colspan=2><small>LAN Access Point</small></th></tr><tr><td>Enable</td><td><input type=checkbox name=wifi2_enable value=1" .. (wifi2_enable == "1" and " checked" or "") .. "></td></tr>")
|
||||||
if phycount > 1 then
|
if phycount > 1 then
|
||||||
html.print("<tr><td>AP band</td><td><select name=wifi2_hwmode>")
|
html.print("<tr><td>AP band</td><td><select name=wifi2_hwmode>")
|
||||||
if wifi_enable ~= "1" then
|
if wifi_enable ~= "1" then
|
||||||
|
@ -1227,7 +1298,7 @@ end
|
||||||
html.print("<tr><td><nobr>DNS 1</nobr></td><td><input type=text size=15 name=wan_dns1 value='" .. wan_dns1 .. "'></td></tr>")
|
html.print("<tr><td><nobr>DNS 1</nobr></td><td><input type=text size=15 name=wan_dns1 value='" .. wan_dns1 .. "'></td></tr>")
|
||||||
html.print("<tr><td><nobr>DNS 2</nobr></td><td><input type=text size=15 name=wan_dns2 value='" .. wan_dns2 .. "'></td></tr>")
|
html.print("<tr><td><nobr>DNS 2</nobr></td><td><input type=text size=15 name=wan_dns2 value='" .. wan_dns2 .. "'></td></tr>")
|
||||||
|
|
||||||
html.print("<tr><td colspan=2><hr></td></tr><tr><th colspan=2>Advanced WAN Access</th></tr>")
|
html.print("<tr><th colspan=2><hr></td></tr><tr><th colspan=2><small>Advanced WAN Access</small></th></tr>")
|
||||||
if wan_proto ~= "disabled" then
|
if wan_proto ~= "disabled" then
|
||||||
html.print("<tr><td><nobr>Allow others to<br>use my WAN</td><td><input type=checkbox name=olsrd_gw value=1 title='Allow this node to provide internet access to other mesh users'" .. (olsrd_gw ~= "0" and " checked" or "") .. "> <a href=\"/help.html#wansettings\" target=\"_blank\"><img src=\"/qmark.png\"></a></td></tr>")
|
html.print("<tr><td><nobr>Allow others to<br>use my WAN</td><td><input type=checkbox name=olsrd_gw value=1 title='Allow this node to provide internet access to other mesh users'" .. (olsrd_gw ~= "0" and " checked" or "") .. "> <a href=\"/help.html#wansettings\" target=\"_blank\"><img src=\"/qmark.png\"></a></td></tr>")
|
||||||
else
|
else
|
||||||
|
@ -1262,7 +1333,7 @@ if (phycount > 1 and (wifi_enable ~= "1" or wifi2_enable ~= "1")) or (phycount =
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
html.print("<tr><td colspan=2><hr></td></tr><tr><th colspan=2>WAN Wifi Client</th></tr><tr><td>Enable</td><td><input type=checkbox name=wifi3_enable value=1" .. (wifi3_enable == "1" and " checked" or "") .. "> <a href=\"/help.html#wanclient\" target=\"_blank\"><img src=\"/qmark.png\"></a></td></tr>")
|
html.print("<tr><td colspan=2><hr></td></tr><tr><th colspan=2><small>WAN Wifi Client</small></th></tr><tr><td>Enable</td><td><input type=checkbox name=wifi3_enable value=1" .. (wifi3_enable == "1" and " checked" or "") .. "> <a href=\"/help.html#wanclient\" target=\"_blank\"><img src=\"/qmark.png\"></a></td></tr>")
|
||||||
|
|
||||||
if wifi_enable ~= "1" and wifi2_enable ~= "1" and phycount > 1 then
|
if wifi_enable ~= "1" and wifi2_enable ~= "1" and phycount > 1 then
|
||||||
html.print("<tr><td>WAN Wifi Client band</td><td><select name=wifi3_hwmode>")
|
html.print("<tr><td>WAN Wifi Client band</td><td><select name=wifi3_hwmode>")
|
||||||
|
|
|
@ -242,6 +242,10 @@ html.print("<input type=submit name=refresh value=Refresh title='Refresh this pa
|
||||||
if config == "mesh" then
|
if config == "mesh" then
|
||||||
html.print(" ")
|
html.print(" ")
|
||||||
html.print("<button type=button onClick='window.location=\"mesh\"' title='See what is on the mesh'>Mesh Status</button>")
|
html.print("<button type=button onClick='window.location=\"mesh\"' title='See what is on the mesh'>Mesh Status</button>")
|
||||||
|
if cursor:get("aredn", "@lqm[0]", "enable") == "1" then
|
||||||
|
html.print(" ")
|
||||||
|
html.print("<button type=button onClick='window.location=\"lqm\"' title='See the link status to our neighbors'>Neighbor Status</button>")
|
||||||
|
end
|
||||||
if not wifi_disabled then
|
if not wifi_disabled then
|
||||||
html.print(" ")
|
html.print(" ")
|
||||||
html.print("<button type=button onClick='window.location=\"scan\"' title='See what wireless networks are nearby'>WiFi Scan</button>")
|
html.print("<button type=button onClick='window.location=\"scan\"' title='See what wireless networks are nearby'>WiFi Scan</button>")
|
||||||
|
|
|
@ -55,7 +55,7 @@ end
|
||||||
info={}
|
info={}
|
||||||
|
|
||||||
-- API version
|
-- API version
|
||||||
info['api_version']="1.10"
|
info['api_version']="1.11"
|
||||||
|
|
||||||
|
|
||||||
-- NODE name
|
-- NODE name
|
||||||
|
@ -164,6 +164,29 @@ if string.find(nixio.getenv("QUERY_STRING"):lower(),"link_info=1") then
|
||||||
info['link_info']=aredn_olsr.getCurrentNeighbors(true)
|
info['link_info']=aredn_olsr.getCurrentNeighbors(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- LQM INFO
|
||||||
|
if string.find(nixio.getenv("QUERY_STRING"):lower(),"lqm=1") then
|
||||||
|
local lqm = { enabled = false }
|
||||||
|
if ctx:get("aredn", "@lqm[0]", "enable") == "1" then
|
||||||
|
lqm.enabled = true
|
||||||
|
lqm.config = {
|
||||||
|
min_snr = tonumber(ctx:get("aredn", "@lqm[0]", "min_snr")),
|
||||||
|
margin_snr = tonumber(ctx:get("aredn", "@lqm[0]", "margin_snr")),
|
||||||
|
min_distance = tonumber(ctx:get("aredn", "@lqm[0]", "min_distance")),
|
||||||
|
max_distance = tonumber(ctx:get("aredn", "@lqm[0]", "max_distance")),
|
||||||
|
min_quality = tonumber(ctx:get("aredn", "@lqm[0]", "min_quality")),
|
||||||
|
margin_quality = tonumber(ctx:get("aredn", "@lqm[0]", "margin_quality")),
|
||||||
|
ping_penalty = tonumber(ctx:get("aredn", "@lqm[0]", "ping_penalty")),
|
||||||
|
user_blocks = ctx:get("aredn", "@lqm[0]", "user_blocks") or {}
|
||||||
|
}
|
||||||
|
lqm.info = {}
|
||||||
|
if nixio.fs.stat("/tmp/lqm.info") then
|
||||||
|
lqm.info = json.parse(io.open("/tmp/lqm.info", "r"):read("*a"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
info['lqm']=lqm
|
||||||
|
end
|
||||||
|
|
||||||
-- Output the HTTP header for JSON
|
-- Output the HTTP header for JSON
|
||||||
json_header()
|
json_header()
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue