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.
Agent-thread logs carried real /home paths and project names. Remove from
tracking and gitignore per the no-live-data-in-git convention. Files remain on
disk locally; history is intentionally left intact (no force-push).
requires_kicad keyed off a hardcoded /usr/share/kicad path, so symbol-dependent
tests silently skipped on macOS even with KiCad installed. Derive DEVICE_LIB
from get_kicad_symbol_dir() so those tests actually run on the macOS testbed.
Bump CalVer to reflect the SchDocument refactor and macOS resolution fixes.
Add [tool.hatch.build.targets.sdist] excludes so the published archive ships
only src + packaging metadata — keeping CLAUDE.md, docs/ (agent threads with
operator paths), docs-site/, and tests/ out of the sdist.
test_root_resolves_to_project_schematic built its project directly in tmp_path,
where the autouse _set_test_search_paths fixture also drops a
test_project.kicad_pro. With two .kicad_pro files in one directory, root
resolution returned whichever os.listdir yielded first — myproject on ext4,
test_project on APFS — so it passed on Linux and failed on macOS. Anchor the
test's project in its own subdirectory so the walk stops at the intended root.
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.
test_root_resolves_to_project_schematic created its project directly in
tmp_path, where the autouse _set_test_search_paths fixture also drops a
test_project.kicad_pro. With two .kicad_pro files in one directory,
_resolve_root_schematic returns whichever os.listdir yields first — myproject
on ext4, test_project on APFS — so the test passed on Linux and failed on macOS.
Anchor the test's project in its own subdirectory so the root walk finds this
project's .kicad_pro and stops before reaching the sibling. Product code is
unchanged and correct; this was test isolation only.
IC chip favicon eliminates the 404. Hero image shows an actual
schematic fragment (MCU, decoupling cap, LED circuit, pull-up)
so the landing page demonstrates the output, not the technology.
Scenario-driven homepage that introduces mckicad through what
engineers actually say when they sit down to work. Removes the
root redirect so / serves index.html directly.
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.
Starlight has no index.html at document root — the landing page
lives at /getting-started/index.html. Added a 302 redirect from /
and fixed try_files to resolve directory indices correctly.
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.
A4 is too small for real schematics. A3 matches US engineering
convention (close to ANSI B/Tabloid) and is KiCad's standard
template size. Batch JSON also supports paper_size field.
Autowire guide covers the decision tree, threshold tuning, workflow
patterns, and troubleshooting. CLAUDE.md updated to reflect the new
autowire/ package and tools/autowire.py.
The obstacle pre-scan ran before component placement, caching None
for every pin on components being created in the same batch. Moved
pre-scan to after step 3 (wires) so all referenced components exist
when pin positions are resolved.
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.
Default stub_length increased from 2.54mm to 7.62mm (3 grid units)
so labels clear component bodies. Per-connection stub_length and
direction overrides added for edge cases where auto-placement puts
labels in bad spots.
007: Pin-ref fix not active after /mcp reconnect (transport
reconnect doesn't reload Python modules). 008: Reply confirming
server restart required to load new sexp_parser.py code.
Classifies unconnected nets into optimal wiring methods (direct wire,
local/global label, power symbol, no-connect) based on pin distance,
crossing count, fanout, and net name patterns. Delegates all file
manipulation to apply_batch, inheriting collision detection and label
serialization for free.
Strategy concepts informed by KICAD-autowire (MIT, arashmparsa).
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.
validate_schematic now exports a netlist via kicad-cli to traverse the
full sheet hierarchy for connectivity analysis, replacing the single-file
kicad-sch-api approach that returned zeros on hierarchical projects.
Unconnected pins derived from ERC pin_not_connected count.
Pass unit field through to kicad-sch-api's native multi-unit validation
instead of custom bypass. Removes _add_multi_unit() that used incompatible
internal API (_add_item vs _add_item_to_collection across API versions).
Combines hierarchy-aware ERC (via root schematic resolution) with
connectivity analysis and optional baseline comparison into one atomic
call. Supports fail_on parameter to gate pass/fail on specific ERC
violation types (default: multiple_net_names). Baseline comparison
detects connection count decreases and unconnected pin increases as
regressions.
Replaces the 20+ tool call workflow of running ERC + connectivity on
each sub-sheet individually.
280/280 tests pass, ruff + mypy clean.
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.
KiCad 9 nests violations under sheets[].violations instead of
a top-level violations key. The parser now iterates all sheets
and aggregates violations, with a fallback for flat format.
no_connects now accept {pin_ref, pin_number} as an alternative to
{x, y} coordinates, matching the pattern used by labels. Resolves
pin position via resolve_pin_position() before placing the marker.
snap_to_grid() used 2.54mm default grid for symbol position,
silently rounding sub-2.54mm stub lengths up and causing shorts
on tightly-spaced connectors. Now uses 1.27mm fine grid.
add_hierarchical_sheet now returns sheet_uuid and parent_uuid.
apply_batch accepts these as optional params to call
set_hierarchy_context() before placing components, fixing
kicad-cli netlist export for hierarchical designs.
330 power symbols placed across 10 sheets but zero power nets appear in
kicad-cli export. Affects both mckicad and generate_schematic.py output.
Instance path mismatch identified as secondary issue. Workaround: use
verify_connectivity which reads power symbols via kicad-sch-api natively.
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.
Netlists exceeding INLINE_RESULT_THRESHOLD (20 nets) now return a
compact summary with the top nets by connection count as a preview.
Full data is always written to the sidecar JSON file. Small netlists
still return everything inline.
KiCad 9 defaults to s-expression netlist export, not XML. Add
_parse_kicad_sexp() parser with pinfunction/pintype metadata, update
auto-detection to distinguish (export from <export by content.
Fix export_netlist: use kicad-cli's actual format names (kicadsexpr,
kicadxml) instead of invalid 'kicad', use --format instead of -f, and
treat non-zero exit with valid output as success with warnings.
Adds KiCad XML (.net) and CSV/TSV netlist import with auto-format
detection. Output nets dict is directly compatible with
verify_connectivity's expected parameter.
Parses external netlist files into the component-pin-net graph that
verify_connectivity and apply_batch consume. Supports KiCad XML (.net)
exported by kicad-cli, and CSV/TSV with flexible column name matching.
Auto-detects format from file extension and content. Output is directly
compatible with verify_connectivity's expected parameter, closing the
loop between "I have a design" and "I can build it in KiCad."
Requested by ESP32-P4 project (agent thread message 028).
Hierarchical KiCad projects store sub-sheets in subdirectories (e.g.
sheets/). The flat os.listdir scan missed all of them. Use recursive
glob to find .kicad_sch files at any depth under the project directory.
Reported by ESP32-P4 project (agent thread message 025) — their 8
malformed property-private entries were all in sheets/ subdirectory.
validate_project now scans all .kicad_sch files for two classes of
silent rendering failure: quoted (property "private" ...) that should
be a bare keyword in KiCad 9, and lib_id references with no matching
lib_symbols entry. Both cause kicad-cli to blank entire pages during
export without any error message.
Also pass working_dir to run_kicad_command in export_pdf for robust
library path resolution.
Addresses feedback from ESP32-P4 project (agent thread message 021).
Power symbols at a pin coordinate create a direct electrical connection
without a wire. Note this in audit_wiring (explains wire_count: 0 for
connected pins) and add_power_symbol (explains why it draws a stub).
Group results by net name instead of per-pin, keeping the summary
compact enough to stay inline even for 100+ pin components. Add
anomaly detection (unconnected pins, high-fanout nets, auto-named
nets) and optional pin/net filters. Wire coordinates are now opt-in
via include_wires flag to avoid flooding the calling LLM with
coordinate noise.