--[[ 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 . 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 ip = require("luci.ip") local info = require("aredn.info") local socket = require("socket") 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 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 tx_quality_run_avg = 0.8 -- tx quality running average local ping_timeout = 1.0 -- timeout before ping gives a qualtiy penalty local ping_time_run_avg = 0.8 -- ping time runnng average local bitrate_run_avg = 0.8 -- rx/tx running average local dtd_distance = 50 -- distance (meters) after which nodes connected with DtD links are considered different sites local connect_timeout = 5 -- timeout (seconds) when fetching information from other nodes local speed_time = 10 -- local speed_limit = 1000 -- close connection if it's too slow (< 1kB/s for 10 seconds) local default_retries_scale = 2 local default_short_retries = 6 local default_long_retries = 4 local NFT = "/usr/sbin/nft" local IW = "/usr/sbin/iw" 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")), rts_threshold = tonumber(c:get("aredn", "@lqm[0]", "rts_threshold") or "1"), min_distance = tonumber(c:get("aredn", "@lqm[0]", "min_distance")), max_distance = tonumber(c:get("aredn", "@lqm[0]", "max_distance")), auto_distance = tonumber(c:get("aredn", "@lqm[0]", "auto_distance") or "0"), 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 "", user_allows = c:get("aredn", "@lqm[0]", "user_allows") or "" } end -- Connected if we have tracked this link recently function is_connected(track) if track.lastseen >= now then return true else return false end end -- Pending if this link is too new function is_pending(track) if track.pending > now then return true else return false end end function should_block(track) if track.user_allow then return false elseif is_pending(track) then return track.blocks.dtd or track.blocks.user else return track.blocks.dtd or track.blocks.signal or track.blocks.distance or track.blocks.user or track.blocks.dup or track.blocks.quality 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 or track.type ~= "RF" end function inject_quality_traffic(track) return track.ip and track.type ~= "DtD" and 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 should_ping(track) if track.ip and is_connected(track) and track.routable and not (track.blocks.dtd or track.blocks.distance or track.blocks.user) then return true else return false end end function nft_handle(list, query) for line in io.popen(NFT .. " -a list chain ip fw4 " .. list):lines() do local handle = line:match(query .. ".*# handle (%d+)$") if handle then return handle end end return nil end function update_block(track) if should_block(track) then track.blocked = true if track.type == "Tunnel" or track.type == "Wireguard" then if not nft_handle("input_lqm", "iifname \\\"" .. track.device .. "\\\" udp dport 698 drop") then os.execute(NFT .. " insert rule ip fw4 input_lqm iifname \\\"" .. track.device .. "\\\" udp dport 698 drop 2> /dev/null") return "blocked" end else if not nft_handle("input_lqm", "udp dport 698 ether saddr " .. track.mac:lower() .. " drop") then os.execute(NFT .. " insert rule ip fw4 input_lqm udp dport 698 ether saddr " .. track.mac .. " drop 2> /dev/null") return "blocked" end end else track.blocked = false if track.type == "Tunnel" or track.type == "Wireguard" then local handle = nft_handle("input_lqm", "iifname \\\"" .. track.device .. "\\\" udp dport 698 drop") if handle then os.execute(NFT .. " delete rule ip fw4 input_lqm handle " .. handle) return "unblocked" end else local handle = nft_handle("input_lqm", "udp dport 698 ether saddr " .. track.mac:lower() .. " drop") if handle then os.execute(NFT .. " delete rule ip fw4 input_lqm handle " .. handle) return "unblocked" end end end return "unchanged" end function force_remove_block(track) track.blocked = false local handle = nft_handle("input_lqm", "udp dport 698 ether saddr " .. track.mac:lower() .. " drop") if handle then os.execute(NFT .. " delete rule ip fw4 input_lqm handle " .. handle) end handle = nft_handle("input_lqm", "iifname \\\"" .. track.device .. "\\\" udp dport 698 drop") if handle then os.execute(NFT .. " delete rule ip fw4 input_lqm handle " .. handle) end end -- Distance in meters between two points function calc_distance(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 -- Canonical hostname function canonical_hostname(hostname) if hostname then hostname = hostname:lower():gsub("^dtdlink%.",""):gsub("^mid%d+%.",""):gsub("^xlink%d+%.",""):gsub("%.local%.mesh$", "") end return hostname end local myhostname = canonical_hostname(info.get_nvram("node") or "localnode") local myip = uci.cursor():get("network", "wifi", "ipaddr") -- Clear old data local f = io.open("/tmp/lqm.info", "w") f:write('{"trackers":{},"hidden_nodes":[]}') f:close() -- Get radio local radiomode = "none" local wlan = aredn.hardware.get_iface_name("wifi") local phy = "none" if wlan:match("^wlan(%d+)$") then phy = iwinfo.nl80211.phyname(wlan) radiomode = "adhoc" end function iw_set(cmd) if phy ~= "none" then os.execute(IW .. " " .. phy .. " set " .. cmd .. " > /dev/null 2>&1") end end function lqm() if uci.cursor():get("aredn", "@lqm[0]", "enable") ~= "1" then exit_app() return end -- Let things startup for a while before we begin wait_for_ticks(math.max(0, 30 - nixio.sysinfo().uptime)) -- Create filters (cannot create during install as they disappear on reboot) os.execute(NFT .. " flush chain ip fw4 input_lqm 2> /dev/null") os.execute(NFT .. " delete chain ip fw4 input_lqm 2> /dev/null") os.execute(NFT .. " add chain ip fw4 input_lqm 2> /dev/null") local handle = nft_handle("input", "jump input_lqm comment") if handle then os.execute(NFT .. " delete rule ip fw4 input handle " .. handle) end os.execute(NFT .. " insert rule ip fw4 input jump input_lqm comment \\\"block low quality links\\\"") -- We dont know any distances yet iw_set("distance auto") -- Or any hidden nodes iw_set("rts off") -- If the channel bandwidth is less than 20, we need to adjust what we report as the values from 'iw' will not -- be correct local channel_bw_scale = 1 local chanbw = read_all("/sys/kernel/debug/ieee80211/" .. phy .. "/ath10k/chanbw") if not chanbw then chanbw = read_all("/sys/kernel/debug/ieee80211/" .. phy .. "/ath9k/chanbw") end if chanbw then chanbw = tonumber(chanbw) if chanbw == 10 then channel_bw_scale = 0.5 elseif chanbw == 5 then channel_bw_scale = 0.25 end end local noise = -95 local tracker = {} local dtdlinks = {} local rflinks = {} local hidden_nodes = {} local last_coverage = -1 local last_short_retries = -1 local last_long_retries = -1 while true do now = nixio.sysinfo().uptime local config = get_config() local cursor = uci.cursor() local cursorm = uci.cursor("/etc/config.mesh") local lat = cursor:get("aredn", "@location[0]", "lat") local lon = cursor:get("aredn", "@location[0]", "lon") lat = tonumber(lat) lon = tonumber(lon) local arps = {} arptable( function (entry) if entry["Flags"] ~= "0x0" then entry["HW address"] = entry["HW address"]:upper() arps[#arps + 1] = entry end end ) -- Know our macs so we can exclude them local our_macs = {} for _, i in ipairs(nixio.getifaddrs()) do if i.family == "packet" and i.addr then our_macs[i.addr:upper()] = true end end local stations = {} -- RF if radiomode == "adhoc" then local kv = { ["signal avg:"] = "signal", ["tx packets:"] = "tx_packets", ["tx retries:"] = "tx_retries", ["tx failed:"] = "tx_fail", ["tx bitrate:"] = "tx_bitrate", ["rx bitrate:"] = "rx_bitrate" } local station = {} local cnoise = iwinfo.nl80211.noise(wlan) if cnoise and cnoise < -70 then noise = math.ceil(noise * 0.9 + cnoise * 0.1) end for line in io.popen(IW .. " " .. wlan .. " station dump"):lines() do local mac = line:match("^Station ([0-9a-fA-F:]+) ") if mac then station = { type = "RF", device = wlan, mac = mac:upper(), signal = 0, noise = noise, ip = nil, tx_bitrate = 0, rx_bitrate = 0 } for _, entry in ipairs(arps) do if entry["HW address"] == station.mac and entry.Device:match("^wlan") then station.ip = entry["IP address"] break end end stations[#stations + 1] = station else for k, v in pairs(kv) do local val = line:match(k .. "%s*([%d%-]+)") if val then station[v] = tonumber(val) if v == "tx_bitrate" or v == "rx_bitrate" then station[v] = station[v] * channel_bw_scale end end end end end end -- Tunnels local tunnel = {} for line in io.popen("ifconfig"):lines() do local tun = line:match("^(tun%d+)%s") if tun then tunnel = { type = "Tunnel", device = tun, signal = nil, ip = nil, mac = nil, tx_packets = 0, tx_fail = 0, tx_retries = 0, tx_bitrate = 0, rx_bitrate = 0 } stations[#stations + 1] = tunnel elseif line:match("^%s*$") then tunnel = nil elseif tunnel then local ip = line:match("P-t-P:(%d+%.%d+%.%d+%.%d+)") if ip then tunnel.ip = ip -- Fake a mac from the ip local a, b, c, d = ip:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)$") tunnel.mac = string.format("00:00:%02X:%02X:%02X:%02X", a, b, c, d) else local txp, txf = line:match("TX packets:(%d+)%s+errors:(%d+)") if txp and txf then tunnel.tx_packets = txp tunnel.tx_fail = txf end end end end -- Wireguard local wgc = 0 cursorm:foreach("wireguard", "client", function(s) if s.enabled == "1" then local a, b, c, d = s.clientip:match("^(%d+)%.(%d+)%.(%d+)%.(%d+):") d = tonumber(d) + 1 stations[#stations + 1] = { type = "Wireguard", device = "wgc" .. wgc, signal = nil, ip = string.format("%d.%d.%d.%d", a, b, c, d), mac = string.format("00:00:%02X:%02X:%02X:%02X", a, b, c, d), tx_packets = 0, tx_fail = 0, tx_retries = 0, tx_bitrate = 0, rx_bitrate = 0 } wgc = wgc + 1 end end ) local wgs = 0 cursorm:foreach("vtun", "server", function(s) if s.enabled == "1" and s.netip:match(":") then local a, b, c, d, _ = s.netip:match("^(%d+)%.(%d+)%.(%d+)%.(%d+):(%d+)$") stations[#stations + 1] = { type = "Wireguard", device = "wgs" .. wgs, signal = nil, ip = string.format("%d.%d.%d.%d", a, b, c, d), mac = string.format("00:00:%02X:%02X:%02X:%02X", a, b, c, d), tx_packets = 0, tx_fail = 0, tx_retries = 0, tx_bitrate = 0, rx_bitrate = 0 } wgs = wgs + 1 end end ) -- DtD for _, entry in ipairs(arps) do if entry.Device:match("%.2$") or entry.Device:match("^br%-dtdlink") then stations[#stations + 1] = { type = "DtD", device = entry.Device, signal = nil, ip = entry["IP address"], mac = entry["HW address"], tx_packets = 0, tx_fail = 0, tx_retries = 0, tx_bitrate = 0, rx_bitrate = 0 } end end -- Xlink if nixio.fs.stat("/etc/config.mesh/xlink") then uci.cursor("/etc/config.mesh"):foreach("xlink", "interface", function(section) if section.ifname then for _, entry in ipairs(arps) do if entry["Device"] == section.ifname then stations[#stations + 1] = { type = "Xlink", device = section.ifname, signal = nil, ip = entry["IP address"], mac = entry["HW address"], tx_packets = 0, tx_fail = 0, tx_retries = 0, tx_bitrate = 0, rx_bitrate = 0 } end end end end ) end for _, station in ipairs(stations) do if station.signal ~= 0 and not our_macs[station.mac] then if not tracker[station.mac] then tracker[station.mac] = { type = station.type, device = station.device, firstseen = now, lastseen = now, lastrouted = now, pending = now + pending_timeout, refresh = 0, mac = station.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 = 0, rev_snr = nil, avg_snr = 0, last_tx = nil, last_tx_total = nil, tx_quality = 100, ping_quality = 100, ping_success_time = 0, tx_bitrate = nil, rx_bitrate = nil, quality = 100, exposed = false } end local track = tracker[station.mac] -- IP and Hostname if station.ip and station.ip ~= track.ip then track.ip = station.ip track.hostname = nil end if not track.hostname and track.ip then local hostname = nixio.getnameinfo(track.ip) if hostname then track.hostname = canonical_hostname(hostname) end end -- Running average SNR if station.type == "RF" then local snr = station.signal - station.noise if track.snr == 0 then track.snr = snr else track.snr = math.ceil(snr_run_avg * track.snr + (1 - snr_run_avg) * snr) end end -- 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.min(100, math.max(0, math.ceil(tx_quality_run_avg * track.tx_quality + (1 - tx_quality_run_avg) * tx_quality))) end if not track.tx_bitrate then track.tx_bitrate = station.tx_bitrate else track.tx_bitrate = track.tx_bitrate * bitrate_run_avg + station.tx_bitrate * (1 - bitrate_run_avg) end if not track.rx_bitrate then track.rx_bitrate = station.rx_bitrate else track.rx_bitrate = track.rx_bitrate * bitrate_run_avg + station.rx_bitrate * (1 - bitrate_run_avg) end track.lastseen = now end end -- Count the RF links we have local rfcount = 0 for _, track in pairs(tracker) do if track.type == "RF" then rfcount = rfcount + 1 end end -- Update link tracking state for _, track in pairs(tracker) do -- Only refresh remote attributes periodically if track.ip and (now > track.refresh or is_pending(track)) then track.refresh = now + refresh_timeout local old_rev_snr = track.rev_snr track.rev_snr = null dtdlinks[track.mac] = {} track.exposed = false -- Refresh the hostname periodically as it can change local hostname = nixio.getnameinfo(track.ip) if hostname then track.hostname = canonical_hostname(hostname) end local raw = io.popen("/usr/bin/curl --retry 0 --connect-timeout " .. connect_timeout .. " --speed-time " .. speed_time .. " --speed-limit " .. speed_limit .. " -s \"http://" .. track.ip .. ":8080/cgi-bin/sysinfo.json?link_info=1&lqm=1\" -o - 2> /dev/null") local info = luci.jsonc.parse(raw:read("*a")) raw:close() wait_for_ticks(0) if info then rflinks[track.mac] = nil 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 = calc_distance(lat, lon, track.lat, track.lon) end end if track.type == "RF" then if info.lqm and info.lqm.enabled and info.lqm.info and info.lqm.info.trackers then rflinks[track.mac] = {} for _, rtrack in pairs(info.lqm.info.trackers) do if not rtrack.type or rtrack.type == "RF" then local rhostname = canonical_hostname(rtrack.hostname) if rtrack.ip then local rdistance = nil if tonumber(rtrack.lat) and tonumber(rtrack.lon) and lat and lon then rdistance = calc_distance(lat, lon, tonumber(rtrack.lat), tonumber(rtrack.lon)) end if rtrack.routable then rflinks[track.mac][rtrack.ip] = { ip = rtrack.ip, hostname = rhostname, distance = rdistance } end end if myhostname == rhostname 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 if rtrack.routable and not tracker[rtrack.mac] and not our_macs[rtrack.mac] and rfcount > 1 then track.exposed = true end end end for ip, link in pairs(info.link_info) do if link.hostname and link.linkType == "DTD" then dtdlinks[track.mac][canonical_hostname(link.hostname)] = true end end elseif info.link_info then rflinks[track.mac] = {} -- If there's no LQM information we fallback on using link information. for ip, link in pairs(info.link_info) do if link.linkType == "RF" then rflinks[track.mac][ip] = { ip = ip, hostname = canonical_hostname(link.hostname) } end if link.hostname then local hostname = canonical_hostname(link.hostname) if link.linkType == "DTD" then dtdlinks[track.mac][hostname] = true elseif link.linkType == "RF" and link.signal and link.noise and myhostname == hostname then local snr = link.signal - link.noise 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 end end if is_connected(track) then -- 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 track.lastrouted = now else track.routable = false end else -- Clear snr when we've not seen the node this time (disconnected) track.snr = 0 track.rev_snr = nil track.routable = false end -- Ping addresses and penalize quality for excessively slow links if config.ping_penalty <= 0 then track.ping_quality = 100 elseif should_ping(track) then local success = 100 local ptime = ping_timeout -- Measure the "ping" time directly to the device local sigsock = nixio.socket("inet", "dgram") sigsock:setopt("socket", "rcvtimeo", ping_timeout) sigsock:setopt("socket", "bindtodevice", track.device) sigsock:setopt("socket", "dontroute", 1) -- Must connect or we wont see the error sigsock:connect(track.ip, 8080) local pstart = socket.gettime(0) 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 success = 0 end ptime = socket.gettime(0) - pstart sigsock:close() wait_for_ticks(0) local ping_loss_run_avg = 1 - config.ping_penalty / 100 if success > 0 then track.ping_success_time = track.ping_success_time * ping_time_run_avg + ptime * (1 - ping_time_run_avg) end track.ping_quality = math.ceil(ping_loss_run_avg * track.ping_quality + (1 - ping_loss_run_avg) * success) if success == 0 and track.type == "DtD" and track.firstseen == now then -- If local ping immediately fail, ditch this tracker. This can happen sometimes when we -- find arp entries which aren't valid. tracker[track.mac] = nil end end -- Calculate overall link quality track.quality = math.ceil((track.tx_quality + track.ping_quality) / 2) -- 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 inject_quality_traffic(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", track.device) sigsock:setopt("socket", "dontroute", 1) for _ = 1,quality_injection_max do sigsock:sendto("", track.ip, 8080) end sigsock:close() end end -- Work out what to block, unblock and limit for _, track in pairs(tracker) do -- SNR and distance blocks only related to RF links if track.type == "RF" then -- If we have a direct dtd connection to this device, make sure we use that track.blocks.dtd = false for _, dtd in pairs(tracker) do if dtd.type == "DtD" and dtd.hostname == track.hostname then if dtd.distance and dtd.distance < dtd_distance then track.blocks.dtd = true end break end end -- 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.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 end -- Block if user requested it track.blocks.user = false for val in string.gmatch(config.user_blocks, "([^,]+)") do if val:gsub("%s+", ""):gsub("-", ":"):upper() == track.mac then track.blocks.user = true break end end -- Block if quality is poor if track.quality then if not track.blocks.quality and track.quality < config.min_quality and (track.type ~= "DtD" or (track.distance and track.distance >= dtd_distance)) then track.blocks.quality = true elseif track.blocks.quality and track.quality >= config.min_quality + config.margin_quality then track.blocks.quality = false end end -- Always allow if user requested it track.user_allow = false; for val in string.gmatch(config.user_allows, "([^,]+)") do if val:gsub("%s+", ""):gsub("-", ":"):upper() == track.mac then track.user_allow = true break 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 if dtdlinks[track.mac][track2.hostname] then if not (track.lat and track.lon and track2.lat and track2.lon) or calc_distance(track.lat, track.lon, track2.lat, track2.lon) < dtd_distance then tracklist[#tracklist + 1] = track2 end 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 local distance = -1 -- Update the block state and calculate the routable distance for _, track in pairs(tracker) do if is_connected(track) then 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, RF node if track.type == "RF" then if track.distance then if track.distance > distance and (not track.blocked or is_pending(track)) then distance = track.distance end elseif is_pending(track) then distance = config.max_distance end end end -- Remove any trackers which are too old or if they disconnect when first seen if ((now > track.lastseen + lastseen_timeout) or (not is_connected(track) and track.firstseen + pending_timeout > now)) then force_remove_block(track) tracker[track.mac] = nil end end -- Default distances if we haven't calcuated anything if distance < 0 then if config.auto_distance > 0 then distance = config.auto_distance else distance = config.max_distance end end -- Update the wifi distance local coverage = math.min(255, math.floor((distance * 2 * 0.0033) / 3)) if coverage ~= last_coverage then iw_set("coverage " .. coverage) last_coverage = coverage end -- Set the RTS/CTS state depending on whether everyone can see everyone -- Build a list of all the nodes our neighbors can see local theres = {} for mac, rfneighbor in pairs(rflinks) do local track = tracker[mac] if track and not track.blocked and track.routable then for nip, ninfo in pairs(rfneighbor) do theres[nip] = ninfo end end end -- Remove all the nodes we can see from this set for _, track in pairs(tracker) do if track.ip then theres[track.ip] = nil end end -- Including ourself theres[myip] = nil -- If there are any nodes left, then our neighbors can see hidden nodes we cant. Enable RTS/CTS local hidden = {} for _, ninfo in pairs(theres) do hidden[#hidden + 1] = ninfo end -- Don't adjust RTS on ath10k for the moment - appear to be some bug to be worked out here if (#hidden == 0) ~= (#hidden_nodes == 0) and config.rts_threshold >= 0 and config.rts_threshold <= 2347 then if #hidden > 0 then iw_set("rts " .. config.rts_threshold) else iw_set("rts off") end end hidden_nodes = hidden -- Adjust retry attempts. If we're just managing a single connection, we can retry failed transmissions. -- If we're managing many, we can't afford the delays associated with retries, to reduce them to the minimum. -- Don't retry when distance is max because the delay makes thing unusable and blocks other closer traffic. local short_retries local long_retries if distance == config.max_distance or rfcount > 1 then short_retries = 1 long_retries = 1 else short_retries = default_short_retries long_retries = default_long_retries end if short_retries ~= last_short_retries or long_retries ~= last_long_retries then iw_set("retry short " .. short_retries .. " long " .. long_retries) last_short_retries = short_retries last_long_retries = long_retries end -- Save this for the UI f = io.open("/tmp/lqm.info", "w") if f then f:write(luci.jsonc.stringify({ now = now, trackers = tracker, distance = distance, coverage = coverage, hidden_nodes = hidden_nodes }, true)) f:close() end wait_for_ticks(60) -- 1 minute end end return lqm