8d17148c548d9fb3aee32c4e5d4db28326b05da3
[hw/altusmetrum] / packages / fplht.py
1 #! /usr/bin/python3
2 # Python library to assist in generating lihata format footprints for pcb-rnd
3 # Copyright 2020 by Bdale Garbee <bdale@gag.com>
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14
15 # Each footprint should have a parent Python script that initializes this
16 # library and provides a high-level description of the primitives required.
17 # The footprint is delivered to stdout, so the encapsulating Makefile and/or
18 # iterating wrapper script is responsible for redirecting to the desired
19 # <something>.lht file(s).
20 #
21 # all dimensions are assumed to be in mm
22
23 import hashlib
24
25 def mils2mm(mils):
26     return 25.4 * (float(mils) / 1000)
27
28 class footprint(object):
29     def __init__(self):
30         self.name = ""
31         self.description = ""
32         self.copyright = ""
33         self.dist_license = "GPLv3"
34         self.use_license = "Unlimited"
35         self.cnt = 0
36         self.items = []
37         self.padstacks = dict()
38
39         # process defaults, based on greatest process minimum at our vendors
40
41         self.process_soldermask = mils2mm(3)    # space between pad and mask
42         self.process_trace = mils2mm(6)        # trace width
43         self.process_space = mils2mm(6)        # spacing
44         self.process_drill = mils2mm(13)        # drill dize
45         self.process_ring = mils2mm(7)                # annular ring
46
47         self.line_thickness = mils2mm(10)        # default silk line width
48
49         # the basic plan is that each primitive function puts the object
50         # definition in the relevant dictionary .. when we go to emit the
51         # part, we'll do things like work out the minimum set of padstack
52         # prototypes by iterating over the dictionaries... then output the
53         # working set of required objects
54
55     def pad(self, d):
56         # we expect these to be provided
57         #
58         # x             center of pad
59         # y
60         # width         size of pad
61         # height
62         # name
63         # number
64         #
65         # the following are normally computed / defaulted, but can be provided
66         #
67         # x1             start of pad "line"
68         # y1
69         # x2             end of pad "line"
70         # y2
71         # thickness      thickness of pad "line"
72         # spacing        space between pad and other traces
73         # soldermask     space between pad and solder mask
74         # clearance      twice the spacing between pad and other traces
75         # mask           thickness of pad and solder mask
76         # options        default to 'square'
77
78         # print ("adding pad")
79         d['type'] = 'pad'
80
81         if 'x1' not in d:
82             d['x1'] = d['x'] - max(0, (d['width'] - d['height']) / 2)
83         if 'x2' not in d:
84             d['x2'] = d['x'] + max(0, (d['width'] - d['height']) / 2)
85         if 'y1' not in d:
86             d['y1'] = d['y'] - max(0, (d['height'] - d['width']) / 2)
87         if 'y2' not in d:
88             d['y2'] = d['y'] + max(0, (d['height'] - d['width']) / 2)
89         if 'thickness' not in d:
90             d['thickness'] = min(d['width'], d['height'])
91         if 'spacing' not in d:
92             d['spacing'] = self.process_space
93         if 'soldermask' not in d:
94             d['soldermask'] = self.process_soldermask
95         if 'clearance' not in d:
96             d['clearance'] = d['spacing'] / 2
97         if 'mask' not in d:
98             d['mask'] = d['thickness'] + d['soldermask'] * 2
99         if 'options' not in d:
100             d['options'] = 'square'
101
102         self.items = self.items + [d]
103
104     def pin(self, d):
105         # we expect these to be provided
106         #
107         # x             center of pin
108         # y
109         # drill         diameter of drill hole
110         # name
111         # number
112         #
113         # the following are normally computed / defaulted, but can be provided
114         #
115         # ring          width of annular ring around hole
116         # spacing       space between pin and other traces
117         # soldermask    space between pin and solder mask
118         # thickness     thickness of pin "line"
119         # clearance     twice the spacing between pad and other traces
120         # mask          thickness of pad and solder mask
121         # options       default to 'square' for pin number 1
122
123         # print ("adding pin")
124         d['type'] = 'pin'
125
126         if 'spacing' not in d:
127             d['spacing'] = self.process_space
128         if 'ring' not in d:
129             d['ring'] = self.process_ring
130         if 'soldermask' not in d:
131             d['soldermask'] = self.process_soldermask
132         if 'clearance' not in d:
133             d['clearance'] = d['spacing'] * 2
134         if 'mask' not in d:
135             d['mask'] = d['thickness'] + d['soldermask'] * 2
136         if 'options' not in d:                # default pin 1 to square ring
137             if d['number'] == '1':
138                 d['options'] = 'square'
139             else:
140                 d['options'] = ''
141         if 'thickness' not in d:
142             d['thickness'] = d['drill'] + d['ring'] * 2
143         if 'mask' not in d:
144             d['mask'] = d['thickness'] + d['soldermask'] * 2
145
146         self.items = self.items + [d]
147
148     def slot(self, d):
149         # we expect these to be provided
150         #
151         # x             center of slot
152         # y
153         # width         size of slot
154         # height
155         # name
156         # number
157         #
158         # the following are normally computed / defaulted, but can be provided
159         #
160         # x1            start of slot "line"
161         # y1
162         # x2            end of slot "line"
163         # y2
164         # spacing       space between slot copper and other traces
165         # thickness     width of copper edge to edge across slot
166         # soldermask    space between slot copper and solder mask
167         # clearance     twice the spacing between slot copper and other traces
168         # mask          thickness of slot copper and solder mask
169
170         # print ("adding slot")
171         d['type'] = 'slot'
172
173         if 'x1' not in d:
174             d['x1'] = d['x'] - max(0, (d['width'] - d['height']) / 2)
175         if 'x2' not in d:
176             d['x2'] = d['x'] + max(0, (d['width'] - d['height']) / 2)
177         if 'y1' not in d:
178             d['y1'] = d['y'] - max(0, (d['height'] - d['width']) / 2)
179         if 'y2' not in d:
180             d['y2'] = d['y'] + max(0, (d['height'] - d['width']) / 2)
181         if 'spacing' not in d:
182             d['spacing'] = self.process_space
183         if 'thickness' not in d:
184             d['thickness'] = d['width'] + self.process_ring * 2
185         if 'soldermask' not in d:
186             d['soldermask'] = self.process_soldermask
187         if 'clearance' not in d:
188             d['clearance'] = d['spacing'] / 2
189         if 'mask' not in d:
190             d['mask'] = d['thickness'] + d['soldermask'] * 2
191
192         self.items = self.items + [d]
193
194     # for silk layers
195     def line(self, d):
196         # we expect these to be provided
197         #
198         # x1            start of silk line
199         # y1
200         # x2            end of silk line
201         # y2
202         #
203         # the following are normally defaulted, but can be provided
204         #
205         # thickness     thickness of silk line
206
207         print("adding line")
208         d['type'] = 'line'
209
210         if 'thickness' not in d:
211             d['thickness'] = self.line_thickness
212
213         self.items = self.items + [d]
214
215     def arc(self, d):
216         # we expect these to be provided
217         #
218         # x             center of silk arc
219         # y
220         # width
221         # height
222         # startangle
223         # deltaangle
224         #
225         # the following are normally defaulted, but can be provided
226         #
227         # thickness     thickness of silk line
228
229         print("adding arc")
230         d['type'] = 'arc'
231
232         if 'thickness' not in d:
233             d['thickness'] = self.line_thickness
234
235         self.items = self.items + [d]
236
237     def write_pin_square(size):
238         print("       li:ps_poly {")
239         print("        %u mm" % -size / 2)
240         print("        %u mm" % -size / 2)
241         print("        %u mm" % size / 2)
242         print("        %u mm" % -size / 2)
243         print("        %u mm" % size / 2)
244         print("        %u mm" % size / 2)
245         print("        %u mm" % -size / 2)
246         print("        %u mm" % size / 2)
247         print("       }")
248
249     def write_pin_circle(size):
250         print("       ha:ps_circ {")
251         print("        x = 0")
252         print("        y = 0")
253         print("        dia = $u mm" % size)
254         print("       }")
255
256     def write_stack_shape(p, layer):
257         print("      ha:ps_shape_v4 {")
258         print("       clearance = %u mm" % p['clearance'])
259         if p['options'] == 'square':
260             write_pin_square(p['copper'])
261         else:
262             write_pin_circle(p['copper'])
263         print("       ha:layer_mask {")
264         print("        copper = 1")
265         print("        $s = 1" % layer)
266         print("       }")
267         print("      }")
268
269     def emit_padstack_prototypes(self):
270         # iterate over copper items creating min set of padstack prototypes,
271         # and annotating items with their prototype number as we go
272         protocount = 0
273         for i in self.items:
274             if i['type'] == 'pad':
275                 myhash = i['type']+str(i['width'])+str(i['height'])+str(i['thickness'])+str(i['spacing'])+str(i['soldermask'])+str(i['clearance'])+str(i['mask'])+i['options']
276             if i['type'] == 'slot':
277                 myhash = i['type']+str(i['width'])+str(i['height'])+str(i['thickness'])+str(i['spacing'])+str(i['soldermask'])+str(i['clearance'])+str(i['mask'])
278             if i['type'] == 'pin':
279                 myhash = i['type']+str(i['drill'])+str(i['ring'])+str(i['spacing'])+str(i['soldermask'])+str(i['clearance'])+str(i['mask'])+i['options']
280
281             # print ("myhash is %s" % myhash)
282             if myhash in self.padstacks:
283                 p = self.padstacks[myhash]
284                 i['prototypenumber'] = p['prototypenumber']
285             else:
286                 # print ("new hash!")
287                 i['prototypenumber'] = protocount
288                 protocount += 1
289                 self.padstacks[myhash] = i
290
291         # print ("%u unique prototypes found" % protocount)
292
293         # now that we have the set of prototypes, emit them
294         print("   li:padstack_prototypes {")
295         for thisp in self.padstacks:
296             p = self.padstacks[thisp]
297             if p['type'] == 'pin':
298                 print("    ha:ps_proto_v4.%u {" % p['prototypenumber'])
299                 print("     htop = 0")
300                 print("     hbottom = 0")
301                 print("     hplated = 1")
302                 print("     hdia = %u mm" % p['drill'])
303                 print("     li:shape {")
304                 write_stack_shape(p, 'top')
305                 write_stack_shape(p, 'bottom')
306                 print("     }")
307                 print("    }")
308
309             if p['type'] == 'pad':
310                 print("    ha:ps_proto_v4.%u {" % p['prototypenumber'])
311                 print("     htop = 0")
312                 print("     hbottom = 0")
313                 print("     hplated = 1")
314                 print("     hdia = %u mm" % 0)         # bogus to the max
315                 print("     li:shape {")
316                 print("     }")
317                 print("    }")
318
319             if p['type'] == 'slot':
320                 # this is just a copy of the 'pad' code for now
321                 print("    ha:ps_proto_v4.%u {" % p['prototypenumber'])
322                 print("     htop = 0")
323                 print("     hbottom = 0")
324                 print("     hplated = 1")
325                 print("     hdia = %u mm" % 0)         # bogus to the max
326                 print("     li:shape {")
327                 print("     }")
328                 print("    }")
329
330         print("   }")
331
332     def emit_padstacks(self):
333         print("padstacks will go here")
334         # process pad, pin, slot items referencing relevant prototypes
335         print("%u items defined" % len(self.items))
336         print("----- start of defined items -----")
337         print(self.items)
338         print("----- end of defined items -----")
339
340     def emit_line(self, role, id, x1, y1, x2, y2, unit):
341         print("      ha:line.%u {" % id)
342         print("       clearance = 0")
343         print("       thickness = 0")
344         print("       ha:attributes {")
345         print("        subc-role = %s" % role)
346         print("       }")
347         print("       x1 = %u%s" % (x1, unit))
348         print("       y1 = %u%s" % (y1, unit))
349         print("       x2 = %u%s" % (x2, unit))
350         print("       y2 = %u%s" % (y2, unit))
351         print("      }")
352         self.cnt = self.cnt + 1
353
354     def emit_top_silk(self):
355         print("top_silk will go here")
356         # process line and arc items
357
358     def emit_bbox(self):
359         self.cnt = 0
360         print("    ha:subc-aux {")
361         print("     lid = 1")
362         print("     ha:type {")
363         print("      top = 1")
364         print("      misc = 1")
365         print("      virtual = 1")
366         print("     }")
367         print("     li:objects {")
368         self.emit_line("origin", self.cnt, 0, 0, 0, 0, "mm")
369         self.emit_line("x", self.cnt, 0, 0, 1, 0, "mm")
370         self.emit_line("y", self.cnt, 0, 0, 0, 1, "mm")
371         print("     }")
372         print("    }")
373
374     def create_uuid(self):
375         return hashlib.md5(self.name.encode('utf-8')).hexdigest()
376
377     def emit(self):
378         print("li:pcb-rnd-subcircuit-v4 {")
379         print(" ha:subc.0 {")
380         print("  ha:attributes {")
381         print("   description = %s" % self.description)
382         print("   copyright = %s" % self.copyright)
383         print("   dist_license = %s" % self.dist_license)
384         print("   use_license = %s" % self.use_license)
385         print("  }")
386         print("  uid = %s" % self.create_uuid())
387         print("  ha:data {")
388         self.emit_padstack_prototypes()
389         print("   li:objects {")
390         self.emit_padstacks()
391         print("   }")
392         print("   li:layers {")
393         self.emit_top_silk()
394         self.emit_bbox()
395         print("   }")
396         print("  }")
397         print(" }")
398         print("}")