Skill/scripts/altium_params.py

190 lines
6.4 KiB
Python
Executable File

#!/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()