OLD | NEW |
---|---|
(Empty) | |
1 // Copyright 2017 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 /** | |
6 * Javascript for ValueControl, served from chrome://bluetooth-internals/. | |
7 */ | |
8 | |
9 cr.define('value_control', function() { | |
10 /** @const */ var Snackbar = snackbar.Snackbar; | |
11 /** @const */ var SnackbarType = snackbar.SnackbarType; | |
12 | |
13 /** @enum {string} */ | |
14 var ValueDataTypes = { | |
15 HEXADECIMAL: 'Hexadecimal', | |
16 UTF8: 'UTF-8', | |
17 DECIMAL: 'Decimal', | |
18 }; | |
19 | |
20 /** | |
21 * A container for an array value that needs to be converted to multiple | |
22 * display formats. Internally, the value is stored as an array and converted | |
23 * to the needed display type at runtime. | |
24 * @constructor | |
25 * @param {!Array<number>} initialValue | |
26 */ | |
27 function Value(initialValue) { | |
28 /** @private {!Array<number>} */ | |
29 this.value_ = initialValue; | |
30 } | |
31 | |
32 Value.prototype = { | |
33 /** | |
34 * Gets the backing array value. | |
35 * @return {!Array<number>} | |
36 */ | |
37 getArray: function() { | |
38 return this.value_; | |
39 }, | |
40 | |
41 /** | |
42 * Sets the backing array value. | |
43 * @return {!Array<number>} | |
44 */ | |
45 setArray: function(newValue) { | |
46 this.value_ = newValue; | |
47 }, | |
48 | |
49 /** | |
50 * Converts the value to a hex string. | |
51 * @return {string} | |
52 */ | |
53 toHex: function() { | |
54 return this.value_.reduce(function(result, value, index) { | |
55 var answer = result + ('0' + value.toString(16)).substr(-2); | |
56 return index === 0 ? '0x' + answer : answer; | |
57 }, ''); | |
dpapad
2017/01/25 19:20:06
Can we use the reduce's initial value instead of c
mbrunson
2017/01/25 22:14:44
Done.
| |
58 }, | |
59 | |
60 /** | |
61 * Sets the value from a hex string. | |
62 * @return {string} | |
63 */ | |
64 setValueFromHex: function(newValue) { | |
65 if (!newValue) { | |
66 this.value_ = []; | |
67 return; | |
68 } | |
69 | |
70 if (!newValue.startsWith('0x')) | |
71 throw new Error('Expected new value to start with "0x".'); | |
72 | |
73 var result = []; | |
74 for (var i = 2; i < newValue.length; i += 2) { | |
75 result.push(parseInt(newValue.substr(i, 2), 16)); | |
76 } | |
77 | |
78 this.value_ = result; | |
79 }, | |
80 | |
81 /** | |
82 * Converts the value to a UTF-8 encoded text string. | |
83 * @return {string} | |
84 */ | |
85 toUTF8: function() { | |
86 return this.value_.reduce(function(result, value) { | |
87 return result + String.fromCharCode(value); | |
88 }, ''); | |
89 }, | |
90 | |
91 /** | |
92 * Sets the value from a UTF-8 encoded text string. | |
93 * @return {string} | |
94 */ | |
95 setValueFromUTF8: function(newValue) { | |
96 if (!newValue) { | |
97 this.value_ = []; | |
98 return; | |
99 } | |
100 | |
101 this.value_ = Array.from(newValue).map(function(char) { | |
102 return char.charCodeAt(0); | |
103 }); | |
104 }, | |
105 | |
106 /** | |
107 * Converts the value to a decimal string with numbers delimited by '-'. | |
108 * @return {string} | |
109 */ | |
110 toDecimal: function() { | |
111 return this.value_.reduce(function(result, value, index) { | |
112 return result + value.toString() + | |
113 (index === this.value_.length - 1 ? '' : '-'); | |
114 }.bind(this), ''); | |
115 }, | |
116 | |
117 /** | |
118 * Sets the value from a decimal string delimited by '-'. | |
119 * @return {string} | |
120 */ | |
121 setValueFromDecimal: function(newValue) { | |
122 if (!newValue) { | |
123 this.value_ = []; | |
124 return; | |
125 } | |
126 | |
127 if (!/^[0-9\-]*$/.test(newValue)) | |
128 throw new Error('New value can only contain numbers and hyphens.'); | |
129 | |
130 this.value_ = newValue.split('-').map(function(val) { | |
131 return parseInt(val); | |
dpapad
2017/01/25 19:20:07
Perhaps be more explicit,
return parseInt(val, 10
mbrunson
2017/01/25 22:14:44
Done.
| |
132 }); | |
133 }, | |
134 }; | |
135 | |
136 /** | |
137 * A set of inputs that allow a user to request reads and writes of values. | |
138 * This control allows the value to be displayed in multiple forms | |
139 * as defined by the |ValueDataTypes| array. Values must be written | |
140 * in these formats. Read and write capability is controlled by a | |
141 * 'properties' bitfield provided by the characteristic. | |
142 * @constructor | |
143 */ | |
144 var ValueControl = cr.ui.define('div'); | |
145 | |
146 ValueControl.prototype = { | |
147 __proto__: HTMLDivElement.prototype, | |
148 | |
149 /** | |
150 * Decorates the element as a ValueControl. Creates the layout for the value | |
151 * control by creating a text input, select element, and two buttons for | |
152 * read/write requests. Event handlers are attached and references to these | |
153 * elements are stored for later use. | |
154 * @override | |
155 */ | |
156 decorate: function() { | |
157 this.classList.add('value-control'); | |
158 | |
159 /** @private {!Value} */ | |
160 this.value_ = new Value([]); | |
161 /** @private {?string} */ | |
162 this.deviceAddress_ = null; | |
163 /** @private {?string} */ | |
164 this.serviceId_ = null; | |
165 /** @private {?interfaces.BluetoothDevice.CharacteristicInfo} */ | |
166 this.characteristicInfo_ = null; | |
167 | |
168 this.unavailableMessage_ = document.createElement('h3'); | |
169 this.unavailableMessage_.textContent = 'Value cannot be read or written.'; | |
170 | |
171 this.valueInput_ = document.createElement('input'); | |
172 this.valueInput_.addEventListener('change', function() { | |
173 try { | |
174 switch (this.typeSelect_.value) { | |
dpapad
2017/01/25 19:20:06
Optional nit: Can we move this switch logic within
mbrunson
2017/01/25 22:14:44
Done.
| |
175 case ValueDataTypes.HEXADECIMAL: | |
176 this.value_.setValueFromHex(this.valueInput_.value); | |
177 break; | |
178 | |
179 case ValueDataTypes.UTF8: | |
180 this.value_.setValueFromUTF8(this.valueInput_.value); | |
181 break; | |
182 | |
183 case ValueDataTypes.DECIMAL: | |
184 this.value_.setValueFromDecimal(this.valueInput_.value); | |
185 break; | |
186 } | |
187 } catch (e) { | |
188 Snackbar.show(e.message, SnackbarType.ERROR); | |
189 } | |
190 }.bind(this)); | |
191 | |
192 this.typeSelect_ = document.createElement('select'); | |
193 | |
194 Object.keys(ValueDataTypes).forEach(function(key) { | |
195 var type = ValueDataTypes[key]; | |
196 var option = document.createElement('option'); | |
197 option.value = type; | |
198 option.text = type; | |
199 this.typeSelect_.add(option); | |
200 }, this); | |
201 | |
202 this.typeSelect_.addEventListener('change', this.redraw.bind(this)); | |
203 | |
204 var inputDiv = document.createElement('div'); | |
205 inputDiv.appendChild(this.valueInput_); | |
206 inputDiv.appendChild(this.typeSelect_); | |
207 | |
208 this.readBtn_ = document.createElement('button'); | |
209 this.readBtn_.textContent = 'Read'; | |
210 this.readBtn_.addEventListener('click', this.readValue_.bind(this)); | |
211 | |
212 this.writeBtn_ = document.createElement('button'); | |
213 this.writeBtn_.textContent = 'Write'; | |
214 this.writeBtn_.addEventListener('click', this.writeValue_.bind(this)); | |
215 | |
216 var buttonsDiv = document.createElement('div'); | |
217 buttonsDiv.appendChild(this.readBtn_); | |
218 buttonsDiv.appendChild(this.writeBtn_); | |
219 | |
220 this.appendChild(this.unavailableMessage_); | |
221 this.appendChild(inputDiv); | |
222 this.appendChild(buttonsDiv); | |
223 }, | |
224 | |
225 /** | |
226 * Sets the settings used by the value control and redraws the control to | |
227 * match the read/write settings provided in | |
228 * |characteristicInfo.properties|. | |
229 * @param {string} deviceAddress | |
230 * @param {string} serviceId | |
231 * @param {!interfaces.BluetoothDevice.CharacteristicInfo} | |
232 * characteristicInfo | |
233 */ | |
234 load: function(deviceAddress, serviceId, characteristicInfo) { | |
235 this.deviceAddress_ = deviceAddress; | |
236 this.serviceId_ = serviceId; | |
237 this.characteristicInfo_ = characteristicInfo; | |
238 | |
239 this.redraw(); | |
240 }, | |
241 | |
242 /** | |
243 * Redraws the value control with updated layout depending on the | |
244 * availability of reads and writes and the current cached value. | |
245 */ | |
246 redraw: function() { | |
247 this.readBtn_.hidden = (this.characteristicInfo_.properties & | |
248 interfaces.BluetoothDevice.Property.READ) === 0; | |
249 this.writeBtn_.hidden = (this.characteristicInfo_.properties & | |
250 interfaces.BluetoothDevice.Property.WRITE) === 0; | |
251 | |
252 var isAvailable = !this.readBtn_.hidden || !this.writeBtn_.hidden; | |
253 this.unavailableMessage_.hidden = isAvailable; | |
254 this.valueInput_.hidden = !isAvailable; | |
255 this.typeSelect_.hidden = !isAvailable; | |
256 | |
257 if (!isAvailable) | |
258 return; | |
259 | |
260 var type = this.typeSelect_.selectedOptions[0].value; | |
dpapad
2017/01/25 19:20:06
Equivalent to this.typeSelect_.value
mbrunson
2017/01/25 22:14:44
Done.
| |
261 | |
262 switch (type) { | |
dpapad
2017/01/25 19:20:07
Similar optional nit here. Perhaps move this switc
mbrunson
2017/01/25 22:14:44
Done.
| |
263 case ValueDataTypes.HEXADECIMAL: | |
264 this.valueInput_.value = this.value_.toHex(); | |
265 break; | |
266 | |
267 case ValueDataTypes.UTF8: | |
268 this.valueInput_.value = this.value_.toUTF8(); | |
269 break; | |
270 | |
271 case ValueDataTypes.DECIMAL: | |
272 this.valueInput_.value = this.value_.toDecimal(); | |
273 break; | |
274 } | |
275 }, | |
276 | |
277 /** | |
278 * Sets the value of the control. | |
279 * @param {!Array<number>} value | |
280 */ | |
281 setValue: function(value) { | |
282 this.value_.setArray(value); | |
283 this.redraw(); | |
284 }, | |
285 | |
286 /** | |
287 * Called when the read button is pressed. Connects to the device and | |
288 * retrieves the current value of the characteristic in the |service_id| | |
289 * with id |characteristic_id| | |
290 * @private | |
291 */ | |
292 readValue_: function() { | |
293 this.readBtn_.disabled = true; | |
294 | |
295 device_broker.connectToDevice(this.deviceAddress_).then(function(device) { | |
296 return device.readValueForCharacteristic( | |
297 this.serviceId_, this.characteristicInfo_.id); | |
298 }.bind(this)).then(function(response) { | |
299 this.readBtn_.disabled = false; | |
300 var GattResult = interfaces.BluetoothDevice.GattResult; | |
301 | |
302 if (response.result === GattResult.SUCCESS) { | |
303 this.setValue(response.value); | |
304 Snackbar.show( | |
305 this.deviceAddress_ + ': Read succeeded', SnackbarType.SUCCESS); | |
306 return; | |
307 } | |
308 | |
309 // TODO(crbug.com/663394): Replace with more descriptive error | |
310 // messages. | |
311 var errorString = Object.keys(GattResult).find(function(key) { | |
312 return GattResult[key] === response.result; | |
313 }); | |
314 | |
315 Snackbar.show( | |
316 this.deviceAddress_ + ': ' + errorString, SnackbarType.ERROR, | |
317 'Retry', this.readValue_.bind(this)); | |
318 }.bind(this)); | |
319 }, | |
320 | |
321 /** | |
322 * Called when the write button is pressed. Connects to the device and | |
323 * retrieves the current value of the characteristic in the |service_id| | |
324 * with id |characteristic_id| | |
325 * @private | |
326 */ | |
327 writeValue_: function() { | |
328 this.writeBtn_.disabled = true; | |
329 | |
330 device_broker.connectToDevice(this.deviceAddress_).then(function(device) { | |
331 return device.writeValueForCharacteristic( | |
332 this.serviceId_, this.characteristicInfo_.id, | |
333 this.value_.getArray()); | |
334 }.bind(this)).then(function(response) { | |
335 this.writeBtn_.disabled = false; | |
336 var GattResult = interfaces.BluetoothDevice.GattResult; | |
337 | |
338 if (response.result === GattResult.SUCCESS) { | |
339 Snackbar.show( | |
340 this.deviceAddress_ + ': Write succeeded', SnackbarType.SUCCESS); | |
341 return; | |
342 } | |
343 | |
344 // TODO(crbug.com/663394): Replace with more descriptive error | |
345 // messages. | |
346 var errorString = Object.keys(GattResult).find(function(key) { | |
dpapad
2017/01/25 19:20:07
Can we make a helper getErrorString_(result) and r
mbrunson
2017/01/25 22:14:44
Done.
| |
347 return GattResult[key] === response.result; | |
348 }); | |
349 | |
350 Snackbar.show( | |
351 this.deviceAddress_ + ': ' + errorString, SnackbarType.ERROR, | |
352 'Retry', this.writeValue_.bind(this)); | |
353 }.bind(this)); | |
354 }, | |
355 } | |
356 | |
357 return { | |
358 ValueControl: ValueControl, | |
359 }; | |
360 }); | |
OLD | NEW |