From 98b7243c9e8373174ce856717c27000ed5923d32 Mon Sep 17 00:00:00 2001 From: makermelissa-ai-assistant <276182804+makermelissa-ai-assistant@users.noreply.github.com> Date: Thu, 23 Jul 2026 12:00:45 -0700 Subject: [PATCH] Add USB drive control helpers --- package.json | 2 +- repl.js | 39 +++++++++++++++++++++++++++++++++++++++ test/usb-drive.test.mjs | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 test/usb-drive.test.mjs diff --git a/package.json b/package.json index d73f60b..eddb186 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ }, "devDependencies": {}, "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "node --test" }, "repository": { "type": "git", diff --git a/repl.js b/repl.js index aaae37f..da0ad78 100644 --- a/repl.js +++ b/repl.js @@ -896,6 +896,45 @@ export class REPL { return this._codeOutput; } + // Best-effort request to make CIRCUITPY writable from the REPL by taking + // its USB mass-storage drive offline. The underlying CircuitPython API is + // intentionally named "unsafe" because host writes in progress can corrupt + // the filesystem. Callers are responsible for deciding when it is safe to + // use and for restoring the drive when finished. + async tryDisableUsbDrive() { + const code = ` +try: + import storage + if hasattr(storage, "unsafe_disable_usb_drive"): + storage.unsafe_disable_usb_drive() + _disabled = True + else: + _disabled = False +except Exception: + _disabled = False +print("USB_DRIVE_DISABLED", _disabled) +`; + const output = await this.runCode(code); + return /USB_DRIVE_DISABLED\s+True/.test(output || ""); + } + + // Reverse a successful tryDisableUsbDrive() call. Returns false when the + // firmware does not support changing USB mass storage after startup or the + // operation otherwise fails. + async enableUsbDrive() { + const code = ` +try: + import storage + storage.enable_usb_drive() + _enabled = True +except Exception: + _enabled = False +print("USB_DRIVE_ENABLED", _enabled) +`; + const output = await this.runCode(code); + return /USB_DRIVE_ENABLED\s+True/.test(output || ""); + } + // Snapshot of the board's runtime state in a single round trip. // Returns { readOnly, usbConnected }. Each field is null when the // underlying CircuitPython attribute isn't available or the probe failed. diff --git a/test/usb-drive.test.mjs b/test/usb-drive.test.mjs new file mode 100644 index 0000000..a47a1f8 --- /dev/null +++ b/test/usb-drive.test.mjs @@ -0,0 +1,38 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import {REPL} from "../repl.js"; + +test("tryDisableUsbDrive returns true when CircuitPython disables the drive", async () => { + const repl = new REPL(); + repl.runCode = async (code) => { + assert.match(code, /unsafe_disable_usb_drive/); + return "USB_DRIVE_DISABLED True\n"; + }; + + assert.equal(await repl.tryDisableUsbDrive(), true); +}); + +test("tryDisableUsbDrive returns false when the feature is unavailable", async () => { + const repl = new REPL(); + repl.runCode = async () => "USB_DRIVE_DISABLED False\n"; + + assert.equal(await repl.tryDisableUsbDrive(), false); +}); + +test("enableUsbDrive returns true when CircuitPython restores the drive", async () => { + const repl = new REPL(); + repl.runCode = async (code) => { + assert.match(code, /storage\.enable_usb_drive\(\)/); + return "USB_DRIVE_ENABLED True\n"; + }; + + assert.equal(await repl.enableUsbDrive(), true); +}); + +test("enableUsbDrive returns false when restoring the drive fails", async () => { + const repl = new REPL(); + repl.runCode = async () => "USB_DRIVE_ENABLED False\n"; + + assert.equal(await repl.enableUsbDrive(), false); +});