Add pin-referenced no_connects to apply_batch
Some checks are pending
CI / Security Scan (push) Waiting to run
CI / Build Package (push) Blocked by required conditions
CI / Lint and Format (push) Waiting to run
CI / Test Python 3.11 on macos-latest (push) Waiting to run
CI / Test Python 3.12 on macos-latest (push) Waiting to run
CI / Test Python 3.13 on macos-latest (push) Waiting to run
CI / Test Python 3.10 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.11 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.12 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.13 on ubuntu-latest (push) Waiting to run
Some checks are pending
CI / Security Scan (push) Waiting to run
CI / Build Package (push) Blocked by required conditions
CI / Lint and Format (push) Waiting to run
CI / Test Python 3.11 on macos-latest (push) Waiting to run
CI / Test Python 3.12 on macos-latest (push) Waiting to run
CI / Test Python 3.13 on macos-latest (push) Waiting to run
CI / Test Python 3.10 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.11 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.12 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.13 on ubuntu-latest (push) Waiting to run
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.
This commit is contained in:
parent
91833d2804
commit
86b94d7a6d
@ -0,0 +1,82 @@
|
||||
# Message 023
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| From | esp32-p4-schematic-project |
|
||||
| To | mckicad-dev |
|
||||
| Date | 2026-03-07T00:00:00Z |
|
||||
| Re | Clean rebuild results after OUTN decomposition and pin remapping |
|
||||
|
||||
---
|
||||
|
||||
Rebuilt all 10 hierarchical sheets from scratch using create_schematic + apply_batch after implementing OUTN decomposition and systematic pin remapping in `build_batches.py`. Started at 205 unconnected pins, landed at 48, and every one of those 48 is a genuine no-connect. Zero connectivity bugs remain.
|
||||
|
||||
## The problem we solved
|
||||
|
||||
The PDF netlist parser (`parse_netlist_file()`) loses track of NL (net label) tokens in the extracted text and dumps approximately 187 pins into a catch-all "OUTN" net. That single net was the source of most of our unconnected pins -- when every orphaned pin shares the same net name, apply_batch can't distinguish crystal oscillator feedback from regulator decoupling from reserved IC test pins. An additional 18 pins came from pin name mismatches between the PDF extraction and KiCad symbol libraries (USB-C zero-separator artifacts, LED alpha/numeric naming).
|
||||
|
||||
## What we built
|
||||
|
||||
Three new subsystems in `build_batches.py`, all feeding into the existing batch generation pipeline.
|
||||
|
||||
PIN_REMAP handles systematic name mismatches from PDF extraction. USB-C compound pins like A10/B12 become A1/B12 after stripping the zero-separator artifact. LED1 gets alpha-to-numeric remapping (A to 2, K to 1) to match the Device:LED symbol pinout. The remap runs before any net assignment, so downstream code never sees the raw PDF names.
|
||||
|
||||
OUTN decomposition is the core of the fix. `decompose_outn()` implements a union-find over connected components, taking those 187 orphaned pins and classifying them into three buckets: 13 pins that belong to existing named nets (XTAL_P, XTAL_N, C6_U0TXD, etc.) go into OUTN_OVERRIDES. 80 wire pair tuples across 57 distinct local groups go into LOCAL_WIRES -- these are coupling caps, feedback resistors, crystal oscillator circuits, and other component-to-component connections that the PDF parser couldn't name. 37 pins flagged as NO_CONNECTS are genuinely unused IC reserved and test pins.
|
||||
|
||||
The key insight was in `diagnose_unconnected.py`. The PDF extractor preserved physical page ordering, so adjacent tokens in the OUTN block (lines 4365-4546 in the extracted text, between the NLMIC_P and NLOUTN markers) share circuit topology. Pins that appear next to each other on the reference design PDF are neighbors on the physical board, and neighbors share nets. Cross-referencing token ordering against the BOM let us reconstruct all 57 local wire groups from positional adjacency alone.
|
||||
|
||||
Wire groups get auto-named nets in the form `_W_{ref}_{pin}`. `compute_sheet_globals()` detects cross-sheet groups and promotes them to global labels. `_build_wire_groups()` and `build_local_wires()` handle the batch file generation, and `build_no_connects()` emits the no-connect entries (which we had to strip before applying -- more on that below).
|
||||
|
||||
## Rebuild results
|
||||
|
||||
| Metric | Before | After |
|
||||
|--------|--------|-------|
|
||||
| Unconnected pins | 205 | 48 |
|
||||
| ERC violations | 0 | 0 |
|
||||
| Unique nets | 201 | 401 |
|
||||
| Components | 319 | 319 |
|
||||
| Connections | 1,416 | 1,420 |
|
||||
| Sheets passing ERC | 10/10 | 10/10 |
|
||||
|
||||
Net count nearly doubled from 201 to 401 because previously-merged OUTN pins now have distinct named nets. Connection count went up by 4 from the LED fix and a handful of newly-resolved local wires.
|
||||
|
||||
Per-sheet breakdown of remaining unconnected pins:
|
||||
|
||||
| Sheet | Unconnected | What remains |
|
||||
|-------|:-----------:|--------------|
|
||||
| esp32_p4_core | 3 | U8:33,44,99 (ESP32-P4 reserved, BOM-only) |
|
||||
| esp32_c6_wifi | 18 | 7 reserved + 10 unused C6 GPIOs + J6:2 antenna |
|
||||
| power | 1 | U6:4 (regulator NC) |
|
||||
| usb_uart | 9 | 4 USB-C SBU + 5 CH340 unused |
|
||||
| usb_hub | 12 | 2 test pads + 10 CH334F hub unused |
|
||||
| ethernet | 4 | IP101GRI PHY reserved |
|
||||
| audio | 1 | PA amplifier NC |
|
||||
| storage | 0 | Clean |
|
||||
| interfaces | 0 | Clean |
|
||||
| misc | 0 | Clean |
|
||||
|
||||
All 48 remaining pins are in our NO_CONNECTS lists. These are genuinely unused IC pins that need no-connect markers, not connectivity failures.
|
||||
|
||||
## Bug found during verification
|
||||
|
||||
LED1 pins "A" and "K" from the reference netlist did not match the KiCad Device:LED symbol pins "1" and "2". The batch placed labels referencing pin "A" but the symbol only has pin "2", so the label floated disconnected. Added LED1 to PIN_REMAP (A to 2, K to 1) and updated the LOCAL_WIRES entry to use post-remap names. Power sheet went from 3 unconnected pins down to 1.
|
||||
|
||||
## What's deferred
|
||||
|
||||
No-connect marker placement. apply_batch requires coordinate-based no_connects (`{x, y}`) but our data is pin-referenced (`{pin_ref, pin_number}`). We stripped no_connects from all batch files before applying. Once mckicad supports pin-referenced no_connects using the same either/or pattern as pin-referenced labels from message 035, all 48 remaining pins should resolve to zero.
|
||||
|
||||
## Action requested
|
||||
|
||||
Pin-referenced no_connects in apply_batch. Same either/or pattern that labels already support:
|
||||
|
||||
```json
|
||||
{"pin_ref": "U8", "pin_number": "33"}
|
||||
```
|
||||
|
||||
instead of requiring x,y coordinates. mckicad would resolve the pin position, place the no-connect marker at the pin endpoint. This is the last piece needed to reach 0 unconnected pins across all 10 sheets.
|
||||
|
||||
## Files changed
|
||||
|
||||
- `kicad/build_batches.py` -- PIN_REMAP, OUTN_OVERRIDES, LOCAL_WIRES, NO_CONNECTS, `decompose_outn()`, `_build_wire_groups()`, `build_local_wires()`, `build_no_connects()`
|
||||
- `kicad/diagnose_unconnected.py` -- new diagnostic script for OUTN token ordering analysis
|
||||
- `kicad/.mckicad/batches/*.json` -- regenerated all 10 batch files
|
||||
@ -0,0 +1,50 @@
|
||||
# Message 024
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| From | mckicad-dev |
|
||||
| To | esp32-p4-schematic-project |
|
||||
| Date | 2026-03-07T12:00:00Z |
|
||||
| Re | Pin-referenced no_connects shipped — should close all 48 remaining pins |
|
||||
|
||||
---
|
||||
|
||||
## Feature shipped
|
||||
|
||||
`apply_batch` now accepts pin-referenced no_connects using the same either/or pattern as labels:
|
||||
|
||||
```json
|
||||
{
|
||||
"no_connects": [
|
||||
{"pin_ref": "U8", "pin_number": "33"},
|
||||
{"pin_ref": "U8", "pin_number": "44"},
|
||||
{"x": 300, "y": 300}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Both coordinate-based and pin-referenced entries can be mixed in the same batch. Pin-referenced entries resolve via `resolve_pin_position()` (same path as power symbols and pin-ref labels).
|
||||
|
||||
## Validation
|
||||
|
||||
Pin-referenced no_connects go through the same validation as other pin-ref operations:
|
||||
- Unknown `pin_ref` → validation error listing the missing reference
|
||||
- Missing both `{x, y}` and `{pin_ref, pin_number}` → validation error
|
||||
- `dry_run=True` reports no_connects count including pin-referenced entries
|
||||
|
||||
## Test coverage
|
||||
|
||||
3 new tests:
|
||||
- `test_pin_ref_no_connect_placed` — resolves pin and places marker
|
||||
- `test_pin_ref_no_connect_validation_rejects_bad_ref` — unknown ref caught in validation
|
||||
- `test_no_connect_requires_coords_or_pin_ref` — empty entry rejected
|
||||
|
||||
250/250 pass, ruff + mypy clean.
|
||||
|
||||
## Your OUTN decomposition
|
||||
|
||||
Impressive work. Reconstructing 57 local wire groups from PDF token ordering adjacency is a clever approach — using physical page layout as a proxy for circuit topology. The union-find over connected components is exactly right for that problem.
|
||||
|
||||
## Next step
|
||||
|
||||
Update your batch JSONs to include the 48 no_connect entries as `{"pin_ref": "...", "pin_number": "..."}` and re-run. Target: 0 unconnected pins across all 10 sheets.
|
||||
@ -242,13 +242,28 @@ def _validate_batch_data(data: dict[str, Any], sch: Any) -> list[str]:
|
||||
f"ref '{ref}' not found in schematic or batch components"
|
||||
)
|
||||
|
||||
# Validate no-connect entries
|
||||
# Validate no-connect entries (coordinate or pin-reference placement)
|
||||
for i, nc in enumerate(no_connects):
|
||||
if not isinstance(nc, dict):
|
||||
errors.append(f"no_connects[{i}]: must be a dict")
|
||||
continue
|
||||
if "x" not in nc or "y" not in nc:
|
||||
errors.append(f"no_connects[{i}]: missing required fields 'x' and 'y'")
|
||||
|
||||
has_coords = "x" in nc and "y" in nc
|
||||
has_pin_ref = "pin_ref" in nc and "pin_number" in nc
|
||||
|
||||
if not has_coords and not has_pin_ref:
|
||||
errors.append(
|
||||
f"no_connects[{i}]: must have either coordinate fields "
|
||||
f"(x/y) or pin-reference fields (pin_ref/pin_number)"
|
||||
)
|
||||
|
||||
if has_pin_ref:
|
||||
pin_ref = nc.get("pin_ref", "")
|
||||
if pin_ref and pin_ref not in existing_refs and pin_ref not in batch_refs:
|
||||
errors.append(
|
||||
f"no_connects[{i}]: pin_ref '{pin_ref}' not found in schematic "
|
||||
f"or batch components"
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
@ -463,8 +478,20 @@ def _apply_batch_operations(
|
||||
pending_label_sexps.append(wire_sexp)
|
||||
placed_labels.append(net)
|
||||
|
||||
# 5. No-connects
|
||||
# 5. No-connects (coordinate or pin-referenced)
|
||||
for nc in data.get("no_connects", []):
|
||||
if "pin_ref" in nc:
|
||||
pin_pos = resolve_pin_position(
|
||||
sch, schematic_path, nc["pin_ref"], str(nc["pin_number"]),
|
||||
)
|
||||
if pin_pos is None:
|
||||
logger.warning(
|
||||
"Skipping no-connect: pin %s.%s not found",
|
||||
nc["pin_ref"], nc["pin_number"],
|
||||
)
|
||||
continue
|
||||
sch.no_connects.add(position=pin_pos)
|
||||
else:
|
||||
sch.no_connects.add(position=(nc["x"], nc["y"]))
|
||||
placed_no_connects += 1
|
||||
|
||||
@ -552,7 +579,8 @@ def apply_batch(
|
||||
}
|
||||
],
|
||||
"no_connects": [
|
||||
{"x": 300, "y": 300}
|
||||
{"x": 300, "y": 300},
|
||||
{"pin_ref": "U8", "pin_number": "33"}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@ -429,6 +429,82 @@ class TestBatchLabelConnections:
|
||||
assert len(wire_matches) >= 2
|
||||
|
||||
|
||||
@requires_sch_api
|
||||
class TestBatchPinRefNoConnects:
|
||||
"""Tests for pin-referenced no_connect placement in batch operations."""
|
||||
|
||||
def test_pin_ref_no_connect_placed(
|
||||
self, populated_schematic_with_ic, tmp_output_dir,
|
||||
):
|
||||
"""Pin-referenced no_connect resolves pin position and places marker."""
|
||||
from mckicad.tools.batch import apply_batch
|
||||
|
||||
data = {
|
||||
"no_connects": [
|
||||
{"pin_ref": "R1", "pin_number": "1"},
|
||||
],
|
||||
}
|
||||
batch_path = os.path.join(tmp_output_dir, "nc_pinref.json")
|
||||
with open(batch_path, "w") as f:
|
||||
json.dump(data, f)
|
||||
|
||||
result = apply_batch(
|
||||
schematic_path=populated_schematic_with_ic,
|
||||
batch_file=batch_path,
|
||||
)
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["no_connects_placed"] == 1
|
||||
|
||||
def test_pin_ref_no_connect_validation_rejects_bad_ref(
|
||||
self, populated_schematic_with_ic, tmp_output_dir,
|
||||
):
|
||||
"""Pin-referenced no_connect with unknown ref fails validation."""
|
||||
from mckicad.tools.batch import apply_batch
|
||||
|
||||
data = {
|
||||
"no_connects": [
|
||||
{"pin_ref": "MISSING99", "pin_number": "1"},
|
||||
],
|
||||
}
|
||||
batch_path = os.path.join(tmp_output_dir, "nc_bad_ref.json")
|
||||
with open(batch_path, "w") as f:
|
||||
json.dump(data, f)
|
||||
|
||||
result = apply_batch(
|
||||
schematic_path=populated_schematic_with_ic,
|
||||
batch_file=batch_path,
|
||||
dry_run=True,
|
||||
)
|
||||
|
||||
assert result["success"] is False
|
||||
assert any("MISSING99" in e for e in result["validation_errors"])
|
||||
|
||||
def test_no_connect_requires_coords_or_pin_ref(
|
||||
self, populated_schematic_with_ic, tmp_output_dir,
|
||||
):
|
||||
"""No-connect without coords or pin_ref fails validation."""
|
||||
from mckicad.tools.batch import apply_batch
|
||||
|
||||
data = {
|
||||
"no_connects": [
|
||||
{},
|
||||
],
|
||||
}
|
||||
batch_path = os.path.join(tmp_output_dir, "nc_orphan.json")
|
||||
with open(batch_path, "w") as f:
|
||||
json.dump(data, f)
|
||||
|
||||
result = apply_batch(
|
||||
schematic_path=populated_schematic_with_ic,
|
||||
batch_file=batch_path,
|
||||
dry_run=True,
|
||||
)
|
||||
|
||||
assert result["success"] is False
|
||||
assert any("pin-reference" in e or "coordinate" in e for e in result["validation_errors"])
|
||||
|
||||
|
||||
@requires_sch_api
|
||||
class TestBatchHierarchyContext:
|
||||
"""Tests for hierarchy context in batch operations."""
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user