freeroute's CLI defaults to the grid MVP, so mckicad was getting its weakest
router: the continuous room engine (routes channels the grid cannot, emits
exactly DRC-clean geometry) and channel packing (fits traces a greedy pass
drops) were unreachable.
Probe 'freeroute --help' once per CLI path and select the best engine the build
advertises, defaulting to --engine room --pack. freeroute exits 2 on an
unsupported (engine, flag) pair rather than ignoring it, so also gate each flag
on the engine's own option matrix (packing is room-only; 45-degree routing is
exact-track-only). An older freeroute that advertises no flags still gets the
bare -de/-do drop-in call. All of it is overridable via routing_config.
Add find_freeroute_cli() + _run_freeroute_cli(): run_freerouting now tries the
Java-free freeroute autorouter first (freeroute's CLI is -de/-do compatible)
and falls back to the JAR for boards freeroute cannot yet route. freeroute is
invoked as a subprocess, not imported, keeping its GPL license cleanly separated
from mckicad (MIT). Raises a clear error only when neither backend is available.
End-to-end verified: DSN -> freeroute -> SES -> native applier = 141 segments on
the Arduino_Mega board, fully headless and Java-free.
Replace the two broken kicad-cli specctra subprocess calls:
- import_ses_to_kicad now uses apply_ses_to_board (native, headless)
instead of 'kicad-cli pcb import specctra-ses', logging segment/via/
net counts and warning on nets missing from the board.
- export_dsn_from_kicad runs KiCad's bundled Python calling
pcbnew.ExportSpecctraDSN (works headless, no wxApp) instead of
'kicad-cli pcb export specctra-dsn'.
find_kicad_python locates a pcbnew-capable interpreter cross-platform:
the current interpreter, then the macOS KiCad framework Python, then a
python3 on PATH.
KiCad 10's kicad-cli has no specctra subcommands, kipy has no Specctra
support, and pcbnew.ImportSpecctraSES needs a GUI display. This adds a
pure-Python applier that injects FreeRouting's routed wires and vias
directly into a .kicad_pcb, reusing the existing sexp_tree engine.
Reads the (resolution unit value) scope for scaling, applies the
Specctra Y-up to KiCad Y-down flip, maps SES net names to board net
numbers, and splices (segment)/(via) nodes in before the board's
closing paren so the rest of the file is preserved byte-for-byte.
Fixtures are a real Arduino_Mega template board and a FreeRouting-routed
SES; the integration test applies 23 segments across GND and +5V.
Two bugs left built-in symbols (Device, Connector, ...) unresolvable, which
cascaded into every batch place/label/wire producing zero results:
- get_kicad_symbol_dir() only checked KICAD9/KICAD8_SYMBOL_DIR and a single
Linux path. It ignored the generic KICAD_SYMBOL_DIR, KiCad 10, and the
macOS/Windows install locations, so it returned None on macOS even with the
symbols present. Now checks KICAD_SYMBOL_DIR + KICAD{10,9,8,7}_SYMBOL_DIR and
per-platform default paths.
- resolve_library_path() never searched the system symbol directory for
<lib>.kicad_sym; the system dir was only used for variable substitution
inside a sym-lib-table. With no table configured (tests, fresh installs),
built-in libraries were unreachable. Added a system-dir fallback and covered
the KiCad 10/7 global-table config dirs.
Update test_env_override to clear ambient symbol-dir vars so the override under
test is decisive regardless of the host environment.
Eliminate the external kicad-sch-api dependency by migrating all MCP
tools to the internal SchDocument class built on sexp_tree.py. This
fixes three serialization bugs (dropped global labels, TypeError on
local labels, mis-quoted property private keywords) and removes ~1,900
lines of workaround code.
New modules:
- sexp_tree.py: S-expression parser and round-trip serializer
- sch_document.py: schematic read/write/mutate API
- lib_resolver.py: symbol library search and resolution
- sch_helpers.py: shared load/validate/expand helpers
Migrated all 9 tool files, resources, and tests. Removed 5 workaround
functions from sexp_parser.py. 531 tests pass, ruff + mypy clean.
Pin angles in lib_symbol use Y-up convention. After applying component
rotation, vertical directions (90/270) must be swapped to account for
the lib Y-up to schematic Y-down coordinate transform. Without this,
vertical stubs on non-mirrored components point into the body instead
of away from it, causing label_multiple_wires ERC violations.
Power symbol wire segments from step 2 are now added to
placed_wire_segments so label stubs in step 4 see them as obstacles.
Prevents bridges where label stubs extend through power wires.
Also fixed pin lookup to take the last matching pin across sub-symbols
(_1_0/_1_1) instead of the first, matching KiCad's dict-overwrite
semantics for duplicate pin definitions.
Blanket exclude_points in clamp_stub_length() skipped same-component
obstacles regardless of direction, allowing stubs to bridge through
adjacent pins (R2 +3V3/SDA). Moved exclusion to batch.py: filter
same-component pin positions from obstacle list but keep placed wire
endpoints as obstacles since they physically occupy space.
clamp_stub_length() was treating all pins as potential obstacles,
including pins on the same component. On vertical caps like C7 with
5.08mm pin spacing, pin 1 clamped pin 2's stub to near-zero. Added
exclude_points parameter so callers can skip same-component pins
that cannot cause external net bridges.
resolve_wire_collision() was shifting both stub endpoints when avoiding
collinear overlaps, detaching the wire from the pin. Now only the label
end shifts perpendicular to the axis while the start stays anchored.
7.62mm default stubs caused shorts on small passives and stacked
labels where pin-to-pin distance was less than the stub length.
clamp_stub_length() now auto-shortens stubs when obstacles (adjacent
pins, placed wire endpoints) are detected in the stub's path, with
a 1.27mm clearance margin and 2.54mm minimum floor.
sch.components.get() returns only the first unit instance for a given
reference. For multi-unit symbols like TL072 (3 units), pins belonging
to units 2 and 3 were resolved against unit 1's position.
Added pin-to-unit mapping via KiCad sub-symbol naming convention
(e.g. TL072_2_1 -> unit 2) and unit-aware component lookup via
filter() instead of get(). Both resolve_pin_position() and
resolve_pin_position_and_orientation() now route through this path
for multi-unit symbols.
Wire collision detection: apply_batch now tracks placed wire segments and
detects collinear stubs on the same axis with overlapping ranges belonging
to different nets. Colliding wires shift perpendicular to their axis by
1.27mm, preventing KiCad from merging wire segments into mega-nets.
Project-local library resolution: apply_batch now scans batch component
lib_ids for unknown libraries and registers them with kicad-sch-api's
SymbolLibraryCache via sym-lib-table parsing before component placement.
Unblocks projects using Samacsys and other non-standard symbol libraries.
Root ERC: run_schematic_erc accepts root=True to resolve to the project
root schematic before running kicad-cli, enabling hierarchy-aware ERC
that eliminates ~180 false-positive global_label_dangling warnings from
sub-sheet isolation.
270/270 tests pass, ruff + mypy clean.
Label collision detection: resolve_label_collision() shifts different-net
labels that share the same (x,y) coordinate by 1.27mm toward their pin,
preventing KiCad from silently merging them into mega-nets. Integrated
at both label placement points in apply_batch.
Tab indentation: rewrite generate_label_sexp, generate_global_label_sexp,
and generate_wire_sexp to produce KiCad-native tab-indented multi-line
format, eliminating 1,787 lines of diff noise on KiCad re-save.
Intersheetrefs property now uses (at 0 0 0) placeholder.
Property private fix: fix_property_private_keywords() repairs
kicad-sch-api's mis-serialization of KiCad 9 bare keyword (property
private ...) as quoted (property "private" ...), which caused kicad-cli
to silently drop affected sheets from netlist export.
243 tests pass, ruff + mypy clean.
Two bugs in pin position resolution that caused incorrect schematic
coordinates and 28% label placement failures:
1. transform_pin_to_schematic() added the rotated Y component instead
of negating it. lib_symbol pins use Y-up; schematics use Y-down.
Fix: comp_y + ry -> comp_y - ry.
2. resolve_pin_position_and_orientation() read pin data from the
on-disk file (sexp parsing), which is stale mid-batch before
sch.save(). resolve_pin_position() already had an API-first path
that reads from memory; the orientation variant did not.
Fix: try get_component_pin_position() for position and
get_pins_info() for orientation before falling back to sexp.
Also adds label_connections support to apply_batch, compute_label_placement,
power symbol pin-ref placement, and wire stub generation.
Refactors _build_connectivity() into a two-layer state builder so the
union-find internals (pin_at, label_at, wire_segments) are accessible
to new analysis tools without duplicating the 200-line connectivity engine.
New tools:
- audit_wiring: trace all wires connected to a component, report per-pin
net membership with wire segment coordinates and connected pins
- remove_wires_by_criteria: bulk-remove wires by coordinate filters
(y, x, min/max ranges, tolerance) with dry_run preview support
- verify_connectivity: compare actual wiring against an expected
net-to-pin mapping, report matches/mismatches/missing nets
New sexp_parser utilities:
- parse_wire_segments: extract (wire ...) blocks with start/end/uuid
- remove_sexp_blocks_by_uuid: atomically remove blocks by UUID set
parse_lib_symbol_pins() now falls back to searching external .kicad_sym
library files when a symbol isn't embedded in the schematic's lib_symbols
section. Splits lib_id ("LibName:SymName") to locate the library file,
then parses pins using the bare symbol name.
Search order: schematic dir, libs/, ../libs/, project root, project
root/libs/, kicad/libs/, and sym-lib-table entries with ${KIPRJMOD}
substitution. Handles nonexistent directories gracefully.
Fixes add_power_symbol for script-generated schematics that reference
project library symbols without embedding them (e.g. D1/SMF5.0CA in
ESP32-P4-WIFI6-DEV-KIT library).
add_label bypasses kicad-sch-api serializer entirely — generates
s-expression strings and inserts them directly into the .kicad_sch
file via atomic write. Fixes two upstream bugs: global labels silently
dropped on save (serializer never iterates "global_label" key), and
local labels raising TypeError (parameter signature mismatch in
LabelCollection.add()).
add_power_symbol now falls back to sexp pin parsing when the API
returns None for custom library symbols (e.g. SMF5.0CA). Extracts
shared resolve_pin_position() utility used by both add_power_symbol
and batch operations.
Batch labels also fixed — collected as sexp strings during the batch
loop and inserted after sch.save() so the serializer can't overwrite
them.
Reduce _rc() and transform_pin_to_schematic() rounding from 3 to 2
decimal places to match KiCad's 0.01mm coordinate quantum — prevents
union-find misses when wire endpoints and sexp-parsed pin positions
differ at the sub-quantum level.
Use schematic stem as subdirectory inside .mckicad/ so multi-sheet
analysis outputs (connectivity.json, etc.) don't collide.
kicad-sch-api has two parsing gaps: get_symbol_definition() returns
None for non-standard library prefixes (e.g. Espressif:ESP32-P4),
and there is no sch.global_labels attribute for (global_label ...)
nodes. This adds a focused parser that reads directly from the raw
.kicad_sch file as a fallback, integrated into the connectivity
engine, pin extraction, and label counting tools.
New modules:
- patterns/ library: decoupling bank, pull resistor, crystal oscillator
placement with power symbol attachment and grid math helpers
- tools/batch.py: atomic file-based batch operations with dry_run
- tools/power_symbols.py: add_power_symbol with auto #PWR refs
- tools/schematic_patterns.py: MCP wrappers for pattern library
- tools/schematic_edit.py: modify/remove components, title blocks, annotations
- resources/schematic.py: schematic data resources
43 new tests (99 total), lint clean.