Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(189)

Unified Diff: chrome/browser/resources/bluetooth_internals/value_control.js

Issue 2627243002: bluetooth: Add control for reading/writing of characteristics to internals page. (Closed)
Patch Set: Merge upstream, add comment detail for type converter Created 3 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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,
+ };
+});

Powered by Google App Engine
This is Rietveld 408576698