Added KiCad 6 support to the pseudo-netlist generation.

This commit is contained in:
Salvador E. Tropea 2021-12-31 16:45:40 -03:00
parent 1eefdeb5a3
commit 7155539777
3 changed files with 185 additions and 70 deletions

View File

@ -541,6 +541,7 @@ class LibComponent(object):
self.alias = None
self.fp_list = []
self.draw = []
self.pins = []
line = f.get_line()
while not line.startswith('ENDDEF'):
if len(line) == 0:
@ -573,11 +574,20 @@ class LibComponent(object):
elif line[0] == 'T':
self.draw.append(DrawText.parse(line))
elif line[0] == 'X':
self.draw.append(Pin.parse(line))
pin = Pin.parse(line)
self.draw.append(pin)
self.pins.append(pin)
else:
logger.warning(W_BADDRAW + 'Unknown draw element `{}`'.format(line))
line = f.get_line()
line = f.get_line()
self.all_pins = self.pins
def get_field_value(self, field):
field = field.lower()
if field in self.dfields:
return self.dfields[field].value
return ''
def write(self, f, id, cross=False):
""" cross is used to cross the component (DNF) """
@ -1384,15 +1394,17 @@ class SchematicSheet(object):
self.id = ''
self.annotation_error = False
def load_sheet(self, project, parent, sheet_path, sheet_path_h, libs, fields, fields_lc):
def load_sheet(self, project, parent, sheet_path, sheet_path_h, libs, fields, fields_lc, parent_obj):
assert self.name
self.sheet = Schematic()
parent_obj.all_sheets.append(self.sheet)
parent_dir = os.path.dirname(parent)
sheet_path += '/'+self.id
if len(sheet_path_h) > 1:
sheet_path_h += '/'
sheet_path_h += self.name if self.name else 'Unknown'
self.sheet.load(os.path.join(parent_dir, self.file), project, sheet_path, sheet_path_h, libs, fields, fields_lc)
self.sheet.load(os.path.join(parent_dir, self.file), project, sheet_path, sheet_path_h, libs, fields, fields_lc,
parent_obj)
return self.sheet
@staticmethod
@ -1457,6 +1469,14 @@ class SchematicSheet(object):
f.write('$EndSheet\n')
def _path(p):
if not p.startswith('/'):
p = '/'+p
if p[-1] != '/':
p = p+'/'
return p
class Schematic(object):
def __init__(self):
super().__init__()
@ -1464,6 +1484,7 @@ class Schematic(object):
self.lib_comps = {}
self.annotation_error = False
self.max_comments = 4
self.netlist_version = 'D'
def _get_title_block(self, f):
line = f.get_line()
@ -1502,7 +1523,7 @@ class Schematic(object):
raise SchFileError('Wrong entry in title block', line, f)
self.title_block[m.group(1)] = m.group(2)
def load(self, fname, project, sheet_path='', sheet_path_h='/', libs={}, fields=[], fields_lc=set()):
def load(self, fname, project, sheet_path='', sheet_path_h='/', libs={}, fields=[], fields_lc=set(), parent=None):
""" Load a v5.x KiCad Schematic.
The caller must be sure the file exists.
Only the schematics are loaded not the libs. """
@ -1581,12 +1602,16 @@ class Schematic(object):
line = f.get_line()
# Load sub-sheets
self.sub_sheets = []
if parent is None:
self.all_sheets = [self]
for sch in self.sheets:
sheet = sch.load_sheet(project, fname, sheet_path, sheet_path_h, libs, fields, fields_lc)
sheet = sch.load_sheet(project, fname, sheet_path, sheet_path_h, libs, fields, fields_lc,
self if parent is None else parent)
if sheet.annotation_error:
self.annotation_error = True
self.sub_sheets.append(sheet)
def get_files(self):
""" A list of the names for all the sheets, including this one.
We avoid repeating the same file. """
@ -1765,68 +1790,108 @@ class Schematic(object):
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, 'date').text = datetime.now().strftime("%c")
SubElement(design, 'tool').text = 'KiBot v'+GS.kibot_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')
for num in range(self.max_comments):
com.set('number', str(num+1))
com.set('value', self.comment[num])
order = 1
is_v5 = self.max_comments == 4
for s in self.all_sheets:
sheet = SubElement(design, 'sheet')
# KiCad v5 numbering is broken
sheet.set('number', str(order) if is_v5 else s.sheet)
sheet.set('name', _path(s.sheet_path_h))
sheet.set('tstamps', _path(s.sheet_path))
tblock = SubElement(sheet, 'title_block')
title = SubElement(tblock, 'title')
if s.title_ori:
title.text = s.title_ori
company = SubElement(tblock, 'company')
if s.company:
company.text = s.company
rev = SubElement(tblock, 'rev')
if s.revision:
rev.text = s.revision
dt = SubElement(tblock, 'date')
if s.date:
dt.text = s.date
SubElement(tblock, 'source').text = os.path.basename(s.fname)
for num in range(s.max_comments):
com = SubElement(tblock, 'comment')
com.set('number', str(num+1))
com.set('value', s.comment[num])
order += 1
def save_netlist_components(self, root, comps, excluded, fitted, no_field):
""" Generates the `components` section of the netlist """
components = SubElement(root, 'components')
# Colapse units
real_comps = []
tstamps = {}
for c in comps:
if c.ref in tstamps:
tstamps[c.ref] += ' '+c.id
else:
tstamps[c.ref] = c.id
real_comps.append(c)
for c in real_comps:
if not excluded and not c.included:
# Excluded, i.e. virtual
continue
if fitted and not c.fitted:
# DNP
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
if c.footprint:
SubElement(comp, 'footprint').text = c.footprint
if len(c.datasheet) and not (self.netlist_version == 'E' and c.datasheet != '~'):
SubElement(comp, 'datasheet').text = c.datasheet
user_fields = c.get_user_fields()
if user_fields:
fields = SubElement(comp, 'fields')
for fname, fvalue in 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)
# v6 properties
if self.netlist_version == 'E':
for fname, fvalue in user_fields:
if fname in no_field:
continue
fld = SubElement(comp, 'property')
fld.set('name', fname)
fld.set('value', fvalue)
prop = SubElement(comp, 'property')
prop.set('name', 'Sheetname')
prop.set('value', os.path.basename(c.sheet_path_h))
prop = SubElement(comp, 'property')
prop.set('name', 'Sheetfile')
prop.set('value', os.path.basename(c.parent_sheet.fname))
shp = SubElement(comp, 'sheetpath')
shp.set('names', c.sheet_path_h)
shp.set('tstamps', c.sheet_path_h)
SubElement(comp, 'tstamp').text = c.id
shp.set('names', _path(c.sheet_path_h))
shp.set('tstamps', _path(c.sheet_path))
if self.netlist_version == 'D':
SubElement(comp, 'tstamp').text = tstamps[c.ref].split()[0]
else:
SubElement(comp, 'tstamps').text = tstamps[c.ref]
def save_netlist_libparts(self, root):
libparts = SubElement(root, 'libparts')
for k, v in self.comps_data.items():
for k in sorted(self.comps_data.keys()):
v = self.comps_data[k]
if not v:
continue
ref = v.get_field_value('reference')
if ref and ref[0] == '#':
continue
libpart = SubElement(libparts, 'libpart')
res = k.split(':')
if res:
@ -1836,35 +1901,59 @@ class Schematic(object):
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
# Description
desc = None
if v.dcm and v.dcm.desc:
desc = v.dcm.desc
else:
desc = v.get_field_value('ki_description')
if desc:
SubElement(libpart, 'description').text = desc
# Datatsheet
datasheet = None
if v.dcm and v.dcm.datasheet:
datasheet = v.dcm.datasheet
else:
datasheet = v.get_field_value('datasheet')
if datasheet:
SubElement(libpart, 'docs').text = datasheet
# Footprint filters
fp_list = None
if v.fp_list:
fp_list = v.fp_list
else:
fp_list = v.get_field_value('ki_fp_filters').split()
if fp_list:
fps = SubElement(libpart, 'footprints')
for fp in v.fp_list:
for fp in fp_list:
SubElement(fps, 'fp').text = fp
# Fields
flds = SubElement(libpart, 'fields')
for fld in v.fields:
if not fld.value:
if not fld.value or fld.name.startswith('ki_'):
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
if v.all_pins:
pins = SubElement(libpart, 'pins')
for pin in sorted(filter(lambda x: isinstance(x, Pin), v.draw), key=lambda x: x.number):
for pin in sorted(v.all_pins, key=lambda x: "%10s" % 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'))
name = pin.name
if self.netlist_version == 'E' and name == '~':
name = ''
pn.set('name', name)
tp = pin.type
if len(tp) == 1:
tp = pin.type2name.get(pin.type, 'unknown')
pn.set('type', tp)
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')
root.set('version', self.netlist_version)
# Design section
self.save_netlist_design(root)
# Components
@ -1881,6 +1970,6 @@ class Schematic(object):
# TODO: May be in the future
SubElement(root, 'nets')
# Make it look nice
rough_string = tostring(root, 'utf-8')
rough_string = tostring(root, encoding='utf8')
reparsed = minidom.parseString(rough_string)
fhandle.write(reparsed.toprettyxml(indent=" "))
fhandle.write(reparsed.toprettyxml(indent=" ", encoding='UTF-8'))

View File

@ -719,12 +719,21 @@ class LibComponent(object):
self.fields = []
self.dfields = {}
self.box = Box()
self.alias = None
self.dcm = None
self.fp_list = None
# This member is used to generate crossed components (DNF).
# When defined means we need to add a cross in this box and then reset the box.
self.cross_box = None
def get_field_value(self, field):
field = field.lower()
if field in self.dfields:
return self.dfields[field].value
return ''
@staticmethod
def load(c, project, is_unit=False): # noqa: C901
def load(c, project, parent=None): # noqa: C901
if not isinstance(c, list):
raise SchError('Library component definition is not a list')
if len(c) < 3:
@ -740,12 +749,12 @@ class LibComponent(object):
if len(res) == 2:
comp.name = res[1]
comp.lib = res[0]
# libs[comp.lib] = None
else:
if not is_unit:
if parent is None:
logger.warning(W_NOLIB + "Component `{}` doesn't specify its library".format(comp.name))
comp.units = []
comp.pins = []
comp.all_pins = []
comp.unit_count = 1
# Variable list
for i in c[2:]:
@ -799,6 +808,8 @@ class LibComponent(object):
elif i_type == 'pin':
vis_obj = PinV6.parse(i)
comp.pins.append(vis_obj)
if parent:
parent.all_pins.append(vis_obj)
# UNITS...
elif i_type == 'symbol':
# They use a special naming scheme:
@ -810,7 +821,7 @@ class LibComponent(object):
# - If the unit has alternative drawing they are *_N_1 and *_N_2
# - If the unit doesn't have alternative we have *_N_x x starts from 0
# Pins and drawings can be in _N_0 and/or _N_1
vis_obj = LibComponent.load(i, project, is_unit=True)
vis_obj = LibComponent.load(i, project, parent=comp if parent is None else parent)
comp.units.append(vis_obj)
m = LibComponent.unit_regex.search(vis_obj.lib_id)
if m is None:
@ -938,6 +949,7 @@ class SchematicComponentV6(SchematicComponent):
comp.project = project
# The path will be computed by the instance
# comp.sheet_path_h = parent.sheet_path_h
comp.parent_sheet = parent
name = 'component'
# First argument is the LIB:NAME
comp.lib_id = comp.name = _check_symbol_str(c, 1, name, 'lib_id')
@ -946,7 +958,6 @@ class SchematicComponentV6(SchematicComponent):
if len(res) == 2:
comp.name = res[1]
comp.lib = res[0]
# libs[comp.lib] = None
else:
logger.warning(W_NOLIB + "Component `{}` doesn't specify its library".format(comp.name))
# 2 The position
@ -1283,9 +1294,9 @@ class Sheet(object):
sheet = SchematicV6()
self.sheet = sheet
parent_dir = os.path.dirname(parent_file)
sheet.path = os.path.join(parent_obj.path, self.uuid)
sheet.sheet_path = os.path.join(parent_obj.sheet_path, self.uuid)
sheet.sheet_path_h = os.path.join(parent_obj.sheet_path_h, self.name)
parent_obj.sheet_paths[sheet.path] = sheet
parent_obj.sheet_paths[sheet.sheet_path] = sheet
sheet.load(os.path.join(parent_dir, self.file), project, parent_obj)
return sheet
@ -1389,6 +1400,7 @@ class SchematicV6(Schematic):
self.comment = ['']*9
self.max_comments = 9
self.title_ori = self.date_ori = None
self.netlist_version = 'E'
def _fill_missing_title_block(self):
# Fill in some missing info
@ -1529,7 +1541,7 @@ class SchematicV6(Schematic):
Is used to save a variant, where we avoid sharing instance data """
# Store it in the UUID -> name
# Used to create a human readable sheet path
self.sheet_names[os.path.join(self.path, sch.uuid)] = os.path.join(self.sheet_path_h, sch.name)
self.sheet_names[os.path.join(self.sheet_path, sch.uuid)] = os.path.join(self.sheet_path_h, sch.name)
# Eliminate subdirs
file = sch.file.replace('/', '_')
fparts = os.path.splitext(file)
@ -1545,7 +1557,7 @@ class SchematicV6(Schematic):
self.fields_lc = set(self.fields)
self.sheet_paths = {'/': self}
self.lib_symbol_names = {}
self.path = '/'
self.sheet_path = '/'
self.sheet_path_h = '/'
self.sheet_names = {}
else:
@ -1554,7 +1566,7 @@ class SchematicV6(Schematic):
self.sheet_paths = parent.sheet_paths
self.lib_symbol_names = parent.lib_symbol_names
self.sheet_names = parent.sheet_names
# self.path is set by sch.load_sheet
# self.sheet_path is set by sch.load_sheet
self.parent = parent
self.fname = fname
self.project = project
@ -1573,9 +1585,11 @@ class SchematicV6(Schematic):
self.sheets = []
self.sheet_instances = []
self.symbol_instances = []
self.libs = {} # Just for compatibility with v5 class
# TODO: this assumes we are expanding the schematic to allow variant.
# This is needed to overcome KiCad 6 limitations (symbol instances only differ in Reference)
# If we don't want to expand the schematic this member should be shared with the parent
# TODO: We must fix some UUIDs because now we expanded them.
self.symbol_uuids = {}
with open(fname, 'rt') as fh:
error = None
@ -1595,7 +1609,7 @@ class SchematicV6(Schematic):
elif e_type == 'generator':
self.generator = _check_symbol(e, 1, e_type)
elif e_type == 'uuid':
self.uuid = _check_symbol(e, 1, e_type)
self.id = self.uuid = _check_symbol(e, 1, e_type)
elif e_type == 'paper':
self.paper = _check_str(e, 1, e_type)
index = 2
@ -1653,6 +1667,14 @@ class SchematicV6(Schematic):
if sheet.annotation_error:
self.annotation_error = True
sch.sch = sheet
# Assign the page numbers
if parent is None:
self.all_sheets = []
for i in self.sheet_instances:
sheet = self.sheet_paths.get(i.path)
if sheet:
sheet.sheet = i.page
self.all_sheets.append(sheet)
# Create the components list
for s in self.symbol_instances:
# Get a copy of the original symbol
@ -1665,7 +1687,9 @@ class SchematicV6(Schematic):
comp.unit = s.unit
comp.value = s.value
comp.set_footprint(s.footprint)
comp.sheet_path = path
comp.sheet_path_h = self.path_to_human(path)
comp.id = comp_uuid
# Link with its library symbol
try:
lib_symbol = self.lib_symbol_names[comp.lib_id]
@ -1674,7 +1698,9 @@ class SchematicV6(Schematic):
lib_symbol = LibComponent()
comp.lib_symbol = lib_symbol
comp.is_power = lib_symbol.is_power
comp.desc = lib_symbol.get_field_value('ki_description')
# Now we have all the data
comp._validate()
# Add it to the list
self.components.append(comp)
self.comps_data = self.lib_symbol_names

View File

@ -141,7 +141,7 @@ class KiCostOptions(VariantOptions):
def run(self, name):
super().run(name)
net_dir = None
if self._comps and GS.ki5():
if self._comps:
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
@ -152,7 +152,7 @@ class KiCostOptions(VariantOptions):
# 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:
with open(netlist, 'wb') as f:
GS.sch.save_netlist(f, self._comps, no_field=var_fields)
else:
# Make sure the XML is there.