| // 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; |
| |
| /** @typedef {{ |
| * deviceAddress: string, |
| * serviceId: string, |
| * characteristicId: string, |
| * descriptorId: (string|undefined) |
| * properties: (number|undefined) |
| * }} |
| */ |
| var ValueLoadOptions; |
| |
| /** @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 {?string} */ |
| this.characteristicId_ = null; |
| /** @private {?string} */ |
| this.descriptorId_ = null; |
| /** @private {number} */ |
| this.properties_ = Number.MAX_SAFE_INTEGER; |
| |
| 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 in |options.properties|. If properties |
| * are not provided, no restrictions on reading/writing are applied. |
| * @param {!ValueLoadOptions} options |
| */ |
| load: function(options) { |
| this.deviceAddress_ = options.deviceAddress; |
| this.serviceId_ = options.serviceId; |
| this.characteristicId_ = options.characteristicId; |
| this.descriptorId_ = options.descriptorId; |
| |
| if (options.properties) |
| this.properties_ = options.properties; |
| |
| 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.properties_ & bluetooth.mojom.Property.READ) === 0; |
| this.writeBtn_.hidden = |
| (this.properties_ & bluetooth.mojom.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 {!bluetooth.mojom.GattResult} result |
| * @private |
| */ |
| getErrorString_: function(result) { |
| // TODO(crbug.com/663394): Replace with more descriptive error |
| // messages. |
| var GattResult = bluetooth.mojom.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|. If |descriptor_id| is defined, the |
| * descriptor value with |descriptor_id| is read instead. |
| * @private |
| */ |
| readValue_: function() { |
| this.readBtn_.disabled = true; |
| |
| device_broker.connectToDevice(this.deviceAddress_) |
| .then(function(device) { |
| if (this.descriptorId_) { |
| return device.readValueForDescriptor( |
| this.serviceId_, this.characteristicId_, this.descriptorId_); |
| } |
| |
| return device.readValueForCharacteristic( |
| this.serviceId_, this.characteristicId_); |
| }.bind(this)) |
| .then(function(response) { |
| this.readBtn_.disabled = false; |
| |
| if (response.result === bluetooth.mojom.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|. If |descriptor_id| is defined, |
| * the descriptor value with |descriptor_id| is written instead. |
| * @private |
| */ |
| writeValue_: function() { |
| this.writeBtn_.disabled = true; |
| |
| device_broker.connectToDevice(this.deviceAddress_) |
| .then(function(device) { |
| if (this.descriptorId_) { |
| return device.writeValueForDescriptor( |
| this.serviceId_, this.characteristicId_, this.descriptorId_, |
| this.value_.getArray()); |
| } |
| |
| return device.writeValueForCharacteristic( |
| this.serviceId_, this.characteristicId_, |
| this.value_.getArray()); |
| }.bind(this)) |
| .then(function(response) { |
| this.writeBtn_.disabled = false; |
| |
| if (response.result === bluetooth.mojom.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, |
| }; |
| }); |