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 Darryl Quinn
|
|
|
|
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("nixio")
|
|
|
|
require("aredn.http")
|
|
|
|
require("aredn.utils")
|
|
|
|
local html = require("aredn.html")
|
2024-04-01 23:15:45 -06:00
|
|
|
require("aredn.info")
|
2022-01-19 06:42:31 -07:00
|
|
|
|
2024-04-01 23:15:45 -06:00
|
|
|
local node = aredn.info.get_nvram("node")
|
2022-01-19 06:42:31 -07:00
|
|
|
if not node then
|
|
|
|
node = "NOCALL"
|
|
|
|
end
|
|
|
|
local device = ""
|
|
|
|
local tzone = capture("date +%Z"):gsub("\n","")
|
|
|
|
|
|
|
|
local dmode = "Realtime"
|
|
|
|
-- query string
|
|
|
|
local query = os.getenv("QUERY_STRING")
|
|
|
|
if query then
|
2023-12-17 17:20:41 -07:00
|
|
|
require('luci.http')
|
2022-01-19 06:42:31 -07:00
|
|
|
local params = luci.http.urldecode_params(query)
|
|
|
|
if params.realtime then
|
|
|
|
dmode = "Realtime"
|
|
|
|
else
|
|
|
|
dmode = "Archived"
|
|
|
|
end
|
|
|
|
device = params.device
|
|
|
|
end
|
|
|
|
|
|
|
|
-- get list of files from /tmp/snrlog
|
|
|
|
local snrfiles = {}
|
|
|
|
local devfound = false
|
|
|
|
if nixio.fs.stat("/tmp/snrlog") then
|
|
|
|
for filename in nixio.fs.dir("/tmp/snrlog")
|
|
|
|
do
|
|
|
|
snrfiles[#snrfiles + 1] = filename
|
|
|
|
if device == filename then
|
|
|
|
devfound = true
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
if not devfound then
|
|
|
|
device = ""
|
|
|
|
end
|
|
|
|
|
|
|
|
http_header()
|
|
|
|
html.header(node .. " " .. dmode .. " signal strength", false)
|
|
|
|
|
|
|
|
local prelude = [[
|
|
|
|
<link href="/loading.css" rel="stylesheet">
|
|
|
|
<script src="/js/jquery-2.1.4.min.js"></script>
|
|
|
|
<script src="/js/jquery.canvasjs.min.js"></script>
|
|
|
|
<script type="text/javascript">
|
|
|
|
var dps=[ [{"label":"Init","y":[-95,-95]}] ];
|
|
|
|
|
|
|
|
// Read a page's GET URL variables and return them as an associative array.
|
|
|
|
function getUrlVars()
|
|
|
|
{
|
|
|
|
var vars = [], hash;
|
|
|
|
var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&');
|
|
|
|
for(var i = 0; i < hashes.length; i++)
|
2015-12-10 10:14:43 -07:00
|
|
|
{
|
2022-01-19 06:42:31 -07:00
|
|
|
hash = hashes[i].split('=');
|
|
|
|
vars.push(hash[0]);
|
|
|
|
vars[ hash[0] ] = hash[1];
|
2015-12-10 10:14:43 -07:00
|
|
|
}
|
2022-01-19 06:42:31 -07:00
|
|
|
return vars;
|
2015-12-10 10:14:43 -07:00
|
|
|
}
|
2022-01-19 06:42:31 -07:00
|
|
|
|
|
|
|
function hide_spinner() {
|
|
|
|
$('#content-loading-spinner').dequeue();
|
|
|
|
$('#content-loading-spinner').hide();
|
|
|
|
$('#content-overlay').dequeue();
|
|
|
|
$('#content-overlay').hide();
|
|
|
|
}
|
|
|
|
|
|
|
|
$(document).ajaxComplete(function() {
|
|
|
|
hide_spinner();
|
|
|
|
});
|
|
|
|
|
|
|
|
$(document).ready(function () {
|
|
|
|
var MAXPOINTS=10;
|
|
|
|
var chart = new CanvasJS.Chart("chartContainer", {
|
|
|
|
zoomEnabled: true,
|
|
|
|
zoomType: "xy",
|
|
|
|
exportEnabled: true,
|
|
|
|
backgroundColor: "#E7E7E7",
|
|
|
|
title: {
|
|
|
|
text: "$dmode Signal to Noise"
|
|
|
|
},
|
|
|
|
legend: {
|
|
|
|
horizontalAlign: "right", // left, center ,right
|
|
|
|
verticalAlign: "center", // top, center, bottom
|
|
|
|
},
|
|
|
|
axisX: {
|
|
|
|
title: "Time (in $tzone)",
|
|
|
|
labelFontSize: 12,
|
|
|
|
labelAngle: -45,
|
|
|
|
valueFormatString: "MM/DD HH:mm",
|
|
|
|
},
|
|
|
|
axisY: {
|
|
|
|
title: "dBm",
|
|
|
|
interlacedColor: "#F0F8FF",
|
|
|
|
},
|
|
|
|
toolTip: {
|
|
|
|
contentFormatter: function (e) {
|
|
|
|
var content = " ";
|
|
|
|
content += "At " + e.entries[0].dataPoint.timestamp.substring(11, 19) + "<br />";
|
|
|
|
if (e.entries[0].dataPoint.signal_dbm) {
|
|
|
|
content += "Signal: " + e.entries[0].dataPoint.signal_dbm + "dBm<br/>";
|
|
|
|
} else {
|
|
|
|
content += "Signal: 0<br/>";
|
|
|
|
}
|
|
|
|
content += "Noise: " + e.entries[0].dataPoint.noise_dbm + "dBm<br/>";
|
|
|
|
content += "SNR: " + e.entries[0].dataPoint.snr + "dB<br/>";
|
|
|
|
content += "TX Rate: " + e.entries[0].dataPoint.tx_rate_mbps + "Mbps<br/>";
|
|
|
|
content += "TX MCS: " + e.entries[0].dataPoint.tx_rate_mcs_index + "<br/>";
|
|
|
|
content += "RX Rate: " + e.entries[0].dataPoint.rx_rate_mbps + "Mbps<br/>";
|
|
|
|
content += "RX MCS: " + e.entries[0].dataPoint.rx_rate_mcs_index + "<br/>";
|
|
|
|
return content;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
data: [
|
|
|
|
{
|
|
|
|
type: "rangeArea",
|
|
|
|
xValueType: "dateTime",
|
|
|
|
showInLegend: true,
|
|
|
|
legendText: "Signal",
|
|
|
|
dataPoints: dps[0]
|
|
|
|
}
|
|
|
|
],
|
|
|
|
}); // --- chart
|
|
|
|
|
|
|
|
var formatDataPoint = function(point) {
|
|
|
|
point.label = point.timestamp.substring(5, 10) + " " + point.timestamp.substring(11, 19);
|
|
|
|
point.y = [point.signal_dbm, point.noise_dbm];
|
|
|
|
point.snr = (point.noise_dbm * -1) - (point.signal_dbm * -1);
|
|
|
|
point.tx_rate_mcs_index = point.tx_rate_mcs_index ? point.tx_rate_mcs_index : "N/A";
|
|
|
|
point.rx_rate_mcs_index = point.rx_rate_mcs_index ? point.rx_rate_mcs_index : "N/A";
|
|
|
|
return point;
|
|
|
|
};
|
|
|
|
|
|
|
|
var updateArchiveChart = function () {
|
|
|
|
$.getJSON("/cgi-bin/api?chart=archive&device=$parms{device}", function (result) {
|
|
|
|
var points = result.pages.chart.archive.map(formatDataPoint);
|
|
|
|
chart.options.data[0].dataPoints = points;
|
|
|
|
if(result.pages.chart.archive.constructor === Array) {
|
|
|
|
chart.render();
|
|
|
|
};
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
var updateRealtimeChart = function () {
|
|
|
|
$.getJSON("/cgi-bin/api?chart=realtime&realtime=1&device=$parms{device}", function (result) {
|
|
|
|
var point = formatDataPoint(result.pages.chart.realtime[0]);
|
|
|
|
dps[0].push(point);
|
|
|
|
chart.render();
|
|
|
|
toneFreq(point.snr);
|
|
|
|
$('#snr').html(point.snr);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
var dmode = getUrlVars()["realtime"];
|
|
|
|
|
|
|
|
if(dmode) {
|
|
|
|
updateRealtimeChart();
|
|
|
|
setInterval(function() {updateRealtimeChart()}, 1000);
|
|
|
|
} else {
|
|
|
|
updateArchiveChart();
|
|
|
|
setInterval(function() {updateArchiveChart()}, 60000);
|
|
|
|
}
|
|
|
|
chart.render();
|
|
|
|
}); // --- document.ready
|
|
|
|
|
|
|
|
var audioCtx = new(window.AudioContext || window.webkitAudioContext)();
|
|
|
|
var oscillator = audioCtx.createOscillator();
|
|
|
|
var gainNode = audioCtx.createGain();
|
2019-01-28 22:39:51 -07:00
|
|
|
oscillator.connect(gainNode);
|
|
|
|
oscillator.type = 'sine';
|
|
|
|
gainNode.connect(audioCtx.destination);
|
2022-01-19 06:42:31 -07:00
|
|
|
gainNode.gain.value = 1;
|
|
|
|
|
|
|
|
function toneFreq(snr) {
|
|
|
|
var p = document.getElementById("tonePitch").value;
|
|
|
|
oscillator.frequency.value = snr * p;
|
|
|
|
var v = document.getElementById("toneVol").value;
|
|
|
|
gainNode.gain.value = v;
|
|
|
|
}
|
|
|
|
|
|
|
|
function toneOn() {
|
|
|
|
oscillator = audioCtx.createOscillator();
|
|
|
|
gainNode = audioCtx.createGain();
|
|
|
|
oscillator.connect(gainNode);
|
|
|
|
oscillator.type = 'sine';
|
|
|
|
gainNode.connect(audioCtx.destination);
|
|
|
|
gainNode.gain.value = .5;
|
|
|
|
oscillator.start();
|
|
|
|
document.getElementById("toneOff").disabled = false;
|
|
|
|
document.getElementById("toneOn").disabled = true;
|
|
|
|
};
|
|
|
|
|
|
|
|
function toneOff() {
|
|
|
|
document.getElementById("toneOff").disabled = true;
|
|
|
|
document.getElementById("toneOn").disabled = false;
|
|
|
|
oscillator.stop();
|
|
|
|
};
|
|
|
|
|
|
|
|
</script>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<div class="overlay" id="content-overlay"></div>
|
|
|
|
<div id="content-loading-spinner-wrapper">
|
|
|
|
<div id="content-loading-spinner">
|
|
|
|
<div class="spinner"></div>
|
|
|
|
<div class="loading-text">
|
|
|
|
Loading . . .
|
|
|
|
</div>
|
2016-01-22 15:32:33 -07:00
|
|
|
</div>
|
|
|
|
</div>
|
2022-01-19 06:42:31 -07:00
|
|
|
<center>
|
|
|
|
<div class="TopBanner">
|
|
|
|
<div class="LogoDiv"><img src="/AREDN.png" class="AREDNLogo"></img></div>
|
|
|
|
</div>
|
|
|
|
<h1><big>$node</big></h1><hr>
|
|
|
|
<nobr>
|
|
|
|
<button onclick="window.location.href='/cgi-bin/signal'">Archive</button>
|
|
|
|
<button onclick="window.location.href='/cgi-bin/signal?realtime=1'">Realtime</button>
|
|
|
|
<button onclick="window.location.href='/cgi-bin/status'">Quit</button>
|
|
|
|
<br />
|
|
|
|
<div id="deviceSelector">
|
|
|
|
<form name="deviceSelector" method="GET" action="/cgi-bin/signal">
|
|
|
|
Selected Device: <select name="device" onChange="this.form.submit();">
|
|
|
|
]]
|
|
|
|
html.print(prelude:gsub("$dmode", dmode):gsub("$tzone", tzone):gsub("$parms{device}", device):gsub("$node", node))
|
|
|
|
|
|
|
|
if dmode == "Realtime" and device == "" then
|
|
|
|
html.print("<option selected value='strongest'>Average signal for all connected stations</option>")
|
|
|
|
end
|
|
|
|
|
|
|
|
local first_sel = true
|
|
|
|
for _, logfile in ipairs(snrfiles)
|
|
|
|
do
|
2023-02-11 12:42:03 -07:00
|
|
|
local dmac, dname = logfile:match("^([%dA-F:]+)-(.*)$")
|
2022-01-19 06:42:31 -07:00
|
|
|
if dname == "" then
|
|
|
|
dname = dmac
|
|
|
|
end
|
|
|
|
if device == logfile or (dmode ~= "Realtime" and first_sel) then
|
|
|
|
html.print("<option selected value='" .. logfile .. "'>" .. dname .."</option>")
|
|
|
|
first_sel = false
|
|
|
|
else
|
|
|
|
html.print("<option value='" .. logfile .."'>" .. dname .. "</option>")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
html.print("</select>")
|
|
|
|
if dmode == "Realtime" then
|
|
|
|
html.print("<input type='hidden' name='realtime' value='1'>")
|
|
|
|
html.print("</form></div><div id='snrValues' style='float: left;'>")
|
|
|
|
html.print("<div><h3>SNR: <span id='snr'>0</span>dB</h3></div>")
|
|
|
|
html.print("<div style='float: right;'>Sound: <button id='toneOn' onclick='toneOn();'>On</button> ")
|
|
|
|
html.print("<button id='toneOff' disabled onclick='toneOff();'>Off</button><br /><br />")
|
|
|
|
html.print("Pitch: <input type='range' id='tonePitch' name='tonePitch' min='5' max='100'></input><br /><br />")
|
|
|
|
html.print("Volume: <input type='range' id='toneVol' name='toneVol' min='0' max='10'></input><br /><br />")
|
|
|
|
html.print("</div>")
|
|
|
|
else
|
|
|
|
html.print("</form></div><div id='snrValues' style='float: left;'>")
|
|
|
|
end
|
|
|
|
html.print("</div><div id='chartContainer' style='width: 80%; height: 60%;'></div></center>")
|
|
|
|
html.print("</body></html>")
|