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 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 |