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.