diff options
4 files changed, 437 insertions, 0 deletions
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 index 0000000000..8f01075d0b --- /dev/null +++ b/modules/luci-mod-status/htdocs/luci-static/resources/svg/channel_analysis.svg @@ -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 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 +}); diff --git a/modules/luci-mod-status/root/usr/share/luci/menu.d/luci-mod-status.json b/modules/luci-mod-status/root/usr/share/luci/menu.d/luci-mod-status.json index e8eee643d5..0f066e67ad 100644 --- a/modules/luci-mod-status/root/usr/share/luci/menu.d/luci-mod-status.json +++ b/modules/luci-mod-status/root/usr/share/luci/menu.d/luci-mod-status.json @@ -71,6 +71,19 @@ } }, + "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, diff --git a/modules/luci-mod-status/root/usr/share/rpcd/acl.d/luci-mod-status.json b/modules/luci-mod-status/root/usr/share/rpcd/acl.d/luci-mod-status.json index 05569d7603..4c9067db15 100644 --- a/modules/luci-mod-status/root/usr/share/rpcd/acl.d/luci-mod-status.json +++ b/modules/luci-mod-status/root/usr/share/rpcd/acl.d/luci-mod-status.json @@ -56,6 +56,15 @@ } }, + "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": { |