luci-mod-status: add channel analysis support
authorAnsuel Smith <[email protected]>
Sat, 21 Nov 2020 01:01:41 +0000 (02:01 +0100)
committerAnsuel Smith <[email protected]>
Sun, 6 Dec 2020 01:24:05 +0000 (02:24 +0100)
Add channel analysis support. This can be very useful as a user can directly use the webui to check wifi channel utilization without using external tool. This use data already provided by iwinfo.

Fixes: #4572
Signed-off-by: Ansuel Smith <[email protected]>
modules/luci-mod-status/htdocs/luci-static/resources/svg/channel_analysis.svg [new file with mode: 0644]
modules/luci-mod-status/htdocs/luci-static/resources/view/status/channel_analysis.js [new file with mode: 0644]
modules/luci-mod-status/root/usr/share/luci/menu.d/luci-mod-status.json
modules/luci-mod-status/root/usr/share/rpcd/acl.d/luci-mod-status.json

diff --git a/modules/luci-mod-status/htdocs/luci-static/resources/svg/channel_analysis.svg b/modules/luci-mod-status/htdocs/luci-static/resources/svg/channel_analysis.svg
new file mode 100644 (file)
index 0000000..8f01075
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+
+<svg width="100%" height="100%" version="1.1" xmlns="http://www.w3.org/2000/svg">
+       <polyline id="rx" points="" style="fill:blue;fill-opacity:0.4;stroke:blue;stroke-width:1" />
+       <polyline id="tx" points="" style="fill:green;fill-opacity:0.4;stroke:green;stroke-width:1" />
+
+       <line x1="0" y1="25%" x2="100%" y2="25%" style="stroke:black;stroke-width:0.1" />
+       <text id="label_75" x="10" y="24%" style="fill:#eee; font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000">-25 dbm</text>
+
+       <line x1="0" y1="50%" x2="100%" y2="50%" style="stroke:black;stroke-width:0.1" />
+       <text id="label_50" x="10" y="49%" style="fill:#eee; font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000">-50 dbm</text>
+
+       <line x1="0" y1="75%" x2="100%" y2="75%" style="stroke:black;stroke-width:0.1" />
+       <text id="label_25" x="10" y="74%" style="fill:#eee; font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000">-75 dbm</text>
+
+       <line x1="0" y1="90%" x2="100%" y2="90%" style="stroke:black;stroke-width:0.1" />
+       <text id="label_10" x="10" y="89%" style="fill:#eee; font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000">-90 dbm</text>
+</svg>
diff --git a/modules/luci-mod-status/htdocs/luci-static/resources/view/status/channel_analysis.js b/modules/luci-mod-status/htdocs/luci-static/resources/view/status/channel_analysis.js
new file mode 100644 (file)
index 0000000..b2f32bb
--- /dev/null
@@ -0,0 +1,396 @@
+'use strict';
+'require view';
+'require poll';
+'require request';
+'require network';
+'require ui';
+'require rpc';
+'require tools.prng as random';
+
+return view.extend({
+       callFrequencyList : rpc.declare({
+               object: 'iwinfo',
+               method: 'freqlist',
+               params: [ 'device' ],
+               expect: { results: [] }
+       }),
+
+       callInfo : rpc.declare({
+               object: 'iwinfo',
+               method: 'info',
+               params: [ 'device' ],
+               expect: { }
+       }),
+
+       render_signal_badge: function(signalPercent, signalValue) {
+               var icon, title, value;
+
+               if (signalPercent < 0)
+                       icon = L.resource('icons/signal-none.png');
+               else if (signalPercent == 0)
+                       icon = L.resource('icons/signal-0.png');
+               else if (signalPercent < 25)
+                       icon = L.resource('icons/signal-0-25.png');
+               else if (signalPercent < 50)
+                       icon = L.resource('icons/signal-25-50.png');
+               else if (signalPercent < 75)
+                       icon = L.resource('icons/signal-50-75.png');
+               else
+                       icon = L.resource('icons/signal-75-100.png');
+
+               value = '%d\xa0%s'.format(signalValue, _('dBm'));
+               title = '%s: %d %s'.format(_('Signal'), signalValue, _('dBm'));
+
+               return E('div', {
+                       'class': 'ifacebadge',
+                       'title': title,
+                       'data-signal': signalValue
+               }, [
+                       E('img', { 'src': icon }),
+                       value
+               ]);
+       },
+
+       add_wifi_to_graph: function(chan_analysis, res, scanCache, channels, channel_width) {
+               var offset_tbl = chan_analysis.offset_tbl,
+                       height = chan_analysis.graph.offsetHeight - 2,
+                       step = chan_analysis.col_width,
+                       height_diff = (height-(height-(res.signal*-4)));
+
+               if (scanCache[res.bssid].color == null)
+                       scanCache[res.bssid].color = random.derive_color(res.bssid);
+
+               if (scanCache[res.bssid].graph == null)
+                       scanCache[res.bssid].graph = [];
+
+               for (var i=0; i < channels.length; i++) {
+                       var chan_offset = offset_tbl[channels[i]],
+                               points = [
+                               (chan_offset-(step*channel_width))+','+height,
+                               (chan_offset-(step*(channel_width-1)))+','+height_diff,
+                               (chan_offset+(step*(channel_width-1)))+','+height_diff,
+                               (chan_offset+(step*(channel_width)))+','+height
+                       ];
+
+                       if (scanCache[res.bssid].graph[i] == null) {
+                               var group = document.createElementNS('http://www.w3.org/2000/svg', 'g'),
+                                       line = document.createElementNS('http://www.w3.org/2000/svg', 'polyline'),
+                                       text = document.createElementNS('http://www.w3.org/2000/svg', 'text'),
+                                       color = scanCache[res.bssid].color;
+
+                               line.setAttribute('style', 'fill:'+color+'4f'+';stroke:'+color+';stroke-width:0.5');
+                               text.setAttribute('style', 'fill:'+color+';font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000');
+                               text.appendChild(document.createTextNode(res.ssid || res.bssid));
+
+                               group.appendChild(line)
+                               group.appendChild(text)
+
+                               chan_analysis.graph.firstElementChild.appendChild(group);
+                               scanCache[res.bssid].graph[i] = { group : group, line : line, text : text };
+                       }
+
+                       scanCache[res.bssid].graph[i].text.setAttribute('x', chan_offset-step);
+                       scanCache[res.bssid].graph[i].text.setAttribute('y', height_diff - 2);
+                       scanCache[res.bssid].graph[i].line.setAttribute('points', points);
+                       scanCache[res.bssid].graph[i].group.style.zIndex = res.signal*-1;
+                       scanCache[res.bssid].graph[i].group.style.opacity = res.stale ? '0.5' : null;
+               }
+       },
+
+       create_channel_graph: function(chan_analysis, freq_tbl, is5GHz) {
+               var columns = is5GHz ? freq_tbl.length * 4 : freq_tbl.length + 3,
+                   chan_graph = chan_analysis.graph,
+                   G = chan_graph.firstElementChild,
+                   step = (chan_graph.offsetWidth - 2) / columns,
+                   curr_offset = step;
+
+               function createGraphHLine(graph, pos) {
+                       var elem = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+                       elem.setAttribute('x1', pos);
+                       elem.setAttribute('y1', 0);
+                       elem.setAttribute('x2', pos);
+                       elem.setAttribute('y2', '100%');
+                       elem.setAttribute('style', 'stroke:black;stroke-width:0.1');
+                       graph.appendChild(elem);
+               }
+
+               function createGraphText(graph, pos, text) {
+                       var elem = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+                       elem.setAttribute('y', 15);
+                       elem.setAttribute('style', 'fill:#eee; font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000');
+                       elem.setAttribute('x', pos + 5);
+                       elem.appendChild(document.createTextNode(text));
+                       graph.appendChild(elem);
+               }
+
+               chan_analysis.col_width = step;
+
+               createGraphHLine(G,curr_offset);
+               for (var i=0; i< freq_tbl.length;i++) {
+                       var channel = freq_tbl[i].channel
+                       chan_analysis.offset_tbl[channel] = curr_offset+step;
+
+                       createGraphHLine(G,curr_offset+step);
+                       createGraphText(G,curr_offset+step, channel);
+                       curr_offset += step;
+
+                       if (is5GHz && freq_tbl[i+1]) {
+                               var next_channel = freq_tbl[i+1].channel;
+                               /* Check if we are transitioning to another 5Ghz band range */
+                               if ((next_channel - channel) == 4) {
+                                       for (var j=1; j < 4; j++) {
+                                               chan_analysis.offset_tbl[channel+j] = curr_offset+step;
+                                               createGraphHLine(G,curr_offset+step);
+                                               curr_offset += step;
+                                       }
+                               } else {
+                                       chan_analysis.offset_tbl[channel+1] = curr_offset+step;
+                                       createGraphHLine(G,curr_offset+step);
+                                       curr_offset += step;
+
+                                       chan_analysis.offset_tbl[next_channel-2] = curr_offset+step;
+                                       createGraphHLine(G,curr_offset+step);
+                                       curr_offset += step;
+
+                                       chan_analysis.offset_tbl[next_channel-1] = curr_offset+step;
+                                       createGraphHLine(G,curr_offset+step);
+                                       curr_offset += step;
+                               }
+                       }
+               }
+               createGraphHLine(G,curr_offset+step);
+
+               chan_analysis.tab.addEventListener('cbi-tab-active', L.bind(function(ev) {
+                       this.active_tab = ev.detail.tab;
+               }, this));
+       },
+
+       handleScanRefresh: function() {
+               if (!this.active_tab)
+                       return;
+
+               var radioDev = this.radios[this.active_tab].dev,
+                   table = this.radios[this.active_tab].table,
+                   chan_analysis = this.radios[this.active_tab].graph,
+                   scanCache = this.radios[this.active_tab].scanCache;
+
+               return Promise.all([
+                       radioDev.getScanList(),
+                       this.callInfo(radioDev.getName())
+               ]).then(L.bind(function(data) {
+                       var results = data[0],
+                           local_wifi = data[1];
+
+                       var rows = [];
+
+                       for (var i = 0; i < results.length; i++) {
+                               if (scanCache[results[i].bssid] == null)
+                                       scanCache[results[i].bssid] = {};
+
+                               scanCache[results[i].bssid].data = results[i];
+                       }
+
+                       if (scanCache[local_wifi.bssid] == null)
+                               scanCache[local_wifi.bssid] = {};
+
+                       scanCache[local_wifi.bssid].data = local_wifi;
+
+                       var center_channels = [local_wifi.center_chan1],
+                           chan_width_text = local_wifi.htmode.replace(/(V)*HT/,''),
+                           chan_width;
+
+                       if (local_wifi.center_chan2) {
+                               center_channels.push(local_wifi.center_chan2);
+                               chan_width = 8;
+                       } else {
+                               chan_width = parseInt(chan_width_text)/10;
+                       }
+
+                       local_wifi.signal = -10;
+                       local_wifi.ssid = 'Local Interface';
+
+                       this.add_wifi_to_graph(chan_analysis, local_wifi, scanCache, center_channels, chan_width);
+                       rows.push([
+                               this.render_signal_badge(q, local_wifi.signal),
+                               [
+                                       E('span', { 'style': 'color:'+scanCache[local_wifi.bssid].color }, '⬤ '),
+                                       local_wifi.ssid
+                               ],
+                               '%d'.format(local_wifi.channel),
+                               '%h MHz'.format(chan_width_text),
+                               '%h'.format(local_wifi.mode),
+                               '%h'.format(local_wifi.bssid)
+                       ]);
+
+                       for (var k in scanCache)
+                               if (scanCache[k].stale)
+                                       results.push(scanCache[k].data);
+
+                       results.sort(function(a, b) {
+                               var diff = (b.quality - a.quality) || (a.channel - b.channel);
+
+                               if (diff)
+                                       return diff;
+
+                               if (a.ssid < b.ssid)
+                                       return -1;
+                               else if (a.ssid > b.ssid)
+                                       return 1;
+
+                               if (a.bssid < b.bssid)
+                                       return -1;
+                               else if (a.bssid > b.bssid)
+                                       return 1;
+                       });
+
+                       for (var i = 0; i < results.length; i++) {
+                               var res = results[i],
+                                       qv = res.quality || 0,
+                                       qm = res.quality_max || 0,
+                                       q = (qv > 0 && qm > 0) ? Math.floor((100 / qm) * qv) : 0,
+                                       s = res.stale ? 'opacity:0.5' : '',
+                                       center_channels = [res.channel],
+                                       chan_width = 2;
+
+                               res.channel_width = "20 MHz";
+                               if (res.ht_operation.channel_width == 2040) { /* 40 MHz Channel Enabled */
+                                       if (res.ht_operation.secondary_channel_offset == "below") {
+                                               res.channel_width = "40 MHz";
+                                               chan_width = 4; /* 40 MHz Channel Used */
+                                               center_channels[0] -= 2;
+                                       } else if (res.ht_operation.secondary_channel_offset == "above") {
+                                               res.channel_width = "40 MHz";
+                                               chan_width = 4; /* 40 MHz Channel Used */
+                                               center_channels[0] += 2;
+                                       } else {
+                                               res.channel_width = "20 MHz (40 MHz Intolerant)";
+                                       }
+                               }
+
+                               if (res.vht_operation != null) {
+                                       center_channels[0] = res.vht_operation.center_freq_1;
+                                       if (res.vht_operation.channel_width == 80) {
+                                               chan_width = 8;
+                                               res.channel_width = "80 MHz";
+                                       } else if (res.vht_operation.channel_width == 8080) {
+                                               res.channel_width = "80+80 MHz";
+                                               chan_width = 8;
+                                               center_channels.push(res.vht_operation.center_freq_2);
+                                       } else if (res.vht_operation.channel_width == 160) {
+                                               res.channel_width = "160 MHz";
+                                               chan_width = 16;
+                                       }
+                               }
+
+                               this.add_wifi_to_graph(chan_analysis, res, scanCache, center_channels, chan_width);
+
+                               rows.push([
+                                       E('span', { 'style': s }, this.render_signal_badge(q, res.signal)),
+                                       E('span', { 'style': s }, [
+                                               E('span', { 'style': 'color:'+scanCache[results[i].bssid].color }, '⬤ '),
+                                               (res.ssid != null) ? '%h'.format(res.ssid) : E('em', _('hidden'))
+                                       ]),
+                                       E('span', { 'style': s }, '%d'.format(res.channel)),
+                                       E('span', { 'style': s }, '%h'.format(res.channel_width)),
+                                       E('span', { 'style': s }, '%h'.format(res.mode)),
+                                       E('span', { 'style': s }, '%h'.format(res.bssid))
+                               ]);
+
+                               res.stale = true;
+                       }
+
+                       cbi_update_table(table, rows);
+               }, this))
+       },
+
+       radios : {},
+
+       loadSVG : function(src) {
+               return request.get(src).then(function(response) {
+                       if (!response.ok)
+                               throw new Error(response.statusText);
+
+                       return E('div', {
+                               'id': 'channel_graph',
+                               'style': 'width:100%;height:400px;border:1px solid #000;background:#fff'
+                       }, E(response.text()));
+               });
+       },
+
+       load: function() {
+               return Promise.all([
+                       this.loadSVG(L.resource('svg/channel_analysis.svg')),
+                       network.getWifiDevices().then(L.bind(function(data) {
+                               var tasks = [], ret = [];
+
+                               for (var i = 0; i < data.length; i++) {
+                                       ret[data[i].getName()] = { dev : data[i] };
+
+                                       tasks.push(this.callFrequencyList(data[i].getName())
+                                       .then(L.bind(function(radio, data) {
+                                               ret[radio.getName()].freq = data;
+                                       }, this, data[i])));
+                               }
+
+                               return Promise.all(tasks).then(function() { return ret; })
+                       }, this))
+               ]);
+       },
+
+       render: function(data) {
+               var svg = data[0],
+                   wifiDevs = data[1];
+
+               var v = E('div', {}, E('div'));
+
+               for (var ifname in wifiDevs) {
+                       var csvg = svg.cloneNode(true),
+                           freq_tbl = wifiDevs[ifname].freq,
+                           is5GHz = freq_tbl[0].mhz >= 5000,
+                           table = E('div', { 'class': 'table' }, [
+                               E('div', { 'class': 'tr table-titles' }, [
+                                       E('div', { 'class': 'th col-2 middle center' }, _('Signal')),
+                                       E('div', { 'class': 'th col-4 middle left' }, _('SSID')),
+                                       E('div', { 'class': 'th col-2 middle center hide-xs' }, _('Channel')),
+                                       E('div', { 'class': 'th col-3 middle left' }, _('Channel Width')),
+                                       E('div', { 'class': 'th col-2 middle left hide-xs' }, _('Mode')),
+                                       E('div', { 'class': 'th col-3 middle left hide-xs' }, _('BSSID'))
+                               ])
+                           ]),
+                           tab = E('div', { 'data-tab': ifname, 'data-tab-title': ifname+' ('+(is5GHz ? '5GHz' : '2.4GHz')+') ' },
+                                       [E('br'),csvg,E('br'),table,E('br')]),
+                           graph_data = {
+                               graph: csvg,
+                               offset_tbl: {},
+                               col_width: 0,
+                               tab: tab,
+                           };
+
+                       this.radios[ifname] = { 
+                               dev: wifiDevs[ifname].dev,
+                               graph: graph_data,
+                               table: table,
+                               scanCache: {}
+                       };
+
+                       cbi_update_table(table, [], E('em', { class: 'spinning' }, _('Starting wireless scan...')));
+
+                       v.firstElementChild.appendChild(tab)
+
+                       requestAnimationFrame(L.bind(this.create_channel_graph, this, graph_data, freq_tbl, is5GHz));
+               }
+
+               ui.tabs.initTabGroup(v.firstElementChild.childNodes);
+
+               this.pollFn = L.bind(this.handleScanRefresh, this);
+
+               poll.add(this.pollFn);
+               poll.start();
+
+               return v;
+       },
+
+       handleSaveApply: null,
+       handleSave: null,
+       handleReset: null
+});
index e8eee643d5eb13a4a88b818875aed384ff78d56c..0f066e67ad15a0ad7af36ed6e6a3cddbbdda9199 100644 (file)
                }
        },
 
+       "admin/status/channel_analysis": {
+               "title": "Channel Analysis",
+               "order": 7,
+               "action": {
+                       "type": "view",
+                       "path": "status/channel_analysis"
+               },
+               "depends": {
+                       "acl": [ "luci-mod-status-channel_analysis" ],
+                       "uci": { "wireless": { "@wifi-device": true } }
+               }
+       },
+
        "admin/status/realtime": {
                "title": "Realtime Graphs",
                "order": 7,
index 05569d76039fb154370af6554359a19593decc23..4c9067db15b90660bb12fe0e97bcc10e186ab40f 100644 (file)
                }
        },
 
+       "luci-mod-status-channel_analysis": {
+               "description": "Grant access to the system route status",
+               "read": {
+                       "ubus": {
+                               "iwinfo": [ "info", "freqlist" ]
+                       }
+               }
+       },
+
        "luci-mod-status-firewall": {
                "description": "Grant access to firewall status",
                "read": {