#!/usr/bin/env python3 """Generate an Altium DelphiScript that stamps the mandatory parameters (SOP ยง5) onto a schematic-library symbol, from a params.json the skill builds off the datasheet. Why a script instead of editing the .SchLib directly: a .SchLib is an OLE compound-binary that we can read but cannot safely grow from outside Altium (adding parameter records means rewriting Altium's own format). So the skill produces the parameter *values* from the datasheet and emits a small script the engineer runs inside Altium (DXP -> Run Script -> ApplyParameters), which adds/updates exactly these parameters on the component and sets the Comment to the MPN. See references/schlib_parameters.md for the parameter set and each value's source. params.json: { "component": "CC0402FRNPO9BN120", # Library Ref of the target symbol; omit -> all comps "comment": "CC0402FRNPO9BN120", # sets the Comment field (usually the MPN) "parameters": {"Value": "12pF_0402", "Manufacturer": "YAGEO", ...} } Usage: python altium_params.py script --params params.json --out apply_params.pas python altium_params.py script --params params.json # prints to stdout The generated .pas is plain text; validate its first run on a new Altium version, since the scripting API can differ slightly between releases. """ import argparse, json, sys def _q(s): """Quote a value as a DelphiScript single-quoted string literal (doubling embedded quotes). None/blank -> ''. Non-strings are stringified.""" if s is None: s = "" return "'" + str(s).replace("'", "''") + "'" PROLOGUE = """{ Auto-generated by library-manager (altium_params.py). Stamps the SOP mandatory parameters onto a schematic-library symbol. HOW TO RUN: open the target .SchLib in Altium, then DXP -> Run Script -> pick this file -> run 'ApplyParameters'. It adds each parameter if missing, updates it if present, sets the Comment field, hides blank-valued parameters, and leaves the library open for you to review and Save to Server (with a revision note, per the SOP). } Var Lib : ISch_Lib; LibIter : ISch_Iterator; Comp : ISch_Component; TargetRef: String; CommentText : String; Procedure SetParam(AComp : ISch_Component; AName : String; AValue : String); Var PIter : ISch_Iterator; P : ISch_Parameter; Found : ISch_Parameter; Begin Found := Nil; PIter := AComp.SchIterator_Create; PIter.AddFilter_ObjectSet(MkSet(eParameter)); Try P := PIter.FirstSchObject; While P <> Nil Do Begin If P.Name = AName Then Found := P; P := PIter.NextSchObject; End; Finally AComp.SchIterator_Destroy(PIter); End; If Found <> Nil Then Begin Found.Text := AValue; Found.IsHidden := (AValue = ''); End Else Begin Found := SchServer.SchObjectFactory(eParameter, eCreate_GlobalCopy); Found.Name := AName; Found.Text := AValue; Found.IsHidden := (AValue = ''); AComp.AddSchObject(Found); End; End; Procedure ApplyToComponent(AComp : ISch_Component); Begin SchServer.RobotManager.SendMessage(AComp.I_ObjectAddress, c_BroadCast, SCHM_BeginModify, c_NoEventData); Try If CommentText <> '' Then AComp.Comment.Text := CommentText; """ EPILOGUE = """ Finally SchServer.RobotManager.SendMessage(AComp.I_ObjectAddress, c_BroadCast, SCHM_EndModify, c_NoEventData); End; End; Procedure ApplyParameters; Begin If SchServer = Nil Then Begin ShowMessage('Schematic server not available.'); Exit; End; Lib := SchServer.GetCurrentSchDocument; If Lib = Nil Then Begin ShowMessage('Open the target .SchLib first.'); Exit; End; If Lib.ObjectID <> eSchLib Then Begin ShowMessage('The active document is not a .SchLib.'); Exit; End; LibIter := Lib.SchLibIterator_Create; LibIter.AddFilter_ObjectSet(MkSet(eSchComponent)); Try Comp := LibIter.FirstSchObject; While Comp <> Nil Do Begin If (TargetRef = '') Or (Comp.LibReference = TargetRef) Then ApplyToComponent(Comp); Comp := LibIter.NextSchObject; End; Finally Lib.SchLibIterator_Destroy(LibIter); End; Lib.GraphicallyInvalidate; ShowMessage('Parameters applied. Review the component, then Save to Server.'); End; """ def generate(params): comp = params.get("component", "") or "" comment = params.get("comment", "") or "" fields = params.get("parameters", {}) or {} setparam_calls = "\n".join( f" SetParam(AComp, {_q(name)}, {_q(value)});" for name, value in fields.items() ) # TargetRef / CommentText are globals set by InitTargets, called first in ApplyParameters. init_block = ( "Procedure InitTargets;\n" "Begin\n" f" TargetRef := {_q(comp)};\n" f" CommentText := {_q(comment)};\n" "End;\n\n" ) body = PROLOGUE + setparam_calls + "\n" + EPILOGUE # Ensure InitTargets is called first inside ApplyParameters. body = body.replace("Procedure ApplyParameters;\nBegin\n", "Procedure ApplyParameters;\nBegin\n InitTargets;\n") # Place InitTargets AFTER the global Var block (Pascal: declarations before use), i.e. just # before the first procedure. The Var block lives at the top of PROLOGUE. return body.replace("Procedure SetParam(AComp", init_block + "Procedure SetParam(AComp", 1) def main(): ap = argparse.ArgumentParser() sub = ap.add_subparsers(dest="cmd", required=True) s = sub.add_parser("script", help="emit the Altium DelphiScript from a params.json") s.add_argument("--params", required=True) s.add_argument("--out", help="write the .pas here (default: stdout)") a = ap.parse_args() params = json.load(open(a.params, encoding="utf-8")) pas = generate(params) if a.out: with open(a.out, "w", encoding="utf-8") as f: f.write(pas) n = len(params.get("parameters", {})) print(f"wrote {a.out} ({n} parameters" + (f", component '{params['component']}'" if params.get("component") else ", all components") + ")") else: sys.stdout.write(pas) if __name__ == "__main__": main()