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:
parent
ae47afa695
commit
984b81fea1
|
|
@ -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 """
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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]))
|
||||
|
|
|
|||
Loading…
Reference in New Issue