From a68db52ad65f2dec93804fbcc2fdeb061f5970ad Mon Sep 17 00:00:00 2001 From: mrfrase3 Date: Sun, 30 Oct 2022 00:13:37 +0800 Subject: [PATCH 1/5] initial working avr109 support --- src/avr/avr-cpu-data.ts | 6 + src/avr/avr109/avr109.ts | 493 ++++++++++++++++++++++++++++++++ src/avr/avr109/device-lookup.ts | 101 +++++++ src/avr/index.ts | 18 +- src/index.d.ts | 1 + src/util/serial-helpers.ts | 5 +- test/boards.ts | 13 +- test/test-config.yml | 20 +- test/util.ts | 10 + 9 files changed, 659 insertions(+), 8 deletions(-) create mode 100644 src/avr/avr109/avr109.ts create mode 100644 src/avr/avr109/device-lookup.ts diff --git a/src/avr/avr-cpu-data.ts b/src/avr/avr-cpu-data.ts index 55ef5a8..e7c854c 100644 --- a/src/avr/avr-cpu-data.ts +++ b/src/avr/avr-cpu-data.ts @@ -14,6 +14,8 @@ interface CPUData { byteDelay?: number; pollValue?: number; pollIndex?: number; + maxWriteDelay?: number; + chipEraseDelay?: number; } interface CPUDefinitions { @@ -75,6 +77,10 @@ const cpuDefs = { atmega32u4: { signature: Buffer.from([0x43, 0x41, 0x54, 0x45, 0x52, 0x49, 0x4e]), protocol: 'avr109', + pageSize: 128, + numPages: 256, + maxWriteDelay: 4500, + chipEraseDelay: 9000, } as CPUData, } as CPUDefinitions; diff --git a/src/avr/avr109/avr109.ts b/src/avr/avr109/avr109.ts new file mode 100644 index 0000000..2046f70 --- /dev/null +++ b/src/avr/avr109/avr109.ts @@ -0,0 +1,493 @@ +import { SerialPort } from 'serialport/dist/index.d'; +import { setDTRRTS, waitForOpen } from '../../util/serial-helpers'; +import asyncTimeout from '../../util/async-timeout'; + +import { getDeviceName } from './device-lookup'; + +interface AVR109Options { + quiet?: boolean; + speed?: number; + signature?: string; + testBlockMode?: boolean; + deviceCode?: number; + writeToEeprom?: boolean; + avr109Reconnect: () => Promise; +} + +interface ReceiveOptions { + timeout?: number; + responseLength?: number; + readUntilNull?: boolean; +} + +interface CommandOptions { + cmd: Buffer | string | number; + timeout?: number; + len?: number; + readUntilNull?: boolean; +} + +interface BootloadOptions { + signature: Buffer, + pageSize?: number; + maxWriteDelay?: number; + chipEraseDelay?: number; +} + +// AVR109 protocol +// https://ww1.microchip.com/downloads/en/Appnotes/doc1644.pdf +const statics = { + CMD_ENTER_PROG_MODE: 'P'.charCodeAt(0), + CMD_AUTO_INC_ADDR: 'a'.charCodeAt(0), + CMD_SET_ADDR: 'A'.charCodeAt(0), + CMD_WRITE_PROG_MEM_LOW: 'c'.charCodeAt(0), + CMD_WRITE_PROG_MEM_HIGH: 'C'.charCodeAt(0), + CMD_ISSUE_PAGE_WRITE: 'm'.charCodeAt(0), + CMD_READ_LOCK_BITS: 'r'.charCodeAt(0), + CMD_READ_PROG_MEM: 'R'.charCodeAt(0), + CMD_READ_DATA_MEM: 'd'.charCodeAt(0), + CMD_WRITE_DATA_MEM: 'D'.charCodeAt(0), + CMD_CHIP_ERASE: 'e'.charCodeAt(0), + CMD_WRITE_LOCK_BITS: 'l'.charCodeAt(0), + CMD_READ_FUSE_BITS: 'F'.charCodeAt(0), + CMD_READ_HIGH_FUSE_BITS: 'N'.charCodeAt(0), + CMD_READ_EXT_FUSE_BITS: 'Q'.charCodeAt(0), + CMD_LEAVE_PROG_MODE: 'L'.charCodeAt(0), + CMD_SELECT_DEVICE_TYPE: 'T'.charCodeAt(0), + CMD_READ_SIGNATURE_BYTES: 's'.charCodeAt(0), + CMD_RETURN_DEVICE_CODES: 't'.charCodeAt(0), + CMD_RETURN_SOFTWARE_ID: 'S'.charCodeAt(0), + CMD_RETURN_SOFTWARE_VER: 'V'.charCodeAt(0), + CMD_RETURN_HARDWARE_VER: 'v'.charCodeAt(0), + CMD_RETURN_PROGRAMMER_TYPE: 'p'.charCodeAt(0), + CMD_SET_LED: 'x'.charCodeAt(0), + CMD_CLEAR_LED: 'y'.charCodeAt(0), + CMD_EXIT_BOOTLOADER: 'E'.charCodeAt(0), + CMD_CHECK_BLOCK_SUPPORT: 'b'.charCodeAt(0), + CMD_START_BLOCK_LOAD: 'B'.charCodeAt(0), + CMD_START_BLOCK_READ: 'g'.charCodeAt(0), + + RES_EMPTY: '\r'.charCodeAt(0), + RES_UNKNOWN: '?'.charCodeAt(0), + + FLAG_FLASH: 'F'.charCodeAt(0), + FLAG_EEPROM: 'E'.charCodeAt(0), +}; + +export default class AVR109 { + opts: AVR109Options; + quiet: boolean; + signature: string; + serial: SerialPort; + orgSerialPort: SerialPort; + orgSpeed: number; + hasAutoIncrAddr: boolean; + bufferSize: number; + useBlockMode: boolean; + deviceCode: number; + + constructor(serial: SerialPort, opts: AVR109Options) { + this.opts = opts || {}; + this.signature = this.opts.signature || 'LUFACDC'; + this.quiet = this.opts.quiet || false; + this.serial = serial; + + this.hasAutoIncrAddr = false; + this.bufferSize = 0; + this.useBlockMode = false; + this.deviceCode = this.opts.deviceCode || 0; + this.orgSerialPort = serial; + this.orgSpeed = this.serial.baudRate || 115200; + } + + log (...args: any[]) { + if (this.quiet) return; + console.log(...args); + } + + async send(data: Buffer | string | number) { + let buf = data; + if (typeof data === 'string') { + buf = Buffer.from(data, 'ascii'); + } + if (typeof data === 'number') { + buf = Buffer.from([data]); + } + await this.serial.write(buf); + } + + recv(opts: ReceiveOptions): Promise { + const { + timeout = 1000, + responseLength = 0, + readUntilNull = false, + } = opts; + return new Promise((resolve, reject) => { + let buffer = Buffer.alloc(0); + let timeoutId = null as NodeJS.Timeout | null; + let handleChunk = (data: Buffer) => {}; + const finished = (err?: Error) => { + if (timeoutId) { + clearTimeout(timeoutId); + } + this.serial.removeListener('data', handleChunk); + if (err) { + reject(err); + } else { + resolve(buffer); + } + }; + handleChunk = (data: Buffer) => { + if (readUntilNull && data.indexOf(0x00) !== -1) { + buffer = Buffer.concat([buffer, data.slice(0, data.indexOf(0x00))]); + return finished(); + } + buffer = Buffer.concat([buffer, data]); + if (!readUntilNull) { + if (buffer.length > responseLength) { + return finished(new Error(`buffer overflow ${buffer.length} > ${responseLength}`)); + } + if (buffer.length == responseLength) { + finished(); + } + } + }; + if (timeout && timeout > 0) { + timeoutId = setTimeout(() => { + timeoutId = null; + finished(new Error(`receiveData timeout after ${timeout}ms`)); + }, timeout); + } + this.serial.on('data', handleChunk); + }); + } + + async cmd(opts: CommandOptions) { + const { cmd, timeout = 100, len = 0, readUntilNull } = opts; + const readPromise = this.recv({ + timeout, + responseLength: len || 1, + readUntilNull, + }); + await this.send(cmd); + const data = await readPromise; + if (!len && !readUntilNull) { + const byte = data[0]; + if (byte !== statics.RES_EMPTY && byte !== statics.RES_UNKNOWN) { + throw new Error(`Command ${cmd} failed, expected \\r, got ${data.toString('hex')}`); + } + } + return data; + } + + async chipErase(timeout?: number) { + await this.cmd({ cmd: statics.CMD_CHIP_ERASE, timeout: timeout || 9000 }); + } + + enterProgrammingMode() { + return this.cmd({ cmd: statics.CMD_ENTER_PROG_MODE }); + } + + leaveProgrammingMode() { + return this.cmd({ cmd: statics.CMD_LEAVE_PROG_MODE }); + } + + programEnable() { + return null; + } + + async init() { + // Get the programmer identifier. Programmer returns exactly 7 chars _without_ the null. + const id = await this.cmd({ cmd: statics.CMD_RETURN_SOFTWARE_ID, len: 7 }); + this.log(`Programmer ID: ${id.toString('ascii')}`); + + // Get the HW and SW versions to see if the programmer is present. + const buffToVer = (buff: Buffer) => buff.toString('ascii').split('').join('.'); + + const sw = await this.cmd({ cmd: statics.CMD_RETURN_SOFTWARE_VER, len: 2 }); + this.log(`Software Version: ${buffToVer(sw)}`); + const hwTest = await this.cmd({ cmd: statics.CMD_RETURN_HARDWARE_VER, len: 1 }); + if (hwTest[0] === statics.RES_UNKNOWN) { + this.log('No hardware version given'); + } else { + const hw = await this.cmd({ cmd: statics.CMD_RETURN_HARDWARE_VER, len: 2 }); + this.log(`Hardware Version: ${buffToVer(hw)}`); + } + + // Get the programmer type (serial or parallel). Expect serial. + const type = await this.cmd({ cmd: statics.CMD_RETURN_PROGRAMMER_TYPE, len: 1 }); + this.log(`Programmer Type: ${type.toString('ascii')}`); + + + // See if programmer supports auto-increment of address. + const autoIncAdd = await this.cmd({ cmd: statics.CMD_AUTO_INC_ADDR, len: 1 }); + this.hasAutoIncrAddr = autoIncAdd.toString('ascii') === 'Y'; + if (this.hasAutoIncrAddr) { + this.log('Programmer supports auto addr increment.'); + } + + // Check support for buffered memory access, ignore if not available + if (this.opts.testBlockMode !== false) { + const bufferSize = await this.cmd({ cmd: statics.CMD_CHECK_BLOCK_SUPPORT, len: 3 }); + this.useBlockMode = bufferSize.subarray(0, 1).toString('ascii') === 'Y'; + if (this.useBlockMode) { + this.bufferSize = bufferSize.readUInt16BE(1); + this.log(`Programmer supports buffered memory access with ${this.bufferSize} bytes buffer.`); + } + } else { + this.useBlockMode = false; + } + + // Get list of devices that the programmer supports. + const devices = await this.cmd({ cmd: statics.CMD_RETURN_DEVICE_CODES, readUntilNull: true }); + if (devices.length) { + this.log('Programmer supports the following devices:'); + for (let i = 0; i < devices.length; i += 1) { + const device = devices[i]; + this.log(` Device Code: 0x${device.toString(16)} (${getDeviceName(device)})`); + } + this.log(''); + if (this.opts.deviceCode && devices.indexOf(this.opts.deviceCode) === -1) { + throw new Error(`Device code 0x${this.opts.deviceCode.toString(16)} not supported by programmer.`); + } else if (!this.deviceCode) { + this.deviceCode = devices[0]; + } + } else { + throw new Error('No devices supported by programmer.'); + } + + // Tell the programmer which part we selected. + await this.cmd({ cmd: Buffer.from([statics.CMD_SELECT_DEVICE_TYPE, this.deviceCode]) }); + this.log(`Selected device: 0x${this.deviceCode.toString(16)} ${getDeviceName(this.deviceCode)}`); + + await this.enterProgrammingMode(); + } + + setAddr(addr: number) { + const cmd = Buffer.alloc(3); + cmd[0] = statics.CMD_SET_ADDR; + cmd.writeUInt16BE(addr, 1); + return this.cmd({ cmd }); + } + + async blockWrite(data: Buffer, addr: number) { + const pageSize = this.opts.writeToEeprom ? 1 : (this.bufferSize || 128); + const pageCount = Math.ceil(data.length / pageSize); + const wrSize = this.opts.writeToEeprom ? 1 : 2; + for (let i = 0; i < pageCount; i += 1) { + const cursor = i * pageSize; + if (!this.hasAutoIncrAddr || i === 0) { + await this.setAddr((addr + cursor) / wrSize); + } + const page = data.slice(cursor, Math.min(cursor + pageSize, data.length)); + let cmd = Buffer.alloc(4); + cmd[0] = statics.CMD_START_BLOCK_LOAD; + cmd.writeUInt16BE(page.length, 1); + cmd[3] = this.opts.writeToEeprom ? statics.FLAG_EEPROM : statics.FLAG_FLASH; + cmd = Buffer.concat([cmd, page]); + + await this.cmd({ cmd }); + } + } + + async blockRead(addr: number, len: number) { + const pageSize = this.opts.writeToEeprom ? 1 : (this.bufferSize || 128); + const pageCount = Math.ceil(len / pageSize); + const wrSize = this.opts.writeToEeprom ? 1 : 2; + const data = Buffer.alloc(len); + for (let i = 0; i < pageCount; i += 1) { + const cursor = i * pageSize; + if (!this.hasAutoIncrAddr || i === 0) { + await this.setAddr((addr + cursor) / wrSize); + } + const readSize = Math.min(pageSize, len - cursor); + let cmd = Buffer.alloc(4); + cmd[0] = statics.CMD_START_BLOCK_READ; + cmd.writeUInt16BE(readSize, 1); + cmd[3] = this.opts.writeToEeprom ? statics.FLAG_EEPROM : statics.FLAG_FLASH; + const page = await this.cmd({ cmd, len: readSize }); + page.copy(data, cursor); + } + return data; + } + + async pagedWriteFlash(data: Buffer, address: number, pageSize: number, timeout?: number) { + const cmds = [statics.CMD_WRITE_PROG_MEM_LOW, statics.CMD_WRITE_PROG_MEM_HIGH]; + const buf = Buffer.alloc(2); + const maxAddr = address + data.length; + let addr = address; + let pageAddr; + let pageBytes = pageSize; + let pageWrCmdPending = false; + + pageAddr = addr; + await this.setAddr(pageAddr); + + while(addr < maxAddr) { + pageWrCmdPending = true; + buf[0] = cmds[addr & 0x01]; + buf[1] = data[addr]; + await this.cmd({ cmd: buf }); + + addr += 1; + pageBytes -= 1; + + if (pageBytes === 0) { + await this.setAddr(pageAddr >> 1); + await this.cmd({ cmd: statics.CMD_ISSUE_PAGE_WRITE, timeout: timeout || 4500 }); + + pageWrCmdPending = false; + await this.setAddr(addr >> 1); + pageAddr = addr; + pageBytes = pageSize; + } else if (!this.hasAutoIncrAddr && (addr & 0x01) === 0) { + await this.setAddr(addr >> 1); + } + } + + if (pageWrCmdPending) { + await this.setAddr(pageAddr >> 1); + await this.cmd({ cmd: statics.CMD_ISSUE_PAGE_WRITE, timeout: timeout || 4500 }); + } + } + + async pagedWriteEeprom(data: Buffer, address: number, timeout?: number) { + const buff = Buffer.alloc(2); + const maxAddr = address + data.length; + let addr = address; + + await this.setAddr(addr); + + buff[0] = statics.CMD_WRITE_DATA_MEM; + + while(addr < maxAddr) { + buff[1] = data[addr]; + await this.cmd({ cmd: buff, timeout: timeout || 4500 }); + + addr += 1; + if (!this.hasAutoIncrAddr) { + await this.setAddr(addr); + } + } + } + + async pagedReadBytes(address: number, len: number) { + const data = Buffer.alloc(len); + const cmd = this.opts.writeToEeprom + ? statics.CMD_READ_DATA_MEM + : statics.CMD_READ_PROG_MEM; + const rdSize = this.opts.writeToEeprom ? 1 : 2; + const maxAddr = address + len; + let addr = address; + await this.setAddr(addr); + while (addr < maxAddr) { + const buff = await this.cmd({ cmd, len: 1 }); + if (rdSize === 2) { + data[addr] = buff[1]; + data[addr + 1] = buff[0]; + } else { + data[addr] = buff[0]; + } + addr += rdSize; + if (!this.hasAutoIncrAddr) { + await this.setAddr(addr / rdSize); + } + } + return data; + } + + async pagedWrite(data: Buffer, address: number, pageSize: number, timeout?: number) { + if (!this.useBlockMode) { + if (this.opts.writeToEeprom) { + await this.pagedWriteEeprom(data, address, timeout); + } else { + await this.pagedWriteFlash(data, address, pageSize, timeout); + } + } else { + await this.blockWrite(data, address); + } + } + + async pagedRead(address: number, len: number) { + if (!this.useBlockMode) { + return this.pagedReadBytes(address, len); + } + return this.blockRead(address, len); + } + + async program(data: Buffer, address: number, pageSize: number, timeout?: number) { + await this.pagedWrite(data, address, pageSize, timeout); + } + + async verify(data: Buffer, address: number) { + const rdData = await this.pagedRead(address, data.length); + return rdData.equals(data); + } + + reconnect(): Promise { + return new Promise((resolve, reject) => { + let res = (serial: SerialPort) => {}; + const timeoutId = setTimeout(res, 30 * 1000); + res = (serial) => { + clearTimeout(timeoutId); + if (!serial) { + this.log('reconnect timed out'); + reject(new Error('reconnect timed out')); + } else { + resolve(serial); + } + }; + this.opts.avr109Reconnect().then(res).catch(reject); + }); + } + + async enterBootloader() { + if (!this.serial.isOpen) { + await this.serial.open(); + await waitForOpen(this.serial); + } + await this.serial.update({ baudRate: 1200 }); + await asyncTimeout(500); + await this.serial.close(); + await asyncTimeout(500); + this.serial = await this.reconnect(); + if (!this.serial.isOpen) { + await waitForOpen(this.serial); + } + await this.serial.update({ baudRate: this.opts.speed || 57600 }); + } + + async exitBootloader() { + await this.cmd({ cmd: statics.CMD_EXIT_BOOTLOADER }); + await this.serial.close(); + } + + async bootload(data: Buffer, opt: BootloadOptions) { + this.log('Entering bootloader'); + await this.enterBootloader(); + this.log('Initialising'); + await this.init(); + this.log('Erasing Chip'); + await this.chipErase(opt.chipEraseDelay); + this.log('Programming'); + await this.program(data, 0, opt.pageSize || 128, opt.maxWriteDelay); + this.log('Verifying'); + const isVerified = await this.verify(data, 0); + if (!isVerified) { + throw new Error('Verification failed'); + } else { + this.log('Verification successful'); + } + this.log('Resetting'); + await this.leaveProgrammingMode(); + await this.exitBootloader(); + this.log('Reconnecting'); + await asyncTimeout(2 * 1000); + this.serial = this.orgSerialPort; + // this.serial = await this.reconnect(); + await this.serial.open(); + await waitForOpen(this.serial, 1000); + await this.serial.update({ baudRate: this.orgSpeed }); + } + +} diff --git a/src/avr/avr109/device-lookup.ts b/src/avr/avr109/device-lookup.ts new file mode 100644 index 0000000..b09c7f9 --- /dev/null +++ b/src/avr/avr109/device-lookup.ts @@ -0,0 +1,101 @@ +// thanks internet stranger +// https://www.avrfreaks.net/comment/349373#comment-349373 + +export const devices = { + // For AVRPROG 1.37 + 0x10: 'AT90S1200 rev. A', + 0x11: 'AT90S1200 rev. B', + 0x12: 'AT90S1200 rev. C', + 0x13: 'AT90S1200', + 0x20: 'AT90S2313', + 0x28: 'AT90S4414', + 0x30: 'AT90S4433', + 0x34: 'AT90S2333', + 0x38: 'AT90S8515', + 0x3A: 'ATmega8515', + 0x3B: 'ATmega8515 BOOT', + 0x41: 'ATmega103', + 0x42: 'ATmega603', + 0x43: 'ATmega128', + 0x44: 'ATmega128 BOOT', + 0x48: 'AT90S2323', + 0x4C: 'AT90S2343', + 0x50: 'ATtiny11', + 0x51: 'ATtiny10', + 0x55: 'ATtiny12', + 0x56: 'ATtiny15', + 0x58: 'ATtiny19', + 0x5C: 'ATtiny28', + 0x5E: 'ATtiny26', + 0x60: 'ATmega161', + 0x61: 'ATmega161 BOOT', + 0x62: 'ATmega163', + 0x65: 'ATmega83', + 0x66: 'ATmega163 BOOT', + 0x67: 'ATmega83 BOOT', + 0x68: 'AT90S8535', + 0x6C: 'AT90S4434', + 0x70: 'AT90C8534', + 0x71: 'AT90C8544', + 0x72: 'ATmega32', + 0x73: 'ATmega32 BOOT', + 0x74: 'ATmega16', + 0x75: 'ATmega16 BOOT', + 0x76: 'ATmega8', + 0x77: 'ATmega8 BOOT', + 0x80: 'AT89C1051', + 0x81: 'AT89C2051', + 0x86: 'AT89S8252', + 0x87: 'AT89S53', + + // For AVRprog v1.4 + // 0x10: 'AT90S1200rev.A', + // 0x11: 'AT90S1200rev.B', + // 0x12: 'AT90S1200rev.C', + // 0x13: 'AT90S1200', + // 0x20: 'AT90S2313', + // 0x28: 'AT90S4414', + // 0x30: 'AT90S4433', + // 0x34: 'AT90S2333', + // 0x38: 'AT90S8515', + // 0x3A: 'ATmega8515', + // 0x3B: 'ATmega8515 BOOT', + // 0x41: 'ATmega103', + // 0x42: 'ATmega603', + // 0x43: 'ATmega128', + // 0x44: 'ATmega128 BOOT', + 0x45: 'ATmega64', + 0x46: 'ATmega64 BOOT', + // 0x48: 'AT90S2323', + // 0x4C: 'AT90S2343', + // 0x50: 'ATtiny11', + // 0x51: 'ATtiny10', + // 0x55: 'ATtiny12', + // 0x56: 'ATtiny15', + // 0x58: 'ATtiny19', + // 0x5C: 'ATtiny28', + // 0x5E: 'ATtiny26', + // 0x60: 'ATmega161', + // 0x61: 'ATmega161 BOOT', + 0x64: 'ATmega163', + // 0x65: 'ATmega83', + // 0x66: 'ATmega163 BOOT', + // 0x67: 'ATmega83 BOOT', + // 0x68: 'AT90S8535', + 0x69: 'ATmega8535', + // 0x6C: 'AT90S4434', + // 0x70: 'AT90C8534', + // 0x71: 'AT90C8544', + // 0x72: 'ATmega32', + // 0x73: 'ATmega32 BOOT', + // 0x74: 'ATmega16', + // 0x75: 'ATmega16 BOOT', + // 0x76: 'ATmega8', + // 0x77: 'ATmega8 BOOT', + 0x78: 'ATmega169', + 0x79: 'ATmega169 BOOT', +} as { [key: number]: string }; + +export const getDeviceName = (id: number) => { + return devices[id] || 'Unknown'; +}; diff --git a/src/avr/index.ts b/src/avr/index.ts index 62fd834..981054d 100644 --- a/src/avr/index.ts +++ b/src/avr/index.ts @@ -5,10 +5,11 @@ import intelHex from 'intel-hex'; import getCpuData from './avr-cpu-data'; import STK500v1 from './stk500-v1/stk500-v1'; import STK500v2 from './stk500-v2/stk500-v2'; +import AVR109 from './avr109/avr109'; export const upload = async (serial: SerialPort, config: ProgramConfig) => { const cpuData = getCpuData(config.cpu); - let uploader = null as STK500v2 | STK500v1 | null; + let uploader = null as STK500v2 | STK500v1 | AVR109 | null; switch (cpuData.protocol) { case 'stk500v1': uploader = new STK500v1(serial, { quiet: !config.verbose }); @@ -28,6 +29,19 @@ export const upload = async (serial: SerialPort, config: ProgramConfig) => { cpuData, ); break; + case 'avr109': + if (!config.avr109Reconnect) { + throw new Error('avr109Reconnect function not provided'); + } + uploader = new AVR109(serial, { + quiet: !config.verbose, + avr109Reconnect: config.avr109Reconnect, + }); + await uploader.bootload( + intelHex.parse(config.hex || '').data, + cpuData, + ); + break; default: throw new Error(`Protocol ${cpuData.protocol} not supported`); } @@ -36,7 +50,7 @@ export const upload = async (serial: SerialPort, config: ProgramConfig) => { export const isSupported = (cpu: string) => { try { const cpuData = getCpuData(cpu); - return ['stk500v1'].includes(cpuData.protocol); + return ['stk500v1', 'stk500v2', 'avr109'].includes(cpuData.protocol); } catch (e) { return false; } diff --git a/src/index.d.ts b/src/index.d.ts index 13d81ca..036128f 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -13,4 +13,5 @@ export interface ProgramConfig { verbose?: boolean; flashMode?: string; flashFreq?: string; + avr109Reconnect?: () => Promise; } \ No newline at end of file diff --git a/src/util/serial-helpers.ts b/src/util/serial-helpers.ts index 11cfe42..557f587 100644 --- a/src/util/serial-helpers.ts +++ b/src/util/serial-helpers.ts @@ -1,6 +1,9 @@ import { SerialPort } from 'serialport/dist/index.d'; export const waitForOpen = (serial: SerialPort, timeout: number = 1000): Promise => { + let id = ''; + // id = Math.random().toString(36).substring(7); + // console.log('waitForOpen', id); return new Promise((resolve, reject) => { if (serial.isOpen) { return resolve(true); @@ -8,7 +11,7 @@ export const waitForOpen = (serial: SerialPort, timeout: number = 1000): Promise let cleanup = () => {}; const timer = setTimeout(() => { cleanup(); - reject(new Error('Timeout opening port')); + reject(new Error(`Timeout opening port ${id} (${timeout}ms)`)); }, timeout); const handleOpen = () => { cleanup(); diff --git a/test/boards.ts b/test/boards.ts index ca21359..399673e 100644 --- a/test/boards.ts +++ b/test/boards.ts @@ -3,7 +3,11 @@ import { expect } from 'chai'; import 'mocha'; import { SerialPort } from 'serialport'; import { upload } from '../src/index'; -import { waitForData, config, getHex, espIdentify, ESPIdentifyResult } from './util'; +import { + waitForData, waitForDevice, + config, getHex, + espIdentify, ESPIdentifyResult, +} from './util'; import { waitForOpen } from '../src/util/serial-helpers'; import { ProgramFile } from '../src/index.d'; @@ -53,7 +57,7 @@ Object.keys(config.devices).forEach((deviceRef) => { if (!port) throw new Error(`could not locate ${device.name}`); // connect to the device - serial = new SerialPort({ path: port.path, baudRate: device.speed }); + serial = new SerialPort({ path: port.path, baudRate: 115200 }); await waitForOpen(serial); console.log(`connected to ${device.name} on ${port.path}`); }); @@ -75,6 +79,11 @@ Object.keys(config.devices).forEach((deviceRef) => { tool: device.tool, cpu: device.cpu, verbose: config.verbose, + avr109Reconnect: async () => { + const port = await waitForDevice(device); + if (!port) throw new Error(`could not locate ${device.name}`); + return new SerialPort({ path: port.path, baudRate: 1200 }); + } }); console.log(`uploaded to ${device.name}, validating...`); diff --git a/test/test-config.yml b/test/test-config.yml index f118cf9..a1aa4b8 100644 --- a/test/test-config.yml +++ b/test/test-config.yml @@ -1,6 +1,6 @@ verbose: true -# compileServer: https://compile.duino.app -compileServer: http://localhost:3030 +compileServer: https://compile.duino.app +# compileServer: http://localhost:3030 retries: 2 devices: uno: @@ -28,8 +28,22 @@ devices: tool: avrdude speed: 115200 + leonardo: + name: Arduino Leonardo + vendorIds: + - '2341' + - '2A03' + productIds: + - '0036' + - '8036' + code: blink + fqbn: arduino:avr:leonardo + cpu: atmega32u4 + tool: avrdude + speed: 57600 + esp32: - # DOIT ESP32 DEVKIT V1 is the closest match + # DOIT ESP32 DEVKIT V1 is the closest match to what I have name: DOIT ESP32 DEVKIT V1 espChip: ESP32-D0WD-V3 (revision 3) code: ping diff --git a/test/util.ts b/test/util.ts index ab8a9d9..86c648b 100644 --- a/test/util.ts +++ b/test/util.ts @@ -149,3 +149,13 @@ export const espIdentify = async (espCount: number): Promise => { + const list = await SerialPort.list(); + // console.log(list.filter(p => p.vendorId)); + const port = list.find(p => device.vendorIds?.includes(p.vendorId || '') && device.productIds?.includes(p.productId || '')); + if (port) return port; + if (count > 20) return null; + await asyncTimeout(100 + (Math.random() * 500)); + return waitForDevice(device, count + 1); +} \ No newline at end of file From d4afceb211361fea8124554c2b0d335df7fe56f7 Mon Sep 17 00:00:00 2001 From: mrfrase3 Date: Mon, 31 Oct 2022 09:43:32 +0800 Subject: [PATCH 2/5] add rollup for browser bundling --- .eslintignore | 4 +- .gitignore | 3 +- examples/index.html | 21 ++++ package.json | 35 +++++- rollup.config.js | 36 ++++++ src/avr/avr109/avr109.ts | 2 +- src/avr/index.ts | 2 +- src/esp/index.ts | 2 +- src/esp/loader.ts | 12 +- src/global.d.ts | 3 +- src/index.d.ts | 17 --- src/index.ts | 23 +++- test/boards.ts | 2 +- test/util.ts | 2 +- tsconfig.json | 8 +- yarn.lock | 255 +++++++++++++++++++++++++++++++++++++++ 16 files changed, 385 insertions(+), 42 deletions(-) create mode 100644 examples/index.html create mode 100644 rollup.config.js delete mode 100644 src/index.d.ts diff --git a/.eslintignore b/.eslintignore index a449aca..d168b17 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,5 @@ node_modules .eslintrc.js -lib \ No newline at end of file +lib +examples +dist \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9b26ed0..7dbcbd3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules -lib \ No newline at end of file +lib +dist \ No newline at end of file diff --git a/examples/index.html b/examples/index.html new file mode 100644 index 0000000..3e21b4f --- /dev/null +++ b/examples/index.html @@ -0,0 +1,21 @@ + + + + Upload Multitool + + + + + + +
+ Loading... +
+ + + \ No newline at end of file diff --git a/package.json b/package.json index f1e1fa9..b1c1774 100644 --- a/package.json +++ b/package.json @@ -2,14 +2,34 @@ "name": "upload-multitool", "version": "0.0.1", "description": "Micro Controller Uploading Multitool", - "main": "index.js", + "main": "dist/index.js", + "module": "dist/index.min.mjs", + "unpkg": "dist/index.umd.min.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], "scripts": { "test": "mocha -r ts-node/register test/**/*.ts test/*.ts", - "build": "tsc" + "lint": "eslint", + "clean": "rm -fr dist", + "build": "yarn clean && yarn lint && yarn build:tsc && yarn bundle && yarn bundle:esm:min && yarn bundle:umd:min && yarn build:stats", + "build:tsc": "tsc", + "build:stats": "(echo '\\033[35;3m' ; cd dist && ls -lh index*js index*gz | tail -n +2 | awk '{print $5,$9}')", + "bundle": "rollup --config rollup.config.js", + "bundle:esm:min": "terser --ecma 6 --compress --mangle --module -o dist/index.min.mjs -- dist/index.mjs && gzip -9 -c dist/index.min.mjs > dist/index.min.mjs.gz", + "bundle:umd:min": "terser --ecma 6 --compress --mangle -o dist/index.umd.min.js -- dist/index.umd.js && gzip -9 -c dist/index.umd.min.js > dist/index.umd.min.js.gz" }, "author": "Fraser Bullock", "license": "UNLICENSED", + "repository": { + "type": "git", + "url": "git://github.com/duinoapp/upload-multitool.git" + }, "devDependencies": { + "@rollup/plugin-commonjs": "^23.0.2", + "@rollup/plugin-json": "^5.0.1", + "@rollup/plugin-node-resolve": "^15.0.1", "@types/chai": "^4.3.1", "@types/crypto-js": "^4.1.1", "@types/mocha": "^9.1.1", @@ -17,21 +37,24 @@ "@types/pako": "^2.0.0", "@typescript-eslint/eslint-plugin": "^5.23.0", "@typescript-eslint/parser": "^5.23.0", - "axios": "^0.27.2", "chai": "^4.3.6", - "crypto-js": "^4.1.1", "eslint": "^8.15.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-typescript": "^17.0.0", "eslint-plugin-import": "^2.26.0", "mocha": "^10.0.0", - "pako": "^2.0.4", + "rollup": "^3.2.3", + "rollup-plugin-node-polyfills": "^0.2.1", "serialport": "^10.4.0", + "terser": "^5.15.1", "ts-node": "^10.8.0", "typescript": "^4.6.4", "yaml": "^2.1.0" }, "dependencies": { - "intel-hex": "^0.1.2" + "axios": "^0.27.2", + "crypto-js": "^4.1.1", + "intel-hex": "^0.1.2", + "pako": "^2.0.4" } } diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..8f0fa6d --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,36 @@ +const { nodeResolve } = require('@rollup/plugin-node-resolve'); +const commonjs = require('@rollup/plugin-commonjs'); +const json = require('@rollup/plugin-json'); +const nodePolyfills = require('rollup-plugin-node-polyfills'); + +module.exports = { + input: 'dist/index.js', + output: [ + { + file: 'dist/index.cjs', + format: 'cjs', + }, + { + file: 'dist/index.mjs', + format: 'esm', + }, + { + file: 'dist/index.umd.js', + format: 'umd', + name: 'uploadMultitool', + globals: { + axios: 'axios', + }, + }, + ], + context: 'this', + external: ['axios'], + plugins: [ + commonjs({ + ignoreGlobal: true, + }), + nodePolyfills({ include: ['buffer'] }), + nodeResolve({ preferBuiltins: false }), + json(), + ], +}; diff --git a/src/avr/avr109/avr109.ts b/src/avr/avr109/avr109.ts index 2046f70..d63c20e 100644 --- a/src/avr/avr109/avr109.ts +++ b/src/avr/avr109/avr109.ts @@ -1,5 +1,5 @@ import { SerialPort } from 'serialport/dist/index.d'; -import { setDTRRTS, waitForOpen } from '../../util/serial-helpers'; +import { waitForOpen } from '../../util/serial-helpers'; import asyncTimeout from '../../util/async-timeout'; import { getDeviceName } from './device-lookup'; diff --git a/src/avr/index.ts b/src/avr/index.ts index 981054d..e99b9ad 100644 --- a/src/avr/index.ts +++ b/src/avr/index.ts @@ -1,5 +1,5 @@ import { SerialPort } from 'serialport/dist/index.d'; -import { ProgramConfig } from '../index.d'; +import { ProgramConfig } from '../index'; import intelHex from 'intel-hex'; import getCpuData from './avr-cpu-data'; diff --git a/src/esp/index.ts b/src/esp/index.ts index 13ba6ce..d381cc9 100644 --- a/src/esp/index.ts +++ b/src/esp/index.ts @@ -1,5 +1,5 @@ import { SerialPort } from 'serialport/dist/index.d'; -import { ProgramConfig } from '../index.d'; +import { ProgramConfig } from '../index'; import ESPLoader, { ESPOptions, UploadFileDef } from './loader'; import asyncTimeout from '../util/async-timeout'; diff --git a/src/esp/loader.ts b/src/esp/loader.ts index b9f107b..7d1adef 100644 --- a/src/esp/loader.ts +++ b/src/esp/loader.ts @@ -1,9 +1,10 @@ import { SerialPort } from 'serialport/dist/index.d'; import pako from 'pako'; -import CryptoJS from 'crypto-js'; +import MD5 from 'crypto-js/md5'; +import encBase64 from 'crypto-js/enc-base64'; import StubLoader from './stub-loader'; -import roms from './roms'; -import ROM from './roms/rom'; +import roms from './roms/index'; +import ROM from './roms/rom.d'; export interface ESPOptions { quiet?: boolean; @@ -91,7 +92,7 @@ export default class ESPLoader { this.IS_STUB = false; this.chip = null; this.stdout = opts.stdout || process?.stdout || { - write: (str: string) => console.log(str), + write: (str: string) => console.log(str.replace(/(\n|\r)+$/g, '')), }; this.stubLoader = new StubLoader(this.opts.stubUrl); this.syncStubDetected = false; @@ -1055,7 +1056,8 @@ export default class ESPLoader { } let image = this.#padTo(file.data, 4); image = this.#updateImageFlashParams(image, address, flashSize, flashMode, flashFreq); - const calcMd5 = CryptoJS.MD5(CryptoJS.enc.Base64.parse(image.toString('base64'))); + // const calcMd5 = CryptoJS.MD5(CryptoJS.enc.Base64.parse(image.toString('base64'))); + const calcMd5 = MD5(encBase64.parse(image.toString('base64'))).toString() as string; // console.log(`Image MD5 ${calcMd5}`); const rawSize = image.length; let blocks; diff --git a/src/global.d.ts b/src/global.d.ts index 5e458ba..8cdf052 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -1,3 +1,4 @@ declare module 'intel-hex'; declare module 'pako'; -declare module 'crypto-js'; +declare module 'crypto-js/md5'; +declare module 'crypto-js/enc-base64'; diff --git a/src/index.d.ts b/src/index.d.ts deleted file mode 100644 index 036128f..0000000 --- a/src/index.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -export interface ProgramFile { - data: string; - address: number; -} - -export interface ProgramConfig { - hex?: Buffer; - files?: ProgramFile[]; - speed?: number; - uploadSpeed?: number; - tool?: string; - cpu?: string; - verbose?: boolean; - flashMode?: string; - flashFreq?: string; - avr109Reconnect?: () => Promise; -} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index b7e9300..4d1a10b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,26 @@ -import { ProgramConfig } from './index.d'; import { SerialPort } from 'serialport/dist/index.d'; import { setBaud, waitForOpen } from './util/serial-helpers'; -import avr from './avr'; -import esp from './esp'; +import avr from './avr/index'; +import esp from './esp/index'; + +export interface ProgramFile { + data: string; + address: number; +} + +export interface ProgramConfig { + hex?: Buffer; + files?: ProgramFile[]; + speed?: number; + uploadSpeed?: number; + tool?: string; + cpu?: string; + verbose?: boolean; + flashMode?: string; + flashFreq?: string; + avr109Reconnect?: () => Promise; +} export const upload = async (serial: SerialPort, config: ProgramConfig) => { if (!config.hex && !config.files?.length) { diff --git a/test/boards.ts b/test/boards.ts index 399673e..4aee11f 100644 --- a/test/boards.ts +++ b/test/boards.ts @@ -9,7 +9,7 @@ import { espIdentify, ESPIdentifyResult, } from './util'; import { waitForOpen } from '../src/util/serial-helpers'; -import { ProgramFile } from '../src/index.d'; +import { ProgramFile } from '../src/index'; const numEsps = Object.values(config.devices).filter((d) => d.espChip).length; const listPromise = espIdentify(numEsps); diff --git a/test/util.ts b/test/util.ts index 86c648b..7ac2f7a 100644 --- a/test/util.ts +++ b/test/util.ts @@ -4,7 +4,7 @@ import fs from 'fs'; import axios from 'axios'; import path from 'path'; import { SerialPort } from 'serialport'; -import { ProgramFile } from '../src/index.d'; +import { ProgramFile } from '../src/index'; import ESPLoader from '../src/esp/loader'; import { waitForOpen } from '../src/util/serial-helpers'; import asyncTimeout from '../src/util/async-timeout'; diff --git a/tsconfig.json b/tsconfig.json index ac22a09..cc403e5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,11 @@ { "compilerOptions": { "target": "es2015", - "module": "commonjs", + "module": "es6", + "moduleResolution": "node", + "lib": ["es2017", "es7", "es6", "dom", "dom.iterable"], "declaration": true, - "outDir": "./lib", + "outDir": "./dist", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, @@ -18,5 +20,5 @@ "src/global.d.ts" ], "include": ["src"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "examples"] } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 2c46b20..efb186a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -38,11 +38,43 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@jridgewell/gen-mapping@^0.3.0": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" + integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + "@jridgewell/resolve-uri@^3.0.3": version "3.0.7" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz#30cd49820a962aff48c8fffc5cd760151fca61fe" integrity sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA== +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/source-map@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb" + integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/sourcemap-codec@1.4.14": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + "@jridgewell/sourcemap-codec@^1.4.10": version "1.4.13" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz#b6461fb0c2964356c469e115f504c95ad97ab88c" @@ -56,6 +88,14 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@jridgewell/trace-mapping@^0.3.9": + version "0.3.17" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" + integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g== + dependencies: + "@jridgewell/resolve-uri" "3.1.0" + "@jridgewell/sourcemap-codec" "1.4.14" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -77,6 +117,46 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@rollup/plugin-commonjs@^23.0.2": + version "23.0.2" + resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-23.0.2.tgz#3a3a5b7b1b1cb29037eb4992edcaae997d7ebd92" + integrity sha512-e9ThuiRf93YlVxc4qNIurvv+Hp9dnD+4PjOqQs5vAYfcZ3+AXSrcdzXnVjWxcGQOa6KGJFcRZyUI3ktWLavFjg== + dependencies: + "@rollup/pluginutils" "^5.0.1" + commondir "^1.0.1" + estree-walker "^2.0.2" + glob "^8.0.3" + is-reference "1.2.1" + magic-string "^0.26.4" + +"@rollup/plugin-json@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@rollup/plugin-json/-/plugin-json-5.0.1.tgz#d5cd67cc83ede42967447dfabbe1be45a091f5b7" + integrity sha512-QCwhZZLvM8nRcTHyR1vOgyTMiAnjiNj1ebD/BMRvbO1oc/z14lZH6PfxXeegee2B6mky/u9fia4fxRM4TqrUaw== + dependencies: + "@rollup/pluginutils" "^5.0.1" + +"@rollup/plugin-node-resolve@^15.0.1": + version "15.0.1" + resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.0.1.tgz#72be449b8e06f6367168d5b3cd5e2802e0248971" + integrity sha512-ReY88T7JhJjeRVbfCyNj+NXAG3IIsVMsX9b5/9jC98dRP8/yxlZdz7mHZbHk5zHr24wZZICS5AcXsFZAXYUQEg== + dependencies: + "@rollup/pluginutils" "^5.0.1" + "@types/resolve" "1.20.2" + deepmerge "^4.2.2" + is-builtin-module "^3.2.0" + is-module "^1.0.0" + resolve "^1.22.1" + +"@rollup/pluginutils@^5.0.1": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.0.2.tgz#012b8f53c71e4f6f9cb317e311df1404f56e7a33" + integrity sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA== + dependencies: + "@types/estree" "^1.0.0" + estree-walker "^2.0.2" + picomatch "^2.3.1" + "@serialport/binding-mock@10.2.2": version "10.2.2" resolved "https://registry.yarnpkg.com/@serialport/binding-mock/-/binding-mock-10.2.2.tgz#d322a8116a97806addda13c62f50e73d16125874" @@ -196,6 +276,11 @@ resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-4.1.1.tgz#602859584cecc91894eb23a4892f38cfa927890d" integrity sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA== +"@types/estree@*", "@types/estree@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2" + integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ== + "@types/json-schema@^7.0.9": version "7.0.11" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" @@ -221,6 +306,11 @@ resolved "https://registry.yarnpkg.com/@types/pako/-/pako-2.0.0.tgz#12ab4c19107528452e73ac99132c875ccd43bdfb" integrity sha512-10+iaz93qR5WYxTo+PMifD5TSxiOtdRaxBf7INGGXMQgTCu8Z/7GYWYFUOS3q/G0nE5boj1r4FEB+WSy7s5gbA== +"@types/resolve@1.20.2": + version "1.20.2" + resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975" + integrity sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q== + "@typescript-eslint/eslint-plugin@^5.23.0": version "5.23.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.23.0.tgz#bc4cbcf91fbbcc2e47e534774781b82ae25cc3d8" @@ -321,6 +411,11 @@ acorn@^8.4.1, acorn@^8.7.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== +acorn@^8.5.0: + version "8.8.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73" + integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== + ajv@^6.10.0, ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -447,6 +542,16 @@ browser-stdout@1.3.1: resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +builtin-modules@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" + integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== + call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" @@ -534,6 +639,16 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -601,6 +716,11 @@ deep-is@^0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +deepmerge@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" + integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== + define-properties@^1.1.3, define-properties@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" @@ -864,6 +984,16 @@ estraverse@^5.1.0, estraverse@^5.2.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== +estree-walker@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362" + integrity sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w== + +estree-walker@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + esutils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" @@ -1063,6 +1193,17 @@ glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^8.0.3: + version "8.0.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.0.3.tgz#415c6eb2deed9e502c68fa44a272e6da6eeca42e" + integrity sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + globals@^13.6.0, globals@^13.9.0: version "13.15.0" resolved "https://registry.yarnpkg.com/globals/-/globals-13.15.0.tgz#38113218c907d2f7e98658af246cef8b77e90bac" @@ -1190,6 +1331,13 @@ is-boolean-object@^1.1.0: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-builtin-module@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.0.tgz#bb0310dfe881f144ca83f30100ceb10cf58835e0" + integrity sha512-phDA4oSGt7vl1n5tJvTWooWWAsXLY+2xCnxNqvKhGEzujg+A43wPlPOyDg3C8XQHN+6k/JTQWJ/j0dQh/qr+Hw== + dependencies: + builtin-modules "^3.3.0" + is-callable@^1.1.4, is-callable@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" @@ -1202,6 +1350,13 @@ is-core-module@^2.8.1: dependencies: has "^1.0.3" +is-core-module@^2.9.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" + integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== + dependencies: + has "^1.0.3" + is-date-object@^1.0.1: version "1.0.5" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" @@ -1226,6 +1381,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" + integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g== + is-negative-zero@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" @@ -1248,6 +1408,13 @@ is-plain-obj@^2.1.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== +is-reference@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" + integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ== + dependencies: + "@types/estree" "*" + is-regex@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" @@ -1368,6 +1535,20 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +magic-string@^0.25.3: + version "0.25.9" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" + integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ== + dependencies: + sourcemap-codec "^1.4.8" + +magic-string@^0.26.4: + version "0.26.7" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.26.7.tgz#caf7daf61b34e9982f8228c4527474dac8981d6f" + integrity sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow== + dependencies: + sourcemap-codec "^1.4.8" + make-error@^1.1.1: version "1.3.6" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" @@ -1412,6 +1593,13 @@ minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" +minimatch@^5.0.1: + version "5.1.0" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.0.tgz#1717b464f4971b144f6aabe8f2d0b8e4511e09c7" + integrity sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.2.0, minimist@^1.2.6: version "1.2.6" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" @@ -1689,6 +1877,15 @@ resolve@^1.20.0, resolve@^1.22.0: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@^1.22.1: + version "1.22.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" + integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== + dependencies: + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" @@ -1701,6 +1898,36 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" +rollup-plugin-inject@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz#e4233855bfba6c0c12a312fd6649dff9a13ee9f4" + integrity sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w== + dependencies: + estree-walker "^0.6.1" + magic-string "^0.25.3" + rollup-pluginutils "^2.8.1" + +rollup-plugin-node-polyfills@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/rollup-plugin-node-polyfills/-/rollup-plugin-node-polyfills-0.2.1.tgz#53092a2744837164d5b8a28812ba5f3ff61109fd" + integrity sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA== + dependencies: + rollup-plugin-inject "^3.0.0" + +rollup-pluginutils@^2.8.1: + version "2.8.2" + resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz#72f2af0748b592364dbd3389e600e5a9444a351e" + integrity sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ== + dependencies: + estree-walker "^0.6.1" + +rollup@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.2.3.tgz#67d894c981ad50cc811779748e52c05742560c64" + integrity sha512-qfadtkY5kl0F5e4dXVdj2D+GtOdifasXHFMiL1SMf9ADQDv5Eti6xReef9FKj+iQPR2pvtqWna57s/PjARY4fg== + optionalDependencies: + fsevents "~2.3.2" + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -1778,6 +2005,24 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +sourcemap-codec@^1.4.8: + version "1.4.8" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + string-width@^4.1.0, string-width@^4.2.0: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -1841,6 +2086,16 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +terser@^5.15.1: + version "5.15.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.15.1.tgz#8561af6e0fd6d839669c73b92bdd5777d870ed6c" + integrity sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw== + dependencies: + "@jridgewell/source-map" "^0.3.2" + acorn "^8.5.0" + commander "^2.20.0" + source-map-support "~0.5.20" + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" From 639697f3bc1b37bb543d8cc979cad6e9ccd7061c Mon Sep 17 00:00:00 2001 From: mrfrase3 Date: Sat, 17 Jun 2023 10:22:38 +0800 Subject: [PATCH 3/5] web compatibility attempt --- examples/index.html | 250 ++++++++++++++++++++++++++--- package.json | 1 + rollup.config.js | 29 +++- src/avr/avr109/avr109.ts | 57 +++++-- src/avr/index.ts | 17 +- src/avr/stk500-v1/stk500-v1.ts | 6 +- src/avr/stk500-v2/stk500-v2.ts | 4 +- src/esp/index.ts | 1 + src/esp/loader.ts | 5 +- src/index.ts | 25 ++- src/util/serial-helpers.ts | 2 +- src/web-serialport.ts | 278 +++++++++++++++++++++++++++++++++ test/boards.ts | 12 +- test/util.ts | 6 +- tsconfig.json | 5 +- yarn.lock | 5 + 16 files changed, 645 insertions(+), 58 deletions(-) create mode 100644 src/web-serialport.ts diff --git a/examples/index.html b/examples/index.html index 3e21b4f..3836a68 100644 --- a/examples/index.html +++ b/examples/index.html @@ -1,21 +1,235 @@ - - Upload Multitool - - - - - - -
- Loading... -
- - + + Upload Multitool + + + + + + + + + + +
+
+

Upload Multitool

+

+ This is a demo of the Upload Multitool. + It allows you to upload binaries to a microcontroller using a wide range of upload protocols. +

+

+ Select a device test config below. +

+ + + +
+
+
+
+ + \ No newline at end of file diff --git a/package.json b/package.json index b1c1774..88ce611 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@types/mocha": "^9.1.1", "@types/node": "^18.0.6", "@types/pako": "^2.0.0", + "@types/w3c-web-serial": "^1.0.3", "@typescript-eslint/eslint-plugin": "^5.23.0", "@typescript-eslint/parser": "^5.23.0", "chai": "^4.3.6", diff --git a/rollup.config.js b/rollup.config.js index 8f0fa6d..50fa5f4 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -3,7 +3,7 @@ const commonjs = require('@rollup/plugin-commonjs'); const json = require('@rollup/plugin-json'); const nodePolyfills = require('rollup-plugin-node-polyfills'); -module.exports = { +module.exports = [{ input: 'dist/index.js', output: [ { @@ -33,4 +33,29 @@ module.exports = { nodeResolve({ preferBuiltins: false }), json(), ], -}; +}, { + input: 'dist/web-serialport.js', + output: [ + { + file: 'dist/web-serialport.cjs', + format: 'cjs', + }, + { + file: 'dist/web-serialport.mjs', + format: 'esm', + }, + { + file: 'dist/web-serialport.umd.js', + format: 'umd', + name: 'WebSerialPort', + }, + ], + context: 'this', + plugins: [ + commonjs({ + ignoreGlobal: true, + }), + nodePolyfills({ include: ['buffer'] }), + nodeResolve({ preferBuiltins: false }), + ], +}]; diff --git a/src/avr/avr109/avr109.ts b/src/avr/avr109/avr109.ts index d63c20e..a27e443 100644 --- a/src/avr/avr109/avr109.ts +++ b/src/avr/avr109/avr109.ts @@ -1,9 +1,14 @@ import { SerialPort } from 'serialport/dist/index.d'; -import { waitForOpen } from '../../util/serial-helpers'; +import { waitForOpen, setDTRRTS } from '../../util/serial-helpers'; import asyncTimeout from '../../util/async-timeout'; +import { StdOut } from '../../index'; import { getDeviceName } from './device-lookup'; +export interface ReconnectParams { + baudRate: number; +} + interface AVR109Options { quiet?: boolean; speed?: number; @@ -11,7 +16,8 @@ interface AVR109Options { testBlockMode?: boolean; deviceCode?: number; writeToEeprom?: boolean; - avr109Reconnect: () => Promise; + stdout?: StdOut; + avr109Reconnect: (opts: ReconnectParams) => Promise; } interface ReceiveOptions { @@ -102,7 +108,7 @@ export default class AVR109 { log (...args: any[]) { if (this.quiet) return; - console.log(...args); + this.opts.stdout?.write(`${args.join(' ')}\r\n`); } async send(data: Buffer | string | number) { @@ -113,7 +119,7 @@ export default class AVR109 { if (typeof data === 'number') { buf = Buffer.from([data]); } - await this.serial.write(buf); + await (new Promise((resolve, reject) => this.serial.write(buf, undefined, (err) => (err ? reject(err) : resolve(0))))); } recv(opts: ReceiveOptions): Promise { @@ -424,7 +430,7 @@ export default class AVR109 { return rdData.equals(data); } - reconnect(): Promise { + reconnect(opts: ReconnectParams): Promise { return new Promise((resolve, reject) => { let res = (serial: SerialPort) => {}; const timeoutId = setTimeout(res, 30 * 1000); @@ -437,24 +443,38 @@ export default class AVR109 { resolve(serial); } }; - this.opts.avr109Reconnect().then(res).catch(reject); + this.opts.avr109Reconnect(opts).then(res).catch(reject); }); } async enterBootloader() { if (!this.serial.isOpen) { - await this.serial.open(); + if (!this.serial.opening) await this.serial.open(); await waitForOpen(this.serial); } - await this.serial.update({ baudRate: 1200 }); - await asyncTimeout(500); - await this.serial.close(); + await (new Promise((resolve) => this.serial.update({ baudRate: 1200 }, resolve))); await asyncTimeout(500); - this.serial = await this.reconnect(); + // await setDTRRTS(this.serial, false); + // await asyncTimeout(20); + // await setDTRRTS(this.serial, true); + // await asyncTimeout(20); + // await setDTRRTS(this.serial, false); + // await asyncTimeout(20); + // await setDTRRTS(this.serial, true); + // await this.serial.close(); + // await this.serial.open(); + const ts = Date.now(); + await (new Promise((resolve) => this.serial.close(resolve))); + this.serial = await this.reconnect({ baudRate: this.opts.speed || 57600 }); if (!this.serial.isOpen) { await waitForOpen(this.serial); } - await this.serial.update({ baudRate: this.opts.speed || 57600 }); + console.log(this.serial?.port); + if (this.serial.baudRate !== this.opts.speed) { + await this.serial.update({ baudRate: this.opts.speed || 57600 }); + } + await asyncTimeout(200); + console.log('reconnected', Date.now() - ts); } async exitBootloader() { @@ -462,9 +482,22 @@ export default class AVR109 { await this.serial.close(); } + async sync(count = 0): Promise { + try { + await this.cmd({ cmd: statics.CMD_RETURN_SOFTWARE_ID, len: 7 }); + } catch (err: any) { + if (!err.message?.includes('receiveData timeout after')) throw err; + if (count > 5) throw new Error('Failed to connect to bootloader'); + console.error(err); + return this.sync(count + 1); + } + } + async bootload(data: Buffer, opt: BootloadOptions) { this.log('Entering bootloader'); await this.enterBootloader(); + this.log('Synchronising'); + await this.sync(); this.log('Initialising'); await this.init(); this.log('Erasing Chip'); diff --git a/src/avr/index.ts b/src/avr/index.ts index e99b9ad..398ae3c 100644 --- a/src/avr/index.ts +++ b/src/avr/index.ts @@ -12,9 +12,12 @@ export const upload = async (serial: SerialPort, config: ProgramConfig) => { let uploader = null as STK500v2 | STK500v1 | AVR109 | null; switch (cpuData.protocol) { case 'stk500v1': - uploader = new STK500v1(serial, { quiet: !config.verbose }); + uploader = new STK500v1(serial, { + quiet: !config.verbose, + stdout: config.stdout, + }); await uploader.bootload( - intelHex.parse(config.hex || '').data, + intelHex.parse(config.bin || '').data, { signature: cpuData.signature, pageSize: cpuData.pageSize, @@ -23,9 +26,12 @@ export const upload = async (serial: SerialPort, config: ProgramConfig) => { ); break; case 'stk500v2': - uploader = new STK500v2(serial, { quiet: !config.verbose }); + uploader = new STK500v2(serial, { + quiet: !config.verbose, + stdout: config.stdout, + }); await uploader.bootload( - intelHex.parse(config.hex || '').data, + intelHex.parse(config.bin || '').data, cpuData, ); break; @@ -35,10 +41,11 @@ export const upload = async (serial: SerialPort, config: ProgramConfig) => { } uploader = new AVR109(serial, { quiet: !config.verbose, + stdout: config.stdout, avr109Reconnect: config.avr109Reconnect, }); await uploader.bootload( - intelHex.parse(config.hex || '').data, + intelHex.parse(config.bin || '').data, cpuData, ); break; diff --git a/src/avr/stk500-v1/stk500-v1.ts b/src/avr/stk500-v1/stk500-v1.ts index dd3f82e..f94c649 100644 --- a/src/avr/stk500-v1/stk500-v1.ts +++ b/src/avr/stk500-v1/stk500-v1.ts @@ -4,9 +4,11 @@ import { SerialPort } from 'serialport/dist/index.d'; import { setDTRRTS } from '../../util/serial-helpers'; import asyncTimeout from '../../util/async-timeout'; +import { StdOut } from '../../index'; interface STK500v1Options { quiet?: boolean; + stdout?: StdOut; } interface SendCommandOptions { @@ -78,7 +80,7 @@ export default class STK500v1 { log (...args: any[]) { if (this.quiet) return; - console.log(...args); + this.opts.stdout?.write(`${args.join(' ')}\r\n`); } receiveData(timeout = 0, responseLength: number): Promise { @@ -107,7 +109,7 @@ export default class STK500v1 { while (!started && index < data.length) { const byte = data[index]; if (startingBytes.indexOf(byte) !== -1) { - data = data.slice(index, data.length - index); + data = data.subarray(index, data.length - index); started = true; } index += 1; diff --git a/src/avr/stk500-v2/stk500-v2.ts b/src/avr/stk500-v2/stk500-v2.ts index 851a2ea..7a3f00b 100644 --- a/src/avr/stk500-v2/stk500-v2.ts +++ b/src/avr/stk500-v2/stk500-v2.ts @@ -3,9 +3,11 @@ import { SerialPort } from 'serialport/dist/index.d'; import statics from './constants'; import { setDTRRTS } from '../../util/serial-helpers'; import asyncTimeout from '../../util/async-timeout'; +import { StdOut } from '../../index'; interface STK500v2Options { quiet?: boolean; + stdout?: StdOut; } interface SendCommandOptions { @@ -55,7 +57,7 @@ export default class STK500v2 { log(...args: any[]) { if (this.quiet) return; - console.log(...args); + this.opts.stdout?.write(`${args.join(' ')}\r\n`); } receiveData(timeout = 0, responseLength?: number): Promise { diff --git a/src/esp/index.ts b/src/esp/index.ts index d381cc9..80f3430 100644 --- a/src/esp/index.ts +++ b/src/esp/index.ts @@ -20,6 +20,7 @@ export const upload = async (serial: SerialPort, config: ProgramConfig) => { try { espLoader = new ESPLoader(serial, { quiet: !config.verbose, + stdout: config.stdout, } as ESPOptions); await espLoader.mainFn(); // await espLoader.flash_id(); diff --git a/src/esp/loader.ts b/src/esp/loader.ts index 7d1adef..59317ac 100644 --- a/src/esp/loader.ts +++ b/src/esp/loader.ts @@ -5,11 +5,12 @@ import encBase64 from 'crypto-js/enc-base64'; import StubLoader from './stub-loader'; import roms from './roms/index'; import ROM from './roms/rom.d'; +import { StdOut } from '../index'; export interface ESPOptions { quiet?: boolean; stubUrl?: string; - stdout?: any; + stdout?: StdOut; } export interface UploadFileDef { @@ -107,7 +108,7 @@ export default class ESPLoader { // log out a line of text log (...args: any[]) { if (this.quiet) return; - this.stdout.write(`${args.map(arg => `${arg}`).join(' ')}\n`); + this.stdout.write(`${args.map(arg => `${arg}`).join(' ')}\r\n`); } // log out a set of characters diff --git a/src/index.ts b/src/index.ts index 4d1a10b..6a688ea 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ import { SerialPort } from 'serialport/dist/index.d'; import { setBaud, waitForOpen } from './util/serial-helpers'; +import { ReconnectParams } from './avr/avr109/avr109'; import avr from './avr/index'; import esp from './esp/index'; @@ -9,8 +10,12 @@ export interface ProgramFile { address: number; } +export interface StdOut { + write: (data: string) => void; +} + export interface ProgramConfig { - hex?: Buffer; + bin?: Buffer | string; files?: ProgramFile[]; speed?: number; uploadSpeed?: number; @@ -19,16 +24,28 @@ export interface ProgramConfig { verbose?: boolean; flashMode?: string; flashFreq?: string; - avr109Reconnect?: () => Promise; + avr109Reconnect?: (opts: ReconnectParams) => Promise; + stdout?: StdOut; } export const upload = async (serial: SerialPort, config: ProgramConfig) => { - if (!config.hex && !config.files?.length) { + if (!config.bin && !config.files?.length) { throw new Error('No hex or files provided for upload'); } + if (!config.bin && config.files?.length) { + config.bin = Buffer.from(config.files[0].data, 'base64'); + } + if (typeof config.bin === 'string') { + config.bin = Buffer.from(config.bin, 'base64'); + } + if (!config.stdout) { + config.stdout = process?.stdout || { + write: (str: string) => console.log(str.replace(/(\n|\r)+$/g, '')), + }; + } // ensure serial port is open if (!serial.isOpen) { - serial.open(); + if (!serial.opening) serial.open(); await waitForOpen(serial); } diff --git a/src/util/serial-helpers.ts b/src/util/serial-helpers.ts index 557f587..0481007 100644 --- a/src/util/serial-helpers.ts +++ b/src/util/serial-helpers.ts @@ -19,7 +19,7 @@ export const waitForOpen = (serial: SerialPort, timeout: number = 1000): Promise }; serial.on('open', handleOpen); cleanup = () => { - serial.off('open', handleOpen); + serial.removeListener('open', handleOpen); clearTimeout(timer); } }); diff --git a/src/web-serialport.ts b/src/web-serialport.ts new file mode 100644 index 0000000..f966fed --- /dev/null +++ b/src/web-serialport.ts @@ -0,0 +1,278 @@ +import { + PortInfo, BindingPortInterface, BindingInterface, + SetOptions, UpdateOptions, +} from '@serialport/bindings-interface'; +import { SerialPortStream, OpenOptions } from '@serialport/stream'; +import '@types/w3c-web-serial'; +import EventEmitter from 'events'; + +export interface WebPortInfo extends PortInfo { + port: SerialPort; +} + +export interface WebOpenOptions extends Partial { + port: SerialPort | WebPortInfo; + baudRate: number; +} + +interface WebStreamOpenOptions extends WebOpenOptions { + binding: typeof WebSerialPortBinding; + path: string; + streamer: SerialPortStream; +} + +class WebSerialPortBinding extends EventEmitter implements BindingPortInterface { + + static async list(): Promise { + const ports = await navigator.serial.getPorts(); + return ports.map(port => { + const info = port.getInfo(); + return { + path: 'not available', + vendorId: info.usbVendorId?.toString(16).padStart(4, '0'), + productId: info.usbProductId?.toString(16).padStart(4, '0'), + port, + } as WebPortInfo; + }); + } + + static async open(options: OpenOptions) { + const opts = options as WebOpenOptions; + if (!opts.port) throw new Error('Port is required'); + const binding = new WebSerialPortBinding(opts.port as SerialPort | WebPortInfo, opts); + await binding.#open(opts); + return binding; + } + + openOptions: Required; + port: SerialPort; + isOpen: boolean; + baudRate: number; + #opening = false; + #closing = false; + #writePromise: Promise | null = null; + #readPromise: Promise | null = null; + #readableStream: ReadableStreamDefaultReader | null = null; + // #closeReader: () => Promise = () => Promise.resolve(); + + constructor(port: SerialPort | WebPortInfo, opts: WebOpenOptions) { + super(); + this.port = port instanceof SerialPort ? port : port.port; + this.isOpen = false; + this.baudRate = opts.baudRate || 9600; + this.openOptions = opts as Required; + } + + async #closeReader() { + if (!this.#readableStream || !this.port.readable?.locked) return; + await this.#readableStream.cancel(); + await this.#readableStream.releaseLock(); + } + + #registerReader() { + if (!this.port?.readable || this.port.readable.locked) return; + this.#readPromise = new Promise(async (resolve) => { + if (!this.port?.readable) return; + this.#readableStream = this.port.readable.getReader(); + let stop = false; + // this.#closeReader = () => { + // stop = true; + // return this.#readPromise || Promise.resolve(); + // }; + while (!stop && this.port.readable.locked) { + const { value, done } = await this.#readableStream.read(); + if (done) { + stop = true; + } else if (value) { + const buffer = Buffer.from(value.buffer); + console.log('read hex', buffer.toString('hex')); + console.log('read utf8', buffer.toString('utf8')); + this.emit('data', buffer); + } + } + if (this.port.readable.locked) { + await this.#readableStream.cancel(); + await this.#readableStream.releaseLock(); + } + resolve(); + }); + } + + + async #open(opts: WebOpenOptions = { port: this.port, baudRate: this.baudRate }) { + if (this.#opening) return; + this.#opening = true; + try { + const options = { ...this.openOptions, ...opts }; + if (this.isOpen) { + await this.close(); + } + if (options.baudRate) { + this.baudRate = options.baudRate; + } + try { + console.log(this.baudRate); + await this.port.open({ + baudRate: this.baudRate, + // dataBits: options.dataBits, + // stopBits: options.stopBits, + // parity: options.parity as ParityType, + // flowControl: options.rtscts ? 'hardware' : 'none', + // bufferSize: 1, + }); + } catch (err: any) { + if (!err.message.includes('The port is already open')) { + throw err; + } + } + + this.#registerReader(); + this.isOpen = true; + this.emit('open'); + } finally { + this.#opening = false; + } + } + + async close() { + if (this.#closing) return; + if (!this.isOpen) return; + this.#closing = true; + try { + await this.#closeReader(); + console.log(this.port, this.#readPromise); + await this.port.close(); + this.isOpen = false; + this.emit('close'); + } finally { + this.#closing = false; + } + } + + async #write(buffer: Buffer) { + const writer = this.port.writable?.getWriter(); + if (!writer) throw new Error('Port is not writable'); + console.log('write hex', buffer.toString('hex')); + console.log('write utf8', buffer.toString('utf8')); + await writer.write(buffer); + await writer.close(); + } + + write(buffer: Buffer) { + if (this.#writePromise) { + this.#writePromise = this.#writePromise.then(() => this.#write(buffer)); + } else { + this.#writePromise = this.#write(buffer); + } + return this.#writePromise; + } + + read(buffer: Buffer, offset: number, length: number) { + let dataBuffer = Buffer.alloc(0); + return new Promise<{ buffer: Buffer, bytesRead: number }>((resolve, reject) => { + let cleanup = (err?: Error) => {}; + const onData = (data: Buffer) => { + dataBuffer = Buffer.concat([dataBuffer, data]); + if (dataBuffer.length >= length) { + buffer.set(dataBuffer.subarray(0, length), offset); + cleanup(); + } + } + const onClose = () => { + cleanup(new Error('Port closed before read completed')); + } + const onError = (err: Error) => { + cleanup(err); + } + cleanup = (err?: Error) => { + this.removeListener('data', onData); + this.removeListener('close', onClose); + this.removeListener('error', onError); + if (err) { + reject(err); + } else { + resolve({ buffer, bytesRead: dataBuffer.length }); + } + } + this.on('data', onData); + this.on('close', onClose); + this.on('error', onError); + }); + } + + async update(options: UpdateOptions) { + if (this.baudRate === options.baudRate) return; + if (options.baudRate) { + this.baudRate = options.baudRate; + } + if (this.isOpen) { + await this.close(); + await this.#open({ ...options, port: this.port }); + } + } + + async set(options: SetOptions) { + console.log(options, this.port); + const mappedOpts = {} as SerialOutputSignals; + if (typeof options.dtr === 'boolean') mappedOpts.dataTerminalReady = options.dtr; + if (typeof options.rts === 'boolean') mappedOpts.requestToSend = options.rts; + if (typeof options.brk === 'boolean') mappedOpts.break = options.brk; + console.log(mappedOpts); + await this.port.setSignals(mappedOpts); + console.log(1); + } + + async get() { + const signals = await this.port.getSignals(); + return { + cts: signals.clearToSend, + dsr: signals.dataSetReady, + dcd: signals.dataCarrierDetect, + }; + } + + async flush() { + // pretend to flush + } + + async drain() { + // pretend to drain + } + + async getBaudRate() { + return { baudRate: this.baudRate }; + } + +} + +export default class WebSerialPort extends SerialPortStream { + + static isSupported() { + return 'serial' in navigator; + } + + static async requestPort(reqOpts: SerialPortRequestOptions = {}, openOpts: OpenOptions) { + console.log(reqOpts); + const port = await navigator.serial.requestPort(reqOpts); + return new WebSerialPort(port, openOpts); + } + + static list() { + return WebSerialPortBinding.list(); + } + + constructor(port: SerialPort | WebPortInfo, options: Partial) { + super({ + ...options, + path: 'not available', + binding: WebSerialPortBinding, + port, + } as WebStreamOpenOptions); + this.once('open', () => { + const port = this.port as WebSerialPortBinding; + if (port) port.on('data', (data: Buffer) => { + this.emit('data', data); + }); + }); + } +} diff --git a/test/boards.ts b/test/boards.ts index 4aee11f..cd27984 100644 --- a/test/boards.ts +++ b/test/boards.ts @@ -5,7 +5,7 @@ import { SerialPort } from 'serialport'; import { upload } from '../src/index'; import { waitForData, waitForDevice, - config, getHex, + config, getBin, espIdentify, ESPIdentifyResult, } from './util'; import { waitForOpen } from '../src/util/serial-helpers'; @@ -17,7 +17,7 @@ const listPromise = espIdentify(numEsps); Object.keys(config.devices).forEach((deviceRef) => { const device = config.devices[deviceRef]; let key = ''; - let hex: Buffer | undefined; + let bin: Buffer | undefined; let files: ProgramFile[] | undefined; let serial: SerialPort; let flashMode: string | undefined; @@ -28,13 +28,13 @@ Object.keys(config.devices).forEach((deviceRef) => { this.timeout(120 * 1000); before(async () => { - const res = await getHex(device.code, device.fqbn.trim()); + const res = await getBin(device.code, device.fqbn.trim()); key = res.key; - hex = res.hex; + bin = res.bin; files = res.files; flashMode = res.flashMode; flashFreq = res.flashFreq; - console.log('compiled hex'); + console.log('compiled bin'); }); beforeEach(async () => { @@ -70,7 +70,7 @@ Object.keys(config.devices).forEach((deviceRef) => { it(`should upload to ${device.name}`, async function() { this.retries(config.retries || 1); await upload(serial, { - hex, + bin, files, flashMode, flashFreq, diff --git a/test/util.ts b/test/util.ts index 7ac2f7a..53479d8 100644 --- a/test/util.ts +++ b/test/util.ts @@ -61,7 +61,7 @@ interface TestConfig { export const config = YAML.parse(fs.readFileSync(path.join(__dirname, 'test-config.yml'), 'utf8')) as TestConfig; interface HexResult { - hex?: Buffer; + bin?: Buffer; files?: ProgramFile[]; key: string; code: string; @@ -69,7 +69,7 @@ interface HexResult { flashFreq?: string; } -export const getHex = async (file: string, fqbn: string): Promise => { +export const getBin = async (file: string, fqbn: string): Promise => { const key = Math.random().toString(16).substring(7); const code = fs .readFileSync(path.join(__dirname, `code/${file}.ino`), 'utf8') @@ -84,7 +84,7 @@ export const getHex = async (file: string, fqbn: string): Promise => // console.log({ ...res.data, files: null }); // fs.writeFileSync(path.join(__dirname, `compiled-data.json`), JSON.stringify(res.data, null, 2)); return { - hex: res.data.hex ? Buffer.from(res.data.hex, 'base64') : undefined, + bin: res.data.hex ? Buffer.from(res.data.hex, 'base64') : undefined, files: res.data.files as ProgramFile[], key, code, diff --git a/tsconfig.json b/tsconfig.json index cc403e5..1fac056 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es2015", + "target": "esnext", "module": "es6", "moduleResolution": "node", "lib": ["es2017", "es7", "es6", "dom", "dom.iterable"], @@ -17,7 +17,8 @@ }, "files": [ "src/index.ts", - "src/global.d.ts" + "src/global.d.ts", + "src/web-serialport.ts" ], "include": ["src"], "exclude": ["node_modules", "dist", "examples"] diff --git a/yarn.lock b/yarn.lock index efb186a..7474038 100644 --- a/yarn.lock +++ b/yarn.lock @@ -311,6 +311,11 @@ resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975" integrity sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q== +"@types/w3c-web-serial@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@types/w3c-web-serial/-/w3c-web-serial-1.0.3.tgz#9fd5e8542f74e464bb1715b384b5c0dcbf2fb2c3" + integrity sha512-R4J/OjqKAUFQoXVIkaUTfzb/sl6hLh/ZhDTfowJTRMa7LhgEmI/jXV4zsL1u8HpNa853BxwNmDIr0pauizzwSQ== + "@typescript-eslint/eslint-plugin@^5.23.0": version "5.23.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.23.0.tgz#bc4cbcf91fbbcc2e47e534774781b82ae25cc3d8" From 5aca2d2fe411d7f993e80efc7b451651b9ca775d Mon Sep 17 00:00:00 2001 From: mrfrase3 Date: Sun, 18 Jun 2023 14:24:58 +0800 Subject: [PATCH 4/5] final web support and serialport promise wrapper --- examples/index.html | 14 +- rollup.config.js | 2 +- src/avr/avr109/avr109.ts | 73 +++++---- src/avr/index.ts | 9 +- src/avr/stk500-v1/stk500-v1.ts | 7 +- src/avr/stk500-v2/stk500-v2.ts | 9 +- src/esp/index.ts | 7 +- src/esp/loader.ts | 20 ++- src/esp/stub-loader.ts | 13 +- src/index.ts | 21 ++- src/serialport/serialport-promise.ts | 213 +++++++++++++++++++++++++ src/{ => serialport}/web-serialport.ts | 0 src/util/serial-helpers.ts | 42 +++-- test/boards.ts | 46 +++--- test/test-config.yml | 9 +- test/util.ts | 8 +- tsconfig.json | 4 +- 17 files changed, 395 insertions(+), 102 deletions(-) create mode 100644 src/serialport/serialport-promise.ts rename src/{ => serialport}/web-serialport.ts (100%) diff --git a/examples/index.html b/examples/index.html index 3836a68..8616d80 100644 --- a/examples/index.html +++ b/examples/index.html @@ -54,9 +54,7 @@

Upload Multitool

const getFilters = (deviceConfig) => { const filters = []; - if (deviceConfig.espChip || deviceConfig.mac) { - filters.push({ usbVendorId: 0x1a86, usbProductId: 0x7523 }); - } else if (deviceConfig.vendorIds && deviceConfig.productIds) { + if (deviceConfig.vendorIds && deviceConfig.productIds) { deviceConfig.vendorIds.forEach((vendorId) => { deviceConfig.productIds.forEach((productId) => { filters.push({ @@ -65,6 +63,8 @@

Upload Multitool

}); }); }); + } else if (deviceConfig.espChip || deviceConfig.mac) { + filters.push({ usbVendorId: 0x1a86, usbProductId: 0x7523 }); } return filters; }; @@ -159,7 +159,7 @@

Upload Multitool

setStatus('Requesting Device...'); filters = getFilters(deviceConfig); WebSerialPort.list().then(console.log); - const serial = await WebSerialPort.requestPort( + let serial = await WebSerialPort.requestPort( { filters }, { baudRate: deviceConfig.speed || 115200 }, ); @@ -170,7 +170,7 @@

Upload Multitool

} = await getBin(deviceConfig.code, deviceConfig.fqbn); setStatus('Uploading...'); - await upload(serial, { + const res = await upload(serial, { bin, files, flashMode, @@ -197,11 +197,13 @@

Upload Multitool

} }); + serial = res.serialport; + setStatus('Validating Upload...'); await validateUpload(serial, key); setStatus('Cleaning Up...'); await serial.close(); - setStatus('Done! Success! Awesome!'); + setStatus(`Done! Success! Awesome! (${res.time}ms)`); } catch (err) { console.error(err); setStatus(`Error: ${err.message}`); diff --git a/rollup.config.js b/rollup.config.js index 50fa5f4..8e6816c 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -34,7 +34,7 @@ module.exports = [{ json(), ], }, { - input: 'dist/web-serialport.js', + input: 'dist/serialport/web-serialport.js', output: [ { file: 'dist/web-serialport.cjs', diff --git a/src/avr/avr109/avr109.ts b/src/avr/avr109/avr109.ts index a27e443..4ed49ce 100644 --- a/src/avr/avr109/avr109.ts +++ b/src/avr/avr109/avr109.ts @@ -1,4 +1,5 @@ import { SerialPort } from 'serialport/dist/index.d'; +import { SerialPortPromise } from '../../serialport/serialport-promise'; import { waitForOpen, setDTRRTS } from '../../util/serial-helpers'; import asyncTimeout from '../../util/async-timeout'; import { StdOut } from '../../index'; @@ -84,25 +85,25 @@ export default class AVR109 { opts: AVR109Options; quiet: boolean; signature: string; - serial: SerialPort; - orgSerialPort: SerialPort; + serial: SerialPortPromise; + orgSerialPort: SerialPortPromise; orgSpeed: number; hasAutoIncrAddr: boolean; bufferSize: number; useBlockMode: boolean; deviceCode: number; - constructor(serial: SerialPort, opts: AVR109Options) { + constructor(serial: SerialPort | SerialPortPromise, opts: AVR109Options) { this.opts = opts || {}; this.signature = this.opts.signature || 'LUFACDC'; this.quiet = this.opts.quiet || false; - this.serial = serial; + this.serial = serial instanceof SerialPortPromise ? serial : new SerialPortPromise(serial); this.hasAutoIncrAddr = false; this.bufferSize = 0; this.useBlockMode = false; this.deviceCode = this.opts.deviceCode || 0; - this.orgSerialPort = serial; + this.orgSerialPort = this.serial; this.orgSpeed = this.serial.baudRate || 115200; } @@ -112,14 +113,16 @@ export default class AVR109 { } async send(data: Buffer | string | number) { - let buf = data; + let buf; if (typeof data === 'string') { buf = Buffer.from(data, 'ascii'); - } - if (typeof data === 'number') { + } else if (typeof data === 'number') { buf = Buffer.from([data]); + } else { + buf = data; } - await (new Promise((resolve, reject) => this.serial.write(buf, undefined, (err) => (err ? reject(err) : resolve(0))))); + + await this.serial.write(buf); } recv(opts: ReceiveOptions): Promise { @@ -430,29 +433,31 @@ export default class AVR109 { return rdData.equals(data); } - reconnect(opts: ReconnectParams): Promise { + reconnect(opts: ReconnectParams): Promise { return new Promise((resolve, reject) => { - let res = (serial: SerialPort) => {}; - const timeoutId = setTimeout(res, 30 * 1000); - res = (serial) => { - clearTimeout(timeoutId); - if (!serial) { - this.log('reconnect timed out'); - reject(new Error('reconnect timed out')); - } else { - resolve(serial); - } - }; - this.opts.avr109Reconnect(opts).then(res).catch(reject); + let timedOut = false; + let timeoutId: NodeJS.Timeout; + timeoutId = setTimeout(() => { + timedOut = true; + this.log('reconnect timed out'); + reject(new Error('reconnect timed out')); + }, 30 * 1000); + this.opts.avr109Reconnect(opts) + .then((serial: SerialPort | SerialPortPromise) => { + clearTimeout(timeoutId); + if (timedOut) return; + resolve(serial instanceof SerialPortPromise ? serial : new SerialPortPromise(serial)); + }) + .catch(reject); }); } async enterBootloader() { if (!this.serial.isOpen) { - if (!this.serial.opening) await this.serial.open(); + await this.serial.open(); await waitForOpen(this.serial); } - await (new Promise((resolve) => this.serial.update({ baudRate: 1200 }, resolve))); + await this.serial.update({ baudRate: 1200 }); await asyncTimeout(500); // await setDTRRTS(this.serial, false); // await asyncTimeout(20); @@ -464,9 +469,11 @@ export default class AVR109 { // await this.serial.close(); // await this.serial.open(); const ts = Date.now(); - await (new Promise((resolve) => this.serial.close(resolve))); + await this.serial.close(); + await asyncTimeout(500); this.serial = await this.reconnect({ baudRate: this.opts.speed || 57600 }); if (!this.serial.isOpen) { + await this.serial.open(); await waitForOpen(this.serial); } console.log(this.serial?.port); @@ -516,11 +523,17 @@ export default class AVR109 { await this.exitBootloader(); this.log('Reconnecting'); await asyncTimeout(2 * 1000); - this.serial = this.orgSerialPort; - // this.serial = await this.reconnect(); - await this.serial.open(); - await waitForOpen(this.serial, 1000); - await this.serial.update({ baudRate: this.orgSpeed }); + if (typeof window === 'undefined') { + this.serial = this.orgSerialPort; + await this.serial.open(); + await waitForOpen(this.serial, 1000); + await this.serial.update({ baudRate: this.orgSpeed }); + } else { + this.serial = await this.reconnect({ baudRate: this.orgSpeed }); + await this.serial.open(); + await waitForOpen(this.serial, 1000); + } + return this.serial; } } diff --git a/src/avr/index.ts b/src/avr/index.ts index 398ae3c..6306341 100644 --- a/src/avr/index.ts +++ b/src/avr/index.ts @@ -1,13 +1,14 @@ -import { SerialPort } from 'serialport/dist/index.d'; import { ProgramConfig } from '../index'; +import { SerialPortPromise } from '../serialport/serialport-promise'; import intelHex from 'intel-hex'; import getCpuData from './avr-cpu-data'; + import STK500v1 from './stk500-v1/stk500-v1'; import STK500v2 from './stk500-v2/stk500-v2'; import AVR109 from './avr109/avr109'; -export const upload = async (serial: SerialPort, config: ProgramConfig) => { +export const upload = async (serial: SerialPortPromise, config: ProgramConfig) => { const cpuData = getCpuData(config.cpu); let uploader = null as STK500v2 | STK500v1 | AVR109 | null; switch (cpuData.protocol) { @@ -44,14 +45,14 @@ export const upload = async (serial: SerialPort, config: ProgramConfig) => { stdout: config.stdout, avr109Reconnect: config.avr109Reconnect, }); - await uploader.bootload( + return await uploader.bootload( intelHex.parse(config.bin || '').data, cpuData, ); - break; default: throw new Error(`Protocol ${cpuData.protocol} not supported`); } + return serial; }; export const isSupported = (cpu: string) => { diff --git a/src/avr/stk500-v1/stk500-v1.ts b/src/avr/stk500-v1/stk500-v1.ts index f94c649..3d33fb5 100644 --- a/src/avr/stk500-v1/stk500-v1.ts +++ b/src/avr/stk500-v1/stk500-v1.ts @@ -2,6 +2,7 @@ // converted to typescript/modernised by mrfrase3 (GPL-3.0 license) import { SerialPort } from 'serialport/dist/index.d'; +import { SerialPortPromise } from '../../serialport/serialport-promise'; import { setDTRRTS } from '../../util/serial-helpers'; import asyncTimeout from '../../util/async-timeout'; import { StdOut } from '../../index'; @@ -70,12 +71,12 @@ statics.OK_RESPONSE = Buffer.from([statics.RES_STK_INSYNC, statics.RES_STK_OK]); export default class STK500v1 { opts: STK500v1Options; quiet: boolean; - serial: SerialPort; + serial: SerialPortPromise; - constructor(serial: SerialPort, opts: STK500v1Options) { + constructor(serial: SerialPort | SerialPortPromise, opts: STK500v1Options) { this.opts = opts || {}; this.quiet = this.opts.quiet || false; - this.serial = serial; + this.serial = serial instanceof SerialPortPromise ? serial : new SerialPortPromise(serial); } log (...args: any[]) { diff --git a/src/avr/stk500-v2/stk500-v2.ts b/src/avr/stk500-v2/stk500-v2.ts index 7a3f00b..7fd375f 100644 --- a/src/avr/stk500-v2/stk500-v2.ts +++ b/src/avr/stk500-v2/stk500-v2.ts @@ -1,4 +1,5 @@ import { SerialPort } from 'serialport/dist/index.d'; +import { SerialPortPromise } from '../../serialport/serialport-promise'; import statics from './constants'; import { setDTRRTS } from '../../util/serial-helpers'; @@ -45,12 +46,12 @@ const defaultProgramOptions = { export default class STK500v2 { opts: STK500v2Options; quiet: boolean; - serial: SerialPort; + serial: SerialPortPromise; sequence: number; - constructor(serial: SerialPort, opts: STK500v2Options) { + constructor(serial: SerialPort | SerialPortPromise, opts: STK500v2Options) { this.opts = opts; - this.serial = serial; + this.serial = serial instanceof SerialPortPromise ? serial : new SerialPortPromise(serial); this.quiet = opts.quiet || false; this.sequence = 0; } @@ -238,6 +239,8 @@ export default class STK500v2 { async reset(delay1: number, delay2: number) { this.log('reset'); + await setDTRRTS(this.serial, false); + await asyncTimeout(delay1); await setDTRRTS(this.serial, true); await asyncTimeout(delay1); diff --git a/src/esp/index.ts b/src/esp/index.ts index 80f3430..779fda4 100644 --- a/src/esp/index.ts +++ b/src/esp/index.ts @@ -1,11 +1,11 @@ -import { SerialPort } from 'serialport/dist/index.d'; +import { SerialPortPromise } from '../serialport/serialport-promise'; import { ProgramConfig } from '../index'; import ESPLoader, { ESPOptions, UploadFileDef } from './loader'; import asyncTimeout from '../util/async-timeout'; const isSupported = (cpu: string) => ['esp8266', 'esp32'].includes(cpu); -export const upload = async (serial: SerialPort, config: ProgramConfig) => { +export const upload = async (serial: SerialPortPromise, config: ProgramConfig) => { if (!config.files?.length) throw new Error('No files to upload'); // const log = (...args) => config.debug(`${args.join(' ')}\r\n`); const log = (...args: any[]) => console.log(...args); @@ -38,7 +38,7 @@ export const upload = async (serial: SerialPort, config: ProgramConfig) => { // eslint-disable-next-line no-console console.error(err2); } - return; + throw err; } try { @@ -63,6 +63,7 @@ export const upload = async (serial: SerialPort, config: ProgramConfig) => { log('Failed to upload:', err instanceof Error ? err.message : err); } + return serial; }; export default { upload, isSupported }; \ No newline at end of file diff --git a/src/esp/loader.ts b/src/esp/loader.ts index 59317ac..9562c2c 100644 --- a/src/esp/loader.ts +++ b/src/esp/loader.ts @@ -1,4 +1,5 @@ import { SerialPort } from 'serialport/dist/index.d'; +import { SerialPortPromise } from '../serialport/serialport-promise'; import pako from 'pako'; import MD5 from 'crypto-js/md5'; import encBase64 from 'crypto-js/enc-base64'; @@ -78,7 +79,7 @@ export default class ESPLoader { opts: ESPOptions; quiet: boolean; - serial: SerialPort; + serial: SerialPortPromise; IS_STUB: boolean; chip: ROM | null; stdout: any; @@ -86,10 +87,10 @@ export default class ESPLoader { syncStubDetected: boolean; FLASH_WRITE_SIZE: number; - constructor(serial: SerialPort, opts = {} as ESPOptions) { + constructor(serial: SerialPort | SerialPortPromise, opts = {} as ESPOptions) { this.opts = opts || {}; this.quiet = this.opts.quiet || false; - this.serial = serial; + this.serial = serial instanceof SerialPortPromise ? serial : new SerialPortPromise(serial); this.IS_STUB = false; this.chip = null; this.stdout = opts.stdout || process?.stdout || { @@ -239,7 +240,9 @@ export default class ESPLoader { started = true; } } - if (pkt.length) buffer = Buffer.concat([buffer, new Uint8Array(pkt)]); + if (pkt.length) { + buffer = Buffer.concat([buffer, Buffer.from(pkt)]); + } // if the packet is complete, call the finished handler if (buffer.length && !started) { finished(); @@ -349,7 +352,8 @@ export default class ESPLoader { if (mode !== 'no_reset') { // reset the device before syncing await this.serial.set({ dtr: false, rts: false }); - await this.#sleep(100); + await this.#sleep(50); + await this.serial.set({ dtr: true, rts: true }); await this.serial.set({ dtr: false, rts: true }); await this.#sleep(100); if (esp32r0Delay) { @@ -359,15 +363,17 @@ export default class ESPLoader { await this.serial.set({ dtr: true, rts: false }); if (esp32r0Delay) { // await this._sleep(400); + // await this.#sleep(400); } await this.#sleep(50); await this.serial.set({ dtr: false, rts: false }); + await this.serial.set({ dtr: false, rts: false }); } // wait until the device is finished booting (writing initial data to serial) // eslint-disable-next-line no-constant-condition while (1) { try { - await this.read(1000, true); + await this.read(500, true); } catch (err) { // if nothing was read, the device is ready if (err instanceof Error && err.message.includes('timeout')) { @@ -384,6 +390,8 @@ export default class ESPLoader { } catch (err) { if (err instanceof Error && err.message.includes('timeout')) { this.logChar(esp32r0Delay ? '_' : '.'); + } else { + throw err; } } await this.#sleep(50); diff --git a/src/esp/stub-loader.ts b/src/esp/stub-loader.ts index 369e4c4..a2d8be6 100644 --- a/src/esp/stub-loader.ts +++ b/src/esp/stub-loader.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +// import axios from 'axios'; /* Stub loaders are uploaded and run in-memory on the target device. @@ -39,7 +39,16 @@ export default class StubLoader { if (cache[stubName]) { return cache[stubName]; } - const { data: res } = await axios.get(`${this.stubsUrl}/stub_flasher_${stubName}.json`); + let res = null as any; + if (typeof window === 'undefined' && typeof fetch === 'undefined') { + const { default: axios } = await import('axios'); + const response = await axios.get(`${this.stubsUrl}/stub_flasher_${stubName}.json`); + res = response.data; + } else { + const response = await fetch(`${this.stubsUrl}/stub_flasher_${stubName}.json`); + res = await response.json(); + } + const stub = { data: Buffer.from(res.data, 'base64'), diff --git a/src/index.ts b/src/index.ts index 6a688ea..331f564 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ import { SerialPort } from 'serialport/dist/index.d'; +import { SerialPortPromise } from './serialport/serialport-promise'; import { setBaud, waitForOpen } from './util/serial-helpers'; import { ReconnectParams } from './avr/avr109/avr109'; @@ -28,7 +29,8 @@ export interface ProgramConfig { stdout?: StdOut; } -export const upload = async (serial: SerialPort, config: ProgramConfig) => { +export const upload = async (serialport: SerialPort, config: ProgramConfig) => { + const serial = new SerialPortPromise(serialport); if (!config.bin && !config.files?.length) { throw new Error('No hex or files provided for upload'); } @@ -43,9 +45,10 @@ export const upload = async (serial: SerialPort, config: ProgramConfig) => { write: (str: string) => console.log(str.replace(/(\n|\r)+$/g, '')), }; } + const ts = Date.now(); // ensure serial port is open if (!serial.isOpen) { - if (!serial.opening) serial.open(); + await serial.open(); await waitForOpen(serial); } @@ -55,24 +58,30 @@ export const upload = async (serial: SerialPort, config: ProgramConfig) => { await setBaud(serial, config.speed); } + let newPort: SerialPortPromise | undefined; // upload using the correct tool/protocol switch (config.tool) { case 'avr': case 'avrdude': - await avr.upload(serial, config); + newPort = await avr.upload(serial, config); break; case 'esptool': case 'esptool_py': - await esp.upload(serial, config); + newPort = await esp.upload(serial, config); break; default: throw new Error(`Tool ${config.tool} not supported`); } // restore original baud rate - if (serial.baudRate !== existingBaud) { - await setBaud(serial, existingBaud); + if (newPort.baudRate !== existingBaud) { + await setBaud(newPort, existingBaud); } + + return { + serialport: newPort, + time: Date.now() - ts, + }; }; export const isSupported = (tool: string, cpu: string) => { diff --git a/src/serialport/serialport-promise.ts b/src/serialport/serialport-promise.ts new file mode 100644 index 0000000..a83606a --- /dev/null +++ b/src/serialport/serialport-promise.ts @@ -0,0 +1,213 @@ +import { SerialPort } from 'serialport'; +import { SetOptions } from '@serialport/bindings-interface' +import EventEmitter from 'events'; + +// a class that wraps a serial port and provides a promise-based interface + +export class SerialPortPromise extends EventEmitter { + port: SerialPort; + + /** + * Consumes a serial port and returns a similar promise-based interface + * @emits open + * @emits data + * @emits close + * @emits error + */ + constructor(port: SerialPort) { + super(); + this.port = port; + this.port.on('open', () => this.emit('open')); + this.port.on('close', () => this.emit('close')); + this.port.on('error', (err) => this.emit('error', err)); + this.port.on('data', (data) => this.emit('data', data)); + } + + get isOpen(): boolean { + return this.port.isOpen; + } + + get path(): string { + return this.port.path; + } + + get baudRate(): number { + return this.port.baudRate; + } + + /** + * Opens a connection to the given serial port. + * @emits open + */ + async open(): Promise { + return new Promise((resolve, reject) => { + this.port.open((err) => { + if (err) { + if (err.message.includes('Port is opening')) { + this.port.once('open', resolve); + } + else reject(err); + } + else resolve(); + }); + }); + } + + /** + * Closes an open connection. + * + * If there are in progress writes when the port is closed the writes will error. + */ + async close(): Promise { + return new Promise((resolve, reject) => { + this.port.close((err) => { + if (err) reject(err); + else resolve(); + }); + }); + } + + /** + * Returns the number of bytes that have been read in the internal buffer. + * @param {number} [size] Optional number of bytes to return from the read buffer. + * @returns {Buffer|null} The data from the read buffer. null is returned when no data is available. + */ + async read(size?: number): Promise { + return this.port.read(size); + } + + + /** + * Writes data to the given serial port. Buffers written data if the port is not open. + + The write operation is non-blocking. When it returns, data might still not have been written to the serial port. See `drain()`. + + Some devices, like the Arduino, reset when you open a connection to them. In such cases, immediately writing to the device will cause lost data as they wont be ready to receive the data. This is often worked around by having the Arduino send a "ready" byte that your Node program waits for before writing. You can also often get away with waiting around 400ms. + + If a port is disconnected during a write, the write will error in addition to the `close` event. + + From the [stream docs](https://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback) write errors don't always provide the error in the callback, sometimes they use the error event. + > If an error occurs, the callback may or may not be called with the error as its first argument. To reliably detect write errors, add a listener for the 'error' event. + + In addition to the usual `stream.write` arguments (`String` and `Buffer`), `write()` can accept arrays of bytes (positive numbers under 256) which is passed to `Buffer.from([])` for conversion. This extra functionality is pretty sweet. + + * @param {(string|array|buffer)} data Accepts a [`Buffer`](http://nodejs.org/api/buffer.html) object, or a type that is accepted by the `Buffer.from` method (e.g. an array of bytes or a string). + * @param {string=} encoding The encoding, if chunk is a string. Defaults to `'utf8'`. Also accepts `'ascii'`, `'base64'`, `'binary'`, and `'hex'` See [Buffers and Character Encodings](https://nodejs.org/api/buffer.html#buffer_buffers_and_character_encodings) for all available options. + */ + async write(data: string | Array | Buffer, encoding?: BufferEncoding): Promise { + return new Promise((resolve, reject) => { + let needsDrain = false; + const finish = (err: Error | null, isDrain?: boolean) => { + if (err) reject(err); + else if (!needsDrain || isDrain) resolve(); + }; + if (encoding) { + needsDrain = !this.port.write(data, encoding, (err) => { + if (err) reject(err); + else finish(null, false); + }); + } else { + needsDrain = !this.port.write(data, (err) => { + if (err) reject(err); + else finish(null, false); + }); + } + if (needsDrain) { + this.port.once('drain', () => finish(null, true)); + } + }); + } + + async drain(): Promise { + return new Promise((resolve, reject) => { + this.port.drain((err) => { + if (err) reject(err); + else resolve(); + }); + }); + } + + async flush(): Promise { + return new Promise((resolve, reject) => { + this.port.flush((err) => { + if (err) reject(err); + else resolve(); + }); + }); + } + + /** + * Changes the baud rate for an open port. Emits an error or calls the callback if the baud rate isn't supported. + * @param {object=} options Only supports `baudRate`. + * @param {number=} [options.baudRate] The baud rate of the port to be opened. This should match one of the commonly available baud rates, such as 110, 300, 1200, 2400, 4800, 9600, 14400, 19200, 38400, 57600, or 115200. Custom rates are supported best effort per platform. The device connected to the serial port is not guaranteed to support the requested baud rate, even if the port itself supports that baud rate. + * @returns {undefined} + */ + async update(options: { baudRate: number }): Promise { + return new Promise((resolve, reject) => { + this.port.update(options, (err) => { + if (err) reject(err); + else resolve(); + }); + }); + } + + /** + * Set control flags on an open port. Uses [`SetCommMask`](https://msdn.microsoft.com/en-us/library/windows/desktop/aa363257(v=vs.85).aspx) + * for Windows and [`ioctl`](http://linux.die.net/man/4/tty_ioctl) for OS X and Linux. + * + * All options are operating system default when the port is opened. Every flag is set on each call to the provided or default values. If options isn't provided default options is used. + * @param {object=} options + * @param {boolean=} [options.brk=false] sets the brk flag + * @param {boolean=} [options.cts=false] sets the cts flag + * @param {boolean=} [options.dsr=false] sets the dsr flag + * @param {boolean=} [options.dtr=true] sets the dtr flag + * @param {boolean=} [options.rts=true] sets the rts flag + */ + async set(options: SetOptions): Promise { + return new Promise((resolve, reject) => { + this.port.set(options, (err) => { + if (err) reject(err); + else resolve(); + }); + }); + } + + + /** + * Returns the control flags (CTS, DSR, DCD) on the open port. + * Uses [`GetCommModemStatus`](https://msdn.microsoft.com/en-us/library/windows/desktop/aa363258(v=vs.85).aspx) + * for Windows and [`ioctl`](http://linux.die.net/man/4/tty_ioctl) for mac and linux. + * @returns {object} + */ + async get(): Promise<{ cts: boolean; dsr: boolean; dcd: boolean }> { + return new Promise((resolve, reject) => { + this.port.get((err, options) => { + if (err) reject(err); + else if (!options) reject(new Error('No options returned')); + else resolve(options); + }); + }); + } + + /** + * The readable.pause() method will cause a stream in flowing mode to stop emitting 'data' events, switching out of flowing mode. + * Any data that becomes available will remain in the internal buffer. + * @returns {SerialPort} returns `this` + */ + pause(): SerialPortPromise { + this.port.pause(); + return this; + } + + /** + * The readable.resume() method causes an explicitly paused, Readable stream to resume emitting 'data' events, + * switching the stream into flowing mode. + * @returns {SerialPort} returns `this` + */ + resume(): SerialPortPromise { + this.port.resume(); + return this; + } + +} + diff --git a/src/web-serialport.ts b/src/serialport/web-serialport.ts similarity index 100% rename from src/web-serialport.ts rename to src/serialport/web-serialport.ts diff --git a/src/util/serial-helpers.ts b/src/util/serial-helpers.ts index 0481007..e41aec2 100644 --- a/src/util/serial-helpers.ts +++ b/src/util/serial-helpers.ts @@ -1,6 +1,7 @@ import { SerialPort } from 'serialport/dist/index.d'; +import { SerialPortPromise } from '../serialport/serialport-promise'; -export const waitForOpen = (serial: SerialPort, timeout: number = 1000): Promise => { +export const waitForOpen = (serial: SerialPort | SerialPortPromise, timeout: number = 1000): Promise => { let id = ''; // id = Math.random().toString(36).substring(7); // console.log('waitForOpen', id); @@ -8,31 +9,46 @@ export const waitForOpen = (serial: SerialPort, timeout: number = 1000): Promise if (serial.isOpen) { return resolve(true); } - let cleanup = () => {}; - const timer = setTimeout(() => { - cleanup(); - reject(new Error(`Timeout opening port ${id} (${timeout}ms)`)); - }, timeout); - const handleOpen = () => { + let resolved = false; + let timer: NodeJS.Timeout; + let handleOpen: () => void; + const cleanup = () => { + serial.removeListener('open', handleOpen); + clearTimeout(timer); + } + handleOpen = () => { + if (resolved) return; cleanup(); + resolved = true; resolve(true); }; + timer = setTimeout(() => { + if (resolved) return; + cleanup(); + resolved = true; + if (serial.isOpen) resolve(true); + else reject(new Error(`Timeout opening port ${id} (${timeout}ms)`)); + }, timeout); serial.on('open', handleOpen); - cleanup = () => { - serial.removeListener('open', handleOpen); - clearTimeout(timer); - } }); }; -export const setBaud = (serial: SerialPort, baud: number): Promise => new Promise((resolve, reject) => { +export const setBaud = (serial: SerialPort | SerialPortPromise, baud: number): Promise => new Promise((resolve, reject) => { + if (serial instanceof SerialPortPromise) { + serial.update({ baudRate: baud }).then(resolve).catch(reject); + return; + } serial.update({ baudRate: baud }, (err) => { if (err) reject(err); else resolve(); }); }); -export const setDTRRTS = (serial: SerialPort, flag: boolean): Promise => new Promise((resolve, reject) => { +export const setDTRRTS = (serial: SerialPort | SerialPortPromise, flag: boolean): Promise => new Promise((resolve, reject) => { + if (serial instanceof SerialPortPromise) { + serial.set({ dtr: flag, rts: flag }).then(resolve).catch(reject); + return; + } serial.set({ dtr: flag, rts: flag }, (err) => { if (err) reject(err); else resolve(); diff --git a/test/boards.ts b/test/boards.ts index cd27984..b76b190 100644 --- a/test/boards.ts +++ b/test/boards.ts @@ -43,15 +43,15 @@ Object.keys(config.devices).forEach((deviceRef) => { // dynamically find the device path by using VID & PID or esp props portList = await listPromise; const port = portList.find(p => { - if (device.vendorIds && device.productIds) { - return device.vendorIds.includes(p.vendorId || '') && device.productIds.includes(p.productId || ''); - } if (device.espChip) { return p.esp?.chip === device.espChip; } if (device.mac) { return p.esp?.mac === device.mac; } + if (device.vendorIds && device.productIds) { + return device.vendorIds.includes(p.vendorId || '') && device.productIds.includes(p.productId || ''); + } return false; }); if (!port) throw new Error(`could not locate ${device.name}`); @@ -69,28 +69,34 @@ Object.keys(config.devices).forEach((deviceRef) => { it(`should upload to ${device.name}`, async function() { this.retries(config.retries || 1); - await upload(serial, { - bin, - files, - flashMode, - flashFreq, - speed: device.speed, - uploadSpeed: device.uploadSpeed, - tool: device.tool, - cpu: device.cpu, - verbose: config.verbose, - avr109Reconnect: async () => { - const port = await waitForDevice(device); - if (!port) throw new Error(`could not locate ${device.name}`); - return new SerialPort({ path: port.path, baudRate: 1200 }); - } - }); + try { + await upload(serial, { + bin, + files, + flashMode, + flashFreq, + speed: device.speed, + uploadSpeed: device.uploadSpeed, + tool: device.tool, + cpu: device.cpu, + verbose: config.verbose, + avr109Reconnect: async () => { + const port = await waitForDevice(device); + if (!port) throw new Error(`could not locate ${device.name}`); + return new SerialPort({ path: port.path, baudRate: 1200 }); + } + }); + } catch (err) { + console.error(err); + throw err; + } console.log(`uploaded to ${device.name}, validating...`); + const promise = waitForData(serial, key, 10000); if (device.code === 'ping') { await serial.write('ping\n'); } - expect(await waitForData(serial, key, 5000)).to.be.true; + expect(await promise).to.be.true; }); }); }); diff --git a/test/test-config.yml b/test/test-config.yml index a1aa4b8..27d3a48 100644 --- a/test/test-config.yml +++ b/test/test-config.yml @@ -45,7 +45,14 @@ devices: esp32: # DOIT ESP32 DEVKIT V1 is the closest match to what I have name: DOIT ESP32 DEVKIT V1 - espChip: ESP32-D0WD-V3 (revision 3) + # espChip: ESP32-D0WD-V3 (revision 3) + espChip: ESP32-D0WDQ6 (revision 1) + vendorIds: + - '1a86' + - '10c4' + productIds: + - '7523' + - 'ea60' code: ping fqbn: esp32:esp32:esp32doit-devkit-v1 cpu: esp32 diff --git a/test/util.ts b/test/util.ts index 53479d8..448539b 100644 --- a/test/util.ts +++ b/test/util.ts @@ -101,6 +101,10 @@ export interface ESPIdentifyResult extends PortInfo { } } +const espVendorIds = ['1a86', '10c4']; +const espProductIds = ['7523', 'ea60']; +const isEsp = (port: PortInfo) => espVendorIds.includes(port.vendorId || '') && espProductIds.includes(port.productId || ''); + const pollDevices = async ( espCount: number, existingList = [] as PortInfo[], @@ -111,7 +115,7 @@ const pollDevices = async ( if (!acc.find(a => a.path === p.path)) acc.push(p); return acc; }, existingList); - const numEsps = newList.filter(p => p.vendorId === '1a86' && p.productId === '7523').length; + const numEsps = newList.filter(p => isEsp(p)).length; if (numEsps >= espCount) return newList; if (count > 20) throw new Error('Could not detect enough ESPs'); await asyncTimeout(250 + (Math.random() * 500)); @@ -141,7 +145,7 @@ export const espIdentify = async (espCount: number): Promise { await promise; - if (port.vendorId === '1a86' && port.productId === '7523') { + if (isEsp(port)) { results.push(await espIdentifyDevice(port)); } else { results.push({ ...port }); diff --git a/tsconfig.json b/tsconfig.json index 1fac056..242903d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "target": "esnext", - "module": "es6", + "module": "commonjs", "moduleResolution": "node", "lib": ["es2017", "es7", "es6", "dom", "dom.iterable"], "declaration": true, @@ -18,7 +18,7 @@ "files": [ "src/index.ts", "src/global.d.ts", - "src/web-serialport.ts" + "src/serialport/web-serialport.ts" ], "include": ["src"], "exclude": ["node_modules", "dist", "examples"] From 140df3efd70b810c9e951f0fde9ef7b4777cc3d6 Mon Sep 17 00:00:00 2001 From: mrfrase3 Date: Sun, 10 Mar 2024 14:26:52 +0800 Subject: [PATCH 5/5] final fixes and initial release --- .github/workflows/publish.yml | 20 ++++++ LICENSE | 7 ++ README.md | 102 +++++++++++++++++++++++++++ examples/index.html | 3 +- package.json | 2 +- rollup.config.js | 25 ------- src/avr/avr109/avr109.ts | 10 +-- src/avr/stk500-v1/stk500-v1.ts | 6 +- src/avr/stk500-v2/stk500-v2.ts | 4 +- src/esp/loader.ts | 5 +- src/global.d.ts | 4 -- src/index.ts | 19 +++-- src/serialport/serialport-promise.ts | 32 +++++---- src/serialport/web-serialport.ts | 63 ++++++++++++----- src/util/serial-helpers.ts | 17 +++-- tsconfig.json | 7 +- typings/index.d.ts | 14 ++++ 17 files changed, 253 insertions(+), 87 deletions(-) create mode 100644 .github/workflows/publish.yml create mode 100644 LICENSE delete mode 100644 src/global.d.ts create mode 100644 typings/index.d.ts diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..a8518c9 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,20 @@ +name: Publish Package to npmjs +on: + release: + types: [published] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + # Setup .npmrc file to publish to npm + - uses: actions/setup-node@v4 + with: + node-version: '20.x' + registry-url: 'https://registry.npmjs.org' + scope: '@duinoapp' + - run: yarn + - run: yarn build + - run: yarn publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e564b06 --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright 2024 Fraser Bullock + +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. \ No newline at end of file diff --git a/README.md b/README.md index 408362f..70e9284 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,108 @@ This project aims to achieve the following: - Support ESP devices - Platform for easy addition of new protocols +## Usage + +install your favourite way +```bash +npm install @duinoapp/upload-multitool +yarn add @duinoapp/upload-multitool +pnpm add @duinoapp/upload-multitool +``` + +This package exports a few utilities, the main one is upload + +```js +import { upload } from '@duinoapp/upload-multitool'; +import type { ProgramConfig } from '@duinoapp/upload-multitool'; +import { SerialPort } from 'serialport'; + +const serialport = new SerialPort({ path: '/dev/example', baudRate: 115200 }); + +const config = { + // for avr boards, the compiled hex + bin: compiled.hex, + // for esp boards, the compiled files and flash settings + files: compiled.files, + flashFreq: compiled.flashFreq, + flashMode: compiled.flashMode, + // baud rate to connect to bootloader + speed: 115200, + // baud rate to use for upload (ESP) + uploadSpeed: 115200, + // the tool to use, avrdude or esptool + tool: 'avr', + // the CPU of the device + cpu: 'atmega328p', + // a standard out interface ({ write(msg: string): void }) + stdout: process.stdout, + // whether or not to log to stdout verbosely + verbose: true, + // handle reconnecting to AVR109 devices when connecting to the bootloader + // the device ID changes for the bootloader, meaning in some OS's a new connection is required + // avr109Reconnect?: (opts: ReconnectParams) => Promise; +} as ProgramConfig; + +const res = await upload(serial.port, config); + +``` + +If you want to programmatically check if a tool/cpu is supported: + +```js +import { isSupported } from '@duinoapp/upload-multitool'; + +console.log(isSupported('avr', 'atmega328p')); // true +``` + +Also exports some helpful utilities: + +```js +import { WebSerialPort, SerialPortPromise, WebSerialPortPromise } from '@duinoapp/upload-multitool'; + +// WebSerialPort is a drop-in web replacement for serialport, with some useful static methods: + +// Check whether the current browser supports the Web Serial API +// https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility +WebSerialPort.isSupported() // true/false + +// request a serial connection from the user, +// first param takes requestPort options: https://developer.mozilla.org/en-US/docs/Web/API/Serial/requestPort#parameters +// second params takes the default open options +const serialport = WebSerialPort.requestPort({}, { baudRate: 115200 }); +serialport.open((err) => { + if (!err) serialport.write('hello', (err2) => ...) +}); + +// get a list of the serial connections that have already been requested: +const list = WebSerialPort.list(); + +// A wrapper util around SerialPort that exposes the same methods but with promises +const serial = new SerialPortPromise(await WebSerialPort.requestPort()); +await serial.open(); +await serial.write('hello'); + +// A Merged class of both WebSerialPort and SerialPortPromise, probably use this one +const serial = WebSerialPortPromise.requestPort(); +await serial.open(); +await serial.write('hello'); +``` + +### Upload return +The upload function will return an object: +```ts +{ + // the time it took to complete the upload + time: number + // the final serial port used. In most cases the serial port passes in + // if you pass in a non promise port, internally it will wrap with SerialPortPromise + // if you pass in a promise port, it is likely the same object, you can check with the serialport.key value on SerialPortPromise + // if using AVR109 and a reconnect is needed, this will likely be a new connection. + serialport: SerialPortPromise | WebSerialPortPromise +} +``` + + ## Get in touch You can contact me in the #multitool-general channel of the duinoapp discord diff --git a/examples/index.html b/examples/index.html index 8616d80..d4333d0 100644 --- a/examples/index.html +++ b/examples/index.html @@ -5,7 +5,6 @@ - @@ -32,7 +31,7 @@

Upload Multitool