Added support for "Description" BoM column.

For this we are loading:
- The KiCad configuration
- The default and project symbol libs tables
- All the used libs
- All the used DCMs
This commit is contained in:
Salvador E. Tropea 2020-07-31 11:58:07 -03:00
parent ae47afa695
commit 984b81fea1
4 changed files with 821 additions and 10 deletions

View File

@ -253,7 +253,7 @@ class ComponentGroup(object):
self.fields[ColumnList.COL_FP_L] = comp.footprint
self.fields[ColumnList.COL_FP_LIB_L] = comp.footprint_lib
self.fields[ColumnList.COL_SHEETPATH_L] = comp.sheet_path_h
# self.fields[ColumnList.COL_DESCRIPTION_L] = self.components[0].getDescription() TODO
self.fields[ColumnList.COL_DESCRIPTION_L] = comp.desc
def get_row(self, columns):
""" Return a dict of the KiCad data based on the supplied columns """

269
kiplot/kicad/config.py Normal file
View File

@ -0,0 +1,269 @@
"""
KiCad configuration classes.
"""
import os
import re
import sys
from io import StringIO
from glob import glob
import platform
import sysconfig
from ..gs import GS
from .. import log
# Check python version to determine which version of ConfirParser to import
if sys.version_info.major >= 3:
import configparser as ConfigParser
else:
import ConfigParser
logger = log.get_logger(__name__)
SYM_LIB_TABLE = 'sym-lib-table'
KICAD_COMMON = 'kicad_common'
class KiConfError(Exception):
pass
def un_quote(val):
""" Remove optional quotes """
if val[0] == '"':
val.replace('\"', '"')
val = val[1:-1]
return val
def expand_env(val, env):
""" Expand KiCad environment variables """
for var in re.findall(r'\$\{(\S+)\}', val):
if var in env:
val = val.replace('${'+var+'}', env[var])
else:
logger.error('Unable to expand `{}` in `{}`'.format(var, val))
return val
class LibAlias(object):
""" An entry for the symbol libs table """
libs_re = re.compile(r'\(name\s+(\S+|"(?:[^"]|\\")+")\)\s*\(type\s+(\S+|"(?:[^"]|\\")+")\)'
r'\s*\(uri\s+(\S+|"(?:[^"]|\\")+")\)\s*\(options\s+(\S+|"(?:[^"]|\\")+")\)'
r'\s*\(descr\s+(\S+|"(?:[^"]|\\")+")\)')
def __init__(self):
super().__init__()
self.name = None
self.legacy = True
self.uri = None
self.options = None
self.descr = None
@staticmethod
def parse(options, cline, env):
m = LibAlias.libs_re.match(options)
if not m:
raise KiConfError('Malformed lib entry', SYM_LIB_TABLE, cline, options)
lib = LibAlias()
lib.name = un_quote(m.group(1))
lib.legacy = m.group(2) == 'Legacy'
lib.uri = os.path.abspath(expand_env(un_quote(m.group(3)), env))
lib.options = un_quote(m.group(4))
lib.descr = un_quote(m.group(5))
return lib
def __str__(self):
if not self.name:
return 'empty LibAlias'
return self.name+' -> `'+self.uri+'`'
class KiConf(object):
""" Class to load and hold all KiCad configuration """
loaded = False
config_dir = None
dirname = None
sym_lib_dir = None
kicad_env = {}
lib_aliases = {}
def __init__(self):
assert False, "KiConf is fully static, no instances allowed"
def init(fname):
""" fname is the base project name, any extension is allowed.
So it can be the main schematic, the PCB or the project. """
if KiConf.loaded:
return
KiConf.dirname = os.path.dirname(fname)
KiConf.kicad_env['KIPRJMOD'] = KiConf.dirname
KiConf.load_kicad_common()
KiConf.load_all_lib_aliases()
def find_kicad_common():
""" Looks for kicad_common config file.
Returns its name or None. """
# User option has the higher priority
user_set = os.environ.get('KICAD_CONFIG_HOME')
if user_set:
cfg = os.path.join(user_set, KICAD_COMMON)
if os.path.isfile(cfg):
return cfg
# XDG option is second
xdg_set = os.environ.get('XDG_CONFIG_HOME')
if xdg_set:
cfg = os.path.join(xdg_set, 'kicad', KICAD_COMMON)
if os.path.isfile(cfg):
return cfg
# Others depends on the OS
system = platform.system()
if system == 'Linux':
# Linux: ~/.config/kicad/
home = os.environ.get('HOME')
if not home:
logger.warning('Environment variable `HOME` not defined, using `/`')
home = '/'
cfg = os.path.join(home, '.config', 'kicad', KICAD_COMMON)
if os.path.isfile(cfg):
return cfg
elif system == 'Darwin':
# MacOSX: ~/Library/Preferences/kicad/
home = os.environ.get('HOME')
if not home:
logger.warning('Environment variable `HOME` not defined, using `/`')
home = '/'
cfg = os.path.join(home, 'Library', 'Preferences', 'kicad', KICAD_COMMON)
if os.path.isfile(cfg):
return cfg
elif system == 'Windows':
# Windows: C:\Users\username\AppData\Roaming\kicad
# or C:\Documents and Settings\username\Application Data\kicad
username = os.environ.get('username')
if not username:
logger.warning('Unable to determine current user')
return None
cfg = os.path.join('C:', 'Users', username, 'AppData', 'Roaming', 'kicad', KICAD_COMMON)
if os.path.isfile(cfg):
return cfg
cfg = os.path.join('C:', 'Documents and Settings', username, 'Application Data', 'kicad', KICAD_COMMON)
if os.path.isfile(cfg):
return cfg
else:
logger.warning('Unsupported system `{}`'.format(system))
return None
logger.warning('Unable to find KiCad configuration file ({})'.format(cfg))
return None
def guess_symbol_dir():
""" Tries to figure out where libraries are.
Only used if we failed to find the kicad_common file. """
dir = os.environ.get('KICAD_SYMBOL_DIR')
if dir and os.path.isdir(dir):
return dir
system = platform.system()
share = os.path.join('share', 'kicad', 'library')
if system == 'Linux':
scheme_names = sysconfig.get_scheme_names()
# Try in local dir
if 'posix_user' in scheme_names:
dir = os.path.join(sysconfig.get_path('data', 'posix_user'), share)
if os.path.isdir(dir):
return dir
# Try at system level
if 'posix_prefix' in scheme_names:
dir = os.path.join(sysconfig.get_path('data', 'posix_prefix'), share)
if os.path.isdir(dir):
return dir
elif system == 'Darwin':
app_data = os.path.join('Library', 'Application Support', 'kicad', 'library')
home = os.environ.get('HOME')
if home:
dir = os.path.join(home, app_data)
if os.path.isdir(dir):
return dir
dir = os.path.join('/', app_data)
if os.path.isdir(dir):
return dir
elif system == 'Windows':
dir = os.path.join('C:', 'Program Files', 'KiCad', share)
if os.path.isdir(dir):
return dir
dir = os.path.join('C:', 'KiCad', share)
if os.path.isdir(dir):
return dir
username = os.environ.get('username')
dir = os.path.join('C:', 'Users', username, 'Documents', 'KiCad', 'library')
if os.path.isdir(dir):
return dir
return None
def load_kicad_common():
# Try to figure out KiCad configuration file
cfg = KiConf.find_kicad_common()
if cfg and os.path.isfile(cfg):
logger.debug('Reading KiCad config from `{}`'.format(cfg))
KiConf.config_dir = os.path.dirname(cfg)
# Load the "environment variables"
with open(cfg, 'rt') as f:
buf = f.read()
io_buf = StringIO('[Default]\n'+buf)
cf = ConfigParser.RawConfigParser(allow_no_value=True)
cf.optionxform = str
cf.readfp(io_buf, cfg)
if 'EnvironmentVariables' not in cf.sections():
logger.warning('KiCad config without EnvironmentVariables section')
else:
for k, v in cf.items('EnvironmentVariables'):
if GS.debug_level > 1:
logger.debug('- KiCad var: {}="{}"'.format(k, v))
KiConf.kicad_env[k] = v
if 'KICAD_SYMBOL_DIR' in KiConf.kicad_env:
KiConf.sym_lib_dir = KiConf.kicad_env['KICAD_SYMBOL_DIR']
else:
sym_dir = KiConf.guess_symbol_dir()
if sym_dir:
KiConf.kicad_env['KICAD_SYMBOL_DIR'] = sym_dir
KiConf.sym_lib_dir = sym_dir
logger.debug('Detected KICAD_SYMBOL_DIR="{}"'.format(sym_dir))
else:
logger.warning('Unable to find KiCad libraries')
def load_lib_aliases(fname):
if not os.path.isfile(fname):
return
logger.debug('Loading symbols lib table `{}`'.format(fname))
with open(fname, 'rt') as f:
line = f.readline().strip()
if line != '(sym_lib_table':
raise KiConfError('Symbol libs table missing signature', SYM_LIB_TABLE, 1, line)
line = f.readline()
cline = 2
while line and line[0] != ')':
m = re.match(r'\s*\(lib\s*(.*)\)', line)
if m:
alias = LibAlias.parse(m.group(1), cline, KiConf.kicad_env)
if GS.debug_level > 1:
logger.debug('- Adding lib alias '+str(alias))
KiConf.lib_aliases[alias.name] = alias
else:
raise KiConfError('Unknown symbol table entry', SYM_LIB_TABLE, cline, line)
line = f.readline()
cline += 1
def load_all_lib_aliases():
# Load the default symbol libs table.
# This is the list of libraries enabled by the user.
if KiConf.config_dir:
KiConf.load_lib_aliases(os.path.join(KiConf.config_dir, SYM_LIB_TABLE))
else:
logger.warning('Missing default symbol library table')
# No default symbol libs table, try to create one
if KiConf.sym_lib_dir:
for f in glob(os.path.join(KiConf.sym_lib_dir, '*.lib')):
alias = LibAlias()
alias.name = os.path.splitext(os.path.basename(f))[0]
alias.uri = f
if GS.debug_level > 1:
logger.debug('Detected lib alias '+str(alias))
KiConf.lib_aliases[alias.name] = alias
# Load the project's table
KiConf.load_lib_aliases(os.path.join(KiConf.dirname, SYM_LIB_TABLE))

View File

@ -5,11 +5,12 @@ A basic implementation of the .sch file format.
"""
import re
import os
from collections import OrderedDict
from .config import KiConf, un_quote
from ..gs import GS
from .. import log
logger = log.get_logger(__name__)
_sch_line_number = 0
@ -21,6 +22,10 @@ class SchFileError(SchError):
pass
class SchLibError(SchError):
pass
def _get_line(f):
res = f.readline()
if not res:
@ -30,13 +35,475 @@ def _get_line(f):
return res.rstrip()
def _get_line_dcm(f):
global _sch_line_number
res = f.readline()
while res and res[0] == '#':
if res.startswith('#End Doc Library'):
return res.rstrip()
_sch_line_number += 1
res = f.readline()
if not res:
raise SchFileError('Unexpected end of file')
_sch_line_number += 1
return res.rstrip()
def _get_line_lib(f):
global _sch_line_number
res = f.readline()
while res and res[0] == '#':
if res.startswith('#End Library'):
return res.rstrip()
_sch_line_number += 1
res = f.readline()
if not res:
raise SchFileError('Unexpected end of file')
_sch_line_number += 1
return res.rstrip()
def _split_space(s):
res = s.lstrip().split(' ')
return [a for a in res if a]
class LibComponentField(object):
""" A field for a component in the library.
Almost the same as a field in the schematic, but incompatible!!! """
# F n "text" posx posy dimension orientation visibility hjustify vjustify/italic/bold "name"
field_re = re.compile(r'F\s*(\d+)\s+' # 0 Field number
r'"([^"]*)"\s+' # 1 Field value
r'(-?\d+)\s+' # 2 Pos X
r'(-?\d+)\s+' # 3 Pos Y
r'(\d+)\s+' # 4 Dimension
r'([HV])\s+' # 5 Orientation
r'([VI])\s+' # 6 Visibility
r'([LRCBT])\s+' # 7 HJustify
r'([LRCBT][IN][BN])\s*' # 8 VJustify+Italic+Bold
r'("[^"]*")?') # 9 Name for user fields
def __init__(self):
super().__init__()
@staticmethod
def parse(line, lib_name):
m = LibComponentField.field_re.match(line)
if not m:
raise SchLibError('Malformed component field', line, _sch_line_number, lib_name)
field = SchematicField()
gs = m.groups()
field.number = int(gs[0])
field.value = gs[1]
field.x = int(gs[2])
field.y = int(gs[3])
field.size = int(gs[4])
field.horizontal = gs[5] == 'H' # H -> True, V -> False
field.visible = gs[6] == 'V'
field.hjustify = gs[7]
field.vjustify = gs[8][0]
field.italic = gs[8][1] == 'I'
field.bold = gs[8][2] == 'B'
if gs[9]:
field.name = gs[9][1:-1]
else:
if field.number > 4:
raise SchLibError('Missing component field name', line, _sch_line_number, lib_name)
field.name = ['Reference', 'Value', 'Footprint', 'Datasheet'][field.number]
return field
class DrawPoligon(object):
pol_re = re.compile(r'P\s+(\d+)\s+' # 0 Number of points
r'(\d+)\s+' # 1 Sub-part (0 == all)
r'([012])\s+' # 2 Which representation (0 == both) for DeMorgan
r'(-?\d+)\s+' # 3 Thickness (Components from 74xx.lib has poligons with -1000)
r'((?:-?\d+\s+)+)' # 4 The points
r'([NFf])') # 5 Normal, Filled
def __init__(self):
super().__init__()
@staticmethod
def parse(line):
m = DrawPoligon.pol_re.match(line)
if not m:
logger.warning('Unknown poligon definition `{}`'.format(line))
return None
pol = DrawPoligon()
g = m.groups()
pol.points = int(g[0])
pol.sub_part = int(g[1])
pol.convert = int(g[2])
pol.thickness = int(g[3])
pol.fill = g[5]
coords = _split_space(g[4])
if len(coords) != 2*pol.points:
logger.warning('Expected {} coordinates and got {} in poligon'.format(2*pol.points, len(coords)))
pol.coords = coords
return pol
class DrawRectangle(object):
rec_re = re.compile(r'S\s+'
r'(-?\d+)\s+' # 0 Start X
r'(-?\d+)\s+' # 1 Start Y
r'(-?\d+)\s+' # 2 End X
r'(-?\d+)\s+' # 3 End X
r'(\d+)\s+' # 4 Sub-part (0 == all)
r'([012])\s+' # 5 Which representation (0 == both) for DeMorgan
r'(\d+)\s+' # 6 Thickness
r'([NFf])') # 7 Normal, Filled
def __init__(self):
super().__init__()
@staticmethod
def parse(line):
m = DrawRectangle.rec_re.match(line)
if not m:
logger.warning('Unknown square definition `{}`'.format(line))
return None
rec = DrawRectangle()
g = m.groups()
rec.start_x = int(g[0])
rec.start_y = int(g[1])
rec.end_x = int(g[2])
rec.end_y = int(g[3])
rec.sub_part = int(g[4])
rec.convert = int(g[5])
rec.thickness = int(g[6])
rec.fill = g[7]
return rec
class DrawCircle(object):
cir_re = re.compile(r'C\s+'
r'(-?\d+)\s+' # 0 Pos X
r'(-?\d+)\s+' # 1 Pos Y
r'(\d+)\s+' # 2 Radius
r'(\d+)\s+' # 3 Sub-part (0 == all)
r'([012])\s+' # 4 Which representation (0 == both) for DeMorgan
r'(\d+)\s+' # 5 Thickness
r'([NFf])') # 6 Normal, Filled
def __init__(self):
super().__init__()
@staticmethod
def parse(line):
m = DrawCircle.cir_re.match(line)
if not m:
logger.warning('Unknown circle definition `{}`'.format(line))
return None
cir = DrawCircle()
g = m.groups()
cir.pos_x = int(g[0])
cir.pos_y = int(g[1])
cir.radius = int(g[2])
cir.sub_part = int(g[3])
cir.convert = int(g[4])
cir.thickness = int(g[5])
cir.fill = g[6]
return cir
class DrawArc(object):
arc_re = re.compile(r'A\s+'
r'(-?\d+)\s+' # 0 Pos X
r'(-?\d+)\s+' # 1 Pos Y
r'(\d+)\s+' # 2 Radius
r'(-?\d+)\s+' # 3 Start
r'(-?\d+)\s+' # 4 End
r'(\d+)\s+' # 5 Sub-part (0 == all)
r'([012])\s+' # 6 Which representation (0 == both) for DeMorgan
r'(\d+)\s+' # 7 Thickness
r'([NFf])\s+' # 8 Normal, Filled
r'(-?\d+)\s+' # 9 Start Pos X
r'(-?\d+)\s+' # 10 Start Pos Y
r'(-?\d+)\s+' # 11 End Pos X
r'(-?\d+)') # 12 End Pos Y
def __init__(self):
super().__init__()
@staticmethod
def parse(line):
m = DrawArc.arc_re.match(line)
if not m:
logger.warning('Unknown arc definition `{}`'.format(line))
return None
arc = DrawArc()
g = m.groups()
arc.pos_x = int(g[0])
arc.pos_y = int(g[1])
arc.radius = int(g[2])
arc.start = int(g[3])
arc.end = int(g[4])
arc.sub_part = int(g[5])
arc.convert = int(g[6])
arc.thickness = int(g[7])
arc.fill = g[8]
arc.start_x = int(g[9])
arc.start_y = int(g[10])
arc.end_x = int(g[11])
arc.end_y = int(g[12])
return arc
class DrawText(object):
txt_re = re.compile(r'T\s+'
r'(\d+)\s+' # 0 Orientation (0 horizontal)
r'(-?\d+)\s+' # 1 Pos X
r'(-?\d+)\s+' # 2 Pos Y
r'(\d+)\s+' # 3 Dimension
r'(\d+)\s+' # 4 Type?
r'(\d+)\s+' # 5 Sub-part (0 == all)
r'([012])\s+' # 6 Which representation (0 == both) for DeMorgan
r'(\S+|"(?:[^"]|\\")+")\s+' # 7 Text
r'(Normal|Italic)\s+' # 8 Italic
r'([01])\s+' # 9 Bold
r'([CLR])\s+' # 10 HJustify
r'([CBT])') # 11 VJustify
def __init__(self):
super().__init__()
@staticmethod
def parse(line):
m = DrawText.txt_re.match(line)
if not m:
logger.warning('Unknown text definition `{}`'.format(line))
return None
txt = DrawText()
g = m.groups()
txt.vertical = g[0] != '0'
txt.pos_x = int(g[1])
txt.pos_y = int(g[2])
txt.size = int(g[3])
txt.type = int(g[4])
txt.sub_part = int(g[5])
txt.convert = int(g[6])
txt.text = un_quote(g[7])
txt.italic = g[8] == 'Italic'
txt.bold = g[9] == '1'
txt.hjustify = g[10]
txt.vjustify = g[11]
return txt
class Pin(object):
pin_re = re.compile(r'X\s+'
r'(\S+)\s+' # 0 Name (~ for empty)
r'(\S+)\s+' # 1 "Number" (alphanumeric)
r'(-?\d+)\s+' # 2 Pos X
r'(-?\d+)\s+' # 3 Pos Y
r'(\d+)\s+' # 4 Length
r'([RLUD])\s+' # 5 Direction
r'(\d+)\s+' # 6 Text size for the pin name
r'(\d+)\s+' # 7 Text size for the pin number
r'(\d+)\s+' # 8 Sub-part (0 == all)
r'([012])\s+' # 9 Which representation (0 == both) for DeMorgan
r'([IOBTPUWwCEN])' # 10 Electrical type
r'((?:\s+)\S+)?') # 11 Graphic type
def __init__(self):
super().__init__()
@staticmethod
def parse(line):
m = Pin.pin_re.match(line)
if not m:
logger.warning('Unknown pin definition `{}`'.format(line))
return None
pin = Pin()
g = m.groups()
pin.name = g[0]
pin.number = g[1]
pin.pos_x = int(g[2])
pin.pos_y = int(g[3])
pin.len = int(g[4])
pin.dir = g[5]
pin.size_name = int(g[6])
pin.size_num = int(g[7])
pin.sub_part = int(g[8])
pin.convert = int(g[9])
pin.type = g[10]
if len(g) == 12:
pin.gtype = g[11]
else:
pin.gtype = None
return pin
class LibComponent(object):
def_re = re.compile(r'DEF\s+'
r'(\S+)\s+' # 0 Name
r'(\S+)\s+' # 1 Reference prefix
r'(\S+)\s+' # 2 Unused field (0)
r'(-?\d+)\s+' # 3 Text offset
r'([YN])\s+' # 4 Draw pin number
r'([YN])\s+' # 5 Draw pin name
r'(\d+)\s+' # 6 Unit count
r'([LF])\s+' # 7 Unit is locked
r'([NP])') # 8 Power/Normal
def __init__(self, line, f, lib_name):
super().__init__()
self.dcm = None # Extra info from the Doc-Lib (DCM) file
m = self.def_re.match(line)
if m:
g = m.groups()
self.name = g[0]
self.ref_prefix = g[1]
self.unused = g[2]
self.text_offset = int(g[3])
self.draw_pinnumber = g[4] == 'Y'
self.draw_pinname = g[5] == 'Y'
self.unit_count = int(g[6])
self.units_locked = g[7] == 'L'
self.is_power = g[8] == 'P'
if self.name[0] == '~':
self.name = self.name[1:]
self.vname = True
else:
self.vname = False
if GS.debug_level > 1:
logger.debug('- Loading component {} from {}'.format(self.name, lib_name))
else:
logger.warning('Failed to load component definition: `{}`'.format(line))
self.fields = []
self.dfields = {}
self.alias = None
self.fp_list = []
self.draw = []
line = _get_line_dcm(f)
while not line.startswith('ENDDEF'):
if line[0] == 'F':
# A field
field = LibComponentField.parse(line, lib_name)
self.fields.append(field)
self.dfields[field.name.lower()] = field
elif line.startswith('ALIAS'):
self.alias = _split_space(line[6:])
elif line.startswith('$FPLIST'):
line = _get_line_dcm(f)
while not line.startswith('$ENDFPLIST'):
self.fp_list.append(line[1:])
line = _get_line_dcm(f)
elif line.startswith('DRAW'):
line = _get_line_dcm(f)
while not line.startswith('ENDDRAW'):
if line[0] == 'P':
self.draw.append(DrawPoligon.parse(line))
elif line[0] == 'S':
self.draw.append(DrawRectangle.parse(line))
elif line[0] == 'C':
self.draw.append(DrawCircle.parse(line))
elif line[0] == 'A':
self.draw.append(DrawArc.parse(line))
elif line[0] == 'T':
self.draw.append(DrawText.parse(line))
elif line[0] == 'X':
self.draw.append(Pin.parse(line))
else:
logger.warning('Unknown draw element `{}`'.format(line))
line = _get_line_dcm(f)
line = _get_line_dcm(f)
# def __repr__(self):
# s = 'Component('+self.name
# if self.desc:
# s += " desc: '{}'".format(self.desc)
# s += ')'
# return s
class SymLib(object):
""" Content from a symbols library """
def __init__(self):
super().__init__()
self.comps = OrderedDict()
self.alias = {}
def load(self, file):
""" Populates the class, file must exist """
logger.debug('Loading library `{}`'.format(file))
with open(file, 'rt') as f:
global _sch_line_number
_sch_line_number = 0
line = _get_line_lib(f)
if not line.startswith('EESchema-LIBRARY'):
raise SchLibError('Missing library signature', line, _sch_line_number)
line = _get_line_lib(f)
while not line.startswith('#End Library'):
if line.startswith('DEF'):
o = LibComponent(line, f, file)
self.comps[o.name] = o
if o.alias:
for a in o.alias:
self.alias[a] = o
else:
raise SchLibError('Unknown library entry', line, _sch_line_number)
line = _get_line_lib(f)
class DocLibEntry(object):
def __init__(self, name, f):
super().__init__()
self.name = name
self.desc = None
self.keys = None
self.datasheet = None
line = _get_line_dcm(f)
while not line.startswith('$ENDCMP'):
if line[0] == 'D':
self.desc = line[2:].lstrip()
elif line[0] == 'K':
self.keys = _split_space(line[2:])
elif line[0] == 'F':
self.datasheet = line[2:].lstrip()
else:
logger.warning('Unknown DCM entry `{}` on line {}'.format(line, _sch_line_number))
line = _get_line_dcm(f)
def __repr__(self):
s = 'DCM('+self.name
if self.desc:
s += " desc: '{}'".format(self.desc)
s += ')'
return s
class DocLib(object):
""" Content from a DCM """
def __init__(self):
super().__init__()
self.comps = OrderedDict()
def load(self, file):
""" Populates the class, file must exist """
logger.debug('Loading doc-lib `{}`'.format(file))
with open(file, 'rt') as f:
_sch_line_number = 0
line = _get_line_dcm(f)
if not line.startswith('EESchema-DOCLIB'):
raise SchLibError('Missing DCM signature', line, _sch_line_number)
line = _get_line_dcm(f)
while not line.startswith('#End Doc Library'):
if line.startswith('$CMP'):
o = DocLibEntry(line[5:].lstrip(), f)
self.comps[o.name] = o
if GS.debug_level > 1:
logger.debug('- '+repr(o))
else:
raise SchLibError('Unknown DCM entry', line, _sch_line_number)
line = _get_line_dcm(f)
class SchematicField(object):
field_re = re.compile(r'F\s+(\d+)\s+"([^"]*)"\s+([HV])\s+(-?\d+)\s+(-?\d+)\s+(\d+)\s+(\d+)'
# F n "text" orientation posx posy dimension flags hjustify vjustify/italic/bold "name"
field_re = re.compile(r'F\s*(\d+)\s+"([^"]*)"\s+([HV])\s+(-?\d+)\s+(-?\d+)\s+(\d+)\s+(\d+)'
r'\s+([LRCBT])\s+([LRCBT][IN][BN])\s*("[^"]*")?')
def __init__(self):
@ -105,6 +572,7 @@ class SchematicComponent(object):
self.value = ''
self.footprint = ''
self.datasheet = ''
self.desc = ''
def get_field_value(self, field):
field = field.lower()
@ -170,7 +638,7 @@ class SchematicComponent(object):
return '{} ({} {})'.format(self.ref, self.name, self.value)
@staticmethod
def load(f, sheet_path, sheet_path_h):
def load(f, sheet_path, sheet_path_h, libs):
# L lib:name reference
line = _get_line(f)
if line[0] != 'L':
@ -185,6 +653,9 @@ class SchematicComponent(object):
if len(res) == 2:
comp.name = res[1]
comp.lib = res[0]
libs[comp.lib] = None
else:
logger.warning("Component `{}` doesn't specify its library".format(comp.name))
# U N mm time_stamp
line = _get_line(f)
if line[0] != 'U':
@ -409,13 +880,13 @@ class SchematicSheet(object):
self.sheet = None
self.id = ''
def load_sheet(self, parent, sheet_path, sheet_path_h):
def load_sheet(self, parent, sheet_path, sheet_path_h, libs):
assert self.name
self.sheet = Schematic()
parent_dir = os.path.dirname(parent)
sheet_path += '/'+self.id
sheet_path_h += '/'+(self.name if self.name else 'Unknown')
self.sheet.load(os.path.join(parent_dir, self.file), sheet_path, sheet_path_h)
self.sheet.load(os.path.join(parent_dir, self.file), sheet_path, sheet_path_h, libs)
return self.sheet
@staticmethod
@ -469,6 +940,8 @@ class SchematicSheet(object):
class Schematic(object):
def __init__(self):
super().__init__()
self.dcms = {}
self.lib_comps = {}
def _get_title_block(self, f):
line = _get_line(f)
@ -500,11 +973,13 @@ class Schematic(object):
raise SchFileError('Wrong entry in title block', line, _sch_line_number)
self.title_block[m.group(1)] = m.group(2)
def load(self, fname, sheet_path='', sheet_path_h=''):
def load(self, fname, sheet_path='', sheet_path_h='', libs={}):
""" Load a v5.x KiCad Schematic.
The caller must be sure the file exists. """
The caller must be sure the file exists.
Only the schematics are loaded not the libs. """
logger.debug("Loading sheet from "+fname)
self.fname = fname
self.libs = libs
with open(fname, 'rt') as f:
global _sch_line_number
_sch_line_number = 0
@ -536,7 +1011,7 @@ class Schematic(object):
self.sheets = []
while not line.startswith('$EndSCHEMATC'):
if line.startswith('$Comp'):
obj = SchematicComponent.load(f, sheet_path, sheet_path_h)
obj = SchematicComponent.load(f, sheet_path, sheet_path_h, libs)
self.components.append(obj)
elif line.startswith('NoConn'):
obj = SchematicConnection.parse(False, line[7:])
@ -563,7 +1038,7 @@ class Schematic(object):
# Load sub-sheets
self.sub_sheets = []
for sch in self.sheets:
self.sub_sheets.append(sch.load_sheet(fname, sheet_path, sheet_path_h))
self.sub_sheets.append(sch.load_sheet(fname, sheet_path, sheet_path_h, libs))
def get_files(self):
""" A list of the names for all the sheets, including this one. """
@ -584,6 +1059,7 @@ class Schematic(object):
return components
def get_field_names(self, fields):
# TODO collect while loading
fields_lc = {v.lower(): 1 for v in fields}
for c in self.components:
for f in c.fields:
@ -594,3 +1070,68 @@ class Schematic(object):
for sch in self.sheets:
fields = sch.sheet.get_field_names(fields)
return fields
def walk_components(self, function, obj):
for c in self.components:
function(obj, c)
for sch in self.sheets:
sch.sheet.walk_components(function, obj)
@staticmethod
def apply_dcm(obj, c):
dcm = None
# Look for the DCM specific for the lib
if c.lib:
dcm = obj.dcms.get(c.lib)
if dcm:
entry = dcm.comps.get(c.name)
if entry and entry.desc:
c.desc = entry.desc
if GS.debug_level > 2:
logger.debug('Filling desc for {}:{} `{}`'.format(c.lib, c.name, c.desc))
def load_libs(self, fname):
KiConf.init(fname)
# Try to find the library paths
for k in self.libs.keys():
alias = KiConf.lib_aliases.get(k)
if k and alias:
self.libs[k] = alias.uri
if GS.debug_level > 1:
logger.debug('Using `{}` for library alias `{}`'.format(alias.uri, k))
else:
logger.warning('Missing library `{}`'.format(k))
# Load the libraries and descriptions
for k, v in self.libs.items():
if v:
# Load library
if os.path.isfile(v):
o = SymLib()
o.load(v)
else:
logger.warning('Missing library `{}` ({})'.format(v, k))
o = None
self.lib_comps[k] = o
# Load doc-lib
file = os.path.splitext(v)[0]+'.dcm'
if os.path.isfile(file):
o = DocLib()
o.load(file)
else:
o = None
self.dcms[k] = o
else:
# Mark as None if we don't know the file
self.lib_comps[k] = None
self.dcms[k] = None
# Join the descriptions with the components
for k in self.libs.keys():
lib = self.lib_comps[k]
dcm = self.dcms[k]
if lib and dcm:
for name, comp in lib.comps.items():
comp.dcm = dcm.comps.get(name)
if not comp.dcm:
logger.warning('Missing doc-lib entry for {}:{}'.format(k, name))
# Transfer the descriptions to the instances of the components
self.walk_components(self.apply_dcm, self)

View File

@ -123,6 +123,7 @@ def load_sch():
GS.sch = Schematic()
try:
GS.sch.load(GS.sch_file)
GS.sch.load_libs(GS.sch_file)
except SchFileError as e:
logger.error('At line {} of `{}`: {}'.format(e.args[2], GS.sch_file, e.args[0]))
logger.error('Line content: `{}`'.format(e.args[1]))