commit
13ce3251da
|
|
@ -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
|
||||
|
|
|
|||
3
Makefile
3
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:
|
||||
|
|
|
|||
22
README.md
22
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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
(sym_lib_table
|
||||
(lib (name l1)(type Legacy)(uri ${KIPRJMOD}/l1.lib)(options "")(descr ""))
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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.
|
|
@ -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'])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# Example KiBot config file
|
||||
kibot:
|
||||
version: 1
|
||||
|
||||
filters:
|
||||
- name: '_no_inductor'
|
||||
comment: 'Inductors removed'
|
||||
type: generic
|
||||
exclude_refs:
|
||||
- L*
|
||||
|
|
@ -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: '!'
|
||||
|
|
@ -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'
|
||||
|
|
@ -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'
|
||||
|
|
@ -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'
|
||||
|
|
@ -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'
|
||||
|
|
@ -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'
|
||||
Loading…
Reference in New Issue