diff --git a/CHANGELOG.md b/CHANGELOG.md index 4430080e..7c832335 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support for `--subst-models` option for KiCad 6's kicad2step. (#137) - Added global options to define the PCB details (`pcb_material`, `solder_mask_color`, `silk_screen_color` and `pcb_finish`) +- Report generation (for design house) (#93) ### Changed - Internal BoM: now components with different Tolerance, Voltage, Current diff --git a/MANIFEST.in b/MANIFEST.in index b410f47e..1e0cca63 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ include MANIFEST.in include LICENSE include README.md +include kibot/report_templates/*.txt diff --git a/README.md b/README.md index eb7383d3..76a480d6 100644 --- a/README.md +++ b/README.md @@ -1771,6 +1771,25 @@ Next time you need this list just use an alias, like this: - `output_id`: [string=''] Text to use for the %I expansion content. To differentiate variations of this output. - `run_by_default`: [boolean=true] When enabled this output will be created when no specific outputs are requested. +* Design report + * Type: `report` + * Description: Generates a report about the design. + Mainly oriented to be sent to the manufacturer or check PCB details. + * Valid keys: + - `comment`: [string=''] A comment for documentation purposes. + - `dir`: [string='./'] Output directory for the generated files. If it starts with `+` the rest is concatenated to the default dir. + - `disable_run_by_default`: [string|boolean] Use it to disable the `run_by_default` status of other output. + Useful when this output extends another and you don't want to generate the original. + Use the boolean true value to disable the output you are extending. + - `extends`: [string=''] Copy the `options` section from the indicated output. + - `name`: [string=''] Used to identify this particular output definition. + - `options`: [dict] Options for the `report` output. + * Valid keys: + - `output`: [string='%f-%i%v.%x'] Output file name (%i='report', %x='txt'). Affected by global options. + - `template`: [string='full'] Name for one of the internal templates (full, simple) or a custom template file. + - `output_id`: [string=''] Text to use for the %I expansion content. To differentiate variations of this output. + - `run_by_default`: [boolean=true] When enabled this output will be created when no specific outputs are requested. + * Schematic with variant generator * Type: `sch_variant` * Description: Creates a copy of the schematic with all the filters and variants applied. diff --git a/docs/samples/generic_plot.kibot.yaml b/docs/samples/generic_plot.kibot.yaml index a886765b..8b836a8e 100644 --- a/docs/samples/generic_plot.kibot.yaml +++ b/docs/samples/generic_plot.kibot.yaml @@ -1232,6 +1232,18 @@ outputs: # Same result as using the mouse wheel in the 3D viewer zoom: 0 + # Design report: + # Mainly oriented to be sent to the manufacturer or check PCB details. + - name: 'report_example' + comment: 'Generates a report about the design.' + type: 'report' + dir: 'Example/report_dir' + options: + # [string='%f-%i%v.%x'] Output file name (%i='report', %x='txt'). Affected by global options + output: '%f-%i%v.%x' + # [string='full'] Name for one of the internal templates (full, simple) or a custom template file + template: 'full' + # Schematic with variant generator: # This copy isn't intended for development. # Is just a tweaked version of the original where you can look at the results. diff --git a/kibot/out_report.py b/kibot/out_report.py new file mode 100644 index 00000000..b0d63294 --- /dev/null +++ b/kibot/out_report.py @@ -0,0 +1,464 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2022 Salvador E. Tropea +# Copyright (c) 2022 Instituto Nacional de Tecnología Industrial +# License: GPL-3.0 +# Project: KiBot (formerly KiPlot) +import os +import re +import pcbnew + +from .gs import GS +from .misc import UI_SMD, UI_VIRTUAL, MOD_THROUGH_HOLE, MOD_SMD, MOD_EXCLUDE_FROM_POS_FILES +from .out_base import BaseOptions +from .error import KiPlotConfigurationError +from .macros import macros, document, output_class # noqa: F401 +from . import log + +logger = log.get_logger() +INF = float('inf') + + +def do_round(v, dig): + v = round(v+1e-9, dig) + return v if dig else int(v) + + +def to_mm(iu, dig=2): + """ KiCad Internal Units to millimeters """ + return do_round(iu/pcbnew.IU_PER_MM, dig) + + +def to_mils(iu, dig=0): + """ KiCad Internal Units to mils (1/1000 inch) """ + return do_round(iu/pcbnew.IU_PER_MILS, dig) + + +def to_inches(iu, dig=2): + """ KiCad Internal Units to inches """ + return do_round(iu/(pcbnew.IU_PER_MILS*1000), dig) + + +def get_class_index(val, lst): + """ Used to search in an Eurocircuits class vector. + Returns the first match that is >= to val. """ + val = to_mm(val, 3) + for c, v in enumerate(lst): + if val >= v: + return c + return c+1 + + +def get_pattern_class(track, clearance, oar): + """ Returns the Eurocircuits Pattern class for a track width, clearance and OAR """ + c1 = (0.25, 0.2, 0.175, 0.150, 0.125, 0.1, 0.09) + c2 = (0.2, 0.15, 0.15, 0.125, 0.125, 0.1, 0.1) + ct = get_class_index(track, c1) + cc = get_class_index(clearance, c1) + co = get_class_index(oar, c2) + cf = max(ct, max(cc, co)) + return cf + 3 + + +def get_drill_class(via_drill): + """ Returns the Eurocircuits Drill class for a drill size. + This is the real (tool) size. """ + c3 = (0.6, 0.45, 0.35, 0.25, 0.2) + cd = get_class_index(via_drill, c3) + return chr(ord('A') + cd) + + +def to_top_bottom(front, bottom): + """ Returns a text indicating if the feature is in top/bottom layers """ + if front and bottom: + return "TOP / BOTTOM" + elif front: + return "TOP" + elif bottom: + return "BOTTOM" + return "NONE" + + +def to_smd_tht(smd, tht): + """ Returns a text indicating if the components are SMD/THT """ + if smd and tht: + return "SMD + THT" + elif smd: + return "SMD" + elif tht: + return "THT" + return "NONE" + + +class ReportOptions(BaseOptions): + def __init__(self): + with document: + self.output = GS.def_global_output + """ Output file name (%i='report', %x='txt') """ + self.template = 'full' + """ Name for one of the internal templates (full, simple) or a custom template file """ + super().__init__() + self._expand_id = 'report' + self._expand_ext = 'txt' + self._mm_digits = 2 + self._mils_digits = 0 + self._in_digits = 2 + + def config(self, parent): + super().config(parent) + if self.template.lower() in ('full', 'simple'): + self.template = os.path.abspath(os.path.join(os.path.dirname(__file__), 'report_templates', + 'report_'+self.template.lower()+'.txt')) + if not os.path.isfile(self.template): + raise KiPlotConfigurationError("Missing report template: `{}`".format(self.template)) + + def do_replacements(self, line, defined): + """ Replace ${VAR} patterns """ + for var in re.findall(r'\$\{([^\s\}]+)\}', line): + if var[0] == '_': + # Prevent access to internal data + continue + units = None + var_ori = var + if var.endswith('_mm'): + units = to_mm + digits = self._mm_digits + var = var[:-3] + elif var.endswith('_in'): + units = to_inches + digits = self._in_digits + var = var[:-3] + elif var.endswith('_mils'): + units = to_mils + digits = self._mils_digits + var = var[:-5] + if var in defined: + val = defined[var] + if val == INF: + val = 'N/A' + elif units is not None and isinstance(val, (int, float)): + val = units(val, digits) + line = line.replace('${'+var_ori+'}', str(val)) + else: + print('Error: Unable to expand `{}`'.format(var)) + return line + + def context_defined_tracks(self, line): + """ Replace iterator for the `defined_tracks` context """ + text = '' + for t in sorted(self._track_sizes): + if not t: + continue # KiCad 6 + text += self.do_replacements(line, {'track': t}) + return text + + def context_used_tracks(self, line): + """ Replace iterator for the `used_tracks` context """ + text = '' + for t in sorted(self._tracks_m.keys()): + text += self.do_replacements(line, {'track': t, 'count': self._tracks_m[t], + 'defined': 'yes' if t in self._tracks_defined else 'no'}) + return text + + def context_defined_vias(self, line): + """ Replace iterator for the `defined_vias` context """ + text = '' + for v in self._via_sizes_sorted: + text += self.do_replacements(line, {'pad': v[1], 'drill': v[0]}) + return text + + def context_used_vias(self, line): + """ Replace iterator for the `used_vias` context """ + text = '' + for v in self._vias_m: + d = v[1] + h = v[0] + aspect = round(self.thickness/d, 1) + # IPC-2222 Table 9.4 + producibility_level = 'C' + if aspect < 9: + if aspect < 5: + producibility_level = 'A' + else: + producibility_level = 'B' + defined = {'pad': v[1], 'drill': v[0]} + defined['count'] = self._vias[v] + defined['aspect'] = aspect + defined['producibility_level'] = producibility_level + defined['defined'] = 'yes' if (h, d) in self._vias_defined else 'no' + text += self.do_replacements(line, defined) + return text + + def context_hole_sizes_no_vias(self, line): + """ Replace iterator for the `hole_sizes_no_vias` context """ + text = '' + for d in sorted(self._drills.keys()): + text += self.do_replacements(line, {'drill': d, 'count': self._drills[d]}) + return text + + @staticmethod + def is_pure_smd_5(m): + return m.GetAttributes() == UI_SMD + + @staticmethod + def is_pure_smd_6(m): + return m.GetAttributes() & (MOD_THROUGH_HOLE | MOD_SMD) == MOD_SMD + + @staticmethod + def is_not_virtual_5(m): + return m.GetAttributes() != UI_VIRTUAL + + @staticmethod + def is_not_virtual_6(m): + return not (m.GetAttributes() & MOD_EXCLUDE_FROM_POS_FILES) + + def get_attr_tests(self): + if GS.ki5(): + return self.is_pure_smd_5, self.is_not_virtual_5 + return self.is_pure_smd_6, self.is_not_virtual_6 + + def meassure_pcb(self, board): + edge_layer = board.GetLayerID('Edge.Cuts') + x1 = y1 = x2 = y2 = None + draw_type = 'DRAWSEGMENT' if GS.ki5() else 'PCB_SHAPE' + for d in board.GetDrawings(): + if d.GetClass() == draw_type and d.GetLayer() == edge_layer: + if x1 is None: + p = d.GetStart() + x1 = x2 = p.x + y1 = y2 = p.y + for p in [d.GetStart(), d.GetEnd()]: + x2 = max(x2, p.x) + y2 = max(y2, p.y) + x1 = min(x1, p.x) + y1 = min(y1, p.y) + if x1 is None: + self.bb_w = self.bb_h = INF + else: + self.bb_w = x2-x1 + self.bb_h = y2-y1 + + def collect_data(self, board): + ds = board.GetDesignSettings() + ########################################################### + # Board size + ########################################################### + # The value returned by ComputeBoundingBox(True) adds the drawing width! + bb = board.ComputeBoundingBox(True) + self.bb_w_d = bb.GetWidth() + self.bb_h_d = bb.GetHeight() + self.meassure_pcb(board) + ########################################################### + # Board thickness + ########################################################### + self.thickness = ds.GetBoardThickness() + ########################################################### + # Number of layers + ########################################################### + self.layers = ds.GetCopperLayerCount() + ########################################################### + # Solder mask layers + ########################################################### + fmask = board.IsLayerEnabled(board.GetLayerID('F.Mask')) + bmask = board.IsLayerEnabled(board.GetLayerID('B.Mask')) + self.solder_mask = to_top_bottom(fmask, bmask) + ########################################################### + # Silk screen + ########################################################### + fsilk = board.IsLayerEnabled(board.GetLayerID('F.SilkS')) + bsilk = board.IsLayerEnabled(board.GetLayerID('B.SilkS')) + self.silk_screen = to_top_bottom(fsilk, bsilk) + ########################################################### + # Clearance + ########################################################### + self.clearance = ds.GetSmallestClearanceValue() + # This seems to be bogus: + # h2h = ds.m_HoleToHoleMin + ########################################################### + # Track width (min) + ########################################################### + self.track_d = ds.m_TrackMinWidth + tracks = board.GetTracks() + self.oar_vias = self.track = INF + self._vias = {} + self._tracks_m = {} + track_type = 'TRACK' if GS.ki5() else 'PCB_TRACK' + via_type = 'VIA' if GS.ki5() else 'PCB_VIA' + for t in tracks: + tclass = t.GetClass() + if tclass == track_type: + w = t.GetWidth() + self.track = min(w, self.track) + self._tracks_m[w] = self._tracks_m.get(w, 0) + 1 + elif tclass == via_type: + via = t.Cast() + via_id = (via.GetDrill(), via.GetWidth()) + self._vias[via_id] = self._vias.get(via_id, 0) + 1 + self.oar_vias = min(self.oar_vias, via_id[1] - via_id[0]) + self.track_min = min(self.track_d, self.track) + ########################################################### + # Drill (min) + ########################################################### + modules = board.GetModules() if GS.ki5() else board.GetFootprints() + self._drills = {} + self._drills_oval = {} + self.oar_pads = self.pad_drill = INF + self.slot = INF + self.top_smd = self.top_tht = self.bot_smd = self.bot_tht = 0 + top_layer = board.GetLayerID('F.Cu') + bottom_layer = board.GetLayerID('B.Cu') + is_pure_smd, is_not_virtual = self.get_attr_tests() + for m in modules: + layer = m.GetLayer() + if layer == top_layer: + if is_pure_smd(m): + self.top_smd += 1 + elif is_not_virtual(m): + self.top_tht += 1 + elif layer == bottom_layer: + if is_pure_smd(m): + self.bot_smd += 1 + elif is_not_virtual(m): + self.bot_tht += 1 + pads = m.Pads() + for pad in pads: + dr = pad.GetDrillSize() + if not dr.x: + continue + self.pad_drill = min(dr.x, self.pad_drill) + self.pad_drill = min(dr.y, self.pad_drill) + if dr.x == dr.y: + self._drills[dr.x] = self._drills.get(dr.x, 0) + 1 + else: + if dr.x < dr.y: + m = (dr.x, dr.y) + else: + m = (dr.y, dr.x) + self._drills_oval[m] = self._drills_oval.get(m, 0) + 1 + self.slot = min(self.slot, m[0]) + # print('{} @ {}'.format(dr, pad.GetPosition())) + pad_sz = pad.GetSize() + oar_x = pad_sz.x - dr.x + oar_y = pad_sz.y - dr.y + oar_t = min(oar_x, oar_y) + if oar_t: + self.oar_pads = min(self.oar_pads, oar_t) + self._vias_m = list(sorted(self._vias.keys())) + # Via Pad size + self.via_pad_d = ds.m_ViasMinSize + self.via_pad = self._vias_m[0][1] + self.via_pad_min = min(self.via_pad_d, self.via_pad) + # Via Drill size + self.via_drill_d = ds.m_ViasMinDrill if GS.ki5() else ds.m_MinThroughDrill + self.via_drill = self._vias_m[0][0] + self.via_drill_min = min(self.via_drill_d, self.via_drill) + # Via Drill size minus 0.1 mm + self.via_drill_1_d = self.via_drill_d - pcbnew.IU_PER_MM/10 + self.via_drill_1 = self.via_drill - pcbnew.IU_PER_MM/10 + self.via_drill_1_min = self.via_drill_min - pcbnew.IU_PER_MM/10 + # Pad Drill + # No minimum defined + self.pad_drill_min = self.pad_drill if GS.ki5() else ds.m_MinThroughDrill + # Pad Drill size minus 0.1 mm + self.pad_drill_1 = self.pad_drill_1_min = self.pad_drill - pcbnew.IU_PER_MM/10 + # Drill overall + self.drill_d = min(self.via_drill_d, self.pad_drill) + self.drill = min(self.via_drill, self.pad_drill) + self.drill_min = min(self.via_drill_min, self.pad_drill_min) + # Drill overall size minus 0.1 mm + self.drill_1_d = self.drill_d - pcbnew.IU_PER_MM/10 + self.drill_1 = self.drill - pcbnew.IU_PER_MM/10 + self.drill_1_min = self.drill_min - pcbnew.IU_PER_MM/10 + self.top_comp_type = to_smd_tht(self.top_smd, self.top_tht) + self.bot_comp_type = to_smd_tht(self.bot_smd, self.bot_tht) + ########################################################### + # Vias + ########################################################### + self.micro_vias = 'yes' if ds.m_MicroViasAllowed else 'no' + self.blind_vias = 'yes' if ds.m_BlindBuriedViaAllowed else 'no' + self.uvia_pad = ds.m_MicroViasMinSize + self.uvia_drill = ds.m_MicroViasMinDrill + via_sizes = board.GetViasDimensionsList() + self._vias_defined = set() + self._via_sizes_sorted = [] + self.oar_vias_d = INF + for v in sorted(via_sizes, key=lambda x: (x.m_Diameter, x.m_Drill)): + d = v.m_Diameter + h = v.m_Drill + if not d and not h: + continue # KiCad 6 + self.oar_vias_d = min(self.oar_vias_d, d - h) + self._vias_defined.add((h, d)) + self._via_sizes_sorted.append((h, d)) + ########################################################### + # Outer Annular Ring + ########################################################### + self.oar_pads_min = self.oar_pads + self.oar_d = min(self.oar_vias_d, self.oar_pads) + self.oar = min(self.oar_vias, self.oar_pads) + self.oar_min = min(self.oar_d, self.oar) + self.oar_vias_min = min(self.oar_vias_d, self.oar_vias) + ########################################################### + # Eurocircuits class + # https://www.eurocircuits.com/pcb-design-guidelines-classification/ + ########################################################### + # Pattern class + self.pattern_class_min = get_pattern_class(self.track_min, self.clearance, self.oar_min) + self.pattern_class = get_pattern_class(self.track, self.clearance, self.oar) + self.pattern_class_d = get_pattern_class(self.track_d, self.clearance, self.oar_d) + # Drill class + self.drill_class_min = get_drill_class(self.via_drill_min) + self.drill_class = get_drill_class(self.via_drill) + self.drill_class_d = get_drill_class(self.via_drill_d) + ########################################################### + # General stats + ########################################################### + self._track_sizes = board.GetTrackWidthList() + self._tracks_defined = set(self._track_sizes) + + def do_template(self, template_file, output_file): + text = '' + logger.debug("Report template: `{}`".format(template_file)) + with open(template_file, "rt") as f: + for line in f: + done = False + if line[0] == '#' and ':' in line: + context = line[1:].split(':')[0] + logger.debug("Report context: `{}`".format(context)) + try: + # Contexts are members called context_* + line = getattr(self, 'context_'+context)(line[len(context)+2:]) + done = True + except AttributeError: + pass + if not done: + raise KiPlotConfigurationError("Unknown context: `{}`".format(context)) + if not done: + # Just replace using any data member (_* excluded) + line = self.do_replacements(line, self.__dict__) + text += line + logger.debug("Report output: `{}`".format(output_file)) + with open(output_file, "wt") as f: + f.write(text) + + def get_targets(self, out_dir): + return [self._parent.expand_filename(out_dir, self.output)] + + def run(self, fname): + self.pcb_material = GS.global_pcb_material + self.solder_mask_color = GS.global_solder_mask_color + self.silk_screen_color = GS.global_silk_screen_color + self.pcb_finish = GS.global_pcb_finish + self.collect_data(GS.board) + self.do_template(self.template, fname) + + +@output_class +class Report(BaseOutput): # noqa: F821 + """ Design report + Generates a report about the design. + Mainly oriented to be sent to the manufacturer or check PCB details. """ + def __init__(self): + super().__init__() + with document: + self.options = ReportOptions + """ [dict] Options for the `report` output """ diff --git a/kibot/report_templates/report_full.txt b/kibot/report_templates/report_full.txt new file mode 100644 index 00000000..3b5b9f56 --- /dev/null +++ b/kibot/report_templates/report_full.txt @@ -0,0 +1,52 @@ +# PCB + +Board size: ${bb_w_mm}x${bb_h_mm} mm (${bb_w_in}x${bb_h_in} inches) +- This is the size of the rectangle that contains the board +- Thickness: ${thickness_mm} mm (${thickness_mils} mils) +- Material: ${pcb_material} +- Finish: ${pcb_finish} +- Layers: ${layers} +Solder mask: ${solder_mask} +- Color: ${solder_mask_color} +Silk screen: ${silk_screen} +- Color: ${silk_screen_color} + + +# Important sizes + +Clearance: ${clearance_mm} mm (${clearance_mils} mils) +Track width: ${track_mm} mm (${track_mils} mils) +- By design rules: ${track_d_mm} mm (${track_d_mils} mils) +Drill: ${drill_mm} mm (${drill_mils} mils) +- Vias: ${via_drill_mm} mm (${via_drill_mils} mils) [Design: ${via_drill_d_mm} mm (${via_drill_d_mils} mils)] +- Pads: ${pad_drill_mm} mm (${pad_drill_mils} mils) +Via: ${via_pad_mm}/${via_drill_mm} mm (${via_pad_mils}/${via_drill_mils} mils) +- By design rules: ${via_pad_d_mm}/${via_drill_d_mm} mm (${via_pad_d_mils}/${via_drill_d_mils} mils) +- Micro via: ${micro_vias} [${uvia_pad_mm}/${uvia_drill_mm} mm (${uvia_pad_mils}/${uvia_drill_mils} mils)] +- Burried/blind via: ${blind_vias} +Outer Annular Ring: ${oar_mm} mm (${oar_mils} mils) +- By design rules: ${oar_d_mm} mm (${oar_d_mils} mils) + +Eurocircuits class: ${pattern_class}${drill_class} + + +# General stats + +Components count: (SMD/THT) +- Top: ${top_smd}/${top_tht} (${top_comp_type}) +- Bottom: ${bot_smd}/${bot_tht} (${bot_comp_type}) + +Defined tracks: +#defined_tracks:- ${track_mm} mm (${track_mils} mils) + +Used tracks: +#used_tracks:- ${track_mm} mm (${track_mils} mils) (${count}) defined: ${defined} + +Defined vias: +#defined_vias:- ${pad_mm}/${drill_mm} mm (${pad_mils}/${drill_mils} mils) + +Used vias: +#used_vias:- ${pad_mm}/${drill_mm} mm (${pad_mils}/${drill_mils} mils) (Count: ${count}, Aspect: ${aspect} ${producibility_level}) defined: ${defined} + +Holes (excluding vias): +#hole_sizes_no_vias:- ${drill_mm} mm (${drill_mils} mils) (${count}) diff --git a/kibot/report_templates/report_simple.txt b/kibot/report_templates/report_simple.txt new file mode 100644 index 00000000..c5fb308a --- /dev/null +++ b/kibot/report_templates/report_simple.txt @@ -0,0 +1,28 @@ +PCB Specifications: +Size: +- ${bb_w_mm}x${bb_h_mm} mm + +Class: ${pattern_class}${drill_class} +Track width: ≥ ${track_mm} mm +Insulation distance: ≥ ${clearance_mm} mm +Minimum drill size: ≥ ${drill_mm} mm (finished metalized hole: ${drill_1_mm} mm) +Minimum slot width: ≥ ${slot_mm} mm +Ring collar: ≥ ${oar_mm} mm + + +Materials: +- ${pcb_material}, ${thickness_mm} mm +- ${pcb_finish} +- ${layers} layers +- 35 µm copper thickness + +Solder mask: +- ${solder_mask} +- ${solder_mask_color} + +Marking: +- ${silk_screen} screen printing +- Silk: ${silk_screen_color} + +Other markings: +- ROHS / UL / Date - Yes if available diff --git a/setup.py b/setup.py index ae4f29c5..fbffae0f 100644 --- a/setup.py +++ b/setup.py @@ -21,6 +21,7 @@ setup(name='kibot', packages=find_packages(), scripts=['src/kibot', 'src/kiplot'], install_requires=['kiauto', 'pyyaml', 'xlsxwriter', 'colorama', 'requests', 'qrcodegen'], + include_package_data=True, classifiers=['Development Status :: 5 - Production/Stable', 'Environment :: Console', 'Intended Audience :: Developers', diff --git a/tests/reference/5_1_6/light_control-report.txt b/tests/reference/5_1_6/light_control-report.txt new file mode 100644 index 00000000..01f61266 --- /dev/null +++ b/tests/reference/5_1_6/light_control-report.txt @@ -0,0 +1,64 @@ +# PCB + +Board size: 59.69x48.26 mm (2.35x1.9 inches) +- This is the size of the rectangle that contains the board +- Thickness: 1.6 mm (63 mils) +- Material: FR4 +- Finish: ENIG +- Layers: 4 +Solder mask: TOP / BOTTOM +- Color: blue +Silk screen: TOP / BOTTOM +- Color: white + + +# Important sizes + +Clearance: 0.15 mm (6 mils) +Track width: 0.15 mm (6 mils) +- By design rules: 0.13 mm (5 mils) +Drill: 0.25 mm (10 mils) +- Vias: 0.25 mm (10 mils) [Design: 0.2 mm (8 mils)] +- Pads: 0.6 mm (24 mils) +Via: 0.51/0.25 mm (20/10 mils) +- By design rules: 0.46/0.2 mm (18/8 mils) +- Micro via: no [0.2/0.1 mm (8/4 mils)] +- Burried/blind via: no +Outer Annular Ring: 0.25 mm (10 mils) +- By design rules: 0.25 mm (10 mils) + +Eurocircuits class: 6D + + +# General stats + +Components count: (SMD/THT) +- Top: 61/12 (SMD + THT) +- Bottom: 0/0 (NONE) + +Defined tracks: +- 0.15 mm (6 mils) +- 0.25 mm (10 mils) +- 0.3 mm (12 mils) +- 0.64 mm (25 mils) + +Used tracks: +- 0.15 mm (6 mils) (276) defined: yes +- 0.3 mm (12 mils) (11) defined: yes +- 0.64 mm (25 mils) (175) defined: yes + +Defined vias: +- 0.51/0.25 mm (20/10 mils) +- 0.8/0.4 mm (31/16 mils) +- 0.89/0.51 mm (35/20 mils) + +Used vias: +- 0.51/0.25 mm (20/10 mils) (Count: 23, Aspect: 3.1 A) defined: yes +- 0.89/0.51 mm (35/20 mils) (Count: 33, Aspect: 1.8 A) defined: yes + +Holes (excluding vias): +- 0.8 mm (31 mils) (4) +- 0.85 mm (33 mils) (2) +- 0.95 mm (37 mils) (3) +- 1.2 mm (47 mils) (20) +- 3.2 mm (126 mils) (4) diff --git a/tests/reference/5_1_6/light_control-report_simple.txt b/tests/reference/5_1_6/light_control-report_simple.txt new file mode 100644 index 00000000..09c3c177 --- /dev/null +++ b/tests/reference/5_1_6/light_control-report_simple.txt @@ -0,0 +1,28 @@ +PCB Specifications: +Size: +- 59.69x48.26 mm + +Class: 6D +Track width: ≥ 0.15 mm +Insulation distance: ≥ 0.15 mm +Minimum drill size: ≥ 0.25 mm (finished metalized hole: 0.15 mm) +Minimum slot width: ≥ 0.6 mm +Ring collar: ≥ 0.25 mm + + +Materials: +- FR4, 1.6 mm +- ENIG +- 4 layers +- 35 µm copper thickness + +Solder mask: +- TOP / BOTTOM +- blue + +Marking: +- TOP / BOTTOM screen printing +- Silk: white + +Other markings: +- ROHS / UL / Date - Yes if available diff --git a/tests/reference/5_1_7/light_control-report.txt b/tests/reference/5_1_7/light_control-report.txt new file mode 120000 index 00000000..5fbf38c2 --- /dev/null +++ b/tests/reference/5_1_7/light_control-report.txt @@ -0,0 +1 @@ +../5_1_6/light_control-report.txt \ No newline at end of file diff --git a/tests/reference/5_1_7/light_control-report_simple.txt b/tests/reference/5_1_7/light_control-report_simple.txt new file mode 120000 index 00000000..2b2a062d --- /dev/null +++ b/tests/reference/5_1_7/light_control-report_simple.txt @@ -0,0 +1 @@ +../5_1_6/light_control-report_simple.txt \ No newline at end of file diff --git a/tests/reference/6_0_0/light_control-report.txt b/tests/reference/6_0_0/light_control-report.txt new file mode 100644 index 00000000..152a32ff --- /dev/null +++ b/tests/reference/6_0_0/light_control-report.txt @@ -0,0 +1,62 @@ +# PCB + +Board size: 59.69x48.26 mm (2.35x1.9 inches) +- This is the size of the rectangle that contains the board +- Thickness: 1.6 mm (63 mils) +- Material: FR4 +- Finish: ENIG +- Layers: 4 +Solder mask: TOP / BOTTOM +- Color: blue +Silk screen: TOP / BOTTOM +- Color: white + + +# Important sizes + +Clearance: 0.15 mm (6 mils) +Track width: 0.15 mm (6 mils) +- By design rules: 0.13 mm (5 mils) +Drill: 0.25 mm (10 mils) +- Vias: 0.25 mm (10 mils) [Design: 0.2 mm (8 mils)] +- Pads: 0.6 mm (24 mils) +Via: 0.51/0.25 mm (20/10 mils) +- By design rules: 0.46/0.2 mm (18/8 mils) +- Micro via: no [0.2/0.1 mm (8/4 mils)] +- Burried/blind via: no +Outer Annular Ring: 0.25 mm (10 mils) +- By design rules: 0.25 mm (10 mils) + +Eurocircuits class: 6D + + +# General stats + +Components count: (SMD/THT) +- Top: 61/12 (SMD + THT) +- Bottom: 0/0 (NONE) + +Defined tracks: +- 0.15 mm (6 mils) +- 0.3 mm (12 mils) +- 0.64 mm (25 mils) + +Used tracks: +- 0.15 mm (6 mils) (276) defined: yes +- 0.3 mm (12 mils) (11) defined: yes +- 0.64 mm (25 mils) (175) defined: yes + +Defined vias: +- 0.51/0.25 mm (20/10 mils) +- 0.89/0.51 mm (35/20 mils) + +Used vias: +- 0.51/0.25 mm (20/10 mils) (Count: 23, Aspect: 3.1 A) defined: yes +- 0.89/0.51 mm (35/20 mils) (Count: 33, Aspect: 1.8 A) defined: yes + +Holes (excluding vias): +- 0.8 mm (31 mils) (4) +- 0.85 mm (33 mils) (2) +- 0.95 mm (37 mils) (3) +- 1.2 mm (47 mils) (20) +- 3.2 mm (126 mils) (4) diff --git a/tests/reference/6_0_0/light_control-report_simple.txt b/tests/reference/6_0_0/light_control-report_simple.txt new file mode 120000 index 00000000..2b2a062d --- /dev/null +++ b/tests/reference/6_0_0/light_control-report_simple.txt @@ -0,0 +1 @@ +../5_1_6/light_control-report_simple.txt \ No newline at end of file diff --git a/tests/test_plot/test_misc.py b/tests/test_plot/test_misc.py index 41dac940..94f1f46e 100644 --- a/tests/test_plot/test_misc.py +++ b/tests/test_plot/test_misc.py @@ -991,3 +991,14 @@ def test_qr_lib_1(test_dir): if os.path.isfile(bkp): # Not always there, pcbnew_do can remove it os.remove(bkp) + + +def test_report_simple_1(test_dir): + prj = 'light_control' + ctx = context.TestContext(test_dir, 'test_report_simple_1', prj, 'report_simple_1', POS_DIR) + ctx.run() + ctx.expect_out_file(prj+'-report.txt') + ctx.expect_out_file(prj+'-report_simple.txt') + ctx.compare_txt(prj+'-report.txt') + ctx.compare_txt(prj+'-report_simple.txt') + ctx.clean_up() diff --git a/tests/yaml_samples/report_simple_1.kibot.yaml b/tests/yaml_samples/report_simple_1.kibot.yaml new file mode 100644 index 00000000..eb45736c --- /dev/null +++ b/tests/yaml_samples/report_simple_1.kibot.yaml @@ -0,0 +1,20 @@ +# Example KiBot config file +kibot: + version: 1 + +global: + solder_mask_color: blue + pcb_finish: ENIG + +outputs: + - name: 'report_full' + comment: "Full design report" + type: report + + - name: 'report_simple' + comment: "Simple design report" + type: report + output_id: _simple + options: + template: simple +