Commit ccb0f470 authored by Nancy Li's avatar Nancy Li Committed by Commit Bot

[USB Internals WebUI] Add string descriptor display functions

Gets and displays string descriptor in both tree view and byte view.
Adds a button near to the string descriptor index fields to get the
string descriptor of this field. And adds input element and button to
end of page for querying string descriptor manually.


Bug: 928923
Change-Id: Ib4f7be251fcb9532e75b1adf9a31d37b22cb5029
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1534629
Commit-Queue: Nancy Li <nancyly@google.com>
Reviewed-by: default avatarMatt Reynolds <mattreynolds@chromium.org>
Reviewed-by: default avatarReilly Grant <reillyg@chromium.org>
Cr-Commit-Position: refs/heads/master@{#650411}
parent 71465539
......@@ -23,168 +23,96 @@ cr.define('descriptor_panel', function() {
const CONTROL_TRANSFER_TIMEOUT_MS = 2000; // 2 seconds
// Language codes are defined in:
// https://docs.microsoft.com/en-us/windows/desktop/intl/language-identifier-constants-and-strings
const LANGUAGE_CODE_EN_US = 0x0409;
class DescriptorPanel {
/**
* @param {!device.mojom.UsbDeviceInterface} usbDeviceProxy
* @param {HTMLElement} rootElement
* @param {!HTMLElement} rootElement
* @param {DescriptorPanel=} stringDescriptorPanel
*/
constructor(usbDeviceProxy, rootElement) {
constructor(
usbDeviceProxy, rootElement, stringDescriptorPanel = undefined) {
/** @private {!device.mojom.UsbDeviceInterface} */
this.usbDeviceProxy_ = usbDeviceProxy;
/** @private {!HTMLElement} */
this.rootElement_ = rootElement;
this.clearView();
if (stringDescriptorPanel) {
/** @private {!DescriptorPanel} */
this.stringDescriptorPanel_ = stringDescriptorPanel;
}
}
/**
* Adds a display area which contains a tree view and a byte view.
* @return {{rawDataTreeRoot:!cr.ui.Tree,rawDataByteElement:!HTMLElement}}
* @private
*/
addNewDescriptorDisplayElement_() {
const descriptorPanelTemplate =
document.querySelector('#descriptor-panel-template');
const descriptorPanelClone =
document.importNode(descriptorPanelTemplate.content, true);
/** @private {!HTMLElement} */
this.rawDataTreeRoot_ =
const rawDataTreeRoot =
descriptorPanelClone.querySelector('#raw-data-tree-view');
/** @private {!HTMLElement} */
this.rawDataElement_ = descriptorPanelClone.querySelector('#raw-data');
this.clearView();
const rawDataByteElement =
descriptorPanelClone.querySelector('#raw-data-byte-view');
cr.ui.decorate(this.rawDataTreeRoot_, cr.ui.Tree);
this.rawDataTreeRoot_.detail = {payload: {}, children: {}};
cr.ui.decorate(rawDataTreeRoot, cr.ui.Tree);
rawDataTreeRoot.detail = {payload: {}, children: {}};
this.rootElement_.appendChild(descriptorPanelClone);
return {rawDataTreeRoot, rawDataByteElement};
}
/**
* Clears the data first before populating it with the new content.
*/
clearView() {
this.rootElement_.querySelectorAll('.descriptor-panel')
.forEach(el => el.remove());
this.rootElement_.querySelectorAll('error').forEach(el => el.remove());
this.rawDataTreeRoot_.innerText = '';
this.rawDataElement_.textContent = '';
}
/**
* Adds function for mapping between two views.
* @private
*/
addMappingAction_() {
// Highlights the byte(s) that hovered in the tree.
this.rawDataTreeRoot_.querySelectorAll('.tree-row').forEach((el) => {
const classList = el.classList;
// classList[0] is 'tree-row'. classList[1] of tree item for fields
// starts with 'field-offset-', and classList[1] of tree item for
// descriptors (ie. endpoint descriptor) is descriptor type and index.
const fieldOffsetOrDescriptorClass = classList[1];
assert(
fieldOffsetOrDescriptorClass.startsWith('field-offset-') ||
fieldOffsetOrDescriptorClass.startsWith('descriptor-'));
el.addEventListener('pointerenter', (event) => {
this.rawDataElement_
.querySelectorAll(`.${fieldOffsetOrDescriptorClass}`)
.forEach((el) => el.classList.add('hovered-field'));
event.stopPropagation();
});
el.addEventListener('pointerleave', () => {
this.rawDataElement_
.querySelectorAll(`.${fieldOffsetOrDescriptorClass}`)
.forEach((el) => el.classList.remove('hovered-field'));
});
el.addEventListener('click', (event) => {
if (event.target.className != 'expand-icon') {
// Clears all the selected elements before select another.
this.rawDataElement_.querySelectorAll('#raw-data span')
.forEach((el) => el.classList.remove('selected-field'));
this.rawDataElement_
.querySelectorAll(`.${fieldOffsetOrDescriptorClass}`)
.forEach((el) => el.classList.add('selected-field'));
}
});
});
// Selects the tree item that displays the byte hovered in the raw view.
const rawDataByteElements = this.rawDataElement_.querySelectorAll('span');
rawDataByteElements.forEach((el) => {
const classList = el.classList;
const fieldOffsetClass = classList[0];
assert(fieldOffsetClass.startsWith('field-offset-'));
el.addEventListener('pointerenter', () => {
this.rawDataElement_.querySelectorAll(`.${fieldOffsetClass}`)
.forEach((el) => el.classList.add('hovered-field'));
const el =
this.rawDataTreeRoot_.querySelector(`.${fieldOffsetClass}`);
if (el) {
el.classList.add('hover');
}
});
el.addEventListener('pointerleave', () => {
this.rawDataElement_.querySelectorAll(`.${fieldOffsetClass}`)
.forEach((el) => el.classList.remove('hovered-field'));
const el =
this.rawDataTreeRoot_.querySelector(`.${fieldOffsetClass}`);
if (el) {
el.classList.remove('hover');
}
});
el.addEventListener('click', () => {
const el =
this.rawDataTreeRoot_.querySelector(`.${fieldOffsetClass}`);
if (el) {
el.click();
}
});
});
}
/**
* Renders an element to display the raw data in hex, byte by byte, and
* keeps every row no more than 16 bytes.
* @param {!Uint8Array} rawData
* @private
*/
renderRawDataBytes_(rawData) {
const rawDataByteTemplate = document.querySelector('#raw-data-byte');
for (const [i, value] of rawData.entries()) {
const rawDataByteClone =
document.importNode(rawDataByteTemplate.content, true);
const rawDataByteElement = rawDataByteClone.querySelector('span');
rawDataByteElement.textContent =
value.toString(16).padStart(2, '0').slice(-2).toUpperCase();
this.rawDataElement_.appendChild(rawDataByteElement);
}
}
/**
* Renders a tree view to display the raw data in readable text.
* @param {!cr.ui.Tree|!cr.ui.TreeItem} root
* @param {!HTMLElement} rawDataByteElement
* @param {!Array<Object>} fields
* @param {!Uint8Array} rawData
* @param {number} offset
* @param {string=} opt_parentClassName
* @param {string=} parentClassName
* @return {number}
* @private
*/
renderRawDataTree_(root, fields, rawData, offset, opt_parentClassName) {
const rawDataByteElements = this.rawDataElement_.querySelectorAll('span');
renderRawDataTree_(
root, rawDataByteElement, fields, rawData, offset,
parentClassName = undefined) {
const rawDataByteElements = rawDataByteElement.querySelectorAll('span');
for (const field of fields) {
const className = `field-offset-${offset}`;
const item = customTreeItem(
`${field.label}: ${field.formatter(rawData, offset)}`, className);
`${field.label}${field.formatter(rawData, offset)}`, className);
if (field.isIndex) {
this.renderIndexItem_(rawData[offset], item, field.label);
}
for (let i = 0; i < field.size; i++) {
rawDataByteElements[offset + i].classList.add(className);
if (opt_parentClassName) {
rawDataByteElements[offset + i].classList.add(opt_parentClassName);
if (parentClassName) {
rawDataByteElements[offset + i].classList.add(parentClassName);
}
}
......@@ -196,11 +124,50 @@ cr.define('descriptor_panel', function() {
return offset;
}
/**
* Renders a get string descriptor button for the String Descriptor Index
* field, and adds an autocomplete value to the index input area in string
* descriptor panel.
* @param {number} index
* @param {cr.ui.TreeItem} item
* @param {string} fieldLabel
*/
renderIndexItem_(index, item, fieldLabel) {
if (index > 0) {
if (!this.stringDescriptorPanel_.stringDescriptorIndexes.has(index)) {
const optionElement = cr.doc.createElement('option');
optionElement.label = index;
optionElement.value = index;
this.stringDescriptorPanel_.indexesListElement.appendChild(
optionElement);
this.stringDescriptorPanel_.stringDescriptorIndexes.add(index);
}
const buttonTemplate = document.querySelector('#raw-data-tree-button');
const button = document.importNode(buttonTemplate.content, true)
.querySelector('button');
item.querySelector('.tree-row').appendChild(button);
button.addEventListener('click', (event) => {
event.stopPropagation();
// Clear the previous string descriptors.
item.querySelector('.tree-children').textContent = '';
this.stringDescriptorPanel_.clearView();
this.stringDescriptorPanel_.renderStringDescriptorForAllLanguages(
index, item);
});
} else if (index < 0) {
// Delete the ': ' in fieldLabel.
const fieldName = fieldLabel.slice(0, -2);
this.showError_(`Invalid String Descriptor occurs in \
field ${fieldName} of this descriptor.`);
}
}
/**
* Checks the if the status of a descriptor read indicates success.
* @param {number} status
* @param {string} defaultMessage
* @return {boolean}
* @private
*/
checkDescriptorGetSuccess_(status, defaultMessage) {
......@@ -234,6 +201,7 @@ cr.define('descriptor_panel', function() {
break;
}
this.showError_(`${defaultMessage} (Reason: ${failReason})`);
// Throws an error to stop rendering descriptor.
throw new Error(`${defaultMessage} (${failReason})`);
}
......@@ -278,7 +246,7 @@ cr.define('descriptor_panel', function() {
usbControlTransferParams, length, timeout);
this.checkDescriptorGetSuccess_(
response.status, 'Failed to read the device descriptor');
response.status, 'Failed to read the device descriptor.');
return new Uint8Array(response.data);
}
......@@ -288,92 +256,107 @@ cr.define('descriptor_panel', function() {
* and raw form.
*/
async renderDeviceDescriptor() {
const rawData = await this.getDeviceDescriptor_();
this.renderRawDataBytes_(rawData);
let rawData;
try {
rawData = await this.getDeviceDescriptor_();
} catch (e) {
// Stop rendering if failed to read the device descriptor.
return;
}
const fields = [
{
label: 'Length',
label: 'Length: ',
size: 1,
formatter: formatByte,
},
{
label: 'Descriptor Type',
label: 'Descriptor Type: ',
size: 1,
formatter: formatDescriptorType,
},
{
label: 'USB Version',
label: 'USB Version: ',
size: 2,
formatter: formatUsbVersion,
},
{
label: 'Class Code',
label: 'Class Code: ',
size: 1,
formatter: formatByte,
},
{
label: 'Subclass Code',
label: 'Subclass Code: ',
size: 1,
formatter: formatByte,
},
{
label: 'Protocol Code',
label: 'Protocol Code: ',
size: 1,
formatter: formatByte,
},
{
label: 'Control Pipe Maximum Packet Size',
label: 'Control Pipe Maximum Packet Size: ',
size: 1,
formatter: formatByte,
},
{
label: 'Vendor ID',
label: 'Vendor ID: ',
size: 2,
formatter: formatTwoBytesToHex,
},
{
label: 'Product ID',
label: 'Product ID: ',
size: 2,
formatter: formatTwoBytesToHex,
},
{
label: 'Device Version',
label: 'Device Version: ',
size: 2,
formatter: formatUsbVersion,
},
{
label: 'Manufacturer String Index',
label: 'Manufacturer String Index: ',
size: 1,
formatter: formatByte,
isIndex: true,
},
{
label: 'Product String Index',
label: 'Product String Index: ',
size: 1,
formatter: formatByte,
isIndex: true,
},
{
label: 'Serial Number Index',
label: 'Serial Number Index: ',
size: 1,
formatter: formatByte,
isIndex: true,
},
{
label: 'Number of Configurations',
label: 'Number of Configurations: ',
size: 1,
formatter: formatByte,
},
];
const displayElement = this.addNewDescriptorDisplayElement_();
/** @type {!cr.ui.Tree} */
const rawDataTreeRoot = displayElement.rawDataTreeRoot;
/** @type {!HTMLElement} */
const rawDataByteElement = displayElement.rawDataByteElement;
renderRawDataBytes(rawDataByteElement, rawData);
let offset = 0;
offset = this.renderRawDataTree_(
this.rawDataTreeRoot_, fields, rawData, offset);
rawDataTreeRoot, rawDataByteElement, fields, rawData, offset);
assert(
offset === DEVICE_DESCRIPTOR_LENGTH,
'Device Descriptor Rendering Error');
this.addMappingAction_();
addMappingAction(rawDataTreeRoot, rawDataByteElement);
}
/**
......@@ -412,7 +395,7 @@ cr.define('descriptor_panel', function() {
this.checkDescriptorGetSuccess_(
response.status,
'Failed to read the complete configuration descriptor');
'Failed to read the complete configuration descriptor.');
return new Uint8Array(response.data);
}
......@@ -422,63 +405,75 @@ cr.define('descriptor_panel', function() {
* view and raw form.
*/
async renderConfigurationDescriptor() {
const rawData = await this.getConfigurationDescriptor_();
this.renderRawDataBytes_(rawData);
let rawData;
try {
rawData = await this.getConfigurationDescriptor_();
} catch (e) {
// Stop rendering if failed to read the configuration descriptor.
return;
}
const fields = [
{
label: 'Length',
label: 'Length: ',
size: 1,
formatter: formatByte,
},
{
label: 'Descriptor Type',
label: 'Descriptor Type: ',
size: 1,
formatter: formatDescriptorType,
},
{
label: 'Total Length',
label: 'Total Length: ',
size: 2,
formatter: formatShort,
},
{
label: 'Number of Interfaces',
label: 'Number of Interfaces: ',
size: 1,
formatter: formatByte,
},
{
label: 'Configuration Value',
label: 'Configuration Value: ',
size: 1,
formatter: formatByte,
},
{
label: 'Configuration String Index',
label: 'Configuration String Index: ',
size: 1,
formatter: formatByte,
isIndex: true,
},
{
label: 'Attribute Bitmap',
label: 'Attribute Bitmap: ',
size: 1,
formatter: formatBitmap,
},
{
label: 'Max Power (2mA increments)',
label: 'Max Power (2mA increments): ',
size: 1,
formatter: formatByte,
},
];
let offset = 0;
const displayElement = this.addNewDescriptorDisplayElement_();
/** @type {!cr.ui.Tree} */
const rawDataTreeRoot = displayElement.rawDataTreeRoot;
/** @type {!HTMLElement} */
const rawDataByteElement = displayElement.rawDataByteElement;
const expectNumInterfaces = rawData[offset + 4];
renderRawDataBytes(rawDataByteElement, rawData);
let offset = 0;
const expectNumInterfaces = rawData[4];
offset = this.renderRawDataTree_(
this.rawDataTreeRoot_, fields, rawData, offset);
rawDataTreeRoot, rawDataByteElement, fields, rawData, offset);
if (offset != CONFIGURATION_DESCRIPTOR_LENGTH) {
this.showError_(
'Some error(s) occurs during rendering configuration descriptor');
'An error occurred while rendering configuration descriptor.');
}
let indexInterface = 0;
......@@ -491,107 +486,115 @@ cr.define('descriptor_panel', function() {
switch (rawData[offset + 1]) {
case INTERFACE_DESCRIPTOR_TYPE:
[offset, expectNumEndpoints] = this.renderInterfaceDescriptor_(
rawData, offset, indexInterface, expectNumEndpoints);
rawDataTreeRoot, rawDataByteElement, rawData, offset,
indexInterface, expectNumEndpoints);
indexInterface++;
break;
case ENDPOINT_DESCRIPTOR_TYPE:
offset =
this.renderEndpointDescriptor_(rawData, offset, indexEndpoint);
offset = this.renderEndpointDescriptor_(
rawDataTreeRoot, rawDataByteElement, rawData, offset,
indexEndpoint);
indexEndpoint++;
break;
default:
offset =
this.renderUnknownDescriptor_(rawData, offset, indexUnknown);
offset = this.renderUnknownDescriptor_(
rawDataTreeRoot, rawDataByteElement, rawData, offset,
indexUnknown);
indexUnknown++;
break;
}
}
if (expectNumInterfaces != indexInterface) {
this.showError_(`Expected to find ${
expectNumInterfaces} interface descriptors but only encountered ${
indexInterface}.`);
this.showError_(`Expected to find \
${expectNumInterfaces} interface descriptors \
but only encountered ${indexInterface}.`);
}
if (expectNumEndpoints != indexEndpoint) {
this.showError_(`Expected to find ${
expectNumEndpoints} interface descriptors but only encountered ${
indexEndpoint}.`);
this.showError_(`Expected to find \
${expectNumEndpoints} interface descriptors \
but only encountered ${indexEndpoint}.`);
}
assert(
offset === rawData.length,
'Complete Configuration Descriptor Rendering Error');
this.addMappingAction_();
addMappingAction(rawDataTreeRoot, rawDataByteElement);
}
/**
* Renders a tree item to display indexInterface-th interface descriptor.
* Renders a tree item to display interface descriptor at index
* indexInterface.
* @param {!cr.ui.Tree} rawDataTreeRoot
* @param {!HTMLElement} rawDataByteElement
* @param {!Uint8Array} rawData
* @param {number} originalOffset
* @param {number} indexInterface
* @param {number} expectNumEndpoints
* @return {number}
* @return {!Array<number>}
* @private
*/
renderInterfaceDescriptor_(
rawData, originalOffset, indexInterface, expectNumEndpoints) {
rawDataTreeRoot, rawDataByteElement, rawData, originalOffset,
indexInterface, expectNumEndpoints) {
if (originalOffset + INTERFACE_DESCRIPTOR_LENGTH > rawData.length) {
this.showError_(
`Failed to read the ${indexInterface}-th interface descriptor.`);
this.showError_(`Failed to read the interface descriptor\
at index ${indexInterface}.`);
}
const interfaceItem = customTreeItem(
`Interface ${indexInterface}`,
`descriptor-interface-${indexInterface}`);
this.rawDataTreeRoot_.add(interfaceItem);
rawDataTreeRoot.add(interfaceItem);
const fields = [
{
label: 'Length',
label: 'Length: ',
size: 1,
formatter: formatByte,
},
{
label: 'Descriptor Type',
label: 'Descriptor Type: ',
size: 1,
formatter: formatDescriptorType,
},
{
label: 'Interface Number',
label: 'Interface Number: ',
size: 1,
formatter: formatByte,
},
{
label: 'Alternate String',
label: 'Alternate String: ',
size: 1,
formatter: formatByte,
},
{
label: 'Number of Endpoints',
label: 'Number of Endpoint: ',
size: 1,
formatter: formatByte,
},
{
label: 'Interface Class Code',
label: 'Interface Class Code: ',
size: 1,
formatter: formatByte,
},
{
label: 'Interface Subclass Code',
label: 'Interface Subclass Code: ',
size: 1,
formatter: formatByte,
},
{
label: 'Interface Protocol Code',
label: 'Interface Protocol Code: ',
size: 1,
formatter: formatByte,
},
{
label: 'Interface String Index',
label: 'Interface String Index: ',
size: 1,
formatter: formatByte,
isIndex: true,
},
];
......@@ -600,63 +603,69 @@ cr.define('descriptor_panel', function() {
expectNumEndpoints += rawData[offset + 4];
offset = this.renderRawDataTree_(
interfaceItem, fields, rawData, offset,
interfaceItem, rawDataByteElement, fields, rawData, offset,
`descriptor-interface-${indexInterface}`);
if (offset != originalOffset + INTERFACE_DESCRIPTOR_LENGTH) {
this.showError_(`Some error(s) occurred while rendering ${
indexInterface}-th interface descriptor.`);
this.showError_(
`An error occurred while rendering interface descriptor at \
index ${indexInterface}.`);
}
return [offset, expectNumEndpoints];
}
/**
* Renders a tree item to display indexEndpoint-th endpoint descriptor.
* Renders a tree item to display endpoint descriptor at index
* indexEndpoint.
* @param {!cr.ui.Tree} rawDataTreeRoot
* @param {!HTMLElement} rawDataByteElement
* @param {!Uint8Array} rawData
* @param {number} originalOffset
* @param {number} indexInterface
* @param {number} indexEndpoint
* @return {number}
* @private
*/
renderEndpointDescriptor_(rawData, originalOffset, indexEndpoint) {
renderEndpointDescriptor_(
rawDataTreeRoot, rawDataByteElement, rawData, originalOffset,
indexEndpoint) {
if (originalOffset + ENDPOINT_DESCRIPTOR_LENGTH > rawData.length) {
this.showError_(
`Failed to read the ${indexEndpoint}-th endpoint descriptor.`);
this.showError_(`Failed to read the endpoint descriptor at \
index ${indexEndpoint}.`);
}
const endpointItem = customTreeItem(
`Endpoint ${indexEndpoint}`, `descriptor-endpoint-${indexEndpoint}`);
this.rawDataTreeRoot_.add(endpointItem);
rawDataTreeRoot.add(endpointItem);
const fields = [
{
label: 'Length',
label: 'Length: ',
size: 1,
formatter: formatByte,
},
{
label: 'Descriptor Type',
label: 'Descriptor Type: ',
size: 1,
formatter: formatDescriptorType,
},
{
label: 'EndPoint Address',
label: 'EndPoint Address: ',
size: 1,
formatter: formatByte,
},
{
label: 'Attribute Bitmap',
label: 'Attribute Bitmap: ',
size: 1,
formatter: formatBitmap,
},
{
label: 'Max Packet Size',
label: 'Max Packet Size: ',
size: 2,
formatter: formatShort,
},
{
label: 'Interval',
label: 'Interval: ',
size: 1,
formatter: formatByte,
},
......@@ -664,48 +673,53 @@ cr.define('descriptor_panel', function() {
let offset = originalOffset;
offset = this.renderRawDataTree_(
endpointItem, fields, rawData, offset,
endpointItem, rawDataByteElement, fields, rawData, offset,
`descriptor-endpoint-${indexEndpoint}`);
if (offset != originalOffset + ENDPOINT_DESCRIPTOR_LENGTH) {
this.showError_(`Some error(s) occurred while rendering ${
indexEndpoint}-th endpoint descriptor.`);
this.showError_(
`An error occurred while rendering endpoint descriptor at \
index ${indexEndpoint}.`);
}
return offset;
}
/**
* Renders a tree item to display length and type of indexUnknown-th unknown
* descriptor.
* Renders a tree item to display length and type of unknown descriptor at
* index indexUnknown.
* @param {!cr.ui.Tree} rawDataTreeRoot
* @param {!HTMLElement} rawDataByteElement
* @param {!Uint8Array} rawData
* @param {number} originalOffset
* @param {number} indexInterface
* @param {number} indexUnknown
* @return {number}
* @private
*/
renderUnknownDescriptor_(rawData, originalOffset, indexUnknown) {
renderUnknownDescriptor_(
rawDataTreeRoot, rawDataByteElement, rawData, originalOffset,
indexUnknown) {
const length = rawData[originalOffset];
if (originalOffset + length > rawData.length) {
this.showError_(
`Failed to read the ${indexUnknown}-th unknown descriptor.`);
`Failed to read the unknown descriptor at index ${indexUnknown}.`);
return;
}
const unknownItem = customTreeItem(
`Unknown Descriptor ${indexUnknown}`,
`descriptor-unknown-${indexUnknown}`);
this.rawDataTreeRoot_.add(unknownItem);
rawDataTreeRoot.add(unknownItem);
const fields = [
{
label: 'Length',
label: 'Length: ',
size: 1,
formatter: formatByte,
},
{
label: 'Descriptor Type',
label: 'Descriptor Type: ',
size: 1,
formatter: formatDescriptorType,
},
......@@ -713,10 +727,10 @@ cr.define('descriptor_panel', function() {
let offset = originalOffset;
offset = this.renderRawDataTree_(
unknownItem, fields, rawData, offset,
unknownItem, rawDataByteElement, fields, rawData, offset,
`descriptor-unknown-${indexUnknown}`);
const rawDataByteElements = this.rawDataElement_.querySelectorAll('span');
const rawDataByteElements = rawDataByteElement.querySelectorAll('span');
for (; offset < originalOffset + length; offset++) {
rawDataByteElements[offset].classList.add(`field-offset-${offset}`);
......@@ -726,25 +740,423 @@ cr.define('descriptor_panel', function() {
return offset;
}
/**
* Gets all the Supported Language Code of this device, and adds them to
* autocomplete value of the language code input area in string descriptor
* panel.
* @return {!Array<number>}
*/
async getAllLanguageCodes() {
/** @type {device.mojom.UsbControlTransferParams} */
const usbControlTransferParams = {};
usbControlTransferParams.type =
device.mojom.UsbControlTransferType.STANDARD;
usbControlTransferParams.recipient =
device.mojom.UsbControlTransferRecipient.DEVICE;
usbControlTransferParams.request = GET_DESCRIPTOR_REQUEST;
usbControlTransferParams.value = (STRING_DESCRIPTOR_TYPE << 8);
usbControlTransferParams.index = 0;
const length = 0xFF;
const timeout = CONTROL_TRANSFER_TIMEOUT_MS;
await this.usbDeviceProxy_.open();
const response = await this.usbDeviceProxy_.controlTransferIn(
usbControlTransferParams, length, timeout);
try {
this.checkDescriptorGetSuccess_(
response.status,
'Failed to read the device string descriptor to determine ' +
'all supported languages.');
} catch (e) {
// Stop rendering autocomplete datalist if failed to read the string
// descriptor.
return;
}
const responseData = new Uint8Array(response.data);
this.languageCodesListElement_.innerText = '';
const optionAllElement = cr.doc.createElement('option');
optionAllElement.value = 'All';
this.languageCodesListElement_.appendChild(optionAllElement);
const languageCodesList = [];
// First two bytes are length and descriptor type(0x03);
for (let i = 2; i < responseData.length; i += 2) {
const languageCode = parseShort(responseData, i);
const optionElement = cr.doc.createElement('option');
optionElement.label = parseLanguageCode(languageCode);
optionElement.value = `0x${toHex(languageCode, 4)}`;
this.languageCodesListElement_.appendChild(optionElement);
languageCodesList.push(languageCode);
}
return languageCodesList;
}
/**
* Gets string descriptor of current device with index and language code.
* @param {number}
* @param {number}
* @return {{languageCode:string,rawData:!Uint8Array}}
* @private
*/
async getStringDescriptorForLanguageCode_(index, languageCode) {
await this.usbDeviceProxy_.open();
/** @type {device.mojom.UsbControlTransferParams} */
const usbControlTransferParams = {};
usbControlTransferParams.type =
device.mojom.UsbControlTransferType.STANDARD;
usbControlTransferParams.recipient =
device.mojom.UsbControlTransferRecipient.DEVICE;
usbControlTransferParams.request = GET_DESCRIPTOR_REQUEST;
const length = 0xFF;
const timeout = CONTROL_TRANSFER_TIMEOUT_MS;
usbControlTransferParams.index = languageCode;
usbControlTransferParams.value = (STRING_DESCRIPTOR_TYPE << 8) | index;
const response = await this.usbDeviceProxy_.controlTransferIn(
usbControlTransferParams, length, timeout);
const languageCodeStr = parseLanguageCode(languageCode);
this.checkDescriptorGetSuccess_(
response.status, `Failed to read the device string descriptor of\
index: ${index}, language: ${languageCodeStr}.`);
const rawData = new Uint8Array(response.data);
return {languageCodeStr, rawData};
}
/**
* Gets string descriptor of current device with given index and language
* code.
* @param {number} index
* @param {number} languageCode
* @param {cr.ui.TreeItem=} treeItem
*/
async renderStringDescriptorForLanguageCode(
index, languageCode, treeItem = undefined) {
this.rootElement_.hidden = false;
this.indexInput_.value = index;
let rawDataMap;
try {
rawDataMap =
await this.getStringDescriptorForLanguageCode_(index, languageCode);
} catch (e) {
// Stop rendering if failed to read the string descriptor.
return;
}
const languageStr = rawDataMap.languageCodeStr;
const rawData = rawDataMap.rawData;
const length = rawData[0];
if (length > rawData.length) {
this.showError_(`Failed to read the string descriptor at \
index ${index} in ${languageStr}.`);
return;
}
const fields = [
{
'label': 'Length: ',
'size': 1,
'formatter': formatByte,
},
{
'label': 'Descriptor Type: ',
'size': 1,
'formatter': formatDescriptorType,
},
];
// The first two elements are length and descriptor type.
for (let i = 2; i < rawData.length; i += 2) {
const field = {
'label': '',
'size': 2,
'formatter': formatLetter,
};
fields.push(field);
}
const displayElement = this.addNewDescriptorDisplayElement_();
/** @type {!cr.ui.Tree} */
const rawDataTreeRoot = displayElement.rawDataTreeRoot;
/** @type {!HTMLElement} */
const rawDataByteElement = displayElement.rawDataByteElement;
const stringDescriptorItem = customTreeItem(
`${languageStr}: ${decodeArray(rawData)}`,
`descriptor-string-${index}-language-code-${languageStr}`);
rawDataTreeRoot.add(stringDescriptorItem);
if (treeItem) {
treeItem.add(customTreeItem(`${languageStr}: ${decodeArray(rawData)}`));
treeItem.expanded = true;
}
renderRawDataBytes(rawDataByteElement, rawData);
let offset = 0;
offset = this.renderRawDataTree_(
stringDescriptorItem, rawDataByteElement, fields, rawData, offset,
`descriptor-string-${index}-language-code-${languageStr}`);
addMappingAction(rawDataTreeRoot, rawDataByteElement);
}
/**
* Gets string descriptor in all supported languages of current device with
* given index.
* @param {number} index
* @param {cr.ui.TreeItem=} treeItem
*/
async renderStringDescriptorForAllLanguages(index, treeItem = undefined) {
this.rootElement_.hidden = false;
this.indexInput_.value = index;
/** @type {!Array<number>|undefined} */
const languageCodesList = await this.getAllLanguageCodes();
for (const languageCode of languageCodesList) {
await this.renderStringDescriptorForLanguageCode(
index, languageCode, treeItem);
}
}
/**
* Initializes the string descriptor panel for autocomplete functionality.
* @param {number} tabId
*/
initialStringDescriptorPanel(tabId) {
// Binds the input area and datalist use each tab's unique id.
this.rootElement_.querySelectorAll('input').forEach(
el => el.setAttribute('list', `${el.getAttribute('list')}-${tabId}`));
this.rootElement_.querySelectorAll('datalist')
.forEach(el => el.id = `${el.id}-${tabId}`);
/** @type {!HTMLElement} */
const button = this.rootElement_.querySelector('button');
/** @type {!HTMLElement} */
this.indexInput_ = this.rootElement_.querySelector('#index-input');
/** @type {!HTMLElement} */
const languageCodeInput =
this.rootElement_.querySelector('#language-code-input');
button.addEventListener('click', () => {
this.clearView();
const index = Number.parseInt(this.indexInput_.value);
if (this.checkIndexValueValid_(index)) {
if (languageCodeInput.value === 'All') {
this.renderStringDescriptorForAllLanguages(index);
} else {
const languageCode = Number.parseInt(languageCodeInput.value);
if (this.checkLanguageCodeValueValid_(languageCode)) {
this.renderStringDescriptorForLanguageCode(index, languageCode);
}
}
}
});
/** @type {!Set<number>} */
this.stringDescriptorIndexes = new Set();
/** @type {!HTMLElement} */
this.indexesListElement =
this.rootElement_.querySelector(`#indexes-${tabId}`);
/** @type {!HTMLElement} */
this.languageCodesListElement_ =
this.rootElement_.querySelector(`#languages-${tabId}`);
}
/**
* Checks if the user input index is a valid uint8 number.
* @param {number} index
* @return {boolean}
*/
checkIndexValueValid_(index) {
// index is 8 bit. 0 is reserved to query all supported language codes.
if (Number.isNaN(index) || index < 1 || index > 255) {
this.showError_('Invalid Index.');
return false;
}
return true;
}
/**
* Checks if the user input language code is a valid uint16 number.
* @param {number} languageCode
* @return {boolean}
*/
checkLanguageCodeValueValid_(languageCode) {
if (Number.isNaN(languageCode) || languageCode < 0 ||
languageCode > 65535) {
this.showError_('Invalid Language Code.');
return false;
}
return true;
}
}
/**
* Renders a customized TreeItem with the given content and class name.
* @param {string} itemLabel
* @param {string=} opt_className
* @param {string=} className
* @return {!cr.ui.TreeItem}
*/
function customTreeItem(itemLabel, opt_className) {
function customTreeItem(itemLabel, className = undefined) {
const item = new cr.ui.TreeItem({
label: itemLabel,
icon: '',
});
if (opt_className) {
item.querySelector('.tree-row').classList.add(opt_className);
if (className) {
item.querySelector('.tree-row').classList.add(className);
}
return item;
}
/**
* Adds function for mapping between two views.
* @param {!cr.ui.Tree} rawDataTreeRoot
* @param {!HTMLElement} rawDataByteElement
*/
function addMappingAction(rawDataTreeRoot, rawDataByteElement) {
// Highlights the byte(s) that hovered in the tree.
rawDataTreeRoot.querySelectorAll('.tree-row').forEach((el) => {
const classList = el.classList;
// classList[0] is 'tree-row'. classList[1] of tree item for fields
// starts with 'field-offset-', and classList[1] of tree item for
// descriptors (ie. endpoint descriptor) is descriptor type and index.
const fieldOffsetOrDescriptorClass = classList[1];
assert(
fieldOffsetOrDescriptorClass.startsWith('field-offset-') ||
fieldOffsetOrDescriptorClass.startsWith('descriptor-'));
el.addEventListener('pointerenter', (event) => {
rawDataByteElement.querySelectorAll(`.${fieldOffsetOrDescriptorClass}`)
.forEach((el) => el.classList.add('hovered-field'));
event.stopPropagation();
});
el.addEventListener('pointerleave', () => {
rawDataByteElement.querySelectorAll(`.${fieldOffsetOrDescriptorClass}`)
.forEach((el) => el.classList.remove('hovered-field'));
});
el.addEventListener('click', (event) => {
if (event.target.className != 'expand-icon') {
// Clears all the selected elements before select another.
rawDataByteElement.querySelectorAll('#raw-data-byte-view span')
.forEach((el) => el.classList.remove('selected-field'));
rawDataByteElement
.querySelectorAll(`.${fieldOffsetOrDescriptorClass}`)
.forEach((el) => el.classList.add('selected-field'));
}
});
});
// Selects the tree item that displays the byte hovered in the raw view.
const rawDataByteElements = rawDataByteElement.querySelectorAll('span');
rawDataByteElements.forEach((el) => {
const classList = el.classList;
const fieldOffsetClass = classList[0];
assert(fieldOffsetClass.startsWith('field-offset-'));
el.addEventListener('pointerenter', () => {
rawDataByteElement.querySelectorAll(`.${fieldOffsetClass}`)
.forEach((el) => el.classList.add('hovered-field'));
const el = rawDataTreeRoot.querySelector(`.${fieldOffsetClass}`);
if (el) {
el.classList.add('hover');
}
});
el.addEventListener('pointerleave', () => {
rawDataByteElement.querySelectorAll(`.${fieldOffsetClass}`)
.forEach((el) => el.classList.remove('hovered-field'));
const el = rawDataTreeRoot.querySelector(`.${fieldOffsetClass}`);
if (el) {
el.classList.remove('hover');
}
});
el.addEventListener('click', () => {
const el = rawDataTreeRoot.querySelector(`.${fieldOffsetClass}`);
if (el) {
el.click();
}
});
});
}
/**
* Renders an element to display the raw data in hex, byte by byte, and
* keeps every row no more than 16 bytes.
* @param {!HTMLElement} rawDataByteElement
* @param {!Uint8Array} rawData
*/
function renderRawDataBytes(rawDataByteElement, rawData) {
const rawDataByteContainerTemplate =
document.querySelector('#raw-data-byte-container-template');
const rawDataByteContainerClone =
document.importNode(rawDataByteContainerTemplate.content, true);
const rawDataByteContainerElement =
rawDataByteContainerClone.querySelector('div');
const rawDataByteTemplate =
document.querySelector('#raw-data-byte-template');
for (const value of rawData) {
const rawDataByteClone =
document.importNode(rawDataByteTemplate.content, true);
const rawDataByteElement = rawDataByteClone.querySelector('span');
rawDataByteElement.textContent = toHex(value, 2);
rawDataByteContainerElement.appendChild(rawDataByteElement);
}
rawDataByteElement.appendChild(rawDataByteContainerElement);
}
/**
* Converts a number to a hexadecimal string padded with zeros to the given
* number of digits.
* @param {number} number
* @param {number} numOfDigits
* @return {string}
*/
function toHex(number, numOfDigits) {
return number.toString(16)
.padStart(numOfDigits, '0')
.slice(0 - numOfDigits)
.toUpperCase();
}
/**
* Parses unicode array to string.
* @param {!Uint8Array} arr
* @return {string}
*/
function decodeArray(arr) {
let str = '';
// The first two elements are length and descriptor type.
for (let i = 2; i < arr.length; i += 2) {
str += formatLetter(arr, i);
}
return str;
}
/**
* Parses one byte to decimal number string.
* @param {!Uint8Array} rawData
......@@ -776,6 +1188,17 @@ cr.define('descriptor_panel', function() {
return parseShort(rawData, offset).toString();
}
/**
* Parses two bytes to decimal number string.
* @param {!Uint8Array} rawData
* @param {number} offset
* @return {string}
*/
function formatLetter(rawData, offset) {
const num = parseShort(rawData, offset);
return String.fromCodePoint(num);
}
/**
* Parses two bytes to a hex string.
* @param {!Uint8Array} rawData
......@@ -784,7 +1207,7 @@ cr.define('descriptor_panel', function() {
*/
function formatTwoBytesToHex(rawData, offset) {
const num = parseShort(rawData, offset);
return `0x${num.toString(16).padStart(4, '0').slice(-4).toUpperCase()}`;
return `0x${toHex(num, 4)}`;
}
/**
......@@ -815,8 +1238,21 @@ cr.define('descriptor_panel', function() {
* @return {string}
*/
function formatDescriptorType(rawData, offset) {
return `0x${
rawData[offset].toString(16).padStart(2, '0').slice(-2).toUpperCase()}`;
return `0x${toHex(rawData[offset], 2)}`;
}
/**
* Parses language code to readable language name.
* @param {number} languageCode
* @return {string}
*/
function parseLanguageCode(languageCode) {
switch (languageCode) {
case LANGUAGE_CODE_EN_US:
return 'en-US';
default:
return `0x${toHex(languageCode, 4)}`;
}
}
return {
......
......@@ -76,8 +76,7 @@ cr.define('devices_page', function() {
const tabId = device.guid;
if (null == $(tabId)) {
const devicePage = new DevicePage(this.usbManager_);
devicePage.renderTab(device);
const devicePage = new DevicePage(this.usbManager_, device);
}
$(tabId).selected = true;
}
......@@ -90,15 +89,13 @@ cr.define('devices_page', function() {
class DevicePage {
/**
* @param {!device.mojom.UsbDeviceManagerProxy} usbManager
* @param {!device.mojom.UsbDeviceInfo} device
*/
constructor(usbManager) {
constructor(usbManager, device) {
/** @private {device.mojom.UsbDeviceManagerProxy} */
this.usbManager_ = usbManager;
/** @private {boolean} */
this.showDeviceDescriptor_ = false;
/** @private {boolean} */
this.showConfigurationDescriptor_ = false;
this.renderTab(device);
}
/**
......@@ -144,20 +141,37 @@ cr.define('devices_page', function() {
const usbDeviceProxy = new UsbDeviceProxy;
this.usbManager_.getDevice(device.guid, usbDeviceProxy.$.createRequest());
const getStringDescriptorButton =
tabPanelClone.querySelector('#string-descriptor-button');
const stringDescriptorElement =
tabPanelClone.querySelector('.string-descriptor-panel');
const stringDescriptorPanel = new descriptor_panel.DescriptorPanel(
usbDeviceProxy, stringDescriptorElement);
stringDescriptorPanel.initialStringDescriptorPanel(tab.id);
getStringDescriptorButton.addEventListener('click', () => {
stringDescriptorElement.hidden = !stringDescriptorElement.hidden;
// Clear the panel before rendering new data.
stringDescriptorPanel.clearView();
if (!stringDescriptorElement.hidden) {
stringDescriptorPanel.getAllLanguageCodes();
}
});
const getDeviceDescriptorButton =
tabPanelClone.querySelector('#device-descriptor-button');
const deviceDescriptorElement =
tabPanelClone.querySelector('.device-descriptor-panel');
const deviceDescriptorPanel = new descriptor_panel.DescriptorPanel(
usbDeviceProxy, deviceDescriptorElement);
usbDeviceProxy, deviceDescriptorElement, stringDescriptorPanel);
getDeviceDescriptorButton.addEventListener('click', () => {
this.showDeviceDescriptor_ = !this.showDeviceDescriptor_;
deviceDescriptorElement.hidden = !deviceDescriptorElement.hidden;
// Clear the panel before rendering new data.
deviceDescriptorPanel.clearView();
if (this.showDeviceDescriptor_) {
if (!deviceDescriptorElement.hidden) {
deviceDescriptorPanel.renderDeviceDescriptor();
}
});
......@@ -167,14 +181,16 @@ cr.define('devices_page', function() {
const configurationDescriptorElement =
tabPanelClone.querySelector('.configuration-descriptor-panel');
const configurationDescriptorPanel = new descriptor_panel.DescriptorPanel(
usbDeviceProxy, configurationDescriptorElement);
usbDeviceProxy, configurationDescriptorElement,
stringDescriptorPanel);
getConfigurationDescriptorButton.addEventListener('click', () => {
this.showConfigurationDescriptor_ = !this.showConfigurationDescriptor_;
configurationDescriptorElement.hidden =
!configurationDescriptorElement.hidden;
// Clear the panel before rendering new data.
configurationDescriptorPanel.clearView();
if (this.showConfigurationDescriptor_) {
if (!configurationDescriptorElement.hidden) {
configurationDescriptorPanel.renderConfigurationDescriptor();
}
});
......
......@@ -3,6 +3,13 @@
* found in the LICENSE file.
*/
tabs {
position: sticky;
top: 0;
/* selected treeItem is 2 */
z-index: 3;
}
/* Devices Tab */
table.styled-table {
border-collapse: collapse;
......@@ -38,58 +45,76 @@ table.styled-table,
width: 100%;
}
.descriptor-button {
position: sticky;
/* height of <tabs> */
top: 20px;
/* selected treeItem is 2 */
z-index: 3;
}
/* descriptor panel */
.device-descriptor-panel,
.configuration-descriptor-panel {
.descriptor-panel {
display: flex;
overflow: auto;
overflow: visible;
}
.string-descriptor-panel input {
margin-inline-end: 16px;
margin-inline-start: 8px;
}
#raw-data,
#raw-data-byte-view,
#raw-data-tree-view {
flex: 1 0;
}
#raw-data {
#raw-data-byte-view {
font-size: 14px;
line-height: 250%;
white-space: pre;
}
#raw-data-byte-view div {
position: sticky;
/* height of <tabs> and button */
top: 41px;
}
@media screen and (min-width: 1200px) {
#raw-data span:nth-child(16n)::after {
#raw-data-byte-view div span:nth-child(16n)::after {
content: '\A';
}
}
@media screen and (min-width: 600px) and (max-width: 1199px) {
#raw-data span:nth-child(8n)::after {
#raw-data-byte-view div span:nth-child(8n)::after {
content: '\A';
}
}
@media screen and (min-width: 300px) and (max-width: 599px) {
#raw-data span:nth-child(4n)::after {
#raw-data-byte-view div span:nth-child(4n)::after {
content: '\A';
}
}
@media screen and (min-width: 150px) and (max-width: 299px) {
#raw-data span:nth-child(2n)::after {
#raw-data-byte-view div span:nth-child(2n)::after {
content: '\A';
}
}
#raw-data span {
#raw-data-byte-view div span {
padding-inline-end: .5em;
padding-inline-start: .5em;
}
#raw-data .selected-field {
#raw-data-byte-view .selected-field {
background: red;
}
#raw-data .hovered-field {
#raw-data-byte-view .hovered-field {
background: yellow;
}
......@@ -99,6 +124,10 @@ table.styled-table,
z-index: 1;
}
.tree-row button {
margin-inline-start: 16px;
}
error {
color: red;
display: block;
......
......@@ -141,25 +141,47 @@
<div class="descriptor-button">
<button id="device-descriptor-button">Get Device Descriptor</button>
</div>
<div class="device-descriptor-panel"></div>
<div class="device-descriptor-panel" hidden></div>
<div class="descriptor-button">
<button id="configuration-descriptor-button">
Get Configuration Descriptor
</button>
</div>
<div class="configuration-descriptor-panel"></div>
<div class="configuration-descriptor-panel" hidden></div>
<div class="descriptor-button">
<button id="string-descriptor-button">Get String Descriptor</button>
</div>
<div class="string-descriptor-panel" hidden>
String Descriptor Index:
<input id="index-input" type="number" min="1" list="indexes">
<datalist id="indexes"></datalist>
Language Code:
<input id="language-code-input" list="languages">
<datalist id="languages"></datalist>
<button>GET</button>
</div>
</tabpanel>
</template>
<template id="descriptor-panel-template">
<div class="descriptor-panel">
<tree id="raw-data-tree-view"></tree>
<div id="raw-data"></div>
<div id="raw-data-byte-view"></div>
</div>
</template>
<template id="raw-data-byte">
<template id="raw-data-byte-container-template">
<div></div>>
</template>
<template id="raw-data-byte-template">
<span></span>
</template>
<template id="raw-data-tree-button">
<button>GET</button>
</template>
<template id="error">
<error></error>
</template>
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment