app
Altium - the Altium Bridge
Public Made by Adomby adom
Adom Bridge for Altium Designer: a resident DelphiScript agent gives the AI eval, the 1,100-command process launcher, all 8 EDA surfaces with screenshots, compile/ERC, DRC, OutJob outputs, native SchLib/PcbLib authoring with STEP 3D bodies, and dialog handling by PID.
← 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
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)}")