treewide: import utility classes explicitly
[oweals/luci.git] / modules / luci-mod-status / htdocs / luci-static / resources / view / status / wireless.js
1 'use strict';
2 'require view';
3 'require poll';
4 'require request';
5 'require ui';
6 'require rpc';
7 'require network';
8
9 var callLuciRealtimeStats = rpc.declare({
10         object: 'luci',
11         method: 'getRealtimeStats',
12         params: [ 'mode', 'device' ],
13         expect: { result: [] }
14 });
15
16 var graphPolls = [],
17     pollInterval = 3;
18
19 Math.log2 = Math.log2 || function(x) { return Math.log(x) * Math.LOG2E; };
20
21 return view.extend({
22         load: function() {
23                 return Promise.all([
24                         this.loadSVG(L.resource('wireless.svg')),
25                         this.loadSVG(L.resource('wifirate.svg')),
26                         network.getWifiDevices().then(function(radios) {
27                                 var tasks = [], all_networks = [];
28
29                                 for (var i = 0; i < radios.length; i++)
30                                         tasks.push(radios[i].getWifiNetworks().then(function(networks) {
31                                                 all_networks.push.apply(all_networks, networks);
32                                         }));
33
34                                 return Promise.all(tasks).then(function() {
35                                         return all_networks;
36                                 });
37                         })
38                 ]);
39         },
40
41         updateGraph: function(ifname, svg, lines, cb) {
42                 var G = svg.firstElementChild;
43
44                 var view = document.querySelector('#view');
45
46                 var width  = view.offsetWidth - 2;
47                 var height = 300 - 2;
48                 var step   = 5;
49
50                 var data_wanted = Math.floor(width / step);
51
52                 var data_values = [],
53                     line_elements = [];
54
55                 for (var i = 0; i < lines.length; i++)
56                         if (lines[i] != null)
57                                 data_values.push([]);
58
59                 var info = {
60                         line_current: [],
61                         line_average: [],
62                         line_peak:    []
63                 };
64
65                 /* prefill datasets */
66                 for (var i = 0; i < data_values.length; i++)
67                         for (var j = 0; j < data_wanted; j++)
68                                         data_values[i][j] = 0;
69
70                 /* plot horizontal time interval lines */
71                 for (var i = width % (step * 60); i < width; i += step * 60) {
72                         var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
73                                 line.setAttribute('x1', i);
74                                 line.setAttribute('y1', 0);
75                                 line.setAttribute('x2', i);
76                                 line.setAttribute('y2', '100%');
77                                 line.setAttribute('style', 'stroke:black;stroke-width:0.1');
78
79                         var text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
80                                 text.setAttribute('x', i + 5);
81                                 text.setAttribute('y', 15);
82                                 text.setAttribute('style', 'fill:#eee; font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000');
83                                 text.appendChild(document.createTextNode(Math.round((width - i) / step / 60) + 'm'));
84
85                         G.appendChild(line);
86                         G.appendChild(text);
87                 }
88
89                 info.interval = pollInterval;
90                 info.timeframe = data_wanted / 60;
91
92                 graphPolls.push({
93                         ifname: ifname,
94                         svg:    svg,
95                         lines:  lines,
96                         cb:     cb,
97                         info:   info,
98                         width:  width,
99                         height: height,
100                         step:   step,
101                         values: data_values,
102                         timestamp: 0,
103                         fill: 1
104                 });
105         },
106
107         pollData: function() {
108                 poll.add(L.bind(function() {
109                         var tasks = [];
110
111                         for (var i = 0; i < graphPolls.length; i++) {
112                                 var ctx = graphPolls[i];
113                                 tasks.push(L.resolveDefault(callLuciRealtimeStats('wireless', ctx.ifname), []));
114                         }
115
116                         return Promise.all(tasks).then(L.bind(function(datasets) {
117                                 for (var gi = 0; gi < graphPolls.length; gi++) {
118                                         var ctx = graphPolls[gi],
119                                             data = datasets[gi],
120                                             values = ctx.values,
121                                             lines = ctx.lines,
122                                             info = ctx.info;
123
124                                         var data_scale = 0;
125                                         var data_wanted = Math.floor(ctx.width / ctx.step);
126                                         var last_timestamp = NaN;
127
128                                         for (var i = 0, di = 0; di < lines.length; di++) {
129                                                 if (lines[di] == null)
130                                                         continue;
131
132                                                 var multiply = (lines[di].multiply != null) ? lines[di].multiply : 1,
133                                                     offset = (lines[di].offset != null) ? lines[di].offset : 0;
134
135                                                 for (var j = ctx.timestamp ? 0 : 1; j < data.length; j++) {
136                                                         /* skip overlapping entries */
137                                                         if (data[j][0] <= ctx.timestamp)
138                                                                 continue;
139
140                                                         if (i == 0) {
141                                                                 ctx.fill++;
142                                                                 last_timestamp = data[j][0];
143                                                         }
144
145                                                         info.line_current[i] = data[j][di + 1] * multiply;
146                                                         info.line_current[i] -= Math.min(info.line_current[i], offset);
147                                                         values[i].push(info.line_current[i]);
148                                                 }
149
150                                                 i++;
151                                         }
152
153                                         /* cut off outdated entries */
154                                         ctx.fill = Math.min(ctx.fill, data_wanted);
155
156                                         for (var i = 0; i < values.length; i++) {
157                                                 var len = values[i].length;
158                                                 values[i] = values[i].slice(len - data_wanted, len);
159
160                                                 /* find peaks, averages */
161                                                 info.line_peak[i] = NaN;
162                                                 info.line_average[i] = 0;
163
164                                                 for (var j = 0; j < values[i].length; j++) {
165                                                         info.line_peak[i] = isNaN(info.line_peak[i]) ? values[i][j] : Math.max(info.line_peak[i], values[i][j]);
166                                                         info.line_average[i] += values[i][j];
167                                                 }
168
169                                                 info.line_average[i] = info.line_average[i] / ctx.fill;
170                                         }
171
172                                         info.peak = Math.max.apply(Math, info.line_peak);
173
174                                         /* remember current timestamp, calculate horizontal scale */
175                                         if (!isNaN(last_timestamp))
176                                                 ctx.timestamp = last_timestamp;
177
178                                         var size = Math.floor(Math.log2(info.peak)),
179                                             div = Math.pow(2, size - (size % 10)),
180                                             mult = info.peak / div,
181                                             mult = (mult < 5) ? 2 : ((mult < 50) ? 10 : ((mult < 500) ? 100 : 1000));
182
183                                         info.peak = info.peak + (mult * div) - (info.peak % (mult * div));
184
185                                         data_scale = ctx.height / info.peak;
186
187                                         /* plot data */
188                                         for (var i = 0, di = 0; di < lines.length; di++) {
189                                                 if (lines[di] == null)
190                                                         continue;
191
192                                                 var el = ctx.svg.firstElementChild.getElementById(lines[di].line),
193                                                     pt = '0,' + ctx.height,
194                                                     y = 0;
195
196                                                 if (!el)
197                                                         continue;
198
199                                                 for (var j = 0; j < values[i].length; j++) {
200                                                         var x = j * ctx.step;
201
202                                                         y = ctx.height - Math.floor(values[i][j] * data_scale);
203                                                         //y -= Math.floor(y % (1 / data_scale));
204
205                                                         pt += ' ' + x + ',' + y;
206                                                 }
207
208                                                 pt += ' ' + ctx.width + ',' + y + ' ' + ctx.width + ',' + ctx.height;
209
210                                                 el.setAttribute('points', pt);
211
212                                                 i++;
213                                         }
214
215                                         info.label_25 = 0.25 * info.peak;
216                                         info.label_50 = 0.50 * info.peak;
217                                         info.label_75 = 0.75 * info.peak;
218
219                                         if (typeof(ctx.cb) == 'function')
220                                                 ctx.cb(ctx.svg, info);
221                                 }
222                         }, this));
223                 }, this), pollInterval);
224         },
225
226         loadSVG: function(src) {
227                 return request.get(src).then(function(response) {
228                         if (!response.ok)
229                                 throw new Error(response.statusText);
230
231                         return E('div', {
232                                 'style': 'width:100%;height:300px;border:1px solid #000;background:#fff'
233                         }, E(response.text()));
234                 });
235         },
236
237         render: function(data) {
238                 var svg1 = data[0],
239                     svg2 = data[1],
240                     wifidevs = data[2];
241
242                 var v = E('div', {}, E('div'));
243
244                 for (var i = 0; i < wifidevs.length; i++) {
245                         var ifname = wifidevs[i].getIfname();
246
247                         if (!ifname)
248                                 continue;
249
250                         var csvg1 = svg1.cloneNode(true),
251                             csvg2 = svg2.cloneNode(true);
252
253                         v.firstElementChild.appendChild(E('div', { 'data-tab': ifname, 'data-tab-title': ifname }, [
254                                 csvg1,
255                                 E('div', { 'class': 'right' }, E('small', { 'id': 'scale' }, '-')),
256                                 E('br'),
257
258                                 E('div', { 'class': 'table', 'style': 'width:100%;table-layout:fixed' }, [
259                                         E('div', { 'class': 'tr' }, [
260                                                 E('div', { 'class': 'td right top' }, E('strong', { 'style': 'border-bottom:2px solid blue' }, [ _('Signal:') ])),
261                                                 E('div', { 'class': 'td', 'id': 'rssi_bw_cur' }, [ '0 ' + _('dBm') ]),
262
263                                                 E('div', { 'class': 'td right top' }, E('strong', {}, [ _('Average:') ])),
264                                                 E('div', { 'class': 'td', 'id': 'rssi_bw_avg' }, [ '0 ' + _('dBm') ]),
265
266                                                 E('div', { 'class': 'td right top' }, E('strong', {}, [ _('Peak:') ])),
267                                                 E('div', { 'class': 'td', 'id': 'rssi_bw_peak' }, [ '0 ' + _('dBm') ])
268                                         ]),
269                                         E('div', { 'class': 'tr' }, [
270                                                 E('div', { 'class': 'td right top' }, E('strong', { 'style': 'border-bottom:2px solid red' }, [ _('Noise:') ])),
271                                                 E('div', { 'class': 'td', 'id': 'noise_bw_cur' }, [ '0 ' + _('dBm') ]),
272
273                                                 E('div', { 'class': 'td right top' }, E('strong', {}, [ _('Average:') ])),
274                                                 E('div', { 'class': 'td', 'id': 'noise_bw_avg' }, [ '0 ' + _('dBm') ]),
275
276                                                 E('div', { 'class': 'td right top' }, E('strong', {}, [ _('Peak:') ])),
277                                                 E('div', { 'class': 'td', 'id': 'noise_bw_peak' }, [ '0 ' + _('dBm') ])
278                                         ])
279                                 ]),
280                                 E('br'),
281
282                                 csvg2,
283                                 E('div', { 'class': 'right' }, E('small', { 'id': 'scale2' }, '-')),
284                                 E('br'),
285
286                                 E('div', { 'class': 'table', 'style': 'width:100%;table-layout:fixed' }, [
287                                         E('div', { 'class': 'tr' }, [
288                                                 E('div', { 'class': 'td right top' }, E('strong', { 'style': 'border-bottom:2px solid green' }, [ _('Phy Rate:') ])),
289                                                 E('div', { 'class': 'td', 'id': 'rate_bw_cur' }, [ '0 MBit/s' ]),
290
291                                                 E('div', { 'class': 'td right top' }, E('strong', {}, [ _('Average:') ])),
292                                                 E('div', { 'class': 'td', 'id': 'rate_bw_avg' }, [ '0 MBit/s' ]),
293
294                                                 E('div', { 'class': 'td right top' }, E('strong', {}, [ _('Peak:') ])),
295                                                 E('div', { 'class': 'td', 'id': 'rate_bw_peak' }, [ '0 MBit/s' ])
296                                         ])
297                                 ])
298                         ]));
299
300                         this.updateGraph(ifname, csvg1, [ null, { line: 'rssi', offset: 155 }, { line: 'noise', offset: 155 } ], function(svg, info) {
301                                 var G = svg.firstElementChild, tab = svg.parentNode;
302
303                                 G.getElementById('label_25').firstChild.data = '%d %s'.format(info.label_25 - 100, _('dBm'));
304                                 G.getElementById('label_50').firstChild.data = '%d %s'.format(info.label_50 - 100, _('dBm'));
305                                 G.getElementById('label_75').firstChild.data = '%d %s'.format(info.label_75 - 100, _('dBm'));
306
307                                 tab.querySelector('#scale').firstChild.data = _('(%d minute window, %d second interval)').format(info.timeframe, info.interval);
308
309                                 tab.querySelector('#rssi_bw_cur').firstChild.data = '%d %s'.format(info.line_current[0] - 100, _('dBm'));
310                                 tab.querySelector('#rssi_bw_avg').firstChild.data = '%d %s'.format(info.line_average[0] - 100, _('dBm'));
311                                 tab.querySelector('#rssi_bw_peak').firstChild.data = '%d %s'.format(info.line_peak[0] - 100, _('dBm'));
312
313                                 tab.querySelector('#noise_bw_cur').firstChild.data = '%d %s'.format(info.line_current[1] - 100, _('dBm'));
314                                 tab.querySelector('#noise_bw_avg').firstChild.data = '%d %s'.format(info.line_average[1] - 100, _('dBm'));
315                                 tab.querySelector('#noise_bw_peak').firstChild.data = '%d %s'.format(info.line_peak[1] - 100, _('dBm'));
316                         });
317
318                         this.updateGraph(ifname, csvg2, [ { line: 'rate', multiply: 0.001 } ], function(svg, info) {
319                                 var G = svg.firstElementChild, tab = svg.parentNode;
320
321                                 G.getElementById('label_25').firstChild.data = '%.2f %s'.format(info.label_25, _('MBit/s'));
322                                 G.getElementById('label_50').firstChild.data = '%.2f %s'.format(info.label_50, _('MBit/s'));
323                                 G.getElementById('label_75').firstChild.data = '%.2f %s'.format(info.label_75, _('MBit/s'));
324
325                                 tab.querySelector('#scale2').firstChild.data = _('(%d minute window, %d second interval)').format(info.timeframe, info.interval);
326
327                                 tab.querySelector('#rate_bw_cur').firstChild.data = '%d %s'.format(info.line_current[0], _('Mbit/s'));
328                                 tab.querySelector('#rate_bw_avg').firstChild.data = '%d %s'.format(info.line_average[0], _('Mbit/s'));
329                                 tab.querySelector('#rate_bw_peak').firstChild.data = '%d %s'.format(info.line_peak[0], _('Mbit/s'));
330                         });
331                 }
332
333                 ui.tabs.initTabGroup(v.firstElementChild.childNodes);
334
335                 this.pollData();
336
337                 return v;
338         },
339
340         handleSaveApply: null,
341         handleSave: null,
342         handleReset: null
343 });