Skip to content

Commit

Permalink
[E/DRC] Moved common code to a base class
Browse files Browse the repository at this point in the history
  • Loading branch information
set-soft committed Apr 5, 2024
1 parent 7556963 commit 1880df0
Show file tree
Hide file tree
Showing 4 changed files with 342 additions and 524 deletions.
18 changes: 9 additions & 9 deletions docs/source/configuration/sup_preflights.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@ Supported preflights

- Valid keys:

- **output** :index:`: <pair: preflight - drc; output>` [string='%f-%i%I%v.%x'] Name for the generated archive (%i=erc %x=according to format). Affected by global options.
- **output** :index:`: <pair: preflight - drc; output>` [string='%f-%i%I%v.%x'] Name for the generated archive (%i=drc %x=according to format). Affected by global options.
- ``all_track_errors`` :index:`: <pair: preflight - drc; all_track_errors>` [boolean=false] Report all the errors for all the tracks, not just the first.
- ``dir`` :index:`: <pair: preflight - drc; dir>` [string=''] Sub-directory for the report.
- ``dont_stop`` :index:`: <pair: preflight - drc; dont_stop>` [boolean=false] Continue even if we detect DRC errors.
- ``enabled`` :index:`: <pair: preflight - drc; enabled>` [boolean=true] Enable the DRC. This is the replacement for the boolean value.
- ``filters`` :index:`: <pair: preflight - drc; filters>` [list(dict)] Used to manipulate the DRC violations. Avoid using the *filters* preflight.
- ``dont_stop`` :index:`: <pair: preflight - drc; dont_stop>` [boolean=false] Continue even if we detect errors.
- ``enabled`` :index:`: <pair: preflight - drc; enabled>` [boolean=true] Enable the check. This is the replacement for the boolean value.
- ``filters`` :index:`: <pair: preflight - drc; filters>` [list(dict)] Used to manipulate the violations. Avoid using the *filters* preflight.

- Valid keys:

Expand All @@ -63,7 +63,7 @@ Supported preflights
- ``ignore_unconnected`` :index:`: <pair: preflight - drc; ignore_unconnected>` [boolean=false] Ignores the unconnected nets. Useful if you didn't finish the routing.
- ``schematic_parity`` :index:`: <pair: preflight - drc; schematic_parity>` [boolean=true] Check if the PCB and the schematic are coincident.
- ``units`` :index:`: <pair: preflight - drc; units>` [string='millimeters'] [millimeters,inches,mils] Units used for the positions. Affected by global options.
- ``warnings_as_errors`` :index:`: <pair: preflight - drc; warnings_as_errors>` [boolean=false] DRC warnings are considered errors, they still reported as errors, but consider it an error.
- ``warnings_as_errors`` :index:`: <pair: preflight - drc; warnings_as_errors>` [boolean=false] Warnings are considered errors, they still reported as errors, but consider it an error.

- **erc**: :index:`: <pair: preflights; erc>` [boolean=false|dict] Runs the ERC (Electrical Rules Check). To ensure the schematic is electrically correct.
This is a replacement for the *run_erc* preflight that needs KiCad 8 or newer.
Expand All @@ -72,9 +72,9 @@ Supported preflights

- **output** :index:`: <pair: preflight - erc; output>` [string='%f-%i%I%v.%x'] Name for the generated archive (%i=erc %x=according to format). Affected by global options.
- ``dir`` :index:`: <pair: preflight - erc; dir>` [string=''] Sub-directory for the report.
- ``dont_stop`` :index:`: <pair: preflight - erc; dont_stop>` [boolean=false] Continue even if we detect ERC errors.
- ``enabled`` :index:`: <pair: preflight - erc; enabled>` [boolean=true] Enable the ERC. This is the replacement for the boolean value.
- ``filters`` :index:`: <pair: preflight - erc; filters>` [list(dict)] Used to manipulate the ERC violations. Avoid using the *filters* preflight.
- ``dont_stop`` :index:`: <pair: preflight - erc; dont_stop>` [boolean=false] Continue even if we detect errors.
- ``enabled`` :index:`: <pair: preflight - erc; enabled>` [boolean=true] Enable the check. This is the replacement for the boolean value.
- ``filters`` :index:`: <pair: preflight - erc; filters>` [list(dict)] Used to manipulate the violations. Avoid using the *filters* preflight.

- Valid keys:

Expand All @@ -92,7 +92,7 @@ Supported preflights
You can specify multiple formats.

- ``units`` :index:`: <pair: preflight - erc; units>` [string='millimeters'] [millimeters,inches,mils] Units used for the positions. Affected by global options.
- ``warnings_as_errors`` :index:`: <pair: preflight - erc; warnings_as_errors>` [boolean=false] ERC warnings are considered errors, they still reported as errors, but consider it an error.
- ``warnings_as_errors`` :index:`: <pair: preflight - erc; warnings_as_errors>` [boolean=false] Warnings are considered errors, they still reported as errors, but consider it an error.

- **erc_warnings**: :index:`: <pair: preflights; erc_warnings>` [boolean=false] **Deprecated**, use the `warnings_as_errors` option from `run_erc`.
Option for `run_erc`. ERC warnings are considered errors.
Expand Down
308 changes: 308 additions & 0 deletions kibot/pre_any_xrc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 Salvador E. Tropea
# Copyright (c) 2020-2024 Instituto Nacional de Tecnología Industrial
# License: AGPL-3.0
# Project: KiBot (formerly KiPlot)
import json
import os
from . import __version__
from .bom.kibot_logo import KIBOT_LOGO, KIBOT_LOGO_W, KIBOT_LOGO_H
from .error import KiPlotConfigurationError
from .gs import GS
from .kiplot import load_board, load_sch, run_command
from .misc import (ERC_ERROR, DRC_ERROR, W_ERCJSON, W_DRCJSON, STYLE_COMMON, TABLE_MODERN, HEAD_COLOR_B, HEAD_COLOR_B_L,
TD_ERC_CLASSES, GENERATOR_CSS)
from .optionable import Optionable
from .pre_base import BasePreFlight
from .pre_filters import FilterOptions, FiltersOptions
from .macros import macros, document # noqa: F401
from .log import get_logger
logger = get_logger(__name__)
UNITS_2_KICAD = {'millimeters': 'mm', 'inches': 'in', 'mils': 'mils'}


class FilterOptionsXRC(FilterOptions):
def __init__(self):
super().__init__()
with document:
self.change_to = 'ignore'
""" [error,warning,ignore] The action of the filter.
Changing to *ignore* is the default and is used to suppress a violation, but you can also change
it to be an *error* or a *warning*. Note that violations excluded by KiCad are also analyzed,
so you can revert a GUI exclusion """
# number is for KiCad 5, remove it
del self.number
del self.error_number
# Avoid mentioning KiCad 5/6 in the "error" help
self.set_doc('error', " [string=''] Error id we want to exclude")


class ERCOptions(FiltersOptions):
""" ERC options """
def __init__(self):
with document:
self.enabled = True
""" Enable the check. This is the replacement for the boolean value """
self.dir = ''
""" Sub-directory for the report """
self.output = GS.def_global_output
""" *Name for the generated archive (%i=erc %x=according to format) """
self.format = Optionable
""" [string|list(string)='HTML'][RPT,HTML,CSV,JSON] Format/s used for the report.
You can specify multiple formats """
self.warnings_as_errors = False
""" Warnings are considered errors, they still reported as errors, but consider it an error """
self.dont_stop = False
""" Continue even if we detect errors """
self.units = 'millimeters'
""" [millimeters,inches,mils] Units used for the positions. Affected by global options """
super().__init__()
self.filters = FilterOptionsXRC
self.set_doc('filters', " [list(dict)] Used to manipulate the violations. Avoid using the *filters* preflight")
self._unknown_is_error = True
self._format_example = 'HTML,RPT'

def config(self, parent):
super().config(parent)
self.format = Optionable.force_list(self.format)
if not self.format:
self.format = ['HTML']
for f in self.format:
if f not in {'RPT', 'HTML', 'CSV', 'JSON'}:
raise KiPlotConfigurationError(f'unkwnown format `{f}`')


class DRCOptions(ERCOptions):
""" DRC options """
def __init__(self):
with document:
self.schematic_parity = True
""" Check if the PCB and the schematic are coincident """
self.all_track_errors = False
""" Report all the errors for all the tracks, not just the first """
self.ignore_unconnected = False
""" Ignores the unconnected nets. Useful if you didn't finish the routing """
super().__init__()
self.set_doc('output', self.get_doc('output')[0].replace('erc', 'drc'))


class XRC(BasePreFlight):
def __init__(self, name, value, cls):
super().__init__(name, value)
if isinstance(value, bool):
f = cls()
f.enabled = value
f.format = ['HTML']
elif isinstance(value, dict):
f = cls()
f.set_tree(value)
f.config(self)
else:
raise KiPlotConfigurationError('must be boolean or dict')
# Transfer the options to this class
for k, v in dict(f.get_attrs_gen()).items():
setattr(self, '_'+k, v)
self._format = f.format
self._filters = None if isinstance(f.filters, type) else f.unparsed
self._expand_ext = self._format[0].lower()
self._opts_cls = cls

def get_targets(self):
""" Returns a list of targets generated by this preflight """
if self._sch_related:
load_sch()
else:
load_board()
out_dir = self.expand_dirname(GS.out_dir)
if GS.global_dir and GS.global_use_dir_for_preflights:
out_dir = os.path.join(out_dir, self.expand_dirname(GS.global_dir))
names = []
for f in self._format:
self._expand_ext = f.lower()
name = Optionable.expand_filename_both(self, self._output, is_sch=self._sch_related)
names.append(os.path.abspath(os.path.join(out_dir, self._dir, name)))
return names

def get_item_txt(self, item, indent=4, sep='\n'):
desc = item.get('description', '')
pos = item.get('pos', None)
if pos:
x = pos.get('x', 0)
y = pos.get('y', 0)
pos_txt = f'@({x} {self.units}, {y} {self.units}): '
else:
pos_txt = ''
return (' '*indent)+f'{pos_txt}{desc}'+sep

def add_html_violation(self, violation):
severity = violation.get('severity', 'error')
excluded = violation.get('excluded', False)
type = violation.get('type', '')
description = violation.get('description', '')
details = ''
for item in violation.get('items', []):
details += self.get_item_txt(item, indent=0, sep='<br>')
html = f' <tr id="{self.html_id}">\n'
cl = 'td-excluded' if excluded else ('td-error' if severity == 'error' else 'td-warning')
html += f' <td class="{cl}">{type}</td>\n'
html += f' <td>{description}</td>\n'
html += f' <td>{details}</td>\n'
html += ' </tr>\n'
self.html_id += 1
return html

def create_json(self, data):
return json.dumps(data, indent=4)

def create_html_top(self, data):
# HTML Head
html = '<html>\n'
html += '<head>\n'
html += ' <meta charset="UTF-8">\n' # UTF-8 encoding for unicode support
if self._sch_related:
title = 'ERC report for '+(GS.pro_basename or GS.sch_basename or '')
else:
title = 'DRC report for '+(GS.pro_basename or GS.pcb_basename or '')
html += f' <title>{title}</title>\n'
# CSS
html += '<style>\n'
style = STYLE_COMMON
style += TABLE_MODERN.replace('@bg@', HEAD_COLOR_B)
style += TABLE_MODERN.replace('@bgl@', HEAD_COLOR_B_L)
style += TD_ERC_CLASSES
style += GENERATOR_CSS
style += ' .head-table { margin-left: auto; margin-right: auto; }\n'
style += ' .content-table { margin-left: auto; margin-right: auto }\n'
html += style
html += '</style>\n'
html += '</head>\n'
html += '<body>\n'

img = 'data:image/png;base64,'+KIBOT_LOGO
img_w = KIBOT_LOGO_W
img_h = KIBOT_LOGO_H
html += '<table class="head-table">\n'
html += '<tr>\n'
html += ' <td rowspan="3">\n'
html += f' <img src="{img}" alt="Logo" width="{img_w}" height="{img_h}">\n'
html += ' </td>\n'
html += ' <td colspan="2" class="cell-title">\n'
html += f' <div class="title">{title}</div>\n'
html += ' </td>\n'
html += '</tr>\n'
html += '<tr>\n'
html += ' <td class="cell-info">\n'
if self._sch_related:
html += f' <b>Schematic</b>: {GS.sch_basename}<br>\n'
html += f' <b>Revision</b>: {GS.sch.revision}<br>\n'
else:
html += f' <b>PCB</b>: {GS.pcb_basename}<br>\n'
html += f' <b>Revision</b>: {GS.pcb_rev}<br>\n'
dt = data.get('date', '??')
html += f' <b>Date</b>: {dt}<br>\n'
kv = data.get('kicad_version', GS.kicad_version)
html += f' <b>KiCad Version</b>: {kv}<br>\n'
html += ' </td>\n'
html += ' <td class="cell-stats">\n'
txt_error = f'<b>Errors</b>: {self.c_err}'+(f' (+{self.c_err_excl} excluded)' if self.c_err_excl else '')
txt_warn = f'<b>Warnings</b>: {self.c_warn}'+(f' (+{self.c_warn_excl} excluded)' if self.c_warn_excl else '')
txt_total = f'<b>Total</b>: {self.c_tot}'+(f' (+{self.c_tot_excl} excluded)' if self.c_tot_excl else '')
html += f' {txt_error}<br>\n'
html += f' {txt_warn}<br>\n'
html += f' {txt_total}<br>\n'
html += ' </td>\n'
html += '</tr>\n'
html += '</table>\n'
self.html_id = 0
return html

def create_html_bottom(self):
html = ('<p class="generator">Generated by <a href="https://github.com/INTI-CMNB/KiBot/">KiBot</a> v{}</p>\n'.
format(__version__))
html += '</body>\n'
html += '</html>\n'
return html

def create_html_violations(self, violations):
html = '<table class="content-table">\n'
html += ' <thead>\n'
html += ' <tr>\n'
for h in ['Type', 'Description', 'Details']:
html += f' <th>{h}</th>\n'
html += ' </tr>\n'
html += ' </thead>\n'
html += ' <tbody>\n'
# Errors
for violation in violations:
if violation.get('severity', 'error') == 'error' and not violation.get('excluded', False):
html += self.add_html_violation(violation)
# Warnings
for violation in violations:
if violation.get('severity', 'error') == 'warning' and not violation.get('excluded', False):
html += self.add_html_violation(violation)
# Excluded
for violation in violations:
if violation.get('excluded', False):
html += self.add_html_violation(violation)
html += ' </tbody>\n'
html += '</table>\n'
return html

def run(self):
# Differences between ERC and DRC
if self._sch_related:
nm = 'ERC'
err = ERC_ERROR
erc_warnings = BasePreFlight.get_option('erc_warnings')
wjson = W_ERCJSON
else:
nm = 'DRC'
err = DRC_ERROR
erc_warnings = False
wjson = W_DRCJSON
nml = nm.lower()
# Now do the run
if not GS.ki8:
raise KiPlotConfigurationError(f'The `{nml}` preflight needs KiCad 8 or newer, use `run_{nml}` instead')
# Compute the output name and make sure the path exists
outputs = self.get_targets()
output = outputs[0]
os.makedirs(os.path.dirname(output), exist_ok=True)
# Run the xRC from the CLI
cmd = self.get_command(output)
logger.info(f'- Running the {nm}')
run_command(cmd)
# Read the result
with open(output, 'rt') as f:
raw = f.read()
try:
data = json.loads(raw)
except json.decoder.JSONDecodeError:
raise KiPlotConfigurationError(f"Corrupted {nm} report `{output}`:\n{raw}")
if data.get('$schema', '') != f'https://schemas.kicad.org/{nml}.v1.json':
logger.warning(f'{wjson}Unknown JSON schema, {nm} might fail')
self.units = data.get('coordinate_units', 'mm')
# Apply KiBot filters
self.apply_filters(data)
# Generate the desired output format
for (f, output) in zip(self._format, outputs):
if f == 'CSV':
res = self.create_csv(data)
elif f == 'HTML':
res = self.create_html(data)
elif f == 'JSON':
res = self.create_json(data)
else:
res = self.create_txt(data)
# Write it to the output file
with open(output, 'wt') as f:
f.write(res)
# Report the result
self.report('error', self.c_err, data)
self.report('warning', self.c_warn, data)
# Check the final status
error_level = 0 if self._dont_stop else err
if self.c_err:
GS.exit_with_error(f'{nm} errors: {self.c_err}', error_level)
elif self.c_warn and (self._warnings_as_errors or erc_warnings): # noqa: F821
GS.exit_with_error(f'{nm} warnings: {self.c_warn}, promoted as errors', error_level)
Loading

0 comments on commit 1880df0

Please sign in to comment.