Add track/via avoidance and also generate 1-pin heater mesh anchors

This commit is contained in:
jaseg 2023-10-30 15:26:45 +01:00
parent d50efe4695
commit 7e33137f10
6 changed files with 286 additions and 74 deletions

View file

@ -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"
}
]
}

View file

@ -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"
}
]
}

View file

@ -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'<path fill-rule="evenodd" fill="{fill_color}" opacity="{opacity}" stroke="{stroke_color}" '
f'stroke-width="{stroke_width}" d="{path}" />')
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

View file

@ -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 )

View file

@ -48,7 +48,7 @@
<property name="minimum_size"></property>
<property name="name">MainDialog</property>
<property name="pos"></property>
<property name="size">632,580</property>
<property name="size">632,650</property>
<property name="style">wxCLOSE_BOX|wxDEFAULT_DIALOG_STYLE|wxMINIMIZE_BOX|wxRESIZE_BORDER|wxSTAY_ON_TOP</property>
<property name="subclass">; ; forward_declare</property>
<property name="title">Security Mesh Generator Plugin</property>
@ -1555,6 +1555,208 @@
<property name="width">0</property>
</object>
</object>
<object class="sizeritem" expanded="true">
<property name="border">5</property>
<property name="flag">wxALL</property>
<property name="proportion">0</property>
<object class="wxCheckBox" expanded="true">
<property name="BottomDockable">1</property>
<property name="LeftDockable">1</property>
<property name="RightDockable">1</property>
<property name="TopDockable">1</property>
<property name="aui_layer"></property>
<property name="aui_name"></property>
<property name="aui_position"></property>
<property name="aui_row"></property>
<property name="best_size"></property>
<property name="bg"></property>
<property name="caption"></property>
<property name="caption_visible">1</property>
<property name="center_pane">0</property>
<property name="checked">0</property>
<property name="close_button">1</property>
<property name="context_help"></property>
<property name="context_menu">1</property>
<property name="default_pane">0</property>
<property name="dock">Dock</property>
<property name="dock_fixed">0</property>
<property name="docking">Left</property>
<property name="drag_accept_files">0</property>
<property name="enabled">1</property>
<property name="fg"></property>
<property name="floatable">1</property>
<property name="font"></property>
<property name="gripper">0</property>
<property name="hidden">0</property>
<property name="id">wxID_ANY</property>
<property name="label">Respect Tracks and Vias</property>
<property name="max_size"></property>
<property name="maximize_button">0</property>
<property name="maximum_size"></property>
<property name="min_size"></property>
<property name="minimize_button">0</property>
<property name="minimum_size"></property>
<property name="moveable">1</property>
<property name="name">m_trackClearanceCheckbox</property>
<property name="pane_border">1</property>
<property name="pane_position"></property>
<property name="pane_size"></property>
<property name="permission">protected</property>
<property name="pin_button">1</property>
<property name="pos"></property>
<property name="resize">Resizable</property>
<property name="show">1</property>
<property name="size"></property>
<property name="style"></property>
<property name="subclass">; ; forward_declare</property>
<property name="toolbar_pane">0</property>
<property name="tooltip"></property>
<property name="validator_data_type"></property>
<property name="validator_style">wxFILTER_NONE</property>
<property name="validator_type">wxDefaultValidator</property>
<property name="validator_variable"></property>
<property name="window_extra_style"></property>
<property name="window_name"></property>
<property name="window_style"></property>
</object>
</object>
<object class="sizeritem" expanded="true">
<property name="border">5</property>
<property name="flag">wxALIGN_CENTER_VERTICAL|wxALIGN_RIGHT|wxALL</property>
<property name="proportion">0</property>
<object class="wxStaticText" expanded="true">
<property name="BottomDockable">1</property>
<property name="LeftDockable">1</property>
<property name="RightDockable">1</property>
<property name="TopDockable">1</property>
<property name="aui_layer"></property>
<property name="aui_name"></property>
<property name="aui_position"></property>
<property name="aui_row"></property>
<property name="best_size"></property>
<property name="bg"></property>
<property name="caption"></property>
<property name="caption_visible">1</property>
<property name="center_pane">0</property>
<property name="close_button">1</property>
<property name="context_help"></property>
<property name="context_menu">1</property>
<property name="default_pane">0</property>
<property name="dock">Dock</property>
<property name="dock_fixed">0</property>
<property name="docking">Left</property>
<property name="drag_accept_files">0</property>
<property name="enabled">1</property>
<property name="fg"></property>
<property name="floatable">1</property>
<property name="font"></property>
<property name="gripper">0</property>
<property name="hidden">0</property>
<property name="id">wxID_ANY</property>
<property name="label">Track/Via clearance</property>
<property name="markup">0</property>
<property name="max_size"></property>
<property name="maximize_button">0</property>
<property name="maximum_size"></property>
<property name="min_size"></property>
<property name="minimize_button">0</property>
<property name="minimum_size"></property>
<property name="moveable">1</property>
<property name="name">m_staticText15</property>
<property name="pane_border">1</property>
<property name="pane_position"></property>
<property name="pane_size"></property>
<property name="permission">protected</property>
<property name="pin_button">1</property>
<property name="pos"></property>
<property name="resize">Resizable</property>
<property name="show">1</property>
<property name="size"></property>
<property name="style"></property>
<property name="subclass">; ; forward_declare</property>
<property name="toolbar_pane">0</property>
<property name="tooltip"></property>
<property name="window_extra_style"></property>
<property name="window_name"></property>
<property name="window_style"></property>
<property name="wrap">-1</property>
</object>
</object>
<object class="sizeritem" expanded="true">
<property name="border">5</property>
<property name="flag">wxALL</property>
<property name="proportion">0</property>
<object class="wxSpinCtrlDouble" expanded="true">
<property name="BottomDockable">1</property>
<property name="LeftDockable">1</property>
<property name="RightDockable">1</property>
<property name="TopDockable">1</property>
<property name="aui_layer"></property>
<property name="aui_name"></property>
<property name="aui_position"></property>
<property name="aui_row"></property>
<property name="best_size"></property>
<property name="bg"></property>
<property name="caption"></property>
<property name="caption_visible">1</property>
<property name="center_pane">0</property>
<property name="close_button">1</property>
<property name="context_help"></property>
<property name="context_menu">1</property>
<property name="default_pane">0</property>
<property name="digits">3</property>
<property name="dock">Dock</property>
<property name="dock_fixed">0</property>
<property name="docking">Left</property>
<property name="drag_accept_files">0</property>
<property name="enabled">1</property>
<property name="fg"></property>
<property name="floatable">1</property>
<property name="font"></property>
<property name="gripper">0</property>
<property name="hidden">0</property>
<property name="id">wxID_ANY</property>
<property name="inc">0.1</property>
<property name="initial">0.2</property>
<property name="max">1000</property>
<property name="max_size"></property>
<property name="maximize_button">0</property>
<property name="maximum_size"></property>
<property name="min">0</property>
<property name="min_size"></property>
<property name="minimize_button">0</property>
<property name="minimum_size"></property>
<property name="moveable">1</property>
<property name="name">m_trackClearanceSpin</property>
<property name="pane_border">1</property>
<property name="pane_position"></property>
<property name="pane_size"></property>
<property name="permission">protected</property>
<property name="pin_button">1</property>
<property name="pos"></property>
<property name="resize">Resizable</property>
<property name="show">1</property>
<property name="size"></property>
<property name="style">wxSP_ARROW_KEYS</property>
<property name="subclass">; ; forward_declare</property>
<property name="toolbar_pane">0</property>
<property name="tooltip"></property>
<property name="value"></property>
<property name="window_extra_style"></property>
<property name="window_name"></property>
<property name="window_style"></property>
</object>
</object>
<object class="sizeritem" expanded="true">
<property name="border">5</property>
<property name="flag">wxEXPAND</property>
<property name="proportion">1</property>
<object class="spacer" expanded="true">
<property name="height">0</property>
<property name="permission">protected</property>
<property name="width">0</property>
</object>
</object>
<object class="sizeritem" expanded="true">
<property name="border">5</property>
<property name="flag">wxALL</property>

View file

@ -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',