The KiCad s-expression parser rejects raw newline and carriage-return
characters inside quoted string literals — a multi-line text annotation
written through `add_text` produced a `.kicad_sch` file that eeschema
silently tolerated but `kicad-cli sch ...` refused with "Failed to load
schematic." The escape pass only handled backslashes and double quotes.
Add `\\n` → `\\\\n` and `\\r` → `\\\\r` to the same escape chain. Order
matters: backslashes are escaped first so we don't double-escape our
own escapes.
A new regression test (`test_escapes_newlines_in_multiline_text`)
checks both that the resulting quoted string literal contains no raw
newline characters and that the file round-trips cleanly through the
sexpdata parser.
End-to-end smoke: a 4-line annotation written through the patched
add_text now passes `kicad-cli sch erc` (exit 0) where the previous
behaviour failed parse.
Note: the same escape gap exists in `_make_hierarchical_label_text`
and `_make_sheet_pin_text` for unescaped quotes/newlines in the user-
supplied text. Not fixed here to keep this PR scoped to the documented
add_text bug; happy to fold it in if a reviewer prefers.
1. add_schematic_net_label failed on schematics with no existing labels.
The clone-based path required a pre-existing label to copy from;
the documented "fallback to sexpdata" was a misleading log line —
the RuntimeError was caught and the call silently returned False.
Restore hand-built sexpdata construction (without the buggy
fields_autoplaced token, with orientation-aware justify).
2. get_pin_angle returned the wrong angle for every mirrored symbol
(off by exactly 180°, all rotations, both mirror axes). The
mirror_x and mirror_y formulas were swapped relative to the
pin_world_xy convention — pin_world_xy mirrors a position by
flipping its local axis component, so the matching angle
transform is (180 - θ) for mirror_x and -θ for mirror_y.
Add regression tests:
- test_add_label_empty_schematic.py — first label on empty schematic,
orientation-aware justify.
- test_get_pin_angle.py — full 24-case matrix
(4 rotations × 3 mirror states × 2 pins).
Upstream added `from annotations import AnnotationLoader` and moved
`from commands.wire_manager import WireManager` to module-level in
kicad_interface.py. The smoke test now stubs annotations and ensures
python/ is on sys.path so commands.* imports resolve without installing.
Previously get_pin_location and get_pin_angle read symbol state from a
kicad-skip cache that does not reflect (mirror x/y) tokens written by
rotate_schematic_component. Pin coordinates were always computed as if
the symbol was unmirrored.
Fix:
- Added _get_symbol_transform() which reads position, rotation, mirror_x,
mirror_y, and lib_id directly from the .kicad_sch file via sexpdata +
WireDragger.find_symbol (the authoritative source after a rotate/mirror)
- get_pin_location now delegates the full transform (mirror → rotate →
translate) to WireDragger.pin_world_xy, matching the logic used by
move_schematic_component and rotate_schematic_component
- get_pin_angle now applies mirror-induced angle reflection before adding
symbol rotation: mirror_x negates the angle, mirror_y reflects across 180°
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs fixed:
1. fields_autoplaced yes was always injected — caused incorrect visual
rendering of label text in KiCAD. Removed by using clone() which
copies an existing label without that field.
2. (justify left bottom) was hardcoded regardless of orientation.
For orientation 180/270 KiCAD requires (justify right bottom).
Now set correctly via new_label.effects.justify._tree[1].
Implementation switches from manual sexpdata list construction to
kicad-skip Schematic.label[0].clone(), which produces a structurally
correct label that KiCAD can round-trip without modification.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously the handler used kicad-skip to apply rotation and mirror.
kicad-skip has no API for (mirror x/y) on placed symbols, causing:
'NoneType' object has no attribute 'value'
Fix:
- Rewrote _handle_rotate_schematic_component to use sexpdata (same
approach as move_schematic_component) for both rotation and mirror
- Added WireDragger.compute_pin_positions_for_rotation: computes old
and new pin world positions when rotation/mirror changes at fixed (x,y)
- Added WireDragger.update_symbol_rotation_mirror: updates (at) rotation
and adds/removes/replaces the (mirror x/y) sexpdata token cleanly
- Connected wires now follow pin positions after rotate/mirror via the
existing WireDragger.drag_wires infrastructure
Tests: 10 unit tests in tests/test_rotate_schematic_mirror.py covering
update_symbol_rotation_mirror, compute_pin_positions_for_rotation, and
a handler smoke test.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PWR_FLAG instances use a #FLG reference prefix, not #PWR, so their
pin positions were never registered as virtual connection anchors in
_parse_virtual_connections. As a result, find_orphaned_wires reported
wire ends terminating on a PWR_FLAG as dangling. Other call sites
(schematic_analysis.py:127, kicad_interface.py:3814) already recognize
#FLG as a power symbol; align this site with them.
Also coerce a previously-validated Optional[int] to int in board/layers.py
so the file passes mypy (required by the pre-commit hook); behavior is
unchanged because the value is already None-checked above.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The disclaimer is project-stance commentary, not a changelog entry; moved
to local notes outside the repo.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Refactor _handle_rotate_schematic_component to use raw sexp throughout
and write the schematic once instead of three times
- Hoist sexpdata and WireManager imports to module scope; drop the
unnecessary underscore aliases in move/rotate handlers
- Move WireManager._SUB_UNIT_RE to the top of the class body
- Promote the per-call Symbol("symbol")/Symbol("unit") allocations in
_parse_lib_pins / _collect_pin_positions to module-level _SYM_*
constants
- Document the assumption behind _SUB_UNIT_RE's greedy match
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the manual add_schematic_junction tool with automatic junction
management. WireManager.sync_junctions inserts/removes junction dots
based on wire endpoints plus component pin positions and is invoked
after add_wire, add_polyline_wire, delete_wire, move, and rotate.
- Pin-aware: parses lib_symbols and applies KiCad's mirror/rotate/
translate transform to compute world pin coordinates
- Multi-unit safe: filters lib_symbols sub-units by the placed
symbol's (unit N) field plus the unit-0 common body
- Removes the now-unused WireManager.add_junction static method
- Updates CHANGELOG [Unreleased] with the tool removal notice
- Adds .mcp.json to .gitignore (machine-local paths)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Configuring logging at module import time hard-failed in environments
without write access to the user's home directory (sandboxed pytest
runners, restricted CI containers, read-only filesystems). The
unhandled OSError/PermissionError aborted module import and cascaded
into ~100 test failures during collection.
Wrap the FileHandler setup in try/except and fall back to console-only
logging when the log directory cannot be created. Production behavior
is unchanged - file logging in ~/.kicad-mcp/logs is still used
whenever the directory is writable.
Also picks up an isort-driven import reorder applied by the project's
pre-commit hook.
The symbol-to-schematic y-flip was applied twice in sequence (two identical
negation blocks with matching comments), cancelling out and leaving pin
Y-coordinates mirrored about the placement Y. For symmetric passives the
bug is invisible (pin 1 and pin 2 are electrically interchangeable); for
ICs with non-equivalent pins (power pins, opamp inputs, etc.) this causes
tools that go through PinLocator — connect_to_net, add_schematic_connection,
add_schematic_net_label with componentRef+pinNumber — to place connections
at the mirror-flipped pin. Verified against kicad-cli generate_netlist
ground truth: on a Device:R placed at (111.76, 83.82), pin 1 resolves to
y=80.01 (actual) vs y=87.63 (pre-fix).
This is a regression of PR #103, which originally fixed the y-negation;
the redundant second block was added subsequently.
Includes a regression test with both a straight Device:R and a rotated
Device:C to exercise the y-flip + rotation pipeline.
connect_to_net and connect_passthrough previously only touched the
schematic: a wire stub plus a net label were added via
ConnectionManager, but the corresponding pad on the .kicad_pcb was
never updated.
pcbnew.SaveBoard() silently drops every net that is not referenced by
at least one board element (pad/track/via/zone). A net that exists
only in the schematic therefore disappears on save, and in KiCad the
board's ratsnest never shows anything.
Fix:
- new _assign_net_to_pad(component_ref, pin_name, net_name):
ensures the net exists on the board (creates NETINFO_ITEM if not)
and sets it on the matching pad of the matching footprint.
- _handle_connect_to_net calls it after the schematic op when
self.board is loaded.
- _handle_connect_passthrough parses ConnectionManager's 'connected'
entries ('J1/1 <-> J2/1 [PIN_1]') and assigns nets to both pads.
- register 'connect_to_net' in _BOARD_MUTATING_COMMANDS so the
existing auto-save path persists the pad assignment to disk.
Passing net_name='*' now deletes all tracks on the board (respecting
the include_vias flag). Useful for wiping a test layout before
re-routing without having to iterate every net by name.
board.GetLayerStack() was removed in KiCad 9. Call SetLayerName and
SetLayerType directly on the board instead, and grow the copper layer
count via SetCopperLayerCount when adding inner layers. Without this,
add_layer raises AttributeError on any KiCad 9 installation.
KiCad 10.0 on Windows ships pcbnew.py under bin/Lib/site-packages rather
than the lib/python3/dist-packages path used by KiCad 9.x. Without this
path the MCP server's Python process fails to import pcbnew and exits
immediately with "Failed to import pcbnew module - KiCAD Python API not found".
Changes:
- Prioritise 10.0 in the version list so the newer path is checked first
- Add bin/Lib/site-packages check before the existing lib/python3/dist-packages
check for every version, so both layouts are supported side-by-side
Note: the pcbnew extension is compiled against KiCad's bundled Python 3.11,
so callers must also set KICAD_PYTHON to the KiCad python.exe
(C:\Program Files\KiCad\10.0\bin\python.exe) to avoid a DLL version
conflict when the system Python differs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
KiCad serialises rescued or locally-customised library entries with an
extra (lib_name "...") child before (lib_id "..."):
(symbol
(lib_name "RESISTOR_0603_4")
(lib_id "MF_Passives:RESISTOR_0603")
(at 132.08 44.45 90)
...)
The block-matching regex in _handle_get_schematic_component,
_handle_edit_schematic_component, and _handle_delete_schematic_component
required (lib_id IMMEDIATELY after (symbol, so any placed component
using this form was silently invisible to lookup. The user-visible
symptom is "Component '<ref>' not found in schematic" even though the
component is plainly present (and reachable through list / IPC paths).
This bug also affected set/remove_schematic_component_property and the
existing footprint/value/reference rewriting paths in edit, since they
all share the same lookup code.
The parent-position lookup used a similarly-strict regex
((symbol (lib_id "...") (at ...))), which silently fell back to (0,0)
on (lib_name)-first symbols and caused new properties added through
the custom-properties path to anchor at the schematic origin instead
of the parent symbol.
Fix: relax the symbol-block opening pattern to (symbol\s+\( — matching
any opening paren after (symbol — and read the symbol's origin from
the first (at ...) inside the block. Library-definition entries inside
(lib_symbols ...) are still excluded by the existing range check
(they use the (symbol "name" ...) form with a quoted string, not a
paren).
Adds 7 regression tests in TestLibNameBeforeLibIdOrdering using a
real-world (lib_name)-first resistor block, covering get / edit /
set-property / remove-property / delete and verifying that newly
added properties anchor to the symbol origin instead of (0, 0).