From 0b748e1d9d4402a2fb7085dec325d73c0b5a9ace Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Fri, 10 Feb 2023 09:12:59 -0300 Subject: [PATCH] [Added] Basic support reading and writing v7 schematics --- kibot/__main__.py | 6 +- kibot/kicad/v6_sch.py | 472 ++++++++++++------ tests/board_samples/kicad_7/test_v5.kicad_sch | 8 +- tests/utils/context.py | 2 +- 4 files changed, 320 insertions(+), 168 deletions(-) diff --git a/kibot/__main__.py b/kibot/__main__.py index 0eade1fa..9c33d5fb 100644 --- a/kibot/__main__.py +++ b/kibot/__main__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2020-2022 Salvador E. Tropea -# Copyright (c) 2020-2022 Instituto Nacional de Tecnología Industrial +# Copyright (c) 2020-2023 Salvador E. Tropea +# Copyright (c) 2020-2023 Instituto Nacional de Tecnología Industrial # Copyright (c) 2018 John Beard # License: GPL-3.0 # Project: KiBot (formerly KiPlot) @@ -83,7 +83,6 @@ from . import __version__, __copyright__, __license__ from . import log log.set_domain('kibot') logger = log.init() -from . import dep_downloader from .docopt import docopt # GS will import pcbnew, so we must solve the nightly setup first # Check if we have to run the nightly KiCad build @@ -99,6 +98,7 @@ if os.environ.get('KIAUS_USE_NIGHTLY'): # pragma: no cover (nightly) os.environ['PYTHONPATH'] = pcbnew_path nightly = True from .gs import GS +from . import dep_downloader from .misc import EXIT_BAD_ARGS, W_VARCFG, NO_PCBNEW_MODULE, W_NOKIVER, hide_stderr, TRY_INSTALL_CHECK, W_ONWIN from .pre_base import BasePreFlight from .error import KiPlotConfigurationError, config_error diff --git a/kibot/kicad/v6_sch.py b/kibot/kicad/v6_sch.py index 8b739949..ef222b87 100644 --- a/kibot/kicad/v6_sch.py +++ b/kibot/kicad/v6_sch.py @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2021-2022 Salvador E. Tropea -# Copyright (c) 2021-2022 Instituto Nacional de Tecnología Industrial +# Copyright (c) 2021-2023 Salvador E. Tropea +# Copyright (c) 2021-2023 Instituto Nacional de Tecnología Industrial # License: GPL-3.0 # Project: KiBot (formerly KiPlot) """ -KiCad v6 Schematic format. +KiCad v6/7 Schematic format. A basic implementation of the .kicad_sch file format. -Currently oriented to collect the components for the BoM. +Currently oriented to collect the components for the BoM and create a variant of the SCH Documentation: https://dev-docs.kicad.org/en/file-formats/sexpr-schematic/ """ # Encapsulate file/line @@ -22,6 +22,10 @@ from .sexpdata import load, SExpData, Symbol, dumps, Sep logger = log.get_logger() CROSSED_LIB = 'kibot_crossed' NO_YES = ['no', 'yes'] +version = None +KICAD_7_VER = 20230121 +SHEET_FILE = {'Sheet file', 'Sheetfile'} +SHEET_NAME = {'Sheet name', 'Sheetname'} def _check_is_symbol_list(e, allow_orphan_symbol=()): @@ -372,7 +376,9 @@ class Stroke(object): def write(self): data = [_symbol('width', [self.width])] data.append(_symbol('type', [Symbol(self.type)])) - data.append(self.color.write()) + c = self.color + if version < KICAD_7_VER or c.r+c.g+c.b+c.a != 0: + data.append(c.write()) return _symbol('stroke', data) @@ -723,24 +729,37 @@ class SchematicFieldV6(object): self.hide = False @staticmethod - def parse(items): - if len(items) != 6: - _check_len_total(items, 5, 'property') + def parse(items, number): field = SchematicFieldV6() - field.name = _check_str(items, 1, 'field name') - field.value = _check_str(items, 2, 'field value') - field.number = _get_id(items, 3, 'field id') - field.x, field.y, field.ang = _get_at(items, 4, 'field') - if len(items) > 5: - field.effects = _get_effects(items, 5, 'field') - else: - field.effects = None + name = 'field' + field.name = _check_str(items, 1, name+' name') + field.value = _check_str(items, 2, name+' value') + # Default values + field.number = number + field.effects = None + found_at = False + for c, i in enumerate(items[3:]): + i_type = _check_is_symbol_list(i) + if i_type == 'at': + field.x, field.y, field.ang = _get_at(items, c+3, name) + found_at = True + elif i_type == 'effects': + field.effects = FontEffects.parse(i) + elif i_type == 'id': + field.number = _check_integer(i, 1, name+' id') + else: + raise SchError('Unknown property attribute `{}`'.format(i)) + if not found_at: + raise SchError('Missing position for property `{}`'.format(field.name)) return field def write(self): if self.number < 0: return None - data = [self.name, self.value, _symbol('id', [self.number])] + data = [self.name, self.value] + if version < KICAD_7_VER: + # Removed in KiCad 7 + data.append(_symbol('id', [self.number])) data.append(_symbol('at', [self.x, self.y, self.ang])) if self.effects: data.extend([Sep(), self.effects.write(), Sep()]) @@ -811,6 +830,7 @@ class LibComponent(object): comp.pins = [] comp.all_pins = [] comp.unit_count = 1 + field_id = 0 # Variable list for i in c[2:]: i_type = _check_is_symbol_list(i) @@ -838,7 +858,8 @@ class LibComponent(object): comp.is_power = True # SYMBOL_PROPERTIES... elif i_type == 'property': - field = SchematicFieldV6.parse(i) + field = SchematicFieldV6.parse(i, field_id) + field_id += 1 comp.fields.append(field) comp.dfields[field.name.lower()] = field # GRAPHIC_ITEMS... @@ -970,6 +991,42 @@ class LibComponent(object): return _symbol('symbol', sdata) +class SymbolInstance(object): + def __init__(self): + super().__init__() + # Doesn't exist on v7 + self.value = None + self.footprint = None + + @staticmethod + def parse(items): + name = 'symbol instance' + instances = [] + for c, _ in enumerate(items[1:]): + v = _check_symbol_value(items, c+1, name, 'path') + instance = SymbolInstance() + instance.path = _check_str(v, 1, name+' path') + instance.reference = _check_symbol_str(v, 2, name, 'reference') + instance.unit = _check_symbol_int(v, 3, name, 'unit') + if len(v) > 4: + # KiCad 6 + instance.value = _check_symbol_str(v, 4, name, 'value') + instance.footprint = _check_symbol_str(v, 5, name, 'footprint') + instances.append(instance) + return instances + + def write(self): + data = [self.path, Sep(), + _symbol('reference', [self.reference]), + _symbol('unit', [self.unit])] + if self.value is not None: + data.append(_symbol('value', [self.value])) + if self.footprint is not None: + data.append(_symbol('footprint', [self.footprint])) + data.append(Sep()) + return _symbol('path', data) + + class SchematicComponentV6(SchematicComponent): def __init__(self): super().__init__() @@ -982,6 +1039,12 @@ class SchematicComponentV6(SchematicComponent): self.mirror = None self.convert = None self.pin_alternates = {} + # KiCad v7: + self.kicad_dnp = None + # Instances classified by project (v7) + self.projects = None + # All instances, by path (v7) + self.all_instances = {} def set_ref(self, ref): self.ref = ref @@ -1028,6 +1091,21 @@ class SchematicComponentV6(SchematicComponent): # Not documented self.pin_alternates[pin_name] = _check_symbol_str(i, 3, name, 'alternate') + def load_project(self, prj): + name = _check_str(prj, 1, 'instance project') + instances = SymbolInstance().parse(prj[1:]) + for i in instances: + self.all_instances[i.path] = i + return name, instances + + def load_instances(self, i): + self.projects = [] + for prj in i[1:]: + i_type = _check_is_symbol_list(prj) + if i_type != 'project': + raise SchError('Found `{}` instead of `project` in symbol instance'.format(i_type)) + self.projects.append(self.load_project(prj)) + @staticmethod def load(c, project, parent): if not isinstance(c, list): @@ -1044,6 +1122,7 @@ class SchematicComponentV6(SchematicComponent): name = 'component' lib_id_found = False at_found = False + field_id = 0 # Variable list for i in c[1:]: @@ -1077,6 +1156,8 @@ class SchematicComponentV6(SchematicComponent): comp.in_bom = _get_yes_no(i, 1, i_type) elif i_type == 'on_board': comp.on_board = _get_yes_no(i, 1, i_type) + elif i_type == 'dnp': + comp.kicad_dnp = _get_yes_no(i, 1, i_type) elif i_type == 'fields_autoplaced': # Not documented comp.fields_autoplaced = True @@ -1084,7 +1165,8 @@ class SchematicComponentV6(SchematicComponent): comp.uuid = _check_symbol(i, 1, name + ' uuid') # SYMBOL_PROPERTIES... elif i_type == 'property': - field = SchematicFieldV6.parse(i) + field = SchematicFieldV6.parse(i, field_id) + field_id += 1 name_lc = field.name.lower() # Add to the global collection if name_lc not in parent.fields_lc: @@ -1099,6 +1181,20 @@ class SchematicComponentV6(SchematicComponent): # PINS... elif i_type == 'pin': comp.load_pin(i, name) + # KiCad v7 instances + elif i_type == 'instances': + comp.load_instances(i) + # We should get an instance for us + ins = comp.all_instances.get(parent.get_full_path()) + if ins is None: + raise SchError('Missing {} symbol instance for `{}`'.format(comp.name, parent.get_full_path())) + # Translate the instance to the v6 format (remove UUID for / and add the component UUID) + v6_ins = SymbolInstance() + v6_ins.path = os.path.join('/', '/'.join(ins.path.split('/')[2:]), comp.uuid) + v6_ins.reference = ins.reference + v6_ins.unit = ins.unit + # Add to the root symbol_instances, so we reconstruct it + parent.symbol_instances.append(v6_ins) else: raise SchError('Unknown component attribute `{}`'.format(i)) if not lib_id_found or not at_found: @@ -1129,6 +1225,8 @@ class SchematicComponentV6(SchematicComponent): data.append(Sep()) data.append(_symbol('in_bom', [Symbol(NO_YES[self.in_bom])])) data.append(_symbol('on_board', [Symbol(NO_YES[self.on_board])])) + if self.kicad_dnp is not None: + data.append(_symbol('dnp', [Symbol(NO_YES[self.kicad_dnp])])) if self.fields_autoplaced: data.append(_symbol('fields_autoplaced')) data.append(Sep()) @@ -1143,6 +1241,14 @@ class SchematicComponentV6(SchematicComponent): if alternate: pin_data.append(_symbol('alternate', [alternate])) data.extend([_symbol('pin', pin_data), Sep()]) + if self.projects is not None: + prj_data = [Sep()] + for prj, ins in self.projects: + ins_data = [prj, Sep()] + for i in ins: + ins_data.extend([i.write(), Sep()]) + prj_data.extend([_symbol('project', ins_data), Sep()]) + data.extend([_symbol('instances', prj_data), Sep()]) return _symbol('symbol', data) @@ -1267,11 +1373,11 @@ class SchematicBitmapV6(object): class Text(object): @staticmethod def parse(items, name): - _check_len_total(items, 5, name) text = Text() text.name = name text.text = _check_str(items, 1, name) text.pos_x, text.pos_y, text.ang = _get_at(items, 2, name) + text.effects = _get_effects(items, 3, name) text.uuid = _get_uuid(items, 4, name) return text @@ -1294,61 +1400,57 @@ class GlobalLabel(object): self.effects = None self.uuid = None self.properties = [] + self.name = 'global_label' - @staticmethod - def parse(items): - label = GlobalLabel() - label.text = _check_str(items, 1, 'global_label') + @classmethod + def parse(cls, items): + label = cls() + label.text = _check_str(items, 1, label.name) + field_id = 0 for c, i in enumerate(items[2:]): i_type = _check_is_symbol_list(i) if i_type == 'shape': - label.shape = _check_symbol(i, 1, i_type) + label.shape = _check_symbol(i, 1, label.name+' '+i_type) elif i_type == 'fields_autoplaced': label.fields_autoplaced = True elif i_type == 'at': - label.pos_x, label.pos_y, label.ang = _get_at(items, c+2, 'global_label') + label.pos_x, label.pos_y, label.ang = _get_at(items, c+2, label.name) elif i_type == 'effects': label.effects = FontEffects.parse(i) elif i_type == 'uuid': - label.uuid = _get_uuid(items, c+2, 'global_label') + label.uuid = _get_uuid(items, c+2, label.name) elif i_type == 'property': - label.properties.append(SchematicFieldV6.parse(i)) + label.properties.append(SchematicFieldV6.parse(i, field_id)) + field_id += 1 else: - raise SchError('Unknown label attribute `{}`'.format(i)) + raise SchError('Unknown {} attribute `{}`'.format(label.name, i)) return label def write(self): - data = [self.text, - _symbol('shape', [Symbol(self.shape)]), - _symbol('at', [self.pos_x, self.pos_y, self.ang])] + data = [self.text] + if self.shape is not None: + data.append(_symbol('shape', [Symbol(self.shape)])) + data.append(_symbol('at', [self.pos_x, self.pos_y, self.ang])) if self.fields_autoplaced: data.append(_symbol('fields_autoplaced', [])) - data.extend([Sep(), self.effects.write(), Sep(), _symbol('uuid', [Symbol(self.uuid)]), Sep()]) + if self.effects is not None: + data.extend([Sep(), self.effects.write()]) + data.extend([Sep(), _symbol('uuid', [Symbol(self.uuid)]), Sep()]) for p in self.properties: data.extend([p.write(), Sep()]) - return _symbol('global_label', data) + return _symbol(self.name, data) -class HierarchicalLabel(object): - @staticmethod - def parse(items): - name = 'hierarchical_label' - _check_len_total(items, 6, name) - label = HierarchicalLabel() - label.text = _check_str(items, 1, name) - label.shape = _check_symbol(items[2], 1, 'shape') - label.pos_x, label.pos_y, label.ang = _get_at(items, 3, name) - label.effects = _get_effects(items, 4, name) - label.uuid = _get_uuid(items, 5, name) - return label +class Label(GlobalLabel): + def __init__(self): + super().__init__() + self.name = 'label' - def write(self): - data = [self.text, - _symbol('shape', [Symbol(self.shape)]), - _symbol('at', [self.pos_x, self.pos_y, self.ang]), Sep(), - self.effects.write(), Sep(), - _symbol('uuid', [Symbol(self.uuid)]), Sep()] - return _symbol('hierarchical_label', data) + +class HierarchicalLabel(GlobalLabel): + def __init__(self): + super().__init__() + self.name = 'hierarchical_label' class HSPin(object): @@ -1375,82 +1477,6 @@ class HSPin(object): return _symbol('pin', data) -class Sheet(object): - def __init__(self): - super().__init__() - self.pos_x = self.pos_y = self.ang = 0 - self.w = self.h = 0 - self.fields_autoplaced = False - self.stroke = self.fill = self.uuid = None - self.properties = [] - self.name = self.file = '' - self.pins = [] - self.sch = None - - @staticmethod - def parse(items): - sheet = Sheet() - for c, i in enumerate(items[1:]): - i_type = _check_is_symbol_list(i) - if i_type == 'at': - sheet.pos_x, sheet.pos_y, sheet.ang = _get_at(items, c+1, 'sheet') - elif i_type == 'size': - sheet.w = _check_float(i, 1, 'sheet width') - sheet.h = _check_float(i, 2, 'sheet height') - elif i_type == 'fields_autoplaced': - sheet.fields_autoplaced = True - elif i_type == 'stroke': - sheet.stroke = Stroke.parse(i) - elif i_type == 'fill': - sheet.fill = Fill.parse(i) - elif i_type == 'uuid': - sheet.uuid = _get_uuid(items, c+1, 'sheet') - elif i_type == 'property': - field = SchematicFieldV6.parse(i) - sheet.properties.append(field) - if field.name == 'Sheet name': - sheet.name = field.value - elif field.name == 'Sheet file': - sheet.file = field.value - else: - logger.warning(W_UNKFLD+"Unknown sheet property `{}` ({})".format(field.name, field.value)) - elif i_type == 'pin': - sheet.pins.append(HSPin.parse(i)) - else: - raise SchError('Unknown sheet attribute `{}`'.format(i)) - return sheet - - def load_sheet(self, project, parent_file, parent_obj): - assert self.name - sheet = SchematicV6() - self.sheet = sheet - parent_dir = os.path.dirname(parent_file) - 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.sheet_path] = sheet - sheet.load(os.path.join(parent_dir, self.file), project, parent_obj) - return sheet - - def write(self, cross=False): - data = [_symbol('at', [self.pos_x, self.pos_y]), - _symbol('size', [self.w, self.h])] - if self.fields_autoplaced: - data.append(_symbol('fields_autoplaced', [])) - data.extend([Sep(), self.stroke.write(), Sep(), - self.fill.write(), Sep(), - _symbol('uuid', [Symbol(self.uuid)]), Sep()]) - for p in self.properties: - change_file = cross and p.name == 'Sheet file' - if change_file: - p.value = self.flat_file - data.extend([p.write(), Sep()]) - if change_file: - p.value = self.file - for p in self.pins: - data.extend([p.write(), Sep()]) - return _symbol('sheet', data) - - class SheetInstance(object): @staticmethod def parse(items): @@ -1468,29 +1494,121 @@ class SheetInstance(object): return _symbol('path', [self.path, _symbol('page', [self.page])]) -class SymbolInstance(object): +class Sheet(object): + def __init__(self): + super().__init__() + self.pos_x = self.pos_y = self.ang = 0 + self.w = self.h = 0 + self.fields_autoplaced = False + self.stroke = self.fill = self.uuid = None + self.properties = [] + self.name = self.file = '' + self.pins = [] + self.sch = None + # KiCad v7: + # Instances classified by project + self.projects = None + # All instances, by path (page look-up) + self.all_instances = {} + + def load_project(self, prj): + name = _check_str(prj, 1, 'instance project') + instances = SheetInstance().parse(prj[1:]) + for i in instances: + self.all_instances[i.path] = i.page + return name, instances + + def load_instances(self, i): + self.projects = [] + for prj in i[1:]: + i_type = _check_is_symbol_list(prj) + if i_type != 'project': + raise SchError('Found `{}` instead of `project` in sheet instance'.format(i_type)) + self.projects.append(self.load_project(prj)) + @staticmethod def parse(items): - name = 'symbol instance' - instances = [] - for c, _ in enumerate(items[1:]): - v = _check_symbol_value(items, c+1, name, 'path') - instance = SymbolInstance() - instance.path = _check_str(v, 1, name+' path') - instance.reference = _check_symbol_str(v, 2, name, 'reference') - instance.unit = _check_symbol_int(v, 3, name, 'unit') - instance.value = _check_symbol_str(v, 4, name, 'value') - instance.footprint = _check_symbol_str(v, 5, name, 'footprint') - instances.append(instance) - return instances + sheet = Sheet() + field_id = 0 + for c, i in enumerate(items[1:]): + i_type = _check_is_symbol_list(i) + if i_type == 'at': + sheet.pos_x, sheet.pos_y, sheet.ang = _get_at(items, c+1, 'sheet') + elif i_type == 'size': + sheet.w = _check_float(i, 1, 'sheet width') + sheet.h = _check_float(i, 2, 'sheet height') + elif i_type == 'fields_autoplaced': + sheet.fields_autoplaced = True + elif i_type == 'stroke': + sheet.stroke = Stroke.parse(i) + elif i_type == 'fill': + sheet.fill = Fill.parse(i) + elif i_type == 'uuid': + sheet.uuid = _get_uuid(items, c+1, 'sheet') + elif i_type == 'property': + field = SchematicFieldV6.parse(i, field_id) + field_id += 1 + sheet.properties.append(field) + if field.name in SHEET_NAME: + sheet.name = field.value + elif field.name in SHEET_FILE: + sheet.file = field.value + else: + logger.warning(W_UNKFLD+"Unknown sheet property `{}` ({})".format(field.name, field.value)) + elif i_type == 'pin': + sheet.pins.append(HSPin.parse(i)) + elif i_type == 'instances': + sheet.load_instances(i) + else: + raise SchError('Unknown sheet attribute `{}`'.format(i)) + return sheet - def write(self): - data = [self.path, Sep(), - _symbol('reference', [self.reference]), - _symbol('unit', [self.unit]), - _symbol('value', [self.value]), - _symbol('footprint', [self.footprint]), Sep()] - return _symbol('path', data) + def load_sheet(self, project, parent_file, parent_obj): + assert self.name + sheet = SchematicV6() + self.sheet = sheet + parent_dir = os.path.dirname(parent_file) + 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.sheet_path] = sheet + sheet.load(os.path.join(parent_dir, self.file), project, parent_obj) + # self.sheet_paths + if self.projects is not None: + # KiCad v7 sheet pages are here + page = self.all_instances.get(parent_obj.get_full_path()) + if page is None: + raise SchError('Missing sheet instance for `{}`'.format(parent_obj.get_full_path())) + else: + sheet.sheet = page + parent_obj.all_sheets.append(sheet) + return sheet + + def write(self, cross=False): + data = [_symbol('at', [self.pos_x, self.pos_y]), + _symbol('size', [self.w, self.h])] + if self.fields_autoplaced: + data.append(_symbol('fields_autoplaced', [])) + data.extend([Sep(), self.stroke.write(), Sep(), + self.fill.write(), Sep(), + _symbol('uuid', [Symbol(self.uuid)]), Sep()]) + for p in self.properties: + change_file = cross and p.name in SHEET_FILE + if change_file: + p.value = self.flat_file + data.extend([p.write(), Sep()]) + if change_file: + p.value = self.file + for p in self.pins: + data.extend([p.write(), Sep()]) + if self.projects is not None: + prj_data = [Sep()] + for prj, ins in self.projects: + ins_data = [prj, Sep()] + for i in ins: + ins_data.extend([i.write(), Sep()]) + prj_data.extend([_symbol('project', ins_data), Sep()]) + data.extend([_symbol('instances', prj_data), Sep()]) + return _symbol('sheet', data) # Here because we have al s-expr tools here @@ -1680,6 +1798,14 @@ class SchematicV6(Schematic): if base_sheet is None: # We are the base sheet base_sheet = self + # Copy potentially modified data from components + for s in self.symbol_instances: + comp = s.component + s.reference = comp.ref + if s.value is not None: + s.value = comp.value + if s.footprint is not None: + s.footprint = comp.footprint_lib+':'+comp.footprint if comp.footprint_lib else comp.footprint if saved is None: # Start memorizing saved files saved = set() @@ -1739,14 +1865,8 @@ class SchematicV6(Schematic): # Sheet instances _add_items_list('sheet_instances', self.sheet_instances, sch) # Symbol instances - # Copy potentially modified data from components - if base_sheet == self: - for s in self.symbol_instances: - comp = s.component - s.reference = comp.ref - s.value = comp.value - s.footprint = comp.footprint_lib+':'+comp.footprint if comp.footprint_lib else comp.footprint - _add_items_list('symbol_instances', self.symbol_instances, sch) + if version < KICAD_7_VER: + _add_items_list('symbol_instances', self.symbol_instances, sch) logger.debug('Saving schematic: `{}`'.format(fname)) # Keep a back-up of existing files if os.path.isfile(fname): @@ -1785,6 +1905,14 @@ class SchematicV6(Schematic): self.title_ori = title return old_title + def get_full_path(self): + """ Path using the UUID of the root. Used by v7 """ + path = '/'+self.root_sheet.uuid + # Avoid returning /UUID/ + if self.sheet_path != '/': + path += self.sheet_path + return path + def load(self, fname, project, parent=None): # noqa: C901 """ Load a v6.x KiCad Schematic. The caller must be sure the file exists. @@ -1798,13 +1926,19 @@ class SchematicV6(Schematic): self.sheet_path = '/' self.sheet_path_h = '/' self.sheet_names = {} + self.all_sheets = [] + self.root_sheet = self + self.symbol_instances = [] else: self.fields = parent.fields self.fields_lc = parent.fields_lc self.sheet_paths = parent.sheet_paths self.lib_symbol_names = parent.lib_symbol_names + # self.sheet_path/_h is set by sch.load_sheet self.sheet_names = parent.sheet_names - # self.sheet_path is set by sch.load_sheet + self.all_sheets = parent.all_sheets + self.root_sheet = parent.root_sheet + self.symbol_instances = parent.symbol_instances self.parent = parent self.fname = fname self.project = project @@ -1822,7 +1956,6 @@ class SchematicV6(Schematic): self.hlabels = [] self.sheets = [] self.sheet_instances = [] - self.symbol_instances = [] self.bus_alias = [] self.libs = {} # Just for compatibility with v5 class # TODO: this assumes we are expanding the schematic to allow variant. @@ -1830,6 +1963,8 @@ class SchematicV6(Schematic): # 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 = {} + if not os.path.isfile(fname): + raise SchError('Missing subsheet: '+fname) with open(fname, 'rt') as fh: error = None try: @@ -1845,6 +1980,8 @@ class SchematicV6(Schematic): obj = None if e_type == 'version': self.version = _check_integer(e, 1, e_type) + global version + version = self.version elif e_type == 'generator': self.generator = _check_symbol(e, 1, e_type) elif e_type == 'uuid': @@ -1879,7 +2016,10 @@ class SchematicV6(Schematic): elif e_type == 'text': self.texts.append(Text.parse(e, e_type)) elif e_type == 'label': - self.labels.append(Text.parse(e, e_type)) + if self.version < KICAD_7_VER: + self.labels.append(Text.parse(e, e_type)) + else: + self.labels.append(Label.parse(e)) elif e_type == 'global_label': self.glabels.append(GlobalLabel.parse(e)) elif e_type == 'hierarchical_label': @@ -1908,7 +2048,10 @@ class SchematicV6(Schematic): # Here we finished for sub-sheets return # On the main sheet analyze the sheet and symbol instances - self.all_sheets = [] + # Solve the sheet pages: assign the page numbers. + # KiCad 6: for all pages + # KiCad 7: only for /, the rest are already assigned + # We use the page number for the netlist generation for i in self.sheet_instances: sheet = self.sheet_paths.get(i.path) if sheet: @@ -1925,8 +2068,11 @@ class SchematicV6(Schematic): # Transfer the instance data comp.set_ref(s.reference) comp.unit = s.unit - comp.set_value(s.value) - comp.set_footprint(s.footprint) + # Value and footprint were available in v6, but they were just copies, not really used + if s.value is not None: + comp.set_value(s.value) + if s.footprint is not None: + comp.set_footprint(s.footprint) comp.sheet_path = path comp.sheet_path_h = self.path_to_human(path) comp.id = comp_uuid diff --git a/tests/board_samples/kicad_7/test_v5.kicad_sch b/tests/board_samples/kicad_7/test_v5.kicad_sch index 389f4349..569e45a8 100644 --- a/tests/board_samples/kicad_7/test_v5.kicad_sch +++ b/tests/board_samples/kicad_7/test_v5.kicad_sch @@ -1628,9 +1628,15 @@ (effects (font (size 1.27 1.27)) (justify left bottom)) (uuid f144a97d-c3f0-423f-b0a9-3f7dbc42478b) ) - (label "B{AELEM}" (at 165.1 88.9 0) (fields_autoplaced) + (label "B{AELEM}" (at 165.1 88.9 0) (effects (font (size 1.27 1.27)) (justify left bottom)) (uuid fc80fa5b-8c07-4dda-8002-331dcafd556b) + (property "Netclass" "a net class" (at 165.1 90.17 0) + (effects (font (size 1.27 1.27) italic) (justify left)) + ) + (property "Something" "pp2" (at 165.1 92.71 0) + (effects (font (size 1.27 1.27) italic) (justify left)) + ) ) (global_label "L2B" (shape output) (at 39.37 55.88 90) (fields_autoplaced) diff --git a/tests/utils/context.py b/tests/utils/context.py index 86687f56..b4e29476 100644 --- a/tests/utils/context.py +++ b/tests/utils/context.py @@ -45,7 +45,7 @@ kicad_minor = int(m.group(2)) kicad_patch = int(m.group(3)) kicad_version = kicad_major*1000000+kicad_minor*1000+kicad_patch if kicad_version >= KICAD_VERSION_5_99: - BOARDS_DIR = '../board_samples/kicad_6' + BOARDS_DIR = '../board_samples/kicad_'+str(kicad_major) REF_DIR = 'tests/reference/6_0_8' KICAD_SCH_EXT = '.kicad_sch' # Now these layers can be renamed.