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.
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 --versionAlternatively, install Patchcord into the active Python environment with pip:
python -m pip install patchcord
patchcord --versionTo try the unreleased development version instead:
uv tool install git+https://github.com/totocaster/patchcord.gitTo run Patchcord from a source checkout instead:
git clone https://github.com/totocaster/patchcord.git
cd patchcord
uv sync --locked
uv run patchcord --helpThe 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.
Connect a CircuitPython board, then initialize a project:
patchcord init my-board
cd my-boardPatchcord 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-boardUse 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 --offlineEdit device/code.py, then deploy and watch its output:
patchcord deploy
patchcord monitor --seconds 10That is the core Patchcord loop: edit locally, validate, deploy, and inspect the board's output.
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
Run offline validation before touching the board:
patchcord hardware validate --offlineThis 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.
patchcord deployBefore 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 15Deployment 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-settingsAn 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.
Stream until you press Ctrl-C:
patchcord monitorOr capture a bounded window:
patchcord monitor --seconds 10All 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.txtRead previous sessions without opening the serial port:
patchcord logs
patchcord logs --tail 50
patchcord logs --since 10mpatchcord logs shows the last 200 lines by default. Durations accept seconds,
minutes, hours, or days, such as 30s, 10m, 2h, and 1d.
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 \
statusGlobal 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.
Stop the running CircuitPython program and capture its console response:
patchcord interruptSoft-reset the board and capture startup:
patchcord reset
patchcord reset --capture 10Open an interactive serial REPL:
patchcord replInteractive 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 i2cIn 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.
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 installInstall one or more named bundle packages:
patchcord libs install adafruit_requests adafruit_ssd1306Or let circup inspect code.py currently deployed on the board:
patchcord libs install --autoNamed 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 freezeThis 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-unsupportedPatchcord 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 --pyThis 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.
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.SCLEvery 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 --jsonWithout --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.
Most bounded commands accept --json:
patchcord status --json
patchcord deploy --json
patchcord reset --json
patchcord logs --tail 50 --json
patchcord hardware validate --offline --jsonJSON mode writes one result document to stdout with stable result and error
codes. monitor --json must be bounded with --seconds:
patchcord monitor --seconds 10 --jsonInteractive patchcord repl does not support JSON output.
doctor performs read-only checks for project files, connected drives and
serial ports, permissions, installed dependencies, and available capabilities:
patchcord doctor
patchcord doctor --jsonUseful 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.
- Patchcord v0.3 CLI and behavior contract
hardware.yamlv1 formatcircremote0.12.0 acceptance result- Mounted-filesystem deployment decision
uv sync --locked
uv run pytest
uv run ruff check .
uv run ruff format --check .
uv run basedpyright
uv build --no-sourcesBug reports and contributions are welcome through GitHub Issues.
Patchcord is released under the MIT License.