@esp-vision/device-cli 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/THIRD_PARTY_NOTICES.md +2 -2
- package/dist/device-cli.mjs +123 -21
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`@esp-vision/device-cli` installs the `esp-vision-device` command for device discovery, inspection, configuration, control, firmware updates, Python execution, device file operations, JPEG capture, sequential batch plans, and bounded event watches. It is an optional host interface backed by the same `@esp-vision/device-core` package used by ESP-VISION clients.
|
|
4
4
|
|
|
5
|
-
Requires Node.js 22 or newer. Serial access requires Python with `pyserial`; firmware flashing requires Python `esptool
|
|
5
|
+
Requires Node.js 22 or newer. Serial access requires Python with `pyserial`; firmware flashing requires Python `esptool==5.6.0`. LAN discovery uses the bundled multicast-DNS client. Check the selected interpreter with `PYTHON --version` and `PYTHON -c "import serial"`; install pyserial there with `PYTHON -m pip install pyserial` (replace `PYTHON` with that executable). Windows Python Launcher users can use `--python py` and `py -m pip install pyserial`. A failed serial setup gets a bounded environment diagnosis after connection cleanup; successful connections, TCP operations, help, and offline firmware checks do not start it. Busy/access-denied errors keep their port-specific guidance; an unknown adapter failure is not proof of missing Python or pyserial.
|
|
6
6
|
|
|
7
7
|
Run `esp-vision-device --help` for the command reference, or `esp-vision-device --help --json` for the versioned machine-readable command, output, and limit capabilities envelope.
|
|
8
8
|
|
|
@@ -26,7 +26,7 @@ For a continuous app, finish directory operations and uploads before starting it
|
|
|
26
26
|
|
|
27
27
|
`files list`, `files read`, `files write`, `files remove`, and `files mkdir` operate on device paths. `files read DEVICE_PATH --output LOCAL_FILE` writes binary content to a local file. `files write DEVICE_PATH --file LOCAL_FILE` sends local bytes to the device. Reads are capped at 16 MiB by default; `--max-bytes` can lower that limit.
|
|
28
28
|
|
|
29
|
-
`capture camera|screen --output LOCAL.jpg` writes one JPEG and returns its path, byte count, source, MIME type, and capture metadata. Images are never embedded as base64 in JSON output.
|
|
29
|
+
`capture camera|screen --output LOCAL.jpg` writes one JPEG and returns its path, byte count, source, MIME type, and capture metadata. Images are never embedded as base64 in JSON output. The `source` field identifies the requested source; it does not certify whether annotations are present. `camera` observes the application's latest camera frame, not the LCD image. Application drawing depends on firmware publication behavior; firmware advertising `debug.capture_frame.observed.v1` supplies an independent copy made at acquisition, before later drawing, without triggering a new acquisition. `screen` captures the current LCD framebuffer, including drawing rendered there; use it to verify LCD overlays and UI. It requires an initialized display, and camera/display dimensions may differ. Neither source starts the application or initializes the sensor/display for you.
|
|
30
30
|
|
|
31
31
|
## Bounded event watch
|
|
32
32
|
|
|
@@ -58,3 +58,5 @@ For a continuous app, finish directory operations and uploads before starting it
|
|
|
58
58
|
Status, Python output, firmware phases, cancellation, errors, and duration are NDJSON events on stderr. SIGINT exits with code 130, SIGTERM with 143, argument/plan errors with 2, other failures with 1, and success with 0. Busy or uncertain operations are never retried automatically.
|
|
59
59
|
|
|
60
60
|
Inspect JSON output uses the same versioned envelope as other commands. Human-readable inspect output remains a terminal table.
|
|
61
|
+
|
|
62
|
+
Device paths such as `/models` stay absolute device paths on Windows. Git Bash/MSYS may rewrite them; use PowerShell or prefix the individual command with `MSYS2_ARG_CONV_EXCL="*"`, supplying local paths in Windows form. Quoting the device path alone does not prevent conversion. Invalid device paths now fail before connecting, with a shell hint for Windows host paths.
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -120,7 +120,7 @@ Bundled from: `node_modules/@esp-vision/device-core/src/firmware_feed.js`, `node
|
|
|
120
120
|
|
|
121
121
|
### Source notice 7
|
|
122
122
|
|
|
123
|
-
Bundled from: `node_modules/@esp-vision/device-core/src/firmware_io.js`, `node_modules/@esp-vision/device-core/src/firmware_release.js`, `node_modules/@esp-vision/device-core/src/node.js`
|
|
123
|
+
Bundled from: `node_modules/@esp-vision/device-core/src/firmware_io.js`, `node_modules/@esp-vision/device-core/src/firmware_release.js`, `node_modules/@esp-vision/device-core/src/node.js`, `src/serial_diagnostics.js`
|
|
124
124
|
|
|
125
125
|
```text
|
|
126
126
|
// SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
|
@@ -128,7 +128,7 @@ Bundled from: `node_modules/@esp-vision/device-core/src/firmware_io.js`, `node_m
|
|
|
128
128
|
|
|
129
129
|
### Source notice 8
|
|
130
130
|
|
|
131
|
-
Bundled from: `node_modules/@esp-vision/device-core/src/device_files.js`, `node_modules/@esp-vision/device-core/src/device_python.js`, `node_modules/@esp-vision/device-core/src/firmware_feed.js`, `node_modules/@esp-vision/device-core/src/firmware_flash.js`, `node_modules/@esp-vision/device-core/src/firmware_io.js`, `node_modules/@esp-vision/device-core/src/firmware_release.js`, `node_modules/@esp-vision/device-core/src/index.js`, `node_modules/@esp-vision/device-core/src/network_discovery.js`, `node_modules/@esp-vision/device-core/src/network_discovery_node.js`, `node_modules/@esp-vision/device-core/src/node.js`, `node_modules/@esp-vision/device-core/src/node_inventory.js`, `node_modules/@esp-vision/device-core/src/node_rom.js`, `node_modules/@esp-vision/device-core/src/node_transport.js`, `src/cli.js`
|
|
131
|
+
Bundled from: `node_modules/@esp-vision/device-core/src/device_files.js`, `node_modules/@esp-vision/device-core/src/device_python.js`, `node_modules/@esp-vision/device-core/src/firmware_feed.js`, `node_modules/@esp-vision/device-core/src/firmware_flash.js`, `node_modules/@esp-vision/device-core/src/firmware_io.js`, `node_modules/@esp-vision/device-core/src/firmware_release.js`, `node_modules/@esp-vision/device-core/src/index.js`, `node_modules/@esp-vision/device-core/src/network_discovery.js`, `node_modules/@esp-vision/device-core/src/network_discovery_node.js`, `node_modules/@esp-vision/device-core/src/node.js`, `node_modules/@esp-vision/device-core/src/node_inventory.js`, `node_modules/@esp-vision/device-core/src/node_rom.js`, `node_modules/@esp-vision/device-core/src/node_transport.js`, `src/cli.js`, `src/serial_diagnostics.js`
|
|
132
132
|
|
|
133
133
|
```text
|
|
134
134
|
// SPDX-License-Identifier: Apache-2.0
|
package/dist/device-cli.mjs
CHANGED
|
@@ -3795,6 +3795,60 @@ function stringWidth(string, options2 = {}) {
|
|
|
3795
3795
|
return width;
|
|
3796
3796
|
}
|
|
3797
3797
|
|
|
3798
|
+
// src/serial_diagnostics.js
|
|
3799
|
+
import { execFile } from "node:child_process";
|
|
3800
|
+
import { promisify } from "node:util";
|
|
3801
|
+
var check = `import sys
|
|
3802
|
+
try:
|
|
3803
|
+
import serial
|
|
3804
|
+
except ModuleNotFoundError as error:
|
|
3805
|
+
sys.exit(71 if error.name == 'serial' else 73)
|
|
3806
|
+
except Exception:
|
|
3807
|
+
sys.exit(73)
|
|
3808
|
+
sys.exit(0 if callable(getattr(serial, 'Serial', None)) else 72)
|
|
3809
|
+
`;
|
|
3810
|
+
async function explainSerialFailure(failure3, options2, dependencies = {}) {
|
|
3811
|
+
if (failure3?.code === "SERIAL_BUSY") {
|
|
3812
|
+
failure3.message = "Serial port is busy. Close the IDE, serial monitor or other process using this port before retrying.";
|
|
3813
|
+
return failure3;
|
|
3814
|
+
}
|
|
3815
|
+
if (failure3?.code === "SERIAL_ACCESS_DENIED") {
|
|
3816
|
+
failure3.message = "Serial port access was denied. Check port permissions and close other applications using it; Windows may report an occupied port as access denied.";
|
|
3817
|
+
return failure3;
|
|
3818
|
+
}
|
|
3819
|
+
if (!["SERIAL_ERROR", "SERIAL_UNAVAILABLE"].includes(failure3?.code)) return failure3;
|
|
3820
|
+
if (dependencies.signal?.aborted) return failure3;
|
|
3821
|
+
if (["read", "write", "stdout", "stdin"].includes(failure3.details?.stage)) return failure3;
|
|
3822
|
+
let reason = "unknown";
|
|
3823
|
+
try {
|
|
3824
|
+
await (dependencies.execFile ?? promisify(execFile))(options2.python, ["-c", check], {
|
|
3825
|
+
signal: dependencies.signal,
|
|
3826
|
+
timeout: Math.min(options2.timeout ?? 3e3, 3e3),
|
|
3827
|
+
maxBuffer: 4096,
|
|
3828
|
+
windowsHide: true
|
|
3829
|
+
});
|
|
3830
|
+
reason = "available";
|
|
3831
|
+
} catch (cause) {
|
|
3832
|
+
if (dependencies.signal?.aborted) return failure3;
|
|
3833
|
+
if (cause.code === "ENOENT") reason = "missing-python";
|
|
3834
|
+
else if (cause.code === "EACCES") reason = "python-permission";
|
|
3835
|
+
else if (cause.code === 71) reason = "missing-pyserial";
|
|
3836
|
+
else if (cause.code === 72) reason = "invalid-pyserial";
|
|
3837
|
+
else if (cause.code === 73) reason = "import-failed";
|
|
3838
|
+
}
|
|
3839
|
+
const messages = {
|
|
3840
|
+
"missing-python": "The configured Python executable was not found. Check python3 --version (Windows: python --version or py --version), then select an installed interpreter with --python PATH; Windows Python Launcher users can use --python py.",
|
|
3841
|
+
"python-permission": "The configured Python executable could not be started because access was denied. Check executable permissions or select an accessible interpreter with --python PATH.",
|
|
3842
|
+
"missing-pyserial": 'The configured Python interpreter has no pyserial module. Install it in that same interpreter with PYTHON -m pip install pyserial (Windows Launcher: py -m pip install pyserial and --python py), then verify PYTHON -c "import serial". Replace PYTHON with the executable selected by --python.',
|
|
3843
|
+
"invalid-pyserial": "The configured Python imported a serial module without the pyserial Serial API. Check for a conflicting serial.py or serial package; verify pyserial in the interpreter selected by --python.",
|
|
3844
|
+
"import-failed": 'The configured Python could not import pyserial successfully. Run PYTHON -c "import serial" in the same interpreter to inspect the environment; select it with --python PATH.',
|
|
3845
|
+
available: "Python and the pyserial Serial API are available. Check the selected port, cable, OS permissions and whether another application owns the port. The serial failure does not establish a missing dependency.",
|
|
3846
|
+
unknown: 'Serial adapter failed; Python/pyserial availability could not be established. Check PYTHON --version and PYTHON -c "import serial", select that interpreter with --python PATH (Windows Launcher: --python py), and check port permissions and other applications using the port.'
|
|
3847
|
+
};
|
|
3848
|
+
failure3.message = messages[reason];
|
|
3849
|
+
return failure3;
|
|
3850
|
+
}
|
|
3851
|
+
|
|
3798
3852
|
// node_modules/@esp-vision/device-core/src/device_session.js
|
|
3799
3853
|
var EV_MUX_VERSION = 4;
|
|
3800
3854
|
var INLINE_PYTHON_REPL_TIMEOUT_MS = 5e3;
|
|
@@ -7807,8 +7861,8 @@ import { createInterface } from "node:readline";
|
|
|
7807
7861
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
7808
7862
|
|
|
7809
7863
|
// node_modules/@esp-vision/device-core/src/node_inventory.js
|
|
7810
|
-
import { execFile } from "node:child_process";
|
|
7811
|
-
import { promisify } from "node:util";
|
|
7864
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
7865
|
+
import { promisify as promisify2 } from "node:util";
|
|
7812
7866
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
7813
7867
|
|
|
7814
7868
|
// node_modules/@esp-vision/device-core/src/network_discovery_node.js
|
|
@@ -7949,7 +8003,7 @@ var defaultPython = process.platform === "win32" ? "python" : "python3";
|
|
|
7949
8003
|
async function listSerial({ python = defaultPython, signal, timeout = 1e4 } = {}, dependencies = {}) {
|
|
7950
8004
|
if (!Number.isFinite(timeout) || timeout <= 0) throw new RangeError("Serial enumeration timeout must be positive");
|
|
7951
8005
|
try {
|
|
7952
|
-
const run = dependencies.execFile ??
|
|
8006
|
+
const run = dependencies.execFile ?? promisify2(execFile2);
|
|
7953
8007
|
const { stdout } = await run(python, [fileURLToPath2(new URL("./node_serial.py", import.meta.url)), "--list"], { signal, timeout, maxBuffer: 1024 * 1024, windowsHide: true });
|
|
7954
8008
|
const ports = JSON.parse(stdout);
|
|
7955
8009
|
if (!Array.isArray(ports)) throw new Error("Invalid serial list");
|
|
@@ -8174,9 +8228,12 @@ esp-vision-device files write DEVICE_PATH --file LOCAL_FILE [--no-overwrite]
|
|
|
8174
8228
|
esp-vision-device files remove DEVICE_PATH [--type file|directory]
|
|
8175
8229
|
esp-vision-device files mkdir DEVICE_DIR [--recursive]
|
|
8176
8230
|
esp-vision-device capture camera|screen --output LOCAL.jpg [--quality N] [--max-bytes N]
|
|
8231
|
+
camera: Observe the application's camera-frame source, not the LCD image. Freshness and application drawing depend on firmware publication behavior.
|
|
8232
|
+
screen: Capture the current LCD framebuffer, including drawing rendered there. Use this to verify LCD overlays and UI; display initialization is required.
|
|
8177
8233
|
esp-vision-device batch PLAN.json [--port PATH | --tcp HOST[:PORT]]
|
|
8178
8234
|
esp-vision-device watch --duration MS [--port PATH | --tcp HOST[:PORT]]
|
|
8179
8235
|
Common: --timeout MS, --python PYTHON (default python3; Windows python), --json, --help
|
|
8236
|
+
Serial setup: check PYTHON --version and PYTHON -c "import serial" with the selected interpreter. Install pyserial there with PYTHON -m pip install pyserial. Windows Launcher: --python py and py -m pip install pyserial.
|
|
8180
8237
|
All commands that operate on a device require --port or --tcp, except inspect and offline firmware check.
|
|
8181
8238
|
Python --completion start accepts only --path DEVICE.py. It acknowledges the start, disconnects, and exits; it does not retain a daemon or resumable job.
|
|
8182
8239
|
Python --timeout is an execution deadline for --completion wait only; omit it with --completion start.
|
|
@@ -8187,7 +8244,7 @@ Batch plans use schemaVersion 1 with an operations array. Operations run sequent
|
|
|
8187
8244
|
In JSON mode stdout contains one schemaVersion 1 JSON envelope. Python output, status, logs, and errors are NDJSON events on stderr.
|
|
8188
8245
|
Binary file reads and JPEG captures always write to --output. Local output is capped at 16 MiB by default and --max-bytes may lower that cap.
|
|
8189
8246
|
Firmware versions come from dl.espressif.com by default; local catalogs are advanced overrides.
|
|
8190
|
-
Firmware update uses the built-in serial flasher (Python esptool
|
|
8247
|
+
Firmware update uses the built-in serial flasher (Python esptool==5.6.0). TCP is not OTA.
|
|
8191
8248
|
Default mode update preserves the existing layout/data; explicit mode install writes a complete image, replacing data in its written range. Blank boards require --board and a ROM-capable USJ/UART port, not an application USB-OTG interface.`;
|
|
8192
8249
|
var cliCapabilities = Object.freeze({
|
|
8193
8250
|
commands: {
|
|
@@ -8197,7 +8254,7 @@ var cliCapabilities = Object.freeze({
|
|
|
8197
8254
|
firmware: ["check", "update"],
|
|
8198
8255
|
python: ["--source CODE", "--file LOCAL.py", "--path DEVICE.py", "live output, log and state NDJSON events"],
|
|
8199
8256
|
files: ["list", "read", "write", "remove", "mkdir"],
|
|
8200
|
-
capture: ["camera JPEG", "screen JPEG"],
|
|
8257
|
+
capture: ["camera JPEG: latest application camera frame, not the LCD image; drawing depends on firmware publication behavior", "screen JPEG: LCD framebuffer with drawing rendered there; requires display initialization"],
|
|
8201
8258
|
batch: ["sequential schemaVersion 1 JSON plan", "one connection", "stop at first failure"],
|
|
8202
8259
|
watch: ["bounded output, log, state and diagnostic stream", "one connection", "no script launch or reconnect"]
|
|
8203
8260
|
},
|
|
@@ -8215,6 +8272,13 @@ var error = (code, message, options2 = {}) => Object.assign(new Error(message),
|
|
|
8215
8272
|
var optionError = (message) => error("ARGUMENT", message);
|
|
8216
8273
|
var knownDeviceCommands = ["configure", "control", "firmware", "python", "files", "capture", "batch", "watch"];
|
|
8217
8274
|
var commonOptions = ["port", "tcp", "python", "timeout", "json", "role"];
|
|
8275
|
+
function devicePath(value, { root = false, fail: fail2 = optionError } = {}) {
|
|
8276
|
+
if (typeof value !== "string" || !value.startsWith("/") || !root && value === "/" || new TextEncoder().encode(value).length > 123 || /[\x00-\x1f\x7f\\"]/.test(value) || value.includes("..") || value.includes("//") || value.length > 1 && value.endsWith("/")) {
|
|
8277
|
+
const shellHint = typeof value === "string" && /^[A-Za-z]:[\\/]/.test(value) ? ' Received a Windows host path; Git Bash/MSYS may have converted the device path. Use PowerShell or prefix this command with MSYS2_ARG_CONV_EXCL="*" and keep local paths in Windows form.' : "";
|
|
8278
|
+
throw fail2("Expected an absolute device path (for example /models), at most 123 UTF-8 bytes, without traversal, trailing slash or forbidden characters." + shellHint);
|
|
8279
|
+
}
|
|
8280
|
+
return value;
|
|
8281
|
+
}
|
|
8218
8282
|
function parseTcpAddress(value) {
|
|
8219
8283
|
const match = /^(?:\[([0-9a-fA-F:]+)\]|([^\s:/@?#]+))(?::([0-9]+))?$/.exec(value);
|
|
8220
8284
|
if (!match || Number(match[3] ?? 3232) < 1 || Number(match[3] ?? 3232) > 65535) {
|
|
@@ -8367,6 +8431,7 @@ function parseCli(args) {
|
|
|
8367
8431
|
if (!endpoint) throw optionError("Specify --port or --tcp");
|
|
8368
8432
|
const forms = ["source", "file", "path"].filter((key) => v[key] !== void 0);
|
|
8369
8433
|
if (forms.length !== 1) throw optionError("Specify exactly one of --source, --file or --path");
|
|
8434
|
+
if (v.path !== void 0) devicePath(v.path);
|
|
8370
8435
|
options2.completion = v.completion ?? "wait";
|
|
8371
8436
|
if (!["wait", "start"].includes(options2.completion)) throw optionError("--completion must be wait or start");
|
|
8372
8437
|
if (options2.completion === "start" && v.path === void 0) throw optionError("--completion start requires device --path");
|
|
@@ -8390,6 +8455,7 @@ function parseCli(args) {
|
|
|
8390
8455
|
if (v.path !== void 0) throw optionError(`files ${subcommand} uses a positional device path`);
|
|
8391
8456
|
options2.path = pathPositional;
|
|
8392
8457
|
}
|
|
8458
|
+
devicePath(options2.path, { root: subcommand === "list" });
|
|
8393
8459
|
if (subcommand === "read") {
|
|
8394
8460
|
if (!v.output) throw optionError("files read requires --output LOCAL_FILE");
|
|
8395
8461
|
options2.maxBytes = parsePositiveInteger(v["max-bytes"] ?? String(MAX_FILE_OUTPUT), "--max-bytes", { max: MAX_FILE_OUTPUT });
|
|
@@ -8438,7 +8504,27 @@ async function passwordFromInput(options2) {
|
|
|
8438
8504
|
}
|
|
8439
8505
|
function errorInfo(failure3) {
|
|
8440
8506
|
if (!failure3) return void 0;
|
|
8441
|
-
|
|
8507
|
+
const info = { code: failure3.code ?? "COMMAND_FAILED", message: failure3.message ?? "Command failed" };
|
|
8508
|
+
if (failure3.code === "ROM_OPERATION_FAILED") {
|
|
8509
|
+
if (typeof failure3.operation === "string") info.operation = failure3.operation.slice(0, 64);
|
|
8510
|
+
if (typeof failure3.cause === "string") info.detail = failure3.cause.slice(0, 1024);
|
|
8511
|
+
}
|
|
8512
|
+
if (failure3.reset?.acknowledged === false && failure3.reset.outcome === "unknown") {
|
|
8513
|
+
info.reset = { acknowledged: false, outcome: "unknown" };
|
|
8514
|
+
}
|
|
8515
|
+
if (String(failure3.code).startsWith("ROM_") && typeof failure3.transition?.command?.attempted === "boolean") {
|
|
8516
|
+
const transition = failure3.transition;
|
|
8517
|
+
info.transition = {
|
|
8518
|
+
command: {
|
|
8519
|
+
attempted: transition.command.attempted,
|
|
8520
|
+
acknowledged: transition.command.acknowledged === true,
|
|
8521
|
+
outcome: ["unknown", "not_attempted"].includes(transition.command.outcome) ? transition.command.outcome : "unknown"
|
|
8522
|
+
},
|
|
8523
|
+
observed: transition.observed === "rom" ? "rom" : null,
|
|
8524
|
+
cleanup: typeof transition.cleanup?.ok === "boolean" ? { ok: transition.cleanup.ok } : null
|
|
8525
|
+
};
|
|
8526
|
+
}
|
|
8527
|
+
return info;
|
|
8442
8528
|
}
|
|
8443
8529
|
function attachCleanupFailure(primary, cleanupFailure) {
|
|
8444
8530
|
if (primary && cleanupFailure) {
|
|
@@ -8458,12 +8544,7 @@ function validatePlan(plan, planFile) {
|
|
|
8458
8544
|
if (typeof value !== "string" || value.length === 0 || value.includes("\0")) throw planError("Batch local paths must be nonempty strings");
|
|
8459
8545
|
return resolve(base, value);
|
|
8460
8546
|
};
|
|
8461
|
-
const
|
|
8462
|
-
if (typeof value !== "string" || !root && value === "/" || !value.startsWith("/") || value.length > 123 || /[\x00-\x1f\x7f\\"]/.test(value) || value.includes("..") || value.includes("//") || value.length > 1 && value.endsWith("/")) {
|
|
8463
|
-
throw planError("Batch device paths must be absolute valid device paths");
|
|
8464
|
-
}
|
|
8465
|
-
return value;
|
|
8466
|
-
};
|
|
8547
|
+
const planDevicePath = (value, options2 = {}) => devicePath(value, { ...options2, fail: planError });
|
|
8467
8548
|
return plan.operations.map((operation, index) => {
|
|
8468
8549
|
const fail2 = (message) => {
|
|
8469
8550
|
throw planError(`Batch operation ${index + 1}: ${message}`);
|
|
@@ -8484,27 +8565,27 @@ function validatePlan(plan, planFile) {
|
|
|
8484
8565
|
const forms = ["source", "file", "path"].filter((key) => Object.hasOwn(operation, key));
|
|
8485
8566
|
if (forms.length !== 1) fail2("python requires exactly one of source, file or path");
|
|
8486
8567
|
if (operation.source !== void 0 && typeof operation.source !== "string") fail2("source must be a string");
|
|
8487
|
-
if (operation.path !== void 0) copy.path =
|
|
8568
|
+
if (operation.path !== void 0) copy.path = planDevicePath(operation.path);
|
|
8488
8569
|
if (operation.file !== void 0) copy.file = local(operation.file);
|
|
8489
8570
|
} else if (operation.op === "files.list") {
|
|
8490
|
-
copy.path =
|
|
8571
|
+
copy.path = planDevicePath(operation.path ?? "/", { root: true });
|
|
8491
8572
|
if (operation.recursive !== void 0 && typeof operation.recursive !== "boolean") fail2("recursive must be boolean");
|
|
8492
8573
|
} else if (operation.op === "files.read") {
|
|
8493
|
-
copy.path =
|
|
8574
|
+
copy.path = planDevicePath(operation.path);
|
|
8494
8575
|
if (operation.output === void 0) fail2("files.read requires output");
|
|
8495
8576
|
copy.output = local(operation.output);
|
|
8496
8577
|
copy.maxBytes = operation.maxBytes ?? MAX_FILE_OUTPUT;
|
|
8497
8578
|
if (!Number.isSafeInteger(copy.maxBytes) || copy.maxBytes < 1 || copy.maxBytes > MAX_FILE_OUTPUT) fail2("maxBytes must be 1..16 MiB");
|
|
8498
8579
|
} else if (operation.op === "files.write") {
|
|
8499
|
-
copy.path =
|
|
8580
|
+
copy.path = planDevicePath(operation.path);
|
|
8500
8581
|
if (operation.file === void 0) fail2("files.write requires file");
|
|
8501
8582
|
copy.file = local(operation.file);
|
|
8502
8583
|
if (operation.overwrite !== void 0 && typeof operation.overwrite !== "boolean") fail2("overwrite must be boolean");
|
|
8503
8584
|
} else if (operation.op === "files.remove") {
|
|
8504
|
-
copy.path =
|
|
8585
|
+
copy.path = planDevicePath(operation.path);
|
|
8505
8586
|
if (operation.type !== void 0 && !["file", "directory"].includes(operation.type)) fail2("type must be file or directory");
|
|
8506
8587
|
} else if (operation.op === "files.mkdir") {
|
|
8507
|
-
copy.path =
|
|
8588
|
+
copy.path = planDevicePath(operation.path);
|
|
8508
8589
|
if (operation.recursive !== void 0 && typeof operation.recursive !== "boolean") fail2("recursive must be boolean");
|
|
8509
8590
|
} else if (operation.op === "capture") {
|
|
8510
8591
|
if (!["camera", "screen"].includes(operation.source)) fail2("capture source must be camera or screen");
|
|
@@ -8742,6 +8823,24 @@ function createWatchObserver(device, options2, dependencies) {
|
|
|
8742
8823
|
}
|
|
8743
8824
|
};
|
|
8744
8825
|
}
|
|
8826
|
+
async function cliInventory(options2, dependencies) {
|
|
8827
|
+
let serialFailure;
|
|
8828
|
+
const listing = await inventory(options2, {
|
|
8829
|
+
...dependencies,
|
|
8830
|
+
async listSerial(request) {
|
|
8831
|
+
try {
|
|
8832
|
+
return await (dependencies.listSerial ?? listSerial)(request);
|
|
8833
|
+
} catch (failure3) {
|
|
8834
|
+
serialFailure = await explainSerialFailure(failure3, options2, dependencies);
|
|
8835
|
+
throw serialFailure;
|
|
8836
|
+
}
|
|
8837
|
+
}
|
|
8838
|
+
});
|
|
8839
|
+
if (serialFailure) for (const warning of listing.warnings) {
|
|
8840
|
+
if (warning.source === "serial") warning.message = serialFailure.message;
|
|
8841
|
+
}
|
|
8842
|
+
return listing;
|
|
8843
|
+
}
|
|
8745
8844
|
async function runCli(options2, dependencies = {}) {
|
|
8746
8845
|
if (options2.help) return { help: usage };
|
|
8747
8846
|
const startedAt = Date.now();
|
|
@@ -8756,13 +8855,13 @@ async function runCli(options2, dependencies = {}) {
|
|
|
8756
8855
|
}
|
|
8757
8856
|
}
|
|
8758
8857
|
if (options2.command === "inspect" && !endpoint && !options2.select) {
|
|
8759
|
-
const listing = await (dependencies.inventory ??
|
|
8760
|
-
if (listing.sourcesAvailable === 0) throw error("ENUMERATION_UNAVAILABLE",
|
|
8858
|
+
const listing = await (dependencies.inventory ?? cliInventory)({ ...options2, signal: dependencies.signal, timeout: Math.min(options2.timeout, 1e4) }, dependencies);
|
|
8859
|
+
if (listing.sourcesAvailable === 0) throw error("ENUMERATION_UNAVAILABLE", `No inventory source succeeded. ${listing.warnings.map((warning) => warning.message).join(" ")}`);
|
|
8761
8860
|
emit(dependencies, { event: "duration", milliseconds: Date.now() - startedAt });
|
|
8762
8861
|
return listing;
|
|
8763
8862
|
}
|
|
8764
8863
|
if (options2.command === "inspect" && !endpoint && options2.select) {
|
|
8765
|
-
const listing = await (dependencies.inventory ??
|
|
8864
|
+
const listing = await (dependencies.inventory ?? cliInventory)({ ...options2, signal: dependencies.signal, timeout: Math.min(options2.timeout, 1e4) }, dependencies);
|
|
8766
8865
|
endpoint = listing.devices.find((row) => row.number === options2.select);
|
|
8767
8866
|
if (!endpoint) {
|
|
8768
8867
|
emit(dependencies, { event: "duration", milliseconds: Date.now() - startedAt });
|
|
@@ -8845,6 +8944,9 @@ async function runCli(options2, dependencies = {}) {
|
|
|
8845
8944
|
primary = attachCleanupFailure(primary, cleanupFailure);
|
|
8846
8945
|
}
|
|
8847
8946
|
emit(dependencies, { event: "state", state: "disconnected" });
|
|
8947
|
+
if (primary && endpoint?.interface !== "tcp" && (endpoint || options2.port)) {
|
|
8948
|
+
primary = await explainSerialFailure(primary, options2, dependencies);
|
|
8949
|
+
}
|
|
8848
8950
|
const durationMs = Date.now() - startedAt;
|
|
8849
8951
|
emit(dependencies, { event: "duration", milliseconds: durationMs });
|
|
8850
8952
|
if (primary) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@esp-vision/device-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Standalone command line interface for ESP-VISION devices.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"cli"
|
|
37
37
|
],
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@esp-vision/device-core": "0.5.
|
|
39
|
+
"@esp-vision/device-core": "~0.5.0",
|
|
40
40
|
"esbuild": "0.25.12",
|
|
41
41
|
"multicast-dns": "7.2.5",
|
|
42
42
|
"string-width": "7.2.0"
|
|
@@ -45,5 +45,12 @@
|
|
|
45
45
|
"type": "git",
|
|
46
46
|
"url": "git+https://github.com/espressif/esp-vision.git",
|
|
47
47
|
"directory": "host/device-cli"
|
|
48
|
+
},
|
|
49
|
+
"espVisionReleaseType": "patch",
|
|
50
|
+
"espVision": {
|
|
51
|
+
"deviceCore": "~0.5.0",
|
|
52
|
+
"resolved": {
|
|
53
|
+
"deviceCore": "0.5.1"
|
|
54
|
+
}
|
|
48
55
|
}
|
|
49
56
|
}
|