Merge pull request #6 from INTI-CMNB/gen_sch_variant

Gen sch variant
This commit is contained in:
Salvador E. Tropea 2020-09-02 10:34:07 -03:00 committed by GitHub
commit 13ce3251da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1197 additions and 131 deletions

View File

@ -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

View File

@ -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:

View File

@ -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.

View File

@ -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.

View File

@ -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',

View File

@ -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

View File

@ -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):

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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

58
kibot/out_sch_variant.py Normal file
View File

@ -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

View File

@ -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.

View File

@ -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)

View File

@ -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]

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,3 @@
(sym_lib_table
(lib (name l1)(type Legacy)(uri ${KIPRJMOD}/l1.lib)(options "")(descr ""))
)

View File

@ -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

View File

@ -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

View File

@ -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

Binary file not shown.

View File

@ -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'])

View File

@ -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

View File

@ -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():

View File

@ -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()

View File

@ -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:

View File

@ -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'

View File

@ -0,0 +1,10 @@
# Example KiBot config file
kibot:
version: 1
filters:
- name: '_no_inductor'
comment: 'Inductors removed'
type: generic
exclude_refs:
- L*

View File

@ -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: '!'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'