app
adom-desktop-altium-bridge
Public Unreviewedby barrett-land
Adom Desktop bridge for Altium Designer — build and edit file-based symbol/footprint/Integrated libraries with LCSC/JLCPCB part numbers attached, and generate batch outputs (STEP, PDF, Gerber, BOM, CPL) from an OutJob, by driving Altium via DelphiScript.
← Commit history
Publish 0.0.36
17 files changed
+2313
BRIDGE_VERSION+1SKILL.md+25altium-bridge-manifest.json+3altium-bridge-v0.0.35.zipbridge-src/BRIDGE_VERSION+1bridge-src/addin/AdomAltium/AdomAltium.PrjScr+14bridge-src/addin/AdomAltium/AdomAltium.pas+1453bridge-src/addin_client.py+101bridge-src/altium_detect.py+150bridge-src/bridge.json+73bridge-src/deploy.sh+42bridge-src/server.py+339bridge.json+73docs/hero.pnginstall.sh+8package.json+26uninstall.sh+4BRIDGE_VERSIONadded+1@@ -0,0 +1 @@+0.0.35
SKILL.mdadded+25@@ -0,0 +1,25 @@+---+name: adom-desktop-altium-bridge+description: Adom Desktop bridge for Altium Designer. Adds LCSC/JLCPCB-tagged components to file-based Altium libraries (symbols, footprints with 3D, value-only display) by driving Altium via DelphiScript. Install when the user wants Claude to add parts to their Altium library. Pairs with the adom-altium-library-importer skill.+---++# adom-desktop-altium-bridge++A dynamic [Adom Desktop](https://wiki.adom.inc/adom-desktop) bridge that lets cloud Claude drive **Altium Designer** on the user's Windows machine (Altium has no headless/CLI mode — the bridge forwards DelphiScript via `X2.EXE`).++## Install (on the user's Windows desktop)++```bash+adom-desktop bridge_install '{"manifestUrl":"https://wiki.adom.inc/static/apps/adom-desktop-altium-bridge/altium-bridge-manifest.json"}'+adom-desktop altium_ping '{}' # -> {"altiumInstalled": true, "version": "..."}+```++## Verbs++`altium_ping` / `altium_detect`; `altium_extract_intlib`; `altium_import_part` (batched one-round-trip import); `altium_build_footprint` (+STEP 3D); `altium_add_body` (extruded 3D); `altium_clone_symbol`; `altium_add_component` (LCSC + params); `altium_set_visibility`; `altium_rename_component` / `altium_rename_footprint`; `altium_install_library` / `altium_cleanup_libs`; `altium_close_doc` / `altium_read_params` / `altium_list_symbols` / `altium_read_symbol`.++## Use it++Pair with the **[adom-altium-library-importer](https://wiki.adom.inc/adom-altium-library-importer)** skill, which orchestrates these verbs to add catalog parts (with the JLC part number) to the user's Altium libraries.++> The bridge process is single-threaded: a slow DelphiScript save can report "Request timed out" while the work persists — verify with `altium_read_params` rather than trusting the timeout.
altium-bridge-manifest.jsonadded+3@@ -0,0 +1,3 @@+{ "manifest_version": 1, "name": "altium", "version": "0.0.35",+ "url": "https://wiki.adom.inc/download/barrett-land/adom-desktop-altium-bridge/0.0.35/altium-bridge-v0.0.35.zip",+ "sha256": "2caa3ebbf4dc780b8464d3a964556f3ac5bf452065b3a36e92147f1dfed0eddf", "size": 22919, "released_at": "2026-06-22T00:00:00Z" }
altium-bridge-v0.0.35.zipadded⋯ 1 unchanged line ⋯
bridge-src/BRIDGE_VERSIONadded+1@@ -0,0 +1 @@+0.0.33
bridge-src/addin/AdomAltium/AdomAltium.PrjScradded+14@@ -0,0 +1,14 @@+[Design]+Version=1.0+HierarchyMode=0+DefaultConfiguration=Default - All Constraints+UserID=0xFFFFFFFF++[Preferences]+PrefsVaultGUID=+PrefsRevisionGUID=++[Document1]+DocumentPath=AdomAltium.pas+DItemRevisionGUID=+DocumentUniqueId=
bridge-src/addin/AdomAltium/AdomAltium.pasadded+1453@@ -0,0 +1,1453 @@+// AdomAltium.pas — Adom Altium bridge in-host dispatcher (original Adom code).+//+// Invoked by the bridge via:+// X2.EXE -RScriptingSystem:RunScript(ProjectName=...AdomAltium.PrjScr|ProcName=AdomAltium>Run)+// Reads C:\Users\Public\AdomAltium\request.txt (simple key=value lines)+// Writes C:\Users\Public\AdomAltium\response.json (hand-built JSON)+//+// Request keys (per command):+// command = ping | add_lcsc_param | read_params+// add_lcsc_param: schlib, component, lcsc, mpn, manufacturer, value, description, package, datasheet+// read_params: schlib, component++const+ EX_DIR = 'C:\Users\Public\AdomAltium\';++var+ Req : TStringList;+ LastServerDoc : IServerDocument; // set by OpenSchLibDoc, used to save+ GStage : String; // last stage reached, for error pinpointing+ GReqId : String; // id of the request being served (echoed in response)++{ ---- JSON string escaping (minimal, original) ---- }+function JStr(const S : String) : String;+var i : Integer; c : Char; o : String;+begin+ o := '';+ for i := 1 to Length(S) do+ begin+ c := S[i];+ if (c = '\') or (c = '"') then o := o + '\' + c+ else if c = #9 then o := o + '\t'+ else if c = #10 then o := o + '\n'+ else if c = #13 then o := o + '\r'+ else o := o + c;+ end;+ Result := '"' + o + '"';+end;++function BoolStr(b : Boolean) : String;+begin+ if b then Result := 'true' else Result := 'false';+end;++procedure WriteResp(const JsonBody : String);+var R : TStringList;+begin+ if not DirectoryExists(EX_DIR) then ForceDirectories(EX_DIR);+ R := TStringList.Create;+ try+ R.Text := JsonBody;+ R.SaveToFile(EX_DIR + 'response.json');+ finally+ R.Free;+ end;+end;++procedure RespOk(const ExtraPairs : String);+begin+ WriteResp('{ "ok": true, "id": ' + JStr(GReqId) + ExtraPairs + ' }');+end;++procedure RespErr(const Msg : String);+begin+ WriteResp('{ "ok": false, "id": ' + JStr(GReqId) + ', "error": ' + JStr(Msg) + ' }');+end;++{ ---- open a SchLib; returns ISch_Lib and stashes the server doc for saving ---- }+function OpenSchLibDoc(const Path : String) : ISch_Lib;+begin+ Result := nil;+ LastServerDoc := nil;+ if SchServer = nil then Exit;+ LastServerDoc := Client.OpenDocument('SCHLIB', Path);+ if LastServerDoc <> nil then+ Client.ShowDocument(LastServerDoc);+ Result := SchServer.GetSchDocumentByPath(Path);+end;++procedure SaveCurrentDoc;+begin+ if LastServerDoc <> nil then+ LastServerDoc.DoFileSave('SCHLIB');+end;++{ ---- find a component in a SchLib by LibReference (empty = first) ---- }+function FindComponent(SchLib : ISch_Lib; const NameRef : String) : ISch_Component;+var It : ISch_Iterator; Cmp : ISch_Component;+begin+ Result := nil;+ It := SchLib.SchLibIterator_Create;+ It.AddFilter_ObjectSet(MkSet(eSchComponent));+ try+ Cmp := It.FirstSchObject;+ while Cmp <> nil do+ begin+ if (NameRef = '') or (Cmp.LibReference = NameRef) then+ begin+ Result := Cmp;+ Break;+ end;+ Cmp := It.NextSchObject;+ end;+ finally+ SchLib.SchIterator_Destroy(It);+ end;+end;++{ ---- set or replace a parameter on a component ---- }+procedure SetParam(Cmp : ISch_Component; const PName, PVal : String);+var It : ISch_Iterator; P, Existing : ISch_Parameter;+begin+ Existing := nil;+ It := Cmp.SchIterator_Create;+ It.AddFilter_ObjectSet(MkSet(eParameter));+ try+ P := It.FirstSchObject;+ while P <> nil do+ begin+ if P.Name = PName then begin Existing := P; Break; end;+ P := It.NextSchObject;+ end;+ finally+ Cmp.SchIterator_Destroy(It);+ end;++ if Existing <> nil then+ Existing.Text := PVal+ else+ begin+ P := SchServer.SchObjectFactory(eParameter, eCreate_GlobalCopy);+ P.Name := PName;+ P.Text := PVal;+ P.IsHidden := False;+ Cmp.AddSchObject(P);+ end;+end;++procedure MaybeSet(Cmp : ISch_Component; const PName, Key : String);+var V : String;+begin+ V := Req.Values[Key];+ if V <> '' then SetParam(Cmp, PName, V);+end;++{ ---- command: add LCSC + identity params to a component ---- }+procedure CmdAddLcscParam;+var SchLibPath, CompRef : String; SchLib : ISch_Lib; Cmp : ISch_Component;+begin+ SchLibPath := Req.Values['schlib'];+ CompRef := Req.Values['component'];+ if SchLibPath = '' then begin RespErr('missing schlib'); Exit; end;++ SchLib := OpenSchLibDoc(SchLibPath);+ if SchLib = nil then begin RespErr('could not open schlib: ' + SchLibPath); Exit; end;++ Cmp := FindComponent(SchLib, CompRef);+ if Cmp = nil then begin RespErr('component not found: ' + CompRef); Exit; end;++ MaybeSet(Cmp, 'LCSC Part #', 'lcsc');+ MaybeSet(Cmp, 'Manufacturer Part Number', 'mpn');+ MaybeSet(Cmp, 'Manufacturer', 'manufacturer');+ MaybeSet(Cmp, 'Value', 'value');+ MaybeSet(Cmp, 'Description', 'description');+ MaybeSet(Cmp, 'Package', 'package');+ MaybeSet(Cmp, 'Datasheet', 'datasheet');++ SchLib.GraphicallyInvalidate;+ SaveCurrentDoc;++ RespOk(', "component": ' + JStr(Cmp.LibReference) ++ ', "lcsc": ' + JStr(Req.Values['lcsc']));+end;++{ ---- command: read back a component's parameters (verification) ---- }+procedure CmdReadParams;+var SchLibPath, CompRef, Acc : String; SchLib : ISch_Lib; Cmp : ISch_Component;+ It : ISch_Iterator; P : ISch_Parameter; First : Boolean;+begin+ SchLibPath := Req.Values['schlib'];+ CompRef := Req.Values['component'];+ SchLib := OpenSchLibDoc(SchLibPath);+ if SchLib = nil then begin RespErr('could not open schlib: ' + SchLibPath); Exit; end;+ Cmp := FindComponent(SchLib, CompRef);+ if Cmp = nil then begin RespErr('component not found: ' + CompRef); Exit; end;++ Acc := ''; First := True;+ It := Cmp.SchIterator_Create;+ It.AddFilter_ObjectSet(MkSet(eParameter));+ try+ P := It.FirstSchObject;+ while P <> nil do+ begin+ if not First then Acc := Acc + ', ';+ Acc := Acc + JStr(P.Name) + ': ' + JStr(P.Text);+ First := False;+ P := It.NextSchObject;+ end;+ finally+ Cmp.SchIterator_Destroy(It);+ end;+ RespOk(', "component": ' + JStr(Cmp.LibReference) + ', "params": { ' + Acc + ' }');+end;++{ ---- read back all params of a component into a JSON object body ---- }+function ParamsJSON(Cmp : ISch_Component) : String;+var It : ISch_Iterator; P : ISch_Parameter; Acc : String; First : Boolean;+begin+ Acc := ''; First := True;+ It := Cmp.SchIterator_Create;+ It.AddFilter_ObjectSet(MkSet(eParameter));+ try+ P := It.FirstSchObject;+ while P <> nil do+ begin+ if not First then Acc := Acc + ', ';+ Acc := Acc + JStr(P.Name) + ': ' + JStr(P.Text);+ First := False;+ P := It.NextSchObject;+ end;+ finally+ Cmp.SchIterator_Destroy(It);+ end;+ Result := '{ ' + Acc + ' }';+end;++{ ---- command: create a new SchLib + component, attach LCSC/identity params,+ read them back, and best-effort save. Proves the keystone end-to-end+ with no external symbol source. Stage-tracked for error pinpointing. ---- }+procedure CmdCreateAndAttach;+var+ CompName, Path : String;+ SD : IServerDocument;+ SchLib : ISch_Lib;+ Cmp : ISch_Component;+ SavedOK : Boolean;+begin+ CompName := Req.Values['component']; if CompName = '' then CompName := 'TESTPART';+ Path := Req.Values['schlib'];++ GStage := 'SchServer';+ if SchServer = nil then begin RespErr('SchServer is nil'); Exit; end;++ GStage := 'CreateNewDocumentFromDocumentKind(SchLib)';+ SD := CreateNewDocumentFromDocumentKind('SchLib');+ GStage := 'ShowDocument';+ if SD <> nil then Client.ShowDocument(SD);+ LastServerDoc := SD;++ GStage := 'GetCurrentSchDocument';+ SchLib := SchServer.GetCurrentSchDocument;+ if SchLib = nil then begin RespErr('GetCurrentSchDocument nil at ' + GStage); Exit; end;++ GStage := 'SchObjectFactory(eSchComponent)';+ Cmp := SchServer.SchObjectFactory(eSchComponent, eCreate_Default);+ if Cmp = nil then begin RespErr('component factory nil'); Exit; end;++ GStage := 'set component props';+ Cmp.CurrentPartID := 1;+ Cmp.DisplayModeCount := 1;+ Cmp.LibReference := CompName;+ Cmp.Designator.Text := 'U?';+ Cmp.Comment.Text := CompName;++ GStage := 'AddSchComponent';+ SchLib.AddSchComponent(Cmp);++ GStage := 'register primitive';+ SchServer.RobotManager.SendMessage(SchLib.I_ObjectAddress, c_BroadCast,+ SCHM_PrimitiveRegistration, Cmp.I_ObjectAddress);++ GStage := 'set params';+ MaybeSet(Cmp, 'LCSC Part #', 'lcsc');+ MaybeSet(Cmp, 'Manufacturer Part Number', 'mpn');+ MaybeSet(Cmp, 'Manufacturer', 'manufacturer');+ MaybeSet(Cmp, 'Value', 'value');+ MaybeSet(Cmp, 'Description', 'description');++ GStage := 'invalidate';+ SchLib.GraphicallyInvalidate;++ GStage := 'save';+ SavedOK := False;+ if (Path <> '') and (LastServerDoc <> nil) then+ begin+ LastServerDoc.SetFileName(Path);+ SavedOK := LastServerDoc.DoFileSave('');+ end;++ GStage := 'done';+ if SavedOK then+ RespOk(', "component": ' + JStr(CompName) ++ ', "saved": true, "saved_to": ' + JStr(Path) ++ ', "params": ' + ParamsJSON(Cmp))+ else+ RespOk(', "component": ' + JStr(CompName) ++ ', "saved": false, "params": ' + ParamsJSON(Cmp));+end;++{ ---- command: create a proper 2-pin resistor symbol with body, params, and a+ footprint implementation link, then save the SchLib ---- }+procedure CmdCreateResistor;+var+ SD : IServerDocument;+ Lib : ISch_Lib;+ Cmp : ISch_Component;+ R : ISch_Rectangle;+ P1, P2 : ISch_Pin;+ Impl : ISch_Implementation;+ SchPath, FpName, FpLib, SavedStr : String;+ SavedOK : Boolean;+begin+ SchPath := Req.Values['schlib'];+ FpName := Req.Values['footprint']; if FpName = '' then FpName := 'R0603';+ FpLib := Req.Values['footprint_lib']; if FpLib = '' then FpLib := 'Resistors.PcbLib';++ GStage := 'create SchLib doc';+ SD := CreateNewDocumentFromDocumentKind('SchLib');+ if SD <> nil then Client.ShowDocument(SD);+ LastServerDoc := SD;+ Lib := SchServer.GetCurrentSchDocument;+ if Lib = nil then begin RespErr('no SchLib'); Exit; end;++ GStage := 'create component';+ Cmp := SchServer.SchObjectFactory(eSchComponent, eCreate_Default);+ if Cmp = nil then begin RespErr('component factory nil'); Exit; end;+ Cmp.CurrentPartID := 1;+ Cmp.DisplayMode := 0;+ Cmp.LibReference := Req.Values['component'];+ if Cmp.LibReference = '' then Cmp.LibReference := 'R';++ GStage := 'rectangle body';+ R := SchServer.SchObjectFactory(eRectangle, eCreate_Default);+ R.LineWidth := eSmall;+ R.Location := Point(MilsToCoord(0), MilsToCoord(-50));+ R.Corner := Point(MilsToCoord(300), MilsToCoord(50));+ R.IsSolid := False;+ R.OwnerPartId := Cmp.CurrentPartID;+ R.OwnerPartDisplayMode := Cmp.DisplayMode;++ GStage := 'pin 1';+ P1 := SchServer.SchObjectFactory(ePin, eCreate_Default);+ P1.Location := Point(MilsToCoord(-100), MilsToCoord(0));+ P1.Orientation := eRotate0;+ P1.Designator := '1';+ P1.Name := '1';+ P1.OwnerPartId := Cmp.CurrentPartID;+ P1.OwnerPartDisplayMode := Cmp.DisplayMode;++ GStage := 'pin 2';+ P2 := SchServer.SchObjectFactory(ePin, eCreate_Default);+ P2.Location := Point(MilsToCoord(400), MilsToCoord(0));+ P2.Orientation := eRotate180;+ P2.Designator := '2';+ P2.Name := '2';+ P2.OwnerPartId := Cmp.CurrentPartID;+ P2.OwnerPartDisplayMode := Cmp.DisplayMode;++ GStage := 'add primitives';+ Cmp.AddSchObject(P1);+ Cmp.AddSchObject(P2);+ Cmp.AddSchObject(R);++ GStage := 'designator + params';+ Cmp.Designator.Text := 'R?';+ if Req.Values['value'] <> '' then Cmp.Comment.Text := Req.Values['value'];+ MaybeSet(Cmp, 'LCSC Part #', 'lcsc');+ MaybeSet(Cmp, 'Manufacturer Part Number', 'mpn');+ MaybeSet(Cmp, 'Manufacturer', 'manufacturer');+ MaybeSet(Cmp, 'Value', 'value');+ MaybeSet(Cmp, 'Description', 'description');++ GStage := 'footprint implementation';+ Impl := Cmp.AddSchImplementation;+ Impl.ClearAllDatafileLinks;+ Impl.ModelName := FpName;+ Impl.ModelType := 'PCBLIB';+ Impl.AddDataFileLink(FpName, '', 'PCBLIB');+ Impl.IsCurrent := True;++ GStage := 'AddSchComponent';+ Lib.AddSchComponent(Cmp);+ SchServer.RobotManager.SendMessage(Lib.I_ObjectAddress, c_BroadCast,+ SCHM_PrimitiveRegistration, Cmp.I_ObjectAddress);++ GStage := 'save';+ SavedOK := False;+ if (SchPath <> '') and (LastServerDoc <> nil) then+ begin+ LastServerDoc.SetFileName(SchPath);+ SavedOK := LastServerDoc.DoFileSave('');+ end;+ if SavedOK then SavedStr := 'true' else SavedStr := 'false';++ GStage := 'done';+ RespOk(', "component": ' + JStr(Cmp.LibReference) ++ ', "saved": ' + SavedStr ++ ', "saved_to": ' + JStr(SchPath) ++ ', "footprint_link": ' + JStr(FpLib + ':' + FpName) ++ ', "params": ' + ParamsJSON(Cmp));+end;++{ ---- command: create a 2-pad SMD 0603 footprint in a PcbLib, then save ---- }+procedure CmdCreateFootprint;+var+ SD : IServerDocument;+ PcbLib : IPCB_Library;+ FpComp : IPCB_LibComponent;+ Pad : IPCB_Pad;+ PcbPath, FpName, SavedStr : String;+ SavedOK : Boolean;+begin+ PcbPath := Req.Values['pcblib'];+ FpName := Req.Values['footprint']; if FpName = '' then FpName := 'R0603';++ GStage := 'create PcbLib doc';+ SD := CreateNewDocumentFromDocumentKind('PcbLib');+ if SD <> nil then Client.ShowDocument(SD);+ LastServerDoc := SD;++ GStage := 'GetCurrentPCBLibrary';+ if PCBServer = nil then begin RespErr('PCBServer nil'); Exit; end;+ PcbLib := PCBServer.GetCurrentPCBLibrary;+ if PcbLib = nil then begin RespErr('no current PcbLib'); Exit; end;++ GStage := 'CreatePCBLibComp';+ FpComp := PCBServer.CreatePCBLibComp;+ FpComp.Name := FpName;+ PcbLib.RegisterComponent(FpComp);++ GStage := 'PreProcess';+ PCBServer.PreProcess;++ GStage := 'pads';+ MakePad('1', -30); { centers ~1.5 mm apart }+ MakePad('2', 30);++ GStage := 'PostProcess';+ PCBServer.PostProcess;+ PcbLib.Board.ViewManager_FullUpdate;++ GStage := 'save';+ SavedOK := False;+ if (PcbPath <> '') and (LastServerDoc <> nil) then+ begin+ LastServerDoc.SetFileName(PcbPath);+ SavedOK := LastServerDoc.DoFileSave('');+ end;+ if SavedOK then SavedStr := 'true' else SavedStr := 'false';++ GStage := 'done';+ RespOk(', "footprint": ' + JStr(FpName) + ', "pads": 2, "saved": ' + SavedStr ++ ', "saved_to": ' + JStr(PcbPath));+end;++{ ---- command: build/append a symbol in a SchLib with N pins + a footprint link.+ pins as pin0..padN (CommaText): designator,name,x,y,orient(0/90/180/270) mils. ---- }+procedure CmdBuildSymbol;+var+ SD : IServerDocument;+ Lib : ISch_Lib;+ Cmp : ISch_Component;+ R : ISch_Rectangle;+ El : ISch_Ellipse;+ Pin : ISch_Pin;+ Impl : ISch_Implementation;+ flds : TStringList;+ SchPath, CompName, FpName, DesigPfx, v, SavedStr : String;+ npins, i, ori : Integer;+ SavedOK : Boolean;+begin+ SchPath := Req.Values['schlib'];+ CompName := Req.Values['component']; if CompName = '' then CompName := 'COMP';+ FpName := Req.Values['footprint'];+ DesigPfx := Req.Values['designator']; if DesigPfx = '' then DesigPfx := 'U';+ npins := StrToIntDef(Req.Values['pincount'], 1);++ GStage := 'open-or-create SchLib';+ if (SchPath <> '') and FileExists(SchPath) then+ SD := Client.OpenDocument('SCHLIB', SchPath)+ else+ SD := CreateNewDocumentFromDocumentKind('SchLib');+ if SD <> nil then Client.ShowDocument(SD);+ LastServerDoc := SD;+ Lib := SchServer.GetCurrentSchDocument;+ if Lib = nil then begin RespErr('no SchLib'); Exit; end;++ GStage := 'create component';+ Cmp := SchServer.SchObjectFactory(eSchComponent, eCreate_Default);+ Cmp.CurrentPartID := 1;+ Cmp.DisplayMode := 0;+ Cmp.LibReference := CompName;++ GStage := 'body';+ if Req.Values['body'] = 'circle' then+ begin+ El := SchServer.SchObjectFactory(eEllipse, eCreate_Default);+ El.Location := Point(MilsToCoord(50), MilsToCoord(0));+ El.Radius := MilsToCoord(50);+ El.SecondaryRadius := MilsToCoord(50);+ El.LineWidth := eSmall;+ El.OwnerPartId := Cmp.CurrentPartID;+ El.OwnerPartDisplayMode := Cmp.DisplayMode;+ Cmp.AddSchObject(El);+ end+ else+ begin+ R := SchServer.SchObjectFactory(eRectangle, eCreate_Default);+ R.LineWidth := eSmall;+ R.Location := Point(MilsToCoord(0), MilsToCoord(-50));+ R.Corner := Point(MilsToCoord(200), MilsToCoord(50));+ R.OwnerPartId := Cmp.CurrentPartID;+ R.OwnerPartDisplayMode := Cmp.DisplayMode;+ Cmp.AddSchObject(R);+ end;++ GStage := 'pins';+ flds := TStringList.Create;+ try+ for i := 0 to npins - 1 do+ begin+ v := Req.Values['pin' + IntToStr(i)];+ if v = '' then Continue;+ flds.CommaText := v; { designator,name,x,y,orient }+ Pin := SchServer.SchObjectFactory(ePin, eCreate_Default);+ Pin.Location := Point(MilsToCoord(StrToIntDef(flds[2], -100)),+ MilsToCoord(StrToIntDef(flds[3], 0)));+ ori := StrToIntDef(flds[4], 0);+ if ori = 90 then Pin.Orientation := eRotate90+ else if ori = 180 then Pin.Orientation := eRotate180+ else if ori = 270 then Pin.Orientation := eRotate270+ else Pin.Orientation := eRotate0;+ Pin.Designator := flds[0];+ Pin.Name := flds[1];+ Pin.OwnerPartId := Cmp.CurrentPartID;+ Pin.OwnerPartDisplayMode := Cmp.DisplayMode;+ Cmp.AddSchObject(Pin);+ end;+ finally+ flds.Free;+ end;++ GStage := 'designator + params';+ Cmp.Designator.Text := DesigPfx + '?';+ if Req.Values['value'] <> '' then Cmp.Comment.Text := Req.Values['value'];+ MaybeSet(Cmp, 'LCSC Part #', 'lcsc');+ MaybeSet(Cmp, 'Manufacturer Part Number', 'mpn');+ MaybeSet(Cmp, 'Manufacturer', 'manufacturer');+ MaybeSet(Cmp, 'Value', 'value');+ MaybeSet(Cmp, 'Description', 'description');++ GStage := 'footprint link';+ if FpName <> '' then+ begin+ Impl := Cmp.AddSchImplementation;+ Impl.ClearAllDatafileLinks;+ Impl.ModelName := FpName;+ Impl.ModelType := 'PCBLIB';+ Impl.AddDataFileLink(FpName, '', 'PCBLIB');+ Impl.IsCurrent := True;+ end;++ GStage := 'AddSchComponent';+ Lib.AddSchComponent(Cmp);+ SchServer.RobotManager.SendMessage(Lib.I_ObjectAddress, c_BroadCast,+ SCHM_PrimitiveRegistration, Cmp.I_ObjectAddress);++ GStage := 'save';+ SavedOK := False;+ if (SchPath <> '') and (LastServerDoc <> nil) then+ begin+ LastServerDoc.SetFileName(SchPath);+ SavedOK := LastServerDoc.DoFileSave('');+ end;+ if SavedOK then SavedStr := 'true' else SavedStr := 'false';++ RespOk(', "component": ' + JStr(CompName) + ', "footprint": ' + JStr(FpName) ++ ', "pins": ' + IntToStr(npins) + ', "saved": ' + SavedStr);+end;++{ ---- command: build an Altium footprint from a pad spec + optional STEP 3D body.+ pads passed as pad0..padN (CommaText): smd,x,y,w,h,drill,name (all mils). ---- }+procedure CmdBuildFootprint;+var+ SD : IServerDocument;+ PcbLib : IPCB_Library;+ Fp : IPCB_LibComponent;+ Pad : IPCB_Pad;+ Body : IPCB_ComponentBody;+ Mdl : IPCB_Model;+ PcbPath, FpName, StepPath, v, SavedStr, BodyNote : String;+ n, i : Integer;+ flds : TStringList;+ SavedOK : Boolean;+begin+ PcbPath := Req.Values['pcblib'];+ FpName := Req.Values['footprint']; if FpName = '' then FpName := 'FP';+ StepPath := Req.Values['step'];+ n := StrToIntDef(Req.Values['padcount'], 0);++ GStage := 'open-or-create PcbLib doc';+ if (PcbPath <> '') and FileExists(PcbPath) then+ SD := Client.OpenDocument('PCBLIB', PcbPath) { append to existing lib }+ else+ SD := CreateNewDocumentFromDocumentKind('PcbLib');+ if SD <> nil then Client.ShowDocument(SD);+ LastServerDoc := SD;++ GStage := 'GetCurrentPCBLibrary';+ if PCBServer = nil then begin RespErr('PCBServer nil'); Exit; end;+ PcbLib := PCBServer.GetCurrentPCBLibrary;+ if PcbLib = nil then begin RespErr('no current PcbLib'); Exit; end;++ GStage := 'CreatePCBLibComp';+ Fp := PCBServer.CreatePCBLibComp;+ Fp.Name := FpName;+ PcbLib.RegisterComponent(Fp);+ PCBServer.PreProcess;++ GStage := 'pads';+ flds := TStringList.Create;+ try+ for i := 0 to n - 1 do+ begin+ v := Req.Values['pad' + IntToStr(i)];+ if v = '' then Continue;+ flds.CommaText := v; { smd,x,y,w,h,drill,name,shape (mils) }+ Pad := PCBServer.PCBObjectFactory(ePadObject, eNoDimension, eCreate_Default);+ Pad.X := MilsToCoord(StrToIntDef(flds[1], 0));+ Pad.Y := MilsToCoord(StrToIntDef(flds[2], 0));+ Pad.TopXSize := MilsToCoord(StrToIntDef(flds[3], 50));+ Pad.TopYSize := MilsToCoord(StrToIntDef(flds[4], 50));+ if (flds.Count >= 8) and ((flds[7] = 'circle') or (flds[7] = 'oval')) then+ Pad.TopShape := eRounded+ else+ Pad.TopShape := eRectangular;+ if flds[0] = '1' then+ begin+ Pad.HoleSize := 0;+ Pad.Layer := eTopLayer; { SMD }+ end+ else+ begin+ Pad.HoleSize := MilsToCoord(StrToIntDef(flds[5], 0));+ Pad.Layer := eMultiLayer; { through-hole }+ end;+ Pad.Name := flds[6];+ Fp.AddPCBObject(Pad);+ PCBServer.SendMessageToRobots(Fp.I_ObjectAddress, c_Broadcast,+ PCBM_BoardRegisteration, Pad.I_ObjectAddress);+ end;+ finally+ flds.Free;+ end;++ GStage := 'step body';+ BodyNote := 'none';+ if StepPath <> '' then+ begin+ Body := PCBServer.PCBObjectFactory(eComponentBodyObject, eNoDimension, eCreate_Default);+ Mdl := Body.ModelFactory_FromFilename(StepPath, False);+ if Mdl <> nil then+ begin+ Body.SetState_FromModel;+ Body.Model := Mdl;+ Fp.AddPCBObject(Body);+ BodyNote := 'added';+ end+ else BodyNote := 'model load failed';+ end;++ GStage := 'PostProcess';+ PCBServer.PostProcess;++ GStage := 'save';+ SavedOK := False;+ if (PcbPath <> '') and (LastServerDoc <> nil) then+ begin+ LastServerDoc.SetFileName(PcbPath);+ SavedOK := LastServerDoc.DoFileSave('');+ end;+ if SavedOK then SavedStr := 'true' else SavedStr := 'false';++ RespOk(', "footprint": ' + JStr(FpName) + ', "pads": ' + IntToStr(n) ++ ', "step_body": ' + JStr(BodyNote) + ', "saved": ' + SavedStr ++ ', "saved_to": ' + JStr(PcbPath));+end;++{ ---- command: compile a Library Package (.LibPkg) into a .IntLib ---- }+procedure CmdCompileIntLib;+var+ LibPkgPath, OutDir : String;+ Proj : IProject;+begin+ LibPkgPath := Req.Values['libpkg'];+ OutDir := Req.Values['output'];+ if LibPkgPath = '' then begin RespErr('missing libpkg'); Exit; end;++ GStage := 'DM_OpenProject';+ Proj := GetWorkspace.DM_OpenProject(LibPkgPath, True);+ if Proj = nil then begin RespErr('could not open LibPkg: ' + LibPkgPath); Exit; end;++ GStage := 'DM_Compile';+ Proj.DM_Compile;++ GStage := 'CreateIntegratedLibrary';+ IntegratedLibraryManager.CreateIntegratedLibrary(Proj, OutDir, True);++ GStage := 'done';+ RespOk(', "libpkg": ' + JStr(LibPkgPath) + ', "output_dir": ' + JStr(OutDir));+end;++{ ---- command: extract the editable sources (SchLib + PcbLib) from a compiled+ .IntLib, then report the extracted document paths so the caller can edit+ them (e.g. add an LCSC param) before re-installing/compiling. ---- }+procedure CmdExtractIntLib;+var+ IntLibPath, P, SchAcc, PcbAcc : String;+ WS : IWorkspace;+ Proj : IProject;+ Doc : IDocument;+ i, j : Integer;+ FirstS, FirstP : Boolean;+begin+ IntLibPath := Req.Values['intlib'];+ if IntLibPath = '' then begin RespErr('missing intlib'); Exit; end;++ GStage := 'ExtractSources';+ IntegratedLibraryManager.ExtractSources(IntLibPath);++ GStage := 'scan projects';+ WS := GetWorkspace;+ SchAcc := ''; PcbAcc := '';+ FirstS := True; FirstP := True;+ if WS <> nil then+ for i := 0 to WS.DM_ProjectCount - 1 do+ begin+ Proj := WS.DM_Projects(i);+ if Proj = nil then Continue;+ for j := 0 to Proj.DM_LogicalDocumentCount - 1 do+ begin+ Doc := Proj.DM_LogicalDocuments(j);+ if Doc = nil then Continue;+ P := Doc.DM_FullPath;+ if Pos('.SchLib', P) > 0 then+ begin+ if not FirstS then SchAcc := SchAcc + ', ';+ SchAcc := SchAcc + JStr(P); FirstS := False;+ end+ else if Pos('.PcbLib', P) > 0 then+ begin+ if not FirstP then PcbAcc := PcbAcc + ', ';+ PcbAcc := PcbAcc + JStr(P); FirstP := False;+ end;+ end;+ end;++ GStage := 'done';+ RespOk(', "intlib": ' + JStr(IntLibPath) ++ ', "schlibs": [ ' + SchAcc + ' ]' ++ ', "pcblibs": [ ' + PcbAcc + ' ]');+end;++{ ---- helper: does S contain any of the comma-separated lowercase needles? ---- }+function ContainsAny(const S, CsvLower : String) : Boolean;+var parts : TStringList; i : Integer;+begin+ Result := False;+ parts := TStringList.Create;+ try+ parts.CommaText := CsvLower;+ for i := 0 to parts.Count - 1 do+ if (parts[i] <> '') and (Pos(parts[i], S) > 0) then+ begin Result := True; Break; end;+ finally+ parts.Free;+ end;+end;++{ ---- command: list open documents — disabled (Client.DocumentCount is not a+ valid DelphiScript identifier on AD26; calling it threw a script-error+ dialog). Returns an empty list instead of raising. ---- }+procedure CmdListDocs;+begin+ RespOk(', "docs": [], "note": "doc enumeration unsupported on this Altium build"');+end;++{ ---- command: rename a component in a SchLib (in place), optionally updating its+ Description, footprint link, and designator. Preserves the exact symbol+ graphics/pins/params — only identity/metadata changes. ---- }+procedure CmdRenameComponent;+var+ SchPath, OldName, NewName, FpName, Desig, SavedStr : String;+ Lib : ISch_Lib;+ Cmp : ISch_Component;+ It : ISch_Iterator;+ Impl : ISch_Implementation;+ Found : Boolean;+ SavedOK : Boolean;+begin+ SchPath := Req.Values['schlib'];+ OldName := Req.Values['component'];+ NewName := Req.Values['newname'];+ FpName := Req.Values['footprint'];+ Desig := Req.Values['designator'];+ if (SchPath = '') or (OldName = '') or (NewName = '') then+ begin RespErr('need schlib + component + newname'); Exit; end;++ GStage := 'open schlib';+ Lib := OpenSchLibDoc(SchPath);+ if Lib = nil then begin RespErr('could not open: ' + SchPath); Exit; end;++ GStage := 'find component';+ Cmp := FindComponent(Lib, OldName);+ if Cmp = nil then begin RespErr('component not found: ' + OldName); Exit; end;++ GStage := 'set identity';+ Cmp.LibReference := NewName;+ Cmp.DesignItemID := NewName;+ if Desig <> '' then Cmp.Designator.Text := Desig;++ GStage := 'description';+ MaybeSet(Cmp, 'Description', 'description');++ if FpName <> '' then+ begin+ GStage := 'update footprint link';+ Found := False;+ It := Cmp.SchIterator_Create;+ It.AddFilter_ObjectSet(MkSet(eImplementation));+ try+ Impl := It.FirstSchObject;+ while Impl <> nil do+ begin+ Impl.ClearAllDatafileLinks;+ Impl.ModelName := FpName;+ Impl.ModelType := 'PCBLIB';+ Impl.AddDataFileLink(FpName, '', 'PCBLIB');+ Impl.IsCurrent := True;+ Found := True;+ Impl := It.NextSchObject;+ end;+ finally+ Cmp.SchIterator_Destroy(It);+ end;+ if not Found then+ begin+ Impl := Cmp.AddSchImplementation;+ Impl.ClearAllDatafileLinks;+ Impl.ModelName := FpName;+ Impl.ModelType := 'PCBLIB';+ Impl.AddDataFileLink(FpName, '', 'PCBLIB');+ Impl.IsCurrent := True;+ end;+ end;++ GStage := 'save';+ Lib.GraphicallyInvalidate;+ SavedOK := False;+ if LastServerDoc <> nil then SavedOK := LastServerDoc.DoFileSave('');+ if SavedOK then SavedStr := 'true' else SavedStr := 'false';+ RespOk(', "renamed": ' + JStr(OldName) + ' -> ' + JStr(NewName) ++ ', "footprint": ' + JStr(FpName) + ', "saved": ' + SavedStr);+end;++{ ---- command: rename a footprint (IPCB_LibComponent) inside a PcbLib, in place ---- }+procedure CmdRenameFootprint;+var+ PcbPath, OldName, NewName, SavedStr : String;+ SD : IServerDocument;+ PcbLib : IPCB_Library;+ Fp : IPCB_LibComponent;+ i : Integer;+ Found : Boolean;+ SavedOK : Boolean;+begin+ PcbPath := Req.Values['pcblib'];+ OldName := Req.Values['footprint'];+ NewName := Req.Values['newname'];+ if (PcbPath = '') or (NewName = '') then+ begin RespErr('need pcblib + newname'); Exit; end;++ GStage := 'open pcblib';+ SD := Client.OpenDocument('PCBLIB', PcbPath);+ if SD <> nil then Client.ShowDocument(SD);+ LastServerDoc := SD;+ if PCBServer = nil then begin RespErr('PCBServer nil'); Exit; end;+ PcbLib := PCBServer.GetCurrentPCBLibrary;+ if PcbLib = nil then begin RespErr('no current PcbLib'); Exit; end;++ GStage := 'rename component';+ Found := False;+ for i := 0 to PcbLib.ComponentCount - 1 do+ begin+ Fp := PcbLib.GetComponent(i);+ if Fp = nil then Continue;+ if (OldName = '') or (Fp.Name = OldName) then+ begin+ Fp.Name := NewName;+ Found := True;+ if OldName <> '' then Break; { rename only the first if OldName given }+ end;+ end;+ if not Found then begin RespErr('footprint not found: ' + OldName); Exit; end;++ GStage := 'save';+ PcbLib.Board.ViewManager_FullUpdate;+ SavedOK := False;+ if LastServerDoc <> nil then SavedOK := LastServerDoc.DoFileSave('');+ if SavedOK then SavedStr := 'true' else SavedStr := 'false';+ RespOk(', "renamed_footprint": ' + JStr(OldName) + ' -> ' + JStr(NewName) ++ ', "saved": ' + SavedStr);+end;++{ ---- command: import a part in ONE round-trip. Given the already-extracted source+ SchLib+PcbLib, it: renames the footprint -> friendly name + saves-as into the+ target PcbLib; attaches LCSC/identity params + renames the symbol + links it+ to the footprint + saves-as into the target SchLib; installs both; and reads+ the params back. Collapses ~8 separate X2 launches into one. Every primitive+ used here is already proven by the per-step verbs. ---- }+procedure CmdImportPart;+var+ SrcSch, SrcPcb, TgtSch, TgtPcb, NewName, NewFp, Desig, OldComp : String;+ PcbSD, SchSD : IServerDocument;+ PcbLib : IPCB_Library;+ Fp : IPCB_LibComponent;+ SchLib : ISch_Lib;+ Cmp : ISch_Component;+ It : ISch_Iterator;+ Impl : ISch_Implementation;+ i : Integer;+ FoundFp, FoundImpl, Saved1, Saved2 : Boolean;+begin+ SrcSch := Req.Values['src_sch'];+ SrcPcb := Req.Values['src_pcb'];+ TgtSch := Req.Values['tgt_sch'];+ TgtPcb := Req.Values['tgt_pcb'];+ NewName := Req.Values['name'];+ NewFp := Req.Values['footprint'];+ Desig := Req.Values['designator']; if Desig = '' then Desig := 'U?';+ if (SrcSch = '') or (SrcPcb = '') or (TgtSch = '') or (TgtPcb = '')+ or (NewName = '') or (NewFp = '') then+ begin RespErr('need src_sch, src_pcb, tgt_sch, tgt_pcb, name, footprint'); Exit; end;++ { 1) footprint: rename first footprint -> NewFp, save-as into target PcbLib }+ GStage := 'open src pcb';+ PcbSD := Client.OpenDocument('PCBLIB', SrcPcb);+ if PcbSD <> nil then Client.ShowDocument(PcbSD);+ if PCBServer = nil then begin RespErr('PCBServer nil'); Exit; end;+ PcbLib := PCBServer.GetCurrentPCBLibrary;+ if PcbLib = nil then begin RespErr('no current PcbLib'); Exit; end;++ GStage := 'rename footprint';+ FoundFp := False;+ for i := 0 to PcbLib.ComponentCount - 1 do+ begin+ Fp := PcbLib.GetComponent(i);+ if Fp <> nil then begin Fp.Name := NewFp; FoundFp := True; Break; end;+ end;+ if not FoundFp then begin RespErr('no footprint in ' + SrcPcb); Exit; end;+ PcbLib.Board.ViewManager_FullUpdate;++ GStage := 'save pcb as target';+ PcbSD.SetFileName(TgtPcb);+ Saved1 := PcbSD.DoFileSave('');++ { 2) symbol: params + rename + footprint link, save-as into target SchLib }+ GStage := 'open src sch';+ SchSD := Client.OpenDocument('SCHLIB', SrcSch);+ if SchSD <> nil then Client.ShowDocument(SchSD);+ SchLib := SchServer.GetSchDocumentByPath(SrcSch);+ if SchLib = nil then begin RespErr('open src sch failed'); Exit; end;+ LastServerDoc := SchSD;++ GStage := 'find component';+ Cmp := FindComponent(SchLib, ''); { first component }+ if Cmp = nil then begin RespErr('no component in ' + SrcSch); Exit; end;+ OldComp := Cmp.LibReference;++ GStage := 'params';+ MaybeSet(Cmp, 'LCSC Part #', 'lcsc');+ MaybeSet(Cmp, 'Manufacturer Part Number', 'mpn');+ MaybeSet(Cmp, 'Manufacturer', 'manufacturer');+ MaybeSet(Cmp, 'Value', 'value');+ MaybeSet(Cmp, 'Description', 'description');+ MaybeSet(Cmp, 'Package', 'package');++ GStage := 'rename + link';+ Cmp.LibReference := NewName;+ Cmp.DesignItemID := NewName;+ Cmp.Designator.Text := Desig;+ FoundImpl := False;+ It := Cmp.SchIterator_Create;+ It.AddFilter_ObjectSet(MkSet(eImplementation));+ try+ Impl := It.FirstSchObject;+ while Impl <> nil do+ begin+ Impl.ClearAllDatafileLinks;+ Impl.ModelName := NewFp;+ Impl.ModelType := 'PCBLIB';+ Impl.AddDataFileLink(NewFp, '', 'PCBLIB');+ Impl.IsCurrent := True;+ FoundImpl := True;+ Impl := It.NextSchObject;+ end;+ finally+ Cmp.SchIterator_Destroy(It);+ end;+ if not FoundImpl then+ begin+ Impl := Cmp.AddSchImplementation;+ Impl.ClearAllDatafileLinks;+ Impl.ModelName := NewFp;+ Impl.ModelType := 'PCBLIB';+ Impl.AddDataFileLink(NewFp, '', 'PCBLIB');+ Impl.IsCurrent := True;+ end;+ SchLib.GraphicallyInvalidate;++ GStage := 'save sch as target';+ SchSD.SetFileName(TgtSch);+ Saved2 := SchSD.DoFileSave('');++ GStage := 'install';+ IntegratedLibraryManager.InstallLibrary(TgtPcb);+ IntegratedLibraryManager.InstallLibrary(TgtSch);++ GStage := 'done';+ RespOk(', "component": ' + JStr(NewName) + ', "footprint": ' + JStr(NewFp) ++ ', "from": ' + JStr(OldComp) ++ ', "saved_pcb": ' + BoolStr(Saved1) + ', "saved_sch": ' + BoolStr(Saved2) ++ ', "params": ' + ParamsJSON(Cmp));+end;++{ ---- command: remove a component (by LibReference) from a SchLib, then save ---- }+procedure CmdRemoveComponent;+var SchLibPath, CompName : String; Lib : ISch_Lib; Cmp, Target : ISch_Component;+ It : ISch_Iterator; SavedOK : Boolean;+begin+ SchLibPath := Req.Values['schlib'];+ CompName := Req.Values['component'];+ if (SchLibPath = '') or (CompName = '') then begin RespErr('need schlib + component'); Exit; end;++ GStage := 'open schlib';+ Lib := OpenSchLibDoc(SchLibPath);+ if Lib = nil then begin RespErr('could not open: ' + SchLibPath); Exit; end;++ GStage := 'find component';+ Target := nil;+ It := Lib.SchLibIterator_Create;+ It.AddFilter_ObjectSet(MkSet(eSchComponent));+ try+ Cmp := It.FirstSchObject;+ while Cmp <> nil do+ begin+ if Cmp.LibReference = CompName then begin Target := Cmp; Break; end;+ Cmp := It.NextSchObject;+ end;+ finally+ Lib.SchIterator_Destroy(It);+ end;+ if Target = nil then begin RespErr('component not found: ' + CompName); Exit; end;++ GStage := 'remove component';+ Lib.RemoveSchComponent(Target);++ GStage := 'save';+ SavedOK := False;+ if LastServerDoc <> nil then SavedOK := LastServerDoc.DoFileSave('');+ if SavedOK then RespOk(', "removed_component": ' + JStr(CompName) + ', "saved": true')+ else RespOk(', "removed_component": ' + JStr(CompName) + ', "saved": false');+end;++{ ---- command: close an open document by its file path (no save prompt if unmodified) ---- }+procedure CmdCloseDoc;+var Path, Kind : String; SD : IServerDocument;+begin+ Path := Req.Values['path'];+ if Path = '' then begin RespErr('missing path'); Exit; end;+ GStage := 'get handle';+ Kind := 'SCHLIB';+ if Pos('.pcblib', LowerCase(Path)) > 0 then Kind := 'PCBLIB';+ SD := Client.OpenDocument(Kind, Path);+ if SD = nil then begin RespOk(', "closed": false, "note": "no handle for path"'); Exit; end;+ GStage := 'CloseDocument';+ Client.CloseDocument(SD);+ RespOk(', "closed": true, "kind": ' + JStr(Kind) + ', "path": ' + JStr(Path));+end;++{ ---- command: list all components (LibReference) in a SchLib ---- }+procedure CmdListSymbols;+var SchLibPath, Acc : String; Lib : ISch_Lib; Cmp : ISch_Component; It : ISch_Iterator; First : Boolean;+begin+ SchLibPath := Req.Values['schlib'];+ Lib := OpenSchLibDoc(SchLibPath);+ if Lib = nil then begin RespErr('could not open: ' + SchLibPath); Exit; end;+ Acc := ''; First := True;+ It := Lib.SchLibIterator_Create;+ It.AddFilter_ObjectSet(MkSet(eSchComponent));+ try+ Cmp := It.FirstSchObject;+ while Cmp <> nil do+ begin+ if not First then Acc := Acc + ', ';+ Acc := Acc + JStr(Cmp.LibReference);+ First := False;+ Cmp := It.NextSchObject;+ end;+ finally+ Lib.SchIterator_Destroy(It);+ end;+ RespOk(', "components": [ ' + Acc + ' ]');+end;++{ ---- command: clone a template component's graphics exactly into a new component,+ set a fresh footprint link + designator. ---- }+procedure CmdCloneSymbol;+var+ SchPath, TemplateName, NewName, FpName, DesigPfx, SavedStr : String;+ Lib : ISch_Lib;+ Tmpl, NewCmp : ISch_Component;+ It : ISch_Iterator;+ Prim, NewPrim : ISch_GraphicalObject;+ Impl : ISch_Implementation;+ SavedOK : Boolean;+ o : Integer;+begin+ SchPath := Req.Values['schlib'];+ TemplateName := Req.Values['template'];+ NewName := Req.Values['component'];+ FpName := Req.Values['footprint'];+ DesigPfx := Req.Values['designator']; if DesigPfx = '' then DesigPfx := 'MC';++ GStage := 'open schlib';+ Lib := OpenSchLibDoc(SchPath);+ if Lib = nil then begin RespErr('open failed'); Exit; end;++ GStage := 'find template';+ Tmpl := FindComponent(Lib, TemplateName);+ if Tmpl = nil then begin RespErr('template not found: ' + TemplateName); Exit; end;++ GStage := 'create new component';+ NewCmp := SchServer.SchObjectFactory(eSchComponent, eCreate_Default);+ NewCmp.CurrentPartID := 1;+ NewCmp.DisplayMode := 0;+ NewCmp.LibReference := NewName;++ GStage := 'copy graphics by ObjectId';+ It := Tmpl.SchIterator_Create; { no type filter — pick graphics by ObjectId }+ try+ Prim := It.FirstSchObject;+ while Prim <> nil do+ begin+ o := Prim.ObjectId;+ { copy pins + ALL graphics; skip only metadata that's set explicitly:+ 25=designator, 27=parameter, 45=comment, 46=implementation }+ if (o <> 25) and (o <> 27) and (o <> 45) and (o <> 46) then+ begin+ NewPrim := Prim.Replicate;+ NewPrim.OwnerPartId := NewCmp.CurrentPartID;+ NewPrim.OwnerPartDisplayMode := NewCmp.DisplayMode;+ NewCmp.AddSchObject(NewPrim);+ end;+ Prim := It.NextSchObject;+ end;+ finally+ Tmpl.SchIterator_Destroy(It);+ end;++ GStage := 'designator';+ NewCmp.Designator.Text := DesigPfx + '?';++ GStage := 'footprint link';+ if FpName <> '' then+ begin+ Impl := NewCmp.AddSchImplementation;+ Impl.ClearAllDatafileLinks;+ Impl.ModelName := FpName;+ Impl.ModelType := 'PCBLIB';+ Impl.AddDataFileLink(FpName, '', 'PCBLIB');+ Impl.IsCurrent := True;+ end;++ GStage := 'AddSchComponent';+ Lib.AddSchComponent(NewCmp);+ SchServer.RobotManager.SendMessage(Lib.I_ObjectAddress, c_BroadCast,+ SCHM_PrimitiveRegistration, NewCmp.I_ObjectAddress);++ GStage := 'save';+ SavedOK := False;+ if LastServerDoc <> nil then SavedOK := LastServerDoc.DoFileSave('');+ if SavedOK then SavedStr := 'true' else SavedStr := 'false';++ RespOk(', "cloned": ' + JStr(TemplateName) + ' -> ' + JStr(NewName) ++ ', "footprint": ' + JStr(FpName) + ', "saved": ' + SavedStr);+end;++{ ---- command: dump a component's primitives (ObjectId + location) ---- }+procedure CmdReadSymbol;+var SchLibPath, CompName, Acc : String; Lib : ISch_Lib; Cmp : ISch_Component;+ It : ISch_Iterator; Prim : ISch_GraphicalObject; First : Boolean;+begin+ SchLibPath := Req.Values['schlib']; CompName := Req.Values['component'];+ Lib := OpenSchLibDoc(SchLibPath);+ if Lib = nil then begin RespErr('open failed'); Exit; end;+ Cmp := FindComponent(Lib, CompName);+ if Cmp = nil then begin RespErr('component not found: ' + CompName); Exit; end;+ Acc := ''; First := True;+ It := Cmp.SchIterator_Create;+ try+ Prim := It.FirstSchObject;+ while Prim <> nil do+ begin+ if not First then Acc := Acc + ', ';+ Acc := Acc + IntToStr(Prim.ObjectId);+ First := False;+ Prim := It.NextSchObject;+ end;+ finally+ Cmp.SchIterator_Destroy(It);+ end;+ RespOk(', "component": ' + JStr(CompName) + ', "primitives": [ ' + Acc + ' ]');+end;++{ ---- command: install a file-based library (.SchLib/.PcbLib/.IntLib) into Available Libraries ---- }+procedure CmdInstallLibrary;+var Path : String;+begin+ Path := Req.Values['path'];+ if Path = '' then begin RespErr('missing path'); Exit; end;+ GStage := 'InstallLibrary';+ IntegratedLibraryManager.InstallLibrary(Path);+ RespOk(', "installed": ' + JStr(Path));+end;++{ ---- command: uninstall available libraries whose path matches any needle ---- }+procedure CmdCleanupLibs;+var Match, Path, Removed : String; i, n : Integer;+begin+ Match := LowerCase(Req.Values['match']);+ if Match = '' then begin RespErr('missing match (comma-separated path substrings)'); Exit; end;+ Removed := '';+ GStage := 'InstalledLibraryCount';+ n := IntegratedLibraryManager.InstalledLibraryCount;+ GStage := 'iterate+uninstall';+ for i := n - 1 downto 0 do+ begin+ Path := IntegratedLibraryManager.InstalledLibraryPath(i);+ if ContainsAny(LowerCase(Path), Match) then+ begin+ IntegratedLibraryManager.UninstallLibrary(Path);+ if Removed <> '' then Removed := Removed + ', ';+ Removed := Removed + JStr(Path);+ end;+ end;+ RespOk(', "removed": [ ' + Removed + ' ]');+end;++procedure CmdPing;+var WS : IWorkspace; n : Integer;+begin+ n := -1;+ try+ WS := GetWorkspace;+ if WS <> nil then n := WS.DM_ProjectCount;+ except+ end;+ RespOk(', "addin": "AdomAltium", "open_project_count": ' + IntToStr(n));+end;++{ ---- command: attach a simple extruded 3D body (a box) to a footprint, sized in+ mils. Gives passives a 3D model without needing a STEP file. ---- }+procedure CmdAddBody;+var+ PcbPath, FpName, SavedStr : String;+ SD : IServerDocument;+ PcbLib : IPCB_Library;+ Fp : IPCB_LibComponent;+ Body : IPCB_ComponentBody;+ Contour : IPCB_Contour;+ bw, bh, hgt, soff, i : Integer;+ Found, SavedOK : Boolean;+begin+ PcbPath := Req.Values['pcblib'];+ FpName := Req.Values['footprint'];+ bw := StrToIntDef(Req.Values['bw_mil'], 39); { body length, mils }+ bh := StrToIntDef(Req.Values['bh_mil'], 20); { body width, mils }+ hgt := StrToIntDef(Req.Values['h_mil'], 16); { body height, mils }+ soff := StrToIntDef(Req.Values['so_mil'], 1); { standoff, mils }+ if (PcbPath = '') or (FpName = '') then begin RespErr('need pcblib + footprint'); Exit; end;++ GStage := 'open pcblib';+ SD := Client.OpenDocument('PCBLIB', PcbPath);+ if SD <> nil then Client.ShowDocument(SD);+ LastServerDoc := SD;+ if PCBServer = nil then begin RespErr('PCBServer nil'); Exit; end;+ PcbLib := PCBServer.GetCurrentPCBLibrary;+ if PcbLib = nil then begin RespErr('no current PcbLib'); Exit; end;++ GStage := 'find footprint';+ Found := False;+ for i := 0 to PcbLib.ComponentCount - 1 do+ begin+ Fp := PcbLib.GetComponent(i);+ if (Fp <> nil) and (Fp.Name = FpName) then begin Found := True; Break; end;+ end;+ if not Found then begin RespErr('footprint not found: ' + FpName); Exit; end;++ GStage := 'create body';+ PCBServer.PreProcess;+ Body := PCBServer.PCBObjectFactory(eComponentBodyObject, eNoDimension, eCreate_Default);+ Body.BodyProjection := eBoardSide_Top;+ Body.StandoffHeight := MilsToCoord(soff);+ Body.OverallHeight := MilsToCoord(soff + hgt);+ Body.BodyColor3D := $00202020;++ GStage := 'outline';+ Contour := PCBServer.PCBContourFactory;+ Contour.AddPoint(MilsToCoord(-(bw div 2)), MilsToCoord(-(bh div 2)));+ Contour.AddPoint(MilsToCoord( (bw div 2)), MilsToCoord(-(bh div 2)));+ Contour.AddPoint(MilsToCoord( (bw div 2)), MilsToCoord( (bh div 2)));+ Contour.AddPoint(MilsToCoord(-(bw div 2)), MilsToCoord( (bh div 2)));+ Body.SetOutlineContour(Contour);++ GStage := 'add to footprint';+ Fp.AddPCBObject(Body);+ PCBServer.SendMessageToRobots(Fp.I_ObjectAddress, c_Broadcast,+ PCBM_BoardRegisteration, Body.I_ObjectAddress);+ PCBServer.PostProcess;++ GStage := 'save';+ LastServerDoc.SetFileName(PcbPath);+ SavedOK := LastServerDoc.DoFileSave('');+ if SavedOK then SavedStr := 'true' else SavedStr := 'false';+ RespOk(', "footprint": ' + JStr(FpName) + ', "body": "extruded", "saved": ' + SavedStr);+end;++{ ---- command: set which parameters are VISIBLE on a component. Names in 'visible'+ (comma list) stay shown; every other parameter is hidden, and the Comment is+ hidden too unless 'Comment' is listed. Controls what appears when the symbol+ is placed on a schematic. ---- }+procedure CmdSetVisibility;+var+ SchPath, CompRef, Vis, SavedStr : String;+ Lib : ISch_Lib;+ Cmp : ISch_Component;+ It : ISch_Iterator;+ P : ISch_Parameter;+ visList : TStringList;+ SavedOK : Boolean;+ shown, hidden : Integer;+begin+ SchPath := Req.Values['schlib'];+ CompRef := Req.Values['component'];+ Vis := Req.Values['visible']; if Vis = '' then Vis := 'Value';+ if SchPath = '' then begin RespErr('missing schlib'); Exit; end;++ GStage := 'open schlib';+ Lib := OpenSchLibDoc(SchPath);+ if Lib = nil then begin RespErr('could not open: ' + SchPath); Exit; end;+ Cmp := FindComponent(Lib, CompRef);+ if Cmp = nil then begin RespErr('component not found: ' + CompRef); Exit; end;++ visList := TStringList.Create;+ visList.CommaText := Vis; { case-insensitive IndexOf by default }+ shown := 0; hidden := 0;+ try+ GStage := 'set parameter visibility';+ It := Cmp.SchIterator_Create;+ It.AddFilter_ObjectSet(MkSet(eParameter));+ try+ P := It.FirstSchObject;+ while P <> nil do+ begin+ if visList.IndexOf(P.Name) >= 0 then+ begin P.IsHidden := False; Inc(shown); end+ else+ begin P.IsHidden := True; Inc(hidden); end;+ P := It.NextSchObject;+ end;+ finally+ Cmp.SchIterator_Destroy(It);+ end;++ GStage := 'comment visibility';+ if visList.IndexOf('Comment') < 0 then Cmp.Comment.IsHidden := True+ else Cmp.Comment.IsHidden := False;+ finally+ visList.Free;+ end;++ GStage := 'save';+ Lib.GraphicallyInvalidate;+ SavedOK := False;+ if LastServerDoc <> nil then SavedOK := LastServerDoc.DoFileSave('');+ if SavedOK then SavedStr := 'true' else SavedStr := 'false';+ RespOk(', "component": ' + JStr(Cmp.LibReference) + ', "visible": ' + JStr(Vis) ++ ', "shown": ' + IntToStr(shown) + ', "hidden": ' + IntToStr(hidden) ++ ', "saved": ' + SavedStr);+end;++{ ---- shared command dispatch. Assumes the global Req is loaded and GReqId set. ---- }+procedure Dispatch;+var Cmd : String;+begin+ try+ Cmd := Req.Values['command'];+ GStage := 'dispatch ' + Cmd;+ if Cmd = 'ping' then CmdPing+ else if Cmd = 'add_lcsc_param' then CmdAddLcscParam+ else if Cmd = 'read_params' then CmdReadParams+ else if Cmd = 'create_and_attach' then CmdCreateAndAttach+ else if Cmd = 'compile_intlib' then CmdCompileIntLib+ else if Cmd = 'extract_intlib' then CmdExtractIntLib+ else if Cmd = 'cleanup_libs' then CmdCleanupLibs+ else if Cmd = 'create_resistor' then CmdCreateResistor+ else if Cmd = 'list_docs' then CmdListDocs+ else if Cmd = 'remove_component' then CmdRemoveComponent+ else if Cmd = 'close_doc' then CmdCloseDoc+ else if Cmd = 'build_footprint' then CmdBuildFootprint+ else if Cmd = 'build_symbol' then CmdBuildSymbol+ else if Cmd = 'install_library' then CmdInstallLibrary+ else if Cmd = 'list_symbols' then CmdListSymbols+ else if Cmd = 'read_symbol' then CmdReadSymbol+ else if Cmd = 'clone_symbol' then CmdCloneSymbol+ else if Cmd = 'rename_component' then CmdRenameComponent+ else if Cmd = 'rename_footprint' then CmdRenameFootprint+ else if Cmd = 'import_part' then CmdImportPart+ else if Cmd = 'add_body' then CmdAddBody+ else if Cmd = 'set_visibility' then CmdSetVisibility+ else RespErr('unknown command: ' + Cmd);+ except+ RespErr('exception at stage: ' + GStage);+ end;+end;++{ ---- one-shot entry (X2 ...>Run): read request.txt, dispatch, write response. ---- }+procedure Run;+begin+ Req := TStringList.Create;+ try+ try+ if FileExists(EX_DIR + 'request.txt') then+ Req.LoadFromFile(EX_DIR + 'request.txt');+ GReqId := Req.Values['id'];+ Dispatch;+ except+ RespErr('exception at stage: ' + GStage);+ end;+ finally+ Req.Free;+ end;+end;
bridge-src/addin_client.pyadded+101@@ -0,0 +1,101 @@+#!/usr/bin/env python3+"""addin_client — drive the in-Altium AdomAltium dispatcher from the bridge.++Runs ON the Windows desktop (the bridge process). For each call it:+ 1. copies the AdomAltium script project into the fixed exchange dir (no spaces),+ 2. writes request.txt (key=value lines) + clears any old response.json,+ 3. launches X2.EXE with -RScriptingSystem:RunScript(...) — single-instance+ forwarding runs the script inside the already-open Altium,+ 4. polls for response.json and returns the parsed result.++The exchange dir is C:\\Users\\Public\\AdomAltium (deliberately space-free so the+RunScript ProjectName needs no quoting, sidestepping cmd/argv escaping issues).++NOTE on speed: Altium has no headless mode and DelphiScript can't host a resident+callback server (a script proc can't bind to a native VCL event), so each command+is one X2.EXE forward. The way we go fast is FEWER commands — see the batched+`import_part` verb, which does a whole part import in a single forward.++A module lock serializes the file exchange because the bridge HTTP server is+threaded (request.txt / response.json are shared, one command at a time).+"""++import json+import os+import shutil+import subprocess+import threading+import time+from pathlib import Path++from altium_detect import detect_altium++EXCHANGE_DIR = r"C:\Users\Public\AdomAltium"+ADDIN_SRC = Path(__file__).parent / "addin" / "AdomAltium"+SCRIPT_FILES = ("AdomAltium.pas", "AdomAltium.PrjScr")++_LOCK = threading.Lock()+++def _ensure_scripts():+ os.makedirs(EXCHANGE_DIR, exist_ok=True)+ for fname in SCRIPT_FILES:+ src = ADDIN_SRC / fname+ if src.exists():+ shutil.copyfile(str(src), os.path.join(EXCHANGE_DIR, fname))+++def _write_request(command, params):+ lines = ["command=" + command]+ for k, v in (params or {}).items():+ if v is None or v == "":+ continue+ # key=value lines: keep values single-line so TStringList.Values parses cleanly+ lines.append("{}={}".format(k, str(v).replace("\r", " ").replace("\n", " ")))+ with open(os.path.join(EXCHANGE_DIR, "request.txt"), "w", encoding="utf-8") as fh:+ fh.write("\n".join(lines) + "\n")+++def addin_call(command, params=None, timeout=90):+ """Invoke a command in the in-Altium dispatcher and return its parsed JSON result."""+ info = detect_altium()+ if not info.get("installed"):+ return {"ok": False, "error": "Altium Designer not installed",+ "_hint": "Run altium_detect to confirm the install path."}+ exe = info["exe"]++ with _LOCK:+ _ensure_scripts()+ resp_path = os.path.join(EXCHANGE_DIR, "response.json")+ try:+ if os.path.exists(resp_path):+ os.remove(resp_path)+ except OSError:+ pass++ _write_request(command, params)++ prjscr = os.path.join(EXCHANGE_DIR, "AdomAltium.PrjScr") # no spaces -> no quoting+ arg = "-RScriptingSystem:RunScript(ProjectName={}|ProcName=AdomAltium>Run)".format(prjscr)+ try:+ # list form -> no shell, no cmd escaping; arg has no spaces so it passes verbatim+ subprocess.Popen([exe, arg], close_fds=True)+ except Exception as e:+ return {"ok": False, "error": "failed to launch Altium: {}".format(e)}++ deadline = time.time() + timeout+ while time.time() < deadline:+ if os.path.exists(resp_path):+ time.sleep(0.15) # let the script finish writing+ try:+ with open(resp_path, "r", encoding="utf-8") as fh:+ return json.load(fh)+ except (OSError, ValueError):+ time.sleep(0.25)+ continue+ time.sleep(0.4)++ return {"ok": False,+ "error": "timeout after {}s waiting for Altium".format(timeout),+ "_hint": "Is Altium running and not blocked by a modal dialog? "+ "The script writes C:\\Users\\Public\\AdomAltium\\response.json."}
bridge-src/altium_detect.pyadded+150@@ -0,0 +1,150 @@+#!/usr/bin/env python3+"""Detect the Altium Designer install + library locations on this Windows machine.++Runs INSIDE the user's desktop (the bridge is spawned by Adom Desktop on Windows),+so it reads the registry and filesystem directly — no relay/shell round-trip.++Verified facts on the reference machine (BARRETTLAND / user barre):+ - Install: C:\\Program Files\\Altium\\AD26\\X2.exe+ - Uninstall key: HKLM\\...\\Uninstall\\Altium Designer {GUID} (DisplayName "Altium Designer 26",+ InstallLocation, DisplayIcon → X2.exe)+ - Config: %LOCALAPPDATA%\\Altium\\Altium Designer {GUID} and %APPDATA%\\Altium\\...+ - Data: C:\\Users\\Public\\Documents\\Altium\\AD26\\{Library,OutputJobs,Templates,Examples}+"""++import os+import glob++try:+ import winreg # type: ignore+except ImportError: # non-Windows (dev/import only)+ winreg = None++UNINSTALL_PATHS = [+ r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall",+ r"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall",+]+++def _reg_altium_uninstall():+ """Walk the Uninstall keys for an entry whose DisplayName starts with 'Altium Designer'.++ Returns dict {display_name, display_version, install_location, display_icon} or None.+ """+ if winreg is None:+ return None+ for base in UNINSTALL_PATHS:+ try:+ root = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, base)+ except OSError:+ continue+ i = 0+ while True:+ try:+ sub = winreg.EnumKey(root, i)+ except OSError:+ break+ i += 1+ if "Altium Designer" not in sub:+ continue+ try:+ k = winreg.OpenKey(root, sub)+ except OSError:+ continue++ def _get(name):+ try:+ return winreg.QueryValueEx(k, name)[0]+ except OSError:+ return None++ name = _get("DisplayName")+ if not name or not str(name).startswith("Altium Designer"):+ continue+ return {+ "display_name": name,+ "display_version": _get("DisplayVersion"),+ "install_location": _get("InstallLocation"),+ "display_icon": _get("DisplayIcon"),+ }+ return None+++def _scan_program_files():+ """Fallback: find the newest C:\\Program Files\\Altium\\AD<NN>\\X2.exe."""+ candidates = []+ for pf in (os.environ.get("ProgramFiles", r"C:\Program Files"),+ os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")):+ for exe in glob.glob(os.path.join(pf, "Altium", "AD*", "X2.exe")):+ ad = os.path.basename(os.path.dirname(exe)) # e.g. AD26+ num = "".join(ch for ch in ad if ch.isdigit())+ candidates.append((int(num or 0), ad, exe))+ candidates.sort(reverse=True)+ return candidates[0] if candidates else None+++def _config_dirs():+ """Find the Altium config dirs (carry the {GUID} and any Workspace/365 state)."""+ out = []+ for env in ("LOCALAPPDATA", "APPDATA"):+ base = os.path.join(os.environ.get(env, ""), "Altium")+ if os.path.isdir(base):+ for d in os.listdir(base):+ if d.startswith("Altium Designer"):+ out.append(os.path.join(base, d))+ return out+++def detect_altium():+ """Return a structured snapshot of the Altium install on this machine.++ {installed, exe, version, install_location, config_dirs[], data_dir,+ library_dir, outputjobs_dir}+ """+ reg = _reg_altium_uninstall()+ exe = None+ install_location = None+ version = None++ if reg:+ version = reg.get("display_version") or reg.get("display_name")+ install_location = reg.get("install_location")+ icon = reg.get("display_icon")+ if icon and icon.lower().endswith("x2.exe") and os.path.isfile(icon):+ exe = icon+ elif install_location:+ cand = os.path.join(install_location, "X2.exe")+ if os.path.isfile(cand):+ exe = cand++ if not exe:+ scanned = _scan_program_files()+ if scanned:+ _, ad, exe = scanned+ install_location = os.path.dirname(exe)+ version = version or ad++ # Public data tree — derive AD<NN> from the install dir name when possible.+ ad_name = os.path.basename(install_location) if install_location else "AD26"+ public = os.path.join(+ os.environ.get("PUBLIC", r"C:\Users\Public"), "Documents", "Altium", ad_name+ )+ data_dir = public if os.path.isdir(public) else None+ library_dir = os.path.join(public, "Library") if data_dir else None+ outputjobs_dir = os.path.join(public, "OutputJobs") if data_dir else None++ return {+ "installed": bool(exe),+ "exe": exe,+ "version": version,+ "install_location": install_location,+ "config_dirs": _config_dirs(),+ "data_dir": data_dir,+ "library_dir": library_dir,+ "outputjobs_dir": outputjobs_dir,+ }+++if __name__ == "__main__":+ import json+ print(json.dumps(detect_altium(), indent=2))
bridge-src/bridge.jsonadded+73@@ -0,0 +1,73 @@+{+ "manifest_version": 1,+ "name": "altium",+ "displayName": "Altium Bridge",+ "version": "0.0.33",+ "description": "Adds components to an Altium Designer library (Integrated, Database, or Altium 365) with the LCSC/JLCPCB part number attached as a parameter, and exports JLC-ready BOM + pick-and-place (CPL). Pairs with the container-side altium-jlc orchestrator (adom-jlcpcb + chip-fetcher).",+ "homepage": "https://wiki.adom.inc/adom/adom-desktop-altium-bridge",+ "author": "Adom Inc.",+ "license": "MIT",+ "spawn": {+ "kind": "python",+ "entrypoint": "server.py",+ "port": 8801,+ "healthEndpoint": "/status",+ "stopMethod": "kill",+ "killImageName": "python.exe"+ },+ "verbPrefixes": [+ "altium_"+ ],+ "verbs": [+ "altium_ping",+ "altium_detect",+ "altium_addin_ping",+ "altium_add_component",+ "altium_read_params",+ "altium_create_and_attach",+ "altium_compile_intlib",+ "altium_extract_intlib",+ "altium_cleanup_libs",+ "altium_create_resistor",+ "altium_build_footprint",+ "altium_build_symbol",+ "altium_clone_symbol",+ "altium_install_library",+ "altium_list_symbols",+ "altium_read_symbol",+ "altium_list_docs",+ "altium_remove_component",+ "altium_close_doc",+ "altium_rename_component",+ "altium_rename_footprint",+ "altium_import_part",+ "altium_add_body",+ "altium_set_visibility"+ ],+ "dependencies": {+ "python": ">=3.8"+ },+ "category": "CAD",+ "tags": [+ "altium",+ "lcsc",+ "jlcpcb",+ "bom",+ "cpl",+ "library"+ ],+ "docs": "https://wiki.adom.inc/adom/adom-desktop-altium-bridge",+ "platforms": {+ "windows": {+ "supported": true+ },+ "macos": {+ "supported": false,+ "reason": "Altium Designer is Windows-only; no host app to drive on macOS."+ },+ "linux": {+ "supported": false,+ "reason": "Altium Designer is Windows-only; no host app to drive on Linux."+ }+ }+}
bridge-src/deploy.shadded+42@@ -0,0 +1,42 @@+#!/usr/bin/env bash+# deploy.sh — build + publish + (re)install the altium bridge in one shot.+# Usage: ./deploy.sh <new-version> e.g. ./deploy.sh 0.0.30+set -euo pipefail++VER="${1:?usage: deploy.sh <version>}"+ROOT="/home/adom/project"+DIR="$ROOT/altium-bridge"+ZIP="$ROOT/altium-bridge-v${VER}.zip"+MANIFEST="$ROOT/altium-bridge-manifest.json"+BASE="https://barrett-land-dashboard-35mvr0hdyp1h.adom.cloud/proxy/8088"+KILL_PS1="C:\\Users\\barre\\Downloads\\altium\\kill_altium_bridge.ps1"++echo "$VER" > "$DIR/BRIDGE_VERSION"+python3 - "$DIR/bridge.json" "$VER" <<'PY'+import json, sys+p, ver = sys.argv[1], sys.argv[2]+d = json.load(open(p)); d["version"] = ver+json.dump(d, open(p, "w"), indent=2)+PY++rm -f "$ZIP"+( cd "$DIR" && zip -rq "$ZIP" . -x '*__pycache__*' -x '*.pyc' )+SHA=$(sha256sum "$ZIP" | cut -d' ' -f1)+SIZE=$(stat -c%s "$ZIP")+cat > "$MANIFEST" <<EOF+{ "manifest_version": 1, "name": "altium", "version": "${VER}",+ "url": "${BASE}/altium-bridge-v${VER}.zip",+ "sha256": "${SHA}", "size": ${SIZE}, "released_at": "2026-06-19T00:00:00Z" }+EOF+echo "built v${VER} size=${SIZE} sha=${SHA}"++# kill ONLY the altium bridge python PIDs (never a broad python kill), then reinstall.+# `|| true` so a "no running process" case doesn't abort the deploy under pipefail.+{ adom-desktop shell_execute "{\"command\":\"powershell -NoProfile -File ${KILL_PS1}\"}" \+ | python3 -c "import sys,json;print('kill:', (json.load(sys.stdin).get('output') or '').strip() or 'no match')"; } || true+sleep 2+adom-desktop bridge_install "{\"manifestUrl\":\"${BASE}/altium-bridge-manifest.json\"}" \+ | python3 -c "import sys,json;d=json.load(sys.stdin);o=json.loads(d['output']) if isinstance(d.get('output'),str) and d['output'] else d;print('install ok:', o.get('ok'), o.get('error') or '')"+sleep 1+adom-desktop altium_ping '{}' \+ | python3 -c "import sys,json;d=json.load(sys.stdin);o=json.loads(d.get('output','{}')) if d.get('output') else {};print('bridge now v', o.get('version'))"
bridge-src/server.pyadded+339@@ -0,0 +1,339 @@+#!/usr/bin/env python3+"""Adom Altium Bridge — localhost HTTP server (runs on the user's Windows desktop).++Spawned on demand by Adom Desktop (v1.8.0+ dynamic bridge). Implements the standard+dynamic-bridge contract:+ GET /status -> {"ok": true, "version": "...", ...}+ POST /command body {"command"|"verb": "altium_*", "args": {...}}+ -> {"success": true, "output": "<json-string>"} | {"success": false, "error", "_hint"}++Phase 0 verbs: altium_ping, altium_detect. Later phases add the IntLib/DbLib/365+component-add verbs and BOM/CPL export (those route to the in-Altium add-in on :8775).++Usage: python server.py [--port 8801]+"""++import json+import sys+import traceback+from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler+from pathlib import Path++sys.path.insert(0, str(Path(__file__).parent))++from altium_detect import detect_altium+from addin_client import addin_call++DEFAULT_PORT = 8801++VERSION = (Path(__file__).parent / "BRIDGE_VERSION").read_text().strip() \+ if (Path(__file__).parent / "BRIDGE_VERSION").exists() else "0.0.0"++# Address of the in-Altium add-in (Phase 2+); not used by Phase 0 verbs.+ADDIN_URL = "http://127.0.0.1:8775"+++def handle_ping(args: dict) -> dict:+ """Liveness + a quick Altium-presence summary."""+ info = detect_altium()+ return {+ "ok": True,+ "bridge": "altium",+ "version": VERSION,+ "altiumInstalled": info["installed"],+ "altiumVersion": info.get("version"),+ "altiumExe": info.get("exe"),+ "addinUrl": ADDIN_URL,+ }+++def handle_detect(args: dict) -> dict:+ """Full Altium install + library-location snapshot for this machine."""+ info = detect_altium()+ info["ok"] = True+ if not info["installed"]:+ info["_hint"] = (+ "Altium Designer not found. Expected C:\\Program Files\\Altium\\AD<NN>\\X2.exe "+ "or an 'Altium Designer' Uninstall registry entry."+ )+ return info+++def handle_addin_ping(args: dict) -> dict:+ """Round-trip the in-Altium dispatcher (proves request/response works live)."""+ return addin_call("ping", {}, timeout=args.get("timeout", 60))+++def handle_add_component(args: dict) -> dict:+ """Add/refresh a component's identity params (incl. 'LCSC Part #') in a SchLib."""+ if not args.get("schlib"):+ return {"ok": False, "error": "missing 'schlib' (path to the .SchLib on the desktop)"}+ keys = ("schlib", "component", "lcsc", "mpn", "manufacturer",+ "value", "description", "package", "datasheet")+ return addin_call("add_lcsc_param", {k: args.get(k) for k in keys},+ timeout=args.get("timeout", 90))+++def handle_read_params(args: dict) -> dict:+ """Read back a component's parameters (verification)."""+ return addin_call("read_params",+ {"schlib": args.get("schlib"), "component": args.get("component")},+ timeout=args.get("timeout", 60))+++def handle_create_and_attach(args: dict) -> dict:+ """Create a fresh SchLib + component, attach LCSC/identity params, read back, save.+ No external symbol source needed — proves the keystone end to end."""+ keys = ("component", "schlib", "lcsc", "mpn", "manufacturer", "value", "description")+ return addin_call("create_and_attach", {k: args.get(k) for k in keys},+ timeout=args.get("timeout", 90))+++def handle_compile_intlib(args: dict) -> dict:+ """Generate a .LibPkg wrapping the .SchLib, then compile it to .IntLib."""+ import os+ schlib = args.get("schlib") or r"C:\Users\Public\AdomAltium\Adom.SchLib"+ d = os.path.dirname(schlib)+ libname = os.path.splitext(os.path.basename(schlib))[0]+ libpkg = os.path.join(d, libname + ".LibPkg")+ # CreateIntegratedLibrary wants the full .IntLib FILE path, not a folder.+ output = args.get("output_file") or os.path.join(d, libname + ".IntLib")+ # .LibPkg is an INI project file; reference the SchLib by relative name (same dir).+ content = (+ "[Design]\r\n"+ "Version=1.0\r\n"+ "HierarchyMode=1\r\n"+ "\r\n"+ "[Document1]\r\n"+ "DocumentPath=" + os.path.basename(schlib) + "\r\n"+ )+ try:+ with open(libpkg, "w", encoding="utf-8") as fh:+ fh.write(content)+ except OSError as e:+ return {"ok": False, "error": "could not write LibPkg: {}".format(e)}+ return addin_call("compile_intlib", {"libpkg": libpkg, "output": output},+ timeout=args.get("timeout", 180))+++def handle_extract_intlib(args: dict) -> dict:+ """Decompile a compiled .IntLib into editable source libs (.SchLib + .PcbLib);+ returns the extracted document paths so the caller can edit them."""+ if not args.get("intlib"):+ return {"ok": False, "error": "missing 'intlib' (path to the .IntLib on the desktop)"}+ return addin_call("extract_intlib", {"intlib": args.get("intlib")},+ timeout=args.get("timeout", 150))+++def handle_create_resistor(args: dict) -> dict:+ """Create a 2-pin resistor symbol with params + footprint link, save to a Resistors lib."""+ import os+ schlib = args.get("schlib") or r"C:\Users\Public\AdomLibs\Resistors.SchLib"+ try:+ os.makedirs(os.path.dirname(schlib), exist_ok=True)+ except OSError:+ pass+ payload = {+ "schlib": schlib,+ "component": args.get("component") or "R",+ "lcsc": args.get("lcsc"),+ "mpn": args.get("mpn"),+ "manufacturer": args.get("manufacturer"),+ "value": args.get("value"),+ "description": args.get("description"),+ "footprint": args.get("footprint") or "R0603",+ "footprint_lib": args.get("footprint_lib") or "Resistors.PcbLib",+ }+ return addin_call("create_resistor", payload, timeout=args.get("timeout", 120))+++def handle_clone_symbol(args: dict) -> dict:+ keys = ("schlib", "template", "component", "footprint", "designator")+ return addin_call("clone_symbol", {k: args.get(k) for k in keys}, timeout=args.get("timeout", 120))+++def handle_rename_component(args: dict) -> dict:+ """Rename a SchLib component in place; optionally update Description, footprint+ link, and designator. Preserves the exact symbol graphics/pins/params."""+ keys = ("schlib", "component", "newname", "footprint", "designator", "description")+ return addin_call("rename_component", {k: args.get(k) for k in keys},+ timeout=args.get("timeout", 120))+++def handle_rename_footprint(args: dict) -> dict:+ """Rename a footprint (IPCB_LibComponent) inside a PcbLib in place."""+ keys = ("pcblib", "footprint", "newname")+ return addin_call("rename_footprint", {k: args.get(k) for k in keys},+ timeout=args.get("timeout", 120))+++def handle_set_visibility(args: dict) -> dict:+ """Set which parameters are visible on a component (others hidden); controls+ what shows when the symbol is placed on a schematic."""+ keys = ("schlib", "component", "visible")+ return addin_call("set_visibility", {k: args.get(k) for k in keys},+ timeout=args.get("timeout", 90))+++def handle_add_body(args: dict) -> dict:+ """Attach a simple extruded 3D body (box, sized in mils) to a footprint."""+ keys = ("pcblib", "footprint", "bw_mil", "bh_mil", "h_mil", "so_mil")+ return addin_call("add_body", {k: args.get(k) for k in keys},+ timeout=args.get("timeout", 120))+++def handle_import_part(args: dict) -> dict:+ """Batched single-round-trip import: from already-extracted src SchLib+PcbLib,+ rename footprint + save-as into the target PcbLib; attach LCSC/params + rename+ symbol + link + save-as into the target SchLib; install both; read back params."""+ keys = ("src_sch", "src_pcb", "tgt_sch", "tgt_pcb", "name", "footprint",+ "designator", "lcsc", "mpn", "manufacturer", "value", "description", "package")+ return addin_call("import_part", {k: args.get(k) for k in keys},+ timeout=args.get("timeout", 180))+++def handle_list_symbols(args: dict) -> dict:+ return addin_call("list_symbols", {"schlib": args.get("schlib")}, timeout=args.get("timeout", 60))+++def handle_read_symbol(args: dict) -> dict:+ return addin_call("read_symbol",+ {"schlib": args.get("schlib"), "component": args.get("component")},+ timeout=args.get("timeout", 60))+++def handle_install_library(args: dict) -> dict:+ """Register a file-based library (already on the desktop) into Available Libraries."""+ return addin_call("install_library", {"path": args.get("path")}, timeout=args.get("timeout", 60))+++def handle_build_symbol(args: dict) -> dict:+ """Build/append a symbol (N pins + footprint-implementation link) into a SchLib."""+ return addin_call("build_symbol", args, timeout=args.get("timeout", 120))+++def handle_build_footprint(args: dict) -> dict:+ """Build an Altium footprint from a pad spec + optional STEP body. The STEP must+ already be on the desktop (the orchestrator send_files it first). Pads come as+ pad0..padN keys (CommaText: smd,x,y,w,h,drill,name in mils)."""+ return addin_call("build_footprint", args, timeout=args.get("timeout", 150))+++def handle_list_docs(args: dict) -> dict:+ return addin_call("list_docs", {}, timeout=args.get("timeout", 60))+++def handle_remove_component(args: dict) -> dict:+ return addin_call("remove_component",+ {"schlib": args.get("schlib"), "component": args.get("component")},+ timeout=args.get("timeout", 90))+++def handle_close_doc(args: dict) -> dict:+ return addin_call("close_doc", {"path": args.get("path")}, timeout=args.get("timeout", 60))+++def handle_cleanup_libs(args: dict) -> dict:+ """Uninstall available libraries whose path matches any comma-separated needle."""+ match = args.get("match") or "adomaltium,schlib1,adom.intlib,adom.schlib"+ return addin_call("cleanup_libs", {"match": match}, timeout=args.get("timeout", 60))+++COMMAND_HANDLERS = {+ "altium_ping": handle_ping,+ "altium_detect": handle_detect,+ "altium_compile_intlib": handle_compile_intlib,+ "altium_extract_intlib": handle_extract_intlib,+ "altium_cleanup_libs": handle_cleanup_libs,+ "altium_addin_ping": handle_addin_ping,+ "altium_add_component": handle_add_component,+ "altium_read_params": handle_read_params,+ "altium_create_and_attach": handle_create_and_attach,+ "altium_create_resistor": handle_create_resistor,+ "altium_list_docs": handle_list_docs,+ "altium_remove_component": handle_remove_component,+ "altium_close_doc": handle_close_doc,+ "altium_build_footprint": handle_build_footprint,+ "altium_build_symbol": handle_build_symbol,+ "altium_install_library": handle_install_library,+ "altium_list_symbols": handle_list_symbols,+ "altium_read_symbol": handle_read_symbol,+ "altium_clone_symbol": handle_clone_symbol,+ "altium_rename_component": handle_rename_component,+ "altium_rename_footprint": handle_rename_footprint,+ "altium_import_part": handle_import_part,+ "altium_add_body": handle_add_body,+ "altium_set_visibility": handle_set_visibility,+}+++def dispatch(verb: str, args: dict) -> dict:+ handler = COMMAND_HANDLERS.get(verb)+ if handler is None:+ return {+ "success": False,+ "error": f"Unknown verb: {verb}",+ "_hint": f"Valid altium verbs: {', '.join(sorted(COMMAND_HANDLERS))}.",+ }+ payload = handler(args or {})+ # Dynamic-bridge contract: success envelope with output as a JSON string.+ return {"success": True, "output": json.dumps(payload)}+++class AltiumBridgeHandler(BaseHTTPRequestHandler):+ def do_GET(self):+ if self.path == "/status":+ self._respond(200, {"ok": True, "bridge": "altium", "version": VERSION})+ else:+ self._respond(404, {"ok": False, "error": "Not found"})++ def do_POST(self):+ if self.path != "/command":+ self._respond(404, {"success": False, "error": "Not found"})+ return+ length = int(self.headers.get("Content-Length", 0))+ try:+ request = json.loads(self.rfile.read(length) or b"{}")+ except json.JSONDecodeError as e:+ self._respond(400, {"success": False, "error": f"Invalid JSON: {e}"})+ return+ verb = request.get("command") or request.get("verb") or ""+ args = request.get("args", {})+ print(f"[Altium Bridge] {verb} | {json.dumps(args)}")+ try:+ self._respond(200, dispatch(verb, args))+ except Exception as e:+ traceback.print_exc()+ self._respond(500, {"success": False, "error": f"Internal error: {e}"})++ def _respond(self, status: int, data: dict):+ body = json.dumps(data).encode("utf-8")+ self.send_response(status)+ self.send_header("Content-Type", "application/json")+ self.send_header("Content-Length", str(len(body)))+ self.end_headers()+ self.wfile.write(body)++ def log_message(self, fmt, *args):+ print(f"[Altium Bridge] {args[0]} {args[1]} {args[2]}")+++def main():+ port = DEFAULT_PORT+ if "--port" in sys.argv:+ idx = sys.argv.index("--port")+ if idx + 1 < len(sys.argv):+ port = int(sys.argv[idx + 1])+ info = detect_altium()+ print(f"[Altium Bridge] v{VERSION} | Altium installed: {info['installed']} "+ f"({info.get('version')}) exe={info.get('exe')}")+ server = ThreadingHTTPServer(("127.0.0.1", port), AltiumBridgeHandler)+ print(f"[Altium Bridge] Listening on http://127.0.0.1:{port} (status: /status)")+ try:+ server.serve_forever()+ except KeyboardInterrupt:+ server.server_close()+++if __name__ == "__main__":+ main()
bridge.jsonadded+73@@ -0,0 +1,73 @@+{+ "manifest_version": 1,+ "name": "altium",+ "displayName": "Altium Bridge",+ "version": "0.0.33",+ "description": "Adds components to an Altium Designer library (Integrated, Database, or Altium 365) with the LCSC/JLCPCB part number attached as a parameter, and exports JLC-ready BOM + pick-and-place (CPL). Pairs with the container-side altium-jlc orchestrator (adom-jlcpcb + chip-fetcher).",+ "homepage": "https://wiki.adom.inc/adom/adom-desktop-altium-bridge",+ "author": "Adom Inc.",+ "license": "MIT",+ "spawn": {+ "kind": "python",+ "entrypoint": "server.py",+ "port": 8801,+ "healthEndpoint": "/status",+ "stopMethod": "kill",+ "killImageName": "python.exe"+ },+ "verbPrefixes": [+ "altium_"+ ],+ "verbs": [+ "altium_ping",+ "altium_detect",+ "altium_addin_ping",+ "altium_add_component",+ "altium_read_params",+ "altium_create_and_attach",+ "altium_compile_intlib",+ "altium_extract_intlib",+ "altium_cleanup_libs",+ "altium_create_resistor",+ "altium_build_footprint",+ "altium_build_symbol",+ "altium_clone_symbol",+ "altium_install_library",+ "altium_list_symbols",+ "altium_read_symbol",+ "altium_list_docs",+ "altium_remove_component",+ "altium_close_doc",+ "altium_rename_component",+ "altium_rename_footprint",+ "altium_import_part",+ "altium_add_body",+ "altium_set_visibility"+ ],+ "dependencies": {+ "python": ">=3.8"+ },+ "category": "CAD",+ "tags": [+ "altium",+ "lcsc",+ "jlcpcb",+ "bom",+ "cpl",+ "library"+ ],+ "docs": "https://wiki.adom.inc/adom/adom-desktop-altium-bridge",+ "platforms": {+ "windows": {+ "supported": true+ },+ "macos": {+ "supported": false,+ "reason": "Altium Designer is Windows-only; no host app to drive on macOS."+ },+ "linux": {+ "supported": false,+ "reason": "Altium Designer is Windows-only; no host app to drive on Linux."+ }+ }+}
docs/hero.pngadded⋯ 1 unchanged line ⋯
install.shadded+8@@ -0,0 +1,8 @@+#!/bin/bash+set -euo pipefail+H="$(cd "$(dirname "$0")"&&pwd)"+mkdir -p "$HOME/.claude/skills/adom-desktop-altium-bridge"+cp -f "$H/SKILL.md" "$HOME/.claude/skills/adom-desktop-altium-bridge/SKILL.md"+echo "[adom-desktop-altium-bridge] skill installed."+echo "Install the bridge onto your Windows desktop with:"+echo " adom-desktop bridge_install '{\"manifestUrl\":\"https://wiki.adom.inc/static/apps/adom-desktop-altium-bridge/altium-bridge-manifest.json\"}'"
package.jsonadded+26@@ -0,0 +1,26 @@+{+ "slug": "adom-desktop-altium-bridge",+ "version": "0.0.36",+ "type": "app",+ "description": "Adom Desktop bridge for Altium Designer \u2014 add LCSC/JLCPCB-tagged parts to file-based libraries (symbols, footprints with 3D, value-only display) by driving Altium via DelphiScript.",+ "tags": [+ "app",+ "adom-desktop",+ "bridge",+ "altium",+ "lcsc",+ "jlcpcb"+ ],+ "dependencies": {},+ "scripts": {+ "install": "./install.sh",+ "uninstall": "./uninstall.sh"+ },+ "needs_sudo": false,+ "platform": "windows",+ "hero": {+ "headline": "Altium Bridge for Adom Desktop",+ "subhead": "Drive Altium Designer from cloud Claude \u2014 add LCSC-tagged parts to your libraries",+ "screenshot": "docs/hero.png"+ }+}
uninstall.shadded+4@@ -0,0 +1,4 @@+#!/bin/bash+set -euo pipefail+rm -rf "$HOME/.claude/skills/adom-desktop-altium-bridge"+echo "[adom-desktop-altium-bridge] skill removed (desktop bridge: adom-desktop bridge_uninstall '{\"name\":\"altium\"}')."