8 'require tools.prng as random';
11 callFrequencyList
: rpc
.declare({
15 expect
: { results
: [] }
18 callInfo
: rpc
.declare({
25 render_signal_badge: function(signalPercent
, signalValue
) {
26 var icon
, title
, value
;
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');
39 icon
= L
.resource('icons/signal-75-100.png');
41 value
= '%d\xa0%s'.format(signalValue
, _('dBm'));
42 title
= '%s: %d %s'.format(_('Signal'), signalValue
, _('dBm'));
45 'class': 'ifacebadge',
47 'data-signal': signalValue
49 E('img', { 'src': icon
}),
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)));
60 if (scanCache
[res
.bssid
].color
== null)
61 scanCache
[res
.bssid
].color
= random
.derive_color(res
.bssid
);
63 if (scanCache
[res
.bssid
].graph
== null)
64 scanCache
[res
.bssid
].graph
= [];
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
;
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
));
77 group
.appendChild(line
)
78 group
.appendChild(text
)
80 chan_analysis
.graph
.firstElementChild
.appendChild(group
);
81 scanCache
[res
.bssid
].graph
[i
] = { group
: group
, line
: line
, text
: text
};
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
;
91 var main_offset
= offset_tbl
[res
.channel
],
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
98 scanCache
[res
.bssid
].graph
[i
].main
.setAttribute('points', points
);
101 var chan_offset
= offset_tbl
[channel
],
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
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;
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
,
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
);
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
);
143 chan_analysis
.col_width
= step
;
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
;
151 createGraphHLine(G
,curr_offset
+step
, 0.1, 3);
153 createGraphText(G
,curr_offset
-(step
/2), channel
);
155 createGraphText(G
,curr_offset
-step
, channel
);
157 createGraphHLine(G
,curr_offset
+step
, 0.1, 0);
158 createGraphText(G
,curr_offset
+step
, channel
);
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;
169 createGraphHLine(G,curr_offset+step, 0.1, 0);
171 createGraphHLine(G,curr_offset+step, 0.1, 1);
175 chan_analysis.offset_tbl[channel+1] = curr_offset+step;
176 createGraphHLine(G,curr_offset+step, 0.1, 1);
179 chan_analysis.offset_tbl[next_channel-2] = curr_offset+step;
180 createGraphHLine(G,curr_offset+step, 0.5, 0);
183 chan_analysis.offset_tbl[next_channel-1] = curr_offset+step;
184 createGraphHLine(G,curr_offset+step, 0.1, 1);
189 createGraphHLine(G,curr_offset+step, 0.1, 1);
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)
198 handleScanRefresh: function() {
199 if (!this.active_tab)
202 var radio = this.radios[this.active_tab];
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],
211 chan_analysis = radio.graph,
212 scanCache = radio.scanCache,
217 for (var i = 0; i < results.length; i++) {
218 if (scanCache[results[i].bssid] == null)
219 scanCache[results[i].bssid] = {};
221 scanCache[results[i].bssid].data = results[i];
222 scanCache[results[i].bssid].data.stale = false;
225 if (band + 'g' == radio.dev.get('band')) {
226 if (scanCache[local_wifi.bssid] == null)
227 scanCache[local_wifi.bssid] = {};
229 scanCache[local_wifi.bssid].data = local_wifi;
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;
236 if (local_wifi
.center_chan2
) {
237 center_channels
.push(local_wifi
.center_chan2
);
241 local_wifi
.signal
= -10;
242 local_wifi
.ssid
= 'Local Interface';
244 this.add_wifi_to_graph(chan_analysis
, local_wifi
, scanCache
, center_channels
, chan_width
);
246 this.render_signal_badge(q
, local_wifi
.signal
),
248 E('span', { 'style': 'color:'+scanCache
[local_wifi
.bssid
].color
}, '⬤ '),
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
)
259 for (var k
in scanCache
)
260 if (scanCache
[k
].data
.stale
)
261 results
.push(scanCache
[k
].data
);
263 results
.sort(function(a
, b
) {
264 if (a
.channel
- b
.channel
)
269 else if (a
.ssid
> b
.ssid
)
272 if (a
.bssid
< b
.bssid
)
274 else if (a
.bssid
> b
.bssid
)
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
],
287 /* Skip WiFi not supported by the current band */
288 if (band
!= res
.band
)
290 if (chan_analysis
.offset_tbl
[res
.channel
] == null)
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;
305 res
.channel_width
= "20 MHz (40 MHz Intolerant)";
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) {
316 res
.channel_width
= "80 MHz";
317 } else if (res
.vht_operation
.channel_width
== 8080) {
318 res
.channel_width
= "80+80 MHz";
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";
327 this.add_wifi_to_graph(chan_analysis
, res
, scanCache
, center_channels
, chan_width
);
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'))
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
))
341 scanCache
[results
[i
].bssid
].data
.stale
= true;
344 cbi_update_table(table
, rows
);
346 if (!radio
.loadedOnce
) {
347 radio
.loadedOnce
= true;
355 loadSVG : function(src
) {
356 return request
.get(src
).then(function(response
) {
358 throw new Error(response
.statusText
);
361 'id': 'channel_graph',
362 'style': 'width:100%;height:400px;border:1px solid #000;background:#fff'
363 }, E(response
.text()));
369 this.loadSVG(L
.resource('svg/channel_analysis.svg')),
370 network
.getWifiDevices().then(L
.bind(function(data
) {
371 var tasks
= [], ret
= [];
373 for (var i
= 0; i
< data
.length
; i
++) {
374 ret
[data
[i
].getName()] = { dev
: data
[i
] };
376 tasks
.push(this.callFrequencyList(data
[i
].getName())
377 .then(L
.bind(function(radio
, data
) {
378 ret
[radio
.getName()].freq
= data
;
382 return Promise
.all(tasks
).then(function() { return ret
; })
387 render: function(data
) {
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' }, [
395 'class': 'cbi-button cbi-button-edit',
396 'click': ui
.createHandlerFn(this, 'handleScanRefresh')
397 }, [ _('Refresh Channels') ])])
400 var tabs
= E('div', {}, E('div'));
402 for (var ifname
in wifiDevs
) {
404 [2] : { title
: '2.4GHz', channels
: [] },
405 [5] : { title
: '5GHz', channels
: [] },
406 [6] : { title
: '6GHz', channels
: [] },
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
);
415 for (var band
in bands
) {
416 if (bands
[band
].channels
.length
== 0)
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'))
430 tab
= E('div', { 'data-tab': ifname
+band
, 'data-tab-title': ifname
+' ('+bands
[band
].title
+')' },
431 [E('br'),csvg
,E('br'),table
,E('br')]),
439 this.radios
[ifname
+band
] = {
440 dev
: wifiDevs
[ifname
].dev
,
448 cbi_update_table(table
, [], E('em', { class: 'spinning' }, _('Starting wireless scan...')));
450 tabs
.firstElementChild
.appendChild(tab
)
452 requestAnimationFrame(L
.bind(this.create_channel_graph
, this, graph_data
, bands
[band
].channels
, band
));
456 ui
.tabs
.initTabGroup(tabs
.firstElementChild
.childNodes
);
458 this.pollFn
= L
.bind(this.handleScanRefresh
, this);
459 poll
.add(this.pollFn
);
461 return E('div', {}, [h2
, tabs
]);
464 handleSaveApply
: null,