From 535d6e02bb4b3773588cc21bd8bf6d467ab508b1 Mon Sep 17 00:00:00 2001 From: Sebastian Barrenechea Date: Mon, 7 Oct 2024 21:39:56 -0300 Subject: [PATCH] refactor: remove intel-hex dependency and update hex file reading method --- README.md | 13 ++- examples/avr4809.ts | 10 +- examples/diecimila-duemilanove168.ts | 10 +- examples/duemilanove328.ts | 11 +- examples/lgt8f328.ts | 10 +- examples/nano.ts | 10 +- examples/uno.ts | 10 +- package-lock.json | 12 +-- package.json | 3 +- src/index.ts | 29 +++++- src/lib/intelHexParser.ts | 146 +++++++++++++++++++++++++++ 11 files changed, 198 insertions(+), 66 deletions(-) create mode 100644 src/lib/intelHexParser.ts diff --git a/README.md b/README.md index 560cd4e..9321f5a 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ A modern, ESM-compatible, TypeScript implementation of the STK500v1 protocol for - Can be used in Node.js or browser environments - No dependency on avrdude or the Arduino IDE - TypeScript support for improved developer experience +- **Built-in Intel HEX parsing** (no need for external parsing libraries) ## Installation @@ -22,9 +23,8 @@ Here's a basic example of how to use stk500-esm to program an Arduino: ```typescript import { SerialPort } from "serialport"; -import intel_hex from "intel-hex"; -import Stk500 from "stk500-esm"; import fs from "fs/promises"; +import Stk500 from "stk500-esm"; const stk = new Stk500(); @@ -42,9 +42,8 @@ async function upload(path: string) { const hexData = await fs.readFile("path/to/your/sketch.hex", { encoding: "utf8", }); - const hex = intel_hex.parse(hexData).data; serialPort = new SerialPort({ path, baudRate: board.baud }); - await stk.bootload(serialPort, hex, board, false); + await stk.bootload(serialPort, hexData, board, false); console.log("Programming successful!"); } catch (error) { console.error("Programming failed:", error); @@ -83,11 +82,11 @@ Replace `uno.ts` with the appropriate example file and `/dev/ttyACM0` with your The main class `Stk500` provides the following methods: -- `bootload(stream: NodeJS.ReadWriteStream, hex: Buffer, opt: Board, use_8_bit_addresses = false): Promise` +- `bootload(stream: NodeJS.ReadWriteStream, hexData: string | Buffer, opt: Board, use_8_bit_addresses = false): Promise` - `sync(stream: NodeJS.ReadWriteStream, attempts: number, timeout: number): Promise` - `verifySignature(stream: NodeJS.ReadWriteStream, signature: Buffer, timeout: number): Promise` -- `upload(stream: NodeJS.ReadWriteStream, hex: Buffer, pageSize: number, timeout: number, use_8_bit_addresses = false): Promise` -- `verify(stream: NodeJS.ReadWriteStream, hex: Buffer, pageSize: number, timeout: number, use_8_bit_addresses = false): Promise` +- `upload(stream: NodeJS.ReadWriteStream, hexData: string | Buffer, pageSize: number, timeout: number, use_8_bit_addresses = false): Promise` +- `verify(stream: NodeJS.ReadWriteStream, hexData: string | Buffer, pageSize: number, timeout: number, use_8_bit_addresses = false): Promise` For more detailed API information, please refer to the TypeScript definitions or the source code. diff --git a/examples/avr4809.ts b/examples/avr4809.ts index 5f8a8e7..d24d2c0 100644 --- a/examples/avr4809.ts +++ b/examples/avr4809.ts @@ -1,6 +1,5 @@ import fs from "fs/promises"; import { SerialPort } from "serialport"; -import intel_hex from "intel-hex"; import Stk500 from "../src/index.js"; const stk = new Stk500(); @@ -13,11 +12,6 @@ const board = { timeout: 400, }; -async function readHexFile(filePath) { - const data = await fs.readFile(filePath, { encoding: "utf8" }); - return intel_hex.parse(data).data; -} - function createSerialPort(path, baudRate) { return new Promise((resolve, reject) => { const serialPort = new SerialPort({ path, baudRate }); @@ -38,7 +32,9 @@ async function closeSerialPort(serialPort) { async function upload(path) { let serialPort; try { - const hex = await readHexFile("arduino-1.0.6/168/avr4809.cpp.hex"); + const hex = await fs.readFile("arduino-1.0.6/168/avr4809.cpp.hex", { + encoding: "utf8", + }); serialPort = await createSerialPort(path, board.baudRate); await stk.bootload(serialPort, hex, board, true); console.log("Programming SUCCESS!"); diff --git a/examples/diecimila-duemilanove168.ts b/examples/diecimila-duemilanove168.ts index eb9f6b1..d270767 100644 --- a/examples/diecimila-duemilanove168.ts +++ b/examples/diecimila-duemilanove168.ts @@ -1,6 +1,5 @@ import fs from "fs/promises"; import { SerialPort } from "serialport"; -import intel_hex from "intel-hex"; import Stk500 from "../src/index.js"; const stk = new Stk500(); @@ -13,11 +12,6 @@ const board = { timeout: 400, }; -async function readHexFile(filePath) { - const data = await fs.readFile(filePath, { encoding: "utf8" }); - return intel_hex.parse(data).data; -} - function createSerialPort(path, baudRate) { return new Promise((resolve, reject) => { const serialPort = new SerialPort({ path, baudRate }); @@ -38,7 +32,9 @@ async function closeSerialPort(serialPort) { async function upload(path) { let serialPort; try { - const hex = await readHexFile("arduino-1.0.6/168/StandardFirmata.cpp.hex"); + const hex = await fs.readFile("arduino-1.0.6/168/StandardFirmata.cpp.hex", { + encoding: "utf8", + }); serialPort = await createSerialPort(path, board.baudRate); await stk.bootload(serialPort, hex, board, false); console.log("Programming SUCCESS!"); diff --git a/examples/duemilanove328.ts b/examples/duemilanove328.ts index 5d55382..6263a98 100644 --- a/examples/duemilanove328.ts +++ b/examples/duemilanove328.ts @@ -1,6 +1,5 @@ import fs from "fs/promises"; import { SerialPort } from "serialport"; -import intel_hex from "intel-hex"; import Stk500 from "../src/index.js"; const stk = new Stk500(); @@ -13,11 +12,6 @@ const board = { timeout: 400, }; -async function readHexFile(filePath) { - const data = await fs.readFile(filePath, { encoding: "utf8" }); - return intel_hex.parse(data).data; -} - function createSerialPort(path, baudRate) { return new Promise((resolve, reject) => { const serialPort = new SerialPort({ path, baudRate }); @@ -38,7 +32,10 @@ async function closeSerialPort(serialPort) { async function upload(path) { let serialPort; try { - const hex = await readHexFile("arduino-1.0.6/duemilanove328/StandardFirmata.cpp.hex"); + const hex = await fs.readFile( + "arduino-1.0.6/duemilanove328/StandardFirmata.cpp.hex", + { encoding: "utf8" } + ); serialPort = await createSerialPort(path, board.baudRate); await stk.bootload(serialPort, hex, board, false); console.log("Programming SUCCESS!"); diff --git a/examples/lgt8f328.ts b/examples/lgt8f328.ts index e84de2a..e994b9d 100644 --- a/examples/lgt8f328.ts +++ b/examples/lgt8f328.ts @@ -1,6 +1,5 @@ import fs from "fs/promises"; import { SerialPort } from "serialport"; -import intel_hex from "intel-hex"; import Stk500 from "../src/index.js"; const stk = new Stk500(); @@ -13,11 +12,6 @@ const board = { timeout: 400, }; -async function readHexFile(filePath) { - const data = await fs.readFile(filePath, { encoding: "utf8" }); - return intel_hex.parse(data).data; -} - function createSerialPort(path, baudRate) { return new Promise((resolve, reject) => { const serialPort = new SerialPort({ path, baudRate }); @@ -38,7 +32,9 @@ async function closeSerialPort(serialPort) { async function upload(path) { let serialPort; try { - const hex = await readHexFile("arduino-2.3.3/lgt8f328/Blink.ino.hex"); + const hex = await fs.readFile("arduino-2.3.3/lgt8f328/Blink.ino.hex", { + encoding: "utf8", + }); serialPort = await createSerialPort(path, board.baudRate); await stk.bootload(serialPort, hex, board, false); console.log("Programming SUCCESS!"); diff --git a/examples/nano.ts b/examples/nano.ts index 486db5b..63166a4 100644 --- a/examples/nano.ts +++ b/examples/nano.ts @@ -1,6 +1,5 @@ import fs from "fs/promises"; import { SerialPort } from "serialport"; -import intel_hex from "intel-hex"; import Stk500 from "../src/index.js"; const stk = new Stk500(); @@ -13,11 +12,6 @@ const board = { timeout: 400, }; -async function readHexFile(filePath) { - const data = await fs.readFile(filePath, { encoding: "utf8" }); - return intel_hex.parse(data).data; -} - function createSerialPort(path, baudRate) { return new Promise((resolve, reject) => { const serialPort = new SerialPort({ path, baudRate }); @@ -38,7 +32,9 @@ async function closeSerialPort(serialPort) { async function upload(path) { let serialPort; try { - const hex = await readHexFile("arduino-2.3.3/nano/Blink.ino.hex"); + const hex = await fs.readFile("arduino-2.3.3/nano/Blink.ino.hex", { + encoding: "utf8", + }); serialPort = await createSerialPort(path, board.baudRate); await stk.bootload(serialPort, hex, board, false); console.log("Programming SUCCESS!"); diff --git a/examples/uno.ts b/examples/uno.ts index 6b5db81..4205d18 100644 --- a/examples/uno.ts +++ b/examples/uno.ts @@ -1,6 +1,5 @@ import fs from "fs/promises"; import { SerialPort } from "serialport"; -import intel_hex from "intel-hex"; import Stk500 from "../src/index.js"; const stk = new Stk500(); @@ -13,11 +12,6 @@ const board = { timeout: 400, }; -async function readHexFile(filePath) { - const data = await fs.readFile(filePath, { encoding: "utf8" }); - return intel_hex.parse(data).data; -} - function createSerialPort(path, baudRate) { return new Promise((resolve, reject) => { const serialPort = new SerialPort({ path, baudRate }); @@ -38,7 +32,9 @@ async function closeSerialPort(serialPort) { async function upload(path) { let serialPort; try { - const hex = await readHexFile("arduino-1.0.6/uno/StandardFirmata.cpp.hex"); + const hex = await fs.readFile("arduino-1.0.6/uno/StandardFirmata.cpp.hex", { + encoding: "utf8", + }); serialPort = await createSerialPort(path, board.baudRate); await stk.bootload(serialPort, hex, board, false); console.log("Programming SUCCESS!"); diff --git a/package-lock.json b/package-lock.json index 9c802f3..8429bc1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,18 @@ { "name": "stk500-esm", - "version": "1.0.1", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "stk500-esm", - "version": "1.0.1", + "version": "1.1.0", "license": "MIT", "devDependencies": { "@eslint/js": "^9.12.0", "@types/node": "^22.7.5", "eslint": "^9.12.0", "globals": "^15.10.0", - "intel-hex": "^0.2.0", "serialport": "^12.0.0", "tsx": "^4.19.1", "typescript": "^5.6.2", @@ -1805,13 +1804,6 @@ "node": ">=0.8.19" } }, - "node_modules/intel-hex": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/intel-hex/-/intel-hex-0.2.0.tgz", - "integrity": "sha512-PJNkAm4N32VjcP7D14q1M+y/en/VlhqFyeaGiEhW/L9gPa7Lv+7vAcVlVBamKMfk6cZCcA9dTvHVXahcOxKzlw==", - "dev": true, - "license": "MIT" - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", diff --git a/package.json b/package.json index f76f354..3390d3f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stk500-esm", - "version": "1.0.1", + "version": "1.1.0", "description": "A modern, ESM-compatible, TypeScript implementation of the STK500v1 protocol for programming Arduino boards directly from Node.js or the browser.", "main": "dist/index.js", "module": "dist/index.js", @@ -30,7 +30,6 @@ "@types/node": "^22.7.5", "eslint": "^9.12.0", "globals": "^15.10.0", - "intel-hex": "^0.2.0", "serialport": "^12.0.0", "tsx": "^4.19.1", "typescript": "^5.6.2", diff --git a/src/index.ts b/src/index.ts index 5a114a7..f6eb198 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ import Constants from "./lib/constants.js"; import sendCommand from "./lib/sendCommand.js"; +import { parseIntelHex } from "./lib/intelHexParser.js"; interface Board { name: string; @@ -200,13 +201,16 @@ class STK500 { async upload( stream: NodeJS.ReadWriteStream, - hex: Buffer, + hexData: string | Buffer, pageSize: number, timeout: number, use8BitAddresses = false ): Promise { this.log("program"); + // Parse the Intel HEX data + const { data: hex } = parseIntelHex(hexData); + let pageaddr = 0; let writeBytes; let useaddr; @@ -258,13 +262,16 @@ class STK500 { async verify( stream: NodeJS.ReadWriteStream, - hex: Buffer, + hexData: string | Buffer, pageSize: number, timeout: number, use8BitAddresses = false ): Promise { this.log("verify"); + // Parse the Intel HEX data + const { data: hex } = parseIntelHex(hexData); + let pageaddr = 0; let writeBytes; let useaddr; @@ -331,7 +338,7 @@ class STK500 { async bootload( stream: NodeJS.ReadWriteStream, - hex: Buffer, + hexData: string | Buffer, opt: Board, use8BitAddresses = false ): Promise { @@ -347,8 +354,20 @@ class STK500 { await this.verifySignature(stream, opt.signature, opt.timeout); await this.setOptions(stream, parameters, opt.timeout); await this.enterProgrammingMode(stream, opt.timeout); - await this.upload(stream, hex, opt.pageSize, opt.timeout, use8BitAddresses); - await this.verify(stream, hex, opt.pageSize, opt.timeout, use8BitAddresses); + await this.upload( + stream, + hexData, + opt.pageSize, + opt.timeout, + use8BitAddresses + ); + await this.verify( + stream, + hexData, + opt.pageSize, + opt.timeout, + use8BitAddresses + ); await this.exitProgrammingMode(stream, opt.timeout); } } diff --git a/src/lib/intelHexParser.ts b/src/lib/intelHexParser.ts new file mode 100644 index 0000000..0bd6feb --- /dev/null +++ b/src/lib/intelHexParser.ts @@ -0,0 +1,146 @@ +// Ported from https://github.com/bminer/intel-hex.js + +const DATA = 0; +const END_OF_FILE = 1; +const EXT_SEGMENT_ADDR = 2; +const START_SEGMENT_ADDR = 3; +const EXT_LINEAR_ADDR = 4; +const START_LINEAR_ADDR = 5; + +const EMPTY_VALUE = 0xff; + +interface ParseResult { + data: Buffer; + startSegmentAddress: number | null; + startLinearAddress: number | null; +} + +export function parseIntelHex( + data: string | Buffer, + bufferSize?: number, + addressOffset?: number +): ParseResult { + if (data instanceof Buffer) { + data = data.toString("ascii"); + } + + let buf = Buffer.alloc(bufferSize ?? 8192); + let bufLength = 0; + let highAddress = 0; + let startSegmentAddress: number | null = null; + let startLinearAddress: number | null = null; + const offset = addressOffset ?? 0; + let lineNum = 0; + let pos = 0; + + const SMALLEST_LINE = 11; + while (pos + SMALLEST_LINE <= data.length) { + if (data[pos++] !== ":") { + throw new Error(`Line ${lineNum + 1} does not start with a colon (:).`); + } + lineNum++; + + const dataLength = parseInt(data.slice(pos, pos + 2), 16); + pos += 2; + const lowAddress = parseInt(data.slice(pos, pos + 4), 16); + pos += 4; + const recordType = parseInt(data.slice(pos, pos + 2), 16); + pos += 2; + const dataField = data.slice(pos, pos + dataLength * 2); + const dataFieldBuf = Buffer.from(dataField, "hex"); + pos += dataLength * 2; + const checksum = parseInt(data.slice(pos, pos + 2), 16); + pos += 2; + + let calcChecksum = + (dataLength + (lowAddress >> 8) + lowAddress + recordType) & 0xff; + for (let i = 0; i < dataLength; i++) { + calcChecksum = (calcChecksum + dataFieldBuf[i]) & 0xff; + } + calcChecksum = (0x100 - calcChecksum) & 0xff; + + if (checksum !== calcChecksum) { + throw new Error( + `Invalid checksum on line ${lineNum}: got ${checksum}, but expected ${calcChecksum}` + ); + } + + switch (recordType) { + case DATA: { + const absoluteAddress = highAddress + lowAddress - offset; + if (absoluteAddress + dataLength >= buf.length) { + const tmp = Buffer.alloc((absoluteAddress + dataLength) * 2); + buf.copy(tmp, 0, 0, bufLength); + tmp.fill(EMPTY_VALUE, bufLength, absoluteAddress); + dataFieldBuf.copy(tmp, absoluteAddress); + bufLength = Math.max(bufLength, absoluteAddress + dataLength); + buf = tmp; + } else { + if (absoluteAddress > bufLength) { + buf.fill(EMPTY_VALUE, bufLength, absoluteAddress); + } + dataFieldBuf.copy(buf, absoluteAddress); + bufLength = Math.max(bufLength, absoluteAddress + dataLength); + } + if (bufLength >= (bufferSize || 8192)) { + return { + data: buf.subarray(0, bufLength), + startSegmentAddress, + startLinearAddress, + }; + } + break; + } + case END_OF_FILE: + if (dataLength !== 0) { + throw new Error(`Invalid EOF record on line ${lineNum}.`); + } + return { + data: buf.subarray(0, bufLength), + startSegmentAddress, + startLinearAddress, + }; + case EXT_SEGMENT_ADDR: + if (dataLength !== 2 || lowAddress !== 0) { + throw new Error( + `Invalid extended segment address record on line ${lineNum}.` + ); + } + highAddress = parseInt(dataField, 16) << 4; + break; + case START_SEGMENT_ADDR: + if (dataLength !== 4 || lowAddress !== 0) { + throw new Error( + `Invalid start segment address record on line ${lineNum}.` + ); + } + startSegmentAddress = parseInt(dataField, 16); + break; + case EXT_LINEAR_ADDR: + if (dataLength !== 2 || lowAddress !== 0) { + throw new Error( + `Invalid extended linear address record on line ${lineNum}.` + ); + } + highAddress = parseInt(dataField, 16) << 16; + break; + case START_LINEAR_ADDR: + if (dataLength !== 4 || lowAddress !== 0) { + throw new Error( + `Invalid start linear address record on line ${lineNum}.` + ); + } + startLinearAddress = parseInt(dataField, 16); + break; + default: + throw new Error( + `Invalid record type (${recordType}) on line ${lineNum}` + ); + } + + if (data[pos] === "\r") pos++; + if (data[pos] === "\n") pos++; + } + + throw new Error("Unexpected end of input: missing or invalid EOF record."); +}