From 7e33137f104c168b782bc9761571a98e3752d8c8 Mon Sep 17 00:00:00 2001 From: jaseg Date: Mon, 30 Oct 2023 15:26:45 +0100 Subject: Add track/via avoidance and also generate 1-pin heater mesh anchors --- de.jaseg.kimesh.footprints/metadata.json | 27 --- de.jaseg.kimesh.plugin/metadata.json | 27 --- de.jaseg.kimesh.plugin/plugins/mesh_dialog.py | 83 +++++++-- .../plugins/mesh_plugin_dialog.py | 17 +- main_dialog.fbp | 204 ++++++++++++++++++++- package.py | 2 +- 6 files changed, 286 insertions(+), 74 deletions(-) delete mode 100644 de.jaseg.kimesh.footprints/metadata.json delete mode 100644 de.jaseg.kimesh.plugin/metadata.json diff --git a/de.jaseg.kimesh.footprints/metadata.json b/de.jaseg.kimesh.footprints/metadata.json deleted file mode 100644 index 5f0225f..0000000 --- a/de.jaseg.kimesh.footprints/metadata.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "$schema": "https://go.kicad.org/pcm/schemas/v1", - "name": "KiMesh", - "description": "A security mesh generator for KiCad", - "description_full": "This is the footprint package for the KiMesh security mesh generator.", - "identifier": "de.jaseg.kimesh.footprints", - "type": "library", - "author": { - "name": "jaseg", - "contact": { - "web": "https://jaseg.de/" - } - }, - "license": "CERN-OHL", - "resources": { - "homepage": "https://jaseg.de/projects/kimesh", - "git": "https://git.jaseg.de/kimesh.git", - "issues": "https://github.com/jaseg/kimesh/issues" - }, - "versions": [ - { - "version": "1.0.1", - "status": "stable", - "kicad_version": "7.99" - } - ] -} \ No newline at end of file diff --git a/de.jaseg.kimesh.plugin/metadata.json b/de.jaseg.kimesh.plugin/metadata.json deleted file mode 100644 index 5256ccb..0000000 --- a/de.jaseg.kimesh.plugin/metadata.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "$schema": "https://go.kicad.org/pcm/schemas/v1", - "name": "KiMesh", - "description": "A security mesh generator for KiCad", - "description_full": "KiMesh automatically generates security meshes for you. A security mesh is a set of PCB traces that cover an area to detect physical tampering. KiMesh can cover arbitrary areas with two or more traces. Note: The KiMesh footprints Add-On must be installed alongside KiMesh. For detailed usage instructions, please have a look at the KiMesh website linked in the add-on information.", - "identifier": "de.jaseg.kimesh.plugin", - "type": "plugin", - "author": { - "name": "jaseg", - "contact": { - "web": "https://jaseg.de/" - } - }, - "license": "GPL-3.0", - "resources": { - "homepage": "https://jaseg.de/projects/kimesh", - "git": "https://git.jaseg.de/kimesh.git", - "issues": "https://github.com/jaseg/kimesh/issues" - }, - "versions": [ - { - "version": "1.0.1", - "status": "stable", - "kicad_version": "7.99" - } - ] -} \ No newline at end of file diff --git a/de.jaseg.kimesh.plugin/plugins/mesh_dialog.py b/de.jaseg.kimesh.plugin/plugins/mesh_dialog.py index 4bcbc38..3b574de 100644 --- a/de.jaseg.kimesh.plugin/plugins/mesh_dialog.py +++ b/de.jaseg.kimesh.plugin/plugins/mesh_dialog.py @@ -39,6 +39,8 @@ class GeneratorSettings: randomness: float = 1.0 use_keepouts: bool = True use_outline: bool = True + use_tracks: bool = False + track_clearance: float = 0.2 # mm save_visualization: bool = True visualization_path: str = 'mesh_visualizations' @@ -115,6 +117,8 @@ class MeshPluginMainDialog(mesh_plugin_dialog.MainDialog): self.m_useKeepoutCheckbox.Value = settings.use_keepouts self.m_vizTextfield.Value = settings.visualization_path self.m_vizCheckbox.Value = settings.save_visualization + self.m_trackClearanceCheckbox.Value = settings.use_tracks + self.m_trackClearanceSpin.Value = settings.track_clearance self.SetMinSize(self.GetSize()) @@ -204,7 +208,7 @@ class MeshPluginMainDialog(mesh_plugin_dialog.MainDialog): count += 1 self.board.Remove(track) - print(f'Tore up {count} trace segments.') + print(f'KiMesh: Tore up {count} trace segments.') def settings_fn(self): return path.join(path.dirname(self.board.GetFileName()), 'last_kimesh_settings.json') @@ -236,14 +240,16 @@ class MeshPluginMainDialog(mesh_plugin_dialog.MainDialog): use_outline = self.m_useOutlineCheckbox.Value, use_keepouts = self.m_useKeepoutCheckbox.Value, visualization_path = self.m_vizTextfield.Value, - save_visualization = self.m_vizCheckbox.Value) + save_visualization = self.m_vizCheckbox.Value, + use_tracks = self.m_trackClearanceCheckbox.Value, + track_clearance = self.m_trackClearanceSpin.Value) except ValueError as e: return wx.MessageDialog(self, "Invalid input value: {}.".format(e), "Invalid input").ShowModal() try: with open(self.settings_fn(), 'wb') as f: f.write(settings.serialize()) - print('Saved settings to', f.name) + print('KiMesh: Saved settings to', f.name) except: wx.MessageDialog(self, "Cannot save settings: {}.".format(e), "File I/O error").ShowModal() @@ -262,7 +268,7 @@ class MeshPluginMainDialog(mesh_plugin_dialog.MainDialog): for zone in self.board.Zones(): if zone.GetDoNotAllowCopperPour() and zone.GetLayerSet().Contains(target_layer_id): keepouts.append(zone.Outline()) - print(f'Found {len(keepouts)} keepout areas.') + print(f'KiMesh: Found {len(keepouts)} keepout areas.') if self.board_has_outline() and self.m_useOutlineCheckbox.Value: # Avoid foot-gun due to insane API. See note in the function. outlines = pcbnew.SHAPE_POLY_SET() @@ -270,7 +276,7 @@ class MeshPluginMainDialog(mesh_plugin_dialog.MainDialog): board_outlines = list(self.poly_set_to_shapely(outlines)) board_mask = shapely.ops.unary_union(board_outlines) mask = board_mask.buffer(-settings.edge_clearance) - print('board outline bounds:', mask.bounds) + print('KiMesh: Board outline bounds:', mask.bounds) if mask.is_empty: return wx.MessageDialog(self, "Error: Board edge clearance is set too high. There is nothing left for the mesh after applying clearance.").ShowModal() else: @@ -284,15 +290,15 @@ class MeshPluginMainDialog(mesh_plugin_dialog.MainDialog): mask = zone_mask else: mask = zone_mask.intersection(mask) - print('Mesh mask bounds:', zone_mask.bounds) + print('KiMesh: Mesh mask bounds:', zone_mask.bounds) if self.m_useKeepoutCheckbox.Value: keepout_outlines = [ outline for zone in keepouts for outline in self.poly_set_to_shapely(zone) ] keepout_mask = shapely.ops.unary_union(keepout_outlines) if not keepout_mask.is_empty: mask = shapely.difference(mask, keepout_mask) - print('keepout mask bounds:', keepout_mask.bounds) - print('resulting mask bounds:', mask.bounds) + print('KiMesh: Keepout mask bounds:', keepout_mask.bounds) + print('KiMesh: Total mask bounds:', mask.bounds) if mask.is_empty: return wx.MessageDialog(self, "Error: After applying all keepouts, and intersecting with the board's outline, the mesh outline is empty.") @@ -345,7 +351,7 @@ class MeshPluginMainDialog(mesh_plugin_dialog.MainDialog): width_per_trace = trace_width + space_width grid_cell_width = width_per_trace * num_traces * 2 - print(f'mesh cell size is {grid_cell_width}') + print(f'KiMesh: mesh cell size is {grid_cell_width} mm') x0, y0 = anchor_pads[len(anchor_pads)//2].GetPosition() x0, y0 = pcbnew.ToMM(x0), pcbnew.ToMM(y0) @@ -353,7 +359,7 @@ class MeshPluginMainDialog(mesh_plugin_dialog.MainDialog): xl, yl = pcbnew.ToMM(xl), pcbnew.ToMM(yl) mesh_angle = math.atan2(xl-x0, yl-y0) - print('mesh angle is', math.degrees(mesh_angle)) + print('KiMesh Mesh angle is', math.degrees(mesh_angle), 'degrees') len_along = - width_per_trace/2 x0 += len_along * math.sin(mesh_angle) y0 += len_along * math.cos(mesh_angle) @@ -366,7 +372,7 @@ class MeshPluginMainDialog(mesh_plugin_dialog.MainDialog): grid_origin = grid_x0*grid_cell_width, grid_y0*grid_cell_width grid_rows = int(math.ceil((bbox[3] - grid_origin[1]) / grid_cell_width)) grid_cols = int(math.ceil((bbox[2] - grid_origin[0]) / grid_cell_width)) - print(f'generating grid of size {grid_rows} * {grid_cols} with origin {grid_x0}, {grid_y0}') + print(f'KiMesh: Generating grid of size {grid_rows} * {grid_cols} with origin {grid_x0}, {grid_y0}') grid = [] for y in range(grid_y0, grid_y0+grid_rows): @@ -380,6 +386,17 @@ class MeshPluginMainDialog(mesh_plugin_dialog.MainDialog): row.append(cell) grid.append(row) + def check_track_collision(cell, clearance=0.2): + cell_lc = pcbnew.SHAPE_LINE_CHAIN([pcbnew.VECTOR2I(pcbnew.FromMM(pt_x), pcbnew.FromMM(pt_y)) + for pt_x, pt_y in cell.exterior.coords], True) + for track_or_via in self.board.GetTracks(): + if not track_or_via.GetLayerSet().Contains(target_layer_id): + continue + + if pcbnew.ToMM(track_or_via.GetEffectiveShape().GetClearance(cell_lc)) < clearance: + return True + return False + num_valid = 0 with self.viz('mesh_grid.svg') as dbg: dbg.add(mask, color='#00000020') @@ -389,11 +406,17 @@ class MeshPluginMainDialog(mesh_plugin_dialog.MainDialog): if mask.contains(cell): if x == -1 and y == 0: # exit cell color = '#ff00ff80' + + elif check_track_collision(cell): + color = '#ffff0080' + else: num_valid += 1 color = '#00ff0080' + elif mask.overlaps(cell): - color = '#ffff0080' + color = '#ff800080' + else: color = '#ff000080' dbg.add(cell, color=color) @@ -401,11 +424,32 @@ class MeshPluginMainDialog(mesh_plugin_dialog.MainDialog): for foo in anchor_outlines: dbg.add(foo, color='#0000ff00', stroke_width=0.05, stroke_color='#000000ff') + for track in self.board.GetTracks(): + if not track.GetLayerSet().Contains(target_layer_id): + continue + + shape = track.GetEffectiveShape().Cast() + if isinstance(shape, pcbnew.SHAPE_SEGMENT): + seg = shape.GetSeg() + dbg.add([[(pcbnew.ToMM(seg.A.x), pcbnew.ToMM(seg.A.y)), + (pcbnew.ToMM(seg.B.x), pcbnew.ToMM(seg.B.y))]], + color='none', stroke_width=pcbnew.ToMM(shape.GetWidth()), stroke_color='#ff0000ff') + + elif isinstance(shape, pcbnew.SHAPE_CIRCLE): + center = shape.GetCenter() + c_cx, c_cy = pcbnew.ToMM(center.y), pcbnew.ToMM(center.y) + c_r = pcbnew.ToMM(shape.GetRadius()) + dbg.add([[(c_cx, c_cy-c_r), (c_cx, c_cy+c_r)], [(c_cx-c_r, c_cy), (c_cx+c_r, c_cy)]], color='none', stroke_width=0.05, stroke_color='#ff0000ff') + dbg.add([[(x0-2, y0), (x0+2, y0)], [(x0, y0-2), (x0, y0+2)]], color='none', stroke_width=0.05, stroke_color='#ff0000ff') def is_valid(cell): if not mask.contains(cell): return False + + if self.m_trackClearanceCheckbox.Value and check_track_collision(cell, self.m_trackClearanceSpin.Value): + return False + return True def iter_neighbors(x, y): @@ -439,8 +483,7 @@ class MeshPluginMainDialog(mesh_plugin_dialog.MainDialog): rnd_state.shuffle(l) yield from l - def add_track(segment:geometry.LineString, net=None): - coords = list(segment.coords) + def add_track(coords, net=None): for (x1, y1), (x2, y2) in zip(coords, coords[1:]): if (x1, y1) == (x2, y2): # zero-length track due to zero chamfer continue @@ -530,6 +573,7 @@ class MeshPluginMainDialog(mesh_plugin_dialog.MainDialog): segment = affinity.translate(segment, x0, y0) dbg_per_tile.add(segment, stroke_width=trace_width, color='#ff000000', stroke_color=stroke_color) + tracks_to_add = [] armed = False while not_visited or stack: for n_x, n_y, bmask in skewed_random_iter(iter_neighbors(x, y), entry_dir, settings.randomness): @@ -565,16 +609,21 @@ class MeshPluginMainDialog(mesh_plugin_dialog.MainDialog): dbg_composite.add(segment, stroke_width=trace_width, color='#ff000000', stroke_color='#ffffff60') dbg_traces.add(segment, stroke_width=trace_width, color='#ff000000', stroke_color='#000000ff') dbg_tiles.add(segment, stroke_width=trace_width, color='#ff000000', stroke_color=stroke_color) - add_track(segment, netinfos[net]) # FIXME (works, disabled for debug) + tracks_to_add.append((list(segment.coords), netinfos[net])) track_count += 1 + if not stack: break + if armed: i += 1 #dump_output(i) armed = False *stack, (x, y, key, entry_dir, depth) = stack + for coords, net in tracks_to_add: + add_track(coords, net) + dbg_cells.scale_colors('visit_depth', max_depth) dbg_composite.scale_colors('visit_depth', max_depth) @@ -585,7 +634,7 @@ class MeshPluginMainDialog(mesh_plugin_dialog.MainDialog): dbg_tiles.add(foo, color='#00000080', stroke_width=0.05, stroke_color='#00000000') - print(f'Added {track_count} trace segments.') + print(f'KiMesh: Added {track_count} trace segments.') #pcbnew.Refresh() #self.tearup_mesh() @@ -732,7 +781,7 @@ class DebugOutputWrapper: raise ValueError(f'Unhandled shapely object type {type(obj)}') return (f'') + f'stroke-width="{stroke_width}" stroke-linecap="round" stroke-linejoin="round" d="{path}" />') def save(self, margin:'unit'=5, scale:'px/unit'=10): #specify margin in coordinate units diff --git a/de.jaseg.kimesh.plugin/plugins/mesh_plugin_dialog.py b/de.jaseg.kimesh.plugin/plugins/mesh_plugin_dialog.py index 790caa8..062e63c 100644 --- a/de.jaseg.kimesh.plugin/plugins/mesh_plugin_dialog.py +++ b/de.jaseg.kimesh.plugin/plugins/mesh_plugin_dialog.py @@ -17,7 +17,7 @@ import wx.xrc class MainDialog ( wx.Dialog ): def __init__( self, parent ): - wx.Dialog.__init__ ( self, parent, id = wx.ID_ANY, title = u"Security Mesh Generator Plugin", pos = wx.DefaultPosition, size = wx.Size( 632,580 ), style = wx.CLOSE_BOX|wx.DEFAULT_DIALOG_STYLE|wx.MINIMIZE_BOX|wx.RESIZE_BORDER|wx.STAY_ON_TOP ) + wx.Dialog.__init__ ( self, parent, id = wx.ID_ANY, title = u"Security Mesh Generator Plugin", pos = wx.DefaultPosition, size = wx.Size( 632,650 ), style = wx.CLOSE_BOX|wx.DEFAULT_DIALOG_STYLE|wx.MINIMIZE_BOX|wx.RESIZE_BORDER|wx.STAY_ON_TOP ) self.SetSizeHints( wx.DefaultSize, wx.DefaultSize ) @@ -158,6 +158,21 @@ class MainDialog ( wx.Dialog ): fgSizer1.Add( self.m_useOutlineCheckbox, 0, wx.ALL, 5 ) + fgSizer1.Add( ( 0, 0), 1, wx.EXPAND, 5 ) + + self.m_trackClearanceCheckbox = wx.CheckBox( self, wx.ID_ANY, u"Respect Tracks and Vias", wx.DefaultPosition, wx.DefaultSize, 0 ) + fgSizer1.Add( self.m_trackClearanceCheckbox, 0, wx.ALL, 5 ) + + self.m_staticText15 = wx.StaticText( self, wx.ID_ANY, u"Track/Via clearance", wx.DefaultPosition, wx.DefaultSize, 0 ) + self.m_staticText15.Wrap( -1 ) + + fgSizer1.Add( self.m_staticText15, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_RIGHT|wx.ALL, 5 ) + + self.m_trackClearanceSpin = wx.SpinCtrlDouble( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.SP_ARROW_KEYS, 0, 1000, 0.2, 0.1 ) + self.m_trackClearanceSpin.SetDigits( 3 ) + fgSizer1.Add( self.m_trackClearanceSpin, 0, wx.ALL, 5 ) + + fgSizer1.Add( ( 0, 0), 1, wx.EXPAND, 5 ) self.m_vizCheckbox = wx.CheckBox( self, wx.ID_ANY, u"Save layout visualizations", wx.DefaultPosition, wx.DefaultSize, 0 ) diff --git a/main_dialog.fbp b/main_dialog.fbp index d97f4c0..0740008 100644 --- a/main_dialog.fbp +++ b/main_dialog.fbp @@ -48,7 +48,7 @@ MainDialog - 632,580 + 632,650 wxCLOSE_BOX|wxDEFAULT_DIALOG_STYLE|wxMINIMIZE_BOX|wxRESIZE_BORDER|wxSTAY_ON_TOP ; ; forward_declare Security Mesh Generator Plugin @@ -1555,6 +1555,208 @@ 0 + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Respect Tracks and Vias + + 0 + + + 0 + + 1 + m_trackClearanceCheckbox + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + 5 + wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT|wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + Track/Via clearance + 0 + + 0 + + + 0 + + 1 + m_staticText15 + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + 3 + Dock + 0 + Left + 0 + 1 + + 1 + + 0 + 0 + wxID_ANY + 0.1 + 0.2 + 1000 + + 0 + + 0 + + 0 + + 1 + m_trackClearanceSpin + 1 + + + protected + 1 + + Resizable + 1 + + wxSP_ARROW_KEYS + ; ; forward_declare + 0 + + + + + + + + + 5 + wxEXPAND + 1 + + 0 + protected + 0 + + 5 wxALL diff --git a/package.py b/package.py index 942d367..a07ed60 100644 --- a/package.py +++ b/package.py @@ -41,7 +41,7 @@ def do_release(version, increment): footprint_dir.mkdir() print('Re-generating footprints') - for n in range(2, 9): + for n in range(1, 9): subprocess.run(['python', '-m', 'footprint_generator', '-w', '0.100,0.120,0.150,0.200,0.250,0.300,0.350,0.400,0.500,0.600,0.700,0.800,1.000,1.200,1.500,1.800', '-c', '0.100,0.120,0.150,0.200,0.300,0.400,0.500', -- cgit