include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI support for DAWN
-LUCI_DEPENDS:=+luci-base +dawn +luci-compat +luci-lib-json
+LUCI_DEPENDS:=+luci-base +dawn
include ../../luci.mk
--- /dev/null
+'use strict';
+'require baseclass';
+'require rpc';
+
+let callDawnGetNetwork, callDawnGetHearingMap, callHostHints;
+
+callDawnGetNetwork = rpc.declare({
+ object: 'dawn',
+ method: 'get_network',
+ expect: { }
+});
+
+callDawnGetHearingMap = rpc.declare({
+ object: 'dawn',
+ method: 'get_hearing_map',
+ expect: { }
+});
+
+callHostHints = rpc.declare({
+ object: 'luci-rpc',
+ method: 'getHostHints',
+ expect: { }
+});
+
+function getAvailableText(available) {
+ return ( available ? _('Available') : _('Not available') );
+}
+
+function getChannelFromFrequency(freq) {
+ if (freq <= 2400) {
+ return 0;
+ }
+ else if (freq == 2484) {
+ return 14;
+ }
+ else if (freq < 2484) {
+ return (freq - 2407) / 5;
+ }
+ else if (freq >= 4910 && freq <= 4980) {
+ return (freq - 4000) / 5;
+ }
+ else if (freq <= 45000) {
+ return (freq - 5000) / 5;
+ }
+ else if (freq >= 58320 && freq <= 64800) {
+ return (freq - 56160) / 2160;
+ }
+ else {
+ return 0;
+ }
+}
+
+function getFormattedNumber(num, decimals, divider = 1) {
+ return (num/divider).toFixed(decimals);
+}
+
+function getHostnameFromMAC(hosthints, mac) {
+ return ( hosthints[mac] && hosthints[mac].name ? hosthints[mac].name : mac);
+}
+
+return L.Class.extend({
+ callDawnGetNetwork: callDawnGetNetwork,
+ callDawnGetHearingMap: callDawnGetHearingMap,
+ callHostHints: callHostHints,
+ getAvailableText: getAvailableText,
+ getChannelFromFrequency: getChannelFromFrequency,
+ getFormattedNumber: getFormattedNumber,
+ getHostnameFromMAC: getHostnameFromMAC
+});
--- /dev/null
+'use strict';
+'require uci';
+'require view';
+'require dawn.dawn-common as dawn';
+
+return view.extend({
+ handleSaveApply: null,
+ handleSave: null,
+ handleReset: null,
+
+ load: function() {
+ return Promise.all([
+ dawn.callDawnGetHearingMap(),
+ dawn.callHostHints()
+ ]);
+ },
+
+ render: function(data) {
+
+ const dawnHearingMapData = data[0];
+ const hostHintsData = data[1];
+
+ const body = E([
+ E('h2', _('Hearing Map'))
+ ]);
+
+ for (let network in dawnHearingMapData) {
+
+ body.appendChild(
+ E('h3', 'SSID: ' + network)
+ );
+
+ let hearing_map_table = E('table', { 'class': 'table cbi-section-table' }, [
+ E('tr', { 'class': 'tr table-titles' }, [
+ E('th', { 'class': 'th' }, _('Client')),
+ E('th', { 'class': 'th' }, _('Access Point')),
+ E('th', { 'class': 'th' }, _('Frequency')),
+ E('th', { 'class': 'th' }, E('span', { 'data-tooltip': _('High Throughput') }, [ _('HT') ])),
+ E('th', { 'class': 'th' }, E('span', { 'data-tooltip': _('Very High Throughput') }, [ _('VHT') ])),
+ E('th', { 'class': 'th' }, _('Signal')),
+ E('th', { 'class': 'th' }, E('span', { 'data-tooltip': _('Received Channel Power Indication') }, [ _('RCPI') ])),
+ E('th', { 'class': 'th' }, E('span', { 'data-tooltip': _('Received Signal to Noise Indicator') }, [ _('RSNI') ])),
+ E('th', { 'class': 'th' }, _('Channel Utilization')),
+ E('th', { 'class': 'th' }, _('Stations Connected')),
+ E('th', { 'class': 'th' }, _('Score'))
+ ])
+ ]);
+
+ let clients = Object.entries(dawnHearingMapData[network]).map(function(client) {
+
+ return Object.entries(client[1]).map(function(ap) {
+
+ if (ap[1].freq != 0) {
+ return [
+ dawn.getHostnameFromMAC(hostHintsData, client[0]),
+ dawn.getHostnameFromMAC(hostHintsData, ap[0]),
+ dawn.getFormattedNumber(ap[1].freq, 3, 1000) + ' GHz (' + _('Channel') + ': ' + dawn.getChannelFromFrequency(ap[1].freq) + ')',
+ dawn.getAvailableText(ap[1].ht_capabilities && ap[1].ht_support),
+ dawn.getAvailableText(ap[1].vht_capabilities && ap[1].vht_support),
+ ap[1].signal,
+ ap[1].rcpi,
+ ap[1].rsni,
+ dawn.getFormattedNumber(ap[1].channel_utilization, 2, 2.55) + '%',
+ ap[1].num_sta,
+ ap[1].score
+ ]
+ }
+ }).flat()
+
+ });
+
+ cbi_update_table(hearing_map_table, clients, E('em', _('No clients connected.')));
+
+ body.appendChild(hearing_map_table);
+ }
+ return body;
+ }
+});
--- /dev/null
+'use strict';
+'require uci';
+'require view';
+'require dawn.dawn-common as dawn';
+
+return view.extend({
+ handleSaveApply: null,
+ handleSave: null,
+ handleReset: null,
+
+ load: function() {
+ return Promise.all([
+ dawn.callDawnGetNetwork(),
+ dawn.callHostHints()
+ ]);
+ },
+
+ render: function(data) {
+
+ const dawnNetworkData = data[0];
+ const hostHintsData = data[1];
+
+ const body = E([
+ E('h2', _('Network Overview'))
+ ]);
+
+ let client_table = {};
+
+ for (let network in dawnNetworkData) {
+
+ body.appendChild(
+ E('h3', 'SSID: ' + network)
+ );
+
+ let ap_table = E('table', { 'class': 'table cbi-section-table' }, [
+ E('tr', { 'class': 'tr table-titles' }, [
+ E('th', { 'class': 'th left cbi-section-actions' }, _('Access Point')),
+ E('th', { 'class': 'th left cbi-section-actions' }, _('Interface')),
+ E('th', { 'class': 'th left cbi-section-actions' }, _('MAC')),
+ E('th', { 'class': 'th left cbi-section-actions' }, _('Utilization')),
+ E('th', { 'class': 'th left cbi-section-actions' }, _('Frequency')),
+ E('th', { 'class': 'th left cbi-section-actions' }, _('Stations Connected')),
+ E('th', { 'class': 'th left cbi-section-actions' }, E('span', { 'data-tooltip': _('High Throughput') }, [ _('HT') ])),
+ E('th', { 'class': 'th left cbi-section-actions' }, E('span', { 'data-tooltip': _('Very High Throughput') }, [ _('VHT') ])),
+ E('th', { 'class': 'th center cbi-section-actions' }, _('Clients')),
+ ])
+ ]);
+
+ let aps = Object.entries(dawnNetworkData[network]).map(function(ap) {
+ client_table[ap[0]] = E('table', { 'class': 'table cbi-section-table', 'style': 'display: table' }, [
+ E('tr', { 'class': 'tr table-titles' }, [
+ E('th', { 'class': 'th' }, _('Client')),
+ E('th', { 'class': 'th' }, E('span', { 'data-tooltip': _('High Throughput') }, [ _('HT') ])),
+ E('th', { 'class': 'th' }, E('span', { 'data-tooltip': _('Very High Throughput') }, [ _('VHT') ])),
+ E('th', { 'class': 'th' }, _('Signal'))
+ ])
+ ]);
+
+ let clients = [];
+ let clientData = Object.entries(ap[1]);
+ for (let i = 0; i < clientData.length; i++) {
+ if (typeof clientData[i][1] === 'object') {
+ clients.push([
+ dawn.getHostnameFromMAC(hostHintsData ,clientData[i][0]),
+ dawn.getAvailableText(clientData[i][1].ht),
+ dawn.getAvailableText(clientData[i][1].vht),
+ clientData[i][1].signal
+ ]);
+ }
+ }
+
+ cbi_update_table(client_table[ap[0]], clients, E('em', _('No clients connected.')));
+
+ return [
+ ap[1].hostname,
+ ap[1].iface,
+ ap[0],
+ dawn.getFormattedNumber(ap[1].channel_utilization, 2, 2.55) + '%',
+ dawn.getFormattedNumber(ap[1].freq, 3, 1000) + ' GHz (' + _('Channel') + ': ' + dawn.getChannelFromFrequency(ap[1].freq) + ')',
+ ap[1].num_sta,
+ dawn.getAvailableText(ap[1].ht_support),
+ dawn.getAvailableText(ap[1].vht_support),
+ ap[1].num_sta > 0 ? client_table[ap[0]] : E('em', { 'style': 'display: inline' }, _('No clients connected.'))
+ ]
+ });
+
+ cbi_update_table(ap_table, aps, E('em', _('No access points available.')));
+
+ body.appendChild(ap_table);
+ }
+ return body;
+ }
+});
+++ /dev/null
-module("luci.controller.dawn", package.seeall)
-
-function index()
- local e = entry({ "admin", "dawn" }, firstchild(), "DAWN", 60)
- e.dependent = false
- e.acl_depends = { "luci-app-dawn" }
-
- entry({ "admin", "dawn", "view_network" }, cbi("dawn/dawn_network"), "View Network Overview", 1)
- entry({ "admin", "dawn", "view_hearing_map" }, cbi("dawn/dawn_hearing_map"), "View Hearing Map", 2)
-end
+++ /dev/null
-m = Map("dawn", "Hearing Map", translate("Hearing Map"))
-m.pageaction = false
-
-s = m:section(NamedSection, "__hearingmap__")
-
-function s.render(self, sid)
- local tpl = require "luci.template"
- tpl.render_string([[
- <%
- local utl = require "luci.util"
- local xml = require "luci.xml"
- local status = require "luci.tools.ieee80211"
- local stat = utl.ubus("dawn", "get_hearing_map", { })
- local name, macs
-
- for name, macs in pairs(stat) do
- %>
- <div class="cbi-section-node">
- <h3>SSID: <%= xml.pcdata(name) %></h3>
- <table class="table" id="dawn_hearing_map">
- <tr class="tr table-titles">
- <th class="th">Client MAC</th>
- <th class="th">AP MAC</th>
- <th class="th">Frequency</th>
- <th class="th">HT Sup</th>
- <th class="th">VHT Sup</th>
- <th class="th">Signal</th>
- <th class="th">RCPI</th>
- <th class="th">RSNI</th>
- <th class="th">Channel Utilization</th>
- <th class="th">Station connect to AP</th>
- <th class="th">Score</th>
- </tr>
- <%
- local mac, data
- for mac, data in pairs(macs) do
-
- local mac2, data2
- local count_loop = 0
- for mac2, data2 in pairs(data) do
- if data2.freq ~= 0 then --prevent empty entry crashes
- %>
- <tr class="tr">
- <td class="td"><%= (count_loop == 0) and mac or "" %></td>
- <td class="td"><%= mac2 %></td>
- <td class="td"><%= "%.3f" %( data2.freq / 1000 ) %> GHz Channel: <%= "%d" %( status.frequency_to_channel(data2.freq) ) %></td>
- <td class="td"><%= (data2.ht_capabilities == true and data2.ht_support == true) and "True" or "False" %></td>
- <td class="td"><%= (data2.vht_capabilities == true and data2.vht_support == true) and "True" or "False" %></td>
- <td class="td"><%= "%d" % data2.signal %></td>
- <td class="td"><%= "%d" % data2.rcpi %></td>
- <td class="td"><%= "%d" % data2.rsni %></td>
- <td class="td"><%= "%.2f" % (data2.channel_utilization / 2.55) %> %</td>
- <td class="td"><%= "%d" % data2.num_sta %></td>
- <td class="td"><%= "%d" % data2.score %></td>
- </tr>
- <%
- count_loop = count_loop + 1
- end
- end
- end
- %>
- </table>
- </div>
- <%
- end
- %>
- ]])
-end
-
-return m
+++ /dev/null
-m = Map("dawn", "Network Overview", translate("Network Overview"))
-m.pageaction = false
-
-s = m:section(NamedSection, "__networkoverview__")
-
-function s.render(self, sid)
- local tpl = require "luci.template"
- local json = require "luci.json"
- local utl = require "luci.util"
- tpl.render_string([[
- <%
- local status = require "luci.tools.ieee80211"
- local utl = require "luci.util"
- local sys = require "luci.sys"
- local xml = require "luci.xml"
- local hosts = sys.net.host_hints()
- local stat = utl.ubus("dawn", "get_network", { })
- local name, macs
- for name, macs in pairs(stat) do
- %>
- <div class="cbi-section-node">
- <h3>SSID: <%= xml.pcdata(name) %></h3>
- <table class="table" id="network_overview_main">
- <tr class="tr table-titles">
- <th class="th">AP</th>
- <th class="th">Clients</th>
- </tr>
- <%
- local mac, data
- for mac, data in pairs(macs) do
- %>
- <tr class="tr">
- <td class="td" style="vertical-align: top;">
- <table class="table" id="ap-<%= mac %>">
- <tr class="tr table-titles">
- <th class="th">Hostname</th>
- <th class="th">Interface</th>
- <th class="th">MAC</th>
- <th class="th">Utilization</th>
- <th class="th">Frequency</th>
- <th class="th">Stations</th>
- <th class="th">HT Sup</th>
- <th class="th">VHT Sup</th>
- </tr>
- <tr class="tr">
- <td class="td"><%= xml.pcdata(data.hostname) %></td>
- <td class="td"><%= xml.pcdata(data.iface) %></td>
- <td class="td"><%= mac %></td>
- <td class="td"><%= "%.2f" %(data.channel_utilization / 2.55) %> %</td>
- <td class="td"><%= "%.3f" %( data.freq / 1000 ) %> GHz (Channel: <%= "%d" %( status.frequency_to_channel(data.freq) ) %>)</td>
- <td class="td"><%= "%d" % data.num_sta %></td>
- <td class="td"><%= (data.ht_support == true) and "available" or "not available" %></td>
- <td class="td"><%= (data.vht_support == true) and "available" or "not available" %></td>
- </tr>
- </table>
- </td>
- <td class="td" style="vertical-align: top;">
- <table class="table" id="clients-<%= mac %>">
- <tr class="tr table-titles">
- <th class="th">MAC</th>
- <th class="th">HT</th>
- <th class="th">VHT</th>
- <th class="th">Signal</th>
- </tr>
- <%
- local mac2, data2
- for clientmac, clientvals in pairs(data) do
- if (type(clientvals) == "table") then
- %>
- <tr class="tr">
- <td class="td"><%= clientmac %></td>
- <td class="td"><%= (clientvals.ht == true) and "available" or "not available" %></td>
- <td class="td"><%= (clientvals.vht == true) and "available" or "not available" %></td>
- <td class="td"><%= "%d" % clientvals.signal %></td>
- </tr>
- <%
- end
- end
- %>
- </table>
- </td>
- </tr>
- <%
- end
- %>
- </table>
- </div>
- <%
- end
- %>
- ]])
-end
-
-return m
+++ /dev/null
-module("luci.tools.ieee80211", package.seeall)
-
-function frequency_to_channel(freq)
- if (freq <= 2400) then
- return 0;
- elseif (freq == 2484) then
- return 14;
- elseif (freq < 2484) then
- return (freq - 2407) / 5;
- elseif (freq >= 4910 and freq <= 4980) then
- return (freq - 4000) / 5;
- elseif (freq <= 45000) then
- return (freq - 5000) / 5;
- elseif (freq >= 58320 and freq <= 64800) then
- return (freq - 56160) / 2160;
- else
- return 0;
- end
-end
-
--- /dev/null
+{
+ "admin/dawn/": {
+ "title": "DAWN",
+ "order": 60,
+ "action": {
+ "type": "firstchild"
+ },
+ "depends": {
+ "acl": [ "luci-app-dawn" ]
+ }
+ },
+
+ "admin/dawn/network_overview": {
+ "title": "Network Overview",
+ "order": 1,
+ "action": {
+ "type": "view",
+ "path": "dawn/network_overview"
+ }
+ },
+
+ "admin/dawn/hearing_map": {
+ "title": "Hearing Map",
+ "order": 2,
+ "action": {
+ "type": "view",
+ "path": "dawn/hearing_map"
+ }
+ }
+}
"luci-app-dawn": {
"description": "Grant UCI access for luci-app-dawn",
"read": {
- "uci": [ "dawn" ]
+ "uci": [ "dawn" ],
+ "ubus": {
+ "dawn": [ "get_network", "get_hearing_map" ],
+ "luci-rpc": [ "getHostHints" ]
+ }
},
"write": {
"uci": [ "dawn" ]