[DRC] Added Workaround for problems with DRC exclusions

- Global option: `drc_exclusions_workaround`
- KiCad bug [11562](https://gitlab.com/kicad/code/kicad/-/issues/11562)
Fixes INTI-CMNB/KiAuto#26
This commit is contained in:
Salvador E. Tropea 2022-08-10 14:01:32 -03:00
parent 69269d8560
commit c5a6d894c9
11 changed files with 259 additions and 39 deletions

View File

@ -16,6 +16,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `--dont-stop` command line option, to try to continue even on errors (#209)
- PDF/SVG PCB Print: option to print all pages/single page (#236)
- iBoM: Support for variants that change component fields (#242)
- Workaround for problems with DRC exclusions (See INTI-CMNB/KiAuto#26)
Global option: `drc_exclusions_workaround`
KiCad bug [11562](https://gitlab.com/kicad/code/kicad/-/issues/11562)
### Fixed
- OAR computation (Report) (#225)

View File

@ -121,7 +121,7 @@ Notes:
[**Requests**](https://pypi.org/project/Requests/) [![Python module](https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/Python-logo-notext-22x22.png)](https://pypi.org/project/Requests/) [![PyPi dependency](https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/PyPI_logo_simplified-22x22.png)](https://pypi.org/project/Requests/) [![Debian](https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/debian-openlogo-22x22.png)](https://packages.debian.org/bullseye/python3-requests)
- Mandatory
[**KiCad Automation tools**](https://github.com/INTI-CMNB/KiAuto) v1.6.13 [![Tool](https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/llave-inglesa-22x22.png)](https://github.com/INTI-CMNB/KiAuto)![PyPi dependency](https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/PyPI_logo_simplified-22x22.png) ![Auto-download](https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/auto_download-22x22.png)
[**KiCad Automation tools**](https://github.com/INTI-CMNB/KiAuto) v2.0.0 [![Tool](https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/llave-inglesa-22x22.png)](https://github.com/INTI-CMNB/KiAuto)![PyPi dependency](https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/PyPI_logo_simplified-22x22.png) ![Auto-download](https://raw.githubusercontent.com/INTI-CMNB/KiBot/master/docs/images/auto_download-22x22.png)
- Mandatory for: `gencad`, `netlist`, `pdf_pcb_print`, `pdf_sch_print`, `render_3d`, `run_drc`, `run_erc`, `step`, `svg_pcb_print`, `svg_sch_print`, `update_xml`
- Optional to print the page frame in GUI mode for `pcb_print`
@ -640,6 +640,10 @@ global:
Uses the `strftime` format.
- `date_time_format`: [string='%Y-%m-%d_%H-%M-%S'] Format used for the PCB and schematic date when using the file timestamp. Uses the `strftime` format.
- `dir`: [string=''] Default pattern for the output directories.
- `drc_exclusions_workaround`: [boolean=false] KiCad 6 introduced DRC exclusions. They are stored in the project but ignored by the Python API.
This is reported as bug number 11562 (https://gitlab.com/kicad/code/kicad/-/issues/11562).
If you really need exclusions enable this option, this will use the GUI version of the DRC (slower).
Current KiCad version is 6.0.7 and the bug is still there.
- `drill_size_increment`: [number=0.05] This is the difference between drill tools in millimeters.
A manufacturer with 0.05 of increment has drills for 0.1, 0.15, 0.2, 0.25, etc..
- `edge_connector`: [string='no'] [yes,no,bevelled] Has the PCB edge connectors?

View File

@ -110,6 +110,11 @@ class Globals(FiltersOptions):
""" Format used for the PCB and schematic date when using the file timestamp. Uses the `strftime` format """
self.dir = ''
""" Default pattern for the output directories """
self.drc_exclusions_workaround = False
""" KiCad 6 introduced DRC exclusions. They are stored in the project but ignored by the Python API.
This is reported as bug number 11562 (https://gitlab.com/kicad/code/kicad/-/issues/11562).
If you really need exclusions enable this option, this will use the GUI version of the DRC (slower).
Current KiCad version is 6.0.7 and the bug is still there """
self.drill_size_increment = 0.05
""" This is the difference between drill tools in millimeters.
A manufacturer with 0.05 of increment has drills for 0.1, 0.15, 0.2, 0.25, etc. """

View File

@ -102,6 +102,7 @@ class GS(object):
global_cross_no_body = None
global_date_format = None
global_date_time_format = None
global_drc_exclusions_workaround = None
global_dir = None
global_drill_size_increment = None
global_edge_connector = None

View File

@ -7,7 +7,7 @@
Dependencies:
- from: KiAuto
role: mandatory
version: 1.4.0
version: 2.0.0
"""
import os
from sys import exit
@ -49,6 +49,8 @@ class Run_DRC(BasePreFlight): # noqa: F821
cmd = [command, 'run_drc', '-o', output]
if GS.filter_file:
cmd.extend(['-f', GS.filter_file])
if GS.global_drc_exclusions_workaround:
cmd.append('-F')
if BasePreFlight.get_option('ignore_unconnected'): # noqa: F821
cmd.append('-i')
cmd.extend([GS.pcb_file, self.expand_dirname(GS.out_dir)])

View File

@ -350,8 +350,8 @@ deps = '{\
"mandatory": true,\
"output": "run_drc",\
"version": [\
1,\
4,\
2,\
0,\
0\
]\
},\

View File

@ -0,0 +1,177 @@
{
"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": [
"clearance|176385001|79614999|7e023245-2c2b-4e2b-bfb9-5d35176e88f2|666713b0-70f4-42df-8761-f65bc212d03b",
"invalid_outline|165100000|80645000|e6d8b5bb-6f41-4222-9a89-b8a6280d0752|00000000-0000-0000-0000-000000000000"
],
"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": "error",
"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": "warning",
"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"
},
"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": "fail-project.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
}
],
"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": {}
}

View File

@ -133,7 +133,7 @@ def test_miss_sch(test_dir):
ctx = context.TestContext(test_dir, prj, 'pre_and_position')
ctx.run(EXIT_BAD_ARGS, extra=['pos_ascii'])
assert ctx.search_err('No SCH file found')
ctx.clean_up()
ctx.clean_up(keep_project=True)
def test_miss_sch_2(test_dir):
@ -141,7 +141,7 @@ def test_miss_sch_2(test_dir):
ctx = context.TestContext(test_dir, prj, 'pre_and_position')
ctx.run(NO_SCH_FILE, no_board_file=True, extra=['-e', 'bogus', 'pos_ascii'])
assert ctx.search_err('Schematic file not found')
ctx.clean_up()
ctx.clean_up(keep_project=True)
def test_miss_pcb(test_dir):

View File

@ -89,14 +89,26 @@ def test_drc_1(test_dir):
ctx.clean_up()
def test_drc_filter(test_dir):
def test_drc_filter_1(test_dir):
""" Test using internal filters """
prj = 'fail-project'
ctx = context.TestContext(test_dir, prj, 'drc_filter', '')
ctx.run()
# Check all outputs are there
ctx.expect_out_file(prj+'-drc.txt')
ctx.expect_out_file('kibot_errors.filter')
ctx.clean_up()
ctx.clean_up(keep_project=True)
def test_drc_filter_2(test_dir):
""" Test using KiCad 6 exclusions """
prj = 'fail-project'
ctx = context.TestContext(test_dir, prj, 'drc_filter_k6_exc', '')
ctx.run(extra_debug=True)
# Check all outputs are there
ctx.expect_out_file(prj+'-drc.txt')
ctx.expect_out_file('kibot_errors.filter')
ctx.clean_up(keep_project=True)
def test_drc_unco(test_dir):

View File

@ -279,49 +279,49 @@ def test_filter_not_list(test_dir):
ctx = context.TestContext(test_dir, PRJ, 'error_filter_not_list')
ctx.run(EXIT_BAD_CONFIG)
assert ctx.search_err(r"Option .?filters.? must be a list\(dict\) not `dict`")
ctx.clean_up()
ctx.clean_up(keep_project=True)
def test_filter_no_number_1(test_dir):
ctx = context.TestContext(test_dir, PRJ, 'error_filter_no_number')
ctx.run(EXIT_BAD_CONFIG)
assert ctx.search_err("Empty option .?number.?")
ctx.clean_up()
ctx.clean_up(keep_project=True)
def test_filter_no_number_2(test_dir):
ctx = context.TestContext(test_dir, PRJ, 'error_filter_no_number_2')
ctx.run(EXIT_BAD_CONFIG)
assert ctx.search_err("Missing .?error.?")
ctx.clean_up()
ctx.clean_up(keep_project=True)
def test_filter_no_regex_1(test_dir):
ctx = context.TestContext(test_dir, PRJ, 'error_filter_no_regex')
ctx.run(EXIT_BAD_CONFIG)
assert ctx.search_err("Empty option .?regex.?")
ctx.clean_up()
ctx.clean_up(keep_project=True)
def test_filter_wrong_entry(test_dir):
ctx = context.TestContext(test_dir, PRJ, 'error_filter_wrong_entry')
ctx.run(EXIT_BAD_CONFIG)
assert ctx.search_err("Unknown option .?numerito.?")
ctx.clean_up()
ctx.clean_up(keep_project=True)
def test_error_pre_list(test_dir):
ctx = context.TestContext(test_dir, PRJ, 'error_pre_list')
ctx.run(EXIT_BAD_CONFIG)
assert ctx.search_err("Incorrect .?preflight.? section")
ctx.clean_up()
ctx.clean_up(keep_project=True)
def test_error_pre_unk(test_dir):
ctx = context.TestContext(test_dir, PRJ, 'error_pre_unk')
ctx.run(EXIT_BAD_CONFIG)
assert ctx.search_err("Unknown preflight: .?run_drcs.?")
ctx.clean_up()
ctx.clean_up(keep_project=True)
def test_error_wrong_type_1(test_dir):
@ -329,7 +329,7 @@ def test_error_wrong_type_1(test_dir):
ctx = context.TestContext(test_dir, PRJ, 'error_pre_wrong_type_1')
ctx.run(EXIT_BAD_CONFIG)
assert ctx.search_err("In preflight 'run_drc': must be boolean")
ctx.clean_up()
ctx.clean_up(keep_project=True)
def test_error_wrong_type_2(test_dir):
@ -337,7 +337,7 @@ def test_error_wrong_type_2(test_dir):
ctx = context.TestContext(test_dir, PRJ, 'error_pre_wrong_type_2')
ctx.run(EXIT_BAD_CONFIG)
assert ctx.search_err("In preflight 'ignore_unconnected': must be boolean")
ctx.clean_up()
ctx.clean_up(keep_project=True)
def test_error_wrong_type_3(test_dir):
@ -345,7 +345,7 @@ def test_error_wrong_type_3(test_dir):
ctx = context.TestContext(test_dir, PRJ, 'error_pre_wrong_type_3')
ctx.run(EXIT_BAD_CONFIG)
assert ctx.search_err("In preflight 'run_erc': must be boolean")
ctx.clean_up()
ctx.clean_up(keep_project=True)
def test_error_wrong_type_4(test_dir):
@ -353,7 +353,7 @@ def test_error_wrong_type_4(test_dir):
ctx = context.TestContext(test_dir, PRJ, 'error_pre_wrong_type_4')
ctx.run(EXIT_BAD_CONFIG)
assert ctx.search_err("In preflight 'update_xml': must be boolean")
ctx.clean_up()
ctx.clean_up(keep_project=True)
def test_error_wrong_type_5(test_dir):
@ -361,56 +361,56 @@ def test_error_wrong_type_5(test_dir):
ctx = context.TestContext(test_dir, PRJ, 'error_pre_wrong_type_5')
ctx.run(EXIT_BAD_CONFIG)
assert ctx.search_err("In preflight 'check_zone_fills': must be boolean")
ctx.clean_up()
ctx.clean_up(keep_project=True)
def test_error_yaml(test_dir):
ctx = context.TestContext(test_dir, PRJ, 'error_yaml')
ctx.run(EXIT_BAD_CONFIG)
assert ctx.search_err("Error loading YAML")
ctx.clean_up()
ctx.clean_up(keep_project=True)
def test_out_not_list(test_dir):
ctx = context.TestContext(test_dir, PRJ, 'error_out_not_list')
ctx.run(EXIT_BAD_CONFIG)
assert ctx.search_err(".?outputs.? must be a list")
ctx.clean_up()
ctx.clean_up(keep_project=True)
def test_unk_section(test_dir):
ctx = context.TestContext(test_dir, PRJ, 'error_unk_section')
ctx.run(EXIT_BAD_CONFIG)
assert ctx.search_err("Unknown section .?bogus.? in config")
ctx.clean_up()
ctx.clean_up(keep_project=True)
def test_error_hpgl_pen_num(test_dir):
ctx = context.TestContext(test_dir, PRJ, 'error_hpgl_pen_num')
ctx.run(EXIT_BAD_CONFIG)
assert ctx.search_err("Option .?pen_number.? outside its range")
ctx.clean_up()
ctx.clean_up(keep_project=True)
def test_error_bom_wrong_format(test_dir):
ctx = context.TestContext(test_dir, PRJ, 'error_bom_wrong_format')
ctx.run(EXIT_BAD_CONFIG, no_board_file=True, extra=['-e', os.path.join(ctx.get_board_dir(), 'bom'+context.KICAD_SCH_EXT)])
assert ctx.search_err("Option .?format.? must be any of")
ctx.clean_up()
ctx.clean_up(keep_project=True)
def test_error_bom_column(test_dir):
ctx = context.TestContext(test_dir, PRJ, 'error_bom_column')
ctx.run(EXIT_BAD_CONFIG, no_board_file=True, extra=['-e', os.path.join(ctx.get_board_dir(), 'bom'+context.KICAD_SCH_EXT)])
assert ctx.search_err("Invalid column name .?Impossible.?")
ctx.clean_up()
ctx.clean_up(keep_project=True)
def test_error_bom_no_columns(test_dir):
ctx = context.TestContext(test_dir, PRJ, 'error_bom_column')
ctx.run(BOM_ERROR, no_board_file=True, extra=['-e', os.path.join(ctx.get_board_dir(), 'bom_no_xml'+context.KICAD_SCH_EXT)])
assert ctx.search_err("Failed to get the column names")
ctx.clean_up()
ctx.clean_up(keep_project=True)
def test_error_bom_no_field(test_dir):
@ -418,28 +418,28 @@ def test_error_bom_no_field(test_dir):
ctx.run(EXIT_BAD_CONFIG, no_board_file=True, extra=['-e', os.path.join(ctx.get_board_dir(),
'fail-erc'+context.KICAD_SCH_EXT)])
assert ctx.search_err("Missing or empty .?field.?")
ctx.clean_up()
ctx.clean_up(keep_project=True)
def test_error_wrong_boolean(test_dir):
ctx = context.TestContext(test_dir, PRJ, 'error_wrong_boolean')
ctx.run(EXIT_BAD_CONFIG)
assert ctx.search_err(".?exclude_edge_layer.? must be a boolean")
ctx.clean_up()
ctx.clean_up(keep_project=True)
def test_error_gerber_precision(test_dir):
ctx = context.TestContext(test_dir, PRJ, 'error_gerber_precision')
ctx.run(EXIT_BAD_CONFIG)
assert ctx.search_err(".?gerber_precision.? must be 4.5 or 4.6")
ctx.clean_up()
ctx.clean_up(keep_project=True)
def test_error_wrong_drill_marks_1(test_dir):
ctx = context.TestContext(test_dir, PRJ, 'error_wrong_drill_marks')
ctx.run(EXIT_BAD_CONFIG)
assert ctx.search_err(r"Option `drill_marks` must be any of \['none', 'small', 'full'\] not `bogus`")
ctx.clean_up()
ctx.clean_up(keep_project=True)
def test_error_print_pcb_no_layer(test_dir):
@ -653,28 +653,28 @@ def test_error_wrong_import_type(test_dir):
ctx = context.TestContext(test_dir, PRJ, 'error_wrong_import_type')
ctx.run(EXIT_BAD_CONFIG)
assert ctx.search_err(r"Incorrect `import` section \(must be a list\)")
ctx.clean_up()
ctx.clean_up(keep_project=True)
def test_error_import_not_str(test_dir):
ctx = context.TestContext(test_dir, PRJ, 'error_import_not_str')
ctx.run(EXIT_BAD_CONFIG)
assert ctx.search_err(r"`import` items must be strings")
ctx.clean_up()
ctx.clean_up(keep_project=True)
def test_error_import_miss_file(test_dir):
ctx = context.TestContext(test_dir, PRJ, 'error_import_miss_file')
ctx.run(EXIT_BAD_CONFIG)
assert ctx.search_err(r"missing import file")
ctx.clean_up()
ctx.clean_up(keep_project=True)
def test_error_import_no_outputs(test_dir):
ctx = context.TestContext(test_dir, PRJ, 'error_import_no_outputs')
ctx.run()
assert ctx.search_err(r"No outputs found in `(.*)drc.kibot.yaml`")
ctx.clean_up()
ctx.clean_up(keep_project=True)
def test_same_name_1(test_dir):
@ -682,7 +682,7 @@ def test_same_name_1(test_dir):
ctx = context.TestContext(test_dir, PRJ, 'error_same_name_1')
ctx.run(EXIT_BAD_CONFIG)
assert ctx.search_err(r"Output name `position` already defined")
ctx.clean_up()
ctx.clean_up(keep_project=True)
def test_same_name_2(test_dir):
@ -690,7 +690,7 @@ def test_same_name_2(test_dir):
ctx = context.TestContext(test_dir, PRJ, 'error_same_name_2')
ctx.run(EXIT_BAD_CONFIG)
assert ctx.search_err(r"Output name `position` already defined")
ctx.clean_up()
ctx.clean_up(keep_project=True)
def test_same_name_3(test_dir):
@ -698,7 +698,7 @@ def test_same_name_3(test_dir):
ctx = context.TestContext(test_dir, PRJ, 'error_same_name_3')
ctx.run(EXIT_BAD_CONFIG)
assert ctx.search_err(r"Output name `position` already defined, while importing from")
ctx.clean_up()
ctx.clean_up(keep_project=True)
def test_extends_1(test_dir):
@ -706,4 +706,4 @@ def test_extends_1(test_dir):
ctx = context.TestContext(test_dir, PRJ, 'error_extends_1')
ctx.run(EXIT_BAD_CONFIG)
assert ctx.search_err(r"In section 'position_mine' \(position\): Unknown output `position2` in `extends`")
ctx.clean_up()
ctx.clean_up(keep_project=True)

View File

@ -0,0 +1,16 @@
# Example KiBot config file
kibot:
version: 1
globals:
drc_exclusions_workaround: true
preflight:
run_drc: true
filters:
- filter_msg: 'Ignore unconnected pad 2 of C4'
error: unconnected_items
regexp: 'Pad 2 .* of C4'
- filter_msg: 'Ignore missing outline'
error: invalid_outline
regexp: 'no edges found'