Chromium Code Reviews| 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 |