├── KiBus
├── version.txt
├── kibus-icon.png
├── mk_icon.sh
├── __init__.py
├── kibus_GUI.py
├── kibus-icon.svg
├── action_kibus.py
└── KiBus_GUI.fbp
├── __init__.py
├── .gitignore
├── .vscode
└── settings.json
└── README.md
/KiBus/version.txt:
--------------------------------------------------------------------------------
1 | 0.1.0
2 |
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
1 | from .KiBus import *
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *bak
2 | __pycache__
3 | *.log
4 |
--------------------------------------------------------------------------------
/KiBus/kibus-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/esden/KiBus/HEAD/KiBus/kibus-icon.png
--------------------------------------------------------------------------------
/KiBus/mk_icon.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | inkscape -o kibus-icon.png -w 32 -h 32 --export-area-snap kibus-icon.svg
4 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "python.analysis.extraPaths": ["/Applications/KiCad/KiCad.app/Contents/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/"],
3 | }
--------------------------------------------------------------------------------
/KiBus/__init__.py:
--------------------------------------------------------------------------------
1 | try:
2 | # Note the relative import!
3 | from .action_kibus import KiBus
4 | # Instantiate and register to Pcbnew
5 | KiBus().register()
6 | except Exception as e:
7 | import os
8 | plugin_dir = os.path.dirname(os.path.realpath(__file__))
9 | log_file = os.path.join(plugin_dir, 'kibus_error.log')
10 | with open(log_file, 'w') as f:
11 | f.write(repr(e))
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # KiBus length matching script
2 |
3 | This script implements way to monitor multiple nets, combined into a bus that
4 | needs to be length matched.
5 |
6 | The goal of this script is to combine the different features implemented
7 | throughout the fairly vast selection of lengthmatching python scripts for
8 | KiCad.
9 |
10 | This script is also meant as a playground and prototyping environment to figure
11 | out how a future UI within KiCad could look like to implement bus length
12 | matching capability.
13 |
14 | # Main features
15 |
16 | Not all of the features are currently implemented. It can be currently
17 | considered a wishlist of the features that we want to implement.
18 |
19 | * Monitor and display the length of multiple nets
20 | * Display and sort by the difference of each trace to the max length trace
21 | * Display and sort by the difference of each trace to the median length trace
22 | * Visually indicate through background color how big the difference of each
23 | trace is to the target length
24 | * Select target length
25 | * Merge nets to consider as one. (needed for bus length matching that includes an in line termination resistor)
26 | * Add bonding wire length adjustment.
27 | * Add via length
28 | * Store and Load a project config file defining the bus nets and the length
29 | matching requirements. (this should eventually become part of the net class
30 | parameters within kicad)
31 |
--------------------------------------------------------------------------------
/KiBus/kibus_GUI.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | ###########################################################################
4 | ## Python code generated with wxFormBuilder (version Feb 20 2021)
5 | ## http://www.wxformbuilder.org/
6 | ##
7 | ## PLEASE DO *NOT* EDIT THIS FILE!
8 | ###########################################################################
9 |
10 | import wx
11 | import wx.xrc
12 | import wx.grid
13 |
14 | ###########################################################################
15 | ## Class KiBusGUI
16 | ###########################################################################
17 |
18 | class KiBusGUI ( wx.Dialog ):
19 |
20 | def __init__( self, parent ):
21 | wx.Dialog.__init__ ( self, parent, id = wx.ID_ANY, title = u"KiBus", pos = wx.DefaultPosition, size = wx.Size( 446,522 ), style = wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER )
22 |
23 | self.SetSizeHints( wx.DefaultSize, wx.DefaultSize )
24 | self.SetBackgroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_3DLIGHT ) )
25 |
26 | bSizer1 = wx.BoxSizer( wx.VERTICAL )
27 |
28 | self.m_panel1 = wx.Panel( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL )
29 | bSizer5 = wx.BoxSizer( wx.VERTICAL )
30 |
31 | self.gnet_list = wx.grid.Grid( self.m_panel1, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0 )
32 |
33 | # Grid
34 | self.gnet_list.CreateGrid( 0, 4 )
35 | self.gnet_list.EnableEditing( False )
36 | self.gnet_list.EnableGridLines( True )
37 | self.gnet_list.EnableDragGridSize( True )
38 | self.gnet_list.SetMargins( 0, 0 )
39 |
40 | # Columns
41 | self.gnet_list.EnableDragColMove( False )
42 | self.gnet_list.EnableDragColSize( True )
43 | self.gnet_list.SetColLabelSize( 30 )
44 | self.gnet_list.SetColLabelValue( 0, u"Net" )
45 | self.gnet_list.SetColLabelValue( 1, u"Len" )
46 | self.gnet_list.SetColLabelValue( 2, u"ΔMed" )
47 | self.gnet_list.SetColLabelValue( 3, u"ΔMax" )
48 | self.gnet_list.SetColLabelAlignment( wx.ALIGN_CENTER, wx.ALIGN_CENTER )
49 |
50 | # Rows
51 | self.gnet_list.AutoSizeRows()
52 | self.gnet_list.EnableDragRowSize( True )
53 | self.gnet_list.SetRowLabelSize( 0 )
54 | self.gnet_list.SetRowLabelAlignment( wx.ALIGN_CENTER, wx.ALIGN_CENTER )
55 |
56 | # Label Appearance
57 |
58 | # Cell Defaults
59 | self.gnet_list.SetDefaultCellAlignment( wx.ALIGN_LEFT, wx.ALIGN_TOP )
60 | bSizer5.Add( self.gnet_list, 1, wx.ALL|wx.EXPAND, 5 )
61 |
62 | bSizer2 = wx.BoxSizer( wx.HORIZONTAL )
63 |
64 | self.chk_cont = wx.CheckBox( self.m_panel1, wx.ID_ANY, u"Continuous refresh", wx.DefaultPosition, wx.DefaultSize, 0 )
65 | bSizer2.Add( self.chk_cont, 0, wx.ALL, 5 )
66 |
67 | self.m_staticline1 = wx.StaticLine( self.m_panel1, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL )
68 | bSizer2.Add( self.m_staticline1, 0, wx.EXPAND |wx.ALL, 5 )
69 |
70 | self.lbl_refresh_time = wx.StaticText( self.m_panel1, wx.ID_ANY, u"Refresh time:", wx.DefaultPosition, wx.DefaultSize, 0 )
71 | self.lbl_refresh_time.Wrap( -1 )
72 |
73 | bSizer2.Add( self.lbl_refresh_time, 0, wx.ALL, 5 )
74 |
75 |
76 | bSizer5.Add( bSizer2, 0, wx.EXPAND, 5 )
77 |
78 | bSizer3 = wx.BoxSizer( wx.HORIZONTAL )
79 |
80 | self.btn_refresh = wx.Button( self.m_panel1, wx.ID_ANY, u"Refresh", wx.DefaultPosition, wx.DefaultSize, 0 )
81 | bSizer3.Add( self.btn_refresh, 0, wx.ALL, 5 )
82 |
83 |
84 | bSizer3.Add( ( 0, 0), 1, wx.EXPAND, 5 )
85 |
86 | self.btn_ok = wx.Button( self.m_panel1, wx.ID_OK, u"OK", wx.DefaultPosition, wx.DefaultSize, 0 )
87 | bSizer3.Add( self.btn_ok, 0, wx.ALIGN_RIGHT|wx.ALL, 5 )
88 |
89 |
90 | bSizer5.Add( bSizer3, 0, wx.EXPAND, 5 )
91 |
92 |
93 | self.m_panel1.SetSizer( bSizer5 )
94 | self.m_panel1.Layout()
95 | bSizer5.Fit( self.m_panel1 )
96 | bSizer1.Add( self.m_panel1, 1, wx.EXPAND |wx.ALL, 5 )
97 |
98 |
99 | self.SetSizer( bSizer1 )
100 | self.Layout()
101 |
102 | self.Centre( wx.BOTH )
103 |
104 | # Connect Events
105 | self.gnet_list.Bind( wx.grid.EVT_GRID_CELL_LEFT_CLICK, self.on_grid_cell_lclick )
106 | self.gnet_list.Bind( wx.grid.EVT_GRID_LABEL_LEFT_CLICK, self.on_grid_label_lclick )
107 | self.gnet_list.Bind( wx.grid.EVT_GRID_RANGE_SELECT, self.on_grid_range_select )
108 | self.gnet_list.Bind( wx.EVT_KEY_DOWN, self.on_grid_key_down )
109 | self.chk_cont.Bind( wx.EVT_CHECKBOX, self.cont_refresh_toggle )
110 | self.btn_refresh.Bind( wx.EVT_BUTTON, self.on_btn_refresh )
111 | self.btn_ok.Bind( wx.EVT_BUTTON, self.on_btn_ok )
112 |
113 | def __del__( self ):
114 | pass
115 |
116 |
117 | # Virtual event handlers, overide them in your derived class
118 | def on_grid_cell_lclick( self, event ):
119 | event.Skip()
120 |
121 | def on_grid_label_lclick( self, event ):
122 | event.Skip()
123 |
124 | def on_grid_range_select( self, event ):
125 | event.Skip()
126 |
127 | def on_grid_key_down( self, event ):
128 | event.Skip()
129 |
130 | def cont_refresh_toggle( self, event ):
131 | event.Skip()
132 |
133 | def on_btn_refresh( self, event ):
134 | event.Skip()
135 |
136 | def on_btn_ok( self, event ):
137 | event.Skip()
138 |
139 |
140 |
--------------------------------------------------------------------------------
/KiBus/kibus-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
137 |
--------------------------------------------------------------------------------
/KiBus/action_kibus.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # action_kibus.py
3 | #
4 | # KiBus action plugin based on Mitja Nemec length stats action plugin.
5 | #
6 | # Copyright (C) 2018 Mitja Nemec
7 | # Copyright (C) 2020 Piotr Esden-Tempski
8 | #
9 | # This program is free software; you can redistribute it and/or modify
10 | # it under the terms of the GNU General Public License as published by
11 | # the Free Software Foundation; either version 2 of the License, or
12 | # (at your option) any later version.
13 | #
14 | # This program is distributed in the hope that it will be useful,
15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 | # GNU General Public License for more details.
18 | #
19 | # You should have received a copy of the GNU General Public License
20 | # along with this program; if not, write to the Free Software
21 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
22 | # MA 02110-1301, USA.
23 | #
24 | #
25 |
26 | import wx
27 | import pcbnew
28 | import os
29 | import logging
30 | import sys
31 | import timeit
32 | from packaging import version
33 | from typing import Union
34 |
35 | from wx.core import Colour, KeyEvent
36 | from wx.grid import GridEvent, GridRangeSelectEvent
37 |
38 | if __name__ == '__main__':
39 | import kibus_GUI
40 | else:
41 | from . import kibus_GUI
42 |
43 | SCALE = 1000000.0
44 |
45 | # get version information
46 | version_filename = os.path.join(os.path.dirname(os.path.realpath(__file__)), "version.txt")
47 | with open(version_filename) as f:
48 | VERSION = f.readline().strip()
49 |
50 | # > V5.1.5 and V 5.99 build information
51 | if hasattr(pcbnew, 'GetBuildVersion'):
52 | BUILD_VERSION = pcbnew.GetBuildVersion()
53 | kicad_version = version.parse(BUILD_VERSION.strip("()").split("-")[0])
54 | else:
55 | BUILD_VERSION = "Unknown"
56 | kicad_version = version.Version("0.0.0")
57 |
58 | is_kicad_stable = kicad_version < version.Version("5.99.0")
59 |
60 | def median(x: Union[int, float]) -> float:
61 | """Return median from a list of values.
62 |
63 | Thanks to http://stackoverflow.com/a/25791644
64 | """
65 | if len(x) % 2 != 0:
66 | return sorted(x)[len(x) // 2]
67 | else:
68 | midavg = (sorted(x)[len(x) // 2] + sorted(x)[len(x) // 2 - 1]) / 2.0
69 | return midavg
70 |
71 |
72 | class KiBusDialog(kibus_GUI.KiBusGUI):
73 | # hack for new wxFormBuilder generating code incompatible with old wxPython
74 | # noinspection PyMethodOverriding
75 | def SetSizeHints(self, sz1, sz2):
76 | try:
77 | # wxPython 3
78 | self.SetSizeHintsSz(sz1, sz2)
79 | except TypeError:
80 | # wxPython 4
81 | super(KiBusDialog, self).SetSizeHints(sz1, sz2)
82 |
83 | def __init__(self, parent, board: pcbnew.BOARD, nets, logger: logging.Logger):
84 | kibus_GUI.KiBusGUI.__init__(self, parent)
85 |
86 | self.gnet_list.AppendRows(len(nets))
87 |
88 | self.net_data = []
89 |
90 | nets.sort()
91 | for net in nets:
92 | index_net = nets.index(net)
93 | self.net_data.append( (net, 0.0) )
94 |
95 | self.logger = logger
96 | self.update_list()
97 |
98 | self.board = board
99 | self.nets = nets
100 |
101 | self.column_sorted = 0
102 | self.column_0_dir = 0
103 | self.column_1_dir = 0
104 |
105 | self.timer = wx.Timer(self, 1)
106 | self.refresh_time = 0.1
107 |
108 | self.Bind(wx.EVT_TIMER, self.on_update, self.timer)
109 |
110 | self.logger.info("Length stats gui initialized")
111 | self.logger.info("Nets for stats are;\n" + repr(self.nets))
112 |
113 | def cont_refresh_toggle(self, event):
114 | if self.chk_cont.IsChecked():
115 | self.logger.info("Automatic refresh turned on")
116 | self.timer.Start(self.refresh_time * 10 * 1000)
117 | else:
118 | self.logger.info("Automatic refresh turned off")
119 | self.timer.Stop()
120 | event.Skip()
121 |
122 | def on_btn_refresh(self, event):
123 | self.logger.info("Refreshing manually")
124 | self.refresh()
125 | event.Skip()
126 |
127 | def on_btn_ok(self, event):
128 | # remove higlightning from tracks
129 | tracks = self.board.GetTracks()
130 | for track in tracks:
131 | track.ClearBrightened()
132 |
133 | pcbnew.Refresh()
134 |
135 | self.logger.info("Closing GUI")
136 | logging.shutdown()
137 | self.Close()
138 | event.Skip()
139 |
140 | def on_update(self, event):
141 | self.logger.info("Autimatic refresh")
142 | x, y = self.gnet_list.GetViewStart()
143 | self.refresh()
144 | self.gnet_list.Scroll(x, y)
145 | event.Skip()
146 |
147 | def update_list(self):
148 | if len(self.net_data) == 0:
149 | return
150 |
151 | maxlen = max(net[1] for net in self.net_data)
152 | minlen = min(net[1] for net in self.net_data)
153 | delta = maxlen - minlen
154 | medlen = median([net[1] for net in self.net_data])
155 |
156 | for i, net in enumerate(self.net_data):
157 | self.gnet_list.SetCellValue(i, 0, net[0])
158 | self.gnet_list.SetCellValue(i, 1, "%.2f" % net[1])
159 | meddiff = (net[1] - medlen)
160 | self.gnet_list.SetCellValue(i, 2, "%.2f" % meddiff)
161 | maxdiff = (net[1] - maxlen)
162 | self.gnet_list.SetCellValue(i, 3, "%.2f" % maxdiff)
163 | if (delta != 0):
164 | #self.logger.info("delta {} maxdiff {}, ratio {}".format(delta, maxdiff, ratio))
165 | lenratio = net[1] / maxlen
166 | lencolor_val = round(255 - (200 * lenratio))
167 | self.gnet_list.SetCellBackgroundColour(i, 1, wx.Colour(255, lencolor_val, lencolor_val))
168 | medratio = abs(meddiff) / delta
169 | medcolor_val = round(255 - (200 * medratio))
170 | self.gnet_list.SetCellBackgroundColour(i, 2, wx.Colour(255, medcolor_val, medcolor_val))
171 | maxratio = maxdiff / -delta
172 | maxcolor_val = round(255 - (200 * maxratio))
173 | self.gnet_list.SetCellBackgroundColour(i, 3, wx.Colour(255, maxcolor_val, maxcolor_val))
174 |
175 | self.gnet_list.AutoSize()
176 | self.Layout()
177 |
178 | def refresh(self):
179 | self.logger.info("Refreshing net lengths")
180 | start_time = timeit.default_timer()
181 |
182 | # calculate new net lengths
183 | for net in self.nets:
184 | # get tracks on net
185 | netcode = self.board.GetNetcodeFromNetname(net)
186 | tracks_on_net = self.board.TracksInNet(netcode)
187 |
188 | # sum their length
189 | length = sum(t.GetLength() / SCALE for t in tracks_on_net)
190 |
191 | # update database
192 | index_net = self.nets.index(net)
193 | self.net_data[index_net] = (net, length)
194 |
195 | self.update_list()
196 |
197 | stop_time = timeit.default_timer()
198 | delta_time = stop_time - start_time
199 | if delta_time > 0.05:
200 | self.refresh_time = delta_time
201 | else:
202 | self.refresh_time = 0.05
203 | self.lbl_refresh_time.SetLabelText(u"Refresh time: %.2f s" % delta_time)
204 |
205 | def on_grid_range_select(self, event: GridRangeSelectEvent):
206 | if not event.Selecting():
207 | self.logger.info("Not Selecting")
208 | return
209 |
210 | self.logger.info("Range Select")
211 |
212 | top_row = event.TopRow
213 | bot_row = event.BottomRow
214 | left_col = event.LeftCol
215 | right_col = event.RightCol
216 | self.logger.info(f"area {left_col} {top_row} {right_col} {bot_row}")
217 | self.gnet_list.ClearSelection()
218 |
219 | event.Skip()
220 |
221 | def on_grid_label_lclick(self, event: GridEvent):
222 | column = event.Col
223 | row = event.Row
224 | self.logger.info("Label Click")
225 | self.logger.info(f"col {column} row {row}")
226 |
227 | if row != -1:
228 | self.logger.error("Row Label Click, expected Column")
229 | return
230 |
231 | self.logger.info("Sorting list")
232 | # find which columnt to sort
233 | self.column_sorted = event.Col
234 |
235 | # sort column 0
236 | if self.column_sorted == 0:
237 | # ascending
238 | if self.column_0_dir == 0:
239 | self.column_0_dir = 1
240 | self.net_data.sort(key=lambda tup: tup[0], reverse=True)
241 | self.gnet_list.SetSortingColumn(self.column_sorted, ascending=True)
242 | # descending
243 | else:
244 | self.column_0_dir = 0
245 | self.net_data.sort(key=lambda tup: tup[0], reverse=False)
246 | self.gnet_list.SetSortingColumn(self.column_sorted, ascending=False)
247 | # sort column 1
248 | else:
249 | # ascending
250 | if self.column_1_dir == 0:
251 | self.column_1_dir = 1
252 | self.net_data.sort(key=lambda tup: tup[1], reverse=True)
253 | self.gnet_list.SetSortingColumn(self.column_sorted, ascending=True)
254 | # descending
255 | else:
256 | self.column_1_dir = 0
257 | self.net_data.sort(key=lambda tup: tup[1], reverse=False)
258 | self.gnet_list.SetSortingColumn(self.column_sorted, ascending=False)
259 | # sort
260 |
261 | self.nets = [x[0] for x in self.net_data]
262 |
263 | self.update_list()
264 |
265 | event.Skip()
266 |
267 | def on_grid_cell_lclick(self, event: GridEvent):
268 | column = event.Col
269 | row = event.Row
270 |
271 | self.logger.info("Cell Click")
272 | self.logger.info(f"col {column} row {row}")
273 |
274 | tracks = self.board.GetTracks()
275 | # get all tracks which we are interested in
276 | list_tracks = []
277 | for track in tracks:
278 | if track.GetNetname() in self.nets:
279 | list_tracks.append(track)
280 |
281 | # remove highlight on all tracks
282 | self.logger.info("Removing highlights for nets:\n" + repr(self.nets))
283 | for track in list_tracks:
284 | track.ClearBrightened()
285 |
286 | pcbnew.Refresh()
287 | # find selected tracks
288 | selected_items = []
289 | # We currently can only highlight one net at a time
290 | # For multi net highlight we need to implement multi row selection
291 | #for index in range(self.net_list.GetItemCount()):
292 | # if self.net_list.IsSelected(index):
293 | # selected_items.append(self.nets[index])
294 | selected_items.append(self.nets[row])
295 |
296 | self.logger.info("Adding highlights for nets:\n" + repr(selected_items))
297 | for track in list_tracks:
298 | if track.GetNetname() in selected_items:
299 | track.SetBrightened()
300 |
301 | pcbnew.Refresh()
302 |
303 | event.Skip()
304 |
305 | def on_grid_key_down(self, event: KeyEvent):
306 | key = event.KeyCode
307 | row = self.gnet_list.GridCursorRow
308 | self.logger.info("KeyDown {!r} at {}".format(key, row))
309 |
310 | # test if delete key was pressed
311 | if key in [wx.WXK_DELETE, wx.WXK_BACK]:
312 | self.logger.info(f"Deleting row {row} " + repr(self.nets[row]))
313 | # Clear highlight for the net that is to be deleted
314 | tracks = self.board.GetTracks()
315 | for track in tracks:
316 | if track.GetNetname() in self.nets[row]:
317 | track.ClearBrightened()
318 | pcbnew.Refresh()
319 |
320 | # Delete the net from local database
321 | del self.nets[row]
322 | del self.net_data[row]
323 |
324 | # Delete the net from the grid widget
325 | self.gnet_list.DeleteRows(pos=row)
326 |
327 | # We can't currently delete multiple rows
328 | # We will get it back when selection is implemented
329 | # find selected items
330 | # selected_items = []
331 | #for index in range(self.net_list.GetItemCount()):
332 | # if self.net_list.IsSelected(index):
333 | # selected_items.append( (index, self.nets[index]))
334 |
335 | #selected_items.sort(key=lambda tup: tup[0], reverse=True)
336 |
337 | # remove selected items from the back
338 | #for item in selected_items:
339 | # self.net_list.DeleteItem(item[0])
340 | # del self.nets[item[0]]
341 | # del self.net_data[item[0]]
342 |
343 | event.Skip()
344 |
345 |
346 | class KiBus(pcbnew.ActionPlugin):
347 | """
348 | A plugin to show track length of all selected nets
349 | How to use:
350 | - move to GAL
351 | - select track segment or pad for net you wish to find the length
352 | - call the plugin
353 | """
354 |
355 | def defaults(self):
356 | self.name = "KiBus"
357 | self.category = "Bus Length Matching Helper"
358 | self.description = "A helper dialog that displays bus signal lengts with comparison and sorting"
359 | self.icon_file_name = os.path.join(
360 | os.path.dirname(__file__), 'kibus-icon.png')
361 |
362 | def Run(self):
363 | # load board
364 | board = pcbnew.GetBoard()
365 |
366 | # go to the project folder - so that log will be in proper place
367 | os.chdir(os.path.dirname(os.path.abspath(board.GetFileName())))
368 |
369 | # Remove all handlers associated with the root logger object.
370 | for handler in logging.root.handlers[:]:
371 | logging.root.removeHandler(handler)
372 |
373 | # set up logger
374 | logging.basicConfig(level=logging.DEBUG,
375 | filename="kibus.log",
376 | filemode='w',
377 | format='%(asctime)s %(name)s %(lineno)d:%(message)s',
378 | datefmt='%m-%d %H:%M:%S')
379 | logger = logging.getLogger(__name__)
380 | logger.info("Plugin executed on: " + repr(sys.platform))
381 | logger.info("Plugin executed with python version: " + repr(sys.version))
382 | logger.info("KiCad build version: " + BUILD_VERSION)
383 | logger.info("Length stats plugin version: " + VERSION + " started")
384 |
385 | stdout_logger = logging.getLogger('STDOUT')
386 | sl_out = StreamToLogger(stdout_logger, logging.INFO)
387 | sys.stdout = sl_out
388 |
389 | stderr_logger = logging.getLogger('STDERR')
390 | sl_err = StreamToLogger(stderr_logger, logging.ERROR)
391 | sys.stderr = sl_err
392 |
393 | if is_kicad_stable:
394 | _pcbnew_frame = [x for x in wx.GetTopLevelWindows() if x.GetTitle().lower().startswith('pcbnew')][0]
395 | else:
396 | _pcbnew_frame = [x for x in wx.GetTopLevelWindows() if x.GetTitle().lower().endswith('pcb editor')][0]
397 |
398 | # find all selected tracks and pads
399 | nets = set()
400 | selected_tracks = [x for x in board.GetTracks() if x.IsSelected()]
401 |
402 | nets.update([track.GetNetname() for track in selected_tracks])
403 |
404 | if is_kicad_stable:
405 | modules = board.GetModules()
406 | else:
407 | modules = board.GetFootprints()
408 | for mod in modules:
409 | pads = mod.Pads()
410 | nets.update([pad.GetNetname() for pad in pads if pad.IsSelected()])
411 |
412 | dlg = KiBusDialog(_pcbnew_frame, board, list(nets), logger)
413 | dlg.Show()
414 |
415 |
416 | class StreamToLogger(object):
417 | """
418 | Fake file-like stream object that redirects writes to a logger instance.
419 | """
420 | def __init__(self, logger, log_level=logging.INFO):
421 | self.logger = logger
422 | self.log_level = log_level
423 | self.linebuf = ''
424 |
425 | def write(self, buf):
426 | for line in buf.rstrip().splitlines():
427 | self.logger.log(self.log_level, line.rstrip())
428 |
429 | def flush(self, *args, **kwargs):
430 | """No-op for wrapper"""
431 | pass
432 |
--------------------------------------------------------------------------------
/KiBus/KiBus_GUI.fbp:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
581 |
582 |
--------------------------------------------------------------------------------