Skip to main content

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 CIRCUITPY drive and serial port;
  • an exact match between hardware.yaml's board.id and the selected drive's published ID or explicit legacy assertion; and
  • a device/code.py entry 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:

  1. Confirm that the CIRCUITPY drive is mounted.
  2. Confirm that the board's serial device is present.
  3. Run patchcord doctor.
  4. Pass explicit --mount and --port values if discovery is ambiguous.
  5. 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

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.

Metadata

Release files for patchcord 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for patchcord 0.3.0
File Size Uploaded Provenance Actions
patchcord-0.3.0.tar.gz 73.2 kB With provenance Details for patchcord-0.3.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for patchcord 0.3.0
File Interpreter ABI Platform Actions
patchcord-0.3.0-py3-none-any.whl Python 3 none any Details for patchcord-0.3.0-py3-none-any.whl

Total release size: 139.7 kB

Release files / patchcord-0.3.0.tar.gz

Download URL patchcord-0.3.0.tar.gz
Size 73.2 kB
Tags Source
SHA-256 checksum
How to use checksums
e96a103e5c0efad6b4d53af845feaace6c67c9ad8554f172355e8fc86852ef20
BLAKE2b-256 checksum
How to use checksums
d059e0a694ee243c9c0989c70d41fd1b4bbbfc08034045fbd0426945edfaf731
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 27, 2026.

Transparency log

Release files / patchcord-0.3.0-py3-none-any.whl

Download URL patchcord-0.3.0-py3-none-any.whl
Size 66.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9a689b860409144ee7e3620e1cf5148d008b7179735ae95709f5ea254bea22c0
BLAKE2b-256 checksum
How to use checksums
8786e12b39844061540323bc0ea091b95648f888279df9c91e77e195d86a7961
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 27, 2026.

Transparency log

Security for patchcord 0.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for patchcord-0.3.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 27, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page