Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
},
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "node --test"
},
"repository": {
"type": "git",
Expand Down
39 changes: 39 additions & 0 deletions repl.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
38 changes: 38 additions & 0 deletions test/usb-drive.test.mjs
Original file line number Diff line number Diff line change
@@ -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);
});