Index: chrome/browser/resources/bluetooth_internals/value_control.js |
diff --git a/chrome/browser/resources/bluetooth_internals/value_control.js b/chrome/browser/resources/bluetooth_internals/value_control.js |
new file mode 100644 |
index 0000000000000000000000000000000000000000..2fccbb8ca667035ac62ed33ce6ca3aa50c156b4a |
--- /dev/null |
+++ b/chrome/browser/resources/bluetooth_internals/value_control.js |
@@ -0,0 +1,382 @@ |
+// Copyright 2017 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+/** |
+ * Javascript for ValueControl, served from chrome://bluetooth-internals/. |
+ */ |
+ |
+cr.define('value_control', function() { |
+ /** @const */ var Snackbar = snackbar.Snackbar; |
+ /** @const */ var SnackbarType = snackbar.SnackbarType; |
+ |
+ /** @enum {string} */ |
+ var ValueDataType = { |
+ HEXADECIMAL: 'Hexadecimal', |
+ UTF8: 'UTF-8', |
+ DECIMAL: 'Decimal', |
+ }; |
+ |
+ /** |
+ * A container for an array value that needs to be converted to multiple |
+ * display formats. Internally, the value is stored as an array and converted |
+ * to the needed display type at runtime. |
+ * @constructor |
+ * @param {!Array<number>} initialValue |
+ */ |
+ function Value(initialValue) { |
+ /** @private {!Array<number>} */ |
+ this.value_ = initialValue; |
+ } |
+ |
+ Value.prototype = { |
+ /** |
+ * Gets the backing array value. |
+ * @return {!Array<number>} |
+ */ |
+ getArray: function() { |
+ return this.value_; |
+ }, |
+ |
+ /** |
+ * Sets the backing array value. |
+ * @param {!Array<number>} newValue |
+ */ |
+ setArray: function(newValue) { |
+ this.value_ = newValue; |
+ }, |
+ |
+ /** |
+ * Sets the value by converting the |newValue| string using the formatting |
+ * specified by |valueDataType|. |
+ * @param {!ValueDataType} valueDataType |
+ * @param {string} newValue |
+ */ |
+ setAs: function(valueDataType, newValue) { |
+ switch (valueDataType) { |
+ case ValueDataType.HEXADECIMAL: |
+ this.setValueFromHex_(newValue); |
+ break; |
+ |
+ case ValueDataType.UTF8: |
+ this.setValueFromUTF8_(newValue); |
+ break; |
+ |
+ case ValueDataType.DECIMAL: |
+ this.setValueFromDecimal_(newValue); |
+ break; |
+ } |
+ }, |
+ |
+ /** |
+ * Gets the value as a string representing the given |valueDataType|. |
+ * @param {!ValueDataType} valueDataType |
+ * @return {string} |
+ */ |
+ getAs: function(valueDataType) { |
+ switch (valueDataType) { |
+ case ValueDataType.HEXADECIMAL: |
+ return this.toHex_(); |
+ |
+ case ValueDataType.UTF8: |
+ return this.toUTF8_(); |
+ |
+ case ValueDataType.DECIMAL: |
+ return this.toDecimal_(); |
+ } |
+ }, |
+ |
+ /** |
+ * Converts the value to a hex string. |
+ * @return {string} |
+ * @private |
+ */ |
+ toHex_: function() { |
+ if (this.value_.length == 0) |
+ return ''; |
+ |
+ return this.value_.reduce(function(result, value, index) { |
+ return result + ('0' + value.toString(16)).substr(-2); |
+ }, '0x'); |
+ }, |
+ |
+ /** |
+ * Sets the value from a hex string. |
+ * @return {string} |
+ * @private |
+ */ |
+ setValueFromHex_: function(newValue) { |
+ if (!newValue) { |
+ this.value_ = []; |
+ return; |
+ } |
+ |
+ if (!newValue.startsWith('0x')) |
+ throw new Error('Expected new value to start with "0x".'); |
+ |
+ var result = []; |
+ for (var i = 2; i < newValue.length; i += 2) { |
+ result.push(parseInt(newValue.substr(i, 2), 16)); |
+ } |
+ |
+ this.value_ = result; |
+ }, |
+ |
+ /** |
+ * Converts the value to a UTF-8 encoded text string. |
+ * @return {string} |
+ * @private |
+ */ |
+ toUTF8_: function() { |
+ return this.value_.reduce(function(result, value) { |
+ return result + String.fromCharCode(value); |
+ }, ''); |
+ }, |
+ |
+ /** |
+ * Sets the value from a UTF-8 encoded text string. |
+ * @return {string} |
+ * @private |
+ */ |
+ setValueFromUTF8_: function(newValue) { |
+ if (!newValue) { |
+ this.value_ = []; |
+ return; |
+ } |
+ |
+ this.value_ = Array.from(newValue).map(function(char) { |
+ return char.charCodeAt(0); |
+ }); |
+ }, |
+ |
+ /** |
+ * Converts the value to a decimal string with numbers delimited by '-'. |
+ * @return {string} |
+ * @private |
+ */ |
+ toDecimal_: function() { |
+ return this.value_.join('-'); |
+ }, |
+ |
+ /** |
+ * Sets the value from a decimal string delimited by '-'. |
+ * @return {string} |
+ * @private |
+ */ |
+ setValueFromDecimal_: function(newValue) { |
+ if (!newValue) { |
+ this.value_ = []; |
+ return; |
+ } |
+ |
+ if (!/^[0-9\-]*$/.test(newValue)) |
+ throw new Error('New value can only contain numbers and hyphens.'); |
+ |
+ this.value_ = newValue.split('-').map(function(val) { |
+ return parseInt(val, 10); |
+ }); |
+ }, |
+ }; |
+ |
+ /** |
+ * A set of inputs that allow a user to request reads and writes of values. |
+ * This control allows the value to be displayed in multiple forms |
+ * as defined by the |ValueDataType| array. Values must be written |
+ * in these formats. Read and write capability is controlled by a |
+ * 'properties' bitfield provided by the characteristic. |
+ * @constructor |
+ */ |
+ var ValueControl = cr.ui.define('div'); |
+ |
+ ValueControl.prototype = { |
+ __proto__: HTMLDivElement.prototype, |
+ |
+ /** |
+ * Decorates the element as a ValueControl. Creates the layout for the value |
+ * control by creating a text input, select element, and two buttons for |
+ * read/write requests. Event handlers are attached and references to these |
+ * elements are stored for later use. |
+ * @override |
+ */ |
+ decorate: function() { |
+ this.classList.add('value-control'); |
+ |
+ /** @private {!Value} */ |
+ this.value_ = new Value([]); |
+ /** @private {?string} */ |
+ this.deviceAddress_ = null; |
+ /** @private {?string} */ |
+ this.serviceId_ = null; |
+ /** @private {?interfaces.BluetoothDevice.CharacteristicInfo} */ |
+ this.characteristicInfo_ = null; |
+ |
+ this.unavailableMessage_ = document.createElement('h3'); |
+ this.unavailableMessage_.textContent = 'Value cannot be read or written.'; |
+ |
+ this.valueInput_ = document.createElement('input'); |
+ this.valueInput_.addEventListener('change', function() { |
+ try { |
+ this.value_.setAs(this.typeSelect_.value, this.valueInput_.value); |
+ } catch (e) { |
+ Snackbar.show(e.message, SnackbarType.ERROR); |
+ } |
+ }.bind(this)); |
+ |
+ this.typeSelect_ = document.createElement('select'); |
+ |
+ Object.keys(ValueDataType).forEach(function(key) { |
+ var type = ValueDataType[key]; |
+ var option = document.createElement('option'); |
+ option.value = type; |
+ option.text = type; |
+ this.typeSelect_.add(option); |
+ }, this); |
+ |
+ this.typeSelect_.addEventListener('change', this.redraw.bind(this)); |
+ |
+ var inputDiv = document.createElement('div'); |
+ inputDiv.appendChild(this.valueInput_); |
+ inputDiv.appendChild(this.typeSelect_); |
+ |
+ this.readBtn_ = document.createElement('button'); |
+ this.readBtn_.textContent = 'Read'; |
+ this.readBtn_.addEventListener('click', this.readValue_.bind(this)); |
+ |
+ this.writeBtn_ = document.createElement('button'); |
+ this.writeBtn_.textContent = 'Write'; |
+ this.writeBtn_.addEventListener('click', this.writeValue_.bind(this)); |
+ |
+ var buttonsDiv = document.createElement('div'); |
+ buttonsDiv.appendChild(this.readBtn_); |
+ buttonsDiv.appendChild(this.writeBtn_); |
+ |
+ this.appendChild(this.unavailableMessage_); |
+ this.appendChild(inputDiv); |
+ this.appendChild(buttonsDiv); |
+ }, |
+ |
+ /** |
+ * Sets the settings used by the value control and redraws the control to |
+ * match the read/write settings provided in |
+ * |characteristicInfo.properties|. |
+ * @param {string} deviceAddress |
+ * @param {string} serviceId |
+ * @param {!interfaces.BluetoothDevice.CharacteristicInfo} |
+ * characteristicInfo |
+ */ |
+ load: function(deviceAddress, serviceId, characteristicInfo) { |
+ this.deviceAddress_ = deviceAddress; |
+ this.serviceId_ = serviceId; |
+ this.characteristicInfo_ = characteristicInfo; |
+ |
+ this.redraw(); |
+ }, |
+ |
+ /** |
+ * Redraws the value control with updated layout depending on the |
+ * availability of reads and writes and the current cached value. |
+ */ |
+ redraw: function() { |
+ this.readBtn_.hidden = (this.characteristicInfo_.properties & |
+ interfaces.BluetoothDevice.Property.READ) === 0; |
+ this.writeBtn_.hidden = (this.characteristicInfo_.properties & |
+ interfaces.BluetoothDevice.Property.WRITE) === 0; |
+ |
+ var isAvailable = !this.readBtn_.hidden || !this.writeBtn_.hidden; |
+ this.unavailableMessage_.hidden = isAvailable; |
+ this.valueInput_.hidden = !isAvailable; |
+ this.typeSelect_.hidden = !isAvailable; |
+ |
+ if (!isAvailable) |
+ return; |
+ |
+ this.valueInput_.value = this.value_.getAs(this.typeSelect_.value); |
+ }, |
+ |
+ /** |
+ * Sets the value of the control. |
+ * @param {!Array<number>} value |
+ */ |
+ setValue: function(value) { |
+ this.value_.setArray(value); |
+ this.redraw(); |
+ }, |
+ |
+ /** |
+ * Gets an error string describing the given |result| code. |
+ * @param {!interfaces.BluetoothDevice.GattResult} result |
+ * @private |
+ */ |
+ getErrorString_: function(result) { |
+ // TODO(crbug.com/663394): Replace with more descriptive error |
+ // messages. |
+ var GattResult = interfaces.BluetoothDevice.GattResult; |
+ return Object.keys(GattResult).find(function(key) { |
+ return GattResult[key] === result; |
+ }); |
+ }, |
+ |
+ /** |
+ * Called when the read button is pressed. Connects to the device and |
+ * retrieves the current value of the characteristic in the |service_id| |
+ * with id |characteristic_id| |
+ * @private |
+ */ |
+ readValue_: function() { |
+ this.readBtn_.disabled = true; |
+ |
+ device_broker.connectToDevice(this.deviceAddress_).then(function(device) { |
+ return device.readValueForCharacteristic( |
+ this.serviceId_, this.characteristicInfo_.id); |
+ }.bind(this)).then(function(response) { |
+ this.readBtn_.disabled = false; |
+ |
+ if (response.result === interfaces.BluetoothDevice.GattResult.SUCCESS) { |
+ this.setValue(response.value); |
+ Snackbar.show( |
+ this.deviceAddress_ + ': Read succeeded', SnackbarType.SUCCESS); |
+ return; |
+ } |
+ |
+ var errorString = this.getErrorString_(response.result); |
+ Snackbar.show( |
+ this.deviceAddress_ + ': ' + errorString, SnackbarType.ERROR, |
+ 'Retry', this.readValue_.bind(this)); |
+ }.bind(this)); |
+ }, |
+ |
+ /** |
+ * Called when the write button is pressed. Connects to the device and |
+ * retrieves the current value of the characteristic in the |service_id| |
+ * with id |characteristic_id| |
+ * @private |
+ */ |
+ writeValue_: function() { |
+ this.writeBtn_.disabled = true; |
+ |
+ device_broker.connectToDevice(this.deviceAddress_).then(function(device) { |
+ return device.writeValueForCharacteristic( |
+ this.serviceId_, this.characteristicInfo_.id, |
+ this.value_.getArray()); |
+ }.bind(this)).then(function(response) { |
+ this.writeBtn_.disabled = false; |
+ |
+ if (response.result === interfaces.BluetoothDevice.GattResult.SUCCESS) { |
+ Snackbar.show( |
+ this.deviceAddress_ + ': Write succeeded', SnackbarType.SUCCESS); |
+ return; |
+ } |
+ |
+ var errorString = this.getErrorString_(response.result); |
+ Snackbar.show( |
+ this.deviceAddress_ + ': ' + errorString, SnackbarType.ERROR, |
+ 'Retry', this.writeValue_.bind(this)); |
+ }.bind(this)); |
+ }, |
+ } |
+ |
+ return { |
+ ValueControl: ValueControl, |
+ ValueDataType: ValueDataType, |
+ }; |
+}); |