├── 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 | 16 | 37 | 45 | 46 | 48 | 49 | 51 | 53 | image/svg+xml 54 | 56 | ps_diff_pair 57 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 73 | 74 | 75 | 77 | 79 | 80 | ps_diff_pair 82 | 88 | 94 | 100 | 106 | 112 | 118 | 124 | 130 | 136 | 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 | 5 | 6 | Python 7 | 1 8 | source_name 9 | 0 10 | 0 11 | res 12 | UTF-8 13 | connect 14 | kibus_GUI 15 | 1000 16 | none 17 | 18 | 0 19 | MyProject1 20 | 21 | . 22 | 23 | 1 24 | 1 25 | 1 26 | 1 27 | UI 28 | 0 29 | 0 30 | 31 | 0 32 | wxAUI_MGR_DEFAULT 33 | wxSYS_COLOUR_3DLIGHT 34 | wxBOTH 35 | 36 | 1 37 | 1 38 | impl_virtual 39 | 40 | 41 | 42 | 0 43 | wxID_ANY 44 | 45 | 46 | KiBusGUI 47 | 48 | 446,522 49 | wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER 50 | ; ; forward_declare 51 | KiBus 52 | 53 | 54 | 55 | 56 | 57 | 58 | bSizer1 59 | wxVERTICAL 60 | none 61 | 62 | 5 63 | wxEXPAND | wxALL 64 | 1 65 | 66 | 1 67 | 1 68 | 1 69 | 1 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 1 78 | 0 79 | 1 80 | 81 | 1 82 | 0 83 | Dock 84 | 0 85 | Left 86 | 1 87 | 88 | 1 89 | 90 | 0 91 | 0 92 | wxID_ANY 93 | 94 | 0 95 | 96 | 97 | 0 98 | 99 | 1 100 | m_panel1 101 | 1 102 | 103 | 104 | protected 105 | 1 106 | 107 | Resizable 108 | 1 109 | 110 | ; forward_declare 111 | 0 112 | 113 | 114 | 115 | wxTAB_TRAVERSAL 116 | 117 | 118 | bSizer5 119 | wxVERTICAL 120 | none 121 | 122 | 5 123 | wxALL|wxEXPAND 124 | 1 125 | 126 | 1 127 | 1 128 | 1 129 | 1 130 | 131 | 132 | 133 | 134 | 0 135 | 1 136 | 137 | 138 | 139 | 1 140 | 141 | 142 | wxALIGN_LEFT 143 | 144 | wxALIGN_TOP 145 | 0 146 | 1 147 | wxALIGN_CENTER 148 | 30 149 | "Net" "Len" "ΔMed" "ΔMax" 150 | wxALIGN_CENTER 151 | 4 152 | 153 | 154 | 1 155 | 0 156 | Dock 157 | 0 158 | Left 159 | 0 160 | 1 161 | 1 162 | 1 163 | 0 164 | 1 165 | 166 | 1 167 | 168 | 169 | 1 170 | 0 171 | 0 172 | wxID_ANY 173 | 174 | 175 | 176 | 0 177 | 0 178 | 179 | 0 180 | 181 | 182 | 0 183 | 184 | 1 185 | gnet_list 186 | 1 187 | 188 | 189 | protected 190 | 1 191 | 192 | Resizable 193 | wxALIGN_CENTER 194 | 0 195 | 196 | wxALIGN_CENTER 197 | 198 | 0 199 | 1 200 | 201 | ; ; forward_declare 202 | 0 203 | 204 | 205 | 206 | 207 | on_grid_cell_lclick 208 | on_grid_label_lclick 209 | on_grid_range_select 210 | on_grid_key_down 211 | 212 | 213 | 214 | 5 215 | wxEXPAND 216 | 0 217 | 218 | 219 | bSizer2 220 | wxHORIZONTAL 221 | none 222 | 223 | 5 224 | wxALL 225 | 0 226 | 227 | 1 228 | 1 229 | 1 230 | 1 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 1 239 | 0 240 | 0 241 | 1 242 | 243 | 1 244 | 0 245 | Dock 246 | 0 247 | Left 248 | 1 249 | 250 | 1 251 | 252 | 0 253 | 0 254 | wxID_ANY 255 | Continuous refresh 256 | 257 | 0 258 | 259 | 260 | 0 261 | 262 | 1 263 | chk_cont 264 | 1 265 | 266 | 267 | protected 268 | 1 269 | 270 | Resizable 271 | 1 272 | 273 | 274 | 275 | 0 276 | 277 | 278 | wxFILTER_NONE 279 | wxDefaultValidator 280 | 281 | 282 | 283 | 284 | cont_refresh_toggle 285 | 286 | 287 | 288 | 5 289 | wxEXPAND | wxALL 290 | 0 291 | 292 | 1 293 | 1 294 | 1 295 | 1 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 1 304 | 0 305 | 1 306 | 307 | 1 308 | 0 309 | Dock 310 | 0 311 | Left 312 | 1 313 | 314 | 1 315 | 316 | 0 317 | 0 318 | wxID_ANY 319 | 320 | 0 321 | 322 | 323 | 0 324 | 325 | 1 326 | m_staticline1 327 | 1 328 | 329 | 330 | protected 331 | 1 332 | 333 | Resizable 334 | 1 335 | 336 | wxLI_HORIZONTAL 337 | 338 | 0 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 5 347 | wxALL 348 | 0 349 | 350 | 1 351 | 1 352 | 1 353 | 1 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 1 362 | 0 363 | 1 364 | 365 | 1 366 | 0 367 | Dock 368 | 0 369 | Left 370 | 1 371 | 372 | 1 373 | 374 | 0 375 | 0 376 | wxID_ANY 377 | Refresh time: 378 | 0 379 | 380 | 0 381 | 382 | 383 | 0 384 | 385 | 1 386 | lbl_refresh_time 387 | 1 388 | 389 | 390 | protected 391 | 1 392 | 393 | Resizable 394 | 1 395 | 396 | 397 | 398 | 0 399 | 400 | 401 | 402 | 403 | -1 404 | 405 | 406 | 407 | 408 | 409 | 5 410 | wxEXPAND 411 | 0 412 | 413 | 414 | bSizer3 415 | wxHORIZONTAL 416 | none 417 | 418 | 5 419 | wxALL 420 | 0 421 | 422 | 1 423 | 1 424 | 1 425 | 1 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 1 435 | 0 436 | 1 437 | 438 | 1 439 | 440 | 0 441 | 0 442 | 443 | Dock 444 | 0 445 | Left 446 | 1 447 | 448 | 1 449 | 450 | 451 | 0 452 | 0 453 | wxID_ANY 454 | Refresh 455 | 456 | 0 457 | 458 | 0 459 | 460 | 461 | 0 462 | 463 | 1 464 | btn_refresh 465 | 1 466 | 467 | 468 | protected 469 | 1 470 | 471 | 472 | 473 | Resizable 474 | 1 475 | 476 | 477 | 478 | 0 479 | 480 | 481 | wxFILTER_NONE 482 | wxDefaultValidator 483 | 484 | 485 | 486 | 487 | on_btn_refresh 488 | 489 | 490 | 491 | 5 492 | wxEXPAND 493 | 1 494 | 495 | 0 496 | protected 497 | 0 498 | 499 | 500 | 501 | 5 502 | wxALIGN_RIGHT|wxALL 503 | 0 504 | 505 | 1 506 | 1 507 | 1 508 | 1 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 1 518 | 0 519 | 1 520 | 521 | 1 522 | 523 | 0 524 | 0 525 | 526 | Dock 527 | 0 528 | Left 529 | 1 530 | 531 | 1 532 | 533 | 534 | 0 535 | 0 536 | wxID_OK 537 | OK 538 | 539 | 0 540 | 541 | 0 542 | 543 | 544 | 0 545 | 546 | 1 547 | btn_ok 548 | 1 549 | 550 | 551 | protected 552 | 1 553 | 554 | 555 | 556 | Resizable 557 | 1 558 | 559 | 560 | ; forward_declare 561 | 0 562 | 563 | 564 | wxFILTER_NONE 565 | wxDefaultValidator 566 | 567 | 568 | 569 | 570 | on_btn_ok 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | --------------------------------------------------------------------------------