# -*- coding: utf-8 -*- # Copyright (c) 2021 Salvador E. Tropea # Copyright (c) 2021 Instituto Nacional de TecnologĂ­a Industrial # License: GPL-3.0 # Project: KiBot (formerly KiPlot) """ KiCad v6 Schematic format. A basic implementation of the .kicad_sch file format. Currently oriented to collect the components for the BoM. Documentation: https://dev-docs.kicad.org/en/file-formats/sexpr-schematic/ """ # Encapsulate file/line import os import re from collections import OrderedDict from ..gs import GS from .. import log from ..misc import W_NOLIB, W_UNKFLD, W_MISSCMP from .v5_sch import SchError, SchematicComponent, Schematic from .sexpdata import load, SExpData, Symbol, dumps, Sep logger = log.get_logger() CROSSED_LIB = 'kibot_crossed' def _check_is_symbol_list(e, allow_orphan_symbol=[]): # Each entry is a list if not isinstance(e, list): if isinstance(e, Symbol): name = e.value() if name in allow_orphan_symbol: return name raise SchError('Orphan symbol `{}`'.format(e.value())) else: raise SchError('Orphan data `{}`'.format(e)) # The first element is a symbol if not isinstance(e[0], Symbol): raise SchError('Orphan data `{}`'.format(e[0])) return e[0].value() def _check_len(items, pos, name): if len(items) < pos+1: raise SchError('Missing argument {} in `{}`'.format(pos, name)) return items[pos] def _check_len_total(items, num, name): if len(items) != num: raise SchError('Wrong number of attributes for {} `{}`'.format(name, items)) def _check_symbol(items, pos, name): value = _check_len(items, pos, name) if not isinstance(value, Symbol): raise SchError('{} is not a Symbol `{}`'.format(name, value)) return value.value() def _check_hide(items, pos, name): value = _check_symbol(items, pos, name + ' hide') if value != 'hide': raise SchError('Found Symbol `{}` when `hide` expected'.format(value)) return True def _check_integer(items, pos, name): value = _check_len(items, pos, name) if not isinstance(value, int): raise SchError('{} is not an integer `{}`'.format(name, value)) return value def _check_float(items, pos, name): value = _check_len(items, pos, name) if not isinstance(value, (float, int)): raise SchError('{} is not a float `{}`'.format(name, value)) return value def _check_str(items, pos, name): value = _check_len(items, pos, name) if not isinstance(value, str): raise SchError('{} is not a string `{}`'.format(name, value)) return value def _check_symbol_value(items, pos, name, sym): value = _check_len(items, pos, name) if not isinstance(value, list) or not isinstance(value[0], Symbol) or value[0].value() != sym: raise SchError('Missing `{}` in `{}`'.format(sym, name)) return value def _check_symbol_float(items, pos, name, sym): name += ' ' + sym values = _check_symbol_value(items, pos, name, sym) return _check_float(values, 1, name) def _check_symbol_int(items, pos, name, sym): name += ' ' + sym values = _check_symbol_value(items, pos, name, sym) return _check_integer(values, 1, name) def _check_symbol_str(items, pos, name, sym): name += ' ' + sym values = _check_symbol_value(items, pos, name, sym) return _check_str(values, 1, name) def _get_offset(items, pos, name): value = _check_symbol_value(items, pos, name, 'offset') return _check_float(value, 1, 'offset') def _get_yes_no(items, pos, name): sym = _check_symbol(items, pos, name) return sym == 'yes' def _get_id(items, pos, name): value = _check_symbol_value(items, pos, name, 'id') return _check_integer(value, 1, 'id') def _get_at(items, pos, name): value = _check_symbol_value(items, pos, name, 'at') angle = 0 if len(value) > 3: angle = _check_float(value, 3, 'at angle') return _check_float(value, 1, 'at x'), _check_float(value, 2, 'at y'), angle class Point(object): def __init__(self, items): super().__init__() self.x = _check_float(items, 1, 'x coord') self.y = _check_float(items, 2, 'y coord') @staticmethod def parse(items): return Point(items) class PointXY(object): def __init__(self, x, y): super().__init__() self.x = x self.y = y class Box(object): def __init__(self, points=None): self.x1 = self.y1 = self.x2 = self.y2 = 0 self.set = False if points: self.x1 = self.x2 = points[0].x self.y1 = self.y2 = points[0].y for p in points[1:]: self.x1 = min(p.x, self.x1) self.x2 = max(p.x, self.x2) self.y1 = min(p.y, self.y1) self.y2 = max(p.y, self.y2) self.set = True def __str__(self): if not self.set: return "Box *uninitialized*" return "Box({},{} to {},{})".format(self.x1, self.y1, self.x2, self.y2) def diagonal(self, inverse=False): if inverse: return [PointXY(self.x1, self.y2), PointXY(self.x2, self.y1)] return [PointXY(self.x1, self.y1), PointXY(self.x2, self.y2)] def union(self, b): if not self.set: self.x1 = b.x1 self.y1 = b.y1 self.x2 = b.x2 self.y2 = b.y2 self.set = True elif b.set: self.x1 = min(self.x1, b.x1) self.y1 = min(self.y1, b.y1) self.x2 = max(self.x2, b.x2) self.y2 = max(self.y2, b.y2) def _get_xy(items): if len(items) != 3: raise SchError('Point definition with wrong args (`{}`)'.format(items)) return Point.parse(items) def _get_points(items): points = [] for i in items[1:]: i_type = _check_is_symbol_list(i) if i_type == 'xy': points.append(_get_xy(i)) else: raise SchError('Unknown points attribute `{}`'.format(i)) return points class FontEffects(object): """ Class used to describe text attributes """ def __init__(self): super().__init__() self.hide = False self.w = self.h = 1.27 self.thickness = None self.bold = self.italic = False self.hjustify = self.vjustify = 'C' self.mirror = False @staticmethod def parse_font(items): w = h = 1.27 thickness = None bold = italic = False for i in items[1:]: if isinstance(i, Symbol): name = i.value() if name == 'bold': bold = True elif name == 'italic': italic = True else: raise SchError('Unknown font effect attribute `{}`'.format(name)) else: # A list i_type = _check_is_symbol_list(i) if i_type == 'size': h = _check_float(i, 1, 'font height') w = _check_float(i, 2, 'font width') elif i_type == 'thickness': thickness = _check_float(i, 1, 'font thickness') else: raise SchError('Unknown font effect attribute `{}`'.format(i)) return w, h, thickness, bold, italic @staticmethod def parse_justify(items): h = v = 'C' mirror = False for i in items[1:]: if isinstance(i, Symbol): name = i.value() if name == 'left': h = 'L' elif name == 'right': h = 'R' elif name == 'top': v = 'T' elif name == 'bottom': v = 'B' elif name == 'mirror': mirror = True else: raise SchError('Unknown font effect attribute `{}`'.format(name)) else: # A list raise SchError('Unknown font effect attribute `{}`'.format(i)) return h, v, mirror @staticmethod def parse(items): o = FontEffects() for c, i in enumerate(items[1:]): if isinstance(i, Symbol): # Only hide exists o.hide = _check_hide(items, c+1, 'font effect') elif isinstance(i, list): i_type = _check_is_symbol_list(i) if i_type == 'font': o.w, o.h, o.thickness, o.bold, o.italic = FontEffects.parse_font(i) elif i_type == 'justify': o.hjustify, o.vjustify, o.mirror = FontEffects.parse_justify(i) else: raise SchError('Unknown font effect attribute `{}`'.format(i)) return o def write_font(self): data = [_symbol('size', [self.h, self.w])] if self.thickness is not None: data.append(_symbol('thickness', [self.thickness])) if self.bold: data.append(Symbol('bold')) if self.italic: data.append(Symbol('italic')) return _symbol('font', data) def write_justify(self): data = [] if self.hjustify == 'L': data.append(Symbol('left')) elif self.hjustify == 'R': data.append(Symbol('right')) if self.vjustify == 'T': data.append(Symbol('top')) elif self.vjustify == 'B': data.append(Symbol('bottom')) if self.mirror: data.append(Symbol('mirror')) return _symbol('justify', data) def write(self): data = [self.write_font()] if self.hjustify != 'C' or self.vjustify != 'C' or self.mirror: data.append(self.write_justify()) if self.hide: data.append(Symbol('hide')) return _symbol('effects', data) class Color(object): def __init__(self, items=None): super().__init__() if items: self.r = _check_integer(items, 1, 'red color') self.g = _check_integer(items, 2, 'green color') self.b = _check_integer(items, 3, 'blue color') # Sheet sheet.fill.color is float ... self.a = _check_float(items, 4, 'alpha color') else: self.r = self.g = self.b = self.a = 0 @staticmethod def parse(items): return Color(items) def write(self): return _symbol('color', [self.r, self.g, self.b, self.a]) class Stroke(object): def __init__(self): super().__init__() self.width = 0 self.type = 'default' self.color = Color() @staticmethod def parse(items): stroke = Stroke() for i in items[1:]: i_type = _check_is_symbol_list(i) if i_type == 'width': stroke.width = _check_float(i, 1, 'stroke width') elif i_type == 'type': stroke.type = _check_symbol(i, 1, 'stroke type') elif i_type == 'color': stroke.color = Color.parse(i) else: raise SchError('Unknown stroke attribute `{}`'.format(i)) return stroke def write(self): data = [_symbol('width', [self.width])] data.append(_symbol('type', [Symbol(self.type)])) data.append(self.color.write()) return _symbol('stroke', data) class Fill(object): def __init__(self): super().__init__() self.type = None self.color = None @staticmethod def parse(items): fill = Fill() for i in items[1:]: i_type = _check_is_symbol_list(i) if i_type == 'type': fill.type = _check_symbol(i, 1, 'fill type') elif i_type == 'color': # Not documented, found in sheet.fill fill.color = Color.parse(i) else: raise SchError('Unknown fill attribute `{}`'.format(i)) return fill def write(self): data = [] if self.type is not None: data.append(_symbol('type', [Symbol(self.type)])) if self.color is not None: data.append(self.color.write()) return _symbol('fill', data) class DrawArcV6(object): def __init__(self): super().__init__() self.start = None self.mid = None self.end = None self.stroke = None self.fill = None @staticmethod def parse(items): arc = DrawArcV6() for i in items[1:]: i_type = _check_is_symbol_list(i) if i_type == 'start': arc.start = _get_xy(i) elif i_type == 'mid': arc.mid = _get_xy(i) elif i_type == 'end': arc.end = _get_xy(i) elif i_type == 'stroke': arc.stroke = Stroke.parse(i) elif i_type == 'fill': arc.fill = Fill.parse(i) else: raise SchError('Unknown arc attribute `{}`'.format(i)) arc.box = Box([arc.start, arc.mid, arc.end]) return arc def write(self): data = [_symbol('start', [self.start.x, self.start.y])] data.append(_symbol('mid', [self.mid.x, self.mid.y])) data.append(_symbol('end', [self.end.x, self.end.y])) data.append(Sep()) data.extend([self.stroke.write(), Sep()]) data.extend([self.fill.write(), Sep()]) return _symbol('arc', data) class DrawCircleV6(object): def __init__(self): super().__init__() self.center = None self.radius = 0 self.stroke = None self.fill = None @staticmethod def parse(items): circle = DrawCircleV6() for i in items[1:]: i_type = _check_is_symbol_list(i) if i_type == 'center': circle.center = _get_xy(i) elif i_type == 'radius': circle.radius = _check_float(i, 1, 'circle radius') elif i_type == 'stroke': circle.stroke = Stroke.parse(i) elif i_type == 'fill': circle.fill = Fill.parse(i) else: raise SchError('Unknown circle attribute `{}`'.format(i)) p1 = PointXY(circle.center.x-circle.radius, circle.center.x-circle.radius) p2 = PointXY(circle.center.x+circle.radius, circle.center.x+circle.radius) circle.box = Box([p1, p2]) return circle def write(self): data = [_symbol('center', [self.center.x, self.center.y])] data.append(_symbol('radius', [self.radius])) data.append(Sep()) data.extend([self.stroke.write(), Sep()]) data.extend([self.fill.write(), Sep()]) return _symbol('circle', data) class DrawRectangleV6(object): def __init__(self): super().__init__() self.start = None self.end = None self.stroke = None self.fill = None @staticmethod def parse(items): rectangle = DrawRectangleV6() for i in items[1:]: i_type = _check_is_symbol_list(i) if i_type == 'start': rectangle.start = _get_xy(i) elif i_type == 'end': rectangle.end = _get_xy(i) elif i_type == 'stroke': rectangle.stroke = Stroke.parse(i) elif i_type == 'fill': rectangle.fill = Fill.parse(i) else: raise SchError('Unknown rectangle attribute `{}`'.format(i)) rectangle.box = Box([rectangle.start, rectangle.end]) return rectangle def write(self): data = [_symbol('start', [self.start.x, self.start.y])] data.append(_symbol('end', [self.end.x, self.end.y])) data.append(Sep()) data.extend([self.stroke.write(), Sep()]) data.extend([self.fill.write(), Sep()]) return _symbol('rectangle', data) class DrawCurve(object): """ Qubic Bezier """ def __init__(self): super().__init__() self.points = [] self.stroke = None self.fill = None @staticmethod def parse(items): curve = DrawCurve() for i in items[1:]: i_type = _check_is_symbol_list(i) if i_type == 'pts': curve.points = _get_points(i) elif i_type == 'stroke': curve.stroke = Stroke.parse(i) elif i_type == 'fill': curve.fill = Fill.parse(i) else: raise SchError('Unknown curve attribute `{}`'.format(i)) curve.box = Box(curve.points) return curve def write(self): points = [Sep()] for p in self.points: points.append(_symbol('xy', [p.x, p.y])) points.append(Sep()) data = [_symbol('pts', points), Sep()] data.extend([self.stroke.write(), Sep()]) data.extend([self.fill.write(), Sep()]) return _symbol('gr_curve', data) class DrawPolyLine(object): def __init__(self): super().__init__() self.points = [] self.stroke = None self.fill = None @staticmethod def parse(items): line = DrawPolyLine() for i in items[1:]: i_type = _check_is_symbol_list(i) if i_type == 'pts': line.points = _get_points(i) elif i_type == 'stroke': line.stroke = Stroke.parse(i) elif i_type == 'fill': line.fill = Fill.parse(i) else: raise SchError('Unknown polyline attribute `{}`'.format(i)) line.box = Box(line.points) return line def write(self): points = [Sep()] for p in self.points: points.append(_symbol('xy', [p.x, p.y])) points.append(Sep()) data = [Sep(), _symbol('pts', points), Sep()] data.extend([self.stroke.write(), Sep()]) data.extend([self.fill.write(), Sep()]) return _symbol('polyline', data) class DrawTextV6(object): def __init__(self): super().__init__() self.text = None self.x = self.y = self.ang = 0 self.effects = None self.box = Box() @staticmethod def parse(items): text = DrawTextV6() text.text = _check_str(items, 1, 'text') text.x, text.y, text.ang = _get_at(items, 2, 'text') text.effects = _get_effects(items, 3, 'text') return text def write(self): data = [self.text, _symbol('at', [self.x, self.y, self.ang]), Sep()] data.extend([self.effects.write(), Sep()]) return _symbol('text', data) def _get_effects(items, pos, name): values = _check_symbol_value(items, pos, name, 'effects') return FontEffects.parse(values) class PinV6(object): def __init__(self): super().__init__() self.type = self.gtype = self.name = self.number = '' self.pos_x = self.pos_y = self.ang = self.len = 0 self.name_effects = self.number_effects = None self.hide = False self.box = Box() @staticmethod def parse(items): name = 'pin' pin = PinV6() pin.type = _check_symbol(items, 1, name+' type') pin.gtype = _check_symbol(items, 2, name+' style') for c, i in enumerate(items[3:]): i_type = _check_is_symbol_list(i, allow_orphan_symbol=['hide']) if i_type == 'at': pin.pos_x, pin.pos_y, pin.ang = _get_at(items, c+3, name) elif i_type == 'length': pin.len = _check_float(i, 1, name+' length') elif i_type == 'hide': # Not documented yet pin.hide = True elif i_type == 'name': pin.name = _check_str(i, 1, name+' name') pin.name_effects = _get_effects(i, 2, name+' name') elif i_type == 'number': pin.number = _check_str(i, 1, name+' number') pin.number_effects = _get_effects(i, 2, name+' number') else: raise SchError('Unknown pin attribute `{}`'.format(i)) if not pin.hide: p1 = PointXY(pin.pos_x, pin.pos_y) ang = pin.ang % 360 if ang == 0: co = 1 si = 0 elif pin.ang == 90: co = 0 si = 1 elif pin.ang == 180: co = -1 si = 0 else: # 270 co = 0 si = -1 p2 = PointXY(pin.pos_x+pin.len*co, pin.pos_y+pin.len*si) pin.box = Box([p1, p2]) return pin def write(self): data = [Symbol(self.type), Symbol(self.gtype), _symbol('at', [self.pos_x, self.pos_y, self.ang]), _symbol('length', [self.len])] if self.hide: data.append(Symbol('hide')) data.extend([Sep(), _symbol('name', [self.name, self.name_effects.write()]), Sep(), _symbol('number', [self.number, self.number_effects.write()]), Sep()]) return _symbol('pin', data) class SchematicFieldV6(object): # Fixed ids: # 0 Reference # 1 Value # 2 Footprint # 3 Datasheet # Reserved names: ki_keywords, ki_description, ki_locked, ki_fp_filters def __init__(self, name='', value='', id=0, x=0, y=0, ang=0): super().__init__() self.name = name self.value = value self.number = id self.x = x self.y = y self.ang = ang self.effects = None self.hide = False @staticmethod def parse(items): if len(items) != 6: _check_len_total(items, 5, 'property') 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 return field def write(self): if self.number < 0: return None data = [self.name, self.value, _symbol('id', [self.number])] data.append(_symbol('at', [self.x, self.y, self.ang])) if self.effects: data.extend([Sep(), self.effects.write(), Sep()]) return _symbol('property', data) class LibComponent(object): unit_regex = re.compile(r'^(.*)_(\d+)_(\d+)$') cross_color = Color() cross_stroke = Stroke() cross_stroke.width = 0.6 cross_stroke.color = cross_color cross_fill = Fill() cross_fill.type = 'none' def __init__(self): super().__init__() self.pin_numbers_hide = None self.pin_names_hide = None self.pin_names_offset = None self.in_bom = False self.on_board = False self.is_power = False self.unit = 0 self.draw = [] 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, parent=None): # noqa: C901 if not isinstance(c, list): raise SchError('Library component definition is not a list') if len(c) < 3: raise SchError('Truncated library component definition (len<3)') if not isinstance(c[0], Symbol) or c[0].value() != 'symbol': raise SchError('Library component definition is of wrong type') comp = LibComponent() comp.project = project # First argument is the LIB:NAME comp.lib_id = comp.name = _check_str(c, 1, 'name') res = comp.name.split(':') comp.lib = None if len(res) == 2: comp.name = res[1] comp.lib = res[0] else: 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:]: i_type = _check_is_symbol_list(i) vis_obj = None if i_type == 'pin_numbers': comp.pin_numbers_hide = _check_hide(i, 1, i_type) elif i_type == 'pin_names': value = _check_len(i, 1, i_type) index = 1 if isinstance(value, list): comp.pin_names_offset = _get_offset(i, 1, i_type) index = 2 comp.pin_names_hide = None try: comp.pin_names_hide = _check_symbol(i, index, i_type) except SchError: # Optional pass elif i_type == 'in_bom': 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 == 'power': # Not yet documented comp.is_power = True # SYMBOL_PROPERTIES... elif i_type == 'property': field = SchematicFieldV6.parse(i) comp.fields.append(field) comp.dfields[field.name.lower()] = field # GRAPHIC_ITEMS... elif i_type == 'arc': vis_obj = DrawArcV6.parse(i) comp.draw.append(vis_obj) elif i_type == 'circle': vis_obj = DrawCircleV6.parse(i) comp.draw.append(vis_obj) elif i_type == 'gr_curve': vis_obj = DrawCurve.parse(i) comp.draw.append(vis_obj) elif i_type == 'polyline': vis_obj = DrawPolyLine.parse(i) comp.draw.append(vis_obj) elif i_type == 'rectangle': vis_obj = DrawRectangleV6.parse(i) comp.draw.append(vis_obj) elif i_type == 'text': comp.draw.append(DrawTextV6.parse(i)) # PINS... 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: # 1) A symbol without real units: # - *_0_1 the body # - *_1_1 the pins # 2) A symbol with real units: # - Each unit is *_N_* where N is the unit starting from 1 # - 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, 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: raise SchError('Malformed unit id `{}`'.format(vis_obj.lib_id)) unit = int(m.group(2)) comp.unit_count = max(unit, comp.unit_count) else: raise SchError('Unknown symbol attribute `{}`'.format(i)) if vis_obj: comp.box.union(vis_obj.box) return comp def assign_crosses(self): """ Compute the box for the crossed components """ name0 = self.name+"_0" # Compute the full box for each unit for c in range(self.unit_count): name = self.name+"_"+str(c+1) box = Box() unit_with_graphs = None for unit in self.units: # Unit 0 is part of unit 1 if unit.name.startswith(name) or (c == 0 and unit.name.startswith(name0)): box.union(unit.box) if len(unit.draw): unit_with_graphs = unit if unit_with_graphs: unit_with_graphs.cross_box = box def write_cross(s, sdata): """ Add the cross drawing """ if s.cross_box: # Add a cross o = DrawPolyLine() o.stroke = LibComponent.cross_stroke o.fill = LibComponent.cross_fill o.points = s.cross_box.diagonal() sdata.extend([o.write(), Sep()]) o.points = s.cross_box.diagonal(True) sdata.extend([o.write(), Sep()]) s.cross_box = None def write(s, cross=False): lib_id = s.lib_id if cross: # Fill the cross_box of our sub/units s.assign_crosses() if s.lib: # Use an alternative name lib_id = CROSSED_LIB+':'+s.name sdata = [lib_id] if s.is_power: sdata.append(_symbol('power', [])) if s.pin_numbers_hide: sdata.append(_symbol('pin_numbers', [Symbol('hide')])) if s.pin_names_hide is not None or s.pin_names_offset is not None: aux = [] if s.pin_names_offset is not None: aux.append(_symbol('offset', [s.pin_names_offset])) if s.pin_names_hide is not None: aux.append(Symbol('hide')) sdata.append(_symbol('pin_names', aux)) if s.in_bom: sdata.append(_symbol('in_bom', [Symbol('yes')])) if s.on_board: sdata.append(_symbol('on_board', [Symbol('yes')])) sdata.append(Sep()) # Properties for f in s.fields: fdata = f.write() if fdata is not None: sdata.extend([fdata, Sep()]) # Graphics for g in s.draw: sdata.extend([g.write(), Sep()]) s.write_cross(sdata) # Pins for p in s.pins: sdata.extend([p.write(), Sep()]) # Units for u in s.units: sdata.extend([u.write(cross), Sep()]) return _symbol('symbol', sdata) class SchematicComponentV6(SchematicComponent): def __init__(self): super().__init__() self.in_bom = False self.on_board = False self.pins = OrderedDict() self.unit = 1 self.unit_specified = False self.ref = None def set_ref(self, ref): self.ref = ref # Separate the reference in its components m = SchematicComponent.ref_re.match(ref) if not m: raise SchError('Malformed component reference `{}`'.format(ref)) self.ref_prefix, self.ref_suffix = m.groups() def set_footprint(self, fp): res = fp.split(':') cres = len(res) if cres == 1: self.footprint = res[0] self.footprint_lib = None elif cres == 2: self.footprint_lib = res[0] self.footprint = res[1] else: raise SchError('Footprint with more than one colon (`{}`)'.format(fp)) @staticmethod def load(c, project, parent): if not isinstance(c, list): raise SchError('Component definition is not a list') if len(c) < 7: raise SchError('Truncated component definition (len<7)') if not isinstance(c[0], Symbol) or c[0].value() != 'symbol': raise SchError('Component definition is of wrong type') comp = SchematicComponentV6() 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') res = comp.name.split(':') comp.lib = None if len(res) == 2: comp.name = res[1] comp.lib = res[0] else: logger.warning(W_NOLIB + "Component `{}` doesn't specify its library".format(comp.name)) # 2 The position comp.x, comp.y, comp.ang = _get_at(c, 2, name) # Variable list for i in c[3:]: i_type = _check_is_symbol_list(i) if i_type == 'unit': # This is documented as mandatory, but isn't always there comp.unit = _check_integer(i, 1, name+' unit') comp.unit_specified = True elif i_type == 'in_bom': 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 == 'uuid': comp.uuid = _check_symbol(i, 1, name + ' uuid') # SYMBOL_PROPERTIES... elif i_type == 'property': field = SchematicFieldV6.parse(i) name_lc = field.name.lower() # Add to the global collection if name_lc not in parent.fields_lc: parent.fields.append(field.name) parent.fields_lc.add(name_lc) # Add to the component comp.add_field(field) if field.number == 3: # Reference, Value and Footprint are defined by the instance. # But datasheet must be transferred from this field. comp.datasheet = field.value # PINS... elif i_type == 'pin': pin_name = _check_str(i, 1, name + 'pin name') pin_uuid = _get_uuid(i, 2, name) comp.pins[pin_name] = pin_uuid # Fake 'Part' field field = SchematicFieldV6() field.name = 'part' field.value = comp.name field.number = -1 comp.add_field(field) return comp def write(self, cross=False): lib_id = self.lib_id is_crossed = not(self.fitted or not self.included) if cross and self.lib and is_crossed: # Use an alternative name lib_id = CROSSED_LIB+':'+self.name data = [_symbol('lib_id', [lib_id]), _symbol('at', [self.x, self.y, self.ang])] if self.unit_specified: data.append(_symbol('unit', [self.unit])) data.append(Sep()) if self.in_bom or self.on_board: if self.in_bom: data.append(_symbol('in_bom', [Symbol('yes')])) if self.on_board: data.append(_symbol('on_board', [Symbol('yes')])) data.append(Sep()) data.extend([_symbol('uuid', [Symbol(self.uuid)]), Sep()]) for f in self.fields: d = f.write() if d: data.extend([d, Sep()]) for k, v in self.pins.items(): data.extend([_symbol('pin', [k, _symbol('uuid', [Symbol(v)])]), Sep()]) return _symbol('symbol', data) def _get_uuid(items, pos, where): values = _check_symbol_value(items, pos, where + ' uuid', 'uuid') return _check_symbol(values, 1, where + ' uuid') class Junction(object): @staticmethod def parse(items): _check_len_total(items, 5, 'junction') jun = Junction() jun.pos_x, jun.pos_y, jun.ang = _get_at(items, 1, 'junction') jun.diameter = _check_symbol_float(items, 2, 'junction', 'diameter') jun.color = Color.parse(items[3]) jun.uuid = _get_uuid(items, 4, 'junction') return jun def write(self): data = [_symbol('at', [self.pos_x, self.pos_y]), _symbol('diameter', [self.diameter]), self.color.write(), Sep(), _symbol('uuid', [Symbol(self.uuid)]), Sep()] return _symbol('junction', data) class BusAlias(object): @staticmethod def parse(items): _check_len_total(items, 3, 'bus_alias') alias = BusAlias() alias.name = _check_str(items, 1, 'bus_alias') elems = _check_symbol_value(items, 2, 'bus_alias', 'members') alias.members = elems[1:] return alias def write(self): return _symbol('bus_alias', [self.name, _symbol('members', self.members)]) class NoConnect(object): @staticmethod def parse(items): _check_len_total(items, 3, 'no_connect') nocon = NoConnect() nocon.pos_x, nocon.pos_y, nocon.ang = _get_at(items, 1, 'no connect') nocon.uuid = _get_uuid(items, 2, 'no connect') return nocon def write(self): data = [_symbol('at', [self.pos_x, self.pos_y]), _symbol('uuid', [Symbol(self.uuid)])] return _symbol('no_connect', data) class BusEntry(object): @staticmethod def parse(items): _check_len_total(items, 5, 'bus entry') buse = BusEntry() buse.pos_x, buse.pos_y, buse.ang = _get_at(items, 1, 'bus entry') values = _check_symbol_value(items, 2, 'bus entry size', 'size') buse.size = _get_xy(values) buse.stroke = Stroke.parse(items[3]) buse.uuid = _get_uuid(items, 4, 'bus entry') return buse def write(self): data = [_symbol('at', [self.pos_x, self.pos_y]), _symbol('size', [self.size.x, self.size.y]), Sep(), self.stroke.write(), Sep(), _symbol('uuid', [Symbol(self.uuid)]), Sep()] return _symbol('bus_entry', data) class SchematicWireV6(object): @staticmethod def parse(items, name): _check_len_total(items, 4, name) wire = SchematicWireV6() wire.type = name # wire, bus, polyline wire.points = _get_points(items[1]) wire.stroke = Stroke.parse(items[2]) wire.uuid = _get_uuid(items, 3, name) return wire def write(self): points = [_symbol('xy', [p.x, p.y]) for p in self.points] data = [_symbol('pts', points), Sep(), self.stroke.write(), Sep(), _symbol('uuid', [Symbol(self.uuid)]), Sep()] return _symbol(self.type, data) class SchematicBitmapV6(object): @staticmethod def parse(items): bmp = SchematicBitmapV6() if len(items) == 5: bmp.scale = _check_symbol_float(items, 2, 'image', 'scale') index = 3 else: _check_len_total(items, 4, 'image') bmp.scale = None index = 2 bmp.pos_x, bmp.pos_y, bmp.ang = _get_at(items, 1, 'image') bmp.uuid = _get_uuid(items, index, 'image') values = _check_symbol_value(items, index+1, 'image data', 'data') bmp.data = [_check_symbol(values, i+1, 'image data') for i, d in enumerate(values[1:])] return bmp def write(self): d = [] for v in self.data: d.append(Symbol(v)) d.append(Sep()) data = [_symbol('at', [self.pos_x, self.pos_y]), Sep(), _symbol('uuid', [Symbol(self.uuid)]), Sep(), _symbol('data', [Sep()] + d), Sep()] return _symbol('image', data) 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 def write(self): data = [self.text, _symbol('at', [self.pos_x, self.pos_y, self.ang]), Sep(), self.effects.write(), Sep(), _symbol('uuid', [Symbol(self.uuid)]), Sep()] return _symbol(self.name, data) class GlobalLabel(object): def __init__(self): super().__init__() self.text = '' self.shape = None self.fields_autoplaced = False self.pos_x = self.pos_y = self.ang = 0 self.effects = None self.uuid = None self.properties = [] @staticmethod def parse(items): label = GlobalLabel() label.text = _check_str(items, 1, 'global_label') 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) 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') elif i_type == 'effects': label.effects = FontEffects.parse(i) elif i_type == 'uuid': label.uuid = _get_uuid(items, c+2, 'global_label') elif i_type == 'property': label.properties.append(SchematicFieldV6.parse(i)) else: raise SchError('Unknown label attribute `{}`'.format(i)) return label def write(self): data = [self.text, _symbol('shape', [Symbol(self.shape)]), _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()]) for p in self.properties: data.extend([p.write(), Sep()]) return _symbol('global_label', 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 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 HSPin(object): """ Hierarchical Sheet Pin """ # TODO base class with HierarchicalLabel @staticmethod def parse(items): name = 'hierarchical sheet pin' _check_len_total(items, 6, name) pin = HSPin() pin.name = _check_str(items, 1, name+' name') pin.type = _check_symbol(items, 2, name+' type') pin.pos_x, pin.pos_y, pin.ang = _get_at(items, 3, name) pin.effects = _get_effects(items, 4, name) pin.uuid = _get_uuid(items, 5, name) return pin def write(self): data = [self.name, Symbol(self.type), _symbol('at', [self.pos_x, self.pos_y, self.ang]), Sep(), self.effects.write(), Sep(), _symbol('uuid', [Symbol(self.uuid)]), Sep()] 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): name = 'sheet instance' instances = [] for c, i in enumerate(items[1:]): v = _check_symbol_value(items, c+1, name, 'path') instance = SheetInstance() instance.path = _check_str(v, 1, name+' path') instance.page = _check_symbol_str(v, 2, name, 'page') instances.append(instance) return instances def write(self): return _symbol('path', [self.path, _symbol('page', [self.page])]) class SymbolInstance(object): @staticmethod def parse(items): name = 'symbol instance' instances = [] for c, i 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 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 _symbol(name, content): return [Symbol(name)] + content def _add_items(items, sch, sep=False, cross=False, pre_sep=True): if len(items): if pre_sep: sch.append(Sep()) for i in items: if cross: sch.append(i.write(cross=True)) else: sch.append(i.write()) sch.append(Sep()) if sep: sch.append(Sep()) if sep: sch.pop() def _add_items_list(name, items, sch): if not len(items): return data = [Sep()] for s in items: data.append(s.write()) data.append(Sep()) sch.extend([Sep(), _symbol(name, data), Sep()]) class SchematicV6(Schematic): def __init__(self): super().__init__() self.annotation_error = False # The title block is optional self.date = self.title = self.revision = self.company = '' 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 self.date = GS.format_date(self.date, self.fname, 'SCH') if not self.title: self.title = os.path.splitext(os.path.basename(self.fname))[0] def _get_title_block(self, items): if not isinstance(items, list): raise SchError('The title block is not a list') for item in items: if not isinstance(item, list) or len(item) < 2 or not isinstance(item[0], Symbol): raise SchError('Wrong title block entry ({})'.format(item)) i_type = item[0].value() if i_type == 'title': self.title_ori = self.title = _check_str(item, 1, i_type) elif i_type == 'date': self.date_ori = self.date = _check_str(item, 1, i_type) elif i_type == 'rev': self.revision = _check_str(item, 1, i_type) elif i_type == 'company': self.company = _check_str(item, 1, i_type) elif i_type == 'comment': index = _check_integer(item, 1, i_type) if index < 1 or index > 9: raise SchError('Unsupported comment index {} in title block'.format(index)) value = _check_str(item, 2, i_type) self.comment[index-1] = value else: raise SchError('Unsupported entry in title block ({})'.format(item)) self._fill_missing_title_block() logger.debug("SCH title: `{}`".format(self.title)) logger.debug("SCH date: `{}`".format(self.date)) logger.debug("SCH revision: `{}`".format(self.revision)) logger.debug("SCH company: `{}`".format(self.company)) def _get_lib_symbols(self, comps): if not isinstance(comps, list): raise SchError('The lib symbols is not a list') for c in comps[1:]: obj = LibComponent.load(c, self.project) self.lib_symbols.append(obj) self.lib_symbol_names[obj.lib_id] = obj def path_to_human(self, path): """ Converts a UUID path into something we can read """ if path == '/': return path res = self.sheet_names[path] return res def write_paper(self): paper_data = [self.paper] if self.paper == "User": paper_data.extend([self.paper_w, self.paper_h]) if self.paper_orientation is not None: paper_data.append(Symbol(self.paper_orientation)) return [Sep(), Sep(), _symbol('paper', paper_data)] def write_title_block(self): data = [Sep()] data += [_symbol('title', [self.title_ori]), Sep()] data += [_symbol('date', [self.date_ori]), Sep()] data += [_symbol('rev', [self.revision]), Sep()] data += [_symbol('company', [self.company]), Sep()] for num, val in enumerate(self.comment): data += [_symbol('comment', [num+1, val]), Sep()] return [Sep(), Sep(), _symbol('title_block', data)] def write_lib_symbols(self, cross=False): data = [Sep()] for s in self.lib_symbols: data.extend([s.write(), Sep()]) if cross: data.extend([s.write(cross), Sep()]) return [Sep(), Sep(), _symbol('lib_symbols', data), Sep()] def save(self, fname, dest_dir): cross = True fname = os.path.join(dest_dir, fname) sch = [Symbol('kicad_sch')] sch.append(_symbol('version', [self.version])) sch.append(_symbol('generator', [Symbol(self.generator)])) sch.append(Sep()) sch.append(Sep()) sch.append(_symbol('uuid', [Symbol(self.uuid)])) sch.extend(self.write_paper()) if self.title_ori is not None: sch.extend(self.write_title_block()) sch.extend(self.write_lib_symbols(cross)) # Bus aliases _add_items(self.bus_alias, sch) # Connections (aka Junctions) _add_items(self.junctions, sch, pre_sep=(len(self.bus_alias) == 0)) # No connect _add_items(self.no_conn, sch) # Bus entry _add_items(self.bus_entry, sch) # Lines (wire, bus and polyline) if self.wires: old_type = 'none' for e in self.wires: if e.type != old_type and old_type != 'wire': sch.append(Sep()) sch.append(e.write()) old_type = e.type sch.append(Sep()) # Images _add_items(self.bitmaps, sch) # Texts _add_items(self.texts, sch) # Labels _add_items(self.labels, sch) # Global Labels _add_items(self.glabels, sch) # Hierarchical Labels _add_items(self.hlabels, sch) # Symbols _add_items(self.symbols, sch, sep=True, cross=cross) # Sheets _add_items(self.sheets, sch, sep=True, cross=cross) # Sheet instances _add_items_list('sheet_instances', self.sheet_instances, sch) # Symbol instances _add_items_list('symbol_instances', self.symbol_instances, sch) with open(fname, 'wt') as f: f.write(dumps(sch)) f.write('\n') for sch in self.sheets: if sch.sch: sch.sch.save(sch.flat_file if cross else sch.file, dest_dir) def save_variant(self, dest_dir): fname = os.path.basename(self.fname) self.save(fname, dest_dir) return fname def _create_flat_name(self, sch): """ Create a unique name that doesn't contain subdirs. 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.sheet_path, sch.uuid)] = os.path.join(self.sheet_path_h, sch.name) # Eliminate subdirs file = sch.file.replace('/', '_') fparts = os.path.splitext(file) sch.flat_file = fparts[0]+'_'+str(len(self.sheet_names))+fparts[1] def load(self, fname, project, parent=None): # noqa: C901 """ Load a v6.x KiCad Schematic. The caller must be sure the file exists. Only the schematics are loaded not the libs. """ logger.debug("Loading sheet from "+fname) if parent is None: self.fields = ['part'] self.fields_lc = set(self.fields) self.sheet_paths = {'/': self} self.lib_symbol_names = {} self.sheet_path = '/' self.sheet_path_h = '/' self.sheet_names = {} 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_names = parent.sheet_names # self.sheet_path is set by sch.load_sheet self.parent = parent self.fname = fname self.project = project self.lib_symbols = [] self.symbols = [] self.components = [] self.junctions = [] # Connect self.no_conn = [] self.bus_entry = [] self.wires = [] self.bitmaps = [] self.texts = [] self.labels = [] self.glabels = [] 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. # 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 try: sch = load(fh)[0] except SExpData as e: error = str(e) if error: raise SchError(error) if not isinstance(sch, list) or sch[0].value() != 'kicad_sch': raise SchError('No kicad_sch signature') for e in sch[1:]: e_type = _check_is_symbol_list(e) obj = None if e_type == 'version': self.version = _check_integer(e, 1, e_type) elif e_type == 'generator': self.generator = _check_symbol(e, 1, e_type) elif e_type == 'uuid': self.id = self.uuid = _check_symbol(e, 1, e_type) elif e_type == 'paper': self.paper = _check_str(e, 1, e_type) index = 2 if self.paper == "User": self.paper_w = _check_float(e, 2, 'paper width') self.paper_h = _check_float(e, 3, 'paper height') index += 2 if len(e) > index: self.paper_orientation = _check_symbol(e, index, 'paper orientation') else: self.paper_orientation = None elif e_type == 'title_block': self._get_title_block(e[1:]) elif e_type == 'lib_symbols': self._get_lib_symbols(e) elif e_type == 'bus_alias': self.bus_alias.append(BusAlias.parse(e)) elif e_type == 'junction': self.junctions.append(Junction.parse(e)) elif e_type == 'no_connect': self.no_conn.append(NoConnect.parse(e)) elif e_type == 'bus_entry': self.bus_entry.append(BusEntry.parse(e)) elif e_type == 'bus' or e_type == 'wire' or e_type == 'polyline': self.wires.append(SchematicWireV6.parse(e, e_type)) elif e_type == 'image': self.bitmaps.append(SchematicBitmapV6.parse(e)) elif e_type == 'text': self.texts.append(Text.parse(e, e_type)) elif e_type == 'label': self.labels.append(Text.parse(e, e_type)) elif e_type == 'global_label': self.glabels.append(GlobalLabel.parse(e)) elif e_type == 'hierarchical_label': self.hlabels.append(HierarchicalLabel.parse(e)) elif e_type == 'symbol': obj = SchematicComponentV6.load(e, self.project, self) if obj.annotation_error: self.annotation_error = True self.symbols.append(obj) self.symbol_uuids[obj.uuid] = obj elif e_type == 'sheet': obj = Sheet.parse(e) self.sheets.append(obj) self._create_flat_name(obj) elif e_type == 'sheet_instances': self.sheet_instances = SheetInstance.parse(e) elif e_type == 'symbol_instances': self.symbol_instances = SymbolInstance.parse(e) else: raise SchError('Unknown kicad_sch attribute `{}`'.format(e)) if not self.title: self._fill_missing_title_block() # Load sub-sheets for sch in self.sheets: sheet = sch.load_sheet(project, fname, self) 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 path = os.path.dirname(s.path) sheet = self.sheet_paths[path] comp_uuid = os.path.basename(s.path) comp = sheet.symbol_uuids[comp_uuid] # Transfer the instance data comp.set_ref(s.reference) 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] except KeyError: logger.warning(W_MISSCMP+'Missing component `{}`'.format(comp.lib_id)) 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