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 ValueDataType = { | |
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 * @param {!Array<number>} newValue | |
44 */ | |
45 setArray: function(newValue) { | |
46 this.value_ = newValue; | |
47 }, | |
48 | |
49 /** | |
50 * Sets the value by converting the |newValue| string using the formatting | |
51 * specified by |valueDataType|. | |
52 * @param {!ValueDataType} valueDataType | |
53 * @param {string} newValue | |
54 */ | |
55 setAs: function(valueDataType, newValue) { | |
56 switch (valueDataType) { | |
57 case ValueDataType.HEXADECIMAL: | |
58 this.setValueFromHex_(newValue); | |
59 break; | |
60 | |
61 case ValueDataType.UTF8: | |
62 this.setValueFromUTF8_(newValue); | |
63 break; | |
64 | |
65 case ValueDataType.DECIMAL: | |
66 this.setValueFromDecimal_(newValue); | |
67 break; | |
68 } | |
69 }, | |
70 | |
71 /** | |
72 * Gets the value as a string representing the given |valueDataType|. | |
73 * @param {!ValueDataType} valueDataType | |
74 * @return {string} | |
75 */ | |
76 getAs: function(valueDataType) { | |
77 switch (valueDataType) { | |
78 case ValueDataType.HEXADECIMAL: | |
79 return this.toHex_(); | |
80 break; | |
dpapad
2017/01/25 23:50:20
Since you are returning in previous line, this bre
mbrunson
2017/01/26 19:16:26
Done.
| |
81 | |
82 case ValueDataType.UTF8: | |
83 return this.toUTF8_(); | |
84 break; | |
85 | |
86 case ValueDataType.DECIMAL: | |
87 return this.toDecimal_(); | |
88 break; | |
89 } | |
90 }, | |
91 | |
92 /** | |
93 * Converts the value to a hex string. | |
94 * @return {string} | |
95 * @private | |
96 */ | |
97 toHex_: function() { | |
98 if (this.value_.length == 0) | |
99 return ''; | |
100 | |
101 return this.value_.reduce(function(result, value, index) { | |
102 return result + ('0' + value.toString(16)).substr(-2); | |
103 }, '0x'); | |
104 }, | |
105 | |
106 /** | |
107 * Sets the value from a hex string. | |
108 * @return {string} | |
109 * @private | |
110 */ | |
111 setValueFromHex_: function(newValue) { | |
112 if (!newValue) { | |
113 this.value_ = []; | |
114 return; | |
115 } | |
116 | |
117 if (!newValue.startsWith('0x')) | |
118 throw new Error('Expected new value to start with "0x".'); | |
119 | |
120 var result = []; | |
121 for (var i = 2; i < newValue.length; i += 2) { | |
122 result.push(parseInt(newValue.substr(i, 2), 16)); | |
123 } | |
124 | |
125 this.value_ = result; | |
126 }, | |
127 | |
128 /** | |
129 * Converts the value to a UTF-8 encoded text string. | |
130 * @return {string} | |
131 * @private | |
132 */ | |
133 toUTF8_: function() { | |
134 return this.value_.reduce(function(result, value) { | |
135 return result + String.fromCharCode(value); | |
136 }, ''); | |
137 }, | |
138 | |
139 /** | |
140 * Sets the value from a UTF-8 encoded text string. | |
141 * @return {string} | |
142 * @private | |
143 */ | |
144 setValueFromUTF8_: function(newValue) { | |
145 if (!newValue) { | |
146 this.value_ = []; | |
147 return; | |
148 } | |
149 | |
150 this.value_ = Array.from(newValue).map(function(char) { | |
151 return char.charCodeAt(0); | |
152 }); | |
153 }, | |
154 | |
155 /** | |
156 * Converts the value to a decimal string with numbers delimited by '-'. | |
157 * @return {string} | |
158 * @private | |
159 */ | |
160 toDecimal_: function() { | |
161 return this.value_.join('-'); | |
162 }, | |
163 | |
164 /** | |
165 * Sets the value from a decimal string delimited by '-'. | |
166 * @return {string} | |
167 * @private | |
168 */ | |
169 setValueFromDecimal_: function(newValue) { | |
170 if (!newValue) { | |
171 this.value_ = []; | |
172 return; | |
173 } | |
174 | |
175 if (!/^[0-9\-]*$/.test(newValue)) | |
176 throw new Error('New value can only contain numbers and hyphens.'); | |
177 | |
178 this.value_ = newValue.split('-').map(function(val) { | |
179 return parseInt(val, 10); | |
180 }); | |
181 }, | |
182 }; | |
183 | |
184 /** | |
185 * A set of inputs that allow a user to request reads and writes of values. | |
186 * This control allows the value to be displayed in multiple forms | |
187 * as defined by the |ValueDataType| array. Values must be written | |
188 * in these formats. Read and write capability is controlled by a | |
189 * 'properties' bitfield provided by the characteristic. | |
190 * @constructor | |
191 */ | |
192 var ValueControl = cr.ui.define('div'); | |
193 | |
194 ValueControl.prototype = { | |
195 __proto__: HTMLDivElement.prototype, | |
196 | |
197 /** | |
198 * Decorates the element as a ValueControl. Creates the layout for the value | |
199 * control by creating a text input, select element, and two buttons for | |
200 * read/write requests. Event handlers are attached and references to these | |
201 * elements are stored for later use. | |
202 * @override | |
203 */ | |
204 decorate: function() { | |
205 this.classList.add('value-control'); | |
206 | |
207 /** @private {!Value} */ | |
208 this.value_ = new Value([]); | |
209 /** @private {?string} */ | |
210 this.deviceAddress_ = null; | |
211 /** @private {?string} */ | |
212 this.serviceId_ = null; | |
213 /** @private {?interfaces.BluetoothDevice.CharacteristicInfo} */ | |
214 this.characteristicInfo_ = null; | |
215 | |
216 this.unavailableMessage_ = document.createElement('h3'); | |
217 this.unavailableMessage_.textContent = 'Value cannot be read or written.'; | |
218 | |
219 this.valueInput_ = document.createElement('input'); | |
220 this.valueInput_.addEventListener('change', function() { | |
221 try { | |
222 this.value_.setAs(this.typeSelect_.value, this.valueInput_.value); | |
223 } catch (e) { | |
224 Snackbar.show(e.message, SnackbarType.ERROR); | |
225 } | |
226 }.bind(this)); | |
227 | |
228 this.typeSelect_ = document.createElement('select'); | |
229 | |
230 Object.keys(ValueDataType).forEach(function(key) { | |
231 var type = ValueDataType[key]; | |
232 var option = document.createElement('option'); | |
233 option.value = type; | |
234 option.text = type; | |
235 this.typeSelect_.add(option); | |
236 }, this); | |
237 | |
238 this.typeSelect_.addEventListener('change', this.redraw.bind(this)); | |
239 | |
240 var inputDiv = document.createElement('div'); | |
241 inputDiv.appendChild(this.valueInput_); | |
242 inputDiv.appendChild(this.typeSelect_); | |
243 | |
244 this.readBtn_ = document.createElement('button'); | |
245 this.readBtn_.textContent = 'Read'; | |
246 this.readBtn_.addEventListener('click', this.readValue_.bind(this)); | |
247 | |
248 this.writeBtn_ = document.createElement('button'); | |
249 this.writeBtn_.textContent = 'Write'; | |
250 this.writeBtn_.addEventListener('click', this.writeValue_.bind(this)); | |
251 | |
252 var buttonsDiv = document.createElement('div'); | |
253 buttonsDiv.appendChild(this.readBtn_); | |
254 buttonsDiv.appendChild(this.writeBtn_); | |
255 | |
256 this.appendChild(this.unavailableMessage_); | |
257 this.appendChild(inputDiv); | |
258 this.appendChild(buttonsDiv); | |
259 }, | |
260 | |
261 /** | |
262 * Sets the settings used by the value control and redraws the control to | |
263 * match the read/write settings provided in | |
264 * |characteristicInfo.properties|. | |
265 * @param {string} deviceAddress | |
266 * @param {string} serviceId | |
267 * @param {!interfaces.BluetoothDevice.CharacteristicInfo} | |
268 * characteristicInfo | |
269 */ | |
270 load: function(deviceAddress, serviceId, characteristicInfo) { | |
271 this.deviceAddress_ = deviceAddress; | |
272 this.serviceId_ = serviceId; | |
273 this.characteristicInfo_ = characteristicInfo; | |
274 | |
275 this.redraw(); | |
276 }, | |
277 | |
278 /** | |
279 * Redraws the value control with updated layout depending on the | |
280 * availability of reads and writes and the current cached value. | |
281 */ | |
282 redraw: function() { | |
283 this.readBtn_.hidden = (this.characteristicInfo_.properties & | |
284 interfaces.BluetoothDevice.Property.READ) === 0; | |
285 this.writeBtn_.hidden = (this.characteristicInfo_.properties & | |
286 interfaces.BluetoothDevice.Property.WRITE) === 0; | |
287 | |
288 var isAvailable = !this.readBtn_.hidden || !this.writeBtn_.hidden; | |
289 this.unavailableMessage_.hidden = isAvailable; | |
290 this.valueInput_.hidden = !isAvailable; | |
291 this.typeSelect_.hidden = !isAvailable; | |
292 | |
293 if (!isAvailable) | |
294 return; | |
295 | |
296 this.valueInput_.value = this.value_.getAs(this.typeSelect_.value); | |
297 }, | |
298 | |
299 /** | |
300 * Sets the value of the control. | |
301 * @param {!Array<number>} value | |
302 */ | |
303 setValue: function(value) { | |
304 this.value_.setArray(value); | |
305 this.redraw(); | |
306 }, | |
307 | |
308 /** | |
309 * Gets an error string describing the given |result| code. | |
310 * @param {!interfaces.BluetoothDevice.GattResult} result | |
311 * @private | |
312 */ | |
313 getErrorString_: function(result) { | |
314 // TODO(crbug.com/663394): Replace with more descriptive error | |
315 // messages. | |
316 var GattResult = interfaces.BluetoothDevice.GattResult; | |
317 return Object.keys(GattResult).find(function(key) { | |
318 return GattResult[key] === result; | |
319 }); | |
320 }, | |
321 | |
322 /** | |
323 * Called when the read button is pressed. Connects to the device and | |
324 * retrieves the current value of the characteristic in the |service_id| | |
325 * with id |characteristic_id| | |
326 * @private | |
327 */ | |
328 readValue_: function() { | |
329 this.readBtn_.disabled = true; | |
330 | |
331 device_broker.connectToDevice(this.deviceAddress_).then(function(device) { | |
332 return device.readValueForCharacteristic( | |
333 this.serviceId_, this.characteristicInfo_.id); | |
334 }.bind(this)).then(function(response) { | |
335 this.readBtn_.disabled = false; | |
336 | |
337 if (response.result === interfaces.BluetoothDevice.GattResult.SUCCESS) { | |
338 this.setValue(response.value); | |
339 Snackbar.show( | |
340 this.deviceAddress_ + ': Read succeeded', SnackbarType.SUCCESS); | |
341 return; | |
342 } | |
343 | |
344 var errorString = this.getErrorString_(response.result); | |
345 Snackbar.show( | |
346 this.deviceAddress_ + ': ' + errorString, SnackbarType.ERROR, | |
347 'Retry', this.readValue_.bind(this)); | |
348 }.bind(this)); | |
349 }, | |
350 | |
351 /** | |
352 * Called when the write button is pressed. Connects to the device and | |
353 * retrieves the current value of the characteristic in the |service_id| | |
354 * with id |characteristic_id| | |
355 * @private | |
356 */ | |
357 writeValue_: function() { | |
358 this.writeBtn_.disabled = true; | |
359 | |
360 device_broker.connectToDevice(this.deviceAddress_).then(function(device) { | |
361 return device.writeValueForCharacteristic( | |
362 this.serviceId_, this.characteristicInfo_.id, | |
363 this.value_.getArray()); | |
364 }.bind(this)).then(function(response) { | |
365 this.writeBtn_.disabled = false; | |
366 | |
367 if (response.result === interfaces.BluetoothDevice.GattResult.SUCCESS) { | |
368 Snackbar.show( | |
369 this.deviceAddress_ + ': Write succeeded', SnackbarType.SUCCESS); | |
370 return; | |
371 } | |
372 | |
373 var errorString = this.getErrorString_(response.result); | |
374 Snackbar.show( | |
375 this.deviceAddress_ + ': ' + errorString, SnackbarType.ERROR, | |
376 'Retry', this.writeValue_.bind(this)); | |
377 }.bind(this)); | |
378 }, | |
379 } | |
380 | |
381 return { | |
382 ValueControl: ValueControl, | |
383 ValueDataType: ValueDataType, | |
384 }; | |
385 }); | |
OLD | NEW |