fa6d26538c5403de51d1039b443cb2d9211df07b
[project/luci.git] / modules / luci-mod-status / htdocs / luci-static / resources / view / status / channel_analysis.js
1 'use strict';
2 'require view';
3 'require poll';
4 'require request';
5 'require network';
6 'require ui';
7 'require rpc';
8 'require tools.prng as random';
9
10 return view.extend({
11 callFrequencyList : rpc.declare({
12 object: 'iwinfo',
13 method: 'freqlist',
14 params: [ 'device' ],
15 expect: { results: [] }
16 }),
17
18 callInfo : rpc.declare({
19 object: 'iwinfo',
20 method: 'info',
21 params: [ 'device' ],
22 expect: { }
23 }),
24
25 render_signal_badge: function(signalPercent, signalValue) {
26 var icon, title, value;
27
28 if (signalPercent < 0)
29 icon = L.resource('icons/signal-none.png');
30 else if (signalPercent == 0)
31 icon = L.resource('icons/signal-0.png');
32 else if (signalPercent < 25)
33 icon = L.resource('icons/signal-0-25.png');
34 else if (signalPercent < 50)
35 icon = L.resource('icons/signal-25-50.png');
36 else if (signalPercent < 75)
37 icon = L.resource('icons/signal-50-75.png');
38 else
39 icon = L.resource('icons/signal-75-100.png');
40
41 value = '%d\xa0%s'.format(signalValue, _('dBm'));
42 title = '%s: %d %s'.format(_('Signal'), signalValue, _('dBm'));
43
44 return E('div', {
45 'class': 'ifacebadge',
46 'title': title,
47 'data-signal': signalValue
48 }, [
49 E('img', { 'src': icon }),
50 value
51 ]);
52 },
53
54 add_wifi_to_graph: function(chan_analysis, res, scanCache, channels, channel_width) {
55 var offset_tbl = chan_analysis.offset_tbl,
56 height = chan_analysis.graph.offsetHeight - 2,
57 step = chan_analysis.col_width,
58 height_diff = (height-(height-(res.signal*-4)));
59
60 if (scanCache[res.bssid].color == null)
61 scanCache[res.bssid].color = random.derive_color(res.bssid);
62
63 if (scanCache[res.bssid].graph == null)
64 scanCache[res.bssid].graph = [];
65
66 channels.forEach(function(channel) {
67 if (scanCache[res.bssid].graph[i] == null) {
68 var group = document.createElementNS('http://www.w3.org/2000/svg', 'g'),
69 line = document.createElementNS('http://www.w3.org/2000/svg', 'polyline'),
70 text = document.createElementNS('http://www.w3.org/2000/svg', 'text'),
71 color = scanCache[res.bssid].color;
72
73 line.setAttribute('style', 'fill:'+color+'4f'+';stroke:'+color+';stroke-width:0.5');
74 text.setAttribute('style', 'fill:'+color+';font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000');
75 text.appendChild(document.createTextNode(res.ssid || res.bssid));
76
77 group.appendChild(line)
78 group.appendChild(text)
79
80 chan_analysis.graph.firstElementChild.appendChild(group);
81 scanCache[res.bssid].graph[i] = { group : group, line : line, text : text };
82 }
83 if (channel_width > 2) {
84 if (!("main" in scanCache[res.bssid].graph[i])) {
85 var main = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
86 main.setAttribute('style', 'fill:url(#GradientVerticalCenteredBlack)');
87 scanCache[res.bssid].graph[i].group.appendChild(main)
88 chan_analysis.graph.firstElementChild.lastElementChild.appendChild(main);
89 scanCache[res.bssid].graph[i]["main"] = main;
90 }
91 var main_offset = offset_tbl[res.channel],
92 points = [
93 (main_offset-(step*(2 )))+','+height,
94 (main_offset-(step*(2-1)))+','+height_diff,
95 (main_offset+(step*(2-1)))+','+height_diff,
96 (main_offset+(step*(2 )))+','+height
97 ];
98 scanCache[res.bssid].graph[i].main.setAttribute('points', points);
99 }
100
101 var chan_offset = offset_tbl[channel],
102 points = [
103 (chan_offset-(step*(channel_width )))+','+height,
104 (chan_offset-(step*(channel_width-1)))+','+height_diff,
105 (chan_offset+(step*(channel_width-1)))+','+height_diff,
106 (chan_offset+(step*(channel_width )))+','+height
107 ];
108
109 scanCache[res.bssid].graph[i].text.setAttribute('x', offset_tbl[res.channel]-step);
110 scanCache[res.bssid].graph[i].text.setAttribute('y', height_diff - 2);
111 scanCache[res.bssid].graph[i].line.setAttribute('points', points);
112 scanCache[res.bssid].graph[i].group.style.zIndex = res.signal*-1;
113 scanCache[res.bssid].graph[i].group.style.opacity = res.stale ? '0.5' : null;
114 })
115 },
116
117 create_channel_graph: function(chan_analysis, freq_tbl, band) {
118 var columns = (band != 2) ? freq_tbl.length * 4 : freq_tbl.length + 3,
119 chan_graph = chan_analysis.graph,
120 G = chan_graph.firstElementChild,
121 step = (chan_graph.offsetWidth - 2) / columns,
122 curr_offset = step;
123
124 function createGraphHLine(graph, pos, width, dash) {
125 var elem = document.createElementNS('http://www.w3.org/2000/svg', 'line');
126 elem.setAttribute('x1', pos);
127 elem.setAttribute('y1', 0);
128 elem.setAttribute('x2', pos);
129 elem.setAttribute('y2', '100%');
130 elem.setAttribute('style', 'stroke:black;stroke-width:'+width+';stroke-dasharray:'+dash);
131 graph.appendChild(elem);
132 }
133
134 function createGraphText(graph, pos, text) {
135 var elem = document.createElementNS('http://www.w3.org/2000/svg', 'text');
136 elem.setAttribute('y', 15);
137 elem.setAttribute('style', 'fill:#eee; font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000');
138 elem.setAttribute('x', pos + 5);
139 elem.appendChild(document.createTextNode(text));
140 graph.appendChild(elem);
141 }
142
143 chan_analysis.col_width = step;
144
145 createGraphHLine(G,curr_offset, 0.1, 1);
146 for (var i=0; i< freq_tbl.length;i++) {
147 var channel = freq_tbl[i]
148 chan_analysis.offset_tbl[channel] = curr_offset+step;
149
150 if (band != 2) {
151 createGraphHLine(G,curr_offset+step, 0.1, 3);
152 if (channel < 100)
153 createGraphText(G,curr_offset-(step/2), channel);
154 else
155 createGraphText(G,curr_offset-step, channel);
156 } else {
157 createGraphHLine(G,curr_offset+step, 0.1, 0);
158 createGraphText(G,curr_offset+step, channel);
159 }
160 curr_offset += step;
161
162 if ((band != 2) && freq_tbl[i+1]) {
163 var next_channel = freq_tbl[i+1];
164 /* Check if we are transitioning to another 5/6Ghz band range */
165 if ((next_channel - channel) == 4) {
166 for (var j=1; j < 4; j++) {
167 chan_analysis.offset_tbl[channel+j] = curr_offset+step;
168 if (j == 2)
169 createGraphHLine(G,curr_offset+step, 0.1, 0);
170 else
171 createGraphHLine(G,curr_offset+step, 0.1, 1);
172 curr_offset += step;
173 }
174 } else {
175 chan_analysis.offset_tbl[channel+1] = curr_offset+step;
176 createGraphHLine(G,curr_offset+step, 0.1, 1);
177 curr_offset += step;
178
179 chan_analysis.offset_tbl[next_channel-2] = curr_offset+step;
180 createGraphHLine(G,curr_offset+step, 0.5, 0);
181 curr_offset += step;
182
183 chan_analysis.offset_tbl[next_channel-1] = curr_offset+step;
184 createGraphHLine(G,curr_offset+step, 0.1, 1);
185 curr_offset += step;
186 }
187 }
188 }
189 createGraphHLine(G,curr_offset+step, 0.1, 1);
190
191 chan_analysis.tab.addEventListener('cbi-tab-active', L.bind(function(ev) {
192 this.active_tab = ev.detail.tab;
193 if (!this.radios[this.active_tab].loadedOnce)
194 poll.start();
195 }, this));
196 },
197
198 handleScanRefresh: function() {
199 if (!this.active_tab)
200 return;
201
202 var radio = this.radios[this.active_tab];
203
204 return Promise.all([
205 radio.dev.getScanList(),
206 this.callInfo(radio.dev.getName())
207 ]).then(L.bind(function(data) {
208 var results = data[0],
209 local_wifi = data[1],
210 table = radio.table,
211 chan_analysis = radio.graph,
212 scanCache = radio.scanCache,
213 band = radio.band;
214
215 var rows = [];
216
217 for (var i = 0; i < results.length; i++) {
218 if (scanCache[results[i].bssid] == null)
219 scanCache[results[i].bssid] = {};
220
221 scanCache[results[i].bssid].data = results[i];
222 scanCache[results[i].bssid].data.stale = false;
223 }
224
225 if (band + 'g' == radio.dev.get('band')) {
226 if (scanCache[local_wifi.bssid] == null)
227 scanCache[local_wifi.bssid] = {};
228
229 scanCache[local_wifi.bssid].data = local_wifi;
230
231 if (chan_analysis.offset_tbl[local_wifi.channel] != null && local_wifi.center_chan1) {
232 var center_channels = [local_wifi.center_chan1],
233 chan_width_text = local_wifi.htmode.replace(/(V)*H[TE]/,''), /* Handle HT VHT HE */
234 chan_width = parseInt(chan_width_text)/10;
235
236 if (local_wifi.center_chan2) {
237 center_channels.push(local_wifi.center_chan2);
238 chan_width = 8;
239 }
240
241 local_wifi.signal = -10;
242 local_wifi.ssid = 'Local Interface';
243
244 this.add_wifi_to_graph(chan_analysis, local_wifi, scanCache, center_channels, chan_width);
245 rows.push([
246 this.render_signal_badge(q, local_wifi.signal),
247 [
248 E('span', { 'style': 'color:'+scanCache[local_wifi.bssid].color }, '⬤ '),
249 local_wifi.ssid
250 ],
251 '%d'.format(local_wifi.channel),
252 '%h MHz'.format(chan_width_text),
253 '%h'.format(local_wifi.mode),
254 '%h'.format(local_wifi.bssid)
255 ]);
256 }
257 }
258
259 for (var k in scanCache)
260 if (scanCache[k].data.stale)
261 results.push(scanCache[k].data);
262
263 results.sort(function(a, b) {
264 if (a.channel - b.channel)
265 return 1;
266
267 if (a.ssid < b.ssid)
268 return -1;
269 else if (a.ssid > b.ssid)
270 return 1;
271
272 if (a.bssid < b.bssid)
273 return -1;
274 else if (a.bssid > b.bssid)
275 return 1;
276 });
277
278 for (var i = 0; i < results.length; i++) {
279 var res = results[i],
280 qv = res.quality || 0,
281 qm = res.quality_max || 0,
282 q = (qv > 0 && qm > 0) ? Math.floor((100 / qm) * qv) : 0,
283 s = res.stale ? 'opacity:0.5' : '',
284 center_channels = [res.channel],
285 chan_width = 2;
286
287 /* Skip WiFi not supported by the current band */
288 if (band != res.band)
289 continue;
290 if (chan_analysis.offset_tbl[res.channel] == null)
291 continue;
292
293 res.channel_width = "20 MHz";
294 if (res.ht_operation != null)
295 if (res.ht_operation.channel_width == 2040) { /* 40 MHz Channel Enabled */
296 if (res.ht_operation.secondary_channel_offset == "below") {
297 res.channel_width = "40 MHz";
298 chan_width = 4; /* 40 MHz Channel Used */
299 center_channels[0] -= 2;
300 } else if (res.ht_operation.secondary_channel_offset == "above") {
301 res.channel_width = "40 MHz";
302 chan_width = 4; /* 40 MHz Channel Used */
303 center_channels[0] += 2;
304 } else {
305 res.channel_width = "20 MHz (40 MHz Intolerant)";
306 }
307 }
308
309 /* if channel_width <= 40, refer to HT (above) for actual channel width,
310 * as vht_operation.channel_width == 40 really only means that the used
311 * bandwidth is <= 40 and could be 20 Mhz as well */
312 if (res.vht_operation != null && res.vht_operation.channel_width > 40) {
313 center_channels[0] = res.vht_operation.center_freq_1;
314 if (res.vht_operation.channel_width == 80) {
315 chan_width = 8;
316 res.channel_width = "80 MHz";
317 } else if (res.vht_operation.channel_width == 8080) {
318 res.channel_width = "80+80 MHz";
319 chan_width = 8;
320 center_channels.push(res.vht_operation.center_freq_2);
321 } else if (res.vht_operation.channel_width == 160) {
322 res.channel_width = "160 MHz";
323 chan_width = 16;
324 }
325 }
326
327 this.add_wifi_to_graph(chan_analysis, res, scanCache, center_channels, chan_width);
328
329 rows.push([
330 E('span', { 'style': s }, this.render_signal_badge(q, res.signal)),
331 E('span', { 'style': s }, [
332 E('span', { 'style': 'color:'+scanCache[results[i].bssid].color }, '⬤ '),
333 (res.ssid != null) ? '%h'.format(res.ssid) : E('em', _('hidden'))
334 ]),
335 E('span', { 'style': s }, '%d'.format(res.channel)),
336 E('span', { 'style': s }, '%h'.format(res.channel_width)),
337 E('span', { 'style': s }, '%h'.format(res.mode)),
338 E('span', { 'style': s }, '%h'.format(res.bssid))
339 ]);
340
341 scanCache[results[i].bssid].data.stale = true;
342 }
343
344 cbi_update_table(table, rows);
345
346 if (!radio.loadedOnce) {
347 radio.loadedOnce = true;
348 poll.stop();
349 }
350 }, this))
351 },
352
353 radios : {},
354
355 loadSVG : function(src) {
356 return request.get(src).then(function(response) {
357 if (!response.ok)
358 throw new Error(response.statusText);
359
360 return E('div', {
361 'id': 'channel_graph',
362 'style': 'width:100%;height:400px;border:1px solid #000;background:#fff'
363 }, E(response.text()));
364 });
365 },
366
367 load: function() {
368 return Promise.all([
369 this.loadSVG(L.resource('svg/channel_analysis.svg')),
370 network.getWifiDevices().then(L.bind(function(data) {
371 var tasks = [], ret = [];
372
373 for (var i = 0; i < data.length; i++) {
374 ret[data[i].getName()] = { dev : data[i] };
375
376 tasks.push(this.callFrequencyList(data[i].getName())
377 .then(L.bind(function(radio, data) {
378 ret[radio.getName()].freq = data;
379 }, this, data[i])));
380 }
381
382 return Promise.all(tasks).then(function() { return ret; })
383 }, this))
384 ]);
385 },
386
387 render: function(data) {
388 var svg = data[0],
389 wifiDevs = data[1];
390
391 var h2 = E('div', {'class' : 'cbi-title-section'}, [
392 E('h2', {'class': 'cbi-title-field'}, [ _('Channel Analysis') ]),
393 E('div', {'class': 'cbi-title-buttons' }, [
394 E('button', {
395 'class': 'cbi-button cbi-button-edit',
396 'click': ui.createHandlerFn(this, 'handleScanRefresh')
397 }, [ _('Refresh Channels') ])])
398 ]);
399
400 var tabs = E('div', {}, E('div'));
401
402 for (var ifname in wifiDevs) {
403 var bands = {
404 [2] : { title: '2.4GHz', channels: [] },
405 [5] : { title: '5GHz', channels: [] },
406 [6] : { title: '6GHz', channels: [] },
407 };
408
409 /* Split FrequencyList in Bands */
410 wifiDevs[ifname].freq.forEach(function(freq) {
411 if (bands[freq.band])
412 bands[freq.band].channels.push(freq.channel);
413 });
414
415 for (var band in bands) {
416 if (bands[band].channels.length == 0)
417 continue;
418
419 var csvg = svg.cloneNode(true),
420 table = E('table', { 'class': 'table' }, [
421 E('tr', { 'class': 'tr table-titles' }, [
422 E('th', { 'class': 'th col-2 middle center' }, _('Signal')),
423 E('th', { 'class': 'th col-4 middle left' }, _('SSID')),
424 E('th', { 'class': 'th col-2 middle center hide-xs' }, _('Channel')),
425 E('th', { 'class': 'th col-3 middle left' }, _('Channel Width')),
426 E('th', { 'class': 'th col-2 middle left hide-xs' }, _('Mode')),
427 E('th', { 'class': 'th col-3 middle left hide-xs' }, _('BSSID'))
428 ])
429 ]),
430 tab = E('div', { 'data-tab': ifname+band, 'data-tab-title': ifname+' ('+bands[band].title+')' },
431 [E('br'),csvg,E('br'),table,E('br')]),
432 graph_data = {
433 graph: csvg,
434 offset_tbl: {},
435 col_width: 0,
436 tab: tab,
437 };
438
439 this.radios[ifname+band] = {
440 dev: wifiDevs[ifname].dev,
441 band: band,
442 graph: graph_data,
443 table: table,
444 scanCache: {},
445 loadedOnce: false,
446 };
447
448 cbi_update_table(table, [], E('em', { class: 'spinning' }, _('Starting wireless scan...')));
449
450 tabs.firstElementChild.appendChild(tab)
451
452 requestAnimationFrame(L.bind(this.create_channel_graph, this, graph_data, bands[band].channels, band));
453 }
454 }
455
456 ui.tabs.initTabGroup(tabs.firstElementChild.childNodes);
457
458 this.pollFn = L.bind(this.handleScanRefresh, this);
459 poll.add(this.pollFn);
460
461 return E('div', {}, [h2, tabs]);
462 },
463
464 handleSaveApply: null,
465 handleSave: null,
466 handleReset: null
467 });