2022-01-19 06:42:31 -07:00
|
|
|
#!/usr/bin/lua
|
|
|
|
--[[
|
|
|
|
|
|
|
|
Part of AREDN -- Used for creating Amateur Radio Emergency Data Networks
|
|
|
|
Copyright (C) 2021 Tim Wilkinson
|
|
|
|
Original Perl Copyright (C) 2015 Conrad Lara
|
|
|
|
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("aredn.http")
|
|
|
|
require("aredn.hardware")
|
|
|
|
local html = require("aredn.html")
|
|
|
|
local aredn_info = require("aredn.info")
|
|
|
|
|
|
|
|
local node = aredn_info.get_nvram("node")
|
|
|
|
if not node then
|
|
|
|
node = "NOCALL"
|
|
|
|
end
|
|
|
|
local wifiiface = aredn.hardware.get_iface_name("wifi")
|
2022-06-24 15:50:05 -06:00
|
|
|
local phy = iwinfo.nl80211.phyname(wifiiface)
|
|
|
|
local rfband = aredn.hardware.get_rfband()
|
2022-06-25 19:45:28 -06:00
|
|
|
local nf = iwinfo.nl80211.noise(wifiiface) or -95
|
2022-01-19 06:42:31 -07:00
|
|
|
|
|
|
|
if not nixio.fs.stat("/tmp/web") then
|
|
|
|
nixio.fs.mkdir("/tmp/web")
|
|
|
|
end
|
|
|
|
|
|
|
|
-- scan start
|
|
|
|
|
|
|
|
local scanned = {}
|
2022-06-24 15:50:05 -06:00
|
|
|
os.execute("echo 'chanscan' > /sys/kernel/debug/ieee80211/" .. phy .. "/ath9k/spectral_scan_ctl")
|
2022-01-19 06:42:31 -07:00
|
|
|
local f = io.popen("iw dev " .. wifiiface .. " scan passive")
|
|
|
|
if f then
|
|
|
|
local scan
|
|
|
|
for line in f:lines()
|
|
|
|
do
|
|
|
|
local m = line:match("^BSS ([%da-fA-F:]+)")
|
|
|
|
if m then
|
|
|
|
scan = {
|
|
|
|
mac = m,
|
|
|
|
mode = "AP",
|
|
|
|
ssid = "",
|
|
|
|
signal = 0,
|
|
|
|
freq = 0,
|
|
|
|
key = ""
|
|
|
|
}
|
2022-03-05 20:50:54 -07:00
|
|
|
scanned[#scanned + 1] = scan
|
2022-01-19 06:42:31 -07:00
|
|
|
if line:match("joined") then
|
|
|
|
scan.mode = "My Ad-Hoc Network"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
m = line:match("freq: (%d+)")
|
|
|
|
if m then
|
|
|
|
scan.freq = tonumber(m)
|
|
|
|
end
|
|
|
|
m = line:match("SSID: (.+)")
|
|
|
|
if m then
|
|
|
|
scan.ssid = m
|
|
|
|
end
|
|
|
|
m = line:match("signal: ([%d-]+)")
|
|
|
|
if m then
|
|
|
|
scan.signal = tonumber(m)
|
|
|
|
end
|
|
|
|
m = line:match("Group cipher: (.+)")
|
|
|
|
if m then
|
|
|
|
scan.key = m
|
|
|
|
end
|
|
|
|
if line:match("capability: IBSS") and scan.mode == "AP" then
|
|
|
|
scan.mode = "Foreign Ad-Hoc Network"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
f:close()
|
|
|
|
end
|
2022-06-24 15:50:05 -06:00
|
|
|
|
|
|
|
local samples = io.open("/sys/kernel/debug/ieee80211/" .. phy .. "/ath9k/spectral_scan0", "rb"):read("*a")
|
|
|
|
os.execute("echo 'disable' > /sys/kernel/debug/ieee80211/" .. phy .. "/ath9k/spectral_scan_ctl")
|
|
|
|
|
2022-01-19 06:42:31 -07:00
|
|
|
local f = io.popen("iw dev " .. wifiiface .. " station dump")
|
|
|
|
if f then
|
|
|
|
local scan
|
|
|
|
local myssid = aredn_info.getSSID()
|
|
|
|
local myfreq = tonumber(aredn_info.getFreq())
|
|
|
|
for line in f:lines()
|
|
|
|
do
|
|
|
|
local m = line:match("^Station ([%da-fA-F:]+) %(on " .. wifiiface .. "%)")
|
|
|
|
if m then
|
2022-03-05 20:50:54 -07:00
|
|
|
scan = {
|
|
|
|
mac = m,
|
|
|
|
mode = "Connected Ad-Hoc Station",
|
|
|
|
ssid = myssid,
|
|
|
|
signal = 0,
|
|
|
|
freq = myfreq,
|
|
|
|
key = ""
|
|
|
|
}
|
|
|
|
scanned[#scanned + 1] = scan
|
2022-01-19 06:42:31 -07:00
|
|
|
end
|
|
|
|
m = line:match("signal avg:%s+([%d-]+)")
|
|
|
|
if m then
|
|
|
|
scan.signal = tonumber(m)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
f:close()
|
|
|
|
end
|
|
|
|
|
|
|
|
-- scan end
|
|
|
|
|
|
|
|
if os.getenv("REQUEST_METHOD") == "POST" then
|
|
|
|
require('luci.http')
|
|
|
|
require('luci.sys')
|
|
|
|
local request = luci.http.Request(luci.sys.getenv(),
|
|
|
|
function()
|
|
|
|
local v = io.read(1024)
|
|
|
|
if not v then
|
|
|
|
io.close()
|
|
|
|
end
|
|
|
|
return v
|
|
|
|
end
|
|
|
|
)
|
|
|
|
if request:formvalue("auto") then
|
|
|
|
io.open("/tmp/web/autoscan", "w"):close()
|
|
|
|
end
|
|
|
|
if request:formvalue("stop") then
|
|
|
|
os.remove("/tmp/web/autoscan")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- generate page
|
|
|
|
http_header()
|
|
|
|
html.header(node .. " WiFi scan", false)
|
|
|
|
local autoscan = nixio.fs.stat("/tmp/web/autoscan");
|
|
|
|
if autoscan then
|
|
|
|
html.print("<meta http-equiv='refresh' content='5;url=/cgi-bin/scan'>")
|
|
|
|
end
|
|
|
|
html.print([[
|
2022-03-11 10:32:54 -07:00
|
|
|
<script src="/js/sorttable-min.js"></script>
|
2022-01-19 06:42:31 -07:00
|
|
|
<style>
|
|
|
|
table.sortable thead {
|
|
|
|
background-color:#eee;
|
|
|
|
color:#666666;
|
|
|
|
font-weight: bold;
|
|
|
|
cursor: default;
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body><form method=post action=/cgi-bin/scan enctype='multipart/form-data'>
|
|
|
|
<center>
|
|
|
|
]])
|
|
|
|
|
|
|
|
html.alert_banner()
|
|
|
|
html.print("<h1>" .. node .. " WiFi scan</h1><hr>")
|
|
|
|
|
|
|
|
if autoscan then
|
|
|
|
html.print("<input type=submit name=stop value=Stop title='Abort continuous scan'>")
|
2013-11-14 23:11:16 -07:00
|
|
|
else
|
2022-01-19 06:42:31 -07:00
|
|
|
html.print("<input type=submit name=refresh value=Refresh title='Refresh this page'>")
|
|
|
|
html.print(" ")
|
|
|
|
html.print("<input type=submit name=auto value=Auto title='Begin continuous scan'>")
|
|
|
|
end
|
|
|
|
|
|
|
|
html.print(" ")
|
|
|
|
html.print("<button type=button onClick='window.location=\"status\"' title='Return to status page'>Quit</button><br><br>")
|
|
|
|
|
|
|
|
-- display scan
|
|
|
|
html.print("<table class=sortable border=1 cellpadding=5>")
|
2022-06-25 19:45:28 -06:00
|
|
|
html.print("<tr><th>SNR</th><th>Signal</th><th>Chan</th><th>Enc</th><th>SSID</th><th>Hostname</th><th>MAC/BSSID</th><th>802.11 Mode</th></tr>")
|
2022-01-19 06:42:31 -07:00
|
|
|
|
|
|
|
-- load arp cache
|
|
|
|
local arpcache = {}
|
|
|
|
arptable(function(a)
|
|
|
|
arpcache[a["HW address"]] = a["IP address"]
|
|
|
|
end)
|
|
|
|
|
|
|
|
local scanlist = {}
|
2022-03-05 20:50:54 -07:00
|
|
|
for _, v in ipairs(scanned)
|
2022-01-19 06:42:31 -07:00
|
|
|
do
|
|
|
|
if v.signal ~= 0 then
|
|
|
|
scanlist[#scanlist + 1] = v
|
|
|
|
end
|
|
|
|
end
|
|
|
|
table.sort(scanlist, function(a, b) return a.signal > b.signal end)
|
|
|
|
for _, scan in ipairs(scanlist)
|
|
|
|
do
|
|
|
|
-- freq to chan
|
|
|
|
local chan = scan.freq
|
|
|
|
if chan < 256 then
|
|
|
|
elseif chan == 2484 then
|
|
|
|
chan = 14
|
|
|
|
elseif chan == 2407 then
|
|
|
|
chan = 0
|
|
|
|
elseif chan < 2484 then
|
|
|
|
chan = (chan - 2407) / 5
|
|
|
|
elseif chan < 5000 then
|
|
|
|
elseif chan < 5380 then
|
|
|
|
chan = (chan - 5000) / 5
|
|
|
|
elseif chan < 5500 then
|
|
|
|
chan = chan - 2000
|
|
|
|
elseif chan < 6000 then
|
|
|
|
chan = (chan - 5000) / 5
|
|
|
|
end
|
|
|
|
-- ip lookup then host lookup
|
|
|
|
local ip = arpcache[scan.mac]
|
|
|
|
if ip then
|
|
|
|
hostname = ip
|
|
|
|
local f = io.popen("nslookup " .. ip)
|
|
|
|
if f then
|
|
|
|
for line in f:lines()
|
|
|
|
do
|
|
|
|
local m = line:match("name = (.*)%.local%.mesh")
|
|
|
|
if m then
|
|
|
|
hostname = m
|
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
|
|
|
f:close()
|
|
|
|
end
|
|
|
|
else
|
|
|
|
hostname = "N/A"
|
|
|
|
end
|
|
|
|
if scan.ssid:match("^AREDN-") then
|
|
|
|
html.print("<tr class=\"wscan-row-node\">")
|
|
|
|
else
|
|
|
|
html.print("<tr>")
|
|
|
|
end
|
2022-06-25 19:45:28 -06:00
|
|
|
html.print("<td>" .. (scan.signal - nf) .. "</td><td>" .. scan.signal .. "</td><td>" .. chan .. "</td><td>" .. scan.key .. "</td><td>" .. scan.ssid .. "</td><td align=center>" .. hostname .. "</td><td>" .. scan.mac:upper() .. "</td><td>" .. scan.mode .. "</td>")
|
2022-01-19 06:42:31 -07:00
|
|
|
html.print("</tr>")
|
|
|
|
end
|
|
|
|
|
2022-06-24 15:50:05 -06:00
|
|
|
html.print("</table><br></center>")
|
|
|
|
|
|
|
|
-- Spectral information
|
|
|
|
local cwidth = 800;
|
|
|
|
local cheight = 400;
|
|
|
|
html.print([[
|
|
|
|
<center style="font-family: Arial">
|
|
|
|
<div style="padding:8px">Spectral View</div>
|
2022-06-25 19:45:28 -06:00
|
|
|
<div style="position:relative;display:inline-block"><div style="position:absolute;left:-30px;top:-200px;transform:rotate(-90deg)">SNR</div></div>
|
2022-06-24 15:50:05 -06:00
|
|
|
<canvas id="spectral" width="]] .. cwidth .. [[" height="]] .. cheight .. [[" style="border:1px solid grey;background-color:white"></canvas>
|
|
|
|
<div style="padding:4px">Channel</div>
|
|
|
|
</center>
|
|
|
|
<script>
|
|
|
|
const canvas = document.getElementById("spectral");
|
|
|
|
const ctx = canvas.getContext("2d");
|
|
|
|
]]);
|
|
|
|
|
|
|
|
function u8tos8(b)
|
|
|
|
return b >= 128 and b - 256 or b
|
|
|
|
end
|
|
|
|
|
2022-06-25 19:45:28 -06:00
|
|
|
local cnf = nf - 17.5 -- sub-carrier noise floor
|
2022-06-24 15:50:05 -06:00
|
|
|
local start_freq
|
2022-06-24 21:24:28 -06:00
|
|
|
local end_freq
|
2022-06-24 15:50:05 -06:00
|
|
|
if rfband == "900" then
|
|
|
|
start_freq = 902
|
2022-06-24 21:24:28 -06:00
|
|
|
end_freq = 925
|
2022-06-24 15:50:05 -06:00
|
|
|
html.print("const freq2chan = (f) => (f - 887) / 5;");
|
|
|
|
elseif rfband == "2400" then
|
|
|
|
start_freq = 2377
|
2022-06-24 21:24:28 -06:00
|
|
|
end_freq = 2477
|
2022-06-24 15:50:05 -06:00
|
|
|
html.print("const freq2chan = (f) => (f - 2407) / 5;");
|
|
|
|
elseif rfband == "3400" then
|
2022-06-24 21:24:28 -06:00
|
|
|
-- transverted
|
|
|
|
start_freq = 5370
|
|
|
|
end_freq = 5505
|
|
|
|
html.print("const freq2chan = (f) => (f - 5000) / 5;");
|
2022-06-24 15:50:05 -06:00
|
|
|
else
|
|
|
|
start_freq = 5650
|
2022-07-01 08:53:18 -06:00
|
|
|
end_freq = 5920
|
2022-06-24 15:50:05 -06:00
|
|
|
html.print("const freq2chan = (f) => (f - 5000) / 5;");
|
|
|
|
end
|
|
|
|
local bw
|
|
|
|
for line in io.lines("/etc/config.mesh/_setup")
|
|
|
|
do
|
|
|
|
bw = line:match("^wifi_chanbw%s=%s(%d+)")
|
|
|
|
if bw then
|
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
|
|
|
bw = tonumber(bw) or 10
|
2022-07-01 08:53:18 -06:00
|
|
|
if bw == 10 then
|
|
|
|
cnf = cnf - 3
|
|
|
|
elseif bw == 5 then
|
|
|
|
cnf = cnf - 6
|
|
|
|
end
|
|
|
|
|
|
|
|
local fbuckets = {}
|
|
|
|
for freq = 1, (end_freq - start_freq + bw) * 56 / bw
|
|
|
|
do
|
|
|
|
fbuckets[math.floor(freq)] = {}
|
|
|
|
end
|
2022-06-24 15:50:05 -06:00
|
|
|
|
2022-06-24 21:24:28 -06:00
|
|
|
local xscale = cwidth / (end_freq - start_freq)
|
2022-07-01 08:53:18 -06:00
|
|
|
local min_sig = -125
|
2022-06-24 15:50:05 -06:00
|
|
|
local max_sig = -60
|
|
|
|
local i = 1
|
|
|
|
while i < #samples
|
|
|
|
do
|
|
|
|
local t = samples:byte(i)
|
|
|
|
local l = samples:byte(i + 1) * 256 + samples:byte(i + 2)
|
|
|
|
if t == 1 then
|
|
|
|
local max_exp = samples:byte(i + 3)
|
2022-06-24 21:24:28 -06:00
|
|
|
local freq = samples:byte(i + 4) * 256 + samples:byte(i + 5)
|
|
|
|
if freq >= start_freq and freq <= end_freq then
|
2022-06-24 15:50:05 -06:00
|
|
|
local rssi = u8tos8(samples:byte(i + 6))
|
|
|
|
local noise = u8tos8(samples:byte(i + 7))
|
|
|
|
local datasqsum = 0
|
|
|
|
local v = {}
|
2022-06-25 19:45:28 -06:00
|
|
|
for dptr = 1,56
|
2022-06-24 15:50:05 -06:00
|
|
|
do
|
2022-06-25 19:45:28 -06:00
|
|
|
local data = nixio.bit.lshift(samples:byte(dptr + i + 19), max_exp)
|
|
|
|
data = data * data
|
|
|
|
datasqsum = datasqsum + data
|
2022-06-24 15:50:05 -06:00
|
|
|
v[dptr] = data
|
|
|
|
end
|
2022-06-25 19:45:28 -06:00
|
|
|
for dptr = 1,56
|
2022-06-24 15:50:05 -06:00
|
|
|
do
|
2022-06-25 19:45:28 -06:00
|
|
|
local datasq = v[dptr]
|
|
|
|
if datasq ~= 0 then
|
|
|
|
local sig = noise + rssi + 10 * math.log10(datasq / datasqsum)
|
2022-07-01 08:53:18 -06:00
|
|
|
if sig >= min_sig then
|
2022-06-25 19:45:28 -06:00
|
|
|
if sig > max_sig then
|
|
|
|
max_sig = sig
|
|
|
|
end
|
2022-07-01 08:53:18 -06:00
|
|
|
local bidx = math.floor((freq - start_freq) / bw * 56 + dptr - 28)
|
|
|
|
local bucket = fbuckets[bidx]
|
|
|
|
bucket[#bucket + 1] = math.floor((sig - min_sig) * 4)
|
2022-06-24 15:50:05 -06:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
i = i + 3 + l
|
|
|
|
end
|
2022-07-01 08:53:18 -06:00
|
|
|
|
|
|
|
local max_fsize = 0
|
|
|
|
for _, b in ipairs(fbuckets)
|
|
|
|
do
|
|
|
|
table.sort(b)
|
|
|
|
if #b > max_fsize then
|
|
|
|
max_fsize = #b
|
|
|
|
end
|
|
|
|
end
|
|
|
|
html.print("const n = null;")
|
|
|
|
html.write("const p=[")
|
|
|
|
local sizes = { 0.20, 0.30, 0.40, 0.50, 0.60 }
|
|
|
|
for _, b in ipairs(fbuckets)
|
|
|
|
do
|
|
|
|
local len = #b
|
|
|
|
local last = 0
|
|
|
|
for _, size in ipairs(sizes)
|
|
|
|
do
|
|
|
|
local max = math.floor(size * max_fsize)
|
|
|
|
if len >= max then
|
|
|
|
html.write((b[max] - last) .. ",")
|
|
|
|
last = b[max]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
if len > 0 and b[#b] ~= last then
|
|
|
|
html.write((b[#b] - last) .. ",")
|
|
|
|
end
|
|
|
|
html.write("n,")
|
|
|
|
end
|
|
|
|
html.print("-1];")
|
|
|
|
|
2022-06-24 15:50:05 -06:00
|
|
|
html.print("const yscale = " .. (-cheight / (max_sig - min_sig) / 4) .. ";")
|
|
|
|
html.print([[
|
2022-07-01 08:53:18 -06:00
|
|
|
const bcolors = [ "red", "yellow", "green", "cyan", "blue", "purple" ];
|
|
|
|
let xcursor = 0;
|
|
|
|
let ycursor = 0;
|
|
|
|
let ccursor = 0;
|
|
|
|
for (let i = 0; i < p.length; i++) {
|
|
|
|
let v = p[i];
|
|
|
|
if (v === null) {
|
|
|
|
xcursor += ]] .. (xscale * bw / 56) .. [[;
|
|
|
|
ycursor = 0;
|
|
|
|
ccursor = 0;
|
2022-06-24 15:50:05 -06:00
|
|
|
}
|
|
|
|
else {
|
2022-07-01 08:53:18 -06:00
|
|
|
v *= yscale;
|
|
|
|
ycursor += v;
|
|
|
|
ctx.fillStyle = bcolors[ccursor];
|
|
|
|
ctx.fillRect(xcursor, ]] .. cheight .. [[ + ycursor, ]] .. (xscale * bw / 56) .. [[, -v);
|
|
|
|
ccursor++;
|
2022-06-24 15:50:05 -06:00
|
|
|
}
|
|
|
|
}
|
2022-07-01 08:53:18 -06:00
|
|
|
ctx.strokeStyle = "rgba(0,0,0,0.5)";
|
2022-06-24 15:50:05 -06:00
|
|
|
ctx.fillStyle = "black";
|
2022-07-01 08:53:18 -06:00
|
|
|
ctx.font = "bold 12px Arial";
|
2022-06-24 15:50:05 -06:00
|
|
|
ctx.textAlign = "center";
|
|
|
|
ctx.beginPath()
|
2022-07-01 08:53:18 -06:00
|
|
|
for (let f = ]] .. start_freq .. [[; f <= ]] .. end_freq .. [[; f += 10) {
|
2022-06-24 21:24:28 -06:00
|
|
|
const x = Math.floor((f - ]] .. start_freq .. [[) * ]] .. xscale .. [[);
|
2022-06-24 15:50:05 -06:00
|
|
|
ctx.moveTo(x, 0);
|
|
|
|
ctx.lineTo(x, ]] .. (cheight - 20) .. [[);
|
|
|
|
ctx.fillText("" + freq2chan(f), x, ]] .. cheight - 4 .. [[);
|
|
|
|
}
|
|
|
|
ctx.stroke();
|
|
|
|
ctx.textAlign = "left";
|
2022-07-01 08:53:18 -06:00
|
|
|
ctx.strokeStyle = "rgba(0,0,0,0.5)";
|
2022-06-24 15:50:05 -06:00
|
|
|
ctx.beginPath();
|
2022-06-25 19:45:28 -06:00
|
|
|
for (let snr = 60; snr >= 0; snr -= 10) {
|
2022-07-01 08:53:18 -06:00
|
|
|
const y = ]] .. cheight .. [[ + yscale * (snr - ]] .. (min_sig - cnf) .. [[) * 4;
|
2022-06-25 19:45:28 -06:00
|
|
|
ctx.moveTo(20, y);
|
2022-06-24 15:50:05 -06:00
|
|
|
ctx.lineTo(]] .. cwidth .. [[, y);
|
2022-06-25 19:45:28 -06:00
|
|
|
ctx.fillText("" + snr, 2, y);
|
2022-06-24 15:50:05 -06:00
|
|
|
}
|
|
|
|
ctx.stroke();
|
|
|
|
</script></form>
|
|
|
|
]])
|
2022-01-19 06:42:31 -07:00
|
|
|
|
|
|
|
html.footer()
|
|
|
|
html.print("</body></html>")
|