From 207c735c7d5505f975f73df945db6f67c2df9135 Mon Sep 17 00:00:00 2001 From: sverben <59171289+sverben@users.noreply.github.com> Date: Sun, 26 Jan 2025 13:52:47 +0100 Subject: [PATCH 1/2] fix: revert to old uploaders and fix android uploads --- package.json | 5 +- .../core/popups/popups/Uploader.svelte | 3 +- src/lib/domain/robots.ts | 9 +- src/lib/programmers/AvrDude.ts | 96 ----- src/lib/programmers/STK500v1/STK500v1.ts | 275 +++++++++++++++ src/lib/programmers/STK500v1/constants.ts | 16 + src/lib/programmers/STK500v2/constants.ts | 93 +++++ src/lib/programmers/STK500v2/index.ts | 331 ++++++++++++++++++ src/lib/programmers/STK500v2/parser.ts | 323 +++++++++++++++++ src/lib/programmers/utils.ts | 67 ++++ src/main.ts | 4 +- vite.config.ts | 12 - yarn.lock | 35 +- 13 files changed, 1141 insertions(+), 128 deletions(-) delete mode 100644 src/lib/programmers/AvrDude.ts create mode 100644 src/lib/programmers/STK500v1/STK500v1.ts create mode 100644 src/lib/programmers/STK500v1/constants.ts create mode 100644 src/lib/programmers/STK500v2/constants.ts create mode 100644 src/lib/programmers/STK500v2/index.ts create mode 100644 src/lib/programmers/STK500v2/parser.ts diff --git a/package.json b/package.json index 586ca45..85c527e 100644 --- a/package.json +++ b/package.json @@ -34,20 +34,21 @@ "@floating-ui/dom": "^1.6.3", "@fortawesome/free-brands-svg-icons": "^6.6.0", "@fortawesome/free-solid-svg-icons": "^6.7.1", - "@leaphy-robotics/avrdude-webassembly": "1.7.1", "@leaphy-robotics/dfu-util-wasm": "^1.0.2", "@leaphy-robotics/leaphy-blocks": "3.3.3", "@leaphy-robotics/picotool-wasm": "1.0.3", - "@leaphy-robotics/webusb-ftdi": "1.0.1", + "@leaphy-robotics/webusb-ftdi": "1.0.2", "@sentry/svelte": "^8.34.0", "@types/w3c-web-usb": "^1.0.10", "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.5.0", "base64-js": "^1.5.1", "blockly": "10", + "buffer": "^6.0.3", "chart.js": "^4.4.2", "chartjs-adapter-date-fns": "^3.0.0", "date-fns": "^4.1.0", + "intel-hex": "^0.2.0", "jszip": "^3.10.1", "monaco-editor": "^0.52.0", "rrweb": "^2.0.0-alpha.4", diff --git a/src/lib/components/core/popups/popups/Uploader.svelte b/src/lib/components/core/popups/popups/Uploader.svelte index e8f2610..83e2224 100644 --- a/src/lib/components/core/popups/popups/Uploader.svelte +++ b/src/lib/components/core/popups/popups/Uploader.svelte @@ -71,7 +71,8 @@ async function upload(res: Record) { currentState = "UPDATE_STARTED"; await SerialState.reserve(); - } catch { + } catch (e) { + console.log(e); popupState.close(); return PopupsState.open({ component: ErrorPopup, diff --git a/src/lib/domain/robots.ts b/src/lib/domain/robots.ts index 6b61734..1526369 100644 --- a/src/lib/domain/robots.ts +++ b/src/lib/domain/robots.ts @@ -15,9 +15,10 @@ import nanoIcon from "$assets/robots/icons/l_nano.svg"; import originalIcon from "$assets/robots/icons/l_original.svg"; import unoIcon from "$assets/robots/icons/l_uno.svg"; import WorkspaceState, { Mode } from "$state/workspace.svelte"; -import AvrDude from "../programmers/AvrDude"; import DFU from "../programmers/DFU"; import Pico from "../programmers/Pico"; +import STK500v1 from "../programmers/STK500v1/STK500v1"; +import STK500v2 from "../programmers/STK500v2"; import { type Programmer, RobotType } from "./robots.types"; const DEFAULT_LIBRARIES = [ @@ -74,14 +75,14 @@ export enum PinMapping { const baseUno = { mapping: PinMapping.UNO, fqbn: "arduino:avr:uno", - programmer: new AvrDude("atmega328p"), + programmer: new STK500v1(), board: "l_uno", }; const baseNano = { mapping: PinMapping.NANO, fqbn: "arduino:avr:nano", - programmer: new AvrDude("atmega328p"), + programmer: new STK500v1(), board: "l_nano", }; @@ -227,7 +228,7 @@ const robotDevices: RobotDevice[] = [ type: RobotType.L_MEGA, mapping: PinMapping.MEGA, name: "Arduino Mega", - programmer: new AvrDude("atmega2560"), + programmer: new STK500v2(), fqbn: "arduino:avr:mega", libraries: DEFAULT_LIBRARIES.concat([ "QMC5883LCompass", diff --git a/src/lib/programmers/AvrDude.ts b/src/lib/programmers/AvrDude.ts deleted file mode 100644 index ce1e269..0000000 --- a/src/lib/programmers/AvrDude.ts +++ /dev/null @@ -1,96 +0,0 @@ -import type { Programmer } from "$domain/robots.types"; -import type { LeaphyPort } from "$state/serial.svelte"; -import WorkspaceState from "$state/workspace.svelte"; -import Module from "@leaphy-robotics/avrdude-webassembly/avrdude.js"; - -declare global { - interface Window { - avrdudeLog: string[]; - writeStream: WritableStreamDefaultWriter; - activePort: LeaphyPort; - funcs: any; - } -} - -const controllerArgs: Record = { - atmega328p: - "avrdude -P /dev/null -V -v -p atmega328p -c stk500v1 -C /tmp/avrdude.conf -b 115200 -D -U flash:w:/tmp/program.hex:i", - atmega2560: - "avrdude -P /dev/null -V -v -p atmega2560 -c stk500v2 -C /tmp/avrdude.conf -b 115200 -D -U flash:w:/tmp/program.hex:i", -}; - -export default class AvrDude implements Programmer { - private args: string; - - constructor(controller: string) { - this.args = controllerArgs[controller]; - } - - async upload( - port: LeaphyPort, - response: Record, - ): Promise { - const avrdude = await Module({ - locateFile: (path: string) => { - return `/${path}`; - }, - }); - window.funcs = avrdude; - - if (port.readable || port.writable) await port.close(); - await port.open({ baudRate: 115200 }); - window.activePort = port; - - const avrdudeConfig = await fetch("/avrdude.conf").then((res) => - res.text(), - ); - avrdude.FS.writeFile("/tmp/avrdude.conf", avrdudeConfig); - avrdude.FS.writeFile("/tmp/program.hex", response.hex); - - const disconnectPromise = new Promise((resolve) => { - if (navigator.serial && port instanceof SerialPort) - port.addEventListener("disconnect", resolve); - }); - const oldConsoleError = console.error; - const workerErrorPromise = new Promise((resolve) => { - console.error = (...data) => { - if (data[1].name === "ExitStatus") { - resolve({ type: "worker-error" }); - } else { - oldConsoleError(...data); - resolve({ type: "error" }); - } - }; - }); - const startAvrdude = avrdude.cwrap("startAvrdude", "number", ["string"]); - - let race = await Promise.race([ - disconnectPromise, - startAvrdude(this.args), - workerErrorPromise, - ]); - - console.error = oldConsoleError; - if (race.type) { - if (race.type === "worker-error") { - race = -3; - } else { - race = -2; - } - } - - if (window.writeStream) window.writeStream.releaseLock(); - - WorkspaceState.uploadLog = window.avrdudeLog; - if (race !== 0) { - if (race === -2) { - throw new Error("Port disconnected"); - } - if (race === -3) { - throw new Error("Worker error"); - } - throw new Error("Avrdude failed"); - } - await port.open({ baudRate: 115200 }); - } -} diff --git a/src/lib/programmers/STK500v1/STK500v1.ts b/src/lib/programmers/STK500v1/STK500v1.ts new file mode 100644 index 0000000..8f58bb8 --- /dev/null +++ b/src/lib/programmers/STK500v1/STK500v1.ts @@ -0,0 +1,275 @@ +import type { Buffer } from "buffer"; +import type { Programmer } from "$domain/robots.types"; +import type { LeaphyPort } from "$state/serial.svelte.js"; +import WorkspaceState from "$state/workspace.svelte.js"; +import { parse } from "intel-hex"; +import { + clearReadBuffer, + convertArrayToHex, + delay, + includesAll, +} from "../utils"; +import { REQUESTS, RESPONSES, SIGNATURE } from "./constants"; + +interface Options { + devicecode: number; + revision: number; + progtype: number; + parmode: number; + polling: number; + selftimed: number; + lockbytes: number; + fusebytes: number; + flashpollval1: number; + flashpollval2: number; + eeprompollval1: number; + eeprompollval2: number; + pagesizehigh: number; + pagesizelow: number; + eepromsizehigh: number; + eepromsizelow: number; + flashsize4: number; + flashsize3: number; + flashsize2: number; + flashsize1: number; +} + +export default class STK500v1 implements Programmer { + port: LeaphyPort; + readStream: ReadableStreamDefaultReader; + writeStream: WritableStreamDefaultWriter; + + async attemptBootloaders() { + let response: Uint8Array; + // start timer + try { + response = await this.attemptNewBootloader(); + WorkspaceState.uploadLog.push("Using new bootloader"); + } catch { + response = await this.attemptOldBootloader(); + if (response !== null) { + WorkspaceState.uploadLog.push("Using old bootloader"); + } + } + + if (response === null) { + throw new Error("Could not connect to Arduino"); + } + + return response; + } + + async upload(port: LeaphyPort, res: Record) { + this.port = port; + this.readStream = this.port.readable.getReader(); + this.writeStream = this.port.writable.getWriter(); + + const response = await this.attemptBootloaders(); + if (!includesAll([RESPONSES.IN_SYNC, RESPONSES.OK], response)) { + throw new Error("Arduino is not in sync"); + } + + // Try to match signature + const signature = await this.send([0x75]); + if (!includesAll(SIGNATURE, signature)) { + throw new Error("Arduino does not match signature"); + } + + const optionsResponse = await this.writeOptions({ + pagesizehigh: 0, + pagesizelow: 128, + }); + if (!includesAll([RESPONSES.OK], optionsResponse)) { + throw new Error("Arduino did not accept options"); + } + + // Start programming + await this.send([REQUESTS.ENTER_PROG_MODE]); + await this.writeProgram(res.hex); + await this.send([REQUESTS.LEAVE_PROG_MODE]); + + // Reset the Arduino + try { + await this.reset(115200); + } catch (error) {} + + this.readStream.releaseLock(); + this.writeStream.releaseLock(); + await this.port.close(); + await this.port.open({ baudRate: 115200 }); + } + + async attemptOldBootloader(): Promise { + let response = null; + try { + await this.reset(57600); + } catch (error) { + throw new Error("Could not connect to Arduino: Reset failed"); + } + for (let i = 0; i < 10; i++) { + try { + response = await this.send([REQUESTS.GET_SYNC], 500); + break; + } catch (error) { + WorkspaceState.uploadLog.push(error.toString()); + } + } + if (response === null) { + this.readStream.releaseLock(); + this.writeStream.releaseLock(); + this.readStream = null; + this.writeStream = null; + WorkspaceState.uploadLog.push( + "Could not connect to Arduino (old bootloader)", + ); + throw new Error("Could not connect to Arduino"); + } + return response; + } + + async attemptNewBootloader(): Promise { + let response = null; + try { + await this.reset(115200); + } catch (error) { + WorkspaceState.uploadLog.push( + "Could not connect to Arduino: Reset failed (new bootloader)", + ); + throw new Error("Could not connect to Arduino: Reset failed"); + } + for (let i = 0; i < 10; i++) { + try { + response = await this.send([REQUESTS.GET_SYNC], 500); + break; + } catch (error) { + WorkspaceState.uploadLog.push(error.toString()); + } + } + if (response === null) { + WorkspaceState.uploadLog.push( + "Could not connect to Arduino (new bootloader)", + ); + throw new Error("Could not connect to Arduino"); + } + return response; + } + + async writeProgram(program: string) { + const hex = parse(program); + + let data: Buffer = hex.data; + let i = 0; + do { + const offset = Math.min(data.length, 128); + const page = data.subarray(0, offset); + data = data.subarray(offset); + const length = page.length; + const lengthHigh = length >> 8; + const lengthLow = length & 0xff; + const startAddress = (hex.startSegmentAddress + i) >> 1; + + await this.send([ + REQUESTS.SET_ADDRESS, + startAddress & 0xff, + (startAddress >> 8) & 0xff, + ]); + const buffer = new Uint8Array([ + REQUESTS.SET_PAGE, + lengthHigh, + lengthLow, + 0x46, + ...page, + ]); + await this.send(buffer); + i += page.length; + } while (data.length > 0); + } + + async writeOptions(options: Partial) { + const buffer = new Uint8Array([ + 0x42, + options.devicecode || 0, + options.revision || 0, + options.progtype || 0, + options.parmode || 0, + options.polling || 0, + options.selftimed || 0, + options.lockbytes || 0, + options.fusebytes || 0, + options.flashpollval1 || 0, + options.flashpollval2 || 0, + options.eeprompollval1 || 0, + options.eeprompollval2 || 0, + options.pagesizehigh || 0, + options.pagesizelow || 0, + options.eepromsizehigh || 0, + options.eepromsizelow || 0, + options.flashsize4 || 0, + options.flashsize3 || 0, + options.flashsize2 || 0, + options.flashsize1 || 0, + ]); + + return await this.send(buffer); + } + + async send(command: number[] | Uint8Array, timeoutMs = 1000) { + const buffer = new Uint8Array([...command, REQUESTS.CRC_EOP]); + WorkspaceState.uploadLog.push( + `Sending: ${convertArrayToHex(buffer).join(" ")}`, + ); + await this.writeStream.write(buffer); + + const timeoutPromise = new Promise((resolve, _) => { + setTimeout(() => { + resolve("Timeout"); + }, timeoutMs); + }); + + let IN_SYNC = false; + let OK = false; + let returnBuffer = new Uint8Array(0); + while (true) { + const promise = this.receive(); + const result = await Promise.race([promise, timeoutPromise]); + + if (result instanceof Uint8Array) { + const answer = Array.from(result); + if (answer.includes(RESPONSES.NOT_IN_SYNC)) { + WorkspaceState.uploadLog.push("Arduino is not in sync"); + throw new Error("Arduino is not in sync"); + } + returnBuffer = new Uint8Array([...returnBuffer, ...answer]); + if (answer.includes(RESPONSES.IN_SYNC)) IN_SYNC = true; + if (answer.includes(RESPONSES.OK)) OK = true; + if (OK && IN_SYNC) return returnBuffer; + } else if (result === "Timeout") { + await this.readStream.cancel(); + this.readStream.releaseLock(); + this.readStream = this.port.readable.getReader(); + throw new Error("Timeout"); + } + } + } + + async receive() { + const ev = await this.readStream.read(); + WorkspaceState.uploadLog.push( + `Received: ${convertArrayToHex(ev.value).join(" ")}`, + ); + return ev.value; + } + + async reset(baudRate: number) { + this.writeStream.releaseLock(); + this.readStream.releaseLock(); + await this.port.close(); + await this.port.open({ baudRate: baudRate }); + this.readStream = this.port.readable.getReader(); + this.writeStream = this.port.writable.getWriter(); + await this.port.setSignals({ dataTerminalReady: false }); + await delay(250); + await this.port.setSignals({ dataTerminalReady: true }); + await clearReadBuffer(this.readStream); + } +} diff --git a/src/lib/programmers/STK500v1/constants.ts b/src/lib/programmers/STK500v1/constants.ts new file mode 100644 index 0000000..5434e08 --- /dev/null +++ b/src/lib/programmers/STK500v1/constants.ts @@ -0,0 +1,16 @@ +export const REQUESTS = { + GET_SYNC: 0x30, + CRC_EOP: 0x20, + ENTER_PROG_MODE: 0x50, + LEAVE_PROG_MODE: 0x51, + SET_PAGE: 0x64, + SET_ADDRESS: 0x55, +}; + +export const RESPONSES = { + OK: 0x10, + IN_SYNC: 0x14, + NOT_IN_SYNC: 0x15, +}; + +export const SIGNATURE = [0x1e, 0x95, 0x0f]; diff --git a/src/lib/programmers/STK500v2/constants.ts b/src/lib/programmers/STK500v2/constants.ts new file mode 100644 index 0000000..9dd950c --- /dev/null +++ b/src/lib/programmers/STK500v2/constants.ts @@ -0,0 +1,93 @@ +export default { + // STK message constants, + MESSAGE_START: 0x1b, + TOKEN: 0x0e, + + // STK general command constants + CMD_SIGN_ON: 0x01, + CMD_SET_PARAMETER: 0x02, + CMD_GET_PARAMETER: 0x03, + CMD_SET_DEVICE_PARAMETERS: 0x04, + CMD_OSCCAL: 0x05, + CMD_LOAD_ADDRESS: 0x06, + CMD_FIRMWARE_UPGRADE: 0x07, + + // STK ISP command constants + CMD_ENTER_PROGMODE_ISP: 0x10, + CMD_LEAVE_PROGMODE_ISP: 0x11, + CMD_CHIP_ERASE_ISP: 0x12, + CMD_PROGRAM_FLASH_ISP: 0x13, + CMD_READ_FLASH_ISP: 0x14, + CMD_PROGRAM_EEPROM_ISP: 0x15, + CMD_READ_EEPROM_ISP: 0x16, + CMD_PROGRAM_FUSE_ISP: 0x17, + CMD_READ_FUSE_ISP: 0x18, + CMD_PROGRAM_LOCK_ISP: 0x19, + CMD_READ_LOCK_ISP: 0x1a, + CMD_READ_SIGNATURE_ISP: 0x1b, + CMD_READ_OSCCAL_ISP: 0x1c, + CMD_SPI_MULTI: 0x1d, + + // STK PP command constants + CMD_ENTER_PROGMODE_PP: 0x20, + CMD_LEAVE_PROGMODE_PP: 0x21, + CMD_CHIP_ERASE_PP: 0x22, + CMD_PROGRAM_FLASH_PP: 0x23, + CMD_READ_FLASH_PP: 0x24, + CMD_PROGRAM_EEPROM_PP: 0x25, + CMD_READ_EEPROM_PP: 0x26, + CMD_PROGRAM_FUSE_PP: 0x27, + CMD_READ_FUSE_PP: 0x28, + CMD_PROGRAM_LOCK_PP: 0x29, + CMD_READ_LOCK_PP: 0x2a, + CMD_READ_SIGNATURE_PP: 0x2b, + CMD_READ_OSCCAL_PP: 0x2c, + CMD_SET_CONTROL_STACK: 0x2d, + + // STK HVSP command constants + CMD_ENTER_PROGMODE_HVSP: 0x30, + CMD_LEAVE_PROGMODE_HVSP: 0x31, + CMD_CHIP_ERASE_HVSP: 0x32, + CMD_PROGRAM_FLASH_HVSP: 0x33, + CMD_READ_FLASH_HVSP: 0x34, + CMD_PROGRAM_EEPROM_HVSP: 0x35, + CMD_READ_EEPROM_HVSP: 0x36, + CMD_PROGRAM_FUSE_HVSP: 0x37, + CMD_READ_FUSE_HVSP: 0x38, + CMD_PROGRAM_LOCK_HVSP: 0x39, + CMD_READ_LOCK_HVSP: 0x3a, + CMD_READ_SIGNATURE_HVSP: 0x3b, + CMD_READ_OSCCAL_HVSP: 0x3c, + + // STK status constants + // Success + STATUS_CMD_OK: 0x00, + // Warnings + STATUS_CMD_TOUT: 0x80, + STATUS_RDY_BSY_TOUT: 0x81, + STATUS_SET_PARAM_MISSING: 0x82, + // Errors + STATUS_CMD_FAILED: 0xc0, + STATUS_CKSUM_ERROR: 0xc1, + STATUS_CMD_UNKNOWN: 0xc9, + + // STK parameter constants + STATUS_BUILD_NUMBER_LOW: 0x80, + STATUS_BUILD_NUMBER_HIGH: 0x81, + STATUS_HW_VER: 0x90, + STATUS_SW_MAJOR: 0x91, + STATUS_SW_MINOR: 0x92, + STATUS_VTARGET: 0x94, + STATUS_VADJUST: 0x95, + STATUS_OSC_PSCALE: 0x96, + STATUS_OSC_CMATCH: 0x97, + STATUS_SCK_DURATION: 0x98, + STATUS_TOPCARD_DETECT: 0x9a, + STATUS_STATUS: 0x9c, + STATUS_DATA: 0x9d, + STATUS_RESET_POLARITY: 0x9e, + STATUS_CONTROLLER_INIT: 0x9f, + + // STK answer constants + ANSWER_CKSUM_ERROR: 0xb0, +}; diff --git a/src/lib/programmers/STK500v2/index.ts b/src/lib/programmers/STK500v2/index.ts new file mode 100644 index 0000000..6a603e5 --- /dev/null +++ b/src/lib/programmers/STK500v2/index.ts @@ -0,0 +1,331 @@ +import type { Programmer } from "$domain/robots.types"; +import type { LeaphyPort } from "$state/serial.svelte"; +import WorkspaceState from "$state/workspace.svelte"; +import hex from "intel-hex"; +import { clearReadBuffer, delay } from "../utils"; +import constants from "./constants"; +import Parser, { concat } from "./parser"; + +const defaultOptions = { + timeout: 0xc8, + stabDelay: 0x64, + cmdexeDelay: 0x19, + synchLoops: 0x20, + byteDelay: 0x00, + pollValue: 0x53, + pollIndex: 0x03, +}; + +export default class STK500v2 implements Programmer { + parser: Parser; + port: LeaphyPort; + writeStream: WritableStreamDefaultWriter; + readStream: ReadableStreamDefaultReader; + + async upload(port: LeaphyPort, response: Record) { + this.port = port; + await this.port.close(); + await this.port.open({ baudRate: 115200 }); + + this.writeStream = port.writable.getWriter(); + this.readStream = port.readable.getReader(); + + const program = response.hex; + + this.parser = new Parser(this.writeStream, this.readStream); + this.parser.addEventListener("log", (event) => { + WorkspaceState.uploadLog.push( + "detail" in event ? (event.detail as string) : `type_${event.type}`, + ); + }); + + return new Promise((resolve, reject) => { + this.sync(5, (err) => { + if (err) { + return reject("NOT_IN_SYNC"); + } + + this.verifySignature(new Uint8Array([30, 152, 1]), (err) => { + if (err) { + return reject("SIGNATURE_MISMATCH"); + } + + this.enterProgrammingMode({}, async (err) => { + if (err) { + return reject("OPTIONS_NOT_ACCEPTED"); + } + + await this.flash(hex.parse(program).data, 256); + + this.exitProgrammingMode(async (err) => { + if (err) reject(err); + + this.writeStream = this.parser.writer; + this.readStream = this.parser.reader; + + // Reset the Arduino + try { + await this.reset(115200); + } catch {} + + this.readStream.releaseLock(); + this.writeStream.releaseLock(); + await this.port.close(); + await this.port.open({ baudRate: 115200 }); + + resolve(); + }); + }); + }); + }); + }); + } + + sync(attempts: number, done: (err: Error | false, res: any) => void) { + const cmd = new Uint8Array([constants.CMD_SIGN_ON]); + let tries = 1; + + const attempt = () => { + tries = tries + 1; + + this.parser + .send(cmd, (err: false | Error, pkt) => { + if (err && tries <= attempts) return attempt(); + + let res: string; + let error: false | Error = false; + + // message response format for CMD_SIGN_ON + // 1 CMD_SIGN_ON + // 1 STATUS_CMD_OK + // 1 8 - length of sig string + // 8 the signature string - "STK500_2" or "AVRISP_2" + const response = pkt.message; + + if (response[0] !== constants.CMD_SIGN_ON) { + // something is wrong. look for error in constants. + error = new Error( + `command response was not CMD_SIGN_ON. ${response[0]}`, + ); + } else if (response[1] !== constants.STATUS_CMD_OK) { + // malformed. check command status constants and return error + error = new Error(`command status was not ok. ${response[1]}`); + } else { + let len = response[2]; + res = `${response.slice(3)}`; + if (res.length !== len) { + // something is wrong but all signs point to right, + } + } + + done(error, res); + }) + .then(); + }; + attempt(); + } + + verifySignature(signature: Uint8Array, done: (err?: Error) => void) { + this.getSignature( + (_error: Error | false, reportedSignature: Uint8Array) => { + const equals = + signature.every((byte, i) => reportedSignature[i] === byte) && + signature.length === reportedSignature.length; + + if (!equals) done(new Error("signature does not match")); + else done(); + }, + ).then(); + } + + async getSignature( + done: (error: Error | false, signature: Uint8Array) => void, + ) { + let reportedSignature = new Uint8Array(3); + const view = new DataView(reportedSignature.buffer); + + try { + await this.getSignatureOffset(view, 0x00); + await this.getSignatureOffset(view, 0x01); + await this.getSignatureOffset(view, 0x02); + } catch (e) { + done(e, reportedSignature); + } + + done(false, reportedSignature); + } + + getSignatureOffset(view: DataView, offset: number) { + const numTx = 0x04; + const numRx = 0x04; + const rxStartAddr = 0x00; + + const cmd = new Uint8Array([ + constants.CMD_SPI_MULTI, + numTx, + numRx, + rxStartAddr, + 0x30, + 0x00, + offset, + 0x00, + ]); + return new Promise((resolve, reject) => { + this.parser + .send(cmd, (error, pkt) => { + if (pkt?.message && pkt.message.length >= 6) { + let sig = pkt.message[5]; + view.setUint8(offset, sig); + } + + if (error) reject(error); + else resolve(); + }) + .then(); + }); + } + + enterProgrammingMode( + setOptions: Partial, + done: (err: Error | false) => void, + ) { + const options = Object.assign(defaultOptions, setOptions); + const cmd1 = 0xac; + const cmd2 = 0x53; + const cmd3 = 0x00; + const cmd4 = 0x00; + + const cmd = new Uint8Array([ + constants.CMD_ENTER_PROGMODE_ISP, + options.timeout, + options.stabDelay, + options.cmdexeDelay, + options.synchLoops, + options.byteDelay, + options.pollValue, + options.pollIndex, + cmd1, + cmd2, + cmd3, + cmd4, + ]); + this.parser + .send(cmd, (err) => { + done(err); + }) + .then(); + } + + loadAddress(useaddr: number, done: (err?: Error) => void) { + const msb = ((useaddr >> 24) & 0xff) | 0x80; + const xsb = (useaddr >> 16) & 0xff; + const ysb = (useaddr >> 8) & 0xff; + const lsb = useaddr & 0xff; + + const cmdBuf = new Uint8Array([ + constants.CMD_LOAD_ADDRESS, + msb, + xsb, + ysb, + lsb, + ]); + this.parser + .send(cmdBuf, (err) => { + if (err) done(err); + else done(); + }) + .then(); + } + + loadPage(writeBytes: Uint8Array, done: (err?: Error) => void) { + const bytesMsb = writeBytes.length >> 8; //Total number of bytes to program, MSB first + const bytesLsb = writeBytes.length & 0xff; //Total number of bytes to program, MSB first + const mode = 0xc1; //paged, rdy/bsy polling, write page + const delay = 0x0a; //Delay, used for different types of programming termination, according to mode byte + const cmd1 = 0x40; // Load Page, Write Program Memory + const cmd2 = 0x4c; // Write Program Memory Page + const cmd3 = 0x20; //Read Program Memory + const poll1 = 0x00; //Poll Value #1 + const poll2 = 0x00; //Poll Value #2 (not used for flash programming) + + let cmdBuf = new Uint8Array([ + constants.CMD_PROGRAM_FLASH_ISP, + bytesMsb, + bytesLsb, + mode, + delay, + cmd1, + cmd2, + cmd3, + poll1, + poll2, + ]); + cmdBuf = concat([cmdBuf, writeBytes]); + this.parser + .send(cmdBuf, (err) => { + if (err) done(err); + else done(); + }) + .then(); + } + + async flash(hex: Uint8Array, pageSize: number) { + let pageaddr = 0; + let writeBytes: Uint8Array; + let useAddr: number; + + while (pageaddr < hex.length) { + useAddr = pageaddr >> 1; + await new Promise((resolve, reject) => { + this.loadAddress(useAddr, (err) => { + if (err) reject(err); + else resolve(); + }); + }); + writeBytes = hex.slice( + pageaddr, + hex.length > pageSize ? pageaddr + pageSize : hex.length - 1, + ); + await new Promise((resolve, reject) => { + this.loadPage(writeBytes, (err) => { + if (err) reject(err); + else resolve(); + }); + }); + await new Promise((resolve) => { + pageaddr = pageaddr + writeBytes.length; + setTimeout(resolve, 4); + }); + } + } + + exitProgrammingMode(done: (err?: Error) => void) { + const preDelay = 0x01; + const postDelay = 0x01; + + const cmd = new Uint8Array([ + constants.CMD_LEAVE_PROGMODE_ISP, + preDelay, + postDelay, + ]); + this.parser + .send(cmd, (err) => { + if (err) done(err); + else done(); + }) + .then(); + } + + async reset(baudRate: number) { + this.writeStream?.releaseLock(); + this.readStream?.releaseLock(); + await this.port.close(); + await this.port.open({ baudRate: baudRate }); + this.readStream = this.port.readable.getReader(); + this.writeStream = this.port.writable.getWriter(); + await this.port.setSignals({ dataTerminalReady: false }); + await delay(250); + await this.port.setSignals({ dataTerminalReady: true }); + await clearReadBuffer(this.readStream); + } +} diff --git a/src/lib/programmers/STK500v2/parser.ts b/src/lib/programmers/STK500v2/parser.ts new file mode 100644 index 0000000..b1a6db9 --- /dev/null +++ b/src/lib/programmers/STK500v2/parser.ts @@ -0,0 +1,323 @@ +import constants from "./constants"; + +class ParserError extends Error { + constructor( + public code: string, + message: string, + ) { + super(message); + } +} + +export function concat(buffers: Uint8Array[]) { + const length = buffers.reduce((value, current) => value + current.length, 0); + const buffer = new Uint8Array(length); + + let pos = 0; + for (const temp of buffers) { + buffer.set(temp, pos); + pos += temp.length; + } + + return buffer; +} + +type Current = + | false + | { + timeout: false | NodeJS.Timeout; + seq: number; + cb: (err: ParserError | false, pkt?: any) => void; + }; + +type PKT = + | false + | { + seq: number; + len: number[] | number; + raw: number[]; + message: number[] | Uint8Array; + checksum: number | boolean; + error?: ParserError; + }; + +export default class Parser extends EventTarget { + private closed = false; + // write + private inc = -1; + private queue = []; + private current: Current = false; + // parser + private state = 0; + private pkt: PKT = false; + + constructor( + public writer: WritableStreamDefaultWriter, + public reader: ReadableStreamDefaultReader, + ) { + super(); + + this.listen().then(); + } + + async listen() { + try { + while (true) { + const { value, done } = await this.reader.read(); + if (done) { + console.log("done"); + return this.cleanup(); + } + + this.dataHandler(value); + } + } catch (e) { + return this.cleanup(e); + } + } + + async send( + body: Uint8Array, + cb: (err: ParserError | false, pkt?: any) => void, + ) { + if (this.closed) { + throw new Error("This parser is closed!"); + } + + const timeout = this.commandTimeout(body[0]); + + const msgLenBuffer = new ArrayBuffer(2); + const msgLenView = new DataView(msgLenBuffer); + msgLenView.setUint16(0, body.length, false); + + const msgLen = new Uint8Array(msgLenBuffer); + + const out = concat([ + new Uint8Array([ + constants.MESSAGE_START, + this.seq(), + msgLen[0], + msgLen[1], + constants.TOKEN, + ]), + body, + ]); + const checksum = this.checksum(out); + + this.queue.push({ + buf: concat([out, new Uint8Array([checksum])]), + seq: this.inc, + cb, + timeout, + }); + this._send().then(); + } + + _pkt(): PKT { + return { + seq: -1, + len: [], + raw: [], + message: [], + checksum: 0, + }; + } + + stateMachine(curByte: number) { + let pkt = this.pkt; + switch (this.state) { + case 0: { + pkt = this.pkt = this._pkt(); + + if (curByte !== 0x1b) { + return this.emit( + "log", + `Invalid header byte expected 27 got: ${curByte}`, + ); + } + ++this.state; + break; + } + case 1: { + if (!this.current || !pkt) break; + if (curByte !== this.current.seq) { + this.state = 0; + return this.emit( + "log", + `Invalid sequence number. back to start. got: ${curByte}`, + ); + } + pkt.seq = curByte; + ++this.state; + break; + } + case 2: { + if (!pkt || !Array.isArray(pkt.len)) break; + pkt.len.push(curByte); + ++this.state; + break; + } + case 3: { + if (!pkt || !Array.isArray(pkt.len)) break; + pkt.len.push(curByte); + pkt.len = (pkt.len[0] << 8) | pkt.len[1]; + ++this.state; + break; + } + case 4: { + if (!pkt) break; + if (curByte !== 0x0e) { + this.state = 0; + pkt.error = new ParserError( + "E_PARSE", + `Invalid message token byte. got: ${curByte}`, + ); + return this.emit("log", pkt?.error); + } + ++this.state; + if (!pkt.len) ++this.state; + + break; + } + case 5: { + if (!pkt) break; + if (pkt.len === 0 && curByte === constants.STATUS_CKSUM_ERROR) { + pkt.error = new ParserError("E_STATUS_CKSUM", "send checksum error"); + } + + (pkt.message as number[]).push(curByte); + if (--(pkt.len as number) === 0) ++this.state; + break; + } + case 6: { + if (!pkt) break; + + pkt.checksum = this.checksum(new Uint8Array(pkt.raw)); + pkt.checksum = pkt.checksum === curByte; + if (!pkt.checksum) { + pkt.error = new ParserError( + "E_RECV_CKSUM", + "recv cecksum didn't match", + ); + } + + pkt.message = new Uint8Array(pkt.message); + this.emit("data", pkt); + this.state++; + pkt.len = pkt.message.length; + pkt.raw = undefined; + this.resolveCurrent(pkt.error ? pkt.error : false, pkt); + break; + } + } + + if (pkt?.raw) pkt.raw.push(curByte); + } + + dataHandler(data: Uint8Array) { + const current = this.current; + this.emit("raw", data); + if (!current) return this.emit("log", "droping data"); + + for (const byte of data) { + this.stateMachine(byte); + } + } + + commandTimeout(typeByte: number) { + let timeout = 1000; + if (typeByte === constants.CMD_SIGN_ON) timeout = 200; + else { + const type = Object.keys(constants).find( + (type) => constants[type] === typeByte, + ); + const types = ["CMD_READ", "PROGRAM_FLASH", "EEPROM"]; + for (const check of types) { + if (type.includes(check)) timeout = 5000; + } + } + + return timeout; + } + + seq() { + this.inc++; + if (this.inc > 0xff) this.inc = 0; + + return this.inc; + } + + checksum(buf: Uint8Array) { + let checksum = 0; + for (const byte of buf) { + checksum ^= byte; + } + + return checksum; + } + + emit(type: string, message?: any) { + // @ts-ignore + this.dispatchEvent(new Event(type, { detail: message })); + } + + async _send() { + if (this.closed) return false; + if (this.current) return; + if (!this.queue.length) return; + + const message = this.queue.shift(); + const current: Current = (this.current = { + timeout: false, + seq: message.seq, + cb: message.cb, + }); + + this.state = 0; + + await this.writer.write(message.buf); + if (current !== this.current) + return this.emit( + "log", + "current was no longer the current message after drain callback", + ); + current.timeout = setTimeout(() => { + const err = new ParserError( + "E_TIMEOUT", + `stk500 timeout. ${message.timeout}ms`, + ); + this.resolveCurrent(err); + }, message.timeout); + this.emit("rawinput", message.buf); + } + + resolveCurrent(err: ParserError | false, pkt?: any) { + const toCall = this.current; + this.current = false; + + let q: false | any[] = false; + if ((err && ["E_PARSE", "E_TIMEOUT"].includes(err?.code)) || this.closed) { + q = this.queue; + this.queue = []; + } + + if (!toCall) return; + if (toCall.timeout) clearTimeout(toCall.timeout); + + toCall.cb(err, pkt); + if (q) { + while (q.length) q.shift()(err); + } + + this._send().then(); + } + + cleanup(err?: ParserError) { + this.closed = true; + + if (this.current) + this.resolveCurrent(err || new ParserError("E_CLOSED", "Serial closed")); + + this.emit("closed"); + } +} diff --git a/src/lib/programmers/utils.ts b/src/lib/programmers/utils.ts index 43f96f8..77a9237 100644 --- a/src/lib/programmers/utils.ts +++ b/src/lib/programmers/utils.ts @@ -1,3 +1,70 @@ +import WorkspaceState from "$state/workspace.svelte"; + export function delay(time: number) { return new Promise((resolve) => setTimeout(resolve, time)); } + +export async function clearReadBuffer( + reader: ReadableStreamDefaultReader, +) { + WorkspaceState.uploadLog.push("Clearing read buffer"); + + const TIMEOUT = Symbol("timeout"); + const READ_TIMEOUT_MS = 100; + const ATTEMPT_TIMEOUT_MS = 1500; + const MAX_ATTEMPTS = 10; + + async function attemptDrainStream() { + while (true) { + const result = await Promise.race([ + reader.read(), + new Promise((resolve) => + setTimeout(() => resolve(TIMEOUT), READ_TIMEOUT_MS), + ), + ]); + + if (result === TIMEOUT) { + WorkspaceState.uploadLog.push( + "No data received - buffer likely cleared", + ); + return true; + } + + if ((result as ReadableStreamReadResult).done) { + WorkspaceState.uploadLog.push("Stream termination detected"); + return true; + } + } + } + + for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { + WorkspaceState.uploadLog.push(`Clearing attempt #${attempt}`); + const cleanupSuccess = await Promise.race([ + attemptDrainStream(), + new Promise((resolve) => + setTimeout(() => resolve(TIMEOUT), ATTEMPT_TIMEOUT_MS), + ), + ]); + + if (cleanupSuccess) { + WorkspaceState.uploadLog.push("Read buffer successfully cleared"); + return; + } + } + + throw new Error("Failed to clear read buffer after maximum attempts"); +} + +export function includesAll(values: number[], array: Uint8Array) { + return values.every((value) => { + return array.includes(value); + }); +} + +export function convertArrayToHex(array: Uint8Array) { + const result: string[] = []; + for (let i = 0; i < array.length; i++) { + result.push(array[i].toString(16)); + } + return result; +} diff --git a/src/main.ts b/src/main.ts index e37aafd..9edf772 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,9 +7,11 @@ import App from "./App.svelte"; import enTranslations from "./assets/translations/en.json"; import nlTranslations from "./assets/translations/nl.json"; import initMatomo from "./lib/matomo"; -import setupRecording from "./lib/recording"; import initSentry from "./lib/sentry"; +import { Buffer } from "buffer"; +window.Buffer = Buffer; + initSentry(); addMessages("en", enTranslations); diff --git a/vite.config.ts b/vite.config.ts index 18142b3..e73d572 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -17,18 +17,6 @@ export default defineConfig({ src: './node_modules/@leaphy-robotics/leaphy-blocks/media/*', dest: 'blockly-assets' }, - { - src: './node_modules/@leaphy-robotics/avrdude-webassembly/avrdude.wasm', - dest: '' - }, - { - src: './node_modules/@leaphy-robotics/avrdude-webassembly/avrdude-worker.js', - dest: '' - }, - { - src: './node_modules/@leaphy-robotics/avrdude-webassembly/avrdude.conf', - dest: '' - }, { src: "./node_modules/@leaphy-robotics/dfu-util-wasm/build/*", dest: 'dfu-util' diff --git a/yarn.lock b/yarn.lock index 546c306..6819ed6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -564,13 +564,6 @@ resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.4.tgz#4d4ff677e1609214fc71c580125ddddd86abcabf" integrity sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w== -"@leaphy-robotics/avrdude-webassembly@1.7.1": - version "1.7.1" - resolved "https://registry.yarnpkg.com/@leaphy-robotics/avrdude-webassembly/-/avrdude-webassembly-1.7.1.tgz#bb05864f576560dff697c5243c8422e23a27c9c7" - integrity sha512-EGvfbuRuVwZe/UTpRsfy/PiYdkps5kkN8xCa20SZkAwHyjUHrnTZapX/iHPlwpmpDeUiyQINFIXhXk+nWJfJTw== - dependencies: - "@leaphy-robotics/webusb-ftdi" "^1.0.0" - "@leaphy-robotics/dfu-util-wasm@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@leaphy-robotics/dfu-util-wasm/-/dfu-util-wasm-1.0.2.tgz#29afcc01266655e1c7b5da0eeceb0ead48c86fd7" @@ -595,10 +588,10 @@ dependencies: serialport "^12.0.0" -"@leaphy-robotics/webusb-ftdi@1.0.1", "@leaphy-robotics/webusb-ftdi@^1.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@leaphy-robotics/webusb-ftdi/-/webusb-ftdi-1.0.1.tgz#3bcc6eeb0f03ebe8a91d727052b04a61a3517b4b" - integrity sha512-qPR4+mEg+mIqrvOMAov+mOfxM15DgMWofZufLpfktIa+4/kTRIjljCgYUO5l5Jeb7AuDUi4Uvrn/WT765T8thQ== +"@leaphy-robotics/webusb-ftdi@1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@leaphy-robotics/webusb-ftdi/-/webusb-ftdi-1.0.2.tgz#1d84826b6b3823f5dc62d70aceb15e100972d9c8" + integrity sha512-8u34rfbkgk+BsYd2/SgRozTIm1cZO+pHZYxg27d+FhWyJEgTSMsZ7xdrRLptOOj9BtOGYACXPo9MDI1Wf9QIRQ== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -1199,7 +1192,7 @@ base64-arraybuffer@^1.0.1: resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc" integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ== -base64-js@^1.5.1: +base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -1240,6 +1233,14 @@ browserslist@^4.24.0: node-releases "^2.0.18" update-browserslist-db "^1.1.1" +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + caniuse-lite@^1.0.30001669: version "1.0.30001687" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001687.tgz#d0ac634d043648498eedf7a3932836beba90ebae" @@ -1704,6 +1705,11 @@ iconv-lite@0.6.3: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" +ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + immediate@~3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" @@ -1714,6 +1720,11 @@ inherits@~2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +intel-hex@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/intel-hex/-/intel-hex-0.2.0.tgz#8737d863e9d0ee2b4728e2d2188fe405a135ec2c" + integrity sha512-PJNkAm4N32VjcP7D14q1M+y/en/VlhqFyeaGiEhW/L9gPa7Lv+7vAcVlVBamKMfk6cZCcA9dTvHVXahcOxKzlw== + intl-messageformat@^10.5.3: version "10.7.3" resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.7.3.tgz#a5cbc886f1db4b6324e6bfc4fd8f91a3e3e37c31" From 22aa9426af08429e4172dddb18e77447a07b2e17 Mon Sep 17 00:00:00 2001 From: Ronald Moesbergen Date: Sun, 26 Jan 2025 15:39:06 +0100 Subject: [PATCH 2/2] fix: bump playwright-arduino, re-enable upload tests --- package.json | 2 +- tests/arduino.spec.ts | 4 ++-- yarn.lock | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 85c527e..f675ace 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ }, "devDependencies": { "@biomejs/biome": "^1.7.2", - "@leaphy-robotics/playwright-arduino": "1.0.5", + "@leaphy-robotics/playwright-arduino": "1.0.6", "@playwright/test": "^1.43.1", "@sentry/vite-plugin": "^2.16.1", "@sveltejs/vite-plugin-svelte": "^5.0.1", diff --git a/tests/arduino.spec.ts b/tests/arduino.spec.ts index 945c099..46d1f6d 100644 --- a/tests/arduino.spec.ts +++ b/tests/arduino.spec.ts @@ -6,7 +6,7 @@ test.describe.configure({ mode: "serial" }); test.beforeEach(setupArduino); test.beforeEach(goToHomePage); -test.fixme("Arduino - Blockly upload", async ({ page }) => { +test("Arduino - Blockly upload", async ({ page }) => { await selectRobot(page, "Leaphy Original", "Original Uno"); await openExample(page, "Blink"); @@ -23,7 +23,7 @@ test.fixme("Arduino - Blockly upload", async ({ page }) => { }); }); -test.fixme("Arduino - C++ upload", async ({ page }) => { +test("Arduino - C++ upload", async ({ page }) => { await selectRobot(page, "Leaphy C++"); await page.getByRole("button", { name: "Upload to robot" }).click(); await expect( diff --git a/yarn.lock b/yarn.lock index 6819ed6..9388420 100644 --- a/yarn.lock +++ b/yarn.lock @@ -581,10 +581,10 @@ resolved "https://registry.yarnpkg.com/@leaphy-robotics/picotool-wasm/-/picotool-wasm-1.0.3.tgz#9047c984c7c261685f6e88d49c9ed54eaeb29391" integrity sha512-K3St0eLNVRPXPMZ+o1q/+S46zPFw8PEsTMxYQqiLdjJvbvzGgwOZVWOkqukeEUTLtmdopTxkFksPZxmI+jDyTg== -"@leaphy-robotics/playwright-arduino@1.0.5": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@leaphy-robotics/playwright-arduino/-/playwright-arduino-1.0.5.tgz#4a89da44c5a979087f65c6d8bb3d17cd93dea382" - integrity sha512-N4wT4M9rR0tmgsthQvDeN4JH3QalXv04hMqr4Mr88mBhsjPTtFhHUWVY4tcCYPBY4kTw3hkfnCjDKi4AzoWbgw== +"@leaphy-robotics/playwright-arduino@1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@leaphy-robotics/playwright-arduino/-/playwright-arduino-1.0.6.tgz#9aad16823032ea5f93476a2fae0bbfff44e6f28f" + integrity sha512-L0UR8/6o1Gpo73CiQLsZqc3USYsSPv4h7rZ6pKGAGOnxzACpBh4SpOCpv2ipkvWQAVtifWbKFdlZjO0HFFP6Tw== dependencies: serialport "^12.0.0"