diff --git a/package.json b/package.json index a98fd06..66b0a7a 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "serve": "serve --cors -p 9000" }, "dependencies": { + "jszip": "^3.10.1", "node-fetch": "^2.6.7", "xml2js": "^0.4.23" }, @@ -41,6 +42,7 @@ "@vscode/test-web": "^0.0.24", "buffer": "^6.0.3", "eslint": "^7.29.0", + "path-browserify": "1.0.1", "serve": "^14.0.1", "stream-browserify": "^3.0.0", "timers-browserify": "^2.0.12", @@ -63,7 +65,7 @@ }, "commands": [ { - "command": "svd-viewer.svd.setValue", + "command": "svd-viewer.svd.updateNode", "title": "Update Value", "icon": "$(edit)" }, @@ -73,7 +75,7 @@ "icon": "$(files)" }, { - "command": "svd-viewer.svd.refreshValue", + "command": "svd-viewer.svd.forceRefresh", "title": "Refresh", "icon": "$(refresh)" }, @@ -96,7 +98,7 @@ "menus": { "commandPalette": [ { - "command": "svd-viewer.svd.setValue", + "command": "svd-viewer.svd.updateNode", "when": "false" }, { @@ -104,7 +106,7 @@ "when": "false" }, { - "command": "svd-viewer.svd.refreshValue", + "command": "svd-viewer.svd.forceRefresh", "when": "false" }, { @@ -122,19 +124,19 @@ ], "touchBar": [ { - "command": "svd-viewer.svd.refreshValue", + "command": "svd-viewer.svd.forceRefresh", "when": "view == svd-viewer.svd && viewItem == registerRW" }, { - "command": "svd-viewer.svd.refreshValue", + "command": "svd-viewer.svd.forceRefresh", "when": "view == svd-viewer.svd && viewItem == register" }, { - "command": "svd-viewer.svd.refreshValue", + "command": "svd-viewer.svd.forceRefresh", "when": "view == svd-viewer.svd && viewItem == registerRO" }, { - "command": "svd-viewer.svd.refreshValue", + "command": "svd-viewer.svd.forceRefresh", "when": "view == svd-viewer.svd && viewItem =~ /peripheral.*/" }, { @@ -148,22 +150,22 @@ ], "view/item/context": [ { - "command": "svd-viewer.svd.setValue", + "command": "svd-viewer.svd.updateNode", "when": "view == svd-viewer.svd && viewItem == field", "group": "inline" }, { - "command": "svd-viewer.svd.setValue", + "command": "svd-viewer.svd.updateNode", "when": "view == svd-viewer.svd && viewItem == fieldWO", "group": "inline" }, { - "command": "svd-viewer.svd.setValue", + "command": "svd-viewer.svd.updateNode", "when": "view == svd-viewer.svd && viewItem == registerRW", "group": "inline" }, { - "command": "svd-viewer.svd.setValue", + "command": "svd-viewer.svd.updateNode", "when": "view == svd-viewer.svd && viewItem == registerWO", "group": "inline" }, @@ -183,22 +185,22 @@ "group": "inline" }, { - "command": "svd-viewer.svd.refreshValue", + "command": "svd-viewer.svd.forceRefresh", "when": "view == svd-viewer.svd && viewItem == registerRW", "group": "inline" }, { - "command": "svd-viewer.svd.refreshValue", + "command": "svd-viewer.svd.forceRefresh", "when": "view == svd-viewer.svd && viewItem == register", "group": "inline" }, { - "command": "svd-viewer.svd.refreshValue", + "command": "svd-viewer.svd.forceRefresh", "when": "view == svd-viewer.svd && viewItem == registerRO", "group": "inline" }, { - "command": "svd-viewer.svd.refreshValue", + "command": "svd-viewer.svd.forceRefresh", "when": "view == svd-viewer.svd && viewItem =~ /peripheral.*/", "group": "inline" }, @@ -229,8 +231,26 @@ }, "svd-viewer.deviceConfig": { "type": "string", - "default": "device", - "description": "Debug configuration key to use to get the device" + "default": "deviceName", + "description": "Debug configuration key to use to get the device name" + }, + "svd-viewer.processorConfig": { + "type": "string", + "default": "processorName", + "description": "Debug configuration key to use to get the procerssor name" + }, + "svd-viewer.svdAddrGapThreshold": { + "type": "number", + "default": 16, + "multipleOf": 1, + "minimum": -1, + "maximum": 32, + "description": "If the gap between registers is less than this threshold (multiple of 8), combine into a single read from device. -1 means never combine registers and is very slow" + }, + "svd-viewer.packAssetUrl": { + "type": "string", + "default": "https://pack-asset-service.keil.arm.com", + "description": "Base URL for CMSIS pack assets" } } } diff --git a/src/addrranges.ts b/src/addrranges.ts new file mode 100644 index 0000000..1632bde --- /dev/null +++ b/src/addrranges.ts @@ -0,0 +1,57 @@ +/* + * Copyright 2017-2019 Marcel Ball + * https://github.com/Marus/cortex-debug + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + * TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +export class AddrRange { + constructor(public base: number, public length: number) { + } + + /** return next address after this addr. range */ + public nxtAddr(): number { + return this.base + this.length; + } + + /** return last address in this range */ + public endAddr(): number { + return this.nxtAddr() - 1; + } +} + +export class AddressRangesUtils { + /** + * Returns a set of address ranges that have 0 < length <= maxBytes + * + * @param ranges array of ranges to check an split + * @param maxBytes limit of each range + * @param dbgMsg To output debug messages -- name of address space + * @param dbgLen To output debug messages -- total length of addr space + */ + public static splitIntoChunks(ranges: AddrRange[], maxBytes: number, _dbgMsg = '', _dbgLen = 0): AddrRange[] { + const newRanges = new Array(); + for (const r of ranges) { + while (r.length > maxBytes) { + newRanges.push(new AddrRange(r.base, maxBytes)); + r.base += maxBytes; + r.length -= maxBytes; + } + if (r.length > 0) { // Watch out, can be negative + newRanges.push(r); + } + } + return newRanges; + } +} diff --git a/src/browser/extension.ts b/src/browser/extension.ts index 8c101d7..48cf516 100644 --- a/src/browser/extension.ts +++ b/src/browser/extension.ts @@ -1,16 +1,26 @@ +/** + * Copyright (C) 2023 Arm Limited + */ + import * as vscode from 'vscode'; -import { PeripheralTree } from '../peripheral-tree'; -import { SvdCommands } from '../svd-commands'; +import { PeripheralTreeProvider } from '../views/peripheral'; +import { Commands } from '../commands'; import { DebugTracker } from '../debug-tracker'; +import { SvdRegistry } from '../svd-registry'; +import { SvdResolver } from '../svd-resolver'; -export const activate = async (context: vscode.ExtensionContext): Promise => { - const peripheralTree = new PeripheralTree(); - const commands = new SvdCommands(peripheralTree); - const tracker = new DebugTracker(peripheralTree); +export const activate = async (context: vscode.ExtensionContext): Promise => { + const tracker = new DebugTracker(); + const registry = new SvdRegistry(); + const resolver = new SvdResolver(registry); + const peripheralTree = new PeripheralTreeProvider(tracker, resolver); + const commands = new Commands(peripheralTree); + await tracker.activate(context); await peripheralTree.activate(context); await commands.activate(context); - await tracker.activate(context); + + return registry; }; export const deactivate = async (): Promise => { diff --git a/src/cmsis-pack/pack-utils.ts b/src/cmsis-pack/pack-utils.ts new file mode 100644 index 0000000..01d86b8 --- /dev/null +++ b/src/cmsis-pack/pack-utils.ts @@ -0,0 +1,53 @@ +/** + * Copyright (C) 2023 Arm Limited + */ + +import * as vscode from 'vscode'; + +const MAIN_DELIMITER = '::'; +const VERSION_DELIMITER = '@'; + +export interface Pack { + vendor: string; + pack: string; + version?: string; +} + +export const parsePackString = (packString: string): Pack | undefined => { + let parts = packString.split(MAIN_DELIMITER); + + if (parts.length < 2) { + return undefined; + } + + const vendor = parts[0]; + let pack = parts[1]; + + parts = pack.split(VERSION_DELIMITER); + let version: string | undefined; + + if (parts.length > 1) { + pack = parts[0]; + version = parts[1]; + } + + return { + vendor, + pack, + version + }; +}; + +export const pdscFromPack = (basePath: string, pack: Pack): vscode.Uri => { + const pdscFile = `${pack.vendor}.${pack.pack}.pdsc`; + return fileFromPack(basePath, pack, pdscFile); +}; + +export const fileFromPack = (basePath: string, pack: Pack, file: string): vscode.Uri => { + if (!pack.version) { + throw new Error('CMSIS pack version is required'); + } + + const baseUri = vscode.Uri.parse(basePath); + return vscode.Uri.joinPath(baseUri, pack.vendor, pack.pack, pack.version, file); +}; diff --git a/src/cmsis-pack/pdsc.ts b/src/cmsis-pack/pdsc.ts new file mode 100644 index 0000000..85b4e74 --- /dev/null +++ b/src/cmsis-pack/pdsc.ts @@ -0,0 +1,185 @@ +/** + * Copyright (C) 2023 Arm Limited + */ + +/** + * Interface describing raw PDSC data returned from xml2js + */ +export interface PDSC { + package: { + devices?: Array<{ + family: DeviceFamily[]; + }>; + }; +} + +export interface DeviceProperties { + processor?: Array<{ + $: { + Pname: string; + Punits: string; + Dclock: string; + DcoreVersion: string; + }; + }>; + debug?: Array<{ + $?: { + __dp?: string; + __ap?: string; + __apid?: string; + address?: string; + svd?: string; + Pname?: string; + Punit?: string; + defaultResetSequence?: string; + }; + }>; +} + +export interface DeviceVariant extends DeviceProperties { + $: { + Dvariant: string; + Dname: string; + }; +} + +export interface Device extends DeviceProperties { + $: { + Dname: string; + }; + variant?: DeviceVariant[]; +} + +export interface DeviceSubFamily extends DeviceProperties { + $: { + DsubFamily: string; + }; + device?: Device[]; +} + +export interface DeviceFamily extends DeviceProperties { + $: { + Dfamily: string; + Dvendor: string; + }; + subFamily?: DeviceSubFamily[]; + device?: Device[]; +} + +// Return whether an item is an object +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const isObject = (item: any): boolean => item && typeof item === 'object' && !Array.isArray(item); + +// Merge two objects recursively, with source overwriting target when there's a conflict +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const deepMerge = (target: { [key: string]: any }, source: T): T => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const output = Object.assign({} as any, target); + + Object.keys(source).forEach(key => { + if (isObject(source[key]) && key in target) { + output[key] = deepMerge(target[key], source[key]); + } else { + Object.assign(output, { [key]: source[key] }); + } + }); + + return output; +}; + +// Recurse DeviceFamily and DeviceSubFamily to find Devices and DeviceVariants, merging them as we go +export const getDevices = (pack: PDSC): Array => { + const result: Device[] = []; + + const addDevice = (device: Device, parent: DeviceProperties = {}) => { + const entry = deepMerge(parent, device); + result.push(entry); + }; + + const walkDevices = (devices?: Device[], parent: DeviceProperties = {}) => { + if (devices) { + for (const device of devices) { + if (device.variant) { + // If there are variants, add them instead of the parent device + for (const variant of device.variant) { + // Merge in device + const variantParent = deepMerge(parent, device); + addDevice(variant, variantParent); + } + } else { + addDevice(device, parent); + } + } + } + }; + + // Walk the DeviceFamily array + if (pack.package.devices) { + for (const device of pack.package.devices) { + for (const family of device.family) { + walkDevices(family.device, family); + + // Walk the DeviceSubFamily array + if (family.subFamily) { + for (const sub of family.subFamily) { + const parent = deepMerge(family, sub); + walkDevices(sub.device, parent); + } + } + } + } + } + + return result; +}; + +/** + * Return list of processor names available for specified device + */ +export const getProcessors = (device: Device): string[] => { + const processors: string[] = []; + + if (device.processor) { + for (const processor of device.processor) { + if (processor.$ && processor.$.Pname) { + processors.push(processor.$.Pname); + } + } + } + + return processors; +}; + +/** + * Return svd path (or undefined) for specified device + * If processorName specified, matching svd file is returned, else the first one + */ +export const getSvdPath = (device: Device, processorName?: string): string | undefined => { + if (device.debug) { + const filtered = filterByProcessor(device.debug, processorName); + for (const debug of filtered) { + if (debug.$ && debug.$.svd) { + return debug.$.svd; + } + } + } + + return undefined; +}; + +const filterByProcessor = (theArray: T[], processorName: string | undefined): T[] => { + // If processorName not specified, return all items + if (!processorName) { + return theArray; + } + + return filter(theArray, item => item.$?.Pname === processorName) as T[]; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const filter = (theArray: T[], predicate: (item: T) => boolean): T[] => { + const filtered = theArray.filter(item => item.$ && predicate(item)); + + // If no items match, return them all + return filtered.length > 0 ? filtered as T[] : theArray; +}; diff --git a/src/commands.ts b/src/commands.ts new file mode 100644 index 0000000..3dc356d --- /dev/null +++ b/src/commands.ts @@ -0,0 +1,88 @@ +/* + * Copyright 2017-2019 Marcel Ball + * https://github.com/Marus/cortex-debug + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + * TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import * as vscode from 'vscode'; +import * as manifest from './manifest'; +import { PeripheralBaseNode } from './views/nodes/basenode'; +import { PeripheralTreeProvider } from './views/peripheral'; +import { NumberFormat } from './common'; + +export class Commands { + public constructor(protected peripheralProvider: PeripheralTreeProvider) { + } + + public async activate(context: vscode.ExtensionContext): Promise { + context.subscriptions.push( + vscode.commands.registerCommand(`${manifest.PACKAGE_NAME}.svd.updateNode`, node => this.peripheralsUpdateNode(node)), + vscode.commands.registerCommand(`${manifest.PACKAGE_NAME}.svd.copyValue`, node => this.peripheralsCopyValue(node)), + vscode.commands.registerCommand(`${manifest.PACKAGE_NAME}.svd.setFormat`, node => this.peripheralsSetFormat(node)), + vscode.commands.registerCommand(`${manifest.PACKAGE_NAME}.svd.forceRefresh`, node => this.peripheralsForceRefresh(node)), + vscode.commands.registerCommand(`${manifest.PACKAGE_NAME}.svd.pin`, node => this.peripheralsTogglePin(node)), + vscode.commands.registerCommand(`${manifest.PACKAGE_NAME}.svd.unpin`, node => this.peripheralsTogglePin(node)) + ); + } + + private async peripheralsUpdateNode(node: PeripheralBaseNode): Promise { + try { + const result = await node.performUpdate(); + if (result) { + this.peripheralsForceRefresh(node); + } + } catch (error) { + vscode.window.showErrorMessage(`Unable to update value: ${(error as Error).message}`); + } + } + + private peripheralsCopyValue(node: PeripheralBaseNode): void { + const cv = node.getCopyValue(); + if (cv) { + vscode.env.clipboard.writeText(cv); + } + } + + private async peripheralsSetFormat(node: PeripheralBaseNode): Promise { + const result = await vscode.window.showQuickPick([ + { label: 'Auto', description: 'Automatically choose format (Inherits from parent)', value: NumberFormat.Auto }, + { label: 'Hex', description: 'Format value in hexadecimal', value: NumberFormat.Hexadecimal }, + { label: 'Decimal', description: 'Format value in decimal', value: NumberFormat.Decimal }, + { label: 'Binary', description: 'Format value in binary', value: NumberFormat.Binary } + ]); + if (result === undefined) { + return; + } + + node.format = result.value; + this.peripheralProvider.refresh(); + } + + private async peripheralsForceRefresh(node: PeripheralBaseNode): Promise { + if (node) { + const p = node.getPeripheral(); + if (p) { + await p.updateData(); + } + } + + this.peripheralProvider.refresh(); + } + + private peripheralsTogglePin(node: PeripheralBaseNode): void { + this.peripheralProvider.togglePinPeripheral(node); + this.peripheralProvider.refresh(); + } +} diff --git a/src/common.ts b/src/common.ts new file mode 100644 index 0000000..9a2651d --- /dev/null +++ b/src/common.ts @@ -0,0 +1,69 @@ +/* + * Copyright 2017-2019 Marcel Ball + * https://github.com/Marus/cortex-debug + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + * TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +export enum NumberFormat { + Auto = 0, + Hexadecimal, + Decimal, + Binary +} + +export interface NodeSetting { + node: string; + expanded?: boolean; + format?: NumberFormat; + pinned?: boolean; +} + +export function toStringDecHexOctBin(val: number/* should be an integer*/): string { + if (Number.isNaN(val)) { + return 'NaN: Not a number'; + } + if (!Number.isSafeInteger(val)) { + // TODO: Handle big nums. We eventually have to. We need to use bigint as javascript + // looses precision beyond 53 bits + return 'Big Num: ' + val.toString() + '\nother-radix values not yet available. Sorry'; + } + + let ret = `dec: ${val}`; + if (val < 0) { + val = -val; + val = (~(val >>> 0) + 1) >>> 0; + } + let str = val.toString(16); + str = '0x' + '0'.repeat(Math.max(0, 8 - str.length)) + str; + ret += `\nhex: ${str}`; + + str = val.toString(8); + str = '0'.repeat(Math.max(0, 12 - str.length)) + str; + ret += `\noct: ${str}`; + + str = val.toString(2); + str = '0'.repeat(Math.max(0, 32 - str.length)) + str; + let tmp = ''; + while (true) { + if (str.length <= 8) { + tmp = str + tmp; + break; + } + tmp = ' ' + str.slice(-8) + tmp; + str = str.slice(0, -8); + } + ret += `\nbin: ${tmp}`; + return ret ; +} diff --git a/src/debug-tracker.ts b/src/debug-tracker.ts index ef31f3a..d451c73 100644 --- a/src/debug-tracker.ts +++ b/src/debug-tracker.ts @@ -1,109 +1,37 @@ -import * as vscode from 'vscode'; -import * as manifest from './manifest'; -import { parseStringPromise } from 'xml2js'; -import { PeripheralTree } from './peripheral-tree'; +/** + * Copyright (C) 2023 Arm Limited + */ -const CORTEX_EXTENSION = 'marus25.cortex-debug'; +import * as vscode from 'vscode'; -const pathToUri = (path: string): vscode.Uri => { - try { - return vscode.Uri.file(path); - } catch (e) { - return vscode.Uri.parse(path); +export class DebugTracker { + public constructor(private debugType = '*') { } -}; -const readFromUrl = async (url: string): Promise => { - // Download using fetch - const response = await fetch(url); - if (!response.ok) { - const body = await response.text(); - const msg = `Request to ${url} failed. Status="${response.status}". Body="${body}".`; - throw new Error(msg); - } + private _onWillStartSession: vscode.EventEmitter = new vscode.EventEmitter(); + public readonly onWillStartSession: vscode.Event = this._onWillStartSession.event; - return response; -}; + private _onWillStopSession: vscode.EventEmitter = new vscode.EventEmitter(); + public readonly onWillStopSession: vscode.Event = this._onWillStopSession.event; -export class DebugTracker { - public constructor(protected tree: PeripheralTree) { - } + private _onDidStopDebug: vscode.EventEmitter = new vscode.EventEmitter(); + public readonly onDidStopDebug: vscode.Event = this._onDidStopDebug.event; public async activate(context: vscode.ExtensionContext): Promise { const createDebugAdapterTracker = (session: vscode.DebugSession): vscode.DebugAdapterTracker => { return { - onWillStartSession: () => this.debugSessionStarted(session), - onWillStopSession: () => this.debugSessionTerminated(session), + onWillStartSession: () => this._onWillStartSession.fire(session), + onWillStopSession: () => this._onWillStopSession.fire(session), onDidSendMessage: message => { if (message.type === 'event' && message.event === 'stopped') { - this.tree.debugStopped(); + this._onDidStopDebug.fire(session); } } }; }; context.subscriptions.push( - vscode.debug.registerDebugAdapterTrackerFactory('*', { createDebugAdapterTracker }) + vscode.debug.registerDebugAdapterTrackerFactory(this.debugType, { createDebugAdapterTracker }) ); } - - protected async debugSessionStarted(session: vscode.DebugSession): Promise { - let svdData: string | undefined; - - const svdConfig = vscode.workspace.getConfiguration(manifest.PACKAGE_NAME).get(manifest.CONFIG_SVD_PATH) || manifest.DEFAULT_SVD_PATH; - let svd = session.configuration[svdConfig]; - - if (!svd) { - // Try loading from device support pack - try { - const deviceConfig = vscode.workspace.getConfiguration(manifest.PACKAGE_NAME).get(manifest.CONFIG_DEVICE) || manifest.DEFAULT_DEVICE; - const device = session.configuration[deviceConfig]; - - if (device) { - const cortexDebug = vscode.extensions.getExtension(CORTEX_EXTENSION); - if (cortexDebug) { - const cdbg = await cortexDebug.activate(); - svd = cdbg.getSVDFile(device); - } - } - } catch(e) { - // eslint-disable-next-line no-console - console.warn(e); - } - } - - if (svd) { - try { - let contents: ArrayBuffer | undefined; - - if (svd.startsWith('http')) { - const response = await readFromUrl(svd); - contents = await response.arrayBuffer(); - } else { - const uri = pathToUri(svd); - contents = await vscode.workspace.fs.readFile(uri); - } - - if (contents) { - const decoder = new TextDecoder(); - const xml = decoder.decode(contents); - svdData = await parseStringPromise(xml); - } - } catch(e) { - // eslint-disable-next-line no-console - console.warn(e); - } - } - - vscode.commands.executeCommand('setContext', `${manifest.PACKAGE_NAME}.svd.hasData`, !!svdData); - - if (svdData) { - this.tree.debugSessionStarted(svdData); - } - } - - protected debugSessionTerminated(_session: vscode.DebugSession): void { - vscode.commands.executeCommand('setContext', `${manifest.PACKAGE_NAME}.svd.hasData`, false); - this.tree.debugSessionTerminated(); - } } diff --git a/src/desktop/extension.ts b/src/desktop/extension.ts index 8c101d7..48cf516 100644 --- a/src/desktop/extension.ts +++ b/src/desktop/extension.ts @@ -1,16 +1,26 @@ +/** + * Copyright (C) 2023 Arm Limited + */ + import * as vscode from 'vscode'; -import { PeripheralTree } from '../peripheral-tree'; -import { SvdCommands } from '../svd-commands'; +import { PeripheralTreeProvider } from '../views/peripheral'; +import { Commands } from '../commands'; import { DebugTracker } from '../debug-tracker'; +import { SvdRegistry } from '../svd-registry'; +import { SvdResolver } from '../svd-resolver'; -export const activate = async (context: vscode.ExtensionContext): Promise => { - const peripheralTree = new PeripheralTree(); - const commands = new SvdCommands(peripheralTree); - const tracker = new DebugTracker(peripheralTree); +export const activate = async (context: vscode.ExtensionContext): Promise => { + const tracker = new DebugTracker(); + const registry = new SvdRegistry(); + const resolver = new SvdResolver(registry); + const peripheralTree = new PeripheralTreeProvider(tracker, resolver); + const commands = new Commands(peripheralTree); + await tracker.activate(context); await peripheralTree.activate(context); await commands.activate(context); - await tracker.activate(context); + + return registry; }; export const deactivate = async (): Promise => { diff --git a/src/manifest.ts b/src/manifest.ts index 738a05b..c724934 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -1,5 +1,15 @@ +/** + * Copyright (C) 2023 Arm Limited + */ + export const PACKAGE_NAME = 'svd-viewer'; export const CONFIG_SVD_PATH = 'svdPathConfig'; export const DEFAULT_SVD_PATH = 'svdPath'; export const CONFIG_DEVICE = 'deviceConfig'; -export const DEFAULT_DEVICE = 'device'; +export const DEFAULT_DEVICE = 'deviceName'; +export const CONFIG_PROCESSOR = 'processorConfig'; +export const DEFAULT_PROCESSOR = 'processorName'; +export const CONFIG_ADDRGAP = 'svdAddrGapThreshold'; +export const DEFAULT_ADDRGAP = 16; +export const CONFIG_ASSET_PATH = 'packAssetUrl'; +export const DEFAULT_ASSET_PATH = 'https://pack-asset-service.keil.arm.com'; diff --git a/src/memreadutils.ts b/src/memreadutils.ts new file mode 100644 index 0000000..163e5a0 --- /dev/null +++ b/src/memreadutils.ts @@ -0,0 +1,109 @@ +/* + * Copyright 2017-2019 Marcel Ball + * https://github.com/Marus/cortex-debug + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + * TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import * as vscode from 'vscode'; +import type { DebugProtocol } from 'vscode-debugprotocol'; +import { AddrRange, AddressRangesUtils } from './addrranges'; + +/** Has utility functions to read memory in chunks into a storage space */ +export class MemReadUtils { + /** + * Make one or more memory reads and update values. For the caller, it should look like a single + * memory read but, if one read fails, all reads are considered as failed. + * + * @param startAddr The start address of the memory region. Everything else is relative to `startAddr` + * @param specs The chunks of memory to read and and update. Addresses should be >= `startAddr`, Can have gaps, overlaps, etc. + * @param storeTo This is where read-results go. The first element represents item at `startAddr` + */ + public static async readMemoryChunks( + session: vscode.DebugSession, startAddr: number, specs: AddrRange[], storeTo: number[]): Promise { + const promises = specs.map(async r => { + try { + const memoryReference = '0x' + r.base.toString(16); + const request: DebugProtocol.ReadMemoryArguments = { + memoryReference, + count: r.length + }; + + const response: Partial = {}; + response.body = await session.customRequest('readMemory', request); + + if (response.body && response.body.data) { + const bytes = Buffer.from(response.body.data, 'base64'); + let dst = r.base - startAddr; + for (const byte of bytes) { + storeTo[dst++] = byte; + } + } + + return true; + } catch(e) { + let dst = r.base - startAddr; + // tslint:disable-next-line: prefer-for-of + for (let ix = 0; ix < r.length; ix++) { + storeTo[dst++] = 0xff; + } + + throw (e); + } + }); + + const results = await Promise.all(promises.map((p) => p.catch((e) => e))); + const errs: string[] = []; + results.map((e) => { + if (e instanceof Error) { + errs.push(e.message); + } + }); + + if (errs.length !== 0) { + throw new Error(errs.join('\n')); + } + + return true; + } + + public static readMemory(session: vscode.DebugSession, startAddr: number, length: number, storeTo: number[]): Promise { + const maxChunk = (4 * 1024); + const ranges = AddressRangesUtils.splitIntoChunks([new AddrRange(startAddr, length)], maxChunk); + return MemReadUtils.readMemoryChunks(session, startAddr, ranges, storeTo); + } + + public static async writeMemory(session: vscode.DebugSession, startAddr: number, value: number, length: number): Promise { + const memoryReference = '0x' + startAddr.toString(16); + const bytes: string[] = []; + const numbytes = length / 8; + + for (let i = 0; i < numbytes; i++) { + const byte = value & 0xFF; + value = value >>> 8; + let bs = byte.toString(16); + if (bs.length === 1) { bs = '0' + bs; } + bytes[i] = bs; + } + + const data = Buffer.from(bytes).toString('base64'); + const request: DebugProtocol.WriteMemoryArguments = { + memoryReference, + data + }; + + await session.customRequest('writeMemory', request); + return true; + } +} diff --git a/src/nodes/address-ranges.ts b/src/nodes/address-ranges.ts deleted file mode 100644 index 4db533f..0000000 --- a/src/nodes/address-ranges.ts +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright 2017-2019 Marcel Ball - * https://github.com/Marus/cortex-debug - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated - * documentation files (the "Software"), to deal in the Software without restriction, including without - * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED - * TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF - * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - * - * This file contains classes to create address ranges that are in use in an address space. - * - * Implementation: (can change at a later date) - * We use a bitset implementation to handle spaces in the range of small megabytes. We could - * also have used an interval tree (Red-Black) but too much work. - * - * With a bit-set, it is a mark and scan method. Each bit in the bitset represents a byte. - * Mark each byte used which is O(1), then scan the space O(N) where N is [size of address space] - * but we can skip in 32/8/4 byte chunks of emptyness easily. Hence a bitset. - * - * Use case here is to calculate used addr-ranges. As a user you can decide what 1-bit represents - */ - -import { FixedBitSet } from './fixed-bit-set'; - -/** Represents a single address-range */ -export class AddrRange { - constructor(public base: number, public length: number) { - } - - /** return next address after this addr. range */ - public nxtAddr(): number { - return this.base + this.length; - } - - /** return last address in this range */ - public endAddr(): number { - return this.nxtAddr() - 1; - } -} - -/** - * Register each byte of an address used, this class can calculate a set of - * address ranges that are in use - */ -export class AddressRangesInUse { - // We could have derived from bitSet but want to be able to change implementation - protected bitSet: FixedBitSet; - - constructor(len: number) { - this.bitSet = new FixedBitSet(len); - } - - public get length(): number { - return this.bitSet.numBits; - } - - public setAddrRange(offset: number, length = 4): void { - if ((offset & 0x3) || (length & 0x3)) { - // either offset or length not a multiple of 4 - for (let ix = 0; ix < length; ix++) { - this.bitSet.setBit(ix + offset); - } - } else { - while (length > 0) { - this.setWord(offset); - offset += 4; - length -= 4; - } - } - } - - public setWord(offset: number): void { - this.bitSet.setNibble(offset); - } - - /** - * Calculates a set of consecutive words/bytes that contain valid addresses. - * - * @param base all the return values will have this base address added to them - * @param aligned if true, we look for 4 byte chunks or it is byte at a time - * @returns an array of ordered unique address ranges containing valid addresses. Can be an empty array - */ - public getAddressRangesExact(base: number, aligned = false): AddrRange[] { - const retVal: AddrRange[] = []; - const incr = aligned ? 4 : 1; - let nxtIx = -1; // init to an impossible value - let range: AddrRange | undefined; - - function gotOne(ix: number): boolean { - if (nxtIx !== ix) { - range = new AddrRange(base + ix, incr); - retVal.push(range); - } else { // continuation of prev. range - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - range!.length += incr; // range! because it can't be null, lint will complain - } - nxtIx = ix + incr; // Got a hit, start watching for adjacents - return true; - } - - if (aligned) { - this.bitSet.findNibbleItor(gotOne); - } else { - this.bitSet.findBitItor(gotOne); - } - return retVal; - } - - /** - * Calculates a set of ranges that contain valid address ranges and eliminates small gaps - * to combine ranges and a fewer set of ranges - * - * @param base all the return values will havd this base address added to them - * @param aligned if true, we look for 4 byte chunks or it is byte at a time - * @param minGap gaps less than specified number of bytes will be merged in multiple of 8 - * @returns an array of ordered compressed address ranges. Can be an empty array - */ - public getAddressRangesOptimized(base: number, aligned = false, minGap = 8): AddrRange[] { - const exactVals = this.getAddressRangesExact(base, aligned); - if ((minGap <= 0) || (exactVals.length < 2)) { - return exactVals; - } - - const retVal = []; - let lastRange: AddrRange | undefined; - if (aligned) { - minGap = (minGap + 7) & ~7; // Make it a multiple of 8 rounding up - } - for (const nxtRange of exactVals) { - if (lastRange && ((lastRange.nxtAddr() + minGap) >= nxtRange.base)) { - lastRange.length = nxtRange.base - lastRange.base + nxtRange.length; - } else { - retVal.push(nxtRange); - lastRange = nxtRange; - } - } - - return retVal; - } - - public toHexString(): string { - return this.bitSet.toHexString(); - } - - public clearAll(): void { - this.bitSet.clearAll(); - } - - /** - * Returns a set of address ranges that have 0 < length <= maxBytes - * - * @param ranges array of ranges to check an split - * @param maxBytes limit of each range - * @param dbgMsg To output debug messages -- name of address space - * @param dbgLen To output debug messages -- total length of addr space - */ - public static splitIntoChunks(ranges: AddrRange[], maxBytes: number): AddrRange[] { - const newRanges = new Array(); - for (const r of ranges) { - while (r.length > maxBytes) { - newRanges.push(new AddrRange(r.base, maxBytes)); - r.base += maxBytes; - r.length -= maxBytes; - } - if (r.length > 0) { // Watch out, can be negative - newRanges.push(r); - } - } - return newRanges; - } -} diff --git a/src/nodes/fixed-bit-set.ts b/src/nodes/fixed-bit-set.ts deleted file mode 100644 index 4c7ab93..0000000 --- a/src/nodes/fixed-bit-set.ts +++ /dev/null @@ -1,254 +0,0 @@ -/* - * Copyright 2017-2019 Marcel Ball - * https://github.com/Marus/cortex-debug - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated - * documentation files (the "Software"), to deal in the Software without restriction, including without - * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED - * TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF - * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - * - * Author to Blame: haneefdm on github - */ - -/** - * Helpful constants. We use 32bit quantities, they are safe in JavaScript. - * Do not use anything larger because we use many bitwise operators and they all - * operate in 32-bit quantities - */ -const enum BitSetConsts { - // We define them here in an ugly way to guarantee constant folding. - // Let me know if there is a better way. Things like const, - // etc. are syntactic sugar -- take a look at generated code. - SHFT = 5, - NBITS = (1 << SHFT), - MASK = (NBITS - 1) -} - -/** - * A simple implementation of a bitset of fixed size. You can make it dynamic by - * extending this class and overriding the setters (setBit, clrBit and invBit) to - * auto-resize. A resize function is provided. - * - * You can use it store flags in a very compact form and if your data is sparse, then you - * can iterate over them faster. Gigantic space, yet super-sparse, consider a balanced tree - * or a set/map/something-else - * - * Get/Set is O(1) - * Traversal is O(N) => N = length of the array, but able to skip large empty spaces - * - * It is also a very efficient way of doing unions/intersections using bitwise operations. - */ -export class FixedBitSet { - protected bitArray: Uint32Array ; - protected xnumBits: number; - - public get numBits(): number { - return this.xnumBits; - } - - // Enable this to do error checking. Maybe there is a better waay, to remove assert overhead - public static doAsserts = false; - - constructor(len: number) { - if (FixedBitSet.doAsserts) { - // console.assert(Number.isInteger(len) && (len >= 0)); - } - this.xnumBits = len; - this.bitArray = new Uint32Array(FixedBitSet.calcAryLen(len)); - } - - public dup(): FixedBitSet { - const ret = new FixedBitSet(this.numBits); - ret.bitArray.set(this.bitArray); - return ret; - } - - protected ixRangeCheck(ix: number): boolean { - return Number.isInteger(ix) && (ix >= 0) && (ix < this.numBits); - } - - /** - * Get bit at specified index - * @return a number that is either zero or non-zero - */ - public getBit(ix: number): number { - if (FixedBitSet.doAsserts) { - // console.assert(this.ixRangeCheck(ix), 'getBit: invalid index ', ix, this); - } - return this.bitArray[ix >>> BitSetConsts.SHFT] & (1 << (ix & BitSetConsts.MASK)) ; - } - - /** Sets the bit at index 'ix' to 1 */ - public setBit(ix: number): void { - if (FixedBitSet.doAsserts) { - // console.assert(this.ixRangeCheck(ix), 'setBit: invalid index ', ix, this); - } - this.bitArray[ix >>> BitSetConsts.SHFT] |= (1 << (ix & BitSetConsts.MASK)) ; - } - - /** Sets the bit at index 'ix' to 0 */ - public clrBit(ix: number): void { - if (FixedBitSet.doAsserts) { - // console.assert(this.ixRangeCheck(ix), 'clrBit: invalid index ', ix, this); - } - this.bitArray[ix >>> BitSetConsts.SHFT] &= ~(1 << (ix & BitSetConsts.MASK)) ; - } - - /** Inverts the bit at index 'ix' to 0 */ - public invBit(ix: number): void { - if (FixedBitSet.doAsserts) { - // console.assert(this.ixRangeCheck(ix), 'invBit: invalid index ', ix, this); - } - this.bitArray[ix >>> BitSetConsts.SHFT] ^= (1 << (ix & BitSetConsts.MASK)) ; - } - - /** clears all bits */ - public clearAll(): void { - this.bitArray.fill(0); - } - - /** Sets a set of four consecutive bits - * @param ix: Must a multiple of four and in range - */ - public setNibble(ix: number): void { - if (FixedBitSet.doAsserts) { - // console.assert(this.ixRangeCheck(ix + 3), 'setNibble: invalid index ', ix, this); - // console.assert((ix & 0x3) === 0, 'setNibble: ix must be >= 0 & multiple of 4'); - } - this.bitArray[ix >>> BitSetConsts.SHFT] |= ((0xf << (ix & BitSetConsts.MASK)) >>> 0); - } - - public toString(): string { - return this.bitArray.toString(); - } - - /** - * Iterator built for efficiency. No guarantees if you modify this object - * while iterating (especially a resize) - * - * @param cb: a function called with the next bit position that is non-zero. If - * callback returns false, iterator will terminate - */ - public findBitItor(cb: (ix: number) => boolean): void { - // Could have used an actual Iterator interface but we have to keep too much - // state to make a next() work properly. - let bitIx = 0; - let aryIx = 0; - while (bitIx < this.xnumBits) { - let elem = this.bitArray[aryIx++]; - if (elem === 0) { - bitIx += BitSetConsts.NBITS; - continue; - } - for (let byteIx = 0; (byteIx < (BitSetConsts.NBITS / 8)) && (bitIx < this.numBits); byteIx++) { - const byteVal = elem & 0xff; - elem >>>= 8; - if (byteVal === 0) { // Try to skip byte at a time - bitIx += 8; - continue; - } - // We do not bail early or skip bits to keep bitIx updated - for (let bitPos = 1; (bitPos < (1 << 8)) && (bitIx < this.numBits); bitPos <<= 1) { - if (byteVal & bitPos) { - if (!cb(bitIx)) { return; } - } - bitIx++; - } - } - } - } - - /** Return an array of indices where a bit positions are non-zero */ - public findAllBits(): number[] { - const ret: number[] = []; - this.findBitItor((ix): boolean => { - ret.push(ix); - return true; - }); - return ret; - } - - /** - * Iterator built for efficiency. No guarantees if you modify this object - * while iterating (especially a resize). It scans four bits at a time. - * We don't check if the entire nibble is set - any bit in the nibble being set - * - * @param cb: a function called with the next nibble position that is non-zero. If - * callback returns false, iterator will terminate - */ - public findNibbleItor(cb: (ix: number) => boolean): void { - let addr = 0; - const stop = this.bitArray.length; - for (let ix = 0; ix < stop; ix++) { - let val = this.bitArray[ix]; - if (val !== 0) { - for (let bits = 0; bits < BitSetConsts.NBITS; bits += 4) { - if ((0xf & val) !== 0) { // got something - if (addr < this.numBits) { - if (!cb(addr)) { return; } - } else { - // console.assert(false, 'Defect in FixedBitset. Not expecting a value in trailing bits'); - } - } - addr += 4; - val >>>= 4; - } - } else { - addr += BitSetConsts.NBITS; - } - } - } - - /** - * Not sure what this looks like on a big endian machine. We can correct for that - * if needed. Expecting this to be used mostly for debugging. Note that the nibbles - * are also backards in each byte. One char represents a nibble. - */ - public toHexString(): string { - const buf = Buffer.from(this.bitArray.buffer); - const str = buf.toString('hex'); - return str; - } - - /** resizes the number of bits -- not yet tested */ - public reSize(len: number): void { - if (FixedBitSet.doAsserts) { - // console.assert(Number.isInteger(len) && (len >= 0)); - } - if (len <= 0) { - this.xnumBits = 0; - this.bitArray = new Uint32Array(0); - } else if (len !== this.xnumBits) { - const numUnits = FixedBitSet.calcAryLen(len); - let newAry: Uint32Array; - if (numUnits <= this.bitArray.length) { - newAry = this.bitArray.subarray(0, numUnits); - const diff = (numUnits * BitSetConsts.NBITS) - len; - if (diff > 0) { // clear any traiiing bits in most sig. portion - // We HAVE to clear trailing bits in case the nibble iterator is being used - const mask = (0xffffffff << (BitSetConsts.NBITS - diff)) >>> 0; - newAry[numUnits - 1] &= mask; - } - } else { - newAry = new Uint32Array(numUnits); - newAry.set(this.bitArray); - } - this.xnumBits = len; - this.bitArray = newAry; - } - } - - /** Basically does a Math.ceil(len / NBITS) using integer ops. */ - protected static calcAryLen(len: number): number { - const ret = (len <= 0) ? 0 : ((len + BitSetConsts.MASK) >>> BitSetConsts.SHFT); - return ret; - } -} diff --git a/src/peripheral-tree.ts b/src/peripheral-tree.ts deleted file mode 100644 index 0b594c0..0000000 --- a/src/peripheral-tree.ts +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2017-2019 Marcel Ball - * https://github.com/Marus/cortex-debug - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated - * documentation files (the "Software"), to deal in the Software without restriction, including without - * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED - * TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF - * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import * as vscode from 'vscode'; -import * as manifest from './manifest'; -import { PeripheralBaseNode } from './nodes/base-node'; -import { PeripheralNode } from './nodes/peripheral-node'; -import { SVDParser } from './svd-parser'; - -export class PeripheralTree implements vscode.TreeDataProvider { - public _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); - public readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; - - private peripherials: PeripheralNode[] = []; - private loaded = false; - - public async activate(context: vscode.ExtensionContext): Promise { - const view = vscode.window.createTreeView(`${manifest.PACKAGE_NAME}.svd`, { treeDataProvider: this }); - context.subscriptions.push( - view, - view.onDidExpandElement((e) => { - e.element.expanded = true; - const p = e.element.getPeripheral(); - if (p) { - p.updateData(); - this.refresh(); - } - }), - view.onDidCollapseElement((e) => { - e.element.expanded = false; - }) - ); - } - - private async loadSVD(SVDData: string): Promise { - const peripherals = await SVDParser.parseSVD(SVDData); - this.peripherials = peripherals; - this.loaded = true; - } - - public refresh(): void { - this._onDidChangeTreeData.fire(undefined); - } - - public getTreeItem(element: PeripheralBaseNode): vscode.TreeItem | Promise { - return element.getTreeItem(); - } - - public getChildren(element?: PeripheralBaseNode): vscode.ProviderResult { - if (this.loaded && this.peripherials.length > 0) { - return element ? element.getChildren() : this.peripherials; - } - - return []; - } - - public async debugSessionStarted(svdData: string): Promise { - this.peripherials = []; - this.loaded = false; - this._onDidChangeTreeData.fire(undefined); - - if (svdData) { - try { - await this.loadSVD(svdData); - this._onDidChangeTreeData.fire(undefined); - } catch (e) { - this.peripherials = []; - this.loaded = false; - this._onDidChangeTreeData.fire(undefined); - const msg = `Unable to parse SVD file: ${e.toString()}`; - vscode.window.showErrorMessage(msg); - if (vscode.debug.activeDebugConsole) { - vscode.debug.activeDebugConsole.appendLine(msg); - } - } - } - } - - public debugSessionTerminated(): void { - this.peripherials = []; - this.loaded = false; - this._onDidChangeTreeData.fire(undefined); - } - - public async debugStopped(): Promise { - if (this.loaded) { - try { - const promises = this.peripherials.map((p) => p.updateData()); - await Promise.all(promises); - } finally { - this._onDidChangeTreeData.fire(undefined); - } - } - } - - public togglePinPeripheral(node: PeripheralBaseNode): void { - node.pinned = !node.pinned; - this.peripherials.sort(PeripheralNode.compare); - } -} diff --git a/src/svd-commands.ts b/src/svd-commands.ts deleted file mode 100644 index 07119a8..0000000 --- a/src/svd-commands.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2017-2019 Marcel Ball - * https://github.com/Marus/cortex-debug - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated - * documentation files (the "Software"), to deal in the Software without restriction, including without - * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED - * TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF - * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import * as vscode from 'vscode'; -import * as manifest from './manifest'; -import { PeripheralBaseNode } from './nodes/base-node'; -import { PeripheralTree } from './peripheral-tree'; -import { NumberFormat } from './util'; - -export class SvdCommands { - public constructor(protected tree: PeripheralTree) { - } - - public async activate(context: vscode.ExtensionContext): Promise { - context.subscriptions.push( - vscode.commands.registerCommand(`${manifest.PACKAGE_NAME}.svd.setValue`, async node => { - try { - const result = await node.performUpdate(); - if (result) { - this.refresh(node); - } - } catch (error) { - vscode.window.showErrorMessage(`Unable to update value: ${error.toString()}`); - } - }), - vscode.commands.registerCommand(`${manifest.PACKAGE_NAME}.svd.copyValue`, node => { - const cv = node.getCopyValue(); - if (cv) { - vscode.env.clipboard.writeText(cv); - } - }), - vscode.commands.registerCommand(`${manifest.PACKAGE_NAME}.svd.refreshValue`, node => this.refresh(node)), - vscode.commands.registerCommand(`${manifest.PACKAGE_NAME}.svd.pin`, node => this.toggle(node)), - vscode.commands.registerCommand(`${manifest.PACKAGE_NAME}.svd.unpin`, node => this.toggle(node)), - vscode.commands.registerCommand(`${manifest.PACKAGE_NAME}.svd.setFormat`, async node => { - const result = await vscode.window.showQuickPick([ - { label: 'Auto', description: 'Automatically choose format (Inherits from parent)', value: NumberFormat.Auto }, - { label: 'Hex', description: 'Format value in hexidecimal', value: NumberFormat.Hexidecimal }, - { label: 'Decimal', description: 'Format value in decimal', value: NumberFormat.Decimal }, - { label: 'Binary', description: 'Format value in binary', value: NumberFormat.Binary } - ]); - if (result === undefined) - return; - - node.format = result.value; - this.tree.refresh(); - }), - ); - } - - protected async refresh(node: PeripheralBaseNode): Promise { - const p = node.getPeripheral(); - if (p) { - await p.updateData(); - this.tree.refresh(); - } - } - - protected toggle(node: PeripheralBaseNode): void { - this.tree.togglePinPeripheral(node); - this.tree.refresh(); - } -} diff --git a/src/svd-parser.ts b/src/svd-parser.ts index 40c4491..b6b6802 100644 --- a/src/svd-parser.ts +++ b/src/svd-parser.ts @@ -16,14 +16,21 @@ * IN THE SOFTWARE. */ -import { PeripheralRegisterNode } from './nodes/register-node'; -import { PeripheralClusterNode } from './nodes/cluster-node'; -import { PeripheralNode } from './nodes/peripheral-node'; -import { PeripheralFieldNode, EnumerationMap, EnumeratedValue, FieldOptions } from './nodes/field-node'; -import { parseInteger, parseDimIndex, AccessType } from './util'; - /* eslint-disable @typescript-eslint/no-explicit-any */ +import * as vscode from 'vscode'; +import { PeripheralRegisterNode } from './views/nodes/peripheralregisternode'; +import { PeripheralClusterNode, PeripheralOrClusterNode } from './views/nodes/peripheralclusternode'; +import { PeripheralFieldNode, EnumerationMap, EnumeratedValue } from './views/nodes/peripheralfieldnode'; +import { PeripheralNode } from './views/nodes/peripheralnode'; +import { parseInteger, parseDimIndex } from './utils'; + +export enum AccessType { + ReadOnly = 1, + ReadWrite, + WriteOnly +} + const accessTypeFromString = (type: string): AccessType => { switch (type) { case 'write-only': @@ -41,14 +48,34 @@ const accessTypeFromString = (type: string): AccessType => { } }; +export interface Peripheral { + name: string[]; +} + +export interface Device { + resetValue: string[]; + size: string[]; + access: string[]; + peripherals: { + peripheral: Peripheral[] + }[]; +} + +export interface SvdData { + device: Device; +} + export class SVDParser { private static enumTypeValuesMap: { [key: string]: any } = {}; private static peripheralRegisterMap: { [key: string]: any } = {}; + private static gapThreshold: number; - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - public static async parseSVD(data: any): Promise { + public static async parseSVD( + session: vscode.DebugSession, svdData: SvdData, gapThreshold: number): Promise { + SVDParser.gapThreshold = gapThreshold; SVDParser.enumTypeValuesMap = {}; SVDParser.peripheralRegisterMap = {}; + const peripheralMap: { [key: string]: any } = {}; const defaultOptions = { accessType: AccessType.ReadWrite, @@ -56,21 +83,22 @@ export class SVDParser { resetValue: 0x0 }; - if (data.device.resetValue) { - defaultOptions.resetValue = parseInteger(data.device.resetValue[0]) || 0; + if (svdData.device.resetValue) { + defaultOptions.resetValue = parseInteger(svdData.device.resetValue[0]) || 0; } - if (data.device.size) { - defaultOptions.size = parseInteger(data.device.size[0]) || 0; + if (svdData.device.size) { + defaultOptions.size = parseInteger(svdData.device.size[0]) || 0; } - if (data.device.access) { - defaultOptions.accessType = accessTypeFromString(data.device.access[0]); + if (svdData.device.access) { + defaultOptions.accessType = accessTypeFromString(svdData.device.access[0]); } - data.device.peripherals[0].peripheral.forEach((element: any) => { + svdData.device.peripherals[0].peripheral.forEach((element) => { const name = element.name[0]; peripheralMap[name] = element; }); + // tslint:disable-next-line:forin for (const key in peripheralMap) { const element = peripheralMap[key]; if (element.$ && element.$.derivedFrom) { @@ -80,14 +108,15 @@ export class SVDParser { } const peripherials = []; + // tslint:disable-next-line:forin for (const key in peripheralMap) { - peripherials.push(SVDParser.parsePeripheral(peripheralMap[key])); + peripherials.push(SVDParser.parsePeripheral(session, peripheralMap[key], defaultOptions)); } peripherials.sort(PeripheralNode.compare); for (const p of peripherials) { - p.markAddresses(); + p.collectRanges(); } return peripherials; @@ -100,7 +129,7 @@ export class SVDParser { private static parseFields(fieldInfo: any[], parent: PeripheralRegisterNode): PeripheralFieldNode[] { const fields: PeripheralFieldNode[] = []; - if (fieldInfo === undefined) { + if (fieldInfo == null) { return fields; } @@ -132,6 +161,7 @@ export class SVDParser { offset = lsb; } } else { + // tslint:disable-next-line:max-line-length throw new Error(`Unable to parse SVD file: field ${f.name[0]} must have either bitOffset and bitWidth elements, bitRange Element, or msb and lsb elements.`); } @@ -139,51 +169,53 @@ export class SVDParser { if (f.enumeratedValues) { valueMap = {}; const eValues = f.enumeratedValues[0]; - if (eValues) { - if (eValues.$ && eValues.$.derivedFrom) { - const found = SVDParser.enumTypeValuesMap[eValues.$.derivedFrom]; - if (!found) { - throw new Error(`Invalid derivedFrom=${eValues.$.derivedFrom} for enumeratedValues of field ${f.name[0]}`); - } - valueMap = found; - } else { - if (eValues.enumeratedValue) { - eValues.enumeratedValue.map((ev: any) => { - if (ev.value && ev.value.length > 0) { - const evname = ev.name[0]; - const evdesc = this.cleanupDescription(ev.description ? ev.description[0] : ''); - const val = ev.value[0].toLowerCase(); - const evvalue = parseInteger(val); - - if (valueMap && evvalue) { - valueMap[evvalue] = new EnumeratedValue(evname, evdesc, evvalue); - } + if (eValues.$ && eValues.$.derivedFrom) { + const found = SVDParser.enumTypeValuesMap[eValues.$.derivedFrom]; + if (!found) { + throw new Error(`Invalid derivedFrom=${eValues.$.derivedFrom} for enumeratedValues of field ${f.name[0]}`); + } + valueMap = found; + } else if (eValues) { + if (eValues.enumeratedValue) { + eValues.enumeratedValue.map((ev: any) => { + if (ev.value && ev.value.length > 0) { + const evname = ev.name[0]; + const evdesc = this.cleanupDescription(ev.description ? ev.description[0] : ''); + const val = ev.value[0].toLowerCase(); + const evvalue = parseInteger(val); + + if (valueMap && evvalue) { + valueMap[evvalue] = new EnumeratedValue(evname, evdesc, evvalue); } - }); - } - - // According to the SVD spec/schema, I am not sure any scope applies. Seems like everything is in a global name space - // No make sense but how I am interpreting it for now. Easy to make it scope based but then why allow referencing - // other peripherals. Global scope it is. Overrides dups from previous definitions!!! - if (eValues.name && eValues.name[0]) { - let evName = eValues.name[0]; - for (const prefix of [undefined, f.name[0], parent.name, parent.parent.name]) { - evName = prefix ? prefix + '.' + evName : evName; - SVDParser.enumTypeValuesMap[evName] = valueMap; } + }); + } + + // According to the SVD spec/schema, I am not sure any scope applies. Seems like everything is in a global name space + // No make sense but how I am interpreting it for now. Easy to make it scope based but then why allow referencing + // other peripherals. Global scope it is. Overrides dups from previous definitions!!! + if (eValues.name && eValues.name[0]) { + let evName = eValues.name[0]; + for (const prefix of [null, f.name[0], parent.name, parent.parent.name]) { + evName = prefix ? prefix + '.' + evName : evName; + SVDParser.enumTypeValuesMap[evName] = valueMap; } } } } - const baseOptions: FieldOptions = { + const baseOptions: any = { name: f.name[0], description: description, - offset: offset || 0, - width: width || 0, + offset: offset, + width: width, enumeration: valueMap }; + if (f.access) { + baseOptions.accessType = accessTypeFromString(f.access[0]); + } + if (f.dim) { if (!f.dimIncrement) { throw new Error(`Unable to parse SVD file: field ${f.name[0]} has dim element, with no dimIncrement element.`); } @@ -215,8 +247,8 @@ export class SVDParser { return fields; } - private static parseRegisters(regInfo_: any[], parent: PeripheralNode | PeripheralClusterNode): PeripheralRegisterNode[] { - const regInfo = [...regInfo_]; // Make a shallow copy,. we will work on this + private static parseRegisters(regInfoOrig: any[], parent: PeripheralNode | PeripheralClusterNode): PeripheralRegisterNode[] { + const regInfo = [...regInfoOrig]; // Make a shallow copy,. we will work on this const registers: PeripheralRegisterNode[] = []; const localRegisterMap: { [key: string]: any } = {}; @@ -313,13 +345,19 @@ export class SVDParser { } registers.sort((a, b) => { - if (a.offset < b.offset) { return -1; } else if (a.offset > b.offset) { return 1; } else { return 0; } + if (a.offset < b.offset) { + return -1; + } else if (a.offset > b.offset) { + return 1; + } else { + return 0; + } }); return registers; } - private static parseClusters(clusterInfo: any, parent: PeripheralNode): PeripheralClusterNode[] { + private static parseClusters(clusterInfo: any, parent: PeripheralOrClusterNode): PeripheralClusterNode[] { const clusters: PeripheralClusterNode[] = []; if (!clusterInfo) { return []; } @@ -367,6 +405,9 @@ export class SVDParser { if (c.register) { SVDParser.parseRegisters(c.register, cluster); } + if (c.cluster) { + SVDParser.parseClusters(c.cluster, cluster); + } clusters.push(cluster); } } @@ -383,14 +424,17 @@ export class SVDParser { SVDParser.parseRegisters(c.register, cluster); clusters.push(cluster); } + if (c.cluster) { + SVDParser.parseClusters(c.cluster, cluster); + clusters.push(cluster); + } } - }); return clusters; } - private static parsePeripheral(p: any): PeripheralNode { + private static parsePeripheral(session: vscode.DebugSession, p: any, _defaults: { accessType: AccessType, size: number, resetValue: number }): PeripheralNode { let totalLength = 0; if (p.addressBlock) { for (const ab of p.addressBlock) { @@ -414,7 +458,7 @@ export class SVDParser { if (p.resetValue) { options.resetValue = parseInteger(p.resetValue[0]); } if (p.groupName) { options.groupName = p.groupName[0]; } - const peripheral = new PeripheralNode(options); + const peripheral = new PeripheralNode(session, SVDParser.gapThreshold, options); if (p.registers) { if (p.registers[0].register) { diff --git a/src/svd-registry.ts b/src/svd-registry.ts new file mode 100644 index 0000000..734230b --- /dev/null +++ b/src/svd-registry.ts @@ -0,0 +1,64 @@ +/* + * Copyright 2017-2019 Marcel Ball + * https://github.com/Marus/cortex-debug + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + * TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import * as vscode from 'vscode'; + +const CORTEX_EXTENSION = 'marus25.cortex-debug'; + +interface SVDInfo { + expression: RegExp; + path: string; +} + +export class SvdRegistry { + private SVDDirectory: SVDInfo[] = []; + + public registerSVDFile(expression: RegExp | string, path: string): void { + if (typeof expression === 'string') { + expression = new RegExp(`^${expression}$`, ''); + } + + this.SVDDirectory.push({ expression: expression, path: path }); + } + + public getSVDFile(device: string): string | undefined { + // Try loading from device support pack registered with this extension + const entry = this.SVDDirectory.find((de) => de.expression.test(device)); + if (entry) { + return entry.path; + } + + return undefined; + } + + public async getSVDFileFromCortexDebug(device: string): Promise { + // Try loading from device support pack registered with this extension + const cortexDebug = vscode.extensions.getExtension(CORTEX_EXTENSION); + if (cortexDebug) { + const cdbg = await cortexDebug.activate(); + if (cdbg) { + const entry = cdbg.getSVDFile(device); + if (entry) { + return entry.path; + } + } + } + + return undefined; + } +} diff --git a/src/svd-resolver.ts b/src/svd-resolver.ts new file mode 100644 index 0000000..b833806 --- /dev/null +++ b/src/svd-resolver.ts @@ -0,0 +1,141 @@ +/** + * Copyright (C) 2023 Arm Limited + */ + +import * as vscode from 'vscode'; +import * as manifest from './manifest'; +import { isAbsolute, join, normalize } from 'path'; +import { parseStringPromise } from 'xml2js'; +import { SvdRegistry } from './svd-registry'; +import { parsePackString, pdscFromPack, fileFromPack, Pack } from './cmsis-pack/pack-utils'; +import { PDSC, Device, DeviceVariant, getDevices, getSvdPath, getProcessors } from './cmsis-pack/pdsc'; +import { readFromUrl } from './utils'; +import { getSelection } from './vscode-utils'; + +export class SvdResolver { + public constructor(protected registry: SvdRegistry) { + } + + public async resolve(session: vscode.DebugSession, wsFolderPath?: vscode.Uri): Promise { + const svdConfig = vscode.workspace.getConfiguration(manifest.PACKAGE_NAME).get(manifest.CONFIG_SVD_PATH) || manifest.DEFAULT_SVD_PATH; + let svdPath = session.configuration[svdConfig]; + + const deviceConfig = vscode.workspace.getConfiguration(manifest.PACKAGE_NAME).get(manifest.CONFIG_DEVICE) || manifest.DEFAULT_DEVICE; + const deviceName = session.configuration[deviceConfig]; + + const processorConfig = vscode.workspace.getConfiguration(manifest.PACKAGE_NAME).get(manifest.CONFIG_PROCESSOR) || manifest.DEFAULT_PROCESSOR; + const processorName = session.configuration[processorConfig]; + + if (!svdPath && !deviceName) { + return undefined; + } + + try { + if (svdPath) { + const pack = parsePackString(svdPath); + + if (pack) { + svdPath = await this.loadFromPack(pack, deviceName, processorName); + } else if (vscode.env.uiKind === vscode.UIKind.Desktop && !svdPath.startsWith('http')) { + // On desktop, ensure full path + if (!isAbsolute(svdPath) && wsFolderPath) { + svdPath = normalize(join(wsFolderPath.fsPath, svdPath)); + } + } + } else if (deviceName) { + svdPath = this.registry.getSVDFile(deviceName); + if (!svdPath) { + svdPath = await this.registry.getSVDFileFromCortexDebug(deviceName); + } + } + } catch(e) { + // eslint-disable-next-line no-console + console.warn(e); + } + + return svdPath; + } + + protected async loadFromPack(pack: Pack, deviceName: string | undefined, processorName: string | undefined): Promise { + const getDeviceName = (device: Device) => (device as DeviceVariant).$.Dvariant || device.$.Dname; + + const assetBase = vscode.workspace.getConfiguration(manifest.PACKAGE_NAME).get(manifest.CONFIG_ASSET_PATH) || manifest.DEFAULT_ASSET_PATH; + const pdscPath = pdscFromPack(assetBase, pack); + const pdscBuffer = await readFromUrl(pdscPath.toString()); + + if (!pdscBuffer) { + throw new Error(`No data loaded from ${pdscPath.toString()}`); + } + + const decoder = new TextDecoder(); + const pdscString = decoder.decode(pdscBuffer); + + const pdsc = await parseStringPromise(pdscString, { + explicitCharkey: true + }) as PDSC; + + // Load devices from pack + const devices = getDevices(pdsc); + const deviceMap = new Map(); + for (const device of devices) { + deviceMap.set(getDeviceName(device), device); + } + + // Select device + let packDevice: Device | undefined; + + if (deviceName && deviceMap.has(deviceName)) { + packDevice = deviceMap.get(deviceName); + } else if (!deviceName && devices.length == 1) { + packDevice = devices[0]; + } else { + // Ask user which device to use + const items = [...deviceMap.keys()].map(label => ({ label })); + const selected = await getSelection('Select a device', items, deviceName); + if (!selected) { + return; + } + + if (!deviceMap.has(selected)) { + throw new Error(`Device not found: ${selected}`); + } + + packDevice = deviceMap.get(selected); + } + + if (!packDevice) { + return; + } + + // Load processors for device + const processors = getProcessors(packDevice); + + // Select processor + if (processorName && processors.includes(processorName)) { + // Keep existing processor name + } else if (!processorName && processors.length == 1) { + processorName = processors[0]; + } else { + // Ask user which processor to use + const items = processors.map(label => ({ label })); + const selected = await getSelection('Select a processor', items, processorName); + if (!selected) { + return; + } + + if (!processors.includes(selected)) { + throw new Error(`Processor not found: ${selected}`); + } + + processorName = selected; + } + + const svdFile = getSvdPath(packDevice, processorName); + if (!svdFile) { + throw new Error(`Unable to load device ${getDeviceName(packDevice)}`); + } + + const svdUri = fileFromPack(assetBase, pack, svdFile); + return svdUri.toString(); + } +} diff --git a/src/util.ts b/src/utils.ts similarity index 87% rename from src/util.ts rename to src/utils.ts index d48a115..fc4106e 100644 --- a/src/util.ts +++ b/src/utils.ts @@ -16,29 +16,9 @@ * IN THE SOFTWARE. */ -export enum AccessType { - ReadOnly = 1, - ReadWrite, - WriteOnly -} - -export enum NumberFormat { - Auto = 0, - Hexidecimal, - Decimal, - Binary -} - -export interface NodeSetting { - node: string; - expanded?: boolean; - format?: NumberFormat; - pinned?: boolean; -} - export function hexFormat(value: number, padding = 8, includePrefix = true): string { - let base = value.toString(16); - while (base.length < padding) { base = '0' + base; } + let base = (value >>> 0).toString(16); + base = base.padStart(padding, '0'); return includePrefix ? '0x' + base : base; } @@ -84,6 +64,7 @@ export function parseInteger(value: string): number | undefined { } else if ((/^#[0-1]+/i).test(value)) { return parseInt(value.substring(1), 2); } + return undefined; } @@ -110,7 +91,7 @@ export function parseDimIndex(spec: string, count: number): string[] { throw new Error('dimIndex Element has invalid specification.'); } - const components = []; + const components: string[] = []; for (let i = 0; i < count; i++) { components.push(`${start + i}`); } @@ -127,7 +108,7 @@ export function parseDimIndex(spec: string, count: number): string[] { throw new Error('dimIndex Element has invalid specification.'); } - const components = []; + const components: string[] = []; for (let i = 0; i < count; i++) { components.push(String.fromCharCode(start + i)); } @@ -137,3 +118,16 @@ export function parseDimIndex(spec: string, count: number): string[] { return []; } + +export const readFromUrl = async (url: string): Promise => { + // Download using fetch + const response = await fetch(url); + if (!response.ok) { + const body = await response.text(); + const msg = `Request to ${url} failed. Status="${response.status}". Body="${body}".`; + throw new Error(msg); + } + + const buffer = await response.arrayBuffer(); + return buffer; +}; diff --git a/src/nodes/base-node.ts b/src/views/nodes/basenode.ts similarity index 68% rename from src/nodes/base-node.ts rename to src/views/nodes/basenode.ts index f60a5a9..8f7ddaa 100644 --- a/src/nodes/base-node.ts +++ b/src/views/nodes/basenode.ts @@ -16,40 +16,59 @@ * IN THE SOFTWARE. */ -import { Command, TreeItem } from 'vscode'; -import { AddressRangesInUse } from './address-ranges'; -import { NumberFormat, NodeSetting } from '../util'; +import { Command, TreeItem, DebugSession } from 'vscode'; +import { NumberFormat, NodeSetting } from '../../common'; +import { AddrRange } from '../../addrranges'; -export abstract class PeripheralBaseNode { +export abstract class BaseNode { public expanded: boolean; + + constructor(protected readonly parent?: BaseNode) { + this.expanded = false; + } + + public getParent(): BaseNode | undefined { + return this.parent; + } + + public abstract getChildren(): BaseNode[] | Promise; + public abstract getTreeItem(): TreeItem | Promise; + + public getCommand(): Command | undefined { + return undefined; + } + + public abstract getCopyValue(): string | undefined; +} + +export abstract class PeripheralBaseNode extends BaseNode { public format: NumberFormat; public pinned: boolean; public readonly name: string | undefined; + public session: DebugSession | undefined; constructor(protected readonly parent?: PeripheralBaseNode) { - this.expanded = false; + super(parent); this.format = NumberFormat.Auto; this.pinned = false; } - public getCommand(): Command | undefined { - return undefined; + public selected(): Thenable { + return Promise.resolve(false); } - public getParent(): PeripheralBaseNode | undefined { - return this.parent; - } - - public abstract getCopyValue(): string | undefined; - public abstract performUpdate(): Promise; - public abstract updateData(): Promise; + public abstract performUpdate(): Thenable; + public abstract updateData(): Thenable; public abstract getChildren(): PeripheralBaseNode[] | Promise; public abstract getPeripheral(): PeripheralBaseNode | undefined; - public abstract getTreeItem(): TreeItem | Promise; - public abstract markAddresses(a: AddressRangesInUse): void; + public abstract collectRanges(ary: AddrRange[]): void; // Append addr range(s) to array public abstract saveState(path?: string): NodeSetting[]; public abstract findByPath(path: string[]): PeripheralBaseNode | undefined; } + +export abstract class ClusterOrRegisterBaseNode extends PeripheralBaseNode { + public readonly offset: number | undefined; +} diff --git a/src/views/nodes/messagenode.ts b/src/views/nodes/messagenode.ts new file mode 100644 index 0000000..440e982 --- /dev/null +++ b/src/views/nodes/messagenode.ts @@ -0,0 +1,69 @@ +/* + * Copyright 2017-2019 Marcel Ball + * https://github.com/Marus/cortex-debug + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + * TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import * as vscode from 'vscode'; +import { PeripheralBaseNode } from './basenode'; +import { AddrRange } from '../../addrranges'; +import { NodeSetting } from '../../common'; + +export class MessageNode extends PeripheralBaseNode { + + constructor(public message: string, public tooltip?: string | vscode.MarkdownString) { + super(); + } + + public getChildren(): PeripheralBaseNode[] | Promise { + return []; + } + + public getTreeItem(): vscode.TreeItem | Promise { + const ti = new vscode.TreeItem(this.message, vscode.TreeItemCollapsibleState.None); + if (this.tooltip) { // A null crashes VSCode Tree renderer + ti.tooltip = this.tooltip; + } + return ti; + } + + public getCopyValue(): string | undefined { + return undefined; + } + + public performUpdate(): Thenable { + return Promise.resolve(false); + } + + public updateData(): Thenable { + return Promise.resolve(false); + } + + public getPeripheral(): PeripheralBaseNode | undefined { + return undefined; + } + + public collectRanges(_ary: AddrRange[]): void { + // Do nothing + } + + public saveState(_path?: string): NodeSetting[] { + return []; + } + + public findByPath(_path: string[]): PeripheralBaseNode | undefined { + return undefined; + } +} diff --git a/src/nodes/cluster-node.ts b/src/views/nodes/peripheralclusternode.ts similarity index 60% rename from src/nodes/cluster-node.ts rename to src/views/nodes/peripheralclusternode.ts index 33e5564..b3abd35 100644 --- a/src/nodes/cluster-node.ts +++ b/src/views/nodes/peripheralclusternode.ts @@ -16,12 +16,14 @@ * IN THE SOFTWARE. */ -import { TreeItem, TreeItemCollapsibleState } from 'vscode'; -import { PeripheralBaseNode } from './base-node'; -import { PeripheralRegisterNode } from './register-node'; -import { PeripheralNode } from './peripheral-node'; -import { AddressRangesInUse } from './address-ranges'; -import { AccessType, hexFormat, NodeSetting, NumberFormat } from '../util'; +import * as vscode from 'vscode'; +import { PeripheralBaseNode, ClusterOrRegisterBaseNode } from './basenode'; +import { PeripheralRegisterNode } from './peripheralregisternode'; +import { PeripheralNode } from './peripheralnode'; +import { AccessType } from '../../svd-parser'; +import { NodeSetting, NumberFormat } from '../../common'; +import { AddrRange } from '../../addrranges'; +import { hexFormat } from '../../utils'; export interface ClusterOptions { name: string; @@ -32,8 +34,11 @@ export interface ClusterOptions { resetValue?: number; } -export class PeripheralClusterNode extends PeripheralBaseNode { - private children: PeripheralRegisterNode[]; +export type PeripheralOrClusterNode = PeripheralNode | PeripheralClusterNode; +export type PeripheralRegisterOrClusterNode = PeripheralRegisterNode | PeripheralClusterNode; + +export class PeripheralClusterNode extends ClusterOrRegisterBaseNode { + private children: PeripheralRegisterOrClusterNode[]; public readonly name: string; public readonly description?: string; public readonly offset: number; @@ -41,7 +46,7 @@ export class PeripheralClusterNode extends PeripheralBaseNode { public readonly resetValue: number; public readonly accessType: AccessType; - constructor(public parent: PeripheralNode, options: ClusterOptions) { + constructor(public parent: PeripheralOrClusterNode, options: ClusterOptions) { super(parent); this.name = options.name; this.description = options.description; @@ -53,28 +58,28 @@ export class PeripheralClusterNode extends PeripheralBaseNode { this.parent.addChild(this); } - public getTreeItem(): TreeItem | Promise { + public getTreeItem(): vscode.TreeItem | Promise { const label = `${this.name} [${hexFormat(this.offset, 0)}]`; - const item = new TreeItem(label, this.expanded ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.Collapsed); + const item = new vscode.TreeItem(label, this.expanded ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.Collapsed); item.contextValue = 'cluster'; item.tooltip = this.description || undefined; return item; } - public getChildren(): PeripheralRegisterNode[] { + public getChildren(): PeripheralRegisterOrClusterNode[] { return this.children; } - public setChildren(children: PeripheralRegisterNode[]): void { + public setChildren(children: PeripheralRegisterOrClusterNode[]): void { this.children = children.slice(0, children.length); - this.children.sort((r1, r2) => r1.offset > r2.offset ? 1 : -1); + this.children.sort((c1, c2) => c1.offset > c2.offset ? 1 : -1); } - public addChild(child: PeripheralRegisterNode): void { + public addChild(child: PeripheralRegisterOrClusterNode): void { this.children.push(child); - this.children.sort((r1, r2) => r1.offset > r2.offset ? 1 : -1); + this.children.sort((c1, c2) => c1.offset > c2.offset ? 1 : -1); } public getBytes(offset: number, size: number): Uint8Array { @@ -85,17 +90,27 @@ export class PeripheralClusterNode extends PeripheralBaseNode { return this.parent.getAddress(this.offset + offset); } - public getOffset(offset: number): number { + public getOffset(offset: number): number { return this.parent.getOffset(this.offset + offset); } public getFormat(): NumberFormat { - if (this.format !== NumberFormat.Auto) { return this.format; } else { return this.parent.getFormat(); } + if (this.format !== NumberFormat.Auto) { + return this.format; + } else { + return this.parent.getFormat(); + } } - public async updateData(): Promise { - const promises = this.children.map((r) => r.updateData()); - await Promise.all(promises); + public updateData(): Thenable { + return new Promise((resolve, reject) => { + const promises = this.children.map((r) => r.updateData()); + Promise.all(promises).then(() => { + resolve(true); + }).catch(() => { + reject('Failed'); + }); + }); } public saveState(path: string): NodeSetting[] { @@ -113,14 +128,20 @@ export class PeripheralClusterNode extends PeripheralBaseNode { } public findByPath(path: string[]): PeripheralBaseNode | undefined { - if (path.length === 0) { return this; } else { + if (path.length === 0) { + return this; + } else { const child = this.children.find((c) => c.name === path[0]); - if (child) { return child.findByPath(path.slice(1)); } else { return undefined; } + if (child) { + return child.findByPath(path.slice(1)); + } else { + return undefined; + } } } - public markAddresses(addrs: AddressRangesInUse): void { - this.children.map((r) => { r.markAddresses(addrs); }); + public collectRanges(ary: AddrRange[]): void { + this.children.map((r) => { r.collectRanges(ary); }); } public getPeripheral(): PeripheralBaseNode { @@ -131,7 +152,7 @@ export class PeripheralClusterNode extends PeripheralBaseNode { throw new Error('Method not implemented.'); } - public performUpdate(): Promise { + public performUpdate(): Thenable { throw new Error('Method not implemented.'); } } diff --git a/src/nodes/field-node.ts b/src/views/nodes/peripheralfieldnode.ts similarity index 75% rename from src/nodes/field-node.ts rename to src/views/nodes/peripheralfieldnode.ts index bee2c59..f5c8a0a 100644 --- a/src/nodes/field-node.ts +++ b/src/views/nodes/peripheralfieldnode.ts @@ -16,10 +16,13 @@ * IN THE SOFTWARE. */ -import { window, TreeItem, TreeItemCollapsibleState, MarkdownString } from 'vscode'; -import { PeripheralBaseNode } from './base-node'; -import { PeripheralRegisterNode } from './register-node'; -import { AccessType, parseInteger, binaryFormat, hexFormat, NumberFormat, NodeSetting } from '../util'; +import * as vscode from 'vscode'; +import { PeripheralBaseNode } from './basenode'; +import { PeripheralRegisterNode } from './peripheralregisternode'; +import { AccessType } from '../../svd-parser'; +import { AddrRange } from '../../addrranges'; +import { NumberFormat, NodeSetting } from '../../common'; +import { parseInteger, binaryFormat, hexFormat } from '../../utils'; export interface EnumerationMap { [value: number]: EnumeratedValue; @@ -49,6 +52,7 @@ export class PeripheralFieldNode extends PeripheralBaseNode { private enumerationValues: string[] = []; // eslint-disable-next-line @typescript-eslint/no-explicit-any private enumerationMap: any; + private prevValue = ''; constructor(public parent: PeripheralRegisterNode, options: FieldOptions) { super(parent); @@ -58,7 +62,9 @@ export class PeripheralFieldNode extends PeripheralBaseNode { this.offset = options.offset; this.width = options.width; - if (!options.accessType) { this.accessType = parent.accessType; } else { + if (!options.accessType) { + this.accessType = parent.accessType; + } else { if (parent.accessType === AccessType.ReadOnly && options.accessType !== AccessType.ReadOnly) { this.accessType = AccessType.ReadOnly; } else if (parent.accessType === AccessType.WriteOnly && options.accessType !== AccessType.WriteOnly) { @@ -73,8 +79,10 @@ export class PeripheralFieldNode extends PeripheralBaseNode { this.enumerationMap = {}; this.enumerationValues = []; + // tslint:disable-next-line:forin for (const key in options.enumeration) { const name = options.enumeration[key].name; + this.enumerationValues.push(name); this.enumerationMap[name] = key; } @@ -83,25 +91,32 @@ export class PeripheralFieldNode extends PeripheralBaseNode { this.parent.addChild(this); } - public getTreeItem(): TreeItem | Promise { + public getTreeItem(): vscode.TreeItem | Promise { const isReserved = this.name.toLowerCase() === 'reserved'; const context = isReserved ? 'field-res' : (this.parent.accessType === AccessType.ReadOnly ? 'field-ro' : 'field'); const rangestart = this.offset; const rangeend = this.offset + this.width - 1; - - const item = new TreeItem(`${this.name} [${rangeend}:${rangestart}]`, TreeItemCollapsibleState.None); + const label = `${this.name} [${rangeend}:${rangestart}]`; + const displayValue = this.getFormattedValue(this.getFormat()); + const labelItem: vscode.TreeItemLabel = { + label: label + ' ' + displayValue + }; + if (displayValue !== this.prevValue) { + labelItem.highlights = [[label.length + 1, labelItem.label.length]]; + this.prevValue = displayValue; + } + const item = new vscode.TreeItem(labelItem, vscode.TreeItemCollapsibleState.None); item.contextValue = context; - item.tooltip = this.generateTooltipMarkdown(isReserved); - item.description = this.getFormattedValue(this.getFormat()); + item.tooltip = this.generateTooltipMarkdown(isReserved) || undefined; return item; } - private generateTooltipMarkdown(isReserved: boolean): MarkdownString | undefined { - const mds = new MarkdownString('', true); + private generateTooltipMarkdown(isReserved: boolean): vscode.MarkdownString | null { + const mds = new vscode.MarkdownString('', true); mds.isTrusted = true; const address = `${ hexFormat(this.parent.getAddress()) }${ this.getFormattedRange() }`; @@ -191,7 +206,7 @@ export class PeripheralFieldNode extends PeripheralBaseNode { case NumberFormat.Binary: formatted = binaryFormat(value, this.width); break; - case NumberFormat.Hexidecimal: + case NumberFormat.Hexadecimal: formatted = hexFormat(value, Math.ceil(this.width / 4), true); break; default: @@ -218,45 +233,33 @@ export class PeripheralFieldNode extends PeripheralBaseNode { if (this.enumeration[value]) { return this.enumeration[value].name; } - - return undefined; } public getChildren(): PeripheralBaseNode[] | Promise { return []; } - public async performUpdate(): Promise { - let numval: number | undefined; - - if (this.enumeration) { - const val = await window.showQuickPick(this.enumerationValues); - if (val === undefined) { - return false; - } - - numval = this.enumerationMap[val]; - } else { - const val = await window.showInputBox({ - prompt: 'Enter new value: (prefix hex with 0x, binary with 0b)', - value: this.getCopyValue() - }); - if (val === undefined) { - return false; - } + public performUpdate(): Thenable { + return new Promise((resolve, reject) => { + if (this.enumeration) { + vscode.window.showQuickPick(this.enumerationValues).then((val) => { + if (val === undefined) { return reject('Input not selected'); } - numval = parseInteger(val || 'none'); - if (numval === undefined) { - throw new Error('Unable to parse input value.'); + const numval = this.enumerationMap[val]; + this.parent.updateBits(this.offset, this.width, numval).then(resolve, reject); + }); + } else { + vscode.window.showInputBox({ prompt: 'Enter new value: (prefix hex with 0x, binary with 0b)', value: this.getCopyValue() }).then((val) => { + if (typeof val === 'string') { + const numval = parseInteger(val); + if (numval === undefined) { + return reject('Unable to parse input value.'); + } + this.parent.updateBits(this.offset, this.width, numval).then(resolve, reject); + } + }); } - } - - if (numval) { - await this.parent.updateBits(this.offset, this.width, numval); - return true; - } - - return false; + }); } public getCopyValue(): string { @@ -266,19 +269,23 @@ export class PeripheralFieldNode extends PeripheralBaseNode { return value.toString(); case NumberFormat.Binary: return binaryFormat(value, this.width); - case NumberFormat.Hexidecimal: + case NumberFormat.Hexadecimal: return hexFormat(value, Math.ceil(this.width / 4), true); default: return this.width >= 4 ? hexFormat(value, Math.ceil(this.width / 4), true) : binaryFormat(value, this.width); } } - public async updateData(): Promise { - // Do nothing + public updateData(): Thenable { + return Promise.resolve(true); } public getFormat(): NumberFormat { - if (this.format !== NumberFormat.Auto) { return this.format; } else { return this.parent.getFormat(); } + if (this.format !== NumberFormat.Auto) { + return this.format; + } else { + return this.parent.getFormat(); + } } public saveState(path: string): NodeSetting[] { @@ -290,14 +297,18 @@ export class PeripheralFieldNode extends PeripheralBaseNode { } public findByPath(path: string[]): PeripheralBaseNode | undefined { - if (path.length === 0) { return this; } else { return undefined; } + if (path.length === 0) { + return this; + } else { + return undefined; + } } public getPeripheral(): PeripheralBaseNode { return this.parent.getPeripheral(); } - public markAddresses(): void { + public collectRanges(_a: AddrRange[]): void { throw new Error('Method not implemented.'); } } diff --git a/src/nodes/peripheral-node.ts b/src/views/nodes/peripheralnode.ts similarity index 66% rename from src/nodes/peripheral-node.ts rename to src/views/nodes/peripheralnode.ts index a207745..d5e6705 100644 --- a/src/nodes/peripheral-node.ts +++ b/src/views/nodes/peripheralnode.ts @@ -16,13 +16,15 @@ * IN THE SOFTWARE. */ -import { debug, TreeItem, TreeItemCollapsibleState, ThemeIcon } from 'vscode'; -import type { DebugProtocol } from 'vscode-debugprotocol'; -import { PeripheralBaseNode } from './base-node'; -import { AddrRange, AddressRangesInUse } from './address-ranges'; -import { PeripheralRegisterNode } from './register-node'; -import { PeripheralClusterNode } from './cluster-node'; -import { AccessType, hexFormat, NumberFormat, NodeSetting } from '../util'; +import * as vscode from 'vscode'; +import { PeripheralBaseNode } from './basenode'; +import { PeripheralRegisterNode } from './peripheralregisternode'; +import { PeripheralClusterNode, PeripheralRegisterOrClusterNode } from './peripheralclusternode'; +import { AddrRange, AddressRangesUtils } from '../../addrranges'; +import { NumberFormat, NodeSetting } from '../../common'; +import { MemReadUtils } from '../../memreadutils'; +import { AccessType } from '../../svd-parser'; +import { hexFormat } from '../../utils'; export interface PeripheralOptions { name: string; @@ -50,8 +52,8 @@ export class PeripheralNode extends PeripheralBaseNode { private currentValue: number[] = []; - constructor(options: PeripheralOptions) { - super(undefined); + constructor(public session: vscode.DebugSession, public gapThreshold: number, options: PeripheralOptions) { + super(); this.name = options.name; this.baseAddress = options.baseAddress; @@ -68,13 +70,13 @@ export class PeripheralNode extends PeripheralBaseNode { return this; } - public getTreeItem(): TreeItem | Promise { + public getTreeItem(): vscode.TreeItem | Promise { const label = `${this.name} @ ${hexFormat(this.baseAddress)}`; - const item = new TreeItem(label, this.expanded ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.Collapsed); + const item = new vscode.TreeItem(label, this.expanded ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.Collapsed); item.contextValue = this.pinned ? 'peripheral.pinned' : 'peripheral'; item.tooltip = this.description || undefined; if (this.pinned) { - item.iconPath = new ThemeIcon('pinned'); + item.iconPath = new vscode.ThemeIcon('pinned'); } return item; } @@ -92,7 +94,7 @@ export class PeripheralNode extends PeripheralBaseNode { this.children.sort((c1, c2) => c1.offset > c2.offset ? 1 : -1); } - public addChild(child: PeripheralRegisterNode | PeripheralClusterNode): void { + public addChild(child: PeripheralRegisterOrClusterNode): void { this.children.push(child); this.children.sort((c1, c2) => c1.offset > c2.offset ? 1 : -1); } @@ -117,66 +119,64 @@ export class PeripheralNode extends PeripheralBaseNode { return this.format; } - public async updateData(): Promise { + public async updateData(): Promise { if (!this.expanded) { - return; + return false; } try { await this.readMemory(); + } catch (e) { + const msg = (e as Error).message || 'unknown error'; + const str = `Failed to update peripheral ${this.name}: ${msg}`; + if (vscode.debug.activeDebugConsole) { + vscode.debug.activeDebugConsole.appendLine(str); + } + } + + try { const promises = this.children.map((r) => r.updateData()); await Promise.all(promises); + return true; } catch (e) { - const msg = e.message || 'unknown error'; + const msg = (e as Error).message || 'unknown error'; const str = `Failed to update peripheral ${this.name}: ${msg}`; - if (debug.activeDebugConsole) { - debug.activeDebugConsole.appendLine(str); + if (vscode.debug.activeDebugConsole) { + vscode.debug.activeDebugConsole.appendLine(str); } throw new Error(str); } } - protected async readMemory(): Promise { + protected readMemory(): Promise { if (!this.currentValue) { this.currentValue = new Array(this.totalLength); } - const promises = this.addrRanges.map(r => this.readRangeMemory(r)); - await Promise.all(promises); - } - - protected async readRangeMemory(r: AddrRange): Promise { - if (debug.activeDebugSession) { - const memoryReference = '0x' + r.base.toString(16); - const request: DebugProtocol.ReadMemoryArguments = { - memoryReference, - count: r.length - }; - - const response: Partial = {}; - response.body = await debug.activeDebugSession.customRequest('readMemory', request); - - if (response.body && response.body.data) { - const data = Buffer.from(response.body.data, 'base64'); - let dst = r.base - this.baseAddress; - for (const byte of data) { - this.currentValue[dst++] = byte; + return MemReadUtils.readMemoryChunks(this.session, this.baseAddress, this.addrRanges, this.currentValue); + } + + public collectRanges(): void { + const addresses: AddrRange[] = []; + this.children.map((child) => child.collectRanges(addresses)); + addresses.sort((a, b) => (a.base < b.base) ? -1 : ((a.base > b.base) ? 1 : 0)); + addresses.map((r) => r.base += this.baseAddress); + + const maxGap = this.gapThreshold; + let ranges: AddrRange[] = []; + if (maxGap >= 0) { + let last: AddrRange | undefined; + for (const r of addresses) { + if (last && ((last.nxtAddr() + maxGap) >= r.base)) { + const max = Math.max(last.nxtAddr(), r.nxtAddr()); + last.length = max - last.base; + } else { + ranges.push(r); + last = r; } } - } - } - - public markAddresses(): void { - let ranges = [new AddrRange(this.baseAddress, this.totalLength)]; // Default range - const skipAddressGaps = true; - - if (skipAddressGaps) { - // Split the entire range into a set of smaller ranges. Some svd files specify - // a very large address space but may use very little of it. - const gapThreshold = 16; // Merge gaps less than this many bytes, avoid too many gdb requests - const addresses = new AddressRangesInUse(this.totalLength); - this.children.map((child) => child.markAddresses(addresses)); - ranges = addresses.getAddressRangesOptimized(this.baseAddress, false, gapThreshold); + } else { + ranges = addresses; } // OpenOCD has an issue where the max number of bytes readable are 8191 (instead of 8192) @@ -184,14 +184,18 @@ export class PeripheralNode extends PeripheralBaseNode { // but in general, it is good to split the reads up. see http://openocd.zylin.com/#/c/5109/ // Another benefit, we can minimize gdb timeouts const maxBytes = (4 * 1024); // Should be a multiple of 4 to be safe for MMIO reads - this.addrRanges = AddressRangesInUse.splitIntoChunks(ranges, maxBytes); + this.addrRanges = AddressRangesUtils.splitIntoChunks(ranges, maxBytes, this.name, this.totalLength); } public getPeripheralNode(): PeripheralNode { return this; } - public saveState(): NodeSetting[] { + public selected(): Thenable { + return this.performUpdate(); + } + + public saveState(_path?: string): NodeSetting[] { const results: NodeSetting[] = []; if (this.format !== NumberFormat.Auto || this.expanded || this.pinned) { @@ -210,14 +214,20 @@ export class PeripheralNode extends PeripheralBaseNode { return results; } - public findByPath(path: string[]): PeripheralBaseNode | undefined { - if (path.length === 0) { return this; } else { + public findByPath(path: string[]): PeripheralBaseNode | undefined{ + if (path.length === 0) { + return this; + } else { const child = this.children.find((c) => c.name === path[0]); - if (child) { return child.findByPath(path.slice(1)); } else { return undefined; } + if (child) { + return child.findByPath(path.slice(1)); + } else { + return undefined; + } } } - public performUpdate(): Promise { + public performUpdate(): Thenable { throw new Error('Method not implemented.'); } diff --git a/src/nodes/register-node.ts b/src/views/nodes/peripheralregisternode.ts similarity index 65% rename from src/nodes/register-node.ts rename to src/views/nodes/peripheralregisternode.ts index dd57c33..79aa841 100644 --- a/src/nodes/register-node.ts +++ b/src/views/nodes/peripheralregisternode.ts @@ -16,14 +16,16 @@ * IN THE SOFTWARE. */ -import { window, debug, TreeItem, TreeItemCollapsibleState, MarkdownString } from 'vscode'; -import type { DebugProtocol } from 'vscode-debugprotocol'; -import { PeripheralNode } from './peripheral-node'; -import { PeripheralClusterNode } from './cluster-node'; -import { PeripheralBaseNode } from './base-node'; -import { PeripheralFieldNode } from './field-node'; -import { AddressRangesInUse } from './address-ranges'; -import { AccessType, extractBits, createMask, hexFormat, binaryFormat, NumberFormat, NodeSetting } from '../util'; +import * as vscode from 'vscode'; +import { PeripheralNode } from './peripheralnode'; +import { PeripheralClusterNode } from './peripheralclusternode'; +import { ClusterOrRegisterBaseNode, PeripheralBaseNode } from './basenode'; +import { PeripheralFieldNode } from './peripheralfieldnode'; +import { extractBits, createMask, hexFormat, binaryFormat } from '../../utils'; +import { NumberFormat, NodeSetting } from '../../common'; +import { AccessType } from '../../svd-parser'; +import { AddrRange } from '../../addrranges'; +import { MemReadUtils } from '../../memreadutils'; export interface PeripheralRegisterOptions { name: string; @@ -34,7 +36,7 @@ export interface PeripheralRegisterOptions { resetValue?: number; } -export class PeripheralRegisterNode extends PeripheralBaseNode { +export class PeripheralRegisterNode extends ClusterOrRegisterBaseNode { public children: PeripheralFieldNode[]; public readonly name: string; public readonly description?: string; @@ -48,6 +50,7 @@ export class PeripheralRegisterNode extends PeripheralBaseNode { private hexRegex: RegExp; private binaryRegex: RegExp; private currentValue: number; + private prevValue = ''; constructor(public parent: PeripheralNode | PeripheralClusterNode, options: PeripheralRegisterOptions) { super(parent); @@ -77,51 +80,62 @@ export class PeripheralRegisterNode extends PeripheralBaseNode { return extractBits(this.currentValue, offset, width); } - public async updateBits(offset: number, width: number, value: number): Promise { - const limit = Math.pow(2, width); - if (value > limit) { - throw new Error(`Value entered is invalid. Maximum value for this field is ${limit - 1} (${hexFormat(limit - 1, 0)})`); - } - - const mask = createMask(offset, width); - const sv = value << offset; - const newval = (this.currentValue & ~mask) | sv; - return this.updateValueInternal(newval); + public updateBits(offset: number, width: number, value: number): Thenable { + return new Promise((resolve, reject) => { + const limit = Math.pow(2, width); + if (value > limit) { + return reject(`Value entered is invalid. Maximum value for this field is ${limit - 1} (${hexFormat(limit - 1, 0)})`); + } else { + const mask = createMask(offset, width); + const sv = value << offset; + const newval = (this.currentValue & ~mask) | sv; + this.updateValueInternal(newval).then(resolve, reject); + } + }); } - public getTreeItem(): TreeItem | Promise { + public getTreeItem(): vscode.TreeItem | Promise { const label = `${this.name} @ ${hexFormat(this.offset, 0)}`; const collapseState = this.children && this.children.length > 0 - ? (this.expanded ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.Collapsed) - : TreeItemCollapsibleState.None; - - const item = new TreeItem(label, collapseState); + ? (this.expanded ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.Collapsed) + : vscode.TreeItemCollapsibleState.None; + + const displayValue = this.getFormattedValue(this.getFormat()); + const labelItem: vscode.TreeItemLabel = { + label: label + ' ' + displayValue + }; + if (displayValue !== this.prevValue) { + labelItem.highlights = [[label.length + 1, labelItem.label.length]]; + this.prevValue = displayValue; + } + const item = new vscode.TreeItem(labelItem, collapseState); item.contextValue = this.accessType === AccessType.ReadWrite ? 'registerRW' : (this.accessType === AccessType.ReadOnly ? 'registerRO' : 'registerWO'); - item.tooltip = this.generateTooltipMarkdown(); - item.description = this.getFormattedValue(this.getFormat()); + item.tooltip = this.generateTooltipMarkdown() || undefined; return item; } - private generateTooltipMarkdown(): MarkdownString | undefined { - const mds = new MarkdownString('', true); + private generateTooltipMarkdown(): vscode.MarkdownString | null { + const mds = new vscode.MarkdownString('', true); mds.isTrusted = true; - const address = `${hexFormat(this.getAddress())}`; + const address = `${ hexFormat(this.getAddress()) }`; const formattedValue = this.getFormattedValue(this.getFormat()); const roLabel = this.accessType === AccessType.ReadOnly ? '(Read Only)' : '      '; - mds.appendMarkdown(`| ${this.name}@${address} | ${roLabel} | *${formattedValue}* |\n`); + mds.appendMarkdown(`| ${ this.name }@${ address } | ${ roLabel } | *${ formattedValue }* |\n`); mds.appendMarkdown('|:---|:---:|---:|\n\n'); if (this.accessType !== AccessType.WriteOnly) { - mds.appendMarkdown(`**Reset Value:** ${this.getFormattedResetValue(this.getFormat())}\n`); + mds.appendMarkdown(`**Reset Value:** ${ this.getFormattedResetValue(this.getFormat()) }\n`); } mds.appendMarkdown('\n____\n\n'); - mds.appendMarkdown(this.description || 'no description'); + if (this.description) { + mds.appendMarkdown(this.description); + } mds.appendMarkdown('\n_____\n\n'); @@ -130,13 +144,13 @@ export class PeripheralRegisterNode extends PeripheralBaseNode { return mds; } - const hex = this.getFormattedValue(NumberFormat.Hexidecimal); + const hex = this.getFormattedValue(NumberFormat.Hexadecimal); const decimal = this.getFormattedValue(NumberFormat.Decimal); const binary = this.getFormattedValue(NumberFormat.Binary); mds.appendMarkdown('| Hex    | Decimal    | Binary    |\n'); mds.appendMarkdown('|:---|:---|:---|\n'); - mds.appendMarkdown(`| ${hex}    | ${decimal}    | ${binary}    |\n\n`); + mds.appendMarkdown(`| ${ hex }    | ${ decimal }    | ${ binary }    |\n\n`); const children = this.getChildren(); if (children.length === 0) { return mds; } @@ -146,7 +160,8 @@ export class PeripheralRegisterNode extends PeripheralBaseNode { mds.appendMarkdown('|:---|:---:|:---|:---|\n'); children.forEach((field) => { - mds.appendMarkdown(`| ${field.name} |        | ${field.getFormattedRange()} | ${field.getFormattedValue(field.getFormat(), true)} |\n`); + mds.appendMarkdown(`| ${ field.name } |        | ${ field.getFormattedRange() } | ` + + `${ field.getFormattedValue(field.getFormat(), true) } |\n`); }); return mds; @@ -213,13 +228,12 @@ export class PeripheralRegisterNode extends PeripheralBaseNode { } public async performUpdate(): Promise { - const val = await window.showInputBox({ prompt: 'Enter new value: (prefix hex with 0x, binary with 0b)', value: this.getCopyValue() }); + const val = await vscode.window.showInputBox({ prompt: 'Enter new value: (prefix hex with 0x, binary with 0b)', value: this.getCopyValue() }); if (!val) { return false; } let numval: number; - if (val.match(this.hexRegex)) { numval = parseInt(val.substr(2), 16); } else if (val.match(this.binaryRegex)) { @@ -241,57 +255,36 @@ export class PeripheralRegisterNode extends PeripheralBaseNode { } private async updateValueInternal(value: number): Promise { - const memoryReference = '0x' + this.parent.getAddress(this.offset).toString(16); - const bytes: string[] = []; - const numbytes = this.size / 8; - - for (let i = 0; i < numbytes; i++) { - const byte = value & 0xFF; - value = value >>> 8; - let bs = byte.toString(16); - if (bs.length === 1) { bs = '0' + bs; } - bytes[i] = bs; - } - - const data = Buffer.from(bytes).toString('base64'); - if (debug.activeDebugSession) { - const request: DebugProtocol.WriteMemoryArguments = { - memoryReference, - data - }; - - await debug.activeDebugSession.customRequest('writeMemory', request); - this.parent.updateData(); - return true; + if (!vscode.debug.activeDebugSession) { + return false; } - return false; + await MemReadUtils.writeMemory(vscode.debug.activeDebugSession, this.parent.getAddress(this.offset), value, this.size); + await this.parent.updateData(); + return true; } - public async updateData(): Promise { - try { - const bc = this.size / 8; - const bytes = this.parent.getBytes(this.offset, bc); - const buffer = Buffer.from(bytes); - switch (bc) { - case 1: - this.currentValue = buffer.readUInt8(0); - break; - case 2: - this.currentValue = buffer.readUInt16LE(0); - break; - case 4: - this.currentValue = buffer.readUInt32LE(0); - break; - default: - window.showErrorMessage(`Register ${this.name} has invalid size: ${this.size}. Should be 8, 16 or 32.`); - break; - } - } catch (error) { - window.showErrorMessage(`Register failed to update: ${error}`); + public updateData(): Thenable { + const bc = this.size / 8; + const bytes = this.parent.getBytes(this.offset, bc); + const buffer = Buffer.from(bytes); + switch (bc) { + case 1: + this.currentValue = buffer.readUInt8(0); + break; + case 2: + this.currentValue = buffer.readUInt16LE(0); + break; + case 4: + this.currentValue = buffer.readUInt32LE(0); + break; + default: + vscode.window.showErrorMessage(`Register ${this.name} has invalid size: ${this.size}. Should be 8, 16 or 32.`); + break; } - this.children.forEach((f) => f.updateData()); + + return Promise.resolve(true); } public saveState(path?: string): NodeSetting[] { @@ -309,18 +302,22 @@ export class PeripheralRegisterNode extends PeripheralBaseNode { } public findByPath(path: string[]): PeripheralBaseNode | undefined { - if (path.length === 0) { return this; } else if (path.length === 1) { + if (path.length === 0) { + return this; + } else if (path.length === 1) { const child = this.children.find((c) => c.name === path[0]); return child; - } else { return undefined; } + } else { + return undefined; + } } public getPeripheral(): PeripheralBaseNode { return this.parent.getPeripheral(); } - public markAddresses(addrs: AddressRangesInUse): void { + public collectRanges(addrs: AddrRange[]): void { const finalOffset = this.parent.getOffset(this.offset); - addrs.setAddrRange(finalOffset, this.size / 8); + addrs.push(new AddrRange(finalOffset, this.size / 8)); } } diff --git a/src/views/peripheral.ts b/src/views/peripheral.ts new file mode 100644 index 0000000..869c2d5 --- /dev/null +++ b/src/views/peripheral.ts @@ -0,0 +1,370 @@ +/* + * Copyright 2017-2019 Marcel Ball + * https://github.com/Marus/cortex-debug + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + * TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import * as vscode from 'vscode'; +import * as manifest from '../manifest'; +import { parseStringPromise } from 'xml2js'; +import { BaseNode, PeripheralBaseNode } from './nodes/basenode'; +import { PeripheralNode } from './nodes/peripheralnode'; +import { MessageNode } from './nodes/messagenode'; +import { NodeSetting } from '../common'; +import { SvdData, SVDParser } from '../svd-parser'; +import { AddrRange } from '../addrranges'; +import { DebugTracker } from '../debug-tracker'; +import { SvdResolver } from '../svd-resolver'; +import { readFromUrl } from '../utils'; +import { uriExists } from '../vscode-utils'; + +const STATE_FILENAME = '.svd-viewer.json'; + +const pathToUri = (path: string): vscode.Uri => { + try { + return vscode.Uri.file(path); + } catch (e) { + return vscode.Uri.parse(path); + } +}; + +export class PeripheralTreeForSession extends PeripheralBaseNode { + public myTreeItem: vscode.TreeItem; + private peripherials: PeripheralNode[] = []; + private loaded = false; + private errMessage = 'No SVD file loaded'; + + constructor( + public session: vscode.DebugSession, + public state: vscode.TreeItemCollapsibleState, + private wsFolderPath: vscode.Uri | undefined, + private fireCb: () => void) { + super(); + this.myTreeItem = new vscode.TreeItem(this.session.name, this.state); + } + + private getSvdStateUri(): vscode.Uri | undefined { + if (!this.wsFolderPath) { + return undefined; + } + + return vscode.Uri.joinPath(this.wsFolderPath, '.vscode', STATE_FILENAME); + } + + private async loadSvdState(): Promise { + const stateUri = this.getSvdStateUri(); + if (stateUri) { + const exists = await uriExists(stateUri); + if (exists) { + await vscode.workspace.fs.stat(stateUri); + const data = await vscode.workspace.fs.readFile(stateUri); + const decoder = new TextDecoder(); + const text = decoder.decode(data); + return JSON.parse(text); + } + } + + return []; + } + + private async saveSvdState(state: NodeSetting[]): Promise { + const stateUri = this.getSvdStateUri(); + if (stateUri) { + try { + const text = JSON.stringify(state); + const encoder = new TextEncoder(); + const data = encoder.encode(text); + await vscode.workspace.fs.writeFile(stateUri, data); + } catch (e) { + vscode.window.showWarningMessage(`Unable to save peripheral preferences ${e}`); + } + } + } + + public saveState(): NodeSetting[] { + const state: NodeSetting[] = []; + this.peripherials.forEach((p) => { + state.push(... p.saveState()); + }); + + return state; + } + + private async createPeripherals(svdPath: string, gapThreshold: number): Promise { + let svdData: SvdData | undefined; + + try { + let contents: ArrayBuffer | undefined; + + if (svdPath.startsWith('http')) { + contents = await readFromUrl(svdPath); + } else { + const uri = pathToUri(svdPath); + contents = await vscode.workspace.fs.readFile(uri); + } + + if (contents) { + const decoder = new TextDecoder(); + const xml = decoder.decode(contents); + svdData = await parseStringPromise(xml); + } + } catch(e) { + // eslint-disable-next-line no-console + console.warn(e); + } + + if (!svdData) { + return; + } + + this.errMessage = `Loading ${svdPath}`; + + try { + this.peripherials = await SVDParser.parseSVD(this.session, svdData, gapThreshold); + this.loaded = true; + } catch(e) { + this.peripherials = []; + this.loaded = false; + throw e; + } + + this.errMessage = ''; + } + + public performUpdate(): Thenable { + throw new Error('Method not implemented.'); + } + + public updateData(): Thenable { + if (this.loaded) { + const promises = this.peripherials.map((p) => p.updateData()); + Promise.all(promises).then((_) => { this.fireCb(); }, (_) => { this.fireCb(); }); + } + return Promise.resolve(true); + } + + public getPeripheral(): PeripheralBaseNode { + throw new Error('Method not implemented.'); + } + + public collectRanges(_ary: AddrRange[]): void { + throw new Error('Method not implemented.'); + } + + public findByPath(_path: string[]): PeripheralBaseNode { + throw new Error('Method not implemented.'); // Shouldn't be called + } + + private findNodeByPath(path: string): PeripheralBaseNode | undefined { + const pathParts = path.split('.'); + const peripheral = this.peripherials.find((p) => p.name === pathParts[0]); + if (!peripheral) { return undefined; } + + return peripheral.findByPath(pathParts.slice(1)); + } + + public refresh(): void { + this.fireCb(); + } + + public getTreeItem(element?: BaseNode): vscode.TreeItem | Promise { + return element ? element.getTreeItem() : this.myTreeItem; + } + + public getChildren(element?: PeripheralBaseNode): PeripheralBaseNode[] | Promise { + if (this.loaded) { + return element ? element.getChildren() : this.peripherials; + } else if (!this.loaded) { + return [new MessageNode(this.errMessage)]; + } else { + return this.peripherials; + } + } + + public getCopyValue(): string | undefined { + return undefined; + } + + public async sessionStarted(svdPath: string, thresh: number): Promise { // Never rejects + if (((typeof thresh) === 'number') && (thresh < 0)) { + thresh = -1; // Never merge register reads even if adjacent + } else { + // Set the threshold between 0 and 32, with a default of 16 and a mukltiple of 8 + thresh = ((((typeof thresh) === 'number') ? Math.max(0, Math.min(thresh, 32)) : 16) + 7) & ~0x7; + } + + this.peripherials = []; + this.fireCb(); + + try { + await this.createPeripherals(svdPath, thresh); + + const settings = await this.loadSvdState(); + settings.forEach((s: NodeSetting) => { + const node = this.findNodeByPath(s.node); + if (node) { + node.expanded = s.expanded || false; + node.pinned = s.pinned || false; + if (s.format) { + node.format = s.format; + } + } + }); + this.peripherials.sort(PeripheralNode.compare); + this.fireCb(); + } catch(e) { + this.errMessage = `Unable to parse SVD file ${svdPath}: ${(e as Error).message}`; + vscode.window.showErrorMessage(this.errMessage); + if (vscode.debug.activeDebugConsole) { + vscode.debug.activeDebugConsole.appendLine(this.errMessage); + } + this.fireCb(); + } + } + + public sessionTerminated(): void { + const state = this.saveState(); + this.saveSvdState(state); + } + + public togglePinPeripheral(node: PeripheralBaseNode): void { + node.pinned = !node.pinned; + this.peripherials.sort(PeripheralNode.compare); + } +} + +export class PeripheralTreeProvider implements vscode.TreeDataProvider { + // tslint:disable-next-line:variable-name + public _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); + public readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; + protected sessionPeripheralsMap = new Map (); + protected oldState = new Map (); + + constructor(tracker: DebugTracker, protected resolver: SvdResolver) { + tracker.onWillStartSession(session => this.debugSessionStarted(session)); + tracker.onWillStopSession(session => this.debugSessionTerminated(session)); + tracker.onDidStopDebug(session => this.debugStopped(session)); + } + + public async activate(context: vscode.ExtensionContext): Promise { + const view = vscode.window.createTreeView(`${manifest.PACKAGE_NAME}.svd`, { treeDataProvider: this }); + context.subscriptions.push( + view, + view.onDidExpandElement((e) => { + e.element.expanded = true; + const p = e.element.getPeripheral(); + if (p) { + p.updateData(); + this.refresh(); + } + }), + view.onDidCollapseElement((e) => { + e.element.expanded = false; + }) + ); + } + + public refresh(): void { + this._onDidChangeTreeData.fire(undefined); + } + + public getTreeItem(element: PeripheralBaseNode): vscode.TreeItem | Promise { + return element?.getTreeItem(); + } + + public getChildren(element?: PeripheralBaseNode): vscode.ProviderResult { + const values = Array.from(this.sessionPeripheralsMap.values()); + if (element) { + return element.getChildren(); + } else if (values.length === 0) { + return [new MessageNode('SVD: No active debug sessions or no SVD files specified')]; + } else if (values.length === 1) { + return values[0].getChildren(); // Don't do root nodes at top-level if there is only one root + } else { + return values; + } + } + + public async debugSessionStarted(session: vscode.DebugSession): Promise { + const wsFolderPath = session.workspaceFolder ? session.workspaceFolder.uri : vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders[0].uri; + const svdPath = await this.resolver.resolve(session, wsFolderPath); + + if (!svdPath) { + return; + } + + if (this.sessionPeripheralsMap.get(session.id)) { + this._onDidChangeTreeData.fire(undefined); + vscode.window.showErrorMessage(`Internal Error: Session ${session.name} id=${session.id} already in the tree view?`); + return; + } + + let state = this.oldState.get(session.name); + if (state === undefined) { + state = this.sessionPeripheralsMap.size === 0 ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.Collapsed; + } + const regs = new PeripheralTreeForSession(session, state, wsFolderPath, () => { + this._onDidChangeTreeData.fire(undefined); + }); + + this.sessionPeripheralsMap.set(session.id, regs); + let thresh = session.configuration[manifest.CONFIG_ADDRGAP]; + + if (!thresh) { + thresh = vscode.workspace.getConfiguration(manifest.PACKAGE_NAME).get(manifest.CONFIG_ADDRGAP) || manifest.DEFAULT_ADDRGAP; + } + + try { + await regs.sessionStarted(svdPath, thresh); // Should never reject + } catch (e) { + vscode.window.showErrorMessage(`Internal Error: Unexpected rejection of promise ${e}`); + } finally { + this._onDidChangeTreeData.fire(undefined); + } + + vscode.commands.executeCommand('setContext', `${manifest.PACKAGE_NAME}.svd.hasData`, this.sessionPeripheralsMap.size > 0); + } + + public debugSessionTerminated(session: vscode.DebugSession): void { + const regs = this.sessionPeripheralsMap.get(session.id); + + if (regs && regs.myTreeItem.collapsibleState) { + this.oldState.set(session.name, regs.myTreeItem.collapsibleState); + this.sessionPeripheralsMap.delete(session.id); + regs.sessionTerminated(); + this._onDidChangeTreeData.fire(undefined); + } + + vscode.commands.executeCommand('setContext', `${manifest.PACKAGE_NAME}.svd.hasData`, this.sessionPeripheralsMap.size > 0); + } + + public debugStopped(session: vscode.DebugSession): void { + const regs = this.sessionPeripheralsMap.get(session.id); + if (regs) { // We are called even before the session has started, as part of reset + regs.updateData(); + } + } + + public togglePinPeripheral(node: PeripheralBaseNode): void { + const session = vscode.debug.activeDebugSession; + if (session) { + const regs = this.sessionPeripheralsMap.get(session.id); + if (regs) { + regs.togglePinPeripheral(node); + this._onDidChangeTreeData.fire(undefined); + } + } + } +} diff --git a/src/vscode-utils.ts b/src/vscode-utils.ts new file mode 100644 index 0000000..cd44d98 --- /dev/null +++ b/src/vscode-utils.ts @@ -0,0 +1,54 @@ +/** + * Copyright (C) 2023 Arm Limited + */ + +import * as vscode from 'vscode'; + +export const uriExists = async (uri: vscode.Uri): Promise => { + try { + await vscode.workspace.fs.stat(uri); + return true; + } catch { + return false; + } +}; + +interface QuickPickItem extends vscode.QuickPickItem { + value?: string; +} + +export const getSelection = async (title: string, items: QuickPickItem[], value?: string): Promise => { + const disposables: vscode.Disposable[] = []; + try { + return await new Promise(resolve => { + const input = vscode.window.createQuickPick(); + input.title = title; + input.items = items; + if (value) { + input.value = value; + } + + for (const item of items) { + if (item.picked === true) { + input.value = item.label; + break; + } + } + + disposables.push( + input.onDidChangeSelection(items => { + const item = items[0] as QuickPickItem; + resolve(item.value || item.label); + input.hide(); + }), + input.onDidHide(() => { + resolve(undefined); + input.dispose(); + }) + ); + input.show(); + }); + } finally { + disposables.forEach(d => d.dispose()); + } +}; diff --git a/webpack.config.js b/webpack.config.js index 6777b4a..39b7042 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -60,6 +60,7 @@ module.exports = [ extensions: ['.tsx', '.ts', '.js'], fallback: { buffer: require.resolve('buffer/'), + path: require.resolve('path-browserify'), stream: require.resolve('stream-browserify'), timers: require.resolve('timers-browserify') } diff --git a/yarn.lock b/yarn.lock index e53a8d4..8ba0d04 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1741,6 +1741,11 @@ ignore@^5.1.8, ignore@^5.2.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== + import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -1929,6 +1934,16 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= +jszip@^3.10.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2" + integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g== + dependencies: + lie "~3.3.0" + pako "~1.0.2" + readable-stream "~2.3.6" + setimmediate "^1.0.5" + keygrip@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226" @@ -2036,6 +2051,13 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +lie@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" + integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== + dependencies: + immediate "~3.0.5" + linkify-it@^3.0.1: version "3.0.3" resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.3.tgz#a98baf44ce45a550efb4d49c769d07524cc2fa2e" @@ -2375,6 +2397,11 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +pako@~1.0.2: + version "1.0.11" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" + integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -2406,6 +2433,11 @@ parseurl@^1.3.2: resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== +path-browserify@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" + integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== + path-exists@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" @@ -2595,7 +2627,7 @@ read@^1.0.7: dependencies: mute-stream "~0.0.4" -readable-stream@^2.0.6, readable-stream@^2.3.0, readable-stream@^2.3.5: +readable-stream@^2.0.6, readable-stream@^2.3.0, readable-stream@^2.3.5, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -2795,7 +2827,7 @@ set-blocking@~2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= -setimmediate@^1.0.4: +setimmediate@^1.0.4, setimmediate@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==