forked from mcbridejc/kicad_component_layout
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcomponent_layout_plugin.py
executable file
·174 lines (145 loc) · 6.94 KB
/
component_layout_plugin.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
"""Allows controlling the location and footprint assignment of components from a
layout.yaml file
Example file:
origin: [x0, y0] # Offset applied to all component locations
components:
R1:
location: [x, y] # mm
rotation: [r] # degrees
flipped: false
footprint:
path: path/to/library.pretty
name: SomeFootprint
J1:
...
All the fields are optional for each component (e.g. leave footprint unspecified
to leave the footprint unchanged)
The footprint path is relative to the directory containing the KiCad PCB file.
"""
import logging
import pcbnew
import os
import sys
import yaml
try:
from yaml import CLoader as Loader, CDumper as Dumper
except ImportError:
from yaml import Loader, Dumper
class StreamToLogger(object):
"""
Fake file-like stream object that redirects writes to a logger instance.
"""
def __init__(self, logger, log_level=logging.INFO):
self.logger = logger
self.log_level = log_level
self.linebuf = ''
def write(self, buf):
for line in buf.rstrip().splitlines():
self.logger.log(self.log_level, line.rstrip())
class ComponentLayout(pcbnew.ActionPlugin):
"""
Uses data in layout.yaml (location in your kicad project directory) to layout footprints
"""
def defaults( self ):
self.name = "Layout footprints from layout.yaml"
self.category = "Modify PCB"
self.description = "Move the components to match the layout.yaml file in the project diretory"
self.show_toolbar_button = True
def Run( self ):
# Interface changed between 5.x and 6.x, but we will support either
v5_compat = pcbnew.GetBuildVersion().startswith('5')
# Handle change in 6.x development branch
# NOTE: Some builds enclose the version string in parentheses,
# so leading parens are removed
# TODO: One day this might be released, and that will break this. But
# I don't know when, so we'll just have to wait and see...
use_vector6 = pcbnew.GetBuildVersion().lstrip('(').startswith('6.99')
use_vector7 = pcbnew.GetBuildVersion().lstrip('(').startswith('7')
pcb = pcbnew.GetBoard()
# In some cases, I have seen KIPRJMOD not set correctly here.
#projdir = os.environ['KIPRJMOD']
projdir = os.path.dirname(os.path.abspath(pcb.GetFileName()))
filehandler = logging.FileHandler(os.path.join(projdir, "component_layout_plugin.log"))
filehandler.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s %(name)s %(lineno)d:%(message)s')
filehandler.setFormatter(formatter)
logger = logging.getLogger(__name__)
# The log setup is persistent accross plugin runs because the kicad python
# kernel keeps running, so clear any existing handlers to avoid multiple
# outputs
while len(logger.handlers) > 0:
logger.removeHandler(logger.handlers[0])
logger.addHandler(filehandler)
logger.setLevel(logging.DEBUG)
logger.info('Logging to {}...'.format(os.path.join(projdir, "component_layout_plugin.log")))
with open(os.path.join(projdir, 'layout.yaml')) as f:
layout = yaml.load(f.read(), Loader)
logger.info("Executing component_layout_plugin")
# Redirect stdout and stderr to logfile
stdout_logger = logging.getLogger('STDOUT')
sl_out = StreamToLogger(stdout_logger, logging.INFO)
sys.stdout = sl_out
stderr_logger = logging.getLogger('STDERR')
sl_err = StreamToLogger(stderr_logger, logging.ERROR)
sys.stderr = sl_err
if 'origin' in layout:
x0 = layout['origin'][0]
y0 = layout['origin'][1]
else:
x0 = 0.0
y0 = 0.0
if not 'components' in layout:
logger.warning("No components field found in layout.yaml")
for refdes, props in layout.get('components', {}).items():
if v5_compat:
mod = pcb.FindModuleByReference(refdes)
else:
mod = pcb.FindFootprintByReference(refdes)
if mod is None:
logger.warning("Did not find component {} in PCB design".format(refdes))
continue
flip = props.get('flip', False) # Generally, flip means put on the bottom
if 'footprint' in props:
# I think there's no API to map the library nickname to a library
# (e.g. using the global and project libraries) so path is passed in.
# I also see no way to find the path from which the footprint was
# previously found, so we're only comparing the name. This should
# be good enough in pretty much all cases, but it is a bit ugly.
footprint_path = os.path.join(projdir, props['footprint']['path'])
footprint_name = props['footprint']['name']
if mod.GetFPID().GetUniStringLibId() != footprint_name:
# As far as I can tell, you can't change the footprint of a module, you have to delete and re-add
# Save important properties of the existing module
ref = mod.GetReference()
pads = list(mod.Pads())
nets = [p.GetNet() for p in pads]
value = mod.GetValue()
newmod = pcbnew.FootprintLoad(footprint_path, footprint_name)
if newmod is None:
logging.error("Failed to load footprint {} from {}".format(footprint_name, footprint_path))
raise RuntimeError("Failed to load footprint %s from %s" % (footprint_name, footprint_path))
pcb.Remove(mod)
# Restore original props to the new module
newmod.SetReference(ref)
for p, net in zip(pads, nets):
p.SetNet(net)
newmod.SetValue(value)
pcb.Add(newmod)
mod = newmod
if 'location' in props:
x = props['location'][0]
y = props['location'][1]
## Latest needs a pcbnew.VECTOR2I, 6.0.1 needs wxPoint
if use_vector6 or use_vector7:
mod.SetPosition(pcbnew.VECTOR2I_MM(x0 + x, y0 + y))
else:
mod.SetPosition(pcbnew.wxPointMM(x0 + x, y0 + y))
if flip ^ (mod.IsFlipped()):
if v5_compat:
mod.Flip(mod.GetPosition())
else:
mod.Flip(mod.GetPosition(), False)
if 'rotation' in props:
rotation = props['rotation']
mod.SetOrientationDegrees(rotation)
ComponentLayout().register()