parent
4b3636313c
commit
e5ed4b6652
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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=" "))
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 '''
|
||||
|
|
|
|||
Loading…
Reference in New Issue