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 /** @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 |