Skip to main content

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 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.2 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.2 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.2 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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

patchcord-0.2.0.tar.gz (72.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

patchcord-0.2.0-py3-none-any.whl (65.8 kB view details)

Uploaded Python 3

File details

Details for the file patchcord-0.2.0.tar.gz.

File metadata

  • Download URL: patchcord-0.2.0.tar.gz
  • Upload date:
  • Size: 72.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for patchcord-0.2.0.tar.gz
Algorithm Hash digest
SHA256 de2a3491f34cfb901932862f1c7bd7140cc5e119e57f529e57383503d4365e98
MD5 afd7766c3e84bbdbd15493e341493842
BLAKE2b-256 b6d4e0808315d5c19a1ee8ccf35b1907e06d9f8a526092a60bf41055ccfe42c3

See more details on using hashes here.

Provenance

The following attestation bundles were made for patchcord-0.2.0.tar.gz:

Publisher: release.yml on totocaster/patchcord

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file patchcord-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: patchcord-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 65.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for patchcord-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 61ecaf0d37bad0ca68d33fc7aecf5f14250aba5d70e93f5560fb4191ee7d68a4
MD5 908d73db39edd9e4cdafff675b65851b
BLAKE2b-256 25ed03a48d07f204bb77ab42b7dad708decd15020148f342fbf21ee3187798e0

See more details on using hashes here.

Provenance

The following attestation bundles were made for patchcord-0.2.0-py3-none-any.whl:

Publisher: release.yml on totocaster/patchcord

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page