Added support for variants on KiCost output.

Related to #106
This commit is contained in:
Salvador E. Tropea 2021-12-30 18:26:31 -03:00
parent 4b3636313c
commit e5ed4b6652
5 changed files with 183 additions and 18 deletions

View File

@ -54,6 +54,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Support for new KiCost options `split_extra_fields` and `board_qty`. (#120)
- Datasheet downloader. (#119)
- Position files now can include virtual components. (#106)
- Support for variants on KiCost output. (#106)
### Changed
- Internal BoM: now components with different Tolerance, Voltage, Current

View File

@ -11,9 +11,13 @@ Currently oriented to collect the components for the BoM.
# Encapsulate file/line
import re
import os
from xml.etree.ElementTree import Element, SubElement, tostring
from xml.dom import minidom
from datetime import datetime
from copy import deepcopy
from collections import OrderedDict
from .config import KiConf, un_quote
from ..__main__ import __version__
from ..gs import GS
from ..misc import (W_BADPOLI, W_POLICOORDS, W_BADSQUARE, W_BADCIRCLE, W_BADARC, W_BADTEXT, W_BADPIN, W_BADCOMP, W_BADDRAW,
W_UNKDCM, W_UNKAR, W_ARNOPATH, W_ARNOREF, W_MISCFLD, W_EXTRASPC, W_NOLIB, W_INCPOS, W_NOANNO, W_MISSLIB,
@ -447,6 +451,8 @@ class Pin(object):
r'([012])\s+' # 9 Which representation (0 == both) for DeMorgan
r'([IOBTPUWwCEN])' # 10 Electrical type
r'((?:\s+)\S+)?') # 11 Graphic type
type2name = {'I': 'input', 'O': 'output', 'B': 'BiDi', 'T': '3state', 'P': 'passive', 'U': 'unspc',
'W': 'power_in', 'w': 'power_out', 'C': 'openCol', 'E': 'openEm', 'N': 'NotConnected'}
def __init__(self):
super().__init__()
@ -1473,7 +1479,7 @@ class Schematic(object):
while True:
line = f.get_line()
if line.startswith('$EndDescr'):
self.title = self.title_block.get('Title', '')
self.title_ori = self.title = self.title_block.get('Title', '')
self.date = self.title_block.get('Date', '')
self.revision = self.title_block.get('Rev', '')
self.company = self.title_block.get('Comp', '')
@ -1507,6 +1513,8 @@ class Schematic(object):
self.fields = fields
self.fields_lc = fields_lc
self.project = project
self.sheet_path = sheet_path
self.sheet_path_h = sheet_path_h
with open(fname, 'rt') as fh:
f = SCHLineReader(fh, fname)
line = f.get_line()
@ -1684,6 +1692,11 @@ class Schematic(object):
comp.dcm = dcm.comps.get(name)
if not comp.dcm and k+':'+name in self.comps_data:
logger.warning(W_MISSDCM + 'Missing doc-lib entry for {}:{}'.format(k, name))
# Also do it for the aliases
for name, comp in lib.alias.items():
comp.dcm = dcm.comps.get(name)
if not comp.dcm and k+':'+name in self.comps_data:
logger.warning(W_MISSDCM + 'Missing doc-lib entry for {}:{}'.format(k, name))
# Transfer the descriptions to the instances of the components
self.walk_components(self.apply_dcm, self)
@ -1750,3 +1763,133 @@ class Schematic(object):
sch = os.path.basename(sch)
fnames.append(os.path.join(dest_dir, sch.replace('/', '_')))
return fnames
def save_netlist_design(self, root):
""" Generates the `design` section of the netlist """
# TODO: Dump subsheets, may be in the future
design = SubElement(root, 'design')
SubElement(design, 'source').text = self.fname
SubElement(design, 'date').text = datetime.now().strftime("%a %b %e %H:%M:%S %Y")
SubElement(design, 'tool').text = 'KiBot v'+__version__
sheet = SubElement(design, 'sheet')
sheet.set('number', str(self.sheet))
sheet.set('name', str(self.sheet_path_h))
sheet.set('tstamps', str('/'+self.sheet_path))
tblock = SubElement(sheet, 'title_block')
title = SubElement(tblock, 'title')
if self.title_ori:
title.text = self.title_ori
company = SubElement(tblock, 'company')
if self.company:
company.text = self.company
rev = SubElement(tblock, 'rev')
if self.revision:
rev.text = self.revision
dt = SubElement(tblock, 'date')
if self.date:
dt.text = self.date
SubElement(tblock, 'source').text = os.path.basename(self.fname)
com = SubElement(tblock, 'comment')
com.set('number', '1')
com.set('value', self.comment1)
com = SubElement(tblock, 'comment')
com.set('number', '2')
com.set('value', self.comment2)
com = SubElement(tblock, 'comment')
com.set('number', '3')
com.set('value', self.comment3)
com = SubElement(tblock, 'comment')
com.set('number', '4')
com.set('value', self.comment4)
def save_netlist_components(self, root, comps, excluded, fitted, no_field):
""" Generates the `components` section of the netlist """
components = SubElement(root, 'components')
for c in comps:
if not excluded and not c.included:
continue
if fitted and not c.fitted:
continue
comp = SubElement(components, 'comp')
comp.set('ref', c.ref)
SubElement(comp, 'value').text = c.value
SubElement(comp, 'footprint').text = c.footprint
SubElement(comp, 'datasheet').text = c.datasheet
fields = SubElement(comp, 'fields')
for fname, fvalue in c.get_user_fields():
if fname in no_field:
continue
fld = SubElement(fields, 'field')
fld.set('name', fname)
fld.text = fvalue
lbs = SubElement(comp, 'libsource')
lbs.set('lib', c.lib)
lbs.set('part', c.name)
lbs.set('description', c.desc)
shp = SubElement(comp, 'sheetpath')
shp.set('names', c.sheet_path_h)
shp.set('tstamps', c.sheet_path_h)
SubElement(comp, 'tstamp').text = c.id
def save_netlist_libparts(self, root):
libparts = SubElement(root, 'libparts')
for k, v in self.comps_data.items():
if not v:
continue
libpart = SubElement(libparts, 'libpart')
res = k.split(':')
if res:
libpart.set('lib', res[0])
libpart.set('part', res[1])
if v.alias:
aliases = SubElement(libpart, 'aliases')
for alias in v.alias:
SubElement(aliases, 'alias').text = alias
if v.dcm:
if v.dcm.desc:
SubElement(libpart, 'description').text = v.dcm.desc
if v.dcm.datasheet:
SubElement(libpart, 'docs').text = v.dcm.datasheet
if v.fp_list:
fps = SubElement(libpart, 'footprints')
for fp in v.fp_list:
SubElement(fps, 'fp').text = fp
flds = SubElement(libpart, 'fields')
for fld in v.fields:
if not fld.value:
continue
field = SubElement(flds, 'field')
field.set('name', fld.name)
field.text = fld.value
# Search pins
if next(filter(lambda x: isinstance(x, Pin), v.draw), False):
pins = SubElement(libpart, 'pins')
for pin in sorted(filter(lambda x: isinstance(x, Pin), v.draw), key=lambda x: x.number):
pn = SubElement(pins, 'pin')
pn.set('num', pin.number)
pn.set('name', pin.name)
pn.set('type', pin.type2name.get(pin.type, 'unknown'))
def save_netlist(self, fhandle, comps, excluded=False, fitted=True, no_field=[]):
""" This is a partial netlist in XML, only useful for BoMs """
root = Element("export")
root.set('version', 'D')
# Design section
self.save_netlist_design(root)
# Components
self.save_netlist_components(root, comps, excluded, fitted, no_field)
# LibParts
self.save_netlist_libparts(root)
# Libraries
libraries = SubElement(root, 'libraries')
for k, v in self.libs.items():
lib = SubElement(libraries, 'library')
lib.set('logical', k)
SubElement(lib, 'uri').text = v
# Nets
# TODO: May be in the future
SubElement(root, 'nets')
# Make it look nice
rough_string = tostring(root, 'utf-8')
reparsed = minidom.parseString(rough_string)
fhandle.write(reparsed.toprettyxml(indent=" "))

View File

@ -216,6 +216,8 @@ W_BADCHARS = '(W074) '
W_DATEFORMAT = '(W075) '
W_UNKFLD = '(W076) '
W_ALRDOWN = '(W077) '
W_KICOSTFLD = '(W078) '
W_MIXVARIANT = '(W079) '
class Rect(object):

View File

@ -3,12 +3,15 @@
# Copyright (c) 2021 Instituto Nacional de Tecnología Industrial
# License: GPL-3.0
# Project: KiBot (formerly KiPlot)
import os
from os.path import isfile, abspath, join, dirname
from subprocess import check_output, STDOUT, CalledProcessError
from .misc import CMD_KICOST, URL_KICOST, BOM_ERROR, DISTRIBUTORS, W_UNKDIST, ISO_CURRENCIES, W_UNKCUR, KICOST_SUBMODULE
from tempfile import mkdtemp
from shutil import rmtree
from .misc import (CMD_KICOST, URL_KICOST, BOM_ERROR, DISTRIBUTORS, W_UNKDIST, ISO_CURRENCIES, W_UNKCUR, KICOST_SUBMODULE,
W_KICOSTFLD, W_MIXVARIANT)
from .error import KiPlotConfigurationError
from .optionable import Optionable
from .registrable import RegOutput
from .gs import GS
from .kiplot import check_script
from .out_base import VariantOptions
@ -17,8 +20,7 @@ from .fil_base import FieldRename
from . import log
logger = log.get_logger()
WARNING_MIX = ("Internal variants and filters are currently ignored.\n"
"Exception: a KiCost variant that uses `variant` as variant field")
WARNING_MIX = ("Don't use the `kicost_variant` when using internal variants/filters")
class Aggregate(Optionable):
@ -100,10 +102,6 @@ class KiCostOptions(VariantOptions):
return val
def config(self, parent):
# If we are using a KiCost variant make it the default for `kicost_variant`
variant = RegOutput.check_variant(self.variant)
if variant is not None and variant.type == 'kicost' and variant.variant_field == 'variant':
self.kicost_variant = variant.variant
super().config(parent)
if not self.output:
self.output = '%f.%x'
@ -142,13 +140,28 @@ class KiCostOptions(VariantOptions):
def run(self, name):
super().run(name)
# Make sure the XML is there.
# Currently we only support the XML mechanism.
netlist = GS.sch_no_ext+'.xml'
if not isfile(netlist):
logger.error('Missing netlist in XML format `{}`'.format(netlist))
logger.error('You can generate it using the `update_xml` pre-flight')
exit(BOM_ERROR)
net_dir = None
if self._comps and GS.ki5():
var_fields = set(['variant', 'version'])
if self.variant and self.variant.type == 'kicost' and self.variant.variant_field not in var_fields:
# Warning about KiCost limitations
logger.warning(W_KICOSTFLD+'KiCost variant `{}` defines `variant_field` as `{}`, not supported by KiCost'.
format(self.variant, self.variant.variant_field))
if self.kicost_variant:
logger.warning(W_MIXVARIANT+'Avoid using KiCost variants and internal variants on the same output')
# Write a custom netlist to a temporal dir
net_dir = mkdtemp(prefix='tmp-kibot-kicost-')
netlist = os.path.join(net_dir, GS.sch_basename+'.xml')
with open(netlist, 'wt') as f:
GS.sch.save_netlist(f, self._comps, no_field=var_fields)
else:
# Make sure the XML is there.
# Currently we only support the XML mechanism.
netlist = GS.sch_no_ext+'.xml'
if not isfile(netlist):
logger.error('Missing netlist in XML format `{}`'.format(netlist))
logger.error('You can generate it using the `update_xml` pre-flight')
exit(BOM_ERROR)
# Check KiCost is available
cmd_kicost = abspath(join(dirname(__file__), KICOST_SUBMODULE))
if not isfile(cmd_kicost):
@ -204,6 +217,10 @@ class KiCostOptions(VariantOptions):
if e.output:
logger.debug('Output from command: '+e.output.decode())
exit(BOM_ERROR)
finally:
if net_dir:
logger.debug('Removing temporal variant dir `{}`'.format(net_dir))
rmtree(net_dir)
logger.debug('Output from command:\n'+cmd_output_dec+'\n')

View File

@ -31,10 +31,12 @@ class KiCost(BaseVariant): # noqa: F821
self.variant = ''
""" Variants to match (regex) """
self.variant_field = 'variant'
""" Name of the field that stores board variant/s for component """
""" Name of the field that stores board variant/s for component.
Only supported internally, don't use it if you plan to use KiCost """
self.separators = ',;/ '
""" Valid separators for variants in the variant field.
Each character is a valid separator """
Each character is a valid separator.
Only supported internally, don't use it if you plan to use KiCost """
def get_variant_field(self):
''' Returns the name of the field used to determine if the component belongs to teh variant '''