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