diff --git a/CHANGELOG.md b/CHANGELOG.md index b87b838f..f5a5c7ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added - Now variants are separated entities. - - Only the internal BoM currently supports it. + - Only the internal BoM and Schematic print currently supports it. - In the future IBoM will also support it, contact me if you think this is high priority. - New filters entities. They implement all the functionality in KiBoM and IBoM. @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Marking components as "Do Not Fit" - Marking components as "Do Not Change" - The internal BoM format supports KiBoM and IBoM style variants +- Schematic print to PDF supports variants. Not fitted components are crossed. ## [0.6.2] - 2020-08-25 ### Changed diff --git a/Makefile b/Makefile index 19d40059..91404387 100644 --- a/Makefile +++ b/Makefile @@ -106,6 +106,9 @@ gen_ref: cp -a $(REFILL).refill $(REFILL) src/kibot -c tests/yaml_samples/pdf_zone-refill.kibot.yaml -b tests/board_samples/zone-refill.kicad_pcb -d $(REFDIR) src/kibot -c tests/yaml_samples/print_pcb_zone-refill.kibot.yaml -b tests/board_samples/zone-refill.kicad_pcb -d $(REFDIR) + src/kibot -c tests/yaml_samples/print_pdf_no_inductors_1.kibot.yaml -e tests/board_samples/test_v5.sch -d $(REFDIR) + mv "$(REFDIR)no_inductor/test_v5-schematic_(no_L).pdf" $(REFDIR) + rmdir $(REFDIR)no_inductor/ cp -a $(REFILL).ok $(REFILL) doc: diff --git a/README.md b/README.md index 6740c4ea..638cdca4 100644 --- a/README.md +++ b/README.md @@ -405,7 +405,7 @@ Next time you need this list just use an alias, like this: - `number`: [number=1] Number of boards to build (components multiplier). - `output`: [string='%f-%i%v.%x'] filename for the output (%i=bom). Affected by global options. - `use_alt`: [boolean=false] Print grouped references in the alternate compressed style eg: R1-R7,R18. - - `variant`: [string=''] Board variant(s), used to determine which components + - `variant`: [string=''] Board variant, used to determine which components are output to the BoM.. - `xlsx`: [dict] Options for the XLSX format. * Valid keys: @@ -522,7 +522,7 @@ Next time you need this list just use an alias, like this: - `exclude_edge_layer`: [boolean=true] do not include the PCB edge layer. - `exclude_pads_from_silkscreen`: [boolean=false] do not plot the component pads in the silk screen. - `force_plot_invisible_refs_vals`: [boolean=false] include references and values even when they are marked as invisible. - - `gerber_job_file`: [string='%f-%i.%x'] name for the gerber job file (%i='job', %x='gbrjob'). + - `gerber_job_file`: [string='%f-%i%v.%x'] name for the gerber job file (%i='job', %x='gbrjob'). Affected by global options. - `gerber_precision`: [number=4.6] this the gerber coordinate format, can be 4.5 or 4.6. - `line_width`: [number=0.1] [0.02,2] line_width for objects without width [mm]. - `output`: [string='%f-%i%v.%x'] output file name, the default KiCad name if empty. Affected by global options. @@ -812,7 +812,10 @@ Next time you need this list just use an alias, like this: - `name`: [string=''] Used to identify this particular output definition. - `options`: [dict] Options for the `pdf_sch_print` output. * Valid keys: + - `dnf_filter`: [string|list(string)=''] Name of the filter to mark components as not fitted. + A short-cut to use for simple cases where a variant is an overkill. - `output`: [string='%f-%i%v.%x'] filename for the output PDF (%i=schematic %x=pdf). Affected by global options. + - `variant`: [string=''] Board variant(s), used to determine which components are crossed.. * Pick & place * Type: `position` @@ -866,6 +869,21 @@ Next time you need this list just use an alias, like this: - `width_adjust`: [number=0] this width factor is intended to compensate PS printers/plotters that do not strictly obey line width settings. Only used to plot pads and tracks. +* Schematic with variant generator + * Type: `sch_variant` + * Description: Creates a copy of the schematic with all the filters and variants applied. + This copy isn't intended for development. + Is just a tweaked version of the original where you can look at the results. + * Valid keys: + - `comment`: [string=''] A comment for documentation purposes. + - `dir`: [string='.'] Output directory for the generated files. + - `name`: [string=''] Used to identify this particular output definition. + - `options`: [dict] Options for the `sch_variant` output. + * Valid keys: + - `dnf_filter`: [string|list(string)=''] Name of the filter to mark components as not fitted. + A short-cut to use for simple cases where a variant is an overkill. + - `variant`: [string=''] Board variant(s) to apply. + * STEP (ISO 10303-21 Clear Text Encoding of the Exchange Structure) * Type: `step` * Description: Exports the PCB as a 3D model. diff --git a/docs/samples/generic_plot.kibot.yaml b/docs/samples/generic_plot.kibot.yaml index 936aee86..e5c745a3 100644 --- a/docs/samples/generic_plot.kibot.yaml +++ b/docs/samples/generic_plot.kibot.yaml @@ -121,7 +121,7 @@ outputs: output: '%f-%i%v.%x' # [boolean=false] Print grouped references in the alternate compressed style eg: R1-R7,R18 use_alt: false - # [string=''] Board variant(s), used to determine which components + # [string=''] Board variant, used to determine which components # are output to the BoM. variant: '' # [dict] Options for the XLSX format @@ -258,8 +258,8 @@ outputs: exclude_pads_from_silkscreen: false # [boolean=false] include references and values even when they are marked as invisible force_plot_invisible_refs_vals: false - # [string='%f-%i.%x'] name for the gerber job file (%i='job', %x='gbrjob') - gerber_job_file: '%f-%i.%x' + # [string='%f-%i%v.%x'] name for the gerber job file (%i='job', %x='gbrjob'). Affected by global options + gerber_job_file: '%f-%i%v.%x' # [number=4.6] this the gerber coordinate format, can be 4.5 or 4.6 gerber_precision: 4.6 # [number=0.1] [0.02,2] line_width for objects without width [mm] @@ -619,8 +619,13 @@ outputs: type: 'pdf_sch_print' dir: 'Example/pdf_sch_print_dir' options: + # [string|list(string)=''] Name of the filter to mark components as not fitted. + # A short-cut to use for simple cases where a variant is an overkill + dnf_filter: '' # [string='%f-%i%v.%x'] filename for the output PDF (%i=schematic %x=pdf). Affected by global options output: '%f-%i%v.%x' + # [string=''] Board variant(s), used to determine which components are crossed. + variant: '' # Pick & place: # This output is what you get from the 'File/Fabrication output/Footprint poistion (.pos) file' menu in pcbnew. @@ -686,6 +691,20 @@ outputs: width_adjust: 0 layers: all + # 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. + - name: 'sch_variant_example' + comment: 'Creates a copy of the schematic with all the filters and variants applied.' + type: 'sch_variant' + dir: 'Example/sch_variant_dir' + options: + # [string|list(string)=''] Name of the filter to mark components as not fitted. + # A short-cut to use for simple cases where a variant is an overkill + dnf_filter: '' + # [string=''] Board variant(s) to apply + variant: '' + # STEP (ISO 10303-21 Clear Text Encoding of the Exchange Structure): # This is the most common 3D format for exchange purposes. # This output is what you get from the 'File/Export/STEP' menu in pcbnew. diff --git a/kibot/__main__.py b/kibot/__main__.py index 7ff7b4c3..75728672 100644 --- a/kibot/__main__.py +++ b/kibot/__main__.py @@ -94,6 +94,7 @@ has_macro = [ 'out_pdf_sch_print', 'out_position', 'out_ps', + 'out_sch_variant', 'out_step', 'out_svg', 'out_svg_sch_print', diff --git a/kibot/bom/bom.py b/kibot/bom/bom.py index 4f3f76c5..d62bb329 100644 --- a/kibot/bom/bom.py +++ b/kibot/bom/bom.py @@ -363,10 +363,15 @@ def group_components(cfg, components): def do_bom(file_name, ext, comps, cfg): # Apply all the filters - for c in comps: - c.in_bom = cfg.exclude_filter.filter(c) - c.fitted = cfg.dnf_filter.filter(c) - c.fixed = cfg.dnc_filter.filter(c) + if cfg.exclude_filter: + for c in comps: + c.in_bom = cfg.exclude_filter.filter(c) + if cfg.dnf_filter: + for c in comps: + c.fitted = cfg.dnf_filter.filter(c) + if cfg.dnc_filter: + for c in comps: + c.fixed = cfg.dnc_filter.filter(c) # Apply the variant cfg.variant.filter(comps) # Group components according to group_fields diff --git a/kibot/config_reader.py b/kibot/config_reader.py index 83c3cbef..3726dcd0 100644 --- a/kibot/config_reader.py +++ b/kibot/config_reader.py @@ -109,12 +109,10 @@ class CfgYamlReader(object): logger.debug("Parsing "+kind+" "+name_type) o_var = reg_class.get_class_for(otype)() o_var.set_tree(o_tree) - # No errors yet -# try: -# o_var.config() -# except KiPlotConfigurationError as e: -# config_error("In section `"+name_type+"`: "+str(e)) - o_var.config() + try: + o_var.config() + except KiPlotConfigurationError as e: + config_error("In section `"+name_type+"`: "+str(e)) return o_var def _parse_variants(self, v): diff --git a/kibot/fil_base.py b/kibot/fil_base.py index 39ccdc6c..b9bc53b1 100644 --- a/kibot/fil_base.py +++ b/kibot/fil_base.py @@ -4,8 +4,23 @@ # License: GPL-3.0 # Project: KiBot (formerly KiPlot) from .registrable import RegFilter, Registrable, RegOutput +from .misc import IFILL_MECHANICAL from .error import KiPlotConfigurationError +from .bom.columnlist import ColumnList from .macros import macros, document # noqa: F401 +from . import log + +logger = log.get_logger(__name__) +DEFAULT_EXCLUDE = [{'column': ColumnList.COL_REFERENCE, 'regex': '^TP[0-9]*'}, + {'column': ColumnList.COL_REFERENCE, 'regex': '^FID'}, + {'column': ColumnList.COL_PART, 'regex': 'mount.*hole'}, + {'column': ColumnList.COL_PART, 'regex': 'solder.*bridge'}, + {'column': ColumnList.COL_PART, 'regex': 'solder.*jump'}, + {'column': ColumnList.COL_PART, 'regex': 'test.*point'}, + {'column': ColumnList.COL_FP, 'regex': 'test.*point'}, + {'column': ColumnList.COL_FP, 'regex': 'mount.*hole'}, + {'column': ColumnList.COL_FP, 'regex': 'fiducial'}, + ] class DummyFilter(Registrable): @@ -54,6 +69,7 @@ class BaseFilter(RegFilter): def __init__(self): super().__init__() self._unkown_is_error = True + self._internal = False with document: self.name = '' """ Used to identify this particular filter definition """ @@ -62,8 +78,57 @@ class BaseFilter(RegFilter): self.comment = '' """ A comment for documentation purposes """ + def config(self): + super().config() + if self.name[0] == '_' and not self._internal: + raise KiPlotConfigurationError('Filter names starting with `_` are reserved ({})'.format(self.name)) + @staticmethod - def solve_filter(names, def_key, def_real, creator, target_name): + def _create_mechanical(name): + o_tree = {'name': name} + o_tree['type'] = 'generic' + o_tree['comment'] = 'Internal default mechanical filter' + o_tree['exclude_all_hash_ref'] = True + o_tree['exclude_any'] = DEFAULT_EXCLUDE + logger.debug('Creating internal filter: '+str(o_tree)) + return o_tree + + @staticmethod + def _create_kibom_dnx(name): + type = name[7:10] + if len(name) > 11: + subtype = name[11:] + else: + subtype = 'config' + o_tree = {'name': name} + o_tree['type'] = 'generic' + o_tree['comment'] = 'Internal KiBoM '+type.upper()+' filter ('+subtype+')' + o_tree['config_field'] = subtype + o_tree['exclude_value'] = True + o_tree['exclude_config'] = True + o_tree['keys'] = type+'_list' + if type[-1] == 'c': + o_tree['invert'] = True + logger.debug('Creating internal filter: '+str(o_tree)) + return o_tree + + @staticmethod + def _create_internal_filter(name): + if name == IFILL_MECHANICAL: + tree = BaseFilter._create_mechanical(name) + elif name.startswith('_kibom_dn') and len(name) >= 10: + tree = BaseFilter._create_kibom_dnx(name) + else: + return None + filter = RegFilter.get_class_for(tree['type'])() + filter._internal = True + filter.set_tree(tree) + filter.config() + RegOutput.add_filter(filter) + return filter + + @staticmethod + def solve_filter(names, target_name, default=None): """ Name can be: - A class, meaning we have to use a default. - A string, the name of a filter. @@ -72,42 +137,39 @@ class BaseFilter(RegFilter): If def_real is not None we pass this name to creator. """ if isinstance(names, type): # Nothing specified, use the default - names = [def_key] + if default is None: + return None + names = [default] elif isinstance(names, str): # User provided, but only one, make a list + if names == '_none': + return None names = [names] # Here we should have a list of strings filters = [] for name in names: - if name and name[0] == '!': + if not name: + continue + if name[0] == '!': invert = True name = name[1:] + # '!' => always False + if not name: + filters.append(NotFilter(DummyFilter())) + continue else: invert = False - filter = None - if name == def_key: - # Matched the default name, translate it to the real name - if def_real: - name = def_real - # Is already defined? - if RegOutput.is_filter(name): - filter = RegOutput.get_filter(name) - else: # Nope, create it - tree = creator(name) - filter = RegFilter.get_class_for(tree['type'])() - filter.set_tree(tree) - filter.config() - RegOutput.add_filter(filter) - elif name: - # A filter that is supposed to exist - if not RegOutput.is_filter(name): - raise KiPlotConfigurationError("Unknown filter `{}` used for `{}`".format(name, target_name)) + # Is already defined? + if RegOutput.is_filter(name): filter = RegOutput.get_filter(name) - if filter: - if invert: - filters.append(NotFilter(filter)) - else: - filters.append(filter) + else: # Nope, can be created? + filter = BaseFilter._create_internal_filter(name) + if filter is None: + raise KiPlotConfigurationError("Unknown filter `{}` used for `{}`".format(name, target_name)) + if invert: + filters.append(NotFilter(filter)) + else: + filters.append(filter) # Finished collecting filters if not filters: return DummyFilter() diff --git a/kibot/fil_generic.py b/kibot/fil_generic.py index 9d6a2fef..b6feb68a 100644 --- a/kibot/fil_generic.py +++ b/kibot/fil_generic.py @@ -64,6 +64,8 @@ class Generic(BaseFilter): # noqa: F821 self.exclude_refs = Optionable """ [list(string)] List of references to be excluded. Use R* for all references with R prefix """ + self.exclude_all_hash_ref = False + """ Exclude all components with a reference starting with # """ # Skip virtual components if needed # TODO: We currently lack this information # if config.blacklist_virtual and m.attr == 'Virtual': @@ -146,6 +148,9 @@ class Generic(BaseFilter): # noqa: F821 # Exclude components with empty 'Value' if self.exclude_empty_val and (value == '' or value == '~'): return exclude + # Exclude all ref == #* + if self.exclude_all_hash_ref and comp.ref[0] == '#': + return exclude # List of references to be excluded if self.exclude_refs and (comp.ref in self.exclude_refs or comp.ref_prefix+'*' in self.exclude_refs): return exclude diff --git a/kibot/kicad/v5_sch.py b/kibot/kicad/v5_sch.py index 47bf6b46..e877145c 100644 --- a/kibot/kicad/v5_sch.py +++ b/kibot/kicad/v5_sch.py @@ -140,6 +140,19 @@ class LibComponentField(object): field.name = ['Reference', 'Value', 'Footprint', 'Datasheet'][field.number] return field + def write(self, f): + s = 'F'+str(self.number) + s += ' "{}" {} {} {} '.format(self.value, self.x, self.y, self.size) + s += 'H' if self.horizontal else 'V' + s += ' ' + s += 'V' if self.visible else 'I' + s += ' '+self.hjustify+' '+self.vjustify + s += 'I' if self.italic else 'N' + s += 'B' if self.bold else 'N' + if self.number > 3: + s += ' "'+self.name+'"' + f.write(s+'\n') + class DrawPoligon(object): pol_re = re.compile(r'P\s+(\d+)\s+' # 0 Number of points @@ -168,9 +181,30 @@ class DrawPoligon(object): coords = _split_space(g[4]) if len(coords) != 2*pol.points: logger.warning('Expected {} coordinates and got {} in poligon'.format(2*pol.points, len(coords))) - pol.coords = coords + pol.points = int(len(coords)/2) + pol.coords = [int(c) for c in coords] return pol + def write(self, f): + f.write('P {} {} {} {}'.format(self.points, self.sub_part, self.convert, self.thickness)) + for p in self.coords: + f.write(' '+str(p)) + f.write(' '+self.fill+'\n') + + def get_rect(self): + if not self.points: + return 0, 0, 0, 0, False + xm = xM = self.coords[0] + ym = yM = self.coords[1] + for i in range(1, self.points): + x = self.coords[i*2] + y = self.coords[i*2+1] + xm = min(x, xm) + xM = max(x, xM) + ym = min(y, ym) + yM = max(y, yM) + return xm, ym, xM, yM, True + class DrawRectangle(object): rec_re = re.compile(r'S\s+' @@ -204,6 +238,17 @@ class DrawRectangle(object): rec.fill = g[7] return rec + def write(self, f): + f.write('S {} {} {} {}'.format(self.start_x, self.start_y, self.end_x, self.end_y)) + f.write(' {} {} {} {}\n'.format(self.sub_part, self.convert, self.thickness, self.fill)) + + def get_rect(self): + xm = min(self.start_x, self.end_x) + ym = min(self.start_y, self.end_y) + xM = max(self.start_x, self.end_x) + yM = max(self.start_y, self.end_y) + return xm, ym, xM, yM, True + class DrawCircle(object): cir_re = re.compile(r'C\s+' @@ -235,6 +280,17 @@ class DrawCircle(object): cir.fill = g[6] return cir + def write(self, f): + f.write('C {} {} {}'.format(self.pos_x, self.pos_y, self.radius)) + f.write(' {} {} {} {}\n'.format(self.sub_part, self.convert, self.thickness, self.fill)) + + def get_rect(self): + xm = self.pos_x-self.radius + ym = self.pos_y-self.radius + xM = self.pos_x+self.radius + yM = self.pos_y+self.radius + return xm, ym, xM, yM, True + class DrawArc(object): arc_re = re.compile(r'A\s+' @@ -278,6 +334,19 @@ class DrawArc(object): arc.end_y = int(g[12]) return arc + def write(self, f): + f.write('A {} {} {}'.format(self.pos_x, self.pos_y, self.radius)) + f.write(' {} {}'.format(self.start, self.end)) + f.write(' {} {} {} {}'.format(self.sub_part, self.convert, self.thickness, self.fill)) + f.write(' {} {} {} {}\n'.format(self.start_x, self.start_y, self.end_x, self.end_y)) + + def get_rect(self): + xm = self.pos_x-self.radius + ym = self.pos_y-self.radius + xM = self.pos_x+self.radius + yM = self.pos_y+self.radius + return xm, ym, xM, yM, True + class DrawText(object): txt_re = re.compile(r'T\s+' @@ -305,7 +374,7 @@ class DrawText(object): return None txt = DrawText() g = m.groups() - txt.vertical = g[0] != '0' + txt.orientation = int(g[0]) txt.pos_x = int(g[1]) txt.pos_y = int(g[2]) txt.size = int(g[3]) @@ -319,6 +388,14 @@ class DrawText(object): txt.vjustify = g[11] return txt + def write(self, f): + f.write('T {} {} {} {}'.format(self.orientation, self.pos_x, self.pos_y, self.size)) + f.write(' {} {} {} "{}"'.format(self.type, self.sub_part, self.convert, self.text)) + f.write(' {} {} {} {}\n'.format(['Normal', 'Italic'][self.italic], int(self.bold), self.hjustify, self.vjustify)) + + def get_rect(self): + return 0, 0, 0, 0, False + class Pin(object): pin_re = re.compile(r'X\s+' @@ -360,6 +437,25 @@ class Pin(object): pin.gtype = g[11] return pin + def write(self, f): + f.write('X {} {} {} {}'.format(self.name, self.number, self.pos_x, self.pos_y)) + f.write(' {} {} {} {}'.format(self.len, self.dir, self.size_name, self.size_num)) + f.write(' {} {} {}'.format(self.sub_part, self.convert, self.type)) + if self.gtype: + f.write(' '+self.gtype) + f.write('\n') + + def get_rect(self): + if self.dir == 'U': + return self.pos_x, self.pos_y, self.pos_x, self.pos_y+self.len, True + if self.dir == 'D': + return self.pos_x, self.pos_y-self.len, self.pos_x, self.pos_y, True + if self.dir == 'R': + return self.pos_x, self.pos_y, self.pos_x+self.len, self.pos_y, True + if self.dir == 'L': + return self.pos_x-self.len, self.pos_y, self.pos_x, self.pos_y, True + return 0, 0, 0, 0, False + class LibComponent(object): def_re = re.compile(r'DEF\s+' @@ -393,7 +489,7 @@ class LibComponent(object): self.vname = True else: self.vname = False - if GS.debug_level > 1: + if GS.debug_level > 2: logger.debug('- Loading component {} from {}'.format(self.name, lib_name)) else: logger.warning('Failed to load component definition: `{}`'.format(line)) @@ -438,6 +534,62 @@ class LibComponent(object): line = f.get_line() line = f.get_line() + def write(self, f, id, cross=False): + """ cross is used to cross the component (DNF) """ + id = id.replace(':', '_') + if self.vname: + id = '~'+id + f.write('#\n# '+id+'\n#\n') + f.write('DEF {} {} {} {} {} {} {} {} {}\n'. + format(id, self.ref_prefix, self.unused, self.text_offset, ['N', 'Y'][self.draw_pinnumber], + ['N', 'Y'][self.draw_pinname], self.unit_count, ['F', 'L'][self.units_locked], + ['N', 'P'][self.is_power])) + for field in self.fields: + field.write(f) + f.write('$FPLIST\n') + for fp in self.fp_list: + f.write(' '+fp+'\n') + f.write('$ENDFPLIST\n') + if self.alias: + f.write('ALIAS '+' '.join(self.alias)+'\n') + f.write('DRAW\n') + for dr in self.draw: + dr.write(f) + if cross: + # Generated the crossed stuff + # logger.debug('Computing size for {}:'.format(id)) + for unit in range(self.unit_count): + xmt = ymt = 1e6 + xMt = yMt = -1e6 + ok_t = False + # logger.debug("Unit "+str(unit+1)) + for dr in self.draw: + if dr.sub_part != unit + 1 and dr.sub_part != 0: + continue + xm, ym, xM, yM, ok = dr.get_rect() + # logger.debug([dr, xm, ym, xM, yM, ok]) + if ok: + ok_t = True + xmt = min(xm, xmt) + ymt = min(ym, ymt) + xMt = max(xM, xMt) + yMt = max(yM, yMt) + if ok_t: + # Cross this component using 2 lines + o = DrawPoligon() + o.points = 2 + o.sub_part = unit+1 + o.convert = 0 + o.thickness = 30 + o.fill = 'N' + o.coords = [xmt, ymt, xMt, yMt] + o.write(f) + o.coords = [xmt, yMt, xMt, ymt] + o.write(f) + f.write('ENDDRAW\n') + f.write('ENDDEF\n') + + # def __repr__(self): # s = 'Component('+self.name # if self.desc: @@ -453,7 +605,20 @@ class SymLib(object): self.comps = OrderedDict() self.alias = {} - def load(self, file): + @staticmethod + def _check_add(o, id, lib, needed): + name = lib+':'+id + if name in needed: + needed[name] = o + return True + else: + name = 'None:'+id + if name in needed: + needed[name] = o + return True + return False + + def load(self, file, lib_alias, needed): """ Populates the class, file must exist """ logger.debug('Loading library `{}`'.format(file)) with open(file, 'rt') as fh: @@ -466,10 +631,13 @@ class SymLib(object): if line.startswith('DEF'): o = LibComponent(line, f, file) if o.name: - self.comps[o.name] = o + # Only add components we need + if self._check_add(o, o.name, lib_alias, needed): + self.comps[o.name] = o if o.alias: for a in o.alias: - self.alias[a] = o + if self._check_add(o, a, lib_alias, needed): + self.alias[a] = o else: raise SchLibError('Unknown library entry', line, f) line = f.get_line() @@ -562,6 +730,14 @@ class SchematicField(object): field.name = ['Reference', 'Value', 'Footprint', 'Datasheet'][field.number] return field + def write(self, f): + f.write('F {} "{}" {}'.format(self.number, self.value, ['V', 'H'][self.horizontal])) + f.write(' {} {} {} {}'.format(self.x, self.y, self.size, self.flags)) + f.write(' {} {}{}{}'.format(self.hjustify, self.vjustify, ['N', 'I'][self.italic], ['N', 'B'][self.bold])) + if self.number > 3: + f.write(' "{}"'.format(self.name)) + f.write('\n') + class SchematicAltRef(): def __init__(self): @@ -589,6 +765,16 @@ class SchematicAltRef(): logger.warning('Alternative Reference without reference `{}`'.format(line)) return ar + def write(self, f): + f.write('AR') + if self.path: + f.write(' Path="{}"'.format(self.path)) + if self.ref: + f.write(' Ref="{}"'.format(self.ref)) + if self.part: + f.write(' Part="{}"'.format(self.part)) + f.write('\n') + class SchematicComponent(object): """ Class for a component in the schematic. @@ -613,6 +799,10 @@ class SchematicComponent(object): self.footprint = '' self.datasheet = '' self.desc = '' + # Will be computed + self.fitted = True + self.in_bom = True + self.fixed = False def get_field_value(self, field): field = field.lower() @@ -800,6 +990,24 @@ class SchematicComponent(object): comp._validate() return comp + def write(self, f): + # Fake lib to reflect fitted status + lib = 'y' if self.fitted else 'n' + # Fake name using cache style + name = '{}:{}_{}'.format(lib, self.lib, self.name) + f.write('$Comp\n') + f.write('L {} {}\n'.format(name, self.f_ref)) + f.write('U {} {} {}\n'.format(self.unit, self.unit2, self.id)) + f.write('P {} {}\n'.format(self.x, self.y)) + for ar in self.ar: + ar.write(f) + for field in self.fields: + if field.number >= 0: + field.write(f) + f.write('\t{} {} {}\n'.format(self.unit, self.x, self.y)) + f.write('\t{} {} {} {}\n'.format(self.matrix[0], self.matrix[1], self.matrix[2], self.matrix[3])) + f.write('$EndComp\n') + class SchematicConnection(object): conn_re = re.compile(r'\s*~\s+(-?\d+)\s+(-?\d+)') @@ -818,29 +1026,58 @@ class SchematicConnection(object): c.y = int(m.group(2)) return c + def write(self, f): + f.write('{} ~ {} {}\n'.format(['NoConn', 'Connection'][self.connect], self.x, self.y)) + class SchematicText(object): label_re = re.compile(r'Text\s+(Notes|HLabel|GLabel|Label)\s+(-?\d+)\s+(-?\d+)\s+(\d)\s+(\d+)\s+(\S+)') + TYPES = ['Notes', 'HLabel', 'GLabel', 'Label'] def __init__(self): super().__init__() @staticmethod def load(f, line): - m = SchematicText.label_re.match(line) - if not m: - raise SchFileError('Malformed text', line, f) + gs = _split_space(line) + c = len(gs) + if c < 6 or gs[0] != 'Text' or gs[1] not in SchematicText.TYPES: + raise SchFileError('Malformed `Text`', line, f) text = SchematicText() - gs = m.groups() - text.type = gs[0] - text.x = int(gs[1]) - text.y = int(gs[2]) - text.orient = int(gs[3]) - text.size = int(gs[4]) - text.shape = gs[5] + text.type = gs[1] + try: + text.x = int(gs[2]) + text.y = int(gs[3]) + text.orient = int(gs[4]) + text.size = int(gs[5]) + offset = 6 + text.shape = None + if gs[1][0] in 'GH': + if c < 7: + raise SchFileError('Missing `Text` shape', line, f) + text.shape = gs[6] + offset += 1 + # New versions adds Italics and Bold, in a different way of course + text.italic = False + if c > offset: + text.italic = gs[offset] == 'Italic' + offset += 1 + text.thickness = 0 + if c > offset: + text.thickness = int(gs[offset]) + offset += 1 + except ValueError: + raise SchFileError('Not a number in `Text`', line, f) text.text = f.get_line() return text + def write(self, f): + f.write('Text {} {} {} {} {}'.format(self.type, self.x, self.y, self.orient, self.size)) + if self.type[0] in 'GH': + f.write(' '+self.shape) + f.write(' {} {}\n'.format(['~', 'Italic'][self.italic], self.thickness)) + f.write(self.text+'\n') + class SchematicWire(object): WIRE = 0 @@ -850,6 +1087,7 @@ class SchematicWire(object): ENTRY_WIRE = 3 ENTRY_BUS = 4 ENTRIES = {'Wire': ENTRY_WIRE, 'Bus': ENTRY_BUS} + NAMES = ['Wire Wire Line', 'Wire Bus Line', 'Wire Notes Line', 'Entry Wire Line', 'Entry Bus Bus'] def __init__(self): super().__init__() @@ -885,6 +1123,10 @@ class SchematicWire(object): wire.ey = int(res[3]) return wire + def write(self, f): + f.write(SchematicWire.NAMES[self.type]) + f.write('\n\t{} {} {} {}\n'.format(self.x, self.y, self.ex, self.ey)) + class SchematicBitmap(object): def __init__(self): @@ -929,6 +1171,18 @@ class SchematicBitmap(object): raise SchFileError('Missing end of bitmap', line, f) return bmp + def write(self, f): + f.write('$Bitmap\n') + f.write('Pos {} {}\n'.format(self.x, self.y)) + f.write('Scale {}\n'.format(self.scale)) + f.write('Data') + for c, b in enumerate(self.data): + if (c % 32) == 0: + f.write('\n') + f.write('%02X ' % b) + f.write('\nEndData\n') + f.write('$EndBitmap\n') + class SchematicPort(object): port_re = re.compile(r'(\d+)\s+"(.*?)"\s+([IOBTU])\s+([RLTB])\s+(-?\d+)\s+(-?\d+)\s+(\d+)$') @@ -952,6 +1206,9 @@ class SchematicPort(object): port.size = int(res[6]) return port + def write(s, f): + f.write('F{} "{}" {} {} {} {} {}\n'.format(s.number, s.name, s.form, s.side, s.x, s.y, s.size)) + class SchematicSheet(object): name_re = re.compile(r'"(.*?)"\s+(\d+)$') @@ -1019,6 +1276,18 @@ class SchematicSheet(object): raise SchFileError('Missing sub-sheet file name', sch.name, f) return sch + def write(self, f): + # Fake file name + file = self.file.replace('/', '_') + f.write('$Sheet\n') + f.write('S {} {} {} {}\n'.format(self.x, self.y, self.w, self.h)) + f.write('U {}\n'.format(self.id)) + f.write('F0 "{}" {}\n'.format(self.name, self.name_size)) + f.write('F1 "{}" {}\n'.format(file, self.file_size)) + for label in self.labels: + label.write(f) + f.write('$EndSheet\n') + class Schematic(object): def __init__(self): @@ -1035,8 +1304,8 @@ class Schematic(object): self.page_width = m.group(2) self.page_height = m.group(3) self.sheet = 1 - self.sheets = 1 - self.title_block = {} + self.nsheets = 1 + self.title_block = OrderedDict() while True: line = f.get_line() if line.startswith('$EndDescr'): @@ -1049,7 +1318,7 @@ class Schematic(object): if len(res) != 2: raise SchFileError('Wrong sheet number', line, f) self.sheet = int(res[0]) - self.sheets = int(res[1]) + self.nsheets = int(res[1]) else: m = re.match(r'(\S+)\s+"(.*)"', line) if not m: @@ -1183,13 +1452,17 @@ class Schematic(object): logger.debug('Using `{}` for library alias `{}`'.format(alias.uri, k)) else: logger.warning('Missing library `{}`'.format(k)) + # Create a hash with all the used components + self.comps_data = {'{}:{}'.format(c.lib, c.name): None for c in self.get_components(exclude_power=False)} + if GS.debug_level > 1: + logger.debug("Components before loading: "+str(self.comps_data)) # Load the libraries and descriptions for k, v in self.libs.items(): if v: # Load library if os.path.isfile(v): o = SymLib() - o.load(v) + o.load(v, k, self.comps_data) else: logger.warning('Missing library `{}` ({})'.format(v, k)) o = None @@ -1206,6 +1479,8 @@ class Schematic(object): # Mark as None if we don't know the file self.lib_comps[k] = None self.dcms[k] = None + if GS.debug_level > 1: + logger.debug("Components after loading: "+str(self.comps_data)) # Join the descriptions with the components for k in self.libs.keys(): lib = self.lib_comps[k] @@ -1213,7 +1488,59 @@ class Schematic(object): if lib and dcm: for name, comp in lib.comps.items(): comp.dcm = dcm.comps.get(name) - if not comp.dcm: + if not comp.dcm and k+':'+name in self.comps_data: logger.warning('Missing doc-lib entry for {}:{}'.format(k, name)) # Transfer the descriptions to the instances of the components self.walk_components(self.apply_dcm, self) + + def gen_lib(self, name, cross=False): + """ Dumps all the used components to one library. + This is like the KiCad cache. """ + with open(name, 'wt') as f: + f.write('EESchema-LIBRARY Version 2.4\n') + f.write('#encoding utf-8\n') + for k, v in self.comps_data.items(): + if v: + v.write(f, k, cross=cross) + else: + logger.warning('Missing component `{}`'.format(k)) + f.write('#\n#End Library\n') + + def save(self, fname, dest_dir): + fname = os.path.join(dest_dir, fname) + with open(fname, 'wt') as f: + f.write('EESchema Schematic File Version {}\n'.format(self.version)) + f.write('EELAYER {} {}\n'.format(self.eelayer_n, self.eelayer_m)) + f.write('EELAYER END\n') + f.write('$Descr {} {} {}\n'.format(self.page_type, self.page_width, self.page_height)) + f.write('encoding utf-8\n') + f.write('Sheet {} {}\n'.format(self.sheet, self.nsheets)) + for k, v in self.title_block.items(): + f.write('{} "{}"\n'.format(k, v)) + f.write('$EndDescr\n') + for e in self.all: + e.write(f) + f.write('$EndSCHEMATC\n') + # Save sub-sheets + for c, sch in enumerate(self.sheets): + # Fake file name + file = sch.file.replace('/', '_') + self.sub_sheets[c].save(file, dest_dir) + + def save_variant(self, dest_dir): + # Currently imposible + # if not os.path.exists(dest_dir): + # os.makedirs(dest_dir) + lib_yes = os.path.join(dest_dir, 'y.lib') + lib_no = os.path.join(dest_dir, 'n.lib') + self.gen_lib(lib_yes) + self.gen_lib(lib_no, cross=True) + fname = os.path.basename(self.fname) + self.save(fname, dest_dir) + # SymLibTable to use y/n + with open(os.path.join(dest_dir, 'sym-lib-table'), 'wt') as f: + f.write('(sym_lib_table\n') + f.write(' (lib (name y)(type Legacy)(uri ${KIPRJMOD}/y.lib)(options "")(descr ""))\n') + f.write(' (lib (name n)(type Legacy)(uri ${KIPRJMOD}/n.lib)(options "")(descr ""))\n') + f.write(')\n') + return fname diff --git a/kibot/misc.py b/kibot/misc.py index 8947524e..571770a2 100644 --- a/kibot/misc.py +++ b/kibot/misc.py @@ -46,6 +46,9 @@ URL_PCBDRAW = 'https://github.com/INTI-CMNB/pcbdraw' EXAMPLE_CFG = 'example.kibot.yaml' AUTO_SCALE = 0 +# Internal filter names +IFILL_MECHANICAL = '_mechanical' + # Supported values for "do not fit" DNF = { "dnf": 1, diff --git a/kibot/out_bom.py b/kibot/out_bom.py index 5b080bde..23e1d7ac 100644 --- a/kibot/out_bom.py +++ b/kibot/out_bom.py @@ -12,6 +12,7 @@ from .gs import GS from .optionable import Optionable, BaseOptions from .registrable import RegOutput from .error import KiPlotConfigurationError +from .misc import IFILL_MECHANICAL from .macros import macros, document, output_class # noqa: F401 from .bom.columnlist import ColumnList, BoMError from .bom.bom import do_bom @@ -174,23 +175,12 @@ class GroupFields(Optionable): class BoMOptions(BaseOptions): - DEFAULT_EXCLUDE = [{'column': ColumnList.COL_REFERENCE, 'regex': '^TP[0-9]*'}, - {'column': ColumnList.COL_REFERENCE, 'regex': '^FID'}, - {'column': ColumnList.COL_PART, 'regex': 'mount.*hole'}, - {'column': ColumnList.COL_PART, 'regex': 'solder.*bridge'}, - {'column': ColumnList.COL_PART, 'regex': 'solder.*jump'}, - {'column': ColumnList.COL_PART, 'regex': 'test.*point'}, - {'column': ColumnList.COL_FP, 'regex': 'test.*point'}, - {'column': ColumnList.COL_FP, 'regex': 'mount.*hole'}, - {'column': ColumnList.COL_FP, 'regex': 'fiducial'}, - ] - def __init__(self): with document: self.number = 1 """ Number of boards to build (components multiplier) """ self.variant = '' - """ Board variant(s), used to determine which components + """ Board variant, used to determine which components are output to the BoM. """ self.output = GS.def_global_output """ filename for the output (%i=bom)""" @@ -282,31 +272,7 @@ class BoMOptions(BaseOptions): self.variant.config_field = self.fit_field self.variant.variant = [] self.variant.name = 'default' - - @staticmethod - def _create_mechanical(name): - o_tree = {'name': name} - o_tree['type'] = 'generic' - o_tree['comment'] = 'Internal default mechanical filter' - o_tree['exclude_any'] = BoMOptions.DEFAULT_EXCLUDE - logger.debug('Creating internal filter: '+str(o_tree)) - return o_tree - - @staticmethod - def _create_kibom_dnx(name): - type = name[7:10] - subtype = name[11:] - o_tree = {'name': name} - o_tree['type'] = 'generic' - o_tree['comment'] = 'Internal KiBoM '+type.upper()+' filter ('+subtype+')' - o_tree['config_field'] = subtype - o_tree['exclude_value'] = True - o_tree['exclude_config'] = True - o_tree['keys'] = type+'_list' - if type[-1] == 'c': - o_tree['invert'] = True - logger.debug('Creating internal filter: '+str(o_tree)) - return o_tree + self.variant.config() # Fill or adjust any detail def config(self): super().config() @@ -335,15 +301,10 @@ class BoMOptions(BaseOptions): # component_aliases if isinstance(self.component_aliases, type): self.component_aliases = DEFAULT_ALIASES - # exclude_filter - self.exclude_filter = BaseFilter.solve_filter(self.exclude_filter, '_mechanical', None, - BoMOptions._create_mechanical, 'exclude_filter') - # dnf_filter - self.dnf_filter = BaseFilter.solve_filter(self.dnf_filter, '_kibom_dnf', '_kibom_dnf_'+self.fit_field, - BoMOptions._create_kibom_dnx, 'dnf_filter') - # dnc_filter - self.dnc_filter = BaseFilter.solve_filter(self.dnc_filter, '_kibom_dnc', '_kibom_dnc_'+self.fit_field, - BoMOptions._create_kibom_dnx, 'dnc_filter') + # Filters + self.exclude_filter = BaseFilter.solve_filter(self.exclude_filter, 'exclude_filter', IFILL_MECHANICAL) + self.dnf_filter = BaseFilter.solve_filter(self.dnf_filter, 'dnf_filter', '_kibom_dnf_'+self.fit_field) + self.dnc_filter = BaseFilter.solve_filter(self.dnc_filter, 'dnc_filter', '_kibom_dnc_'+self.fit_field) # Variants, make it an object self._normalize_variant() # Field names are handled in lowercase diff --git a/kibot/out_pdf_sch_print.py b/kibot/out_pdf_sch_print.py index 6c1ae014..8557c842 100644 --- a/kibot/out_pdf_sch_print.py +++ b/kibot/out_pdf_sch_print.py @@ -4,11 +4,15 @@ # License: GPL-3.0 # Project: KiBot (formerly KiPlot) import os +from tempfile import mkdtemp +from shutil import rmtree from .gs import (GS) from .kiplot import check_eeschema_do, exec_with_retry from .misc import (CMD_EESCHEMA_DO, PDF_SCH_PRINT) -from .optionable import BaseOptions +from .optionable import BaseOptions, Optionable +from .registrable import RegOutput from .macros import macros, document, output_class # noqa: F401 +from .fil_base import BaseFilter from . import log logger = log.get_logger(__name__) @@ -19,11 +23,38 @@ class PDF_Sch_PrintOptions(BaseOptions): with document: self.output = GS.def_global_output """ filename for the output PDF (%i=schematic %x=pdf) """ + self.variant = '' + """ Board variant(s), used to determine which components are crossed. """ + self.dnf_filter = Optionable + """ [string|list(string)=''] Name of the filter to mark components as not fitted. + A short-cut to use for simple cases where a variant is an overkill """ super().__init__() + def config(self): + super().config() + self.variant = RegOutput.check_variant(self.variant) + self.dnf_filter = BaseFilter.solve_filter(self.dnf_filter, 'dnf_filter') + def run(self, output_dir, board): check_eeschema_do() - cmd = [CMD_EESCHEMA_DO, 'export', '--all_pages', '--file_format', 'pdf', GS.sch_file, output_dir] + if self.variant or self.dnf_filter: + # Get the components list from the schematic + comps = GS.sch.get_components() + # Apply the filter + if self.dnf_filter: + for c in comps: + c.fitted = self.dnf_filter.filter(c) + # Apply the variant + if self.variant: + self.variant.filter(comps) + # Save it to a temporal dir + sch_dir = mkdtemp(prefix='tmp-kibot-pdf_sch_print-') + fname = GS.sch.save_variant(sch_dir) + sch_file = os.path.join(sch_dir, fname) + else: + sch_dir = None + sch_file = GS.sch_file + cmd = [CMD_EESCHEMA_DO, 'export', '--all_pages', '--file_format', 'pdf', sch_file, output_dir] if GS.debug_enabled: cmd.insert(1, '-vv') cmd.insert(1, '-r') @@ -38,6 +69,10 @@ class PDF_Sch_PrintOptions(BaseOptions): new = self.expand_filename_sch(output_dir, self.output, id, ext) logger.debug('Moving '+cur+' -> '+new) os.rename(cur, new) + # Remove the temporal dir if needed + if sch_dir: + logger.debug('Removing temporal variant dir `{}`'.format(sch_dir)) + rmtree(sch_dir) @output_class diff --git a/kibot/out_sch_variant.py b/kibot/out_sch_variant.py new file mode 100644 index 00000000..82a124e4 --- /dev/null +++ b/kibot/out_sch_variant.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020 Salvador E. Tropea +# Copyright (c) 2020 Instituto Nacional de TecnologĂ­a Industrial +# License: GPL-3.0 +# Project: KiBot (formerly KiPlot) +from .gs import GS +from .optionable import BaseOptions, Optionable +from .registrable import RegOutput +from .macros import macros, document, output_class # noqa: F401 +from .fil_base import BaseFilter +from . import log + +logger = log.get_logger(__name__) + + +class Sch_Variant_Options(BaseOptions): + def __init__(self): + with document: + self.variant = '' + """ Board variant(s) to apply """ + self.dnf_filter = Optionable + """ [string|list(string)=''] Name of the filter to mark components as not fitted. + A short-cut to use for simple cases where a variant is an overkill """ + super().__init__() + + def config(self): + super().config() + self.variant = RegOutput.check_variant(self.variant) + self.dnf_filter = BaseFilter.solve_filter(self.dnf_filter, 'dnf_filter') + + def run(self, output_dir, board): + if self.dnf_filter or self.variant: + # Get the components list from the schematic + comps = GS.sch.get_components() + # Apply the filter + if self.dnf_filter: + for c in comps: + c.fitted = self.dnf_filter.filter(c) + # Apply the variant + if self.variant: + # Apply the variant + self.variant.filter(comps) + # Create the schematic + GS.sch.save_variant(output_dir) + + +@output_class +class Sch_Variant(BaseOutput): # noqa: F821 + """ Schematic with variant generator + Creates a copy of the schematic with all the filters and variants applied. + This copy isn't intended for development. + Is just a tweaked version of the original where you can look at the results. """ + def __init__(self): + super().__init__() + with document: + self.options = Sch_Variant_Options + """ [dict] Options for the `sch_variant` output """ + self._sch_related = True diff --git a/kibot/registrable.py b/kibot/registrable.py index f44c8834..d1f84368 100644 --- a/kibot/registrable.py +++ b/kibot/registrable.py @@ -4,6 +4,7 @@ # License: GPL-3.0 # Project: KiBot (formerly KiPlot) from .optionable import Optionable +from .error import KiPlotConfigurationError class Registrable(object): @@ -72,6 +73,14 @@ class RegOutput(Optionable, Registrable): def add_filter(obj): RegOutput._def_filters[obj.name] = obj + @staticmethod + def check_variant(variant): + if variant: + if not RegOutput.is_variant(variant): + raise KiPlotConfigurationError("Unknown variant name `{}`".format(variant)) + return RegOutput.get_variant(variant) + return None + class RegVariant(Optionable, Registrable): """ An optionable that is also registrable. diff --git a/kibot/var_base.py b/kibot/var_base.py index 976789fd..cab5ee23 100644 --- a/kibot/var_base.py +++ b/kibot/var_base.py @@ -4,6 +4,8 @@ # License: GPL-3.0 # Project: KiBot (formerly KiPlot) from .registrable import RegVariant +from .optionable import Optionable +from .fil_base import BaseFilter from .macros import macros, document # noqa: F401 @@ -20,3 +22,34 @@ class BaseVariant(RegVariant): """ A comment for documentation purposes """ self.file_id = '' """ Text to use as the """ + # * Filters + self.exclude_filter = Optionable + """ [string|list(string)=''] Name of the filter to exclude components from BoM processing. + Use '_mechanical' for the default KiBoM behavior """ + self.dnf_filter = Optionable + """ [string|list(string)=''] Name of the filter to mark components as 'Do Not Fit'. + Use '_kibom_dnf' for the default KiBoM behavior """ + self.dnc_filter = Optionable + """ [string|list(string)=''] Name of the filter to mark components as 'Do Not Change'. + Use '_kibom_dnc' for the default KiBoM behavior """ + + def config(self): + super().config() + # exclude_filter + self.exclude_filter = BaseFilter.solve_filter(self.exclude_filter, 'exclude_filter') + # dnf_filter + self.dnf_filter = BaseFilter.solve_filter(self.dnf_filter, 'dnf_filter') + # dnc_filter + self.dnc_filter = BaseFilter.solve_filter(self.dnc_filter, 'dnc_filter') + + def filter(self, comps): + # Apply all the filters + if self.exclude_filter: + for c in comps: + c.in_bom = self.exclude_filter.filter(c) + if self.dnf_filter: + for c in comps: + c.fitted = self.dnf_filter.filter(c) + if self.dnc_filter: + for c in comps: + c.fixed = self.dnc_filter.filter(c) diff --git a/kibot/var_ibom.py b/kibot/var_ibom.py index 4c0e31f1..5c3e5a07 100644 --- a/kibot/var_ibom.py +++ b/kibot/var_ibom.py @@ -62,6 +62,7 @@ class IBoM(BaseVariant): # noqa: F821 return False def filter(self, comps): + super().filter(comps) logger.debug("Applying IBoM style variants `{}`".format(self.name)) # Make black/white lists case insensitive self.variants_whitelist = [v.lower() for v in self.variants_whitelist] diff --git a/kibot/var_kibom.py b/kibot/var_kibom.py index 069c88e7..44aba572 100644 --- a/kibot/var_kibom.py +++ b/kibot/var_kibom.py @@ -64,6 +64,7 @@ class KiBoM(BaseVariant): # noqa: F821 return not exclusive def filter(self, comps): + super().filter(comps) logger.debug("Applying KiBoM style variants `{}`".format(self.name)) for c in comps: if not (c.fitted and c.in_bom): diff --git a/tests/board_samples/l1.lib b/tests/board_samples/l1.lib new file mode 100644 index 00000000..8804f064 --- /dev/null +++ b/tests/board_samples/l1.lib @@ -0,0 +1,59 @@ +EESchema-LIBRARY Version 2.4 +#encoding utf-8 +# +# R +# +DEF R R 0 0 N Y 1 F N +F0 "R" 80 0 50 V V C CNN +F1 "R" 0 0 50 V V C CNN +F2 "" -70 0 50 V I C CNN +F3 "" 0 0 50 H I C CNN +F4 "Hi!" 0 0 50 H I C CNN "Test" +ALIAS Resistor +$FPLIST + R_* + R_* +$ENDFPLIST +DRAW +S -40 -100 40 100 0 1 10 N +X ~ 1 0 150 50 D 50 50 1 1 P +X ~ 2 0 -150 50 U 50 50 1 1 P +ENDDRAW +ENDDEF +# +# SYM_CAUTION +# +DEF ~SYM_CAUTION #SYM_CAUTION 0 40 Y Y 1 F N +F0 "#SYM_CAUTION" 0 150 50 H I C CNN +F1 "SYM_CAUTION" 0 -175 50 H I C CNN +F2 "Tedy:Symbol_Caution_Type2_FSilkS_Small" 100 -250 50 H I C CNN +F3 "" 0 0 50 H I C CNN +DRAW +A 0 35 16 1616 184 0 0 0 N -15 40 15 40 +C 0 -46 10 0 0 0 F +T 0 0 -100 20 0 0 0 CAUTION Italic 0 C C +P 2 0 0 0 -5 -30 -15 40 F +P 3 0 0 0 -5 -30 5 -30 15 40 F +P 5 0 0 0 -50 -75 75 -75 0 100 -75 -75 -50 -75 f +ENDDRAW +ENDDEF +# +# C +# +DEF C C 0 10 N Y 1 F N +F0 "C" 25 100 50 H V L CNN +F1 "C" 25 -100 50 H V L CNN +F2 "" 38 -150 50 H I C CNN +F3 "" 0 0 50 H I C CNN +$FPLIST + C_* +$ENDFPLIST +DRAW +P 2 0 1 20 -80 -30 80 -30 N +P 2 0 1 20 -80 30 80 30 N +X ~ 1 0 150 110 D 50 50 1 1 P +X ~ 2 0 -150 110 U 50 50 1 1 P +ENDDRAW +ENDDEF +# +#End Library diff --git a/tests/board_samples/missing.sch b/tests/board_samples/missing.sch new file mode 100644 index 00000000..4969f617 --- /dev/null +++ b/tests/board_samples/missing.sch @@ -0,0 +1,66 @@ +EESchema Schematic File Version 4 +EELAYER 30 0 +EELAYER END +$Descr A4 11693 8268 +encoding utf-8 +Sheet 1 1 +Title "KiBom Test Schematic" +Date "2020-03-12" +Rev "A" +Comp "https://github.com/SchrodingersGat/KiBom" +Comment1 "" +Comment2 "" +Comment3 "" +Comment4 "" +$EndDescr +Text Notes 550 1050 0 118 ~ 0 +This schematic serves as a test file for the KiBot export script.\nHere we have a component without lib (from old KiCad?) \nand another that isn't in any lib. +$Comp +L l1:C C1 +U 1 1 5F43BEC2 +P 1000 1700 +F 0 "C1" H 1115 1746 50 0000 L CNN +F 1 "1nF" H 1115 1655 50 0000 L CNN +F 2 "Capacitor_SMD:C_0805_2012Metric" H 1038 1550 50 0001 C CNN +F 3 "~" H 1000 1700 50 0001 C CNN +F 4 "T2" H 1000 1700 50 0001 C CNN "Config" + 1 1000 1700 + 1 0 0 -1 +$EndComp +$Comp +L l1:C C2 +U 1 1 5F43CE1C +P 1450 1700 +F 0 "C2" H 1565 1746 50 0000 L CNN +F 1 "1000 pF" H 1565 1655 50 0000 L CNN +F 2 "Capacitor_SMD:C_0805_2012Metric" H 1488 1550 50 0001 C CNN +F 3 "~" H 1450 1700 50 0001 C CNN +F 4 "T3" H 1450 1700 50 0001 C CNN "Config" + 1 1450 1700 + 1 0 0 -1 +$EndComp +$Comp +L Resistor R1 +U 1 1 5F43D144 +P 2100 1700 +F 0 "R1" H 2170 1746 50 0000 L CNN +F 1 "1k" H 2170 1655 50 0000 L CNN +F 2 "Resistor_SMD:R_0805_2012Metric" V 2030 1700 50 0001 C CNN +F 3 "~" H 2100 1700 50 0001 C CNN +F 4 "default" H 2100 1700 50 0001 C CNN "Config" + 1 2100 1700 + 1 0 0 -1 +$EndComp +$Comp +L l1:FooBar R2 +U 1 1 5F43D4BB +P 2500 1700 +F 0 "R2" H 2570 1746 50 0000 L CNN +F 1 "1000" H 2570 1655 50 0000 L CNN +F 2 "Resistor_SMD:R_0805_2012Metric" V 2430 1700 50 0001 C CNN +F 3 "~" H 2500 1700 50 0001 C CNN +F 4 "T1" H 2500 1700 50 0001 C CNN "Config" + 1 2500 1700 + 1 0 0 -1 +$EndComp +$EndSCHEMATC diff --git a/tests/board_samples/sym-lib-table b/tests/board_samples/sym-lib-table new file mode 100644 index 00000000..890dc4a1 --- /dev/null +++ b/tests/board_samples/sym-lib-table @@ -0,0 +1,3 @@ +(sym_lib_table + (lib (name l1)(type Legacy)(uri ${KIPRJMOD}/l1.lib)(options "")(descr "")) +) diff --git a/tests/board_samples/test_v5.sch b/tests/board_samples/test_v5.sch index 3b4ac3c9..d1ab1007 100644 --- a/tests/board_samples/test_v5.sch +++ b/tests/board_samples/test_v5.sch @@ -1908,13 +1908,14 @@ B8 00 A2 1F 41 E0 C7 2A 65 BE 7E E1 F5 E7 46 4B 3D 22 C6 18 DB CE 36 66 F9 2E 00 EndData $EndBitmap $Comp -L Device:R R1 +L l1:Resistor R1 U 1 1 5F33EC02 P 1300 3450 F 0 "R1" V 1093 3450 50 0000 C CNN F 1 "R" V 1184 3450 50 0000 C CNN F 2 "" V 1230 3450 50 0001 C CNN F 3 "~" H 1300 3450 50 0001 C CNN +F 4 "Hi!" H 1300 3450 50 0001 C CNN "Test" 1 1300 3450 0 1 1 0 $EndComp @@ -1987,4 +1988,15 @@ Wire Wire Line 3800 4500 3800 3250 Wire Wire Line 3800 3250 4000 3250 +$Comp +L l1:SYM_CAUTION #SYM_CAUTION1 +U 1 1 5F4ECE1F +P 2750 3450 +F 0 "#SYM_CAUTION1" H 2750 3600 50 0001 C CNN +F 1 "SYM_CAUTION" H 2750 3275 50 0001 C CNN +F 2 "" H 2850 3200 50 0001 C CNN +F 3 "" H 2750 3450 50 0001 C CNN + 1 2750 3450 + 1 0 0 -1 +$EndComp $EndSCHEMATC diff --git a/tests/board_samples/v5_errors/error_bad_text2.sch b/tests/board_samples/v5_errors/error_bad_text2.sch new file mode 100644 index 00000000..f0b69d91 --- /dev/null +++ b/tests/board_samples/v5_errors/error_bad_text2.sch @@ -0,0 +1,17 @@ +EESchema Schematic File Version 4 +EELAYER 30 0 +EELAYER END +$Descr A4 11693 8268 +encoding utf-8 +Sheet 1 1 +Title "" +Date "" +Rev "" +Comp "" +Comment1 "" +Comment2 "" +Comment3 "" +Comment4 "" +$EndDescr +Text GLabel 1600 2300 2 50 +$EndSCHEMATC diff --git a/tests/board_samples/v5_errors/error_bad_text3.sch b/tests/board_samples/v5_errors/error_bad_text3.sch new file mode 100644 index 00000000..020591f2 --- /dev/null +++ b/tests/board_samples/v5_errors/error_bad_text3.sch @@ -0,0 +1,17 @@ +EESchema Schematic File Version 4 +EELAYER 30 0 +EELAYER END +$Descr A4 11693 8268 +encoding utf-8 +Sheet 1 1 +Title "" +Date "" +Rev "" +Comp "" +Comment1 "" +Comment2 "" +Comment3 "" +Comment4 "" +$EndDescr +Text GLabel 1600 2300 2 50 BiDi ~ AA +$EndSCHEMATC diff --git a/tests/reference/test_v5-schematic_(no_L).pdf b/tests/reference/test_v5-schematic_(no_L).pdf new file mode 100644 index 00000000..79843505 Binary files /dev/null and b/tests/reference/test_v5-schematic_(no_L).pdf differ diff --git a/tests/test_plot/test_int_bom.py b/tests/test_plot/test_int_bom.py index 316da62b..d1ca4c5d 100644 --- a/tests/test_plot/test_int_bom.py +++ b/tests/test_plot/test_int_bom.py @@ -1283,3 +1283,22 @@ def test_int_bom_fil_1(): rows, header, info = ctx.load_csv('multi.csv') check_kibom_test_netlist(rows, ref_column, 1, None, ['C1-C2']) ctx.clean_up() + + +def test_int_bom_variant_t3(): + """ Test if we can move the filters to the variant. + Also test the '!' filter (always false) """ + prj = 'kibom-variante' + ctx = context.TestContextSCH('test_int_bom_variant_t3', prj, 'int_bom_var_t3_csv', BOM_DIR) + ctx.run() + rows, header, info = ctx.load_csv(prj+'-bom_(V1).csv') + ref_column = header.index(REF_COLUMN_NAME) + check_kibom_test_netlist(rows, ref_column, 2, ['R3', 'R4'], ['R1', 'R2']) + VARIANTE_PRJ_INFO[1] = 't1_v1' + check_csv_info(info, VARIANTE_PRJ_INFO, [4, 20, 2, 1, 2]) + ctx.search_err(r"Creating internal filter(.*)_mechanical") + ctx.search_err(r"Creating internal filter(.*)_kibom_dnf_Config") + ctx.search_err(r"Creating internal filter(.*)_kibom_dnc") + rows, header, info = ctx.load_csv(prj+'-bom_(V1b).csv') + # Here we remove the DNC, so R1 and R2 becomes identical + check_kibom_test_netlist(rows, ref_column, 1, ['R3', 'R4'], ['R1', 'R2']) diff --git a/tests/test_plot/test_print_sch.py b/tests/test_plot/test_print_sch.py index 08d85aea..dc6a1042 100644 --- a/tests/test_plot/test_print_sch.py +++ b/tests/test_plot/test_print_sch.py @@ -11,17 +11,22 @@ pytest-3 --log-cli-level debug import os import sys +import logging +import coverage # Look for the 'utils' module from where the script is running prev_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) if prev_dir not in sys.path: sys.path.insert(0, prev_dir) from kibot.misc import (PDF_SCH_PRINT, SVG_SCH_PRINT) +from kibot.kicad.v5_sch import Schematic, SchFileError, DrawPoligon, Pin # Utils import from utils import context PDF_DIR = '' PDF_FILE = 'Schematic.pdf' SVG_FILE = 'Schematic.svg' +NI_DIR = 'no_inductor' +cov = coverage.Coverage() def test_print_sch_ok(): @@ -54,3 +59,93 @@ def test_print_sch_svg_fail(): ctx = context.TestContext('PrSCHFail_SVG', prj, 'print_sch_svg', PDF_DIR) ctx.run(SVG_SCH_PRINT, no_board_file=True, extra=['-e', os.path.join(ctx.get_board_dir(), 'print_err.sch')]) ctx.clean_up() + + +def check_l1(ctx): + ctx.run() + o_name = os.path.join(NI_DIR, 'test_v5.sch') + ctx.expect_out_file(o_name) + sch = Schematic() + try: + sch.load(ctx.get_out_path(o_name)) + except SchFileError as e: + logging.error('At line {} of `{}`: {}'.format(e.line, e.file, e.msg)) + logging.error('Line content: `{}`'.format(e.code)) + assert False + comps = sch.get_components() + l1 = next(c for c in comps if c.ref == 'L1') + assert l1 + logging.debug('Found L1') + assert l1.lib == 'n' + logging.debug('L1 is crossed') + ctx.clean_up() + + +def test_sch_variant_ni_1(): + """ Using a variant """ + prj = 'test_v5' # Is the most complete, contains every KiCad object I know + ctx = context.TestContextSCH('test_sch_variant_ni_1', prj, 'sch_no_inductors_1', PDF_DIR) + check_l1(ctx) + + +def test_sch_variant_ni_2(): + """ Using a filter """ + prj = 'test_v5' # Is the most complete, contains every KiCad object I know + ctx = context.TestContextSCH('test_sch_variant_ni_2', prj, 'sch_no_inductors_2', PDF_DIR) + check_l1(ctx) + + +def test_print_sch_variant_ni_1(): + """ Using a variant """ + prj = 'test_v5' # Is the most complete, contains every KiCad object I know + ctx = context.TestContextSCH('test_print_sch_variant_ni_1', prj, 'print_pdf_no_inductors_1', PDF_DIR) + ctx.run() + r_name = 'test_v5-schematic_(no_L).pdf' + o_name = os.path.join(NI_DIR, r_name) + ctx.expect_out_file(o_name) + ctx.compare_pdf(o_name, r_name) + ctx.clean_up() + + +def test_print_sch_variant_ni_2(): + """ Using a filter """ + prj = 'test_v5' # Is the most complete, contains every KiCad object I know + ctx = context.TestContextSCH('test_print_sch_variant_ni_2', prj, 'print_pdf_no_inductors_2', PDF_DIR) + ctx.run() + r_name = 'test_v5-schematic_(no_L).pdf' + o_name = os.path.join(NI_DIR, 'test_v5-schematic.pdf') + ctx.expect_out_file(o_name) + ctx.compare_pdf(o_name, r_name) + ctx.clean_up() + + +def test_sch_missing(): + """ R1 exists in l1.lib, but the lib isn't specified. + R2 is bogus, completely missing """ + prj = 'missing' + ctx = context.TestContextSCH('test_sch_missing', prj, 'sch_no_inductors_1', PDF_DIR) + ctx.run() + o_name = os.path.join(NI_DIR, prj+'.sch') + ctx.expect_out_file(o_name) + ctx.search_err("Component .?Resistor.? doesn't specify its library") + ctx.search_err("Missing component .?l1:FooBar.?") + ctx.search_err("Missing component(.*)Resistor", invert=True) + ctx.clean_up() + + +def test_sch_bizarre_cases(): + """ Poligon without points. + Pin with unknown direction. """ + pol = DrawPoligon() + pol.points = 0 + pol.coords = [] + pin = Pin() + pin.dir = 'bogus' + cov.load() + cov.start() + x1, y1, x2, y2, ok_pol = pol.get_rect() + x1, y1, x2, y2, ok_pin = pin.get_rect() + cov.stop() + cov.save() + assert ok_pol is False + assert ok_pin is False diff --git a/tests/test_plot/test_sch_errors.py b/tests/test_plot/test_sch_errors.py index 8f046688..ebed55b3 100644 --- a/tests/test_plot/test_sch_errors.py +++ b/tests/test_plot/test_sch_errors.py @@ -151,7 +151,15 @@ def test_sch_errors_bad_conn(): def test_sch_errors_bad_text(): - setup_ctx('bad_text', 'Malformed text') + setup_ctx('bad_text', 'Malformed .?Text.?') + + +def test_sch_errors_bad_text2(): + setup_ctx('bad_text2', 'Missing .?Text.? shape') + + +def test_sch_errors_bad_text3(): + setup_ctx('bad_text3', 'Not a number in .?Text.?') def test_sch_errors_bad_wire(): diff --git a/tests/test_plot/test_yaml_errors.py b/tests/test_plot/test_yaml_errors.py index 864c3621..523b53c6 100644 --- a/tests/test_plot/test_yaml_errors.py +++ b/tests/test_plot/test_yaml_errors.py @@ -578,3 +578,17 @@ def test_error_fil_unknown(): ctx.run(EXIT_BAD_CONFIG) assert ctx.search_err("Unknown filter (.*) used for ") ctx.clean_up() + + +def test_error_var_unknown(): + ctx = context.TestContextSCH('test_error_var_unknown', 'links', 'error_unk_variant', '') + ctx.run(EXIT_BAD_CONFIG) + assert ctx.search_err("Unknown variant name") + ctx.clean_up() + + +def test_error_wrong_fil_name(): + ctx = context.TestContextSCH('test_error_wrong_fil_name', 'links', 'error_wrong_fil_name', '') + ctx.run(EXIT_BAD_CONFIG) + assert ctx.search_err("Filter names starting with (.*) are reserved") + ctx.clean_up() diff --git a/tests/utils/context.py b/tests/utils/context.py index 7a126a89..75adc322 100644 --- a/tests/utils/context.py +++ b/tests/utils/context.py @@ -20,6 +20,10 @@ MODE_SCH = 1 MODE_PCB = 0 +def quote(s): + return '"'+s+'"' + + class TestContext(object): def __init__(self, test_name, board_name, yaml_name, sub_dir, yaml_compressed=False): @@ -259,18 +263,26 @@ class TestContext(object): logging.debug('output match: `{}` OK'.format(text)) return m - def search_err(self, text): + def search_err(self, text, invert=False): if isinstance(text, list): res = [] for t in text: m = re.search(t, self.err, re.MULTILINE) - assert m is not None, t - logging.debug('error match: `{}` (`{}`) OK'.format(t, m.group(0))) - res.append(m) + if invert: + assert m is None, t + logging.debug('error no match: `{}` OK'.format(t)) + else: + assert m is not None, t + logging.debug('error match: `{}` (`{}`) OK'.format(t, m.group(0))) + res.append(m) return res m = re.search(text, self.err, re.MULTILINE) - assert m is not None - logging.debug('error match: `{}` (`{}`) OK'.format(text, m.group(0))) + if invert: + assert m is None, text + logging.debug('error no match: `{}` OK'.format(text)) + else: + assert m is not None, text + logging.debug('error match: `{}` (`{}`) OK'.format(text, m.group(0))) return m def search_in_file(self, file, texts): @@ -296,17 +308,21 @@ class TestContext(object): m = re.search(t, txt, re.MULTILINE) assert m is None - def compare_image(self, image, reference=None, diff='diff.png'): + def compare_image(self, image, reference=None, diff='diff.png', ref_out_dir=False): """ For images and single page PDFs """ if reference is None: reference = image + if ref_out_dir: + reference = self.get_out_path(reference) + else: + reference = os.path.join(REF_DIR, reference) cmd = ['compare', # Tolerate 5 % error in color '-fuzz', '5%', # Count how many pixels differ '-metric', 'AE', self.get_out_path(image), - os.path.join(REF_DIR, reference), + reference, # Avoid the part where KiCad version is printed '-crop', '100%x92%+0+0', '+repage', '-colorspace', 'RGB', @@ -344,16 +360,7 @@ class TestContext(object): assert len(ref_pages) == len(gen_pages) # Compare each page for page in range(len(ref_pages)): - cmd = ['compare', '-metric', 'MSE', - self.get_out_path('ref-'+str(page)+'.png'), - self.get_out_path('gen-'+str(page)+'.png'), - self.get_out_path(diff.format(page))] - logging.debug('Comparing images with: '+str(cmd)) - res = subprocess.check_output(cmd, stderr=subprocess.STDOUT) - m = re.match(r'([\d\.]+) \(([\d\.]+)\)', res.decode()) - assert m - logging.debug('MSE={} ({})'.format(m.group(1), m.group(2))) - assert float(m.group(2)) == 0.0 + self.compare_image('gen-'+str(page)+'.png', 'ref-'+str(page)+'.png', diff.format(page), ref_out_dir=True) def compare_txt(self, text, reference=None, diff='diff.txt'): if reference is None: diff --git a/tests/yaml_samples/error_unk_variant.kibot.yaml b/tests/yaml_samples/error_unk_variant.kibot.yaml new file mode 100644 index 00000000..497a617f --- /dev/null +++ b/tests/yaml_samples/error_unk_variant.kibot.yaml @@ -0,0 +1,11 @@ +# Example KiBot config file +kibot: + version: 1 + +outputs: + - name: 'no_inductor' + comment: "Inductors removed" + type: sch_variant + dir: no_inductor + options: + variant: 'no_inductor' diff --git a/tests/yaml_samples/error_wrong_fil_name.kibot.yaml b/tests/yaml_samples/error_wrong_fil_name.kibot.yaml new file mode 100644 index 00000000..0fa2ccc5 --- /dev/null +++ b/tests/yaml_samples/error_wrong_fil_name.kibot.yaml @@ -0,0 +1,10 @@ +# Example KiBot config file +kibot: + version: 1 + +filters: + - name: '_no_inductor' + comment: 'Inductors removed' + type: generic + exclude_refs: + - L* diff --git a/tests/yaml_samples/int_bom_var_t3_csv.kibot.yaml b/tests/yaml_samples/int_bom_var_t3_csv.kibot.yaml new file mode 100644 index 00000000..15bbe700 --- /dev/null +++ b/tests/yaml_samples/int_bom_var_t3_csv.kibot.yaml @@ -0,0 +1,39 @@ +# Example KiBot config file +kibot: + version: 1 + + +variants: + - name: 't1_v1' + comment: 'Test 1 Variant V1' + type: kibom + file_id: '_(V1)' + variant: V1 + dnc_filter: '_kibom_dnc' + dnf_filter: '_kibom_dnf_Config' + exclude_filter: '_mechanical' + + - name: 't1_v1b' + comment: 'Test 1 Variant V1' + type: kibom + file_id: '_(V1b)' + variant: V1 + +outputs: + - name: 'bom_internal_v1' + comment: "Bill of Materials in CSV format for variant t1_v1" + type: bom + dir: BoM + options: + variant: t1_v1 + dnc_filter: '_none' + dnf_filter: '_none' + exclude_filter: '_none' + + - name: 'bom_internal_v1b' + comment: "Bill of Materials in CSV format for variant t1_v1" + type: bom + dir: BoM + options: + variant: t1_v1b + dnc_filter: '!' diff --git a/tests/yaml_samples/print_pdf_no_inductors_1.kibot.yaml b/tests/yaml_samples/print_pdf_no_inductors_1.kibot.yaml new file mode 100644 index 00000000..4ae89156 --- /dev/null +++ b/tests/yaml_samples/print_pdf_no_inductors_1.kibot.yaml @@ -0,0 +1,25 @@ +# Example KiBot config file +kibot: + version: 1 + +filters: + - name: 'no_inductor' + comment: 'Inductors removed' + type: generic + exclude_refs: + - L* + +variants: + - name: 'no_inductor' + comment: 'Inductors removed' + type: kibom + file_id: '_(no_L)' + dnf_filter: 'no_inductor' + +outputs: + - name: 'no_inductor' + comment: "Inductors removed" + type: pdf_sch_print + dir: no_inductor + options: + variant: 'no_inductor' diff --git a/tests/yaml_samples/print_pdf_no_inductors_2.kibot.yaml b/tests/yaml_samples/print_pdf_no_inductors_2.kibot.yaml new file mode 100644 index 00000000..e83133a4 --- /dev/null +++ b/tests/yaml_samples/print_pdf_no_inductors_2.kibot.yaml @@ -0,0 +1,18 @@ +# Example KiBot config file +kibot: + version: 1 + +filters: + - name: 'no_inductor' + comment: 'Inductors removed' + type: generic + exclude_refs: + - L* + +outputs: + - name: 'no_inductor' + comment: "Inductors removed" + type: pdf_sch_print + dir: no_inductor + options: + dnf_filter: 'no_inductor' diff --git a/tests/yaml_samples/sch_no_inductors_1.kibot.yaml b/tests/yaml_samples/sch_no_inductors_1.kibot.yaml new file mode 100644 index 00000000..0f31165b --- /dev/null +++ b/tests/yaml_samples/sch_no_inductors_1.kibot.yaml @@ -0,0 +1,25 @@ +# Example KiBot config file +kibot: + version: 1 + +filters: + - name: 'no_inductor' + comment: 'Inductors removed' + type: generic + exclude_refs: + - L* + +variants: + - name: 'no_inductor' + comment: 'Inductors removed' + type: kibom + file_id: '_(no_L)' + dnf_filter: 'no_inductor' + +outputs: + - name: 'no_inductor' + comment: "Inductors removed" + type: sch_variant + dir: no_inductor + options: + variant: 'no_inductor' diff --git a/tests/yaml_samples/sch_no_inductors_2.kibot.yaml b/tests/yaml_samples/sch_no_inductors_2.kibot.yaml new file mode 100644 index 00000000..6c9e16fd --- /dev/null +++ b/tests/yaml_samples/sch_no_inductors_2.kibot.yaml @@ -0,0 +1,18 @@ +# Example KiBot config file +kibot: + version: 1 + +filters: + - name: 'no_inductor' + comment: 'Inductors removed' + type: generic + exclude_refs: + - L* + +outputs: + - name: 'no_inductor' + comment: "Inductors removed" + type: sch_variant + dir: no_inductor + options: + dnf_filter: 'no_inductor' diff --git a/tests/yaml_samples/sch_variant_t1.kibot.yaml b/tests/yaml_samples/sch_variant_t1.kibot.yaml new file mode 100644 index 00000000..71e0db6f --- /dev/null +++ b/tests/yaml_samples/sch_variant_t1.kibot.yaml @@ -0,0 +1,63 @@ +# Example KiBot config file +kibot: + version: 1 + +variants: + - name: 't1_v1' + comment: 'Test 1 Variant V1' + type: kibom + file_id: '_(V1)' + variant: V1 + dnf_filter: '_kibom_dnf' + + - name: 't1_v2' + comment: 'Test 1 Variant V2' + type: kibom + file_id: '_(V2)' + variant: V2 + + - name: 't1_v3' + comment: 'Test 1 Variant V3' + type: kibom + file_id: '_V3' + variant: V3 + + - name: 'bla bla' + comment: 'Test 1 Variant V1+V3' + type: kibom + file_id: '_bla_bla' + variant: ['V1', 'V3'] + +outputs: + - name: 'dummy' + comment: "Copy the Schematic" + type: sch_variant + dir: Copy + + - name: 't1_v1' + comment: "V1 applied" + type: sch_variant + dir: V1 + options: + variant: t1_v1 + + - name: 't1_v2' + comment: "V2 applied" + type: sch_variant + dir: V2 + options: + variant: t1_v2 + + - name: 't1_v3' + comment: "V3 applied" + type: sch_variant + dir: V3 + options: + variant: t1_v3 + + - name: 't1_v1v3' + comment: "V1+V3 applied" + type: sch_variant + dir: V1V3 + options: + variant: 'bla bla'