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
24 changes: 12 additions & 12 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,22 @@
mpftp is a TypeScript VS Code extension, a Python package that holds the
serial session, the firmware engine and the command line, and a browser app.
A change to behavior usually lands in the Python package first. A unit test in
`cli/tests/` or a documentation fix is a good first contribution. How the parts
[`cli/tests/`](cli/tests/) or a documentation fix is a good first contribution. How the parts
fit together is in [How mpftp works](docs/how-it-works/index.md).

## Where things are

| Path | What it holds |
|---|---|
| `extension/src/` | The extension host: bridge, panels, terminal, firmware panel and the session server for the command line |
| `extension/media/` | Webview HTML, JS and CSS. `ftp.html`, `ftp.js` and `ftp.css` are the File Transfer view |
| [`extension/src/`](extension/src/) | The extension host: bridge, panels, terminal, firmware panel and the session server for the command line |
| [`extension/media/`](extension/media/) | Webview HTML, JS and CSS. `ftp.html`, `ftp.js` and `ftp.css` are the File Transfer view |
| `extension/resources/templates/` | Workspace stub `micropython.cmake` and `manifest-micropython.py` |
| `extension/python/mpftp/` | A vendored copy of `cli/src/mpftp/`, staged by `scripts/stage-vendored-python.sh` so `vsce` can package it. Never edit it; edit `cli/src/mpftp/` |
| `extension/python/mpftp/` | A vendored copy of [`cli/src/mpftp/`](cli/src/mpftp/), staged by [`scripts/stage-vendored-python.sh`](scripts/stage-vendored-python.sh) so `vsce` can package it. Never edit it; edit `cli/src/mpftp/` |
| `cli/src/mpftp/` | The `pydevices-mpftp` package: `sidecar.py` (serial session), `cli.py`, `firmware.py` (discover, build, flash, detect), `firmware_build.py` (runs `build_mp.py`), `firmware_download.py` (the official catalog), `pwa.py` (the local web app), `webrepl.py`, `ble.py`, `hold.py`, `config.py`, `uf2.py` |
| `cli/src/mpftp/webui/` | The built browser app. It is committed, not ignored: the release pipeline only runs `python -m build .` with no Node step, so it has to be current on `main`. CI's `ui` job rebuilds it and runs `git diff --exit-code`, so a stale commit fails CI |
| [`cli/src/mpftp/webui/`](cli/src/mpftp/webui/) | The built browser app. It is committed, not ignored: the release pipeline only runs `python -m build .` with no Node step, so it has to be current on `main`. CI's `ui` job rebuilds it and runs `git diff --exit-code`, so a stale commit fails CI |
| `cli/tests/` | The Python tests (`unittest`) |
| `ui/` | The browser app's source: TypeScript with esbuild, `@xterm/xterm` and CodeMirror 6. Its File Transfer panel is the extension's own, copied in by `npm run build`, which stages everything into `cli/src/mpftp/webui/`. `src/host.ts` gives `ftp.js` an `acquireVsCodeApi()` that talks to `cli/src/mpftp/panel.py`, the Python port of the extension's message handling, so a change to one host's behavior belongs in the other too |
| `desktop/` | The desktop app, a Tauri 2 window around the browser app that starts a bundled Python running `cli/src/mpftp/desktop_host.py`. `emulator/` is the simulated board, reached as `emu://`. See [desktop/README.md](desktop/README.md) |
| `ui/` | The browser app's source: TypeScript with esbuild, `@xterm/xterm` and CodeMirror 6. Its File Transfer panel is the extension's own, copied in by `npm run build`, which stages everything into `cli/src/mpftp/webui/`. `src/host.ts` gives `ftp.js` an `acquireVsCodeApi()` that talks to [`cli/src/mpftp/panel.py`](cli/src/mpftp/panel.py), the Python port of the extension's message handling, so a change to one host's behavior belongs in the other too |
| `desktop/` | The desktop app, a Tauri 2 window around the browser app that starts a bundled Python running [`cli/src/mpftp/desktop_host.py`](cli/src/mpftp/desktop_host.py). `emulator/` is the simulated board, reached as `emu://`. See [desktop/README.md](desktop/README.md) |
| `tools/` | `test_cli_workflows.py` (a workflow test that needs an attached board), `ble_bench.py` and `ble_gate.py` (Bluetooth), and `windows/` (the USB-restart task) |
| `scripts/` | The `mpftp` launcher, version and release scripts, `install-cursor-wsl.sh`, `stage-vendored-python.sh` |
| `integrations/` | The Claude Code plugin: a skill that teaches agents the CLI |
Expand All @@ -45,7 +45,7 @@ npm run package # VSIX via @vscode/vsce
boards; it skips flashing unless you pass `--install-latest-firmware`.

On native Linux, serial needs nothing extra: `npm run package` bundles mpremote
and pyserial (the hashed pins in `extension/vendor-requirements.txt`) into
and pyserial (the hashed pins in [`extension/vendor-requirements.txt`](extension/vendor-requirements.txt)) into
`extension/python/_vendor`, and the launcher puts them on the session's path.
For a WSL window that runs the extension remotely:

Expand Down Expand Up @@ -113,7 +113,7 @@ tag-release.yml (push to main touching VERSION)
```

`VERSION` is the single source of truth, not the tag. Every other version site
is committed equal to it, and `scripts/check_versions.py` enforces that in CI on
is committed equal to it, and [`scripts/check_versions.py`](scripts/check_versions.py) enforces that in CI on
every pull request. Both publish workflows only confirm the tag they were
triggered from agrees with the `VERSION` on the commit they checked out.

Expand Down Expand Up @@ -150,10 +150,10 @@ gh pr list --state open # the bot's "Release X.Y.Z" PR
The bot opens the PR in two steps. It sets `VERSION` and prepends a commit list
to `CHANGELOG.md`, then a second commit runs
`python scripts/check_versions.py --write`, which sets every other version site
from `VERSION` in the form each takes: `extension/package.json`,
`ui/package.json`, the root entry of both `package-lock.json` files,
from `VERSION` in the form each takes: [`extension/package.json`](extension/package.json),
[`ui/package.json`](ui/package.json), the root entry of both `package-lock.json` files,
`.claude-plugin/marketplace.json` and
`integrations/claude-code-plugin/.claude-plugin/plugin.json`. If the second
[`integrations/claude-code-plugin/.claude-plugin/plugin.json`](integrations/claude-code-plugin/.claude-plugin/plugin.json). If the second
commit is missing (the workflow's `sync-versions` job failed), run that command
on the `release/vX.Y.Z` branch and push.

Expand Down
14 changes: 7 additions & 7 deletions desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,24 +39,24 @@ window and `host.log` for the Python side.

## How it fits together

The window is a [Tauri 2](https://v2.tauri.app/) app, `src-tauri/src/main.rs`.
The window is a [Tauri 2](https://v2.tauri.app/) app, [`src-tauri/src/main.rs`](src-tauri/src/main.rs).
It shows the same page `python -m mpftp` serves in a browser
(`cli/src/mpftp/webui/`, built from `ui/`), and starts the bundled Python
([`cli/src/mpftp/webui/`](../cli/src/mpftp/webui/), built from `ui/`), and starts the bundled Python
running `python -m mpftp.desktop_host` beside it. The page and the host talk
through the app over stdin and stdout, not a web server, so no port is open.
`ui/src/rpc.ts` picks that route inside the app and the WebSocket in a
[`ui/src/rpc.ts`](../ui/src/rpc.ts) picks that route inside the app and the WebSocket in a
browser, which is the only difference between the two.

The host is restarted if it dies, and on Windows it runs in a Job Object, so
closing the app never leaves a Python behind.

The simulated board is `emulator/`: a window running the WebAssembly runtime,
which the host reaches as an `emu://` port (`cli/src/mpftp/emu.py`).
`emulator/node-emu.mjs` runs the same board core under Node, for trying the
which the host reaches as an `emu://` port ([`cli/src/mpftp/emu.py`](../cli/src/mpftp/emu.py)).
[`emulator/node-emu.mjs`](emulator/node-emu.mjs) runs the same board core under Node, for trying the
`emu://` path without the app.

Desktop's version is the repo's: `tauri.conf.json` reads it from
`package.json`, which `scripts/check_versions.py` keeps equal to the root
`package.json`, which [`scripts/check_versions.py`](../scripts/check_versions.py) keeps equal to the root
`VERSION` file with every other deliverable.

## Build it
Expand Down Expand Up @@ -109,5 +109,5 @@ npm run build:linux # the .deb

It lands in `src-tauri/target/release/bundle/deb/`.
`tauri.linux.conf.json` swaps the Windows Python for the Linux one, and
`scripts/smoke_linux.sh` starts a built package and checks that its Python
[`scripts/smoke_linux.sh`](scripts/smoke_linux.sh) starts a built package and checks that its Python
host comes up.
2 changes: 1 addition & 1 deletion docs/guides/drive-a-board-from-an-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Codex's Plugins panel reads the same plugin (**Plugins → Add → Add plugin
marketplace**, source `PyDevices/mpftp`). Other agents can read this page
directly, for example from your project's `AGENTS.md`. Install the CLI first
([Install](../get-started/install.md)); with a clone of the repository,
`./scripts/mpftp` stands in for `mpftp`.
[`./scripts/mpftp`](../../scripts/mpftp) stands in for `mpftp`.

## How commands find a session

Expand Down
4 changes: 2 additions & 2 deletions docs/guides/recover-a-board.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Two ways through:

That pops one UAC prompt and grants nothing that outlives it. The board goes
from the stale `303A:4001` node to `303A:1001`, and esptool can flash it with
no button press. `tools/windows/restart-esp-usb.ps1` in the repository does
no button press. [`tools/windows/restart-esp-usb.ps1`](../../tools/windows/restart-esp-usb.ps1) in the repository does
this for whatever Espressif board is attached. The order to try is
`machine.bootloader()` over `exec`, then the device-node restart, then flash
the `1001` port, and the physical BOOT-and-plug only when no one can click.
Expand Down Expand Up @@ -108,7 +108,7 @@ names the node after the chip's MAC address, colons included
`--instance` accepts it.

**One elevated install first.** The task needs a one-time install by someone
with administrator rights. Copy `tools/windows/` from the repository to a plain
with administrator rights. Copy [`tools/windows/`](https://github.com/PyDevices/mpftp/tree/main/tools/windows) from the repository to a plain
Windows path (an elevated shell can't reliably read `\\wsl.localhost\...`), then
run, in an administrator PowerShell:

Expand Down
10 changes: 5 additions & 5 deletions docs/how-it-works/wifi-and-bluetooth.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This page is why each link is built the way it is. To use them, see

## Wi-Fi (WebREPL)

`cli/src/mpftp/webrepl.py` holds a small WebSocket client written with the
[`cli/src/mpftp/webrepl.py`](../../cli/src/mpftp/webrepl.py) holds a small WebSocket client written with the
standard library, `WebSocketSerial`. It has the pyserial surface that mpremote's
`SerialTransport` uses (`read`, `write`, `inWaiting`, `timeout`), and a subclass
of `SerialTransport` is handed that object instead of a port. The only
Expand Down Expand Up @@ -63,14 +63,14 @@ session keeps it in memory for reconnects, and the activity log redacts it.

**Discovery.** Every connect reads the board's uid, hostname and, when Wi-Fi is up,
its IP. A `NAME.local` lookup asks the OS resolver first and then sends its own
mDNS query (`cli/src/mpftp/mdns.py`, standard library only); the lookup runs in
mDNS query ([`cli/src/mpftp/mdns.py`](../../cli/src/mpftp/mdns.py), standard library only); the lookup runs in
the session, which on WSL is the Windows Python.

## Bluetooth (bledev)

Windows owns the computer's radio, so the session reaches the board through
bleak in the Windows Python, the same one it uses for `COM` ports.
`cli/src/mpftp/ble.py` dresses the link as a pyserial port the way `webrepl.py`
[`cli/src/mpftp/ble.py`](../../cli/src/mpftp/ble.py) dresses the link as a pyserial port the way `webrepl.py`
does, and hands it to `SerialTransport`:

- **The REPL.** mpftp finds the board by name, connects, subscribes to the Nordic
Expand All @@ -97,7 +97,7 @@ does, and hands it to `SerialTransport`:

Measured for 20 KB transfers between Windows and an ESP32-S3 on MicroPython 1.30
preview, with mpremote 1.29: median of three runs, each checked byte for byte and
against the board's SHA-256 (`tools/ble_bench.py` runs the benchmark):
against the board's SHA-256 ([`tools/ble_bench.py`](../../tools/ble_bench.py) runs the benchmark):

| Path | Up | Down |
|---|---|---|
Expand All @@ -111,5 +111,5 @@ File transfer is about 14 times faster up and 31 times faster down than
mpremote's own raw-REPL transfers, and bigger chunks don't close the gap:
raw-paste waits a round trip for every window of input the board grants, and each
exec's output crosses the same link as text. The upload rate is set by the board,
which writes flash between windows. `tools/ble_gate.py` runs an end-to-end check
which writes flash between windows. [`tools/ble_gate.py`](../../tools/ble_gate.py) runs an end-to-end check
of connect, put, get, exec and interrupt in one session.
4 changes: 2 additions & 2 deletions integrations/claude-code-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,5 @@ app's Code panel, have the same flow in the GUI: Settings → Plugins →

| Path | Role |
|---|---|
| `.claude-plugin/plugin.json` | Plugin manifest; its version tracks the repository's `VERSION` (`scripts/check_versions.py` checks it) |
| `skills/board-tools/SKILL.md` | The skill |
| `.claude-plugin/plugin.json` | Plugin manifest; its version tracks the repository's `VERSION` ([`scripts/check_versions.py`](../../scripts/check_versions.py) checks it) |
| [`skills/board-tools/SKILL.md`](skills/board-tools/SKILL.md) | The skill |
Loading