KiBot/kibot/kicad/v6_sch.py

1136 lines
39 KiB
Python

# -*- 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
from copy import deepcopy
from collections import OrderedDict
from ..gs import GS
from .. import log
from ..misc import W_NOLIB, W_UNKFLD
from .v5_sch import SchError, SchematicComponent, Schematic
from .sexpdata import load, SExpData, Symbol
logger = log.get_logger(__name__)
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)
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':
w = _check_float(i, 1, 'font width')
h = _check_float(i, 2, 'font height')
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':
h = 'T'
elif name == 'bottom':
h = '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
class Color(object):
def __init__(self, items):
super().__init__()
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')
@staticmethod
def parse(items):
return Color(items)
class Stroke(object):
def __init__(self):
super().__init__()
self.width = 0
self.type = 'default'
self.color = None
@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
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
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))
return arc
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))
return circle
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))
return rectangle
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))
return curve
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))
return line
class DrawTextV6(object):
def __init__(self):
super().__init__()
self.text = None
self.x = self.y = self.ang = 0
self.effects = None
@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 _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
@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))
return pin
class SchematicFieldV6(object):
# Fixed ids:
# 0 Reference
# 1 Value
# 2 Footprint
# 3 Datasheet
# Reserved names: ki_keywords, ki_description, ki_locked, ki_fp_filters
@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
class LibComponent(object):
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 = {}
@staticmethod
def load(c, project, is_unit=False): # 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]
# libs[comp.lib] = None
else:
if not is_unit:
logger.warning(W_NOLIB + "Component `{}` doesn't specify its library".format(comp.name))
comp.units = []
comp.pins = []
# Variable list
for i in c[2:]:
i_type = _check_is_symbol_list(i)
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':
comp.draw.append(DrawArcV6.parse(i))
elif i_type == 'circle':
comp.draw.append(DrawCircleV6.parse(i))
elif i_type == 'gr_curve':
comp.draw.append(DrawCurve.parse(i))
elif i_type == 'polyline':
comp.draw.append(DrawPolyLine.parse(i))
elif i_type == 'rectangle':
comp.draw.append(DrawRectangleV6.parse(i))
elif i_type == 'text':
comp.draw.append(DrawTextV6.parse(i))
# PINS...
elif i_type == 'pin':
comp.pins.append(PinV6.parse(i))
# UNITS...
elif i_type == 'symbol':
obj = LibComponent.load(i, project, is_unit=True)
comp.units.append(obj)
# logger.warning('Unit: '+str(obj))
else:
raise SchError('Unknown symbol attribute `{}`'.format(i))
return comp
class SchematicComponentV6(SchematicComponent):
def __init__(self):
super().__init__()
self.in_bom = False
self.on_board = False
self.pins = OrderedDict()
self.unit = 0
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
comp.sheet_path_h = parent.sheet_path_h
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]
# libs[comp.lib] = None
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)
# 3 Unit
# Variable list
for i in c[4:]:
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')
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
return comp
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
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
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
class SchematicWireV6(object):
@staticmethod
def parse(items, name):
_check_len_total(items, 4, name)
wire = SchematicWireV6()
wire.points = _get_points(items[1])
wire.stroke = Stroke.parse(items[2])
wire.uuid = _get_uuid(items, 3, name)
return wire
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
class PolyLine(object):
@staticmethod
def parse(items):
_check_len_total(items, 4, 'polyline')
poly = PolyLine()
poly.points = _get_points(items[1])
poly.stroke = Stroke.parse(items[2])
poly.uuid = _get_uuid(items, 3, 'polyline')
return poly
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
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
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 HSPin(object):
""" Hierarchical Sheet Pin """
@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
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 = []
@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.path = os.path.join(parent_obj.path, self.uuid)
sheet.sheet_path_h = os.path.join(parent_obj.sheet_path_h, self.name)
parent_obj.sheet_paths[sheet.path] = sheet
sheet.load(os.path.join(parent_dir, self.file), project, parent_obj)
return sheet
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
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
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.comment1 = self.comment2 = self.comment3 = self.comment4 = ''
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 = _check_str(item, 1, i_type)
elif i_type == 'date':
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 > 4:
raise SchError('Unsupported comment index {} in title block'.format(index))
value = _check_str(item, 2, i_type)
if index == 1:
self.comment1 = value
elif index == 2:
self.comment2 = value
elif index == 3:
self.comment3 = value
elif index == 4:
self.comment4 = 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 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 = []
self.fields_lc = set()
self.sheet_paths = {'/': self}
self.symbol_uuids = {}
self.lib_symbol_names = {}
self.path = '/'
self.sheet_path_h = '/'
else:
self.fields = parent.fields
self.fields_lc = parent.fields_lc
self.sheet_paths = parent.sheet_paths
self.symbol_uuids = parent.symbol_uuids
self.lib_symbol_names = parent.lib_symbol_names
# self.path is set by sch.load_sheet
self.parent = parent
self.fname = fname
self.project = project
self.all = []
self.lib_symbols = []
self.symbols = []
self.components = []
self.juntions = [] # Connect
self.no_conn = []
self.bus_entry = []
self.wires = []
self.bitmaps = []
self.texts = []
self.lines = []
self.labels = []
self.glabels = []
self.hlabels = []
self.sheets = []
self.sheet_instances = []
self.symbol_instances = []
with open(fname, 'rt') as fh:
error = None
try:
sch = load(fh)
except SExpData as e:
error = str(e)
if error:
raise SchError(error)
if 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.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)
# The following are mixed
elif e_type == 'junction':
obj = Junction.parse(e)
self.juntions.append(obj)
elif e_type == 'no_connect':
obj = NoConnect.parse(e)
self.no_conn.append(obj)
elif e_type == 'bus_entry':
obj = BusEntry.parse(e)
self.bus_entry.append(obj)
elif e_type == 'bus' or e_type == 'wire':
obj = SchematicWireV6.parse(e, e_type)
self.wires.append(obj)
elif e_type == 'image':
obj = SchematicBitmapV6.parse(e)
self.bitmaps.append(obj)
elif e_type == 'polyline':
obj = PolyLine.parse(e)
self.lines.append(obj)
elif e_type == 'text':
obj = Text.parse(e, e_type)
self.texts.append(obj)
elif e_type == 'label':
obj = Text.parse(e, e_type)
self.labels.append(obj)
elif e_type == 'global_label':
obj = GlobalLabel.parse(e)
self.glabels.append(obj)
elif e_type == 'hierarchical_label':
obj = HierarchicalLabel.parse(e)
self.hlabels.append(obj)
# Ordered again
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)
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 obj is not None:
self.all.append(obj)
if not self.title:
self._fill_missing_title_block()
# Load sub-sheets
self.sub_sheets = []
for sch in self.sheets:
sheet = sch.load_sheet(project, fname, self)
if sheet.annotation_error:
self.annotation_error = True
self.sub_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 = deepcopy(self.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)
# Link with its library symbol
lib_symbol = self.lib_symbol_names[comp.lib_id]
comp.lib_symbol = lib_symbol
comp.is_power = lib_symbol.is_power
# Add it to the list
self.components.append(comp)