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 /** @const {!Array<string>} */ | |
14 var VALUE_DATA_TYPES = ['Hexadecimal', 'UTF-8', 'Decimal']; | |
15 | |
16 /** | |
17 * A set of inputs that allow a user to request reads and writes of values. | |
18 * This control allows the value to be displayed in multiple forms | |
19 * as defined by the |VALUE_DATA_TYPES| array. Values must be written | |
20 * in these formats. Read and write capability is controlled by a | |
21 * 'permissions' bitfield provided by the characteristic. | |
22 * @constructor | |
23 */ | |
24 var ValueControl = cr.ui.define('div'); | |
25 | |
26 ValueControl.prototype = { | |
27 __proto__: HTMLDivElement.prototype, | |
28 | |
29 /** | |
30 * Decorates the element as a ValueControl. Creates the layout for the value | |
31 * control by creating a text input, select element, and two buttons for | |
32 * read/write requests. Event handlers are attached and references to these | |
33 * elements are stored for later use. | |
34 * @override | |
35 */ | |
36 decorate: function() { | |
37 this.classList.add('value-control'); | |
38 | |
39 /** @private {!Array<number>} */ | |
40 this.value_ = []; | |
41 /** @private {?string} */ | |
42 this.deviceAddress_ = null; | |
43 /** @private {?string} */ | |
44 this.serviceId_ = null; | |
45 /** @private {!interfaces.BluetoothDevice.CharacteristicInfo} */ | |
46 this.characteristicInfo_ = null; | |
47 | |
48 this.unavailableMessage_ = document.createElement('h3'); | |
49 this.unavailableMessage_.textContent = 'Value cannot be read or written.'; | |
50 | |
51 this.valueInput_ = document.createElement('input'); | |
52 this.valueInput_.addEventListener('change', function() { | |
53 try { | |
54 this.value_ = this.convertValueToArray_(this.valueInput_.value); | |
dpapad
2017/01/24 19:52:30
Why are we converting the input to an array, and t
mbrunson
2017/01/24 23:21:52
Having the array seemed to be a good middle ground
dpapad
2017/01/24 23:54:34
Trying to understand the tradeoffs here, but still
mbrunson
2017/01/25 02:43:36
Ok. I've added Value at the top of this file.
| |
55 } catch (e) { | |
56 Snackbar.show(e.message, SnackbarType.ERROR); | |
57 } | |
58 }.bind(this)); | |
59 | |
60 this.typeSelect_ = document.createElement('select'); | |
61 | |
62 VALUE_DATA_TYPES.forEach(function(type) { | |
63 var option = document.createElement('option'); | |
64 option.value = type; | |
65 option.text = type; | |
66 this.typeSelect_.add(option); | |
67 }, this); | |
68 | |
69 this.typeSelect_.addEventListener('change', this.redraw.bind(this)); | |
70 | |
71 var inputDiv = document.createElement('div'); | |
72 inputDiv.appendChild(this.valueInput_); | |
73 inputDiv.appendChild(this.typeSelect_); | |
74 | |
75 this.readBtn_ = document.createElement('button'); | |
76 this.readBtn_.textContent = 'Read'; | |
77 this.readBtn_.addEventListener('click', this.readValue_.bind(this)); | |
78 | |
79 this.writeBtn_ = document.createElement('button'); | |
80 this.writeBtn_.textContent = 'Write'; | |
81 this.writeBtn_.addEventListener('click', this.writeValue_.bind(this)); | |
82 | |
83 var buttonsDiv = document.createElement('div'); | |
84 buttonsDiv.appendChild(this.readBtn_); | |
85 buttonsDiv.appendChild(this.writeBtn_); | |
86 | |
87 this.appendChild(this.unavailableMessage_); | |
88 this.appendChild(inputDiv); | |
89 this.appendChild(buttonsDiv); | |
90 }, | |
91 | |
92 /** | |
93 * Sets the settings used by the value control and redraws the control to | |
94 * match the read/write settings provided in | |
95 * |characteristicInfo.permissions|. | |
96 * @param {string} deviceAddress | |
97 * @param {string} serviceId | |
98 * @param {!interfaces.BluetoothDevice.CharacteristicInfo} | |
99 * characteristicInfo | |
100 */ | |
101 load: function(deviceAddress, serviceId, characteristicInfo) { | |
102 this.deviceAddress_ = deviceAddress; | |
103 this.serviceId_ = serviceId; | |
104 this.characteristicInfo_ = characteristicInfo; | |
105 | |
106 this.redraw(); | |
107 }, | |
108 | |
109 /** | |
110 * Redraws the value control with updated layout depending on the | |
111 * availability of reads and writes and the current cached value. | |
112 */ | |
113 redraw: function() { | |
114 this.readBtn_.hidden = (this.characteristicInfo_.properties & | |
115 interfaces.BluetoothDevice.Property.READ) === 0; | |
116 this.writeBtn_.hidden = (this.characteristicInfo_.properties & | |
117 interfaces.BluetoothDevice.Property.WRITE) === 0; | |
118 | |
119 var isAvailable = !this.readBtn_.hidden || !this.writeBtn_.hidden; | |
120 this.unavailableMessage_.hidden = isAvailable; | |
121 this.valueInput_.hidden = !isAvailable; | |
122 this.typeSelect_.hidden = !isAvailable; | |
123 | |
124 if (!isAvailable) return; | |
125 | |
126 var type = this.typeSelect_.selectedOptions[0].value; | |
127 | |
128 switch (type) { | |
129 case 'Hexadecimal': | |
dpapad
2017/01/24 19:52:30
We are repeating those string literals multiple ti
mbrunson
2017/01/24 23:21:52
Done.
| |
130 this.valueInput_.value = this.value_.reduce( | |
131 function(result, value, index) { | |
132 var answer = result + ('0' + value.toString(16)).substr(-2); | |
133 if (index === 0) | |
dpapad
2017/01/24 19:52:30
Nit:
return index === 0 ? '0x' + answer : answer;
mbrunson
2017/01/24 23:21:52
Done.
| |
134 return '0x' + answer; | |
135 | |
136 return answer; | |
137 }, ''); | |
138 break; | |
139 | |
140 case 'UTF-8': | |
141 this.valueInput_.value = this.value_.reduce(function(result, value) { | |
142 return result + String.fromCharCode(value); | |
143 }, ''); | |
144 break; | |
145 | |
146 case 'Decimal': | |
147 this.valueInput_.value = this.value_.reduce( | |
148 function(result, value, index) { | |
dpapad
2017/01/24 19:52:30
Nit:
return result + value.toString() +
(index
mbrunson
2017/01/24 23:21:52
Done.
| |
149 if (index === this.value_.length - 1) | |
150 return result + value.toString(); | |
151 else | |
152 return result + value.toString() + '-'; | |
153 }.bind(this), ''); | |
154 break; | |
155 } | |
156 }, | |
157 | |
158 /** | |
159 * Sets the value of the control. | |
160 * @param {!Array<number>} value | |
161 */ | |
162 setValue: function(value) { | |
163 this.value_ = value; | |
164 this.redraw(); | |
165 }, | |
166 | |
167 /** | |
168 * Converts the current value of the text input into an array of numbers | |
169 * using the currently selected format in the type select element. The | |
170 * numbers are created such that they will not exceed 8-bits in length to be | |
171 * compatible with the expected data type of values set in a characteristic. | |
172 * @param {string} value | |
173 * @return {!Array<number>} | |
174 * @private | |
175 */ | |
176 convertValueToArray_: function(value) { | |
177 if (!value) | |
178 return []; | |
179 | |
180 var type = this.typeSelect_.selectedOptions[0].value; | |
dpapad
2017/01/24 19:52:30
Isn't this the same as this.typeSelect_.value?
mbrunson
2017/01/24 23:21:52
Done.
| |
181 switch (type) { | |
182 case 'Hexadecimal': | |
183 if (!value.startsWith('0x')) | |
184 throw new Error('Expected value to start with "0x".'); | |
185 | |
186 var result = []; | |
187 for (var i = 2; i < value.length; i += 2) { | |
188 result.push(parseInt(value.substr(i, 2), 16)); | |
189 } | |
190 return result; | |
191 | |
192 case 'UTF-8': | |
193 return Array.from(value).map(function(char) { | |
194 return char.charCodeAt(0); | |
195 }); | |
196 | |
197 case 'Decimal': | |
198 if (!/^[0-9\-]*$/.test(value)) | |
199 throw new Error('Value can only contain numbers and hyphens.'); | |
200 | |
201 return value.split('-').map(function(val) { return parseInt(val); }); | |
202 } | |
203 }, | |
204 | |
205 /** | |
206 * Called when the read button is pressed. Connects to the device and | |
207 * retrieves the current value of the characteristic in the |service_id| | |
208 * with id |characteristic_id| | |
209 * @private | |
210 */ | |
211 readValue_: function() { | |
212 this.readBtn_.disabled = true; | |
213 | |
214 device_broker.connectToDevice(this.deviceAddress_).then(function(device) { | |
215 return device.readValueForCharacteristic( | |
216 this.serviceId_, this.characteristicInfo_.id); | |
217 }.bind(this)).then(function(response) { | |
218 this.readBtn_.disabled = false; | |
219 var GattResult = interfaces.BluetoothDevice.GattResult; | |
220 | |
221 if (response.result === GattResult.SUCCESS) { | |
222 this.setValue(response.value); | |
223 Snackbar.show( | |
224 this.deviceAddress_ + ': Read succeeded', SnackbarType.SUCCESS); | |
225 return; | |
226 } | |
227 | |
228 // TODO(crbug.com/663394): Replace with more descriptive error | |
229 // messages. | |
230 var errorString = Object.keys(GattResult).find(function(key) { | |
231 return GattResult[key] === response.result; | |
232 }); | |
233 | |
234 Snackbar.show( | |
235 this.deviceAddress_ + ': ' + errorString, SnackbarType.ERROR, | |
236 'Retry', this.readValue_.bind(this)); | |
237 }.bind(this)); | |
238 }, | |
239 | |
240 /** | |
241 * Called when the write button is pressed. Connects to the device and | |
242 * retrieves the current value of the characteristic in the |service_id| | |
243 * with id |characteristic_id| | |
244 * @private | |
245 */ | |
246 writeValue_: function() { | |
247 this.writeBtn_.disabled = true; | |
248 | |
249 device_broker.connectToDevice(this.deviceAddress_).then(function(device) { | |
250 return device.writeValueForCharacteristic( | |
251 this.serviceId_, this.characteristicInfo_.id, this.value_); | |
252 }.bind(this)).then(function(response) { | |
253 this.writeBtn_.disabled = false; | |
254 var GattResult = interfaces.BluetoothDevice.GattResult; | |
255 | |
256 if (response.result === GattResult.SUCCESS) { | |
257 Snackbar.show( | |
258 this.deviceAddress_ + ': Write succeeded', SnackbarType.SUCCESS); | |
259 return; | |
260 } | |
261 | |
262 // TODO(crbug.com/663394): Replace with more descriptive error | |
263 // messages. | |
264 var errorString = Object.keys(GattResult).find(function(key) { | |
265 return GattResult[key] === response.result; | |
266 }); | |
267 | |
268 Snackbar.show( | |
269 this.deviceAddress_ + ': ' + errorString, SnackbarType.ERROR, | |
270 'Retry', this.writeValue_.bind(this)); | |
271 }.bind(this)); | |
272 }, | |
273 } | |
274 | |
275 return { | |
276 ValueControl: ValueControl, | |
277 }; | |
278 }); | |
OLD | NEW |