Agent-facing control plane for CircuitPython boards
Project description
Patchcord
Patchcord is a command-line workflow for developing CircuitPython projects without a dedicated IDE. It keeps your application, library list, and hardware description in a normal Git repository, then gives you one CLI for deploying to a board, watching serial output, controlling execution, and validating the project's hardware description.
Patchcord currently supports CircuitPython boards that expose the standard
CIRCUITPY USB drive and serial console.
Install
Patchcord requires:
- Python 3.11 or newer;
- a board with CircuitPython already installed.
Install the latest stable version from PyPI with uv 0.11.32 or newer:
uv tool install patchcord
patchcord --version
Alternatively, install Patchcord into the active Python environment with pip:
python -m pip install patchcord
patchcord --version
To try the unreleased development version instead:
uv tool install git+https://github.com/totocaster/patchcord.git
To run Patchcord from a source checkout instead:
git clone https://github.com/totocaster/patchcord.git
cd patchcord
uv sync --locked
uv run patchcord --help
The examples below assume patchcord is installed as a tool. From a source
checkout, replace it with uv run patchcord.
Use patchcord --help or patchcord COMMAND --help to inspect the available
commands and options.
Quick start
Connect a CircuitPython board, then initialize a project:
patchcord init my-board
cd my-board
Patchcord creates:
my-board/
├── AGENTS.md # Patchcord and project workflow for coding agents
├── device/
│ └── code.py # Application files deployed to CIRCUITPY
├── hardware.yaml # Intended board, parts, nets, and interfaces
├── requirements.txt # CircuitPython library bundle names
└── .gitignore
If discovery is unambiguous, patchcord init records the connected board ID in
hardware.yaml. Otherwise, fill in board.id before deploying.
CircuitPython releases from before the Board ID: line was added to
boot_out.txt cannot publish that exact identity to Patchcord. Patchcord never
guesses an ID from a product name or USB number. After independently verifying
the official ID, attach it to an explicit drive for initialization:
patchcord \
--mount /Volumes/CIRCUITPY \
--legacy-board-id pyportal_titano \
init my-board
Use the same global options for later drive operations. A legacy assertion cannot replace a different ID published by newer firmware.
AGENTS.md teaches coding agents how to operate Patchcord, work safely with the
initialized project, and use small Git checkpoints as a durable backup and
restore history for hardware iterations. The board remains a deployment target;
the Git-managed local project is the source of truth. As with other initialized
project files, Patchcord preserves an existing AGENTS.md instead of
overwriting it.
Project commands can be run from the project root or any directory below it;
Patchcord finds the nearest project automatically. Runtime locks and logs are
created under the Git-ignored .patchcord/ directory when needed.
Put the project under Git so the local device/ tree remains your recoverable
source of truth:
git init
git add .
git commit -m "Initialize CircuitPython project"
Check the selected board and validate the project:
patchcord status
patchcord hardware validate --offline
Edit device/code.py, then deploy and watch its output:
patchcord deploy
patchcord monitor --seconds 10
That is the core Patchcord loop: edit locally, validate, deploy, and inspect the board's output.
The everyday development workflow
1. Edit locally
Keep every file that should appear on CIRCUITPY under device/. Patchcord
copies regular files from this tree and creates their parent directories; it
does not use the board as the source of truth.
device/
├── code.py
├── helpers.py
└── assets/
└── config.json
2. Validate the project
Run offline validation before touching the board:
patchcord hardware validate --offline
This checks hardware.yaml, endpoint references, I²C declarations, and library
cross-references against requirements.txt. The --offline flag guarantees
that Patchcord will not connect to or interrupt hardware.
3. Deploy
patchcord deploy
Before copying, Patchcord requires:
- a valid
hardware.yaml; - a selected
CIRCUITPYdrive and serial port; - an exact match between
hardware.yaml'sboard.idand the selected drive's published ID or explicit legacy assertion; and - a
device/code.pyentry point.
Deployment interrupts the running program, copies support files before
code.py, soft-resets the board, and captures five seconds of startup output.
It returns a failure if startup contains an uncaught traceback.
Change the capture duration when the program needs longer to start:
patchcord deploy --capture 15
Deployment creates or updates files but never deletes unrelated files already
on the board. Root boot.py and settings.toml require explicit permission:
patchcord deploy --allow-boot
patchcord deploy --allow-settings
An allowed settings.toml is copied opaquely; Patchcord does not display or
publish a digest of its contents. device/settings.toml is ignored by Git when
Patchcord initializes a project.
4. Observe and repeat
Stream until you press Ctrl-C:
patchcord monitor
Or capture a bounded window:
patchcord monitor --seconds 10
All monitor sessions are appended to .patchcord/logs/serial.log. To append the
raw board output to another file as well:
patchcord monitor --seconds 30 --output startup.txt
Read previous sessions without opening the serial port:
patchcord logs
patchcord logs --tail 50
patchcord logs --since 10m
patchcord logs shows the last 200 lines by default. Durations accept seconds,
minutes, hours, or days, such as 30s, 10m, 2h, and 1d.
Selecting a board
Patchcord selects a drive or serial port automatically only when that target component is unambiguous. Use global overrides when multiple boards are attached:
patchcord \
--mount /Volumes/CIRCUITPY \
--port /dev/cu.usbmodem101 \
status
Global options go before the command name. The same pattern works with
deploy, monitor, reset, repl, libs, and hardware validate.
Patchcord v0.3 selects the drive and serial port independently; it cannot prove that two explicit overrides belong to the same physical board. When overriding both, make sure they identify the same device.
Old CircuitPython firmware may expose a recognizable drive and serial port but
omit the exact Board ID: field. After verifying the ID against an
authoritative CircuitPython or board-vendor source, use the deliberately
explicit legacy assertion:
patchcord \
--mount /Volumes/CIRCUITPY \
--port /dev/cu.usbmodem101 \
--legacy-board-id pyportal_titano \
status
--legacy-board-id requires --mount, is reported by status with
board_id_source: legacy_override, and never overrides a conflicting ID from
boot_out.txt. The drive must still contain a readable, parseable CircuitPython
boot banner. It is an operator assertion for legacy firmware, not automatic
board detection.
Serial control and REPL
Stop the running CircuitPython program and capture its console response:
patchcord interrupt
Soft-reset the board and capture startup:
patchcord reset
patchcord reset --capture 10
Open an interactive serial REPL:
patchcord repl
Interactive mode uses pyserial's miniterm and does not save a transcript. Use
monitor --output when you need a separate raw output file.
Patchcord also defines bounded execution and probe commands:
patchcord repl --eval "print('hello')"
patchcord repl --file tools/check_sensor.py
patchcord probe pins
patchcord probe i2c
In the current v0.3 release, these bounded commands are unavailable because the
pinned execution backend did not pass Patchcord's isolation and reset checks.
They return execution_backend_unavailable. Interactive repl, deployment,
monitoring, serial control, library management, and offline hardware validation
remain available.
Managing CircuitPython libraries
List the project's CircuitPython bundle libraries in requirements.txt, one
per line:
adafruit_bus_device
adafruit_requests
adafruit_ssd1306
Install the complete file on the selected board:
patchcord libs install
Install one or more named bundle packages:
patchcord libs install adafruit_requests adafruit_ssd1306
Or let circup inspect code.py currently deployed on the board:
patchcord libs install --auto
Named packages and --auto are mutually exclusive. --auto examines the
board's deployed code, not the local device/ tree. A named install does not
edit requirements.txt; update the file yourself or freeze the board afterward
to make the environment repeatable.
Record the board's installed libraries back into the project:
patchcord libs freeze
This validates circup's generated requirements and atomically replaces
requirements.txt.
Circup rejects CircuitPython releases outside its supported bundle window by default. If retaining old firmware is intentional, explicitly opt into circup's documented compatibility escape hatch:
patchcord libs install --allow-unsupported
patchcord libs freeze --allow-unsupported
Patchcord forwards the selected board ID and CircuitPython version to circup.
--allow-unsupported does not make current libraries compatible with old
firmware. Very old firmware may also lack matching compiled .mpy bundle
artifacts. In that case, Circup's explicit --py option installs source
libraries instead:
patchcord libs install --allow-unsupported --py
This selects the source transport format, not historically compatible library
versions. Patchcord v0.3 does not expose Circup's historical bundle-snapshot
selection, so a non-empty install onto very old firmware may remain unusable
even with both flags. freeze and an empty-requirements install can still be
used for inspection and smoke testing without replacing board libraries.
Describing and validating hardware
hardware.yaml records the intended board and the project-specific parts and
connections around it. A minimal document looks like this:
schema_version: 1
board:
id: adafruit_feather_rp2040
parts: {}
nets: {}
A part can declare pins, an expected I²C address, and libraries:
parts:
oled:
kind: display
model: SSD1306 OLED
pins:
SDA:
role: i2c_sda
SCL:
role: i2c_scl
interfaces:
- kind: i2c
bus: default
address: 0x3c
libraries:
- adafruit_ssd1306
nets:
i2c_sda:
role: signal
endpoints:
- board.SDA
- oled.SDA
i2c_scl:
role: signal
endpoints:
- board.SCL
- oled.SCL
Every library named by a part must also be present in requirements.txt.
Use offline validation during editing and in CI:
patchcord hardware validate --offline
patchcord hardware validate --offline --json
Without --offline, Patchcord also attempts connected pin and I²C checks when a
drive and serial port are selected. Connected validation interrupts and resets
the board. It is currently blocked by the bounded-execution limitation
described above.
The complete format is documented in the
hardware.yaml v1 specification.
JSON output
Most bounded commands accept --json:
patchcord status --json
patchcord deploy --json
patchcord reset --json
patchcord logs --tail 50 --json
patchcord hardware validate --offline --json
JSON mode writes one result document to stdout with stable result and error
codes. monitor --json must be bounded with --seconds:
patchcord monitor --seconds 10 --json
Interactive patchcord repl does not support JSON output.
Diagnose a setup
doctor performs read-only checks for project files, connected drives and
serial ports, permissions, installed dependencies, and available capabilities:
patchcord doctor
patchcord doctor --json
Useful first checks when a command cannot find or open a board are:
- Confirm that the
CIRCUITPYdrive is mounted. - Confirm that the board's serial device is present.
- Run
patchcord doctor. - Pass explicit
--mountand--portvalues if discovery is ambiguous. - Check the operating system's drive and serial-port permissions.
Patchcord reports missing permissions or drivers but does not install or change them automatically.
Further documentation
- Patchcord v0.3 CLI and behavior contract
hardware.yamlv1 formatcircremote0.12.0 acceptance result- Mounted-filesystem deployment decision
Development
uv sync --locked
uv run pytest
uv run ruff check .
uv run ruff format --check .
uv run basedpyright
uv build --no-sources
Bug reports and contributions are welcome through GitHub Issues.
Patchcord is released under the MIT License.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file patchcord-0.3.0.tar.gz.
File metadata
- Download URL: patchcord-0.3.0.tar.gz
- Upload date:
- Size: 73.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e96a103e5c0efad6b4d53af845feaace6c67c9ad8554f172355e8fc86852ef20
|
|
| MD5 |
65033763547dd802ba083fb9cfa1b37c
|
|
| BLAKE2b-256 |
d059e0a694ee243c9c0989c70d41fd1b4bbbfc08034045fbd0426945edfaf731
|
Provenance
The following attestation bundles were made for patchcord-0.3.0.tar.gz:
Publisher:
release.yml on totocaster/patchcord
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
patchcord-0.3.0.tar.gz -
Subject digest:
e96a103e5c0efad6b4d53af845feaace6c67c9ad8554f172355e8fc86852ef20 - Sigstore transparency entry: 2256958946
- Sigstore integration time:
-
Permalink:
totocaster/patchcord@2739addf518af51fc717bf86bbd1c462149ee16e -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/totocaster
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@2739addf518af51fc717bf86bbd1c462149ee16e -
Trigger Event:
push
-
Statement type:
File details
Details for the file patchcord-0.3.0-py3-none-any.whl.
File metadata
- Download URL: patchcord-0.3.0-py3-none-any.whl
- Upload date:
- Size: 66.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9a689b860409144ee7e3620e1cf5148d008b7179735ae95709f5ea254bea22c0
|
|
| MD5 |
3189089d9e30c4b8f51161bfc96d3a3c
|
|
| BLAKE2b-256 |
8786e12b39844061540323bc0ea091b95648f888279df9c91e77e195d86a7961
|
Provenance
The following attestation bundles were made for patchcord-0.3.0-py3-none-any.whl:
Publisher:
release.yml on totocaster/patchcord
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
patchcord-0.3.0-py3-none-any.whl -
Subject digest:
9a689b860409144ee7e3620e1cf5148d008b7179735ae95709f5ea254bea22c0 - Sigstore transparency entry: 2256958956
- Sigstore integration time:
-
Permalink:
totocaster/patchcord@2739addf518af51fc717bf86bbd1c462149ee16e -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/totocaster
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@2739addf518af51fc717bf86bbd1c462149ee16e -
Trigger Event:
push
-
Statement type: