Source code for tendril.gedaif.gsymlib

# Copyright (C) 2015 Chintalagiri Shashank
#
# This file is part of Tendril.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""
gEDA gsymlib Module (:mod:`tendril.gedaif.gsymlib`)
===================================================
"""


import os
import csv
import arrow
from future.utils import viewitems

import jinja2

import iec60063

from tendril.utils.config import GEDA_SYMLIB_ROOT
from tendril.utils.config import GEDA_SUBCIRCUITS_ROOT
from tendril.utils.config import AUDIT_PATH
from tendril.utils.config import TENDRIL_ROOT
from tendril.utils.config import INSTANCE_CACHE
from tendril.utils.config import MAKE_GSYMLIB_IMG_CACHE

from tendril.utils.fsutils import get_file_mtime
from tendril.utils.fsutils import VersionedOutputFile

from tendril.conventions.electronics import ident_transform
from tendril.conventions.electronics import fpismodlen
from tendril.conventions.electronics import fpiswire
from tendril.conventions.electronics import DEVICE_CLASSES
from tendril.conventions.electronics import construct_resistor
from tendril.conventions.electronics import construct_capacitor

from tendril.conventions.electronics import parse_capacitance
from tendril.conventions.electronics import parse_capacitor
from tendril.conventions.electronics import parse_resistance
from tendril.conventions.electronics import parse_resistor
from tendril.conventions.electronics import normalize_resistance

from tendril.utils.types.lengths import Length
from tendril.utils.types.electromagnetic import Resistance
from tendril.utils.types.electromagnetic import Capacitance

from gschem import conv_gsch2png

from tendril.utils.files import yml as yaml
from tendril.utils import log
logger = log.get_logger(__name__, log.INFO)


[docs]class EDASymbolBase(object): def __init__(self): """ Base class for EDA symbols. This class should not be used directly, but sub-classed per EDA suite, the sub-classes designed to interface with the way each EDA suite handles symbol libraries. .. todo:: This class is to eventually move into a ``tendril.edaif`` module, which should be used to proxy to the specific implementation for the EDA suite being used. The constructor for this class should be called from it's subclasses after all the necessary implementation-specific variables are created and filled in. """ self._device = '' self._value = '' self._footprint = '' self._status = None self._description = None self._package = None self._datasheet = None self._indicative_sourcing_info = None self._last_updated = None self._img_repr_path = None self._img_repr_fname = None self._get_sym() self._generate_img_repr()
[docs] def _get_sym(self): raise NotImplementedError
[docs] def _generate_img_repr(self): raise NotImplementedError
# Core Properties @property def device(self): return self._device @device.setter def device(self, value): self._device = value @property def value(self): return self._value @value.setter def value(self, value): self._value = value @property def footprint(self): return self._footprint @footprint.setter def footprint(self, value): self._footprint = value @property def status(self): return self._status @status.setter def status(self, value): self._status = value @property def description(self): return self._description @description.setter def description(self, value): self._description = value @property def package(self): return self._package @package.setter def package(self, value): self._package = value @property def last_updated(self): return self._last_updated @last_updated.setter def last_updated(self, value): self._last_updated = arrow.get(value) # Derived Properties @property def ident(self): return ident_transform(self.device, self.value, self.footprint) @property def ident_generic(self): return ident_transform(self.device, self.value, self.footprint, generic=True) @property def is_wire(self): return fpiswire(self.device) @property def is_modlen(self): return fpismodlen(self.device) @property def img_repr_fname(self): return self._img_repr_fname @property def indicative_sourcing_info(self): if self._indicative_sourcing_info is None: self._indicative_sourcing_info = self.sourcing_info_qty(1) return self._indicative_sourcing_info
[docs] def sourcing_info_qty(self, qty): from tendril.inventory.guidelines import electronics_qty from tendril.sourcing.electronics import get_sourcing_information from tendril.sourcing.electronics import SourcingException if fpiswire(self.device): iqty = Length(qty) else: iqty = qty iqty = electronics_qty.get_compliant_qty(self.ident, iqty) try: vsi = get_sourcing_information(self.ident, iqty, allvendors=True) except SourcingException: vsi = [] return vsi
@property def datasheet_url(self): if self._datasheet is not None: return self._datasheet for source in self.indicative_sourcing_info: if source.vpart.datasheet is not None: return source.vpart.datasheet @property def sym_ok(self): return self._validate()
[docs] def _validate(self): if self.device not in DEVICE_CLASSES: return False return True
# Status @property def is_virtual(self): if self.status == 'Virtual': return True return False @property def is_deprecated(self): if self.status == 'Deprecated': return True return False @property def is_experimental(self): if self.status == 'Experimental': return True return False @is_virtual.setter def is_virtual(self, value): if self.status == 'Generator': if value is True: self.status = 'Virtual' else: raise AttributeError def __repr__(self): return '{0:40}'.format(self.ident)
[docs]class GedaSymbol(EDASymbolBase): def __init__(self, fpath): """ gEDA symbols use a symbol file, located within the gEDA component library folders, usually defined within a ``gafrc`` file. Only the symbol filename is important, and not it's location relative to the component library root. This class accepts a (full) file path to a gEDA symbol in it's constructor, and loads all the necessary detail abouts the symbol into itself. gEDA symbols may also represent a sub-circuit in a hierarchical schematic. Support for handling this type of use is included here. .. todo:: Generator support is also built into this class for the moment. It should eventually be moved into :class:`EDASymbolBase` or a second base. It currently uses parameters seemingly specific gEDA, i.e., ``fpath`` and ``fname``. Handling single-file libraries such as those used by Eagle may need a more thought through approach. :param fpath: os path to the symbol file to be loaded """ self.fpath = fpath self.fname = os.path.split(fpath)[1] self.source = '' self._sch_img_repr_path = None self._sch_img_repr_fname = None super(GedaSymbol, self).__init__()
[docs] def _get_sym(self): self._acq_sym(self.fpath) if self.is_subcircuit: self._sch_img_repr()
[docs] def _acq_sym(self, fpath): _last_updated = get_file_mtime(fpath) with open(fpath, 'r') as f: for line in f.readlines(): if line.startswith('device='): self.device = line.split('=')[1].strip() if line.startswith('value='): self.value = line.split('=')[1].strip() if line.startswith('footprint'): self.footprint = line.split('=')[1].strip() if self.footprint[0:3] == 'MY-': self.footprint = self.footprint[3:] if line.startswith('description'): self.description = line.split('=')[1].strip() if line.startswith('status'): self.status = line.split('=')[1].strip() if line.startswith('package'): self.package = line.split('=')[1].strip() if line.startswith('source'): self.source = line.split('=')[1].strip() if self.status == '': self.status = 'Active' if self.is_generator: _genftime = get_file_mtime(self.genpath) if not _genftime or _genftime > _last_updated: _last_updated = _genftime if self.is_subcircuit: _schftime = get_file_mtime(self.schematic_path) if not _last_updated or _schftime > _last_updated: _last_updated = _schftime self.last_updated = _last_updated
[docs] def _generate_img_repr(self): outfolder = os.path.join(INSTANCE_CACHE, 'gsymlib') self._img_repr_fname = os.path.splitext(self.fname)[0] + '.png' self._img_repr_path = os.path.join(outfolder, self._img_repr_fname) if not os.path.exists(outfolder): os.makedirs(outfolder) if os.path.exists(self._img_repr_path): if get_file_mtime(self._img_repr_path) > get_file_mtime(self.fpath): # noqa return if MAKE_GSYMLIB_IMG_CACHE: conv_gsch2png(self.fpath, outfolder)
[docs] def _sch_img_repr(self): outfolder = os.path.join(INSTANCE_CACHE, 'gsymlib') self._sch_img_repr_fname = self.source + '.png' self._sch_img_repr_path = os.path.join(outfolder, self._sch_img_repr_fname) if not os.path.exists(outfolder): os.makedirs(outfolder) if os.path.exists(self._sch_img_repr_path): if get_file_mtime(self._sch_img_repr_path) > get_file_mtime(self.schematic_path): # noqa return if MAKE_GSYMLIB_IMG_CACHE: conv_gsch2png(self.schematic_path, outfolder, include_extension=True)
# Validation
[docs] def _validate(self): if self.is_subcircuit: if not self.source.endswith('.sch'): return False if not os.path.exists(self.schematic_path): return False return True return super(GedaSymbol, self)._validate()
# Subcircuits @property def is_subcircuit(self): if self.source != '': return True return False @property def schematic_path(self): if not self.is_subcircuit: raise AttributeError return os.path.join(GEDA_SUBCIRCUITS_ROOT, self.source) @property def schematic_fname(self): if not self.is_subcircuit: raise AttributeError return self.source @property def subcircuitident(self): if not self.is_subcircuit: raise AttributeError return os.path.splitext(self.fname)[0] @property def sch_img_repr_fname(self): return self._sch_img_repr_fname # Generators @property def is_generator(self): if self.status == 'Generator': return True return False @property def genident(self): return os.path.splitext(self.fname)[0] + '.gen' @property def genpath(self): if self.is_generator: return os.path.splitext(self.fpath)[0] + '.gen.yaml' else: raise AttributeError @property def generator(self): if not self.is_generator: raise AttributeError return GSymGeneratorFile(self.fpath) @property def idents(self): if not self.is_generator: raise AttributeError if not self.generator.values: return None return [ident_transform(self.device, v, self.footprint) for v in self.generator.values]
[docs]class GSymGeneratorFile(object): def __init__(self, sympath): self._genpath = os.path.splitext(sympath)[0] + '.gen.yaml' self._sympath = sympath self._type = None self._ivalues = [] self._igen = [] self._iunits = [] self._iseries = [] data = self._get_data() self._values = [] for value in data: if value is not None: self._values.append(value) @property def type(self): return self._type @property def igenerators(self): return self._igen @property def iseries(self): return self._iseries @property def ivalues(self): return self._ivalues @property def iunits(self): return self._iunits
[docs] def _get_data(self): with open(self._genpath) as genfile: gendata = yaml.load(genfile) if gendata["schema"]["name"] == "gsymgenerator" and \ gendata["schema"]["version"] == 1.0: if gendata['type'] == 'simple': self._type = 'Simple' self._ivalues = [v for v in gendata['values'] if v is not None and v.strip() is not None] return gendata['values'] values = [] if gendata['type'] == 'resistor': self._type = 'Resistor' giseries = None for resistance in gendata['resistances']: if resistance is not None: values.append(resistance) self._iunits.append(resistance) if 'composite_series' in gendata.keys(): from tendril.conventions.series import CustomValueSeries try: name = gendata['composite_series']['name'] tsymbol = GedaSymbol(self._sympath) giseries = CustomValueSeries( name, 'resistor', device=tsymbol.device, footprint=tsymbol.footprint ) except KeyError: pass if 'generators' in gendata.keys(): for generator in gendata['generators']: self._igen.append(generator) if generator['std'] == 'iec60063': rvalues = iec60063.gen_vals( generator['series'], iec60063.res_ostrs, start=generator['start'], end=generator['end'] ) for rvalue in rvalues: pval = construct_resistor( rvalue, generator['wattage'] ) values.append(pval) if giseries is not None: giseries.add_value(rvalue, pval) else: raise ValueError if giseries is not None: self._iseries.append(giseries) if 'values' in gendata.keys(): if gendata['values'][0].strip() != '': values += gendata['values'] self._ivalues.extend(gendata['values']) if 'custom_series' in gendata.keys(): from tendril.conventions.series import CustomValueSeries for name, series in viewitems(gendata['custom_series']): if series['detail'].pop('type') != 'resistor': raise ValueError('Expected a resistor series') vals = series['values'] tsymbol = GedaSymbol(self._sympath) iseries = CustomValueSeries(name, 'resistor', device=tsymbol.device, footprint=tsymbol.footprint) for type_val, val in viewitems(vals): iseries.add_value(type_val, val) iseries._desc = series['detail'].pop('desc') iseries._aparams = series['detail'] self._iseries.append(iseries) values.extend(vals.values()) return values if gendata['type'] == 'capacitor': self._type = 'Capacitor' giseries = None for capacitance in gendata['capacitances']: if capacitance is not None: values.append(capacitance) self._iunits.append(capacitance) if 'composite_series' in gendata.keys(): from tendril.conventions.series import CustomValueSeries try: name = gendata['composite_series']['name'] tsymbol = GedaSymbol(self._sympath) giseries = CustomValueSeries( name, 'capacitor', device=tsymbol.device, footprint=tsymbol.footprint ) except KeyError: pass if 'generators' in gendata.keys(): for generator in gendata['generators']: self._igen.append(generator) if generator['std'] == 'iec60063': cvalues = iec60063.gen_vals( generator['series'], iec60063.cap_ostrs, start=generator['start'], end=generator['end'] ) for cvalue in cvalues: pval = construct_capacitor( cvalue, generator['voltage'] ) values.append(pval) if giseries is not None: giseries.add_value(cvalue, pval) else: raise ValueError if giseries is not None: self._iseries.append(giseries) if 'values' in gendata.keys(): if gendata['values'][0].strip() != '': values += gendata['values'] self._ivalues.append(gendata['values']) if 'custom_series' in gendata.keys(): from tendril.conventions.series import CustomValueSeries for name, series in viewitems(gendata['custom_series']): if series['detail'].pop('type') != 'resistor': raise ValueError('Expected a resistor series') vals = series['values'] tsymbol = GedaSymbol(self._sympath) iseries = CustomValueSeries(name, 'resistor', device=tsymbol.device, footprint=tsymbol.footprint) for type_val, val in viewitems(vals): iseries.add_value(type_val, val) iseries._desc = series['detail'].pop('desc') iseries._aparams = series['detail'] self._iseries.append(iseries) values.extend(vals.values()) return values elif gendata['type'] == 'wire': for gauge in gendata['gauges']: for color in gendata['colors']: values.append('{0} {1}'.format(gauge, color)) return values else: logger.ERROR("Config file schema is not supported")
@property def values(self): if len(self._values) > 0: return self._values return None
[docs]def get_folder_symbols(path, template=None, resolve_generators=True, include_generators=False): if template is None: template = _jinja_init() symbols = [] files = [f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))] for f in files: if f.endswith(".sym"): symbol = GedaSymbol(os.path.join(path, f)) if symbol.is_generator: generators.append(symbol) if include_generators is True: symbols.append(symbol) if resolve_generators is True: genpath = os.path.splitext(symbol.fpath)[0] + '.gen.yaml' if os.path.exists(genpath): genfile = GSymGeneratorFile(symbol.fpath) values = genfile.values if values is not None: for value in values: if value is not None: vsymbol = GedaSymbol(symbol.fpath) vsymbol.is_virtual = True vsymbol.value = value symbols.append(vsymbol) else: stage = {'symbolfile': os.path.split(symbol.fpath)[1], 'value': symbol.value.strip(), 'description': symbol.description} with open(genpath, 'w') as gf: gf.write(template.render(stage=stage)) else: if symbol.is_subcircuit: subcircuits.append(symbol) symbols.append(symbol) # TODO This needs to be reimplemented in a cleaner form. if symbol.value.startswith('DUAL'): nsymbol = GedaSymbol(symbol.fpath) nsymbol.value = symbol.value.split(' ', 1)[1] symbols.append(nsymbol) return symbols
[docs]def gen_symlib(path=GEDA_SYMLIB_ROOT, recursive=True, resolve_generators=True, include_generators=False): symbols = [] template = _jinja_init() if recursive: for root, dirs, files in os.walk(path): symbols += get_folder_symbols( root, template, resolve_generators=resolve_generators, include_generators=include_generators ) else: symbols = get_folder_symbols( path, template, resolve_generators=resolve_generators, include_generators=include_generators ) return symbols
[docs]def gen_index(symlist, upper=False): lindex = {} for symbol in symlist: ident = symbol.ident_generic if upper: ident = ident.upper() if ident in lindex.keys(): lindex[ident].append(symbol) else: lindex[ident] = [symbol] return lindex
[docs]def _jinja_init(): templates_path = os.path.join(TENDRIL_ROOT, 'gedaif', 'templates') logger.debug("Loading templates from " + templates_path) loader = jinja2.FileSystemLoader( searchpath=templates_path ) renderer = jinja2.Environment(loader=loader) template_file = 'generator.gen.yaml' template = renderer.get_template(template_file) return template
generators = [] subcircuits = [] custom_series = {} gsymlib = [] index = {} index_upper = {} generator_names = [] subcircuit_names = [] gsymlib_idents = []
[docs]def regenerate_symlib(): global generators generators = [] global subcircuits subcircuits = [] global gsymlib gsymlib = gen_symlib(GEDA_SYMLIB_ROOT) global index index = gen_index(gsymlib) global index_upper index_upper = gen_index(gsymlib, upper=True) global generator_names generator_names = [os.path.splitext(x.fname)[0] + '.gen' for x in generators] global subcircuit_names subcircuit_names = [x.subcircuitident for x in subcircuits] global gsymlib_idents gsymlib_idents = set(index.keys()) global custom_series custom_series = {} for sym in generators: for iseries in sym.generator.iseries: custom_series[iseries._name] = iseries
regenerate_symlib()
[docs]def get_generator(gen): for generator in generators: if os.path.splitext(generator.fname)[0] + '.gen' == gen: return generator
[docs]def get_subcircuit(sc): for subcircuit in subcircuits: if subcircuit.subcircuitident == sc: return subcircuit
[docs]def is_recognized(ident): if ident in gsymlib_idents: return True return False
[docs]def get_symbol(ident, case_insensitive=False, get_all=False): if case_insensitive is False: if ident in index.keys(): if not get_all: return index[ident][0] else: return index[ident] else: if ident.upper() in index_upper.keys(): if not get_all: return index_upper[ident.upper()][0] else: return index_upper[ident.upper()] raise NoGedaSymbolException(ident)
[docs]def get_symbol_folder(ident, case_insensitive=False): symobj = get_symbol(ident, case_insensitive=case_insensitive) sympath = symobj.fpath symfolder = os.path.split(sympath)[0] return os.path.relpath(symfolder, GEDA_SYMLIB_ROOT)
[docs]def get_latest_symbols(n=10, include_virtual=False): if include_virtual is False: tlib = [x for x in gsymlib if x.is_virtual is False] else: tlib = gsymlib slib = sorted(tlib, key=lambda y: y.last_updated, reverse=True) return slib[:n]
[docs]def find_capacitor(capacitance, footprint, device='CAP CER SMD', voltage=None): if isinstance(capacitance, str): try: capacitance = Capacitance(capacitance) except ValueError: raise NoGedaSymbolException(capacitance) if isinstance(capacitance, Capacitance): capacitance = capacitance._value if footprint[0:3] == "MY-": footprint = footprint[3:] for symbol in gsymlib: if symbol.device == device and symbol.footprint == footprint: cap, volt = parse_capacitor(symbol.value) sym_capacitance = parse_capacitance(cap) if capacitance == sym_capacitance: return symbol raise NoGedaSymbolException
[docs]def find_resistor(resistance, footprint, device='RES SMD', wattage=None): # TODO This should return a symbol instead, and usages should be adapted # accordingly to make consistent with find_capacitor if isinstance(resistance, str): try: resistance = Resistance(resistance) except ValueError: raise NoGedaSymbolException(resistance) if isinstance(resistance, Resistance): resistance = resistance._value if footprint[0:3] == "MY-": footprint = footprint[3:] if device == 'RES THRU': resistances = iec60063.gen_vals(iec60063.get_series('E24'), iec60063.res_ostrs) if resistance in [parse_resistance(x) for x in resistances]: return construct_resistor(normalize_resistance(resistance), '0.25W') # noqa else: raise NoGedaSymbolException(resistance, device) for symbol in gsymlib: if symbol.device == device and symbol.footprint == footprint: res, watt = parse_resistor(symbol.value) sym_resistance = parse_resistance(res) if resistance == sym_resistance: return symbol.value raise NoGedaSymbolException(resistance)
[docs]def export_gsymlib_audit(): auditfname = os.path.join(AUDIT_PATH, 'gsymlib-audit.csv') outf = VersionedOutputFile(auditfname) outw = csv.writer(outf) outw.writerow(['filename', 'status', 'ident', 'device', 'value', 'footprint', 'description', 'path', 'package']) for symbol in gsymlib: outw.writerow( [symbol.fname, symbol.status, symbol.ident, symbol.device, symbol.value, symbol.footprint, symbol.description, symbol.fpath, symbol.package] ) outf.close()
[docs]class NoGedaSymbolException(Exception): pass