Advanced HiWIRE®The HiWIRE® editor is designed to be extended by using Python scripts. Python is a high-level programming language that features an elegant, crystal-clear syntax. It is a prime counterexample to Greenspun's Tenth Rule Despite Python's simplicity, we won't attempt to teach it here. There are several good books available. This writer found Martelli's Python in a Nutshell (O'Reilly Press, 2003-2006) and Zelle's Python Programming (Franklin, Beedle & Associates, 2003) to be particularly cogent and well written. Another good source is the official Python website where you'll find the Tutorial and Language Reference. Seeing the Big PictureEach time you start the HiWIRE editor, it locates and runs a special script, setup.pyo. This precompiled script extracts various preferences and settings from a companion script, Prefs.py, and transmits these settings back to the editor where they are used to configure the Scripts menu, the default color scheme, physical units, scaling, etc. Subsequently, when you select an item from its Scripts menu, the editor delegates a Python script to carry out the command. The specific script it uses was set earlier by the Menu = … setting in Prefs.py. So that HiWIRE can find them, you must place all delegate scripts into a subdirectory, scripts/, of the directory holding the editor executable (ee or ee.exe). In this subdirectory, you'll also find setup.pyo, Prefs.py, render.py (the printing delegate), gerber.py (the photoplotting delegate), netlist.py (the netlisting delegate), and a host of other ancillary scripts. While the delegate script runs, it becomes a client, and the editor becomes its server. The two programs communicate via interprocess channels commonly known as pipes. On the client side, the communication protocol is implemented by the pywire extension module, pywire.so (Linux, Mac) or pywire.pyd (Windows). Loosely speaking, the client formulates questions about the drawing and sends them along to the server with the help of the extension. The server responds with the answers; the extension translates them into Python and delivers them back to the client. Often, the answer to a question asked by the client is one or more drawing objects. Pywire delivers these answers as Python tuples: Some Pywire Answers
(Id, 'LAB’, Text, Attribute) Where:
Importing and Using PywireLike any other Python extension, to use the features of pywire, you must first import it. Pywire provides two functions that establish and terminate the communication with the server. After establishing communication, your script should be sure to terminate them in an orderly manner before it exits. Starting and Stopping Communication
import pywire # Exposes pywire functionality to your script
try: # After all, something could go wrong!
pywire.start(...) # Summon our servant, the editor
: # Real action omitted!
finally: # Whether or not something went wrong,
pywire.stop() # dismiss our servant, the editor.
pywire.start(*Dcomp)This function negotiates for the services of the editor. Your script should call it exactly once before requesting any other action. If the negotiation completes successfully, the function will return. When your script has finished working, make sure it calls pywire.stop(), described below, to discharge the editor from further duty. Within a single script, renegotiation (calling pywire.start(…) a second time) is not allowed. Optional ArgumentsDcomp may include any of the 3-character primitive type strings plus the special string 'ALL’. Including 'ARC’ or 'PAD’ asks pywire to decompose the corresponding objects into an easier-to-render form. You may also use the other 3-character strings, 'LIN’, 'GON’, etc., but these objects are always decomposed unless pywire.start() is called with no arguments. Examples
pywire.start('PAD') # Decompose pads, lines, polygons and labels
pywire.start('PAD', 'GON') # Same effect as pywire.start('PAD')
pywire.start('ARC', 'PAD') # Decompose arcs, pads, lines, polygons and labels
pywire.start('ALL') # Same as pywire.start('ARC', 'PAD')
pywire.start('LIN') # Decompose lines, polygons and labels
pywire.start('LAB') # Same effect as pywire.start('LIN')
pywire.start() # Don't decompose anything
Decomposed pads are delivered as polygons whose perimeter closely approximates the pad shape. Decomposed arcs are delivered as multi-segment lines; each segment is a chord that closely approximates a part of the original arc. Decomposed labels are delivered as the set of lines needed to render them. The resulting lines and polygons are clipped to a rectangular area of interest, discussed further below. Realize that each line may be further fragmented into two or more disjoint pieces if it exits and reenters the area. As a further convenience, coordinates are specified as offsets from the area's lower left corner. Scripts that import pywire fall into two general classes: renderers and analyzers. In support of these endeavors, pywire operates in two distinct modes and tailors the information to the task at hand. RenderersRenderers are concerned with translating your drawing data into a different graphical representation. They are oblivious to the meaning of all those circles, arrows, lines and labels; they only want render them in PostScript, PDF, or RS274 (Gerber) format. Render.py and gerber.py are examples of renderers. Renderers usually invoke pywire.start(…) with at least one argument. For the benefit of devices that cannot render arcs, pywire can deliver a chord approximation. If a printer cannot render a rotated, filled ellipse, pywire can deliver a polygon instead. Labels will be faithfully rendered as you see on the screen because pywire asks the editor to deliver them in exactly the right form. AnalyzersIn contrast, analyzers like netlist.py and drcheck.py need to know the exact shape of your conductors so they can perform geometrical calculations needed to infer connections, check clearances, etc. Invoking pywire.start() with no arguments assures conductors are delivered to your script completely intact. For labels, an analyzer needs not only the visible text, but the invisible attribute. However, it doesn't care one whit about the appearance of the label (its size, rotation, etc.). It does need to determine what conductors are named, but no tedious geometry is required. Pywire provides a much better way to answer that question. Returned ObjectPywire.start(…) returns a dictionary object comprising an executive summary of your drawing data. It includes the following keys and corresponding values:
This dictionary is often ignored, but occasionally contains useful information. For example a renderer needs the extent of the stuff you're asking to see in order to calculate a scaling factor: Example: How big is this stuff?
import pywire
try:
summary = pywire.start('ALL') # Tear it all apart, but remember summary info
N, E, S, W = summary['NESW'] # Unpack extent tuple into separate variables
Width = E - W; Height = N - S # Calculate wingspans
:
pywire.stop()This function takes no arguments. It notifies the editor that the script's work is done, and that no further services are required. Make sure your script has finished all its other I/O operations (e.g., writing a summary report to a file and closing it) before calling this function. pywire.fetch(AoI=None, LoI=None, ToI=None, Lsz=0)This function constructs an iterator object that will deliver a sequence of drawing objects on demand. The objects it delivers are based on several filtering criteria you can specify. If you lasso- or click-select objects in your drawing before running a script, only selected objects that meet your criteria are delivered. But, if you deselect everything in your drawing, all objects meeting your criteria are yielded up. Optional Arguments
Example: Build a list of all layer-0 PAD centers
try:
pywire.start() # Summon the servant
:
result = [] # Start with empty list
for p in pywire.fetch(None, [0], ['PAD']): # Loop through layer-0 pads:
result.append(p[3]) # Add center (X,Y) to list
:
finally: pywire.stop() # Discharge the servant
In this example, pywire.start() was called without arguments and the yielded tuples will be in the form (Id, 'PAD’, Layer, (Xc,Yc), … ). That is, the center coordinates are the fourth element of the tuple; the index of this element is 3. We used positional arguments, and we had to supply a value for AoI even though we used the default. With so many parameters, it is often clearer to use named arguments: Example: Named argument equivalent
try:
:
result = [] # Start with empty list
for p in pywire.fetch(LoI=[0], ToI=['PAD']): # Loop through layer-0 pads:
result.append(p[3]) # Add center coord. to list
:
finally: ...
pywire.vicinity(Layer, Dist=0, Cxy=None)This function constructs a new iterator whose behaviour depends on a reference primitive, the one last yielded by an existing iterator. Its primary uses are:
On request, the iterator delivers a sequence of nearby conducting primitives: pads, lines, arcs, and (if the drawing is a PCB layout), polygons. Note
The vicinity iterator does tedious computational geometry calculations to decide which subset of objects to deliver. This intricate work allows a Python analyzer script to check your design and infer connections in your drawings based on appearance alone. Remember, schematics and layouts use different conventions to signify connection. Polygons are ignored in the former, but form connections in the latter. In circuit board layouts, conductors must be on the same conducting layer to interconnect; in schematics they can connect if they are on any conducting layer. The sign of the required first parameter, Layer, specifies which convention to use. The absolute value of this same parameter specifies the maximum conducting layer. The vicinity iterator will ignore all objects on higher, insulating layers and thereby reduce its workload. When Layer < 0, schematic conventions are used: Polygons are ignored, but the candidate pad, line or arc and the reference primitive can be on any conducting layer. In contrast, when Layer >= 0, circuit board conventions are used. To be considered for delivery, a candidate conductor (including a polygon) must be on the same layer as the reference, or at least one must be on layer 0. If the reference object is a label, the second parameter is ignored, and the vicinity iterator returns all conductors the label might name. Recall that to name a conductor, the label attribute must be empty, its hotspot must touch the conductor, and the conductor must belong to the label's group. The vicinity operator will only yield conductors when these additional constraints are met. Note
For schematics, the vicinity iterator does not discriminate between buses and wires; both will be delivered. It is up to the script to decide which conductor is named; netlist.py would favor the wire. If the reference primitive is a conductor, the second parameter, Dist, specifies a distance threshold in HiWIRE units. The iterator will not yield conductors spaced further away. The default value, 0, delivers all conductors that touch the reference and thus would be connected in a circuit board layout. Larger values are useful to confirm minimum spacing requirements between circuit board conductors in disjoint nets. Finally, a two-element integer HiWIRE (X,Y) coordinate tuple may be supplied as the third argument. When it is, the reference primitive is completely ignored, and pywire.vicinity() yields all conductors within Dist of the parameter point. This facility is useful when Layer < 0 to infer schematic connections made by a reference conductor's end or internal vertex. The harvest will include conductors on any conducting layer <= -Layer. In PCBs (when Layer >= 0) the conductors must be on that layer or on layer 0. Warning: The vicinity iterator is very fragile
The vicinity iterator performs an intricate calculation based on the relative geometry of the reference object and candidates culled from your drawing based on their layer and bounding box. There is only one reference object, and this imposes severe restrictions on the use of this iterator.
Additional Method: details()In PCB usage, when Layer >= 0 and the reference is a pad, line, arc or polygon, the vicinity iterator's details() method returns a three-element tuple containing certain interesting features about the relationship between the reference and its neighbor: (Dist, (Xc,Yc), (Xr,Yr)), where: - Dist is the distance of closest approach between the reference and candidate. If the value is less than zero, the two objects overlap. - (Xc,Yc) and (Xr,Yr) are the closest points in the candidate and reference objects, respectively. If max < 0 or the reference is a label, details() returns None. pywire.replace(prim)This function replaces an existing primitive in your drawing with a replacement you specify in its argument. Prim should be a tuple or list in same form as pywire would deliver the primitive as an answer without the optional integer Id. Call the function right after an iterator, such as pywire.fetch(…), serves up the primitive you want to replace. You cannot replace a ('GRP’, … ) or ('END’, … ). For labels, the tuple ('LAB’, Text, Attribute) can only be used to replace another label; the layer, hotspot position and rotation are poached from the original. However, an additional style is allowed for replacement labels: ('LAB’, Layer, (Xh,Yh), Sz, Rotation, Text, Attribute) Where
Returned ObjectPywire.replace(…) returns a unique integer identifier assigned to the replacement. The replaced object's identifier is no longer valid, and it may subsequently be recycled. Example: Replace 40x40 mil round pads with 50x50
try:
summary = pywire.start() # Summary has units and scale.
if summary['UNITS'] != 'mil': # BUG: in Preferences, Units must
raise ValueError # be set to 'mil'
scale = summary['SCALE'] # mils = HiWIRE_units*scale
W_40 = int(round(40/scale)) # 40 mils (in HiWIRE units)
W_50 = int(round(50/scale)) # 50 mils (in HiWIRE units)
for P in pywire.fetch(ToI=['PAD']): # Pad tuple includes <Id>
P = list( P[1:] ) # Slice off <Id>, convert to list
W, H = P[3]; D = P[4] # Round pads: W, H and D are equal
if W == H == D == W_40: # If 40-mil round,
W = H = D = W_50 # values for 50-mil round
P[3] = (W,H); P[4] = D # replace changed values in list
pywire.replace(P) # replace pad in drawing
finally: pywire.stop()
The scheme to calculate the HiWIRE-unit equivalent of 40- and 50-mil dimensions is primarily intended to illustrate another use of the dictionary returned by pywire.start. There may be a better way, and you'll see an alternative later on. HiWIRE library footprints have place-holder site names like Unnn. Here's a naive approach to automatically assigning actual names: Example: Assign specific numbers to “Unnn”
try:
pywire.start() # Returned dictionary ignored.
U_Nr = 0 # Site number "pool"
for L in pywire.fetch(ToI=['LAB']): # Loop to find all Labels
L = list( L[1:] ) # Chop <Id> and thaw tuple
if L[1] == "Unnn": # If have placeholder site label,
U_Nr += 1 # Get next number from pool
L[1] = "U%03d" % U_Nr # Cobble together a replacement
pywire.replace(L) # Replace original label.
finally: pywire.stop()
pywire.insert(prim)This function is similar to pyiwre.replace() except that it injects a new primitive into your drawing. The new primitive is placed within the same group as the object most recent yielded by an iterator. It is undefined whether or not an exisiting iterator will subsequently yield the new primitive. You can insert immediately after seeing a ('GRP’, ) or ('END’,). The new primitive goes outside the group in the first case, inside it in the second. If you save up your additions until you see ('END’,), the iterator that yielded it won't subsequently yield the new items. Example: Solder masking surface pads on layers 250, 251
try:
pywire.start() # We don't need no steenkin' dictionary!
"""
Here's a way of handling physical units that doesn't rely on Preferences.
"""
swell = 5 # mils # Mask misalignment "slop"
from Prefs import Scale # HiWire units per mm.
swell *= 0.0254 # Convert swell to mm., and
swell *= Scale # then to HiWIRE units
for P in pywire.fetch(ToI = ['PAD'], # Loop through surface pads
LoI = [0,1,2]): # (Layers 1, 2 and unlayer)
P = list( P[1:] ) # Chop <Id> and thaw tuple
L = P[1]; W, H = P[3]; D = P[4] # Extract layer, dimensions
W += swell; H += swell # Increase width, height by slop.
if D is not None: D += swell # Unless elliptical, swell corner
P[3] = (W, H); P[4] = D # Revise list to swollen pad
if L in (0, 1): # If on layer 0 or 1,
P[1] = 250; pywire.insert(P) # add mask on layer 250
if L in (0, 2): # If on layer 0 or 2,
P[1] = 251; pywire.insert(P) # add mask on layer 251
finally: pywire.stop() # We're outa' here!
Pywire, stdin and stdoutPython's print statement is often used to print debugging information to the standard output stream, stdout. Similarly, the built-in raw_input() function can read interactive input from the standard input stream, stdin. A pywire-wielding client conscripts both these streams for communication with the editor/server. Neither stdin nor stdout are available for other uses. On Linux or Mac, when running HiWIRE from a terminal, you can print to the standard error stream, stderr, instead. Or, you can simply rebind stdout and use print … the way you normally would. Printing debug messages to stderr
import sys # Python home of stdout, stderr print>>sys.stderr, "Your message here" # Will appear on terminal sys.stdout = sys.stderr # Rebind sys.stdout to terminal stream print "Your other message here" # Also goes to terminal In Windows, conventional stream I/O is quite broken anyway. If you want to write your own scripts, developing on Linux is strongly recommended. Some ExamplesExample: Print the location and size of all layer-0 pads
try:
pywire.start() # Returned dictionary ignored
import sys # For sys.stdout, sys.stderr
sys.stdout = sys.stderr # We don't need no steenkin' stdout!
for pad in pywire.fetch(LoI=[0],ToI=["PAD"]): # Loop through layer-0 pads
print pad[1:] # Print pad as 6-element tuple
finally: pywire.stop() # Our work is done!
This example negotiates for analysis-mode services. Since no arguments were passed to pywire.start(), the results will include an integer Id, but here, we chop it off before we print ('PAD’, … ) for each result. |