diff options
author | Ansuel Smith <ansuelsmth@gmail.com> | 2020-11-21 02:01:41 +0100 |
---|---|---|
committer | Ansuel Smith <ansuelsmth@gmail.com> | 2020-12-06 02:24:05 +0100 |
commit | 91a00084575621d786d5cc49b202012d293b3baa (patch) | |
tree | 1b120224ba674de954f49725015c63e82ed1575a /modules/luci-mod-status/htdocs/luci-static/resources/view | |
parent | 84ba852993612701060410e7497c3c8d5f6f9c11 (diff) |
luci-mod-status: add channel analysis support
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 <ansuelsmth@gmail.com>
Diffstat (limited to 'modules/luci-mod-status/htdocs/luci-static/resources/view')
-rw-r--r-- | modules/luci-mod-status/htdocs/luci-static/resources/view/status/channel_analysis.js | 396 |
1 files changed, 396 insertions, 0 deletions
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 index 0000000000..b2f32bb635 --- /dev/null +++ b/modules/luci-mod-status/htdocs/luci-static/resources/view/status/channel_analysis.js @@ -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 +}); |