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:
Tim Wilkinson 2022-05-18 10:49:00 -07:00 committed by GitHub
parent 276d1411f1
commit b23ab5ee8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 1057 additions and 22 deletions

View File

@ -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'

View File

@ -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

View File

@ -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

603
files/usr/local/bin/mgr/lqm.lua Executable file
View File

@ -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

View File

@ -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!")

View File

@ -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")

202
files/www/cgi-bin/lqm Executable file
View File

@ -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>
&nbsp;&nbsp;
<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()

View File

@ -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 &amp; Link Quality</small></th></tr>")
else
html.print("<tr><th colspan=2 align=center><hr><small>Power &amp; 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>&nbsp;&nbsp;<a href=\"/help.html#power\" target=\"_blank\"><img src=\"/qmark.png\"></a></td></tr>") html.print("</select>&nbsp;&nbsp;<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'>&nbsp;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'>&nbsp;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'>&nbsp;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'>&nbsp;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'>&nbsp;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'>&nbsp;%</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'>&nbsp;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'>&nbsp;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'>&nbsp;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 "") .. ">&nbsp;&nbsp;<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 "") .. ">&nbsp;&nbsp;<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 "") .. ">&nbsp;&nbsp;<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 "") .. ">&nbsp;&nbsp;<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>")

View File

@ -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("&nbsp;&nbsp;") html.print("&nbsp;&nbsp;")
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("&nbsp;&nbsp;")
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("&nbsp;&nbsp;") html.print("&nbsp;&nbsp;")
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>")

View File

@ -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()