Skip to content

Hardware tooling

Use root just aliases or the public Python CLI:

uv run --script scripts/hw.py --help
just --list
just test

scripts/hw.py owns the normal driver dependency metadata. Reusable code lives in scripts/espurr_hw/; _native.py is a private worker launched with compatible KICAD_PYTHON. Keep the KiCad and ordinary Python environments separate. Setup and executable overrides are in the README.

For ad hoc KiCad Python work, use the same worker through the public CLI:

just kicad-python build/my-script.py argument
just kicad-python -c 'import pcbnew; print(pcbnew.Version())'
just kicad-python - < build/my-script.py
# Equivalent driver command:
uv run --script scripts/hw.py python -- -m my_module argument

This is a batch runner, not an interactive Python REPL. Script, -c, -m and stdin execution preserve arguments, exceptions and exit status. It honours KICAD_PYTHON; it does not require KiKit or the other build tools.

On macOS, workers that imported pcbnew or wx flush stdout/stderr and call os._exit after the task returns or unwinds. This avoids an observed visit_decref / PyGC_Collect / Py_FinalizeEx shutdown SIGSEGV in the bundled Python 3.9.13 of KiCad 10.0.7 (macOS 27.0.1, checked 2026-10-26). It is a workaround for interpreter teardown, not a fix for native crashes during a task. Finish saves, file closures and other cleanup explicitly in the task; destructors and atexit hooks do not run on this path. Failed tasks keep their traceback and nonzero status. Linux and processes without those native modules exit normally.

Workers pass -ApplePersistenceIgnoreState YES -NSQuitAlwaysKeepsWindows NO per invocation. No system/user crash-reporting preference or shared org.python.python preference is changed, and KiCad's app bundle is untouched. Directly invoking the bundled interpreter bypasses this workaround.

Native PCB IPC

just ipc-python runs script, -c, -m or stdin code in the ordinary driver environment with pinned kicad-python==0.8.0 bindings and the repository modules:

just ipc-python build/my-ipc-script.py argument
just ipc-python -c 'from espurr_hw.ipc import PCBSession; print(PCBSession.__name__)'

Use espurr_hw.ipc.PCBSession for live board access. Every request from its board object is checked, including calls through retained board references. This runner is separate from just kicad-python, which uses the legacy bundled pcbnew/wx environment. The runner itself selects no default target. The default live PCB workflow on macOS is with PCBSession.launch(board_path=<absolute path>, hidden=True) as session:. Use one isolated owned editor per board, reusing the session within the task. It discovers and binds its socket and closes only that process on context exit. Use PCBSession(socket_path=..., board_path=...) only when the task specifically needs an existing live document; it attaches without owning the editor. Both retain the same document checks. Saves are explicit; owned shutdown discards unsaved changes. keep_open_on_error=True retains a launched editor on a body exception for inspection, followed by explicit session.close().

Owned launch requests hidden/background operation by default, but macOS focus and Space changes were observed despite hidden/inactive status. This is best effort GUI launch, not headless operation or a no-focus guarantee. Existing native board/project locks are refused rather than overridden.

For live PCB work, follow the verified native IPC workflow: one owned editor process per board, explicit socket/token binding, fresh full-path document checks, native transactions and independent saved readback. It includes macOS launch commands and the 2026-10-31 edit/save isolation results. The tested KiCad version does not enforce request project paths, so those client-side checks are required even when every API call supplies a document specifier.

Workflows

just mic-flex <fresh-build-output> validates/exports the separate 40mm common L/R microphone flex, including pib_0.225.gbr, BOM/CPL and source hashes. It uses kikit fab jlcpcb for the standard fabrication/assembly files, adds the rear PI artwork omitted by KiKit's stock layer list, and validates the final CAM. See its construction and supplier gates.

just mic-flex-panel <fresh-output> creates the 74 × 96mm, 8 × 2 bare-flex laser-tab panel on copies, with retained waste, copper rails, tooling and global and per-board fiducials. PI flex uses interrupted laser slits, with 0.8mm bare-flex and 1mm mic-support bridges. It uses KiKit for copying and fabrication exports, then checks pad/net and CAM parity. See the panel contract; PCBA is out of scope.

Examples use fresh output directories. Choose another name if one already exists.

just band-quote build/headband-quote-YYYYMMDD exports the saved joined rigid headband on copies, with refilled DRC/parity, ERC, native assembly/drill inventory, parsed CAM and matching BOM/CPL. See the quote guide for order settings, sourcing supplements and placement-preview requirements. It does not alter source CAD or export the separate ear/microphone flexes.

just band-panel build/headband-panel-YYYYMMDD runs KiKit panelize with a checked-in preset and tab annotations on input copies, then independently checks and exports a two-set Standard-PCBA carrier. It includes panel BOM/CPL, drill and copper comparisons, and preserves the source project. See the panel guide for retained inputs, expected warnings and supplier-preview requirements.

uv run --script scripts/hw.py check
uv run --script scripts/hw.py reports
uv run --script scripts/hw.py ear right --output build/ears/aw-right-next
just aw-ear-fab /path/to/com_github_mitxela_kicad-round-tracks build/ears/aw-fab-next
just test-aw-ear-fab build/ears/aw-fab-next
uv run --script scripts/hw.py ear coverlay
uv run --script scripts/hw.py ear rules
uv run --script scripts/hw.py ear moat-svg --output build/moat-next
uv run --script scripts/hw.py ear references
uv run --script scripts/hw.py mic build
uv run --script scripts/hw.py web build
uv run --script scripts/hw.py web compare build/web-first build/web
uv run --script scripts/hw.py ear render build/ears/aw-fab-next --output build/renders/ears-next
uv run --script scripts/hw.py render espurr/espurr-ears/espurr-ears.kicad_pcb --output build/renders/left-next --appearance white-flex

Common just aliases keep their established names/defaults: check, reports, ears, panel, ear-quote, rounded-panel, web and mic-breakout. right-ear and rounded-right-ear generate the current AW/B-only right-ear review and run its independent checks; see the routing record. aw-ear-fab <round-tracks-plugin> <fresh-output> produces the rounded AW-equipped working ears and rail-free left/right/pair CAM/BOM/CPL, with a measured pair-width limit below 109mm. Top-level manufacturing files retain removable blank moat infill on 0.8mm laser tabs; finished post-removal boards remain under variants/ for validation/rendering. See the moat cleanup. The older ear build/ear verify paths retain their LED-only artifact contract. ear-renders <complete-aw-build> <fresh-output> generates the eight raytraced ear views in the agreed band orientation. pcb-renders <saved-pcb> <fresh-output> generates four views of one board. Both default to high quality; optional recipe arguments are scale (default 1) and quality (high/basic). See raytraced render sets for output contracts, model limitations and tagged website gallery consumption.

Build output and cleanup

build/ is disposable working output, not a record of the current design. Keep the newest useful complete ear build and matching renders, active headband work, research/evidence still in use and installed tool environments. Move superseded trials, repeated smoke renders, obsolete pointer files and downloaded package/tree metadata out together; retain an inventory and original paths when using recoverable Trash. Do not rename complete builds internally: manifests and render inputs can contain absolute paths.

The local build/README.md indexes retained snapshots and the latest cleanup inventory. A retained successful manifest establishes its recorded snapshot, not freshness against today's source or tooling. Historical build/ paths in dated docs can disappear during cleanup; durable decisions belong in maintained docs, dated evidence in the archive and immutable upload inputs in manufacturing/orders/.

New project/library snapshots use espurr_hw.common.source_copy_ignore as the shutil.copytree(..., ignore=source_copy_ignore) callback. It shares the source stager's exclusions: .git (including worktree pointer files), .history, __pycache__, KiCad backups, autosaves, locks and local temporary files. Use it for ad hoc source snapshots too. This affects future copies only; retained snapshots, manifests and frozen reproducers are not pruned or rewritten.

A cleanup inventory certifies its recorded move and hashes, not indefinite Trash availability. Check the recorded destination before promising recovery; retain the original inventory and record later availability separately when it is absent.

just clean is a permanent whole-tree deletion, not this selective cleanup. It refuses symlinks, including tool-environment links; preserve manual work, research, recovery data and tool environments first. No age-based deletion is safe by default.

Verification and historical workflows

Ear verification automatically runs route checks for native builds and both quote/route checks for a full quote. --checks quote|routes|all selects explicitly. The existing test-ear-quote and test-ear-routes aliases remain available. The AW verifier compares its recorded hashes with the entire current source tree, including the headband. A retained ear build can therefore pass geometry checks yet fail source freshness after unrelated band work. Keep the original manifest and inspect that difference; do not rewrite hashes to bless an old build. Normal comparisons require matching source, tooling and part-policy hashes. ear compare --allow-tooling-change explicitly permits different tool hashes for refactor comparisons; source and part-policy matching still apply, and the report records the override. It is not a manufacturing waiver.

See the ear pipeline for stage/output contracts and hardware website for MkDocs/KiBot setup, release renders and CI. Retired historical commands are indexed in the archive.

Tests and preservation

source_hashes() and preserve_sources() protect KiCad designs, local libraries and tables, worksheets/jobsets and model geometry under espurr/ and breakouts/. README/prose edits, component caches, .DS_Store and unrelated files are outside that default fingerprint. A concurrent documentation edit must not abort CAD work. Staging still copies the existing project tree; this changes preservation checks, not project organisation or staging contents.

A workflow that consumes a non-CAD dependency can protect it explicitly with preserve_sources(extra_paths=[path_to_policy_json]) or obtain the same fingerprint with source_hashes(extra_paths=[path_to_policy_json]). These paths must be regular repository files without symlinks; changes, additions and deletions are detected. Frozen manufacturing/fit/artwork contracts and their dedicated checks still apply. Never restore files over concurrent work, whether or not they are in the fingerprint. New manifests use this narrower source scope; retained manifests keep their original recorded coverage and must not be rewritten to manufacture freshness.

just test runs all fast tests under tests/test_*.py: source/output guards, layout/chains, arcs, Gerber parsing, web publication, CLI dispatch and import safety. Project inventory counts Edge.Cuts in both board drawings and footprint graphics (including the ear's mechanical M1 outline). test web|layout|arcs|renders selects a subset. Imports do not load generated fixtures or initialise KiCad. Artifact verifiers load an explicitly supplied build through ear verify; native flex-rule fixtures run separately through ear rules.

Generators work on build/ copies and preserve source hashes on success/failure. Tooling manifests include the public/private entry points, package modules and justfile. Required Python/Ruff checks are in AGENTS.md. Frozen fit recipes deliberately retain their standalone, recorded reproducers under docs/ear-fit/; manufacturing order snapshots are never migrated in place.