← Commit history

0.2.52 source (not yet released, needs signing): library pins keep their electrical type (input, output, IO, power...) instead of all Passive; read_symbol returns it. Verified live on AD 26.11.1

Colby Knox ·4d4e0306fb ·1d ago ·parent 54bdc45
5 files changed +28−10
CHANGELOG.md+1
@@ -1,6 +1,7 @@ # Changelog  ## 0.2.52 (2026-10-20)+- **Library pins keep their electrical type.** `altium-symbol-modes` carries each KiCad pin type into the spec (a 10th field), and `build_symbol_modes` sets Altium's pin type from it: input, output, bidirectional (IO), tri-state (HiZ), open collector, open emitter, power in/out (Power), else Passive. Every pin used to be Passive, so Altium's ERC and AI Flow's pin-type check could not tell a power or output pin from a resistor's. `read_symbol` returns each pin's `electrical` (0 Input, 1 IO, 2 Output, 3 OpenCollector, 4 Passive, 5 HiZ, 6 OpenEmitter, 7 Power). Live on AD 26.11.1: the TPS54202's GND, SW and VIN read 7, FB and EN 0, BOOT 4. - **Importing a part again replaces its 3D body instead of stacking a second one.** `add_body` (and the `step` of `import_part`, which calls it) added a new body every call and never removed the old one, and `normalize_bodies` then centred both on the pads: the AMPLH5030S inductor reached a board (2026-10-16) with the old model and the fixed one inside each other, repaired only by `remove_footprint`, a re-import and `PCB:UpdateFootprints`. The agent now deletes the footprint's existing 3D bodies before it adds the new one, the way the editor's Edit > Delete does (it selects exactly those bodies and runs `PCB:Clear`; `RemovePCBObject` left them in the stored footprint after save), how `build_symbol_modes` replaces a symbol of the same LibReference. It removes nothing until the new STEP has loaded, so a bad path keeps the old body. - The `add_body` reply says `replacedBodies` (also at the top level), `keptExisting`, `bodiesBefore`, `sameNameFootprints` and `bodiesNow`, and its `_hint` warns when bodies are still stacked or when several footprints share the name (only the first gets the body). `import_part`'s add_body step carries `replacedBodies`, `bodiesNow` and `sameNameFootprints`. - New `add_body` argument `keepExisting` (default false): add next to the existing bodies, for a deliberate second body.
bridge-rs/src/verbs.rs+3−3
@@ -499,7 +499,7 @@ fn describe() -> Value {             &["component_pins", "plate_vias", "test_board", "pcb_report"], &["Needs a .PcbDoc current: it reads a placed component, not a PcbLib member (altium_pcb_report reads libraries).", "3D bodies, fills and regions only show up in otherPrimitives."], json!({"designator": "U1"})),         verb_doc("schematic_data", "Components on a schematic sheet (filePath, else the CURRENT sheet) with their pins (number, name, electrical type), position and orientation. designator filters to one part.",             json!({"filePath": "path to the .SchDoc - optional, made current first (alias sheet)", "designator": "string - optional filter", "limit": "int, default 500", "pins": "bool, default true", "params": "bool, default false - include each component's parameters"}),-            json!({"data": "{document, count, components: [{designator, partId, libReference, comment, xMil, yMil, orientation, pins: [{number, name, electrical}], params: []}]}", "roundtripMs": "int"}), 120,+            json!({"data": "{document, count, components: [{designator, partId, libReference, comment, xMil, yMil, orientation, pins: [{number, name, electrical (0 Input, 1 IO, 2 Output, 3 OpenCollector, 4 Passive, 5 HiZ, 6 OpenEmitter, 7 Power), xMil, yMil, orientation, lengthMil}], params: []}]}", "roundtripMs": "int"}), 120,             "Pin-level view of a sheet. params[] is empty unless you pass params:true; altium_read_schematic is the fuller parameter view.",             &["read_schematic", "cross_check", "show_schematic"], &["One sheet per call. Without filePath the current document must be a .SchDoc: a schematic library is refused (no_current_schematic), not read as an empty sheet.", "electrical is Altium's pin-type ordinal, not a name."], json!({"designator": "U1"})),         // ---- surfaces ----@@ -718,9 +718,9 @@ fn describe() -> Value {             "altium_save afterwards; altium_show_symbol to see where the text landed.",             &["set_params", "read_symbol", "show_symbol"], &["'parameter not found' = the name must match a parameter altium_read_symbol lists.", "Visible parameters stack over the symbol body: show few."],             json!({"libraryPath": "C:/lib/Adom_ToF.SchLib", "symbolName": "VL53L5CX", "param": "Manufacturer Part Number", "visible": true})),-        verb_doc("read_symbol", "Read a SchLib component back: description, display-mode count, the pins of mode 0 (designator, name), every parameter with its hidden flag, and its model links (footprints, 3D, sim).",+        verb_doc("read_symbol", "Read a SchLib component back: description, display-mode count, the pins of mode 0 (designator, name, electrical type), every parameter with its hidden flag, and its model links (footprints, 3D, sim).",             json!({"libraryPath": "path to a .SchLib - optional; opened first", "symbolName": "string", "timeout": "seconds, default 90"}),-            json!({"data": "{libref, description, displayModeCount, pinsMode0: int, pins: [{des, name}], params: [{name, value, hidden}], models: [{type, name, isCurrent}]}", "roundtripMs": "int"}), 90,+            json!({"data": "{libref, description, displayModeCount, pinsMode0: int, pins: [{des, name, electrical: 0 Input, 1 IO, 2 Output, 3 OpenCollector, 4 Passive, 5 HiZ, 6 OpenEmitter, 7 Power}], params: [{name, value, hidden}], models: [{type, name, isCurrent}]}", "roundtripMs": "int"}), 90,             "The verification step after altium_import_part / altium_set_params: models[] shows which footprint the symbol is bound to (type PCBLIB, isCurrent true for the one a placed part uses). For the drawing use altium_symbol_prims.",             &["symbol_prims", "set_params", "list_library", "show_symbol"], &["The SCH Library panel's Model pane can lag behind altium_show_symbol; models[] here is the symbol's own record.", "Only mode 0 pins are listed."],             json!({"libraryPath": "C:/lib/Adom_ToF.SchLib", "symbolName": "VL53L5CX"})),
dev-skills/altium-bridge-test/test_symbol_modes.py+3−1
@@ -29,6 +29,8 @@ def main():                 rects[f[1]] = tuple(int(v) for v in f[2:6])             elif f[0] == "PIN":                 pins.append((f[1], f[2], int(f[4]), int(f[5]), f[6]))+                if len(f) < 10 or f[9] != "bidirectional":   # the KiCad pin type rides along (0.2.52)+                    raise SystemExit(f"FAIL: pin {f[2]} in mode {f[1]} lost its pin type: {line.strip()}")     bad = []     for mode, num, x, y, orient in pins:         x1, y1, x2, y2 = rects[mode]@@ -40,7 +42,7 @@ def main():     if bad:         print("FAIL\n  " + "\n  ".join(bad[:12]))         sys.exit(1)-    print(f"PASS: {len(pins)} pins across {len(rects)} display modes all start on the body edge")+    print(f"PASS: {len(pins)} pins across {len(rects)} display modes all start on the body edge and keep their pin type")   if __name__ == "__main__":
src/agent/AdomAgent.pas.tmpl+13−2
@@ -2873,6 +2873,7 @@ End;   OwnerPartId 1 and OwnerPartDisplayMode = its mode. } Function CmdBuildSymbolModes(Lines : TStringList) : String; Var+    EType : String;     SLib  : ISch_Lib;     Comp  : ISch_Component;     Rect  : ISch_Rectangle;@@ -2951,7 +2952,17 @@ Begin             Else If Orient = 'up' Then Pin.Orientation := eRotate90             Else Pin.Orientation := eRotate270;             Pin.Location := Point(MilsToCoord(AdomInt(FieldP(L, 5))), MilsToCoord(AdomInt(FieldP(L, 6))));-            Pin.Electrical := eElectricPassive;+            { the KiCad pin type (field 10): until 0.2.52 every pin was Passive, so Altium's ERC+              and AI Flow's pin-type check could not see an output, an input or a power pin }+            EType := FieldP(L, 10);+            If EType = 'input' Then Pin.Electrical := eElectricInput+            Else If EType = 'output' Then Pin.Electrical := eElectricOutput+            Else If EType = 'bidirectional' Then Pin.Electrical := eElectricIO+            Else If EType = 'tri_state' Then Pin.Electrical := eElectricHiZ+            Else If EType = 'open_collector' Then Pin.Electrical := eElectricOpenCollector+            Else If EType = 'open_emitter' Then Pin.Electrical := eElectricOpenEmitter+            Else If (EType = 'power_in') Or (EType = 'power_out') Then Pin.Electrical := eElectricPower+            Else Pin.Electrical := eElectricPassive;             { a small part's pins carry no visible name or number, as a library resistor's do }             If FieldP(L, 9) = 'hide' Then             Begin@@ -3978,7 +3989,7 @@ Begin             If Pin.OwnerPartDisplayMode = 0 Then             Begin                 If Pins <> '' Then Pins := Pins + ',';-                Pins := Pins + '{' + JStr('des', Pin.Designator) + ',' + JStr('name', Pin.Name) + '}';+                Pins := Pins + '{' + JStr('des', Pin.Designator) + ',' + JStr('name', Pin.Name) + ',' + JRaw('electrical', IntToStr(Pin.Electrical)) + '}';                 NPins := NPins + 1;             End;         End
tools/symbol_modes.py+8−4
@@ -11,7 +11,9 @@ A PIN's x,y is its BODY end, on the rectangle's edge (Altium's Pin.Location; the in its orientation). Until 2026-10-19 the outer end was written here, so every pin floated one pin length off the body with its name in the gap. Spec lines: META|libref|desc|designator  MODE|i|name  RECT|mode|x1|y1|x2|y2-PIN|mode|number|name|x|y|orient|len  LINE|mode|x1|y1|x2|y2+PIN|mode|number|name|x|y|orient|len|hide|etype  LINE|mode|x1|y1|x2|y2+(etype: the KiCad pin type, input/output/bidirectional/tri_state/passive/power_in/power_out/...,+which the agent maps to Altium's pin electrical type) """  import sys as _sys@@ -40,7 +42,7 @@ def parse_pins(path):     for m in re.finditer(r'\(pin\s+(\w+)\s+(\w+)\s*\(at\s+([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)\)\s*\(length\s+([-\d.]+)\)', s):         tail = s[m.end():m.end() + 400]         name = re.search(r'\(name\s+"([^"]*)"', tail); num = re.search(r'\(number\s+"([^"]*)"', tail)-        pins.append({"num": num.group(1), "name": name.group(1)})+        pins.append({"num": num.group(1), "name": name.group(1), "etype": m.group(1)})     pins.sort(key=lambda p: int(p["num"]) if p["num"].isdigit() else 999)     return pins @@ -203,7 +205,8 @@ def native_lines(path):         dx, dy = {0: (1, 0), 90: (0, 1), 180: (-1, 0), 270: (0, -1)}.get(a, (1, 0))         orient = {0: "left", 90: "down", 180: "right", 270: "up"}.get(a, "left")         name = nm.group(1) if nm and nm.group(1) != "~" else ""-        out.append("PIN|0|%s|%s|%d|%d|%s|%d|hide" % (num.group(1) if num else "?", name, _mil(x + dx * L), _mil(y + dy * L), orient, _mil(L)))+        et = re.match(r"\(pin\s+(\w+)", p)+        out.append("PIN|0|%s|%s|%d|%d|%s|%d|hide|" % (num.group(1) if num else "?", name, _mil(x + dx * L), _mil(y + dy * L), orient, _mil(L)) + (et.group(1) if et else ""))     ref = re.search(r'\(property\s+"Reference"\s+"([A-Za-z]+)', s)     return (ref.group(1) if ref else "U"), g + out @@ -218,8 +221,9 @@ def build(pins, libref, desc, out):             body, pins_out = fn(pins, grouped, chip)             lines.append(f"MODE|{mode}|{name}{'-chip' if chip else ''}")             lines.append(f"RECT|{mode}|0|{-body['h']}|{body['w']}|0")+            et = {p["num"]: p.get("etype", "") for p in pins}             for _, num, pname, x, y, orient in pins_out:-                lines.append(f"PIN|{mode}|{num}|{pname}|{int(x)}|{int(y)}|{orient}|{PLEN}")+                lines.append(f"PIN|{mode}|{num}|{pname}|{int(x)}|{int(y)}|{orient}|{PLEN}||{et.get(num, '')}")             if chip:                 for (a, b, c, d) in chip_lines(body):                     lines.append(f"LINE|{mode}|{int(a)}|{int(b)}|{int(c)}|{int(d)}")