diff --git a/examples/typescript/src/index.html b/examples/typescript/src/index.html
index 91299155..db84dc18 100644
--- a/examples/typescript/src/index.html
+++ b/examples/typescript/src/index.html
@@ -86,6 +86,10 @@
Console
+
+
+
+
diff --git a/examples/typescript/src/index.ts b/examples/typescript/src/index.ts
index 5c4b7738..f5114b76 100644
--- a/examples/typescript/src/index.ts
+++ b/examples/typescript/src/index.ts
@@ -19,13 +19,14 @@ const lblConsoleFor = document.getElementById("lblConsoleFor");
const lblConnTo = document.getElementById("lblConnTo");
const table = document.getElementById("fileTable") as HTMLTableElement;
const alertDiv = document.getElementById("alertDiv");
+const addElfFileButton = document.getElementById("addElfFile") as HTMLInputElement;
const debugLogging = document.getElementById("debugLogging") as HTMLInputElement;
// This is a frontend example of Esptool-JS using local bundle file
// To optimize use a CDN hosted version like
// https://unpkg.com/esptool-js@0.5.0/bundle.js
-import { ESPLoader, FlashOptions, LoaderOptions, Transport } from "../../../lib";
+import { ESPLoader, FlashOptions, LoaderOptions, Transport, AddressDecoder } from "../../../lib";
import { serial } from "web-serial-polyfill";
const serialLib = !navigator.serial && navigator.usb ? serial : navigator.serial;
@@ -72,6 +73,26 @@ function handleFileSelect(evt) {
reader.readAsBinaryString(file);
}
+/**
+ * File reader handler to read given local files.
+ * @param {Event} evt File Select event
+ */
+async function handleElfFileSelect(evt) {
+ const files = evt.target.files;
+
+ if (files.length === 0) return;
+ // get all files as an array of arrayBuffers
+ const elfFileBuffers = await Promise.all(Array.from(files).map((file: File) => file.arrayBuffer()));
+ await AddressDecoder.update(elfFileBuffers);
+}
+
+addElfFileButton.onchange = handleElfFileSelect;
+
+const encoder = new TextEncoder();
+export const stringToUInt8Array = function (textString: string) {
+ return encoder.encode(textString);
+};
+
const espLoaderTerminal = {
clean() {
term.clear();
@@ -231,12 +252,28 @@ disconnectButton.onclick = async () => {
cleanUp();
};
+/**
+ * Handles incoming data from the terminal and writes it to the transport device.
+ * @param {string} data - The string data received from the terminal.
+ */
+function onDataHandler(data: string) {
+ const writer = transport.device.writable?.getWriter();
+ if (writer) {
+ writer.write(stringToUInt8Array(data));
+ writer.releaseLock();
+ } else {
+ console.error("Unable to write to serial port");
+ }
+}
+let onDataDispose: () => void;
+
let isConsoleClosed = false;
consoleStartButton.onclick = async () => {
if (device === null) {
device = await serialLib.requestPort({});
transport = new Transport(device, true);
}
+ onDataDispose = term.onData(onDataHandler).dispose;
lblConsoleFor.style.display = "block";
lblConsoleBaudrate.style.display = "none";
consoleBaudrates.style.display = "none";
@@ -248,6 +285,10 @@ consoleStartButton.onclick = async () => {
await transport.connect(parseInt(consoleBaudrates.value));
isConsoleClosed = false;
+ const output = (line: string) => {
+ term.writeln(line);
+ };
+ let lastLine = "";
while (true && !isConsoleClosed) {
const readLoop = transport.rawRead();
const { value, done } = await readLoop.next();
@@ -255,12 +296,23 @@ consoleStartButton.onclick = async () => {
if (done || !value) {
break;
}
- term.write(value);
+
+ const valueStr = uInt8ArrayToString(value);
+ lastLine += valueStr;
+ const splitLine = lastLine.split("\r\n");
+ while (splitLine.length > 1) {
+ const line = splitLine.shift();
+ if (line !== undefined) {
+ AddressDecoder.parser(line, output);
+ }
+ }
+ lastLine = splitLine[0];
}
console.log("quitting console");
};
consoleStopButton.onclick = async () => {
+ onDataDispose();
isConsoleClosed = true;
if (transport) {
await transport.disconnect();
@@ -277,6 +329,19 @@ consoleStopButton.onclick = async () => {
cleanUp();
};
+/**
+ * Convert a Uint8Array to a string
+ * @param {Uint8Array} fileBuffer Uint8Array to convert
+ * @returns {string} String representation of the Uint8Array
+ */
+export function uInt8ArrayToString(fileBuffer: Uint8Array): string {
+ let fileBufferString = "";
+ for (let i = 0; i < fileBuffer.length; i++) {
+ fileBufferString += String.fromCharCode(fileBuffer[i]);
+ }
+ return fileBufferString;
+}
+
/**
* Validate the provided files images and offset to see if they're valid.
* @returns {string} Program input validation result
diff --git a/package-lock.json b/package-lock.json
index de78d61b..aa288cf3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,7 @@
"license": "Apache-2.0",
"dependencies": {
"atob-lite": "^2.0.0",
+ "jselftools": "^0.2.5",
"pako": "^2.1.0",
"tslib": "^2.4.1"
},
@@ -2524,6 +2525,12 @@
"node": ">=12.0.0"
}
},
+ "node_modules/jselftools": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/jselftools/-/jselftools-0.2.5.tgz",
+ "integrity": "sha512-OV1ef2ocEVa2BCYL3FO9kEaGsPNqbWb20ssnwnWOjxlZTSpGsrgIYJK083B4hJl+5zc7bLed3Aw3Y4zfAGLeZg==",
+ "license": "MIT"
+ },
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
@@ -5580,6 +5587,11 @@
"integrity": "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==",
"dev": true
},
+ "jselftools": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/jselftools/-/jselftools-0.2.5.tgz",
+ "integrity": "sha512-OV1ef2ocEVa2BCYL3FO9kEaGsPNqbWb20ssnwnWOjxlZTSpGsrgIYJK083B4hJl+5zc7bLed3Aw3Y4zfAGLeZg=="
+ },
"jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
diff --git a/package.json b/package.json
index 61fbcb8f..c0daaafc 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,7 @@
},
"dependencies": {
"atob-lite": "^2.0.0",
+ "jselftools": "^0.2.5",
"pako": "^2.1.0",
"tslib": "^2.4.1"
},
diff --git a/src/address_decoder.ts b/src/address_decoder.ts
new file mode 100644
index 00000000..7d172a55
--- /dev/null
+++ b/src/address_decoder.ts
@@ -0,0 +1,221 @@
+import ELFFile, { CompileUnit, DWARFInfo } from "jselftools";
+import { AddressLocation } from "./types/decoder";
+import { getSHA256 } from "./util";
+
+const ADDRESS_RE = /0x[0-9a-f]{8}/gi;
+
+type SubprogramInfo = [start: number, end: number, fnName: string, dwarfinfo?: DWARFInfo, CU?: CompileUnit];
+
+/**
+ * Class to check and decode an address
+ */
+export class AddressDecoder {
+ private static subprograms: SubprogramInfo[][] = [];
+ private static sha: string[] = [];
+ private static intervals: number[][] = [];
+
+ /**
+ * load elf files and filter for faster address decoding
+ * @param {ArrayBufferLike[]} elfFileBuffers elf file buffers
+ */
+ static async update(elfFileBuffers: ArrayBufferLike[]): Promise {
+ this.subprograms = [];
+ this.sha = [];
+ for (const elfFileBuffer of elfFileBuffers) {
+ const { subprograms, isRom } = await this.loadElfFile(elfFileBuffer);
+ if (!isRom) {
+ this.sha.push(await getSHA256(elfFileBuffer));
+ }
+ const start = subprograms[0][0];
+ const end = subprograms[subprograms.length - 1][1];
+ this.intervals.push([start, end]);
+ this.subprograms.push(subprograms);
+ }
+ }
+
+ /**
+ * load elf file and parse it
+ * @param {ArrayBufferLike} elfFileBuffer elf file buffer
+ * @returns {Promise} sorted subprograms
+ */
+ static async loadElfFile(elfFileBuffer: ArrayBufferLike): Promise<{
+ subprograms: SubprogramInfo[];
+ isRom: boolean;
+ }> {
+ const elffile = new ELFFile(elfFileBuffer);
+ const subprograms: SubprogramInfo[] = [];
+ let isRom = false;
+ if (elffile.has_dwarf_info()) {
+ // most app elf files have dwarf info
+ const dwarfinfo = elffile.get_dwarf_info();
+ for (const CU of dwarfinfo.get_CUs()) {
+ for (const die of CU.dies) {
+ if (die.has_children) {
+ for (const child of die.children) {
+ if (child.tag === "DW_TAG_subprogram") {
+ const lowPc = child.attributes["DW_AT_low_pc"];
+ const highPc = child.attributes["DW_AT_high_pc"];
+ if (lowPc && lowPc.value > 0 && highPc.value > 0) {
+ const fnName = child.attributes["DW_AT_name"]?.value;
+ subprograms.push([lowPc.value, highPc.value + lowPc.value, fnName, dwarfinfo, CU]);
+ }
+ }
+ }
+ }
+ }
+ }
+ } else {
+ // rom elf files don't have dwarf info
+ isRom = true;
+ const symtab = elffile.get_symtab();
+ if (symtab) {
+ for (const symbol of symtab.iter_symbols()) {
+ if (symbol.info.type == "STT_FUNC") {
+ const start = Number(symbol.value);
+ const end = start + Number(symbol.size);
+ subprograms.push([start, end, symbol.name]);
+ }
+ }
+ }
+ }
+ subprograms.sort((a, b) => a[0] - b[0]);
+ return { subprograms, isRom };
+ }
+
+ /**
+ * Given an address, decode it and return the function name and line
+ * @param { number } address the address to decode
+ * @returns { { fnName: string; line: AddressLocation | undefined } | undefined } the decoded address or undefined if it wasn't decoded
+ */
+ static getDecodedAddress(address: number): { fnName: string; line: AddressLocation | undefined } | undefined {
+ let i = 0;
+ for (const [start, end] of this.intervals) {
+ if (start <= address && address < end) {
+ for (const [start, end, fnName, dwarfinfo, cu] of this.subprograms[i]) {
+ if (end < address) {
+ continue;
+ }
+ if (start > address) {
+ // already after the function
+ break;
+ }
+ let line = undefined;
+ if (cu && dwarfinfo) {
+ line = this.checkLineprogram(cu, address, dwarfinfo);
+ }
+ return {
+ fnName: fnName,
+ line,
+ };
+ }
+ }
+ i++;
+ }
+ return undefined;
+ }
+
+ /**
+ * decode the address and call the output function with the decoded address
+ * @param {number} address the address to decode
+ * @param {(message: string) => void} outputFn the function to call with the decoded address
+ * @returns {boolean} true if the address was decoded, false otherwise
+ */
+ static decode(address: number, outputFn: (message: string) => void): boolean {
+ const decodedAddress = this.getDecodedAddress(address);
+ if (decodedAddress === undefined) {
+ return false;
+ }
+ const hexAddress = address.toString(16);
+ const { fnName, line } = decodedAddress;
+ let decodedLine = "0x" + hexAddress + ": " + fnName;
+ if (line !== undefined) {
+ decodedLine += ` at ${line.directory}/${line.filename}:${line.lineNumber}:${line.column}`;
+ if (line.discriminator > 0) {
+ decodedLine += ` (discriminator ${line.discriminator})`;
+ }
+ } else {
+ decodedLine += " in ROM";
+ }
+ outputFn(decodedLine);
+ return true;
+ }
+
+ static parser(line: string, outputFn: (message: string) => void) {
+ const parserOutput = (line: string) => {
+ outputFn("\x1b[33m-- " + line + "\x1b[0m");
+ };
+ const match = line.match(ADDRESS_RE);
+ if (match) {
+ outputFn(line);
+ const addrMap = match.map((hex) => parseInt(hex, 16));
+ let decoded = false;
+ for (const addr of addrMap) {
+ if (this.decode(addr, parserOutput)) {
+ decoded = true;
+ }
+ }
+ if (decoded) {
+ outputFn("");
+ }
+ return;
+ } else if (line.includes("ELF file SHA256:")) {
+ const hash = this.extractHash(line);
+ if (hash && this.sha) {
+ outputFn(line);
+ if (this.sha.length !== 0) {
+ let foundHash = false;
+ for (const sha of this.sha) {
+ if (sha.startsWith(hash)) {
+ foundHash = true;
+ }
+ }
+ if (!foundHash) {
+ parserOutput(
+ "Warning: Checksum mismatch between flashed and built applications. Checksum of built application is " +
+ this.sha.join(", "),
+ );
+ }
+ }
+ return;
+ }
+ }
+ outputFn(line);
+ }
+
+ static extractHash(line: string): string {
+ const pattern = /(?:I \(\d+\) cpu_start: )?ELF file SHA256:\s+(\w+)/;
+ const match = line.match(pattern);
+ return match ? match[1] : "";
+ }
+
+ private static checkLineprogram(cu: CompileUnit, address: number, dwarfinfo: DWARFInfo): AddressLocation | undefined {
+ const lineprog = dwarfinfo.line_program_for_CU(cu);
+ if (!lineprog) {
+ return undefined;
+ }
+ const delta = lineprog.header.version < 5 ? 1 : 0;
+ let prevstate = null;
+ for (const entry of lineprog.get_entries()) {
+ if (entry.state === null) {
+ continue;
+ }
+ if (prevstate && prevstate.address <= address && address < entry.state.address) {
+ const filename = lineprog.header.file_entry[prevstate.file - delta];
+ const directory = lineprog.header.include_directory[filename.dir_index - delta];
+ return {
+ directory,
+ filename: filename.name,
+ lineNumber: prevstate.line,
+ column: prevstate.column,
+ discriminator: prevstate.discriminator,
+ };
+ }
+ if (entry.state.end_sequence) {
+ prevstate = null;
+ } else {
+ prevstate = entry.state;
+ }
+ }
+ return undefined;
+ }
+}
diff --git a/src/index.ts b/src/index.ts
index 438f978f..84a5b1a1 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -14,3 +14,4 @@ export { LoaderOptions } from "./types/loaderOptions.js";
export { FlashOptions } from "./types/flashOptions.js";
export { IEspLoaderTerminal } from "./types/loaderTerminal.js";
export { Before, After } from "./types/resetModes.js";
+export { AddressDecoder } from "./address_decoder";
diff --git a/src/types/decoder.ts b/src/types/decoder.ts
new file mode 100644
index 00000000..687cc917
--- /dev/null
+++ b/src/types/decoder.ts
@@ -0,0 +1,7 @@
+export interface AddressLocation {
+ directory: string;
+ filename: string;
+ lineNumber: number;
+ column: number;
+ discriminator: number;
+}
diff --git a/src/util.ts b/src/util.ts
index 1a4d437e..9dcf1aa4 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -16,3 +16,15 @@ export function padTo(data: Uint8Array, alignment: number, padCharacter = 0xff):
}
return data;
}
+
+/**
+ * get the SHA256 hash of an ArrayBuffer
+ * @param {ArrayBufferLike} arrayBuffer ArrayBuffer to hash
+ * @returns {string} SHA256 hash of the ArrayBuffer
+ */
+export async function getSHA256(arrayBuffer: ArrayBufferLike): Promise {
+ const hashBuffer = await crypto.subtle.digest("SHA-256", arrayBuffer);
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
+ const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
+ return hashHex;
+}