luci-nlbwmon: i18n sync translations
[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 var chan_offset = offset_tbl[channel],
68 points = [
69 (chan_offset-(step*channel_width))+','+height,
70 (chan_offset-(step*(channel_width-1)))+','+height_diff,
71 (chan_offset+(step*(channel_width-1)))+','+height_diff,
72 (chan_offset+(step*(channel_width)))+','+height
73 ];
74
75 if (scanCache[res.bssid].graph[i] == null) {
76 var group = document.createElementNS('http://www.w3.org/2000/svg', 'g'),
77 line = document.createElementNS('http://www.w3.org/2000/svg', 'polyline'),
78 text = document.createElementNS('http://www.w3.org/2000/svg', 'text'),
79 color = scanCache[res.bssid].color;
80
81 line.setAttribute('style', 'fill:'+color+'4f'+';stroke:'+color+';stroke-width:0.5');
82 text.setAttribute('style', 'fill:'+color+';font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000');
83 text.appendChild(document.createTextNode(res.ssid || res.bssid));
84
85 group.appendChild(line)
86 group.appendChild(text)
87
88 chan_analysis.graph.firstElementChild.appendChild(group);
89 scanCache[res.bssid].graph[i] = { group : group, line : line, text : text };
90 }
91
92 scanCache[res.bssid].graph[i].text.setAttribute('x', chan_offset-step);
93 scanCache[res.bssid].graph[i].text.setAttribute('y', height_diff - 2);
94 scanCache[res.bssid].graph[i].line.setAttribute('points', points);
95 scanCache[res.bssid].graph[i].group.style.zIndex = res.signal*-1;
96 scanCache[res.bssid].graph[i].group.style.opacity = res.stale ? '0.5' : null;
97 })
98 },
99
100 create_channel_graph: function(chan_analysis, freq_tbl, freq) {
101 var is5GHz = freq == '5GHz',
102 columns = is5GHz ? freq_tbl.length * 4 : freq_tbl.length + 3,
103 chan_graph = chan_analysis.graph,
104 G = chan_graph.firstElementChild,
105 step = (chan_graph.offsetWidth - 2) / columns,
106 curr_offset = step;
107
108 function createGraphHLine(graph, pos) {
109 var elem = document.createElementNS('http://www.w3.org/2000/svg', 'line');
110 elem.setAttribute('x1', pos);
111 elem.setAttribute('y1', 0);
112 elem.setAttribute('x2', pos);
113 elem.setAttribute('y2', '100%');
114 elem.setAttribute('style', 'stroke:black;stroke-width:0.1');
115 graph.appendChild(elem);
116 }
117
118 function createGraphText(graph, pos, text) {
119 var elem = document.createElementNS('http://www.w3.org/2000/svg', 'text');
120 elem.setAttribute('y', 15);
121 elem.setAttribute('style', 'fill:#eee; font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000');
122 elem.setAttribute('x', pos + 5);
123 elem.appendChild(document.createTextNode(text));
124 graph.appendChild(elem);
125 }
126
127 chan_analysis.col_width = step;
128
129 createGraphHLine(G,curr_offset);
130 for (var i=0; i< freq_tbl.length;i++) {
131 var channel = freq_tbl[i]
132 chan_analysis.offset_tbl[channel] = curr_offset+step;
133
134 createGraphHLine(G,curr_offset+step);
135 createGraphText(G,curr_offset+step, channel);
136 curr_offset += step;
137
138 if (is5GHz && freq_tbl[i+1]) {
139 var next_channel = freq_tbl[i+1];
140 /* Check if we are transitioning to another 5Ghz band range */
141 if ((next_channel - channel) == 4) {
142 for (var j=1; j < 4; j++) {
143 chan_analysis.offset_tbl[channel+j] = curr_offset+step;
144 createGraphHLine(G,curr_offset+step);
145 curr_offset += step;
146 }
147 } else {
148 chan_analysis.offset_tbl[channel+1] = curr_offset+step;
149 createGraphHLine(G,curr_offset+step);
150 curr_offset += step;
151
152 chan_analysis.offset_tbl[next_channel-2] = curr_offset+step;
153 createGraphHLine(G,curr_offset+step);
154 curr_offset += step;
155
156 chan_analysis.offset_tbl[next_channel-1] = curr_offset+step;
157 createGraphHLine(G,curr_offset+step);
158 curr_offset += step;
159 }
160 }
161 }
162 createGraphHLine(G,curr_offset+step);
163
164 chan_analysis.tab.addEventListener('cbi-tab-active', L.bind(function(ev) {
165 this.active_tab = ev.detail.tab;
166 }, this));
167 },
168
169 handleScanRefresh: function() {
170 if (!this.active_tab)
171 return;
172
173 var radioDev = this.radios[this.active_tab].dev,
174 table = this.radios[this.active_tab].table,
175 chan_analysis = this.radios[this.active_tab].graph,
176 scanCache = this.radios[this.active_tab].scanCache;
177
178 return Promise.all([
179 radioDev.getScanList(),
180 this.callInfo(radioDev.getName())
181 ]).then(L.bind(function(data) {
182 var results = data[0],
183 local_wifi = data[1];
184
185 var rows = [];
186
187 for (var i = 0; i < results.length; i++) {
188 if (scanCache[results[i].bssid] == null)
189 scanCache[results[i].bssid] = {};
190
191 scanCache[results[i].bssid].data = results[i];
192 }
193
194 if (scanCache[local_wifi.bssid] == null)
195 scanCache[local_wifi.bssid] = {};
196
197 scanCache[local_wifi.bssid].data = local_wifi;
198
199 if (chan_analysis.offset_tbl[local_wifi.channel] != null && local_wifi.center_chan1) {
200 var center_channels = [local_wifi.center_chan1],
201 chan_width_text = local_wifi.htmode.replace(/(V)*HT/,''),
202 chan_width = parseInt(chan_width_text)/10;
203
204 if (local_wifi.center_chan2) {
205 center_channels.push(local_wifi.center_chan2);
206 chan_width = 8;
207 }
208
209 local_wifi.signal = -10;
210 local_wifi.ssid = 'Local Interface';
211
212 this.add_wifi_to_graph(chan_analysis, local_wifi, scanCache, center_channels, chan_width);
213 rows.push([
214 this.render_signal_badge(q, local_wifi.signal),
215 [
216 E('span', { 'style': 'color:'+scanCache[local_wifi.bssid].color }, '⬤ '),
217 local_wifi.ssid
218 ],
219 '%d'.format(local_wifi.channel),
220 '%h MHz'.format(chan_width_text),
221 '%h'.format(local_wifi.mode),
222 '%h'.format(local_wifi.bssid)
223 ]);
224 }
225
226 for (var k in scanCache)
227 if (scanCache[k].stale)
228 results.push(scanCache[k].data);
229
230 results.sort(function(a, b) {
231 var diff = (b.quality - a.quality) || (a.channel - b.channel);
232
233 if (diff)
234 return diff;
235
236 if (a.ssid < b.ssid)
237 return -1;
238 else if (a.ssid > b.ssid)
239 return 1;
240
241 if (a.bssid < b.bssid)
242 return -1;
243 else if (a.bssid > b.bssid)
244 return 1;
245 });
246
247 for (var i = 0; i < results.length; i++) {
248 var res = results[i],
249 qv = res.quality || 0,
250 qm = res.quality_max || 0,
251 q = (qv > 0 && qm > 0) ? Math.floor((100 / qm) * qv) : 0,
252 s = res.stale ? 'opacity:0.5' : '',
253 center_channels = [res.channel],
254 chan_width = 2;
255
256 /* Skip WiFi not supported by the current band */
257 if (chan_analysis.offset_tbl[res.channel] == null)
258 continue;
259
260 res.channel_width = "20 MHz";
261 if (res.ht_operation != null)
262 if (res.ht_operation.channel_width == 2040) { /* 40 MHz Channel Enabled */
263 if (res.ht_operation.secondary_channel_offset == "below") {
264 res.channel_width = "40 MHz";
265 chan_width = 4; /* 40 MHz Channel Used */
266 center_channels[0] -= 2;
267 } else if (res.ht_operation.secondary_channel_offset == "above") {
268 res.channel_width = "40 MHz";
269 chan_width = 4; /* 40 MHz Channel Used */
270 center_channels[0] += 2;
271 } else {
272 res.channel_width = "20 MHz (40 MHz Intolerant)";
273 }
274 }
275
276 if (res.vht_operation != null) {
277 center_channels[0] = res.vht_operation.center_freq_1;
278 if (res.vht_operation.channel_width == 80) {
279 chan_width = 8;
280 res.channel_width = "80 MHz";
281 } else if (res.vht_operation.channel_width == 8080) {
282 res.channel_width = "80+80 MHz";
283 chan_width = 8;
284 center_channels.push(res.vht_operation.center_freq_2);
285 } else if (res.vht_operation.channel_width == 160) {
286 res.channel_width = "160 MHz";
287 chan_width = 16;
288 }
289 }
290
291 this.add_wifi_to_graph(chan_analysis, res, scanCache, center_channels, chan_width);
292
293 rows.push([
294 E('span', { 'style': s }, this.render_signal_badge(q, res.signal)),
295 E('span', { 'style': s }, [
296 E('span', { 'style': 'color:'+scanCache[results[i].bssid].color }, '⬤ '),
297 (res.ssid != null) ? '%h'.format(res.ssid) : E('em', _('hidden'))
298 ]),
299 E('span', { 'style': s }, '%d'.format(res.channel)),
300 E('span', { 'style': s }, '%h'.format(res.channel_width)),
301 E('span', { 'style': s }, '%h'.format(res.mode)),
302 E('span', { 'style': s }, '%h'.format(res.bssid))
303 ]);
304
305 res.stale = true;
306 }
307
308 cbi_update_table(table, rows);
309 }, this))
310 },
311
312 radios : {},
313
314 loadSVG : function(src) {
315 return request.get(src).then(function(response) {
316 if (!response.ok)
317 throw new Error(response.statusText);
318
319 return E('div', {
320 'id': 'channel_graph',
321 'style': 'width:100%;height:400px;border:1px solid #000;background:#fff'
322 }, E(response.text()));
323 });
324 },
325
326 load: function() {
327 return Promise.all([
328 this.loadSVG(L.resource('svg/channel_analysis.svg')),
329 network.getWifiDevices().then(L.bind(function(data) {
330 var tasks = [], ret = [];
331
332 for (var i = 0; i < data.length; i++) {
333 ret[data[i].getName()] = { dev : data[i] };
334
335 tasks.push(this.callFrequencyList(data[i].getName())
336 .then(L.bind(function(radio, data) {
337 ret[radio.getName()].freq = data;
338 }, this, data[i])));
339 }
340
341 return Promise.all(tasks).then(function() { return ret; })
342 }, this))
343 ]);
344 },
345
346 render: function(data) {
347 var svg = data[0],
348 wifiDevs = data[1];
349
350 var v = E('div', {}, E('div'));
351
352 for (var ifname in wifiDevs) {
353 var freq_tbl = {
354 ['2.4GHz'] : [],
355 ['5GHz'] : [],
356 };
357
358 /* Split FrequencyList in Bands */
359 wifiDevs[ifname].freq.forEach(function(freq) {
360 if (freq.mhz >= 5000) {
361 freq_tbl['5GHz'].push(freq.channel);
362 } else {
363 freq_tbl['2.4GHz'].push(freq.channel);
364 }
365 });
366
367 for (var freq in freq_tbl) {
368 if (freq_tbl[freq].length == 0)
369 continue;
370
371 var csvg = svg.cloneNode(true),
372 table = E('div', { 'class': 'table' }, [
373 E('div', { 'class': 'tr table-titles' }, [
374 E('div', { 'class': 'th col-2 middle center' }, _('Signal')),
375 E('div', { 'class': 'th col-4 middle left' }, _('SSID')),
376 E('div', { 'class': 'th col-2 middle center hide-xs' }, _('Channel')),
377 E('div', { 'class': 'th col-3 middle left' }, _('Channel Width')),
378 E('div', { 'class': 'th col-2 middle left hide-xs' }, _('Mode')),
379 E('div', { 'class': 'th col-3 middle left hide-xs' }, _('BSSID'))
380 ])
381 ]),
382 tab = E('div', { 'data-tab': ifname+freq, 'data-tab-title': ifname+' ('+freq+')' },
383 [E('br'),csvg,E('br'),table,E('br')]),
384 graph_data = {
385 graph: csvg,
386 offset_tbl: {},
387 col_width: 0,
388 tab: tab,
389 };
390
391 this.radios[ifname+freq] = {
392 dev: wifiDevs[ifname].dev,
393 graph: graph_data,
394 table: table,
395 scanCache: {}
396 };
397
398 cbi_update_table(table, [], E('em', { class: 'spinning' }, _('Starting wireless scan...')));
399
400 v.firstElementChild.appendChild(tab)
401
402 requestAnimationFrame(L.bind(this.create_channel_graph, this, graph_data, freq_tbl[freq], freq));
403 }
404 }
405
406 ui.tabs.initTabGroup(v.firstElementChild.childNodes);
407
408 this.pollFn = L.bind(this.handleScanRefresh, this);
409
410 poll.add(this.pollFn);
411 poll.start();
412
413 return v;
414 },
415
416 handleSaveApply: null,
417 handleSave: null,
418 handleReset: null
419 });