Skip to content

Commit

Permalink
Use netclass defined track width when routing
Browse files Browse the repository at this point in the history
Fixes #40
  • Loading branch information
adamws committed Sep 28, 2024
1 parent d5d29d5 commit 0fd5fdf
Show file tree
Hide file tree
Showing 10 changed files with 1,211 additions and 16 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ python -m com_github_adamws_kicad-kbplacer --help
Enables automatic routing of switch-diode pairs. If user manually route first pair,
then it replicates the connection for remaining pairs. If not, uses automatic
internal router. When automatic router used, attempts to connect only two closest
pads of the same net.
pads of the same net. Automatic router will use default track width of a routed netclass.
</td>
</tr>
<tr>
Expand Down Expand Up @@ -343,6 +343,7 @@ python -m com_github_adamws_kicad-kbplacer --help
<code>ROW(\d+)</code> regular expressions (ignoring case) and connecting them
using simplified internal router. Configuration of row/column naming scheme
is not yet supported.
Automatic router will use default track width of a routed netclass.
</td>
</tr>
<tr>
Expand Down
6 changes: 5 additions & 1 deletion kbplacer/board_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import json
import logging
import os
import re
from collections import defaultdict
from dataclasses import dataclass
Expand Down Expand Up @@ -36,14 +37,17 @@ def load(self) -> pcbnew.FOOTPRINT:
class BoardBuilder:
def __init__(
self,
board_path: Union[str, os.PathLike],
*,
switch_footprint: str,
diode_footprint: str,
) -> None:
self.switch_footprint = Footprint.from_str(switch_footprint)
self.diode_footprint = Footprint.from_str(diode_footprint)

self.board = pcbnew.CreateEmptyBoard()
# use `NewBoard` over `CreateNewBoard` because it respects netclass
# settings from .kicad_pro file if it already exist
self.board = pcbnew.NewBoard(board_path)
self.nets: dict[str, pcbnew.NETINFO_ITEM] = {}
self.net_info = self.board.GetNetInfo()
self.net_count = self.board.GetNetCount()
Expand Down
41 changes: 37 additions & 4 deletions kbplacer/board_modifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,24 @@ def prim_mst(distances: List[List[int]]) -> List[Tuple[int, int]]:
return mst_edges


def get_netclass(
board: pcbnew.BOARD, item: pcbnew.BOARD_CONNECTED_ITEM
) -> pcbnew.NETCLASS:
netclass_name = item.GetNetClassName()
if KICAD_VERSION < (7, 0, 0):
netclasses = board.GetNetClasses()
netclass = netclasses.Find(netclass_name)
return netclass if netclass else netclasses.Find("Default")
else:
# workaround, see https://gitlab.com/kicad/code/kicad/-/issues/18609
try:
return board.GetNetClasses()[netclass_name]
except IndexError:
# may happen when item has no net assigned yet or netclass is
# equal "Default" (which is not a part of GetNetClasses collection)
return board.GetAllNetClasses()["Default"]


class BoardModifier:
def __init__(self, board: pcbnew.BOARD) -> None:
self.board = board
Expand Down Expand Up @@ -441,11 +459,13 @@ def add_track_segment_by_points(
self,
start: pcbnew.VECTOR2I,
end: pcbnew.VECTOR2I,
*,
layer: int = pcbnew.B_Cu,
netcode: int = 0,
width: int = 200000,
) -> Optional[pcbnew.VECTOR2I]:
track = pcbnew.PCB_TRACK(self.board)
track.SetWidth(pcbnew.FromMM(0.25))
track.SetWidth(width)
track.SetLayer(layer)
if netcode:
track.SetNetCode(netcode)
Expand Down Expand Up @@ -506,6 +526,19 @@ def route(self, pad1: pcbnew.PAD, pad2: pcbnew.PAD) -> bool:
return False

netcode = pad1.GetNetCode()
netclass = get_netclass(self.board, pad1)
track_width = netclass.GetTrackWidth()

if KICAD_VERSION < (7, 0, 0):
# on KiCad 6.0.11 `netclass.GetName()` for Default netclass
# ends with segmentation fault
netclass_str = f"{netclass}"
else:
netclass_str = netclass.GetName()

logger.debug(f"Netclass: {netclass_str}, track width: {track_width}")

track_args = {"layer": layer, "netcode": netcode, "width": track_width}

def _calculate_corners(
pos1: pcbnew.VECTOR2I, pos2: pcbnew.VECTOR2I
Expand All @@ -530,8 +563,8 @@ def _calculate_corners(
def _route(
pos1: pcbnew.VECTOR2I, pos2: pcbnew.VECTOR2I, corner: pcbnew.VECTOR2I
) -> bool:
if end := self.add_track_segment_by_points(pos1, corner, layer, netcode):
end = self.add_track_segment_by_points(end, pos2, layer, netcode)
if end := self.add_track_segment_by_points(pos1, corner, **track_args):
end = self.add_track_segment_by_points(end, pos2, **track_args)
return end is not None
return False

Expand Down Expand Up @@ -574,7 +607,7 @@ def _angles_equal(angle1: float, angle2: float) -> bool:
end = None
# if in line, use one track segment
if pos1.x == pos2.x or pos1.y == pos2.y:
end = self.add_track_segment_by_points(pos1, pos2, layer, netcode)
end = self.add_track_segment_by_points(pos1, pos2, **track_args)
else:
# pads are not in single line, attempt routing with two segment track
if angle != 0:
Expand Down
6 changes: 3 additions & 3 deletions kbplacer/kbplacer_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@ class PluginSettings:
def run(settings: PluginSettings) -> pcbnew.BOARD:
if settings.create_from_annotated_layout:
builder = BoardBuilder(
settings.board_path,
switch_footprint=settings.switch_footprint,
diode_footprint=settings.diode_footprint,
)
board = builder.create_board(settings.layout_path)
board.Save(settings.board_path)

board = pcbnew.LoadBoard(settings.board_path)
else:
board = pcbnew.LoadBoard(settings.board_path)

placer = KeyPlacer(board, settings.key_distance)
placer.run(
Expand Down
20 changes: 19 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ def ignore_selected_drc_rules(board_path: Union[str, os.PathLike]) -> None:
for rule in rules_to_ignore:
project_data["board"]["design_settings"]["rule_severities"][rule] = "ignore"
with open(project_file, "w") as f:
json.dump(project_data, f)
json.dump(project_data, f, indent=2)


def kicad_cli() -> str:
Expand Down Expand Up @@ -350,6 +350,24 @@ def generate_drc(tmpdir, board_path: Union[str, os.PathLike]) -> None:
logger.debug(f.read())


def prepare_project_file(request, board_path: Union[str, os.PathLike]) -> None:
test_dir = Path(request.module.__file__).parent
major = KICAD_VERSION[0] if KICAD_VERSION else 0
templates_dir = test_dir / f"data/examples-references/kicad{major}/kicad-defaults"

destination = Path(board_path).parent
name = Path(board_path).stem
project_file = shutil.copy(
f"{templates_dir}/keyboard.kicad_pro", f"{destination}/{name}.kicad_pro"
)

with open(project_file, "r") as f:
project_data = json.load(f)
with open(project_file, "w") as f:
project_data["meta"]["filename"] = f"{name}.kicad_pro"
json.dump(project_data, f, indent=2)


def add_url_to_report(tmpdir, url: str) -> None:
url_path = tmpdir / "report"
urls = len(glob.glob(f"{url_path}/*url"))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
{
"board": {
"design_settings": {
"defaults": {
"board_outline_line_width": 0.049999999999999996,
"copper_line_width": 0.19999999999999998,
"copper_text_italic": false,
"copper_text_size_h": 1.5,
"copper_text_size_v": 1.5,
"copper_text_thickness": 0.3,
"copper_text_upright": false,
"courtyard_line_width": 0.049999999999999996,
"dimension_precision": 4,
"dimension_units": 3,
"dimensions": {
"arrow_length": 1270000,
"extension_offset": 500000,
"keep_text_aligned": true,
"suppress_zeroes": false,
"text_position": 0,
"units_format": 1
},
"fab_line_width": 0.09999999999999999,
"fab_text_italic": false,
"fab_text_size_h": 1.0,
"fab_text_size_v": 1.0,
"fab_text_thickness": 0.15,
"fab_text_upright": false,
"other_line_width": 0.09999999999999999,
"other_text_italic": false,
"other_text_size_h": 1.0,
"other_text_size_v": 1.0,
"other_text_thickness": 0.15,
"other_text_upright": false,
"pads": {
"drill": 0.762,
"height": 1.524,
"width": 1.524
},
"silk_line_width": 0.12,
"silk_text_italic": false,
"silk_text_size_h": 1.0,
"silk_text_size_v": 1.0,
"silk_text_thickness": 0.15,
"silk_text_upright": false,
"zones": {
"45_degree_only": false,
"min_clearance": 0.508
}
},
"diff_pair_dimensions": [],
"drc_exclusions": [],
"meta": {
"version": 2
},
"rule_severities": {
"annular_width": "error",
"clearance": "error",
"copper_edge_clearance": "error",
"courtyards_overlap": "error",
"diff_pair_gap_out_of_range": "error",
"diff_pair_uncoupled_length_too_long": "error",
"drill_out_of_range": "error",
"duplicate_footprints": "warning",
"extra_footprint": "warning",
"footprint_type_mismatch": "error",
"hole_clearance": "error",
"hole_near_hole": "error",
"invalid_outline": "ignore",
"item_on_disabled_layer": "error",
"items_not_allowed": "error",
"length_out_of_range": "error",
"malformed_courtyard": "error",
"microvia_drill_out_of_range": "error",
"missing_courtyard": "ignore",
"missing_footprint": "warning",
"net_conflict": "warning",
"npth_inside_courtyard": "ignore",
"padstack": "error",
"pth_inside_courtyard": "ignore",
"shorting_items": "error",
"silk_over_copper": "warning",
"silk_overlap": "ignore",
"skew_out_of_range": "error",
"through_hole_pad_without_hole": "error",
"too_many_vias": "error",
"track_dangling": "warning",
"track_width": "error",
"tracks_crossing": "error",
"unconnected_items": "error",
"unresolved_variable": "error",
"via_dangling": "warning",
"zone_has_empty_net": "error",
"zones_intersect": "error",
"lib_footprint_mismatch": "ignore"
},
"rules": {
"allow_blind_buried_vias": false,
"allow_microvias": false,
"max_error": 0.005,
"min_clearance": 0.0,
"min_copper_edge_clearance": 0.01,
"min_hole_clearance": 0.25,
"min_hole_to_hole": 0.25,
"min_microvia_diameter": 0.19999999999999998,
"min_microvia_drill": 0.09999999999999999,
"min_silk_clearance": 0.0,
"min_through_hole_diameter": 0.3,
"min_track_width": 0.19999999999999998,
"min_via_annular_width": 0.049999999999999996,
"min_via_diameter": 0.39999999999999997,
"use_height_for_length_calcs": true
},
"track_widths": [],
"via_dimensions": [],
"zones_allow_external_fillets": false,
"zones_use_no_outline": true
},
"layer_presets": []
},
"boards": [],
"cvpcb": {
"equivalence_files": []
},
"libraries": {
"pinned_footprint_libs": [],
"pinned_symbol_libs": []
},
"meta": {
"filename": "keyboard.kicad_pro",
"version": 1
},
"net_settings": {
"classes": [
{
"bus_width": 12.0,
"clearance": 0.2,
"diff_pair_gap": 0.25,
"diff_pair_via_gap": 0.25,
"diff_pair_width": 0.2,
"line_style": 0,
"microvia_diameter": 0.3,
"microvia_drill": 0.1,
"name": "Default",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.25,
"via_diameter": 0.8,
"via_drill": 0.4,
"wire_width": 6.0
},
{
"bus_width": 12.0,
"clearance": 0.2,
"diff_pair_gap": 0.25,
"diff_pair_via_gap": 0.25,
"diff_pair_width": 0.2,
"line_style": 0,
"microvia_diameter": 0.3,
"microvia_drill": 0.1,
"name": "Custom1",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.2,
"via_diameter": 0.8,
"via_drill": 0.4,
"wire_width": 6.0
},
{
"bus_width": 12.0,
"clearance": 0.2,
"diff_pair_gap": 0.25,
"diff_pair_via_gap": 0.25,
"diff_pair_width": 0.2,
"line_style": 0,
"microvia_diameter": 0.3,
"microvia_drill": 0.1,
"name": "Custom2",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.4,
"via_diameter": 0.8,
"via_drill": 0.4,
"wire_width": 6.0
}
],
"meta": {
"version": 2
},
"net_colors": null
},
"pcbnew": {
"last_paths": {
"gencad": "",
"idf": "",
"netlist": "",
"specctra_dsn": "",
"step": "",
"vrml": ""
},
"page_layout_descr_file": ""
},
"schematic": {
"legacy_lib_dir": "",
"legacy_lib_list": []
},
"sheets": [],
"text_variables": {}
}
Loading

0 comments on commit 0fd5fdf

Please sign in to comment.