From 260e5eab3f602a3e7ad5812857276641f318981a Mon Sep 17 00:00:00 2001 From: Brian Ignacio Date: Thu, 7 Dec 2023 18:57:47 +0800 Subject: [PATCH] further refactoring of transport and esploader --- src/esploader.ts | 275 ++++++++++++---------------- src/index.ts | 2 +- src/reset.ts | 2 +- src/transport/AbstractTransport.ts | 84 +++++++++ src/transport/ITransport.ts | 50 ----- src/transport/WebSerialTransport.ts | 84 ++------- src/utils/convert.ts | 78 ++++++++ src/utils/hex.ts | 44 +++++ src/utils/slip.ts | 20 ++ 9 files changed, 355 insertions(+), 284 deletions(-) create mode 100644 src/transport/AbstractTransport.ts delete mode 100644 src/transport/ITransport.ts create mode 100644 src/utils/convert.ts create mode 100644 src/utils/hex.ts create mode 100644 src/utils/slip.ts diff --git a/src/esploader.ts b/src/esploader.ts index 96673abb..554b1f5c 100644 --- a/src/esploader.ts +++ b/src/esploader.ts @@ -1,8 +1,10 @@ import { ESPError } from "./error"; import { Data, deflate, Inflate } from "pako"; -import { AbstractTransport, ISerialOptions } from "./transport/ITransport"; +import { AbstractTransport, ISerialOptions } from "./transport/AbstractTransport"; import { ROM } from "./targets/rom"; -import { customReset, usbJTAGSerialReset } from "./reset"; +import { classicReset, customReset, hardReset, usbJTAGSerialReset } from "./reset"; +import { hexConvert } from "./utils/hex"; +import { appendArray, bstrToUi8, byteArrayToInt, intToByteArray, shortToBytearray, ui8ToBstr } from "./utils/convert"; /** * Options for flashing a device with firmware. @@ -58,6 +60,32 @@ export interface FlashOptions { calculateMD5Hash?: (image: string) => string; } +/** + * Set of reset functions for ESP Loader connection. + * @interface ResetFunctions + */ +export interface ResetFunctions { + /** + * Execute a classic set of commands that will reset the chip. + */ + classicReset: (transport: AbstractTransport, resetDelay?: number) => Promise; + + /** + * Execute a set of commands for USB JTAG serial reset. + */ + usbJTAGSerialReset: (transport: AbstractTransport) => Promise; + + /** + * Execute a set of commands that will hard reset the chip. + */ + hardReset: (transport: AbstractTransport, usingUsbOtg?: boolean) => Promise; + + /** + * Custom reset strategy defined with a string. + */ + customReset: (transport: AbstractTransport, sequenceString: string) => Promise; +} + /** * Options to configure ESPLoader. * @interface LoaderOptions @@ -92,6 +120,11 @@ export interface LoaderOptions { * @type {boolean} */ debugLogging?: boolean; + + /** + * Reset functions for connection. If undefined will use default ones. + */ + resetFunctions?: ResetFunctions; } /** @@ -243,6 +276,7 @@ export class ESPLoader { private romBaudrate = 115200; public serialOptions: SerialOptions; private debugLogging = false; + private resetFunctions: ResetFunctions; /** * Create a new ESPLoader to perform serial communication @@ -257,6 +291,12 @@ export class ESPLoader { this.FLASH_WRITE_SIZE = 0x4000; this.serialOptions = options.serialOptions; this.transport = options.transport; + this.resetFunctions = { + classicReset, + customReset, + hardReset, + usbJTAGSerialReset, + }; if (options.romBaudrate) { this.romBaudrate = options.romBaudrate; } @@ -264,6 +304,18 @@ export class ESPLoader { this.terminal = options.terminal; this.terminal.clean(); } + if (options.resetFunctions?.classicReset) { + this.resetFunctions.classicReset = options.resetFunctions?.classicReset; + } + if (options.resetFunctions?.customReset) { + this.resetFunctions.customReset = options.resetFunctions?.customReset; + } + if (options.resetFunctions?.hardReset) { + this.resetFunctions.hardReset = options.resetFunctions?.hardReset; + } + if (options.resetFunctions?.usbJTAGSerialReset) { + this.resetFunctions.usbJTAGSerialReset = options.resetFunctions?.usbJTAGSerialReset; + } if (typeof options.debugLogging !== "undefined") { this.debugLogging = options.debugLogging; } @@ -323,98 +375,6 @@ export class ESPLoader { } } - /** - * Convert short integer to byte array - * @param {number} i - Number to convert. - * @returns {Uint8Array} Byte array. - */ - _shortToBytearray(i: number): Uint8Array { - return new Uint8Array([i & 0xff, (i >> 8) & 0xff]); - } - - /** - * Convert an integer to byte array - * @param {number} i - Number to convert. - * @returns {ROM} The chip ROM class related to given magic hex number. - */ - _intToByteArray(i: number): Uint8Array { - return new Uint8Array([i & 0xff, (i >> 8) & 0xff, (i >> 16) & 0xff, (i >> 24) & 0xff]); - } - - /** - * Convert a byte array to short integer. - * @param {number} i - Number to convert. - * @param {number} j - Number to convert. - * @returns {number} Return a short integer number. - */ - _byteArrayToShort(i: number, j: number): number { - return i | (j >> 8); - } - - /** - * Convert a byte array to integer. - * @param {number} i - Number to convert. - * @param {number} j - Number to convert. - * @param {number} k - Number to convert. - * @param {number} l - Number to convert. - * @returns {number} Return a integer number. - */ - _byteArrayToInt(i: number, j: number, k: number, l: number): number { - return i | (j << 8) | (k << 16) | (l << 24); - } - - /** - * Append a buffer array after another buffer array - * @param {ArrayBuffer} buffer1 - First array buffer. - * @param {ArrayBuffer} buffer2 - magic hex number to select ROM. - * @returns {ArrayBufferLike} Return an array buffer. - */ - _appendBuffer(buffer1: ArrayBuffer, buffer2: ArrayBuffer): ArrayBufferLike { - const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength); - tmp.set(new Uint8Array(buffer1), 0); - tmp.set(new Uint8Array(buffer2), buffer1.byteLength); - return tmp.buffer; - } - - /** - * Append a buffer array after another buffer array - * @param {Uint8Array} arr1 - First array buffer. - * @param {Uint8Array} arr2 - magic hex number to select ROM. - * @returns {Uint8Array} Return a 8 bit unsigned array. - */ - _appendArray(arr1: Uint8Array, arr2: Uint8Array): Uint8Array { - const c = new Uint8Array(arr1.length + arr2.length); - c.set(arr1, 0); - c.set(arr2, arr1.length); - return c; - } - - /** - * Convert a unsigned 8 bit integer array to byte string. - * @param {Uint8Array} u8Array - magic hex number to select ROM. - * @returns {string} Return the equivalent string. - */ - ui8ToBstr(u8Array: Uint8Array): string { - let bStr = ""; - for (let i = 0; i < u8Array.length; i++) { - bStr += String.fromCharCode(u8Array[i]); - } - return bStr; - } - - /** - * Convert a byte string to unsigned 8 bit integer array. - * @param {string} bStr - binary string input - * @returns {Uint8Array} Return a 8 bit unsigned integer array. - */ - bstrToUi8(bStr: string): Uint8Array { - const u8Array = new Uint8Array(bStr.length); - for (let i = 0; i < bStr.length; i++) { - u8Array[i] = bStr.charCodeAt(i); - } - return u8Array; - } - /** * Flush the serial input by raw read with 200 ms timeout. */ @@ -438,7 +398,7 @@ export class ESPLoader { const p = await this.transport.read(timeout); const resp = p[0]; const opRet = p[1]; - const val = this._byteArrayToInt(p[4], p[5], p[6], p[7]); + const val = byteArrayToInt(p[4], p[5], p[6], p[7]); const data = p.slice(8); if (resp == 1) { if (op == null || opRet == op) { @@ -473,19 +433,19 @@ export class ESPLoader { this.transport.trace( `command op:0x${op.toString(16).padStart(2, "0")} data len=${data.length} wait_response=${ waitResponse ? 1 : 0 - } timeout=${(timeout / 1000).toFixed(3)} data=${this.transport.hexConvert(data)}`, + } timeout=${(timeout / 1000).toFixed(3)} data=${hexConvert(data)}`, ); } const pkt = new Uint8Array(8 + data.length); pkt[0] = 0x00; pkt[1] = op; - pkt[2] = this._shortToBytearray(data.length)[0]; - pkt[3] = this._shortToBytearray(data.length)[1]; - pkt[4] = this._intToByteArray(chk)[0]; - pkt[5] = this._intToByteArray(chk)[1]; - pkt[6] = this._intToByteArray(chk)[2]; - pkt[7] = this._intToByteArray(chk)[3]; + pkt[2] = shortToBytearray(data.length)[0]; + pkt[3] = shortToBytearray(data.length)[1]; + pkt[4] = intToByteArray(chk)[0]; + pkt[5] = intToByteArray(chk)[1]; + pkt[6] = intToByteArray(chk)[2]; + pkt[7] = intToByteArray(chk)[3]; let i; for (i = 0; i < data.length; i++) { @@ -508,7 +468,7 @@ export class ESPLoader { * @returns {Promise} - Command number value */ async readReg(addr: number, timeout: number = 3000): Promise { - const pkt = this._intToByteArray(addr); + const pkt = intToByteArray(addr); const val = await this.command(this.ESP_READ_REG, pkt, undefined, undefined, timeout); return val[0]; } @@ -528,15 +488,15 @@ export class ESPLoader { delayUs: number = 0, delayAfterUs: number = 0, ) { - let pkt = this._appendArray(this._intToByteArray(addr), this._intToByteArray(value)); - pkt = this._appendArray(pkt, this._intToByteArray(mask)); - pkt = this._appendArray(pkt, this._intToByteArray(delayUs)); + let pkt = appendArray(intToByteArray(addr), intToByteArray(value)); + pkt = appendArray(pkt, intToByteArray(mask)); + pkt = appendArray(pkt, intToByteArray(delayUs)); if (delayAfterUs > 0) { - pkt = this._appendArray(pkt, this._intToByteArray(this.chip.UART_DATE_REG_ADDR)); - pkt = this._appendArray(pkt, this._intToByteArray(0)); - pkt = this._appendArray(pkt, this._intToByteArray(0)); - pkt = this._appendArray(pkt, this._intToByteArray(delayAfterUs)); + pkt = appendArray(pkt, intToByteArray(this.chip.UART_DATE_REG_ADDR)); + pkt = appendArray(pkt, intToByteArray(0)); + pkt = appendArray(pkt, intToByteArray(0)); + pkt = appendArray(pkt, intToByteArray(delayAfterUs)); } await this.checkCommand("write target memory", this.ESP_WRITE_REG, pkt); @@ -579,10 +539,10 @@ export class ESPLoader { if (this.transport.getPID() === this.USB_JTAG_SERIAL_PID) { // Custom reset sequence, which is required when the device // is connecting via its USB-JTAG-Serial peripheral - await usbJTAGSerialReset(this.transport); + await this.resetFunctions.usbJTAGSerialReset(this.transport); } else { const strSequence = esp32r0Delay ? "D0|R1|W100|W2000|D1|R0|W50|D0" : "D0|R1|W100|D1|R0|W50|D0"; - await customReset(this.transport, strSequence); + await this.resetFunctions.customReset(this.transport, strSequence); } } let i = 0; @@ -708,9 +668,9 @@ export class ESPLoader { async memBegin(size: number, blocks: number, blocksize: number, offset: number) { /* XXX: Add check to ensure that STUB is not getting overwritten */ this.debug("mem_begin " + size + " " + blocks + " " + blocksize + " " + offset.toString(16)); - let pkt = this._appendArray(this._intToByteArray(size), this._intToByteArray(blocks)); - pkt = this._appendArray(pkt, this._intToByteArray(blocksize)); - pkt = this._appendArray(pkt, this._intToByteArray(offset)); + let pkt = appendArray(intToByteArray(size), intToByteArray(blocks)); + pkt = appendArray(pkt, intToByteArray(blocksize)); + pkt = appendArray(pkt, intToByteArray(offset)); await this.checkCommand("enter RAM download mode", this.ESP_MEM_BEGIN, pkt); } @@ -735,10 +695,10 @@ export class ESPLoader { * @param {number} seq Sequence number */ async memBlock(buffer: Uint8Array, seq: number) { - let pkt = this._appendArray(this._intToByteArray(buffer.length), this._intToByteArray(seq)); - pkt = this._appendArray(pkt, this._intToByteArray(0)); - pkt = this._appendArray(pkt, this._intToByteArray(0)); - pkt = this._appendArray(pkt, buffer); + let pkt = appendArray(intToByteArray(buffer.length), intToByteArray(seq)); + pkt = appendArray(pkt, intToByteArray(0)); + pkt = appendArray(pkt, intToByteArray(0)); + pkt = appendArray(pkt, buffer); const checksum = this.checksum(buffer); await this.checkCommand("write to target RAM", this.ESP_MEM_DATA, pkt, checksum); } @@ -749,7 +709,7 @@ export class ESPLoader { */ async memFinish(entrypoint: number) { const isEntry = entrypoint === 0 ? 1 : 0; - const pkt = this._appendArray(this._intToByteArray(isEntry), this._intToByteArray(entrypoint)); + const pkt = appendArray(intToByteArray(isEntry), intToByteArray(entrypoint)); await this.checkCommand("leave RAM download mode", this.ESP_MEM_END, pkt, undefined, 50); // XXX: handle non-stub with diff timeout } @@ -758,7 +718,7 @@ export class ESPLoader { * @param {number} hspiArg - Argument for SPI attachment */ async flashSpiAttach(hspiArg: number) { - const pkt = this._intToByteArray(hspiArg); + const pkt = intToByteArray(hspiArg); await this.checkCommand("configure SPI flash pins", this.ESP_SPI_ATTACH, pkt); } @@ -796,11 +756,11 @@ export class ESPLoader { } this.debug("flash begin " + eraseSize + " " + numBlocks + " " + this.FLASH_WRITE_SIZE + " " + offset + " " + size); - let pkt = this._appendArray(this._intToByteArray(eraseSize), this._intToByteArray(numBlocks)); - pkt = this._appendArray(pkt, this._intToByteArray(this.FLASH_WRITE_SIZE)); - pkt = this._appendArray(pkt, this._intToByteArray(offset)); + let pkt = appendArray(intToByteArray(eraseSize), intToByteArray(numBlocks)); + pkt = appendArray(pkt, intToByteArray(this.FLASH_WRITE_SIZE)); + pkt = appendArray(pkt, intToByteArray(offset)); if (this.IS_STUB == false) { - pkt = this._appendArray(pkt, this._intToByteArray(0)); // XXX: Support encrypted + pkt = appendArray(pkt, intToByteArray(0)); // XXX: Support encrypted } await this.checkCommand("enter Flash download mode", this.ESP_FLASH_BEGIN, pkt, undefined, timeout); @@ -836,9 +796,9 @@ export class ESPLoader { } this.info("Compressed " + size + " bytes to " + compsize + "..."); - let pkt = this._appendArray(this._intToByteArray(writeSize), this._intToByteArray(numBlocks)); - pkt = this._appendArray(pkt, this._intToByteArray(this.FLASH_WRITE_SIZE)); - pkt = this._appendArray(pkt, this._intToByteArray(offset)); + let pkt = appendArray(intToByteArray(writeSize), intToByteArray(numBlocks)); + pkt = appendArray(pkt, intToByteArray(this.FLASH_WRITE_SIZE)); + pkt = appendArray(pkt, intToByteArray(offset)); if ( (this.chip.CHIP_NAME === "ESP32-S2" || @@ -847,7 +807,7 @@ export class ESPLoader { this.chip.CHIP_NAME === "ESP32-C2") && this.IS_STUB === false ) { - pkt = this._appendArray(pkt, this._intToByteArray(0)); + pkt = appendArray(pkt, intToByteArray(0)); } await this.checkCommand("enter compressed flash mode", this.ESP_FLASH_DEFL_BEGIN, pkt, undefined, timeout); const t2 = d.getTime(); @@ -864,10 +824,10 @@ export class ESPLoader { * @param {number} timeout Timeout in milliseconds (ms) */ async flashBlock(data: Uint8Array, seq: number, timeout: number) { - let pkt = this._appendArray(this._intToByteArray(data.length), this._intToByteArray(seq)); - pkt = this._appendArray(pkt, this._intToByteArray(0)); - pkt = this._appendArray(pkt, this._intToByteArray(0)); - pkt = this._appendArray(pkt, data); + let pkt = appendArray(intToByteArray(data.length), intToByteArray(seq)); + pkt = appendArray(pkt, intToByteArray(0)); + pkt = appendArray(pkt, intToByteArray(0)); + pkt = appendArray(pkt, data); const checksum = this.checksum(data); @@ -881,10 +841,10 @@ export class ESPLoader { * @param {number} timeout Timeout in milliseconds (ms) */ async flashDeflBlock(data: Uint8Array, seq: number, timeout: number) { - let pkt = this._appendArray(this._intToByteArray(data.length), this._intToByteArray(seq)); - pkt = this._appendArray(pkt, this._intToByteArray(0)); - pkt = this._appendArray(pkt, this._intToByteArray(0)); - pkt = this._appendArray(pkt, data); + let pkt = appendArray(intToByteArray(data.length), intToByteArray(seq)); + pkt = appendArray(pkt, intToByteArray(0)); + pkt = appendArray(pkt, intToByteArray(0)); + pkt = appendArray(pkt, data); const checksum = this.checksum(data); this.debug("flash_defl_block " + data[0].toString(16) + " " + data[1].toString(16)); @@ -904,7 +864,7 @@ export class ESPLoader { */ async flashFinish(reboot: boolean = false) { const val = reboot ? 0 : 1; - const pkt = this._intToByteArray(val); + const pkt = intToByteArray(val); await this.checkCommand("leave Flash mode", this.ESP_FLASH_END, pkt); } @@ -915,7 +875,7 @@ export class ESPLoader { */ async flashDeflFinish(reboot: boolean = false) { const val = reboot ? 0 : 1; - const pkt = this._intToByteArray(val); + const pkt = intToByteArray(val); await this.checkCommand("leave compressed flash mode", this.ESP_FLASH_DEFL_END, pkt); } @@ -1002,11 +962,11 @@ export class ESPLoader { } else { if (data.length % 4 != 0) { const padding = new Uint8Array(data.length % 4); - data = this._appendArray(data, padding); + data = appendArray(data, padding); } let nextReg = SPI_W0_REG; for (i = 0; i < data.length - 4; i += 4) { - val = this._byteArrayToInt(data[i], data[i + 1], data[i + 2], data[i + 3]); + val = byteArrayToInt(data[i], data[i + 1], data[i + 2], data[i + 3]); await this.writeReg(nextReg, val); nextReg += 4; } @@ -1075,9 +1035,9 @@ export class ESPLoader { */ async flashMd5sum(addr: number, size: number): Promise { const timeout = this.timeoutPerMb(this.MD5_TIMEOUT_PER_MB, size); - let pkt = this._appendArray(this._intToByteArray(addr), this._intToByteArray(size)); - pkt = this._appendArray(pkt, this._intToByteArray(0)); - pkt = this._appendArray(pkt, this._intToByteArray(0)); + let pkt = appendArray(intToByteArray(addr), intToByteArray(size)); + pkt = appendArray(pkt, intToByteArray(0)); + pkt = appendArray(pkt, intToByteArray(0)); let res = await this.checkCommand("calculate md5sum", this.ESP_SPI_FLASH_MD5, pkt, undefined, timeout); if (res instanceof Uint8Array && res.length > 16) { @@ -1088,9 +1048,9 @@ export class ESPLoader { } async readFlash(addr: number, size: number, onPacketReceived: FlashReadCallback = null) { - let pkt = this._appendArray(this._intToByteArray(addr), this._intToByteArray(size)); - pkt = this._appendArray(pkt, this._intToByteArray(0x1000)); - pkt = this._appendArray(pkt, this._intToByteArray(1024)); + let pkt = appendArray(intToByteArray(addr), intToByteArray(size)); + pkt = appendArray(pkt, intToByteArray(0x1000)); + pkt = appendArray(pkt, intToByteArray(1024)); const res = await this.checkCommand("read flash", this.ESP_READ_FLASH, pkt); @@ -1104,8 +1064,8 @@ export class ESPLoader { if (packet instanceof Uint8Array) { if (packet.length > 0) { - resp = this._appendArray(resp, packet); - await this.transport.write(this._intToByteArray(resp.length)); + resp = appendArray(resp, packet); + await this.transport.write(intToByteArray(resp.length)); if (onPacketReceived) { onPacketReceived(packet, resp.length, size); @@ -1178,7 +1138,7 @@ export class ESPLoader { async changeBaud() { this.info("Changing baudrate to " + this.serialOptions.baudRate); const secondArg = this.IS_STUB ? this.romBaudrate : 0; - const pkt = this._appendArray(this._intToByteArray(this.serialOptions.baudRate), this._intToByteArray(secondArg)); + const pkt = appendArray(intToByteArray(this.serialOptions.baudRate), intToByteArray(secondArg)); const resp = await this.command(this.ESP_CHANGE_BAUDRATE, pkt); this.debug(resp[0].toString()); this.info("Changed"); @@ -1359,8 +1319,8 @@ export class ESPLoader { const uncsize = image.length; let blocks: number; if (options.compress) { - const uncimage = this.bstrToUi8(image); - image = this.ui8ToBstr(deflate(uncimage, { level: 9 })); + const uncimage = bstrToUi8(image); + image = ui8ToBstr(deflate(uncimage, { level: 9 })); blocks = await this.flashDeflBegin(uncsize, image.length, address); } else { blocks = await this.flashBegin(uncsize, address); @@ -1390,7 +1350,7 @@ export class ESPLoader { Math.floor((100 * (seq + 1)) / blocks) + "%)", ); - const block = this.bstrToUi8(image.slice(0, this.FLASH_WRITE_SIZE)); + const block = bstrToUi8(image.slice(0, this.FLASH_WRITE_SIZE)); if (options.compress) { const lenUncompressedPrevious = totalLenUncompressed; @@ -1477,15 +1437,6 @@ export class ESPLoader { return this.DETECTED_FLASH_SIZES_NUM[flidLowbyte]; } - /** - * Perform a chip hard reset by setting RTS to LOW and then HIGH. - */ - async hardReset() { - await this.transport.setRTS(true); // EN->LOW - await this._sleep(100); - await this.transport.setRTS(false); - } - /** * Soft reset the device chip. Soft reset with run user code is the closest. */ diff --git a/src/index.ts b/src/index.ts index e5d94d0c..8f4cbbe7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ export { IEspLoaderTerminal, ESPLoader, FlashOptions, LoaderOptions } from "./esploader"; export { classicReset, customReset, hardReset, usbJTAGSerialReset, validateCustomResetStringSequence } from "./reset"; export { ROM } from "./targets/rom"; -export { AbstractTransport, ISerialOptions } from "./transport/ITransport"; +export { AbstractTransport, ISerialOptions } from "./transport/AbstractTransport"; export { SerialOptions, WebSerialTransport } from "./transport/WebSerialTransport"; diff --git a/src/reset.ts b/src/reset.ts index d984aaec..cf99095b 100644 --- a/src/reset.ts +++ b/src/reset.ts @@ -1,4 +1,4 @@ -import { AbstractTransport } from "./transport/ITransport"; +import { AbstractTransport } from "./transport/AbstractTransport"; const DEFAULT_RESET_DELAY = 50; diff --git a/src/transport/AbstractTransport.ts b/src/transport/AbstractTransport.ts new file mode 100644 index 00000000..e8ef7bd6 --- /dev/null +++ b/src/transport/AbstractTransport.ts @@ -0,0 +1,84 @@ +/** + * Options for device connection. + * @interface ISerialOptions + */ +export interface ISerialOptions { + /** + * A positive, non-zero value indicating the baud rate at which serial communication should be established. + * @type {number} + */ + baudRate: number; +} + +/** + * Template to define Transport class which can be consumed by the ESPLoader. + * A Webserial reference implementation is found at src/transport/WebSerialTransport.ts + */ +export abstract class AbstractTransport { + public abstract tracing: boolean; + public abstract slipReaderEnabled: boolean; + + /** + * Request the serial device information as string. + * @returns {string} Return the serial device information as formatted string. + */ + public abstract getInfo(): string; + + /** + * Request the serial device product id. + * @returns {number | undefined} Return the product ID. + */ + public abstract getPID(): number | undefined; + + /** + * Format received or sent data for tracing output. + * @param {string} message Message to format as trace line. + */ + public abstract trace(message: string): void; + + /** + * Write binary data to device. + * @param {Uint8Array} data 8 bit unsigned data array to write to device. + */ + public abstract write(data: Uint8Array): Promise; + + /** + * Read from serial device. + * @param {number} timeout Read timeout number + * @param {number} minData Minimum packet array length + * @returns {Promise} 8 bit unsigned data array read from device. + */ + public abstract read(timeout?: number, minData?: number): Promise; + + /** + * Read from serial device without formatting. + * @param {number} timeout Read timeout in milliseconds (ms) + * @returns {Promise} 8 bit unsigned data array read from device. + */ + public abstract rawRead(timeout?: number): Promise; + + /** + * Send the RequestToSend (RTS) signal to given state + * # True for EN=LOW, chip in reset and False EN=HIGH, chip out of reset + * @param {boolean} state Boolean state to set the signal + */ + public abstract setRTS(state: boolean): Promise; + + /** + * Send the dataTerminalReady (DTS) signal to given state + * # True for IO0=LOW, chip in reset and False IO0=HIGH + * @param {boolean} state Boolean state to set the signal + */ + public abstract setDTR(state: boolean): Promise; + + /** + * Connect to serial device using the Webserial open method. + * @param {SerialOptions} serialOptions Serial Options for WebUSB SerialPort class. + */ + public abstract connect(serialOptions: ISerialOptions): Promise; + + /** + * Disconnect from serial device by running SerialPort.close() after streams unlock. + */ + public abstract disconnect(): Promise; +} diff --git a/src/transport/ITransport.ts b/src/transport/ITransport.ts deleted file mode 100644 index 0260b4d0..00000000 --- a/src/transport/ITransport.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Options for device connection. - * @interface ISerialOptions - */ -export interface ISerialOptions { - /** - * A positive, non-zero value indicating the baud rate at which serial communication should be established. - * @type {number} - */ - baudRate: number; -} - -/** - * Template to define Transport class which can be consumed by the ESPLoader. - * A Webserial reference implementation is found at src/transport/WebSerialTransport.ts - */ -export abstract class AbstractTransport { - public abstract tracing: boolean; - public abstract slipReaderEnabled: boolean; - - public abstract getInfo(): string; - - public abstract getPID(): number | undefined; - - public abstract trace(message: string): void; - - public abstract returnTrace(): Promise; - - public abstract hexify(s: Uint8Array): string; - - public abstract hexConvert(uint8Array: Uint8Array, autoSplit?: boolean): string; - - public abstract slipWriter(data: Uint8Array): Uint8Array; - - public abstract write(data: Uint8Array): Promise; - - public abstract slipReader(data: Uint8Array): Uint8Array; - - public abstract read(timeout?: number, minData?: number): Promise; - - public abstract rawRead(timeout?: number): Promise; - - public abstract setRTS(state: boolean): Promise; - - public abstract setDTR(state: boolean): Promise; - - public abstract connect(serialOptions: ISerialOptions): Promise; - - public abstract disconnect(): Promise; -} diff --git a/src/transport/WebSerialTransport.ts b/src/transport/WebSerialTransport.ts index 02f0bc9a..3d167b0a 100644 --- a/src/transport/WebSerialTransport.ts +++ b/src/transport/WebSerialTransport.ts @@ -1,6 +1,9 @@ /* global SerialPort, ParityType, FlowControlType */ -import { AbstractTransport, ISerialOptions } from "./ITransport"; +import { AbstractTransport, ISerialOptions } from "./AbstractTransport"; +import { hexConvert } from "../utils/hex"; +import { appendArray } from "../utils/convert"; +import { slipWriter } from "../utils/slip"; /** * Options for device serialPort. @@ -86,6 +89,9 @@ export class WebSerialTransport implements AbstractTransport { this.traceLog += traceMessage + "\n"; } + /** + * Return the whole trace output to the user clipboard. + */ async returnTrace() { try { await navigator.clipboard.writeText(this.traceLog); @@ -95,86 +101,24 @@ export class WebSerialTransport implements AbstractTransport { } } - hexify(s: Uint8Array) { - return Array.from(s) - .map((byte) => byte.toString(16).padStart(2, "0")) - .join("") - .padEnd(16, " "); - } - - hexConvert(uint8Array: Uint8Array, autoSplit = true) { - if (autoSplit && uint8Array.length > 16) { - let result = ""; - let s = uint8Array; - - while (s.length > 0) { - const line = s.slice(0, 16); - const asciiLine = String.fromCharCode(...line) - .split("") - .map((c) => (c === " " || (c >= " " && c <= "~" && c !== " ") ? c : ".")) - .join(""); - s = s.slice(16); - result += `\n ${this.hexify(line.slice(0, 8))} ${this.hexify(line.slice(8))} | ${asciiLine}`; - } - - return result; - } else { - return this.hexify(uint8Array); - } - } - - /** - * Format data packet using the Serial Line Internet Protocol (SLIP). - * @param {Uint8Array} data Binary unsigned 8 bit array data to format. - * @returns {Uint8Array} Formatted unsigned 8 bit data array. - */ - slipWriter(data: Uint8Array) { - const outData = []; - outData.push(0xc0); - for (let i = 0; i < data.length; i++) { - if (data[i] === 0xdb) { - outData.push(0xdb, 0xdd); - } else if (data[i] === 0xc0) { - outData.push(0xdb, 0xdc); - } else { - outData.push(data[i]); - } - } - outData.push(0xc0); - return new Uint8Array(outData); - } - /** * Write binary data to device using the WebSerial device writable stream. * @param {Uint8Array} data 8 bit unsigned data array to write to device. */ async write(data: Uint8Array) { - const outData = this.slipWriter(data); + const outData = slipWriter(data); if (this.device.writable) { const writer = this.device.writable.getWriter(); if (this.tracing) { console.log("Write bytes"); - this.trace(`Write ${outData.length} bytes: ${this.hexConvert(outData)}`); + this.trace(`Write ${outData.length} bytes: ${hexConvert(outData)}`); } await writer.write(outData); writer.releaseLock(); } } - /** - * Concatenate buffer2 to buffer1 and return the resulting ArrayBuffer. - * @param {ArrayBuffer} buffer1 First buffer to concatenate. - * @param {ArrayBuffer} buffer2 Second buffer to concatenate. - * @returns {ArrayBuffer} Result Array buffer. - */ - _appendBuffer(buffer1: ArrayBuffer, buffer2: ArrayBuffer) { - const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength); - tmp.set(new Uint8Array(buffer1), 0); - tmp.set(new Uint8Array(buffer2), buffer1.byteLength); - return tmp.buffer; - } - /** * Take a data array and return the first well formed packet after * replacing the escape sequence. Reads at least 8 bytes. @@ -260,7 +204,7 @@ export class WebSerialTransport implements AbstractTransport { this.leftOver = packet; throw new Error("Timeout"); } - const p = new Uint8Array(this._appendBuffer(packet.buffer, value.buffer)); + const p = appendArray(packet, value); packet = p; } while (packet.length < minData); } finally { @@ -272,14 +216,14 @@ export class WebSerialTransport implements AbstractTransport { if (this.tracing) { console.log("Read bytes"); - this.trace(`Read ${packet.length} bytes: ${this.hexConvert(packet)}`); + this.trace(`Read ${packet.length} bytes: ${hexConvert(packet)}`); } if (this.slipReaderEnabled) { const slipReaderResult = this.slipReader(packet); if (this.tracing) { console.log("Slip reader results"); - this.trace(`Read ${slipReaderResult.length} bytes: ${this.hexConvert(slipReaderResult)}`); + this.trace(`Read ${slipReaderResult.length} bytes: ${hexConvert(slipReaderResult)}`); } return slipReaderResult; } @@ -314,7 +258,7 @@ export class WebSerialTransport implements AbstractTransport { } if (this.tracing) { console.log("Raw Read bytes"); - this.trace(`Read ${value.length} bytes: ${this.hexConvert(value)}`); + this.trace(`Read ${value.length} bytes: ${hexConvert(value)}`); } return value; } finally { @@ -352,7 +296,7 @@ export class WebSerialTransport implements AbstractTransport { /** * Connect to serial device using the Webserial open method. - * @param {SerialOptions} serialOptions Serial Options for WebUSB SerialPort class. + * @param {SerialOptions} serialOptions Serial Options for ESP Loader class. */ async connect(serialOptions: SerialOptions) { await this.device.open({ diff --git a/src/utils/convert.ts b/src/utils/convert.ts new file mode 100644 index 00000000..610bc969 --- /dev/null +++ b/src/utils/convert.ts @@ -0,0 +1,78 @@ +/** + * Concatenate array 2 to array 1 and return the resulting UInt8Array. + * @param {Uint8Array} array1 First array to concatenate. + * @param {Uint8Array} array2 Second array to concatenate. + * @returns {Uint8Array} Result UInt8Array. + */ +export function appendArray(array1: Uint8Array, array2: Uint8Array): Uint8Array { + const result = new Uint8Array(array1.length + array2.length); + result.set(array1, 0); + result.set(array2, array1.length); + return result; +} + +/** + * Convert short integer to byte array + * @param {number} i - Number to convert. + * @returns {Uint8Array} Byte array. + */ +export function shortToBytearray(i: number): Uint8Array { + return new Uint8Array([i & 0xff, (i >> 8) & 0xff]); +} + +/** + * Convert an integer to byte array + * @param {number} i - Number to convert. + * @returns {Uint8Array} Array of byte from interger + */ +export function intToByteArray(i: number): Uint8Array { + return new Uint8Array([i & 0xff, (i >> 8) & 0xff, (i >> 16) & 0xff, (i >> 24) & 0xff]); +} + +/** + * Convert a byte array to short integer. + * @param {number} i - Number to convert. + * @param {number} j - Number to convert. + * @returns {number} Return a short integer number. + */ +export function byteArrayToShort(i: number, j: number): number { + return i | (j >> 8); +} + +/** + * Convert a byte array to integer. + * @param {number} i - Number to convert. + * @param {number} j - Number to convert. + * @param {number} k - Number to convert. + * @param {number} l - Number to convert. + * @returns {number} Return a integer number. + */ +export function byteArrayToInt(i: number, j: number, k: number, l: number): number { + return i | (j << 8) | (k << 16) | (l << 24); +} + +/** + * Convert a unsigned 8 bit integer array to byte string. + * @param {Uint8Array} u8Array - magic hex number to select ROM. + * @returns {string} Return the equivalent string. + */ +export function ui8ToBstr(u8Array: Uint8Array): string { + let bStr = ""; + for (let i = 0; i < u8Array.length; i++) { + bStr += String.fromCharCode(u8Array[i]); + } + return bStr; +} + +/** + * Convert a byte string to unsigned 8 bit integer array. + * @param {string} bStr - binary string input + * @returns {Uint8Array} Return a 8 bit unsigned integer array. + */ +export function bstrToUi8(bStr: string): Uint8Array { + const u8Array = new Uint8Array(bStr.length); + for (let i = 0; i < bStr.length; i++) { + u8Array[i] = bStr.charCodeAt(i); + } + return u8Array; +} diff --git a/src/utils/hex.ts b/src/utils/hex.ts new file mode 100644 index 00000000..feb9be0c --- /dev/null +++ b/src/utils/hex.ts @@ -0,0 +1,44 @@ +/** + * Converts a Uint8Array to a hexadecimal string representation. + * @param {Uint8Array} s - The Uint8Array to be converted. + * @returns {string} A hexadecimal string representation of the input Uint8Array. + * @example + * const byteArray = new Uint8Array([255, 0, 127]); + * const hexString = hexify(byteArray); // "ff007f" + */ +export function hexify(s: Uint8Array): string { + return Array.from(s) + .map((byte) => byte.toString(16).padStart(2, "0")) + .join("") + .padEnd(16, " "); +} + +/** + * Converts a Uint8Array to a formatted hexadecimal string representation with ASCII. + * @param {Uint8Array} uint8Array - The Uint8Array to be converted. + * @param {boolean} autoSplit - Indicates whether to automatically split the output into lines. + * @returns {string} A formatted hexadecimal string representation of the input Uint8Array. + * @example + * const byteArray = new Uint8Array([ 192, 0, 5, 16, 0, 0, 0, 0, 0, 6, 104, 13, 0, 0, 1, 0 ]); + * const hexString = hexConvert(byteArray); // "c000051000000000 00680d0000010000 | .........h......" + */ +export function hexConvert(uint8Array: Uint8Array, autoSplit: boolean = true): string { + if (autoSplit && uint8Array.length > 16) { + let result = ""; + let s = uint8Array; + + while (s.length > 0) { + const line = s.slice(0, 16); + const asciiLine = String.fromCharCode(...line) + .split("") + .map((c) => (c === " " || (c >= " " && c <= "~" && c !== " ") ? c : ".")) + .join(""); + s = s.slice(16); + result += `\n ${hexify(line.slice(0, 8))} ${hexify(line.slice(8))} | ${asciiLine}`; + } + + return result; + } else { + return hexify(uint8Array); + } +} diff --git a/src/utils/slip.ts b/src/utils/slip.ts new file mode 100644 index 00000000..cfa1b1d6 --- /dev/null +++ b/src/utils/slip.ts @@ -0,0 +1,20 @@ +/** + * Format data packet using the Serial Line Internet Protocol (SLIP). + * @param {Uint8Array} data Binary unsigned 8 bit array data to format. + * @returns {Uint8Array} Formatted unsigned 8 bit data array. + */ +export function slipWriter(data: Uint8Array): Uint8Array { + const outData = []; + outData.push(0xc0); + for (let i = 0; i < data.length; i++) { + if (data[i] === 0xdb) { + outData.push(0xdb, 0xdd); + } else if (data[i] === 0xc0) { + outData.push(0xdb, 0xdc); + } else { + outData.push(data[i]); + } + } + outData.push(0xc0); + return new Uint8Array(outData); +}