From 158f267eb568edfc5aeaeb5e22d158cea2a9c2a6 Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Thu, 30 Mar 2023 12:39:36 -0300 Subject: [PATCH] Support for extra data in the Value field - Currently we just use the tolerance for the 3D resistors - Uses a port of the JavaScript Electro-Grammar --- CHANGELOG.md | 7 + MANIFEST.in | 1 + Makefile | 2 +- debian/install | 1 + kibot/PcbDraw/README.md | 82 ++++++- kibot/PcbDraw/plot.py | 27 ++- kibot/PcbDraw/unit.py | 6 +- kibot/bom/bom.py | 36 +-- kibot/bom/electro_grammar.py | 218 ++++++++++++++++++ kibot/bom/units.py | 154 +++++++++---- kibot/misc.py | 1 + kibot/out_base_3d.py | 28 ++- kibot/resources/parsers/electro.lark | 214 +++++++++++++++++ setup.cfg | 1 + .../kicad_6/resistor_tht.kicad_pcb | 2 +- .../kicad_6/resistor_tht.kicad_sch | 4 +- .../kicad_7/resistor_tht.kicad_pcb | 2 +- .../kicad_7/resistor_tht.kicad_sch | 2 +- tests/test_plot/test_int_bom.py | 4 +- tests/test_plot/test_misc_2.py | 184 +++++++++++++-- 20 files changed, 840 insertions(+), 136 deletions(-) create mode 100644 kibot/bom/electro_grammar.py create mode 100644 kibot/resources/parsers/electro.lark diff --git a/CHANGELOG.md b/CHANGELOG.md index 179a87e7..701c5a1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 using all pages or individually. - Plot related outputs: - All outputs now support scaling. +- BoM: + - Support for extra information in the *Value* field. + Currently just parsed, not rejected. ### Fixed - Makefile: don't skip all preflights on each run, just the ones we generate @@ -33,6 +36,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Diff: - Problems when using an output and no variant specified. +### Changed: +- Some R, L and C values that were rejected are accepted now. You just get a + warning about what part of the value was discarded. + ## [1.6.1] - 2023-03-16 ### Added diff --git a/MANIFEST.in b/MANIFEST.in index 26c16f11..2a74988a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -11,6 +11,7 @@ include kibot/resources/config_templates/panelize/*.yaml include kibot/resources/config_templates/*.yaml include kibot/resources/images/*.svg include kibot/resources/images/*.ico +include kibot/resources/parsers/*.lark include kibot/resources/pcbdraw/styles/*.json include kibot/resources/pcbdraw/templates/*.handlebars include kibot/blender_scripts/*.py diff --git a/Makefile b/Makefile index e5e68db2..f1054a23 100644 --- a/Makefile +++ b/Makefile @@ -48,7 +48,7 @@ test: lint rm -f tests/.local $(PY_COV) erase # python3-pytest-xdist - $(PYTEST) -m "not slow" -n 2 --test_dir=output + $(PYTEST) -m "not slow" -n 4 --test_dir=output $(PYTEST) -m "slow" --test_dir=output $(PY_COV) combine $(PY_COV) report diff --git a/debian/install b/debian/install index 5f35b709..74f74491 100644 --- a/debian/install +++ b/debian/install @@ -4,3 +4,4 @@ kibot/resources/kicad_colors/ /usr/share/kibot/ kibot/resources/kicad_layouts/ /usr/share/kibot/ kibot/resources/pcbdraw/ /usr/share/kibot/ kibot/resources/report_templates/ /usr/share/kibot/ +kibot/resources/parsers/ /usr/share/kibot/ diff --git a/kibot/PcbDraw/README.md b/kibot/PcbDraw/README.md index 68425dfe..a795812b 100644 --- a/kibot/PcbDraw/README.md +++ b/kibot/PcbDraw/README.md @@ -218,6 +218,7 @@ This file comes from KiKit, but it has too much in common with `populate.py`. ## 2023-03-20 Various fixes and changes in resistor colors +```diff diff --git a/kibot/PcbDraw/plot.py b/kibot/PcbDraw/plot.py index 8ca660e6..9dc45ba9 100644 --- a/kibot/PcbDraw/plot.py @@ -288,10 +289,11 @@ index 8ca660e6..9dc45ba9 100644 ref = footprint.GetReference().strip() center = footprint.GetPosition() orient = math.radians(footprint.GetOrientation().AsDegrees()) - +``` ## 2023-03-27 Fixe for KiCad 7.0.1 polygons +```diff diff --git a/kibot/PcbDraw/plot.py b/kibot/PcbDraw/plot.py index 9dc45ba9..8df84469 100644 --- a/kibot/PcbDraw/plot.py @@ -320,4 +322,82 @@ index 9dc45ba9..8df84469 100644 elif svg_element.tag == "circle": # Convert circle to path att = svg_element.attrib +``` +## 2023-03-30 Removed the tolerance look-up, now using electro_grammar + +So now *unit.py* is in charge of returning the tolerance. +Note that we still use a field, but in a very ridiculous way because we add it to the value, to then separate it. + +```diff +diff --git a/kibot/PcbDraw/plot.py b/kibot/PcbDraw/plot.py +index 23b7d31f..65fbea66 100644 +--- a/kibot/PcbDraw/plot.py ++++ b/kibot/PcbDraw/plot.py +@@ -938,21 +938,20 @@ class PlotComponents(PlotInterface): + return + + def _get_resistance_from_value(self, value: str) -> Tuple[Decimal, str]: +- res, tolerance = None, str(GS.global_default_resistor_tolerance)+"%" +- value_l = value.split(" ", maxsplit=1) ++ res, tolerance = None, None + try: +- res = read_resistance(value_l[0]) ++ res, tolerance = read_resistance(value) + except ValueError: +- raise UserWarning(f"Invalid resistor value {value_l[0]}") +- if len(value_l) > 1: +- t_string = value_l[1].strip().replace(" ", "") +- if "%" in t_string: +- s = self._plotter.get_style("tht-resistor-band-colors") +- if not isinstance(s, dict): +- raise RuntimeError(f"Invalid style specified, tht-resistor-band-colors should be dictionary, got {type(s)}") +- if t_string.strip() not in s: +- raise UserWarning(f"Invalid resistor tolerance {value_l[1]}") +- tolerance = t_string ++ raise UserWarning(f"Invalid resistor value {value}") ++ if tolerance is None: ++ tolerance = GS.global_default_resistor_tolerance ++ tolerance = str(tolerance)+"%" ++ s = self._plotter.get_style("tht-resistor-band-colors") ++ if not isinstance(s, dict): ++ raise RuntimeError(f"Invalid style specified, tht-resistor-band-colors should be dictionary, got {type(s)}") ++ if tolerance not in s: ++ raise UserWarning(f"Invalid resistor tolerance {tolerance}") ++ tolerance = "5%" + return res, tolerance + + +@@ -1113,7 +1112,7 @@ class PcbPlotter(): + prop = footprint.GetProperties() + tol = next(filter(lambda x: x, map(prop.get, GS.global_field_tolerance)), None) + if tol: +- value = value+' '+tol ++ value = value+' '+tol.strip() + ref = footprint.GetReference().strip() + center = footprint.GetPosition() + orient = math.radians(footprint.GetOrientation().AsDegrees()) +diff --git a/kibot/PcbDraw/unit.py b/kibot/PcbDraw/unit.py +index 2fad683c..0c5dfcab 100644 +--- a/kibot/PcbDraw/unit.py ++++ b/kibot/PcbDraw/unit.py +@@ -1,10 +1,9 @@ + # Author: Salvador E. Tropea + # License: MIT +-from decimal import Decimal + from ..bom.units import comp_match + + +-def read_resistance(value: str) -> Decimal: ++def read_resistance(value: str): + """ + Given a string, try to parse resistance and return it as Ohms (Decimal) + +@@ -13,5 +12,4 @@ def read_resistance(value: str) -> Decimal: + res = comp_match(value, 'R') + if res is None: + raise ValueError(f"Cannot parse '{value}' to resistance") +- v, mul, uni = res +- return Decimal(str(v))*Decimal(str(mul[0])) ++ return res.get_decimal(), res.get_extra('tolerance') +``` diff --git a/kibot/PcbDraw/plot.py b/kibot/PcbDraw/plot.py index 23b7d31f..65fbea66 100644 --- a/kibot/PcbDraw/plot.py +++ b/kibot/PcbDraw/plot.py @@ -938,21 +938,20 @@ class PlotComponents(PlotInterface): return def _get_resistance_from_value(self, value: str) -> Tuple[Decimal, str]: - res, tolerance = None, str(GS.global_default_resistor_tolerance)+"%" - value_l = value.split(" ", maxsplit=1) + res, tolerance = None, None try: - res = read_resistance(value_l[0]) + res, tolerance = read_resistance(value) except ValueError: - raise UserWarning(f"Invalid resistor value {value_l[0]}") - if len(value_l) > 1: - t_string = value_l[1].strip().replace(" ", "") - if "%" in t_string: - s = self._plotter.get_style("tht-resistor-band-colors") - if not isinstance(s, dict): - raise RuntimeError(f"Invalid style specified, tht-resistor-band-colors should be dictionary, got {type(s)}") - if t_string.strip() not in s: - raise UserWarning(f"Invalid resistor tolerance {value_l[1]}") - tolerance = t_string + raise UserWarning(f"Invalid resistor value {value}") + if tolerance is None: + tolerance = GS.global_default_resistor_tolerance + tolerance = str(tolerance)+"%" + s = self._plotter.get_style("tht-resistor-band-colors") + if not isinstance(s, dict): + raise RuntimeError(f"Invalid style specified, tht-resistor-band-colors should be dictionary, got {type(s)}") + if tolerance not in s: + raise UserWarning(f"Invalid resistor tolerance {tolerance}") + tolerance = "5%" return res, tolerance @@ -1113,7 +1112,7 @@ class PcbPlotter(): prop = footprint.GetProperties() tol = next(filter(lambda x: x, map(prop.get, GS.global_field_tolerance)), None) if tol: - value = value+' '+tol + value = value+' '+tol.strip() ref = footprint.GetReference().strip() center = footprint.GetPosition() orient = math.radians(footprint.GetOrientation().AsDegrees()) diff --git a/kibot/PcbDraw/unit.py b/kibot/PcbDraw/unit.py index 2fad683c..0c5dfcab 100644 --- a/kibot/PcbDraw/unit.py +++ b/kibot/PcbDraw/unit.py @@ -1,10 +1,9 @@ # Author: Salvador E. Tropea # License: MIT -from decimal import Decimal from ..bom.units import comp_match -def read_resistance(value: str) -> Decimal: +def read_resistance(value: str): """ Given a string, try to parse resistance and return it as Ohms (Decimal) @@ -13,5 +12,4 @@ def read_resistance(value: str) -> Decimal: res = comp_match(value, 'R') if res is None: raise ValueError(f"Cannot parse '{value}' to resistance") - v, mul, uni = res - return Decimal(str(v))*Decimal(str(mul[0])) + return res.get_decimal(), res.get_extra('tolerance') diff --git a/kibot/bom/bom.py b/kibot/bom/bom.py index 561c87f2..00f56281 100644 --- a/kibot/bom/bom.py +++ b/kibot/bom/bom.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2020-2022 Salvador E. Tropea -# Copyright (c) 2020-2022 Instituto Nacional de Tecnología Industrial +# Copyright (c) 2020-2023 Salvador E. Tropea +# Copyright (c) 2020-2023 Instituto Nacional de Tecnología Industrial # Copyright (c) 2016-2020 Oliver Henry Walters (@SchrodingersGat) # License: MIT # Project: KiBot (formerly KiPlot) @@ -12,7 +12,7 @@ All the logic to convert a list of components into the rows and columns used to import locale from copy import deepcopy from math import ceil -from .units import compare_values, comp_match, get_last_warning +from .units import compare_values, comp_match from .bom_writer import write_bom from .columnlist import ColumnList from ..misc import DNF, W_FIELDCONF, W_MISSFPINFO @@ -382,14 +382,7 @@ def get_value_sort(comp, fallback_ref=False): """ Try to better sort R, L and C components """ res = comp.value_sort if res: - value, (mult, mult_s), unit = res - if comp.ref_prefix in "CL": - # femto Farads - value = "{0:15d}".format(int(value * 1e15 * mult + 0.1)) - else: - # milli Ohms - value = "{0:15d}".format(int(value * 1000 * mult + 0.1)) - return value + return res.get_sortable() if fallback_ref: return comp.ref_prefix + "{0:15d}".format(_suffix_to_num(comp.ref_suffix)) return comp.value @@ -397,14 +390,11 @@ def get_value_sort(comp, fallback_ref=False): def normalize_value(c, decimal_point): if c.value_sort is None: - return c.value - value, (mult, mult_s), unit = c.value_sort - ivalue = int(value) - if value == ivalue: - value = ivalue - elif decimal_point: - value = str(value).replace('.', decimal_point) - return '{} {}{}'.format(value, mult_s, unit) + return c.value.strip() + value = str(c.value_sort) + if decimal_point: + value = value.replace('.', decimal_point) + return value def compute_multiple_stats(cfg, groups): @@ -443,14 +433,6 @@ def group_components(cfg, components): # Cache the value used to sort if c.ref_prefix in RLC_PREFIX and c.value.lower() not in DNF: c.value_sort = comp_match(c.value, c.ref_prefix, c.ref) - if c.value_sort is None and (' ' in c.value): - # Try with the data before a space - value = c.value.split(' ')[0] - value_sort = comp_match(value, c.ref_prefix) - if value_sort is not None: - c.value_sort = value_sort - extra = ', only for sorting purposes' if not cfg.normalize_values else '' - logger.warning(get_last_warning() + "Using `{}` for {} instead{}".format(value, c.ref, extra)) else: c.value_sort = None # Try to add the component to an existing group diff --git a/kibot/bom/electro_grammar.py b/kibot/bom/electro_grammar.py new file mode 100644 index 00000000..6a134e16 --- /dev/null +++ b/kibot/bom/electro_grammar.py @@ -0,0 +1,218 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2023 Salvador E. Tropea +# Copyright (c) 2023 Instituto Nacional de Tecnología Industrial +# License: MIT +# Project: KiBot (formerly KiPlot) + +from decimal import Decimal +from lark import Lark, Transformer +import os +from ..gs import GS +from .. import log + +logger = log.get_logger() +# Metric to imperial package sizes +TO_IMPERIAL = {'0402': '01005', + '0603': '0201', + '1005': '0402', + '1608': '0603', + '2012': '0805', + '2520': '1008', + '3216': '1206', + '3225': '1210', + '4516': '1806', + '4532': '1812', + '5025': '2010', + '6332': '2512'} +parser = None + + +class ComponentTransformer(Transformer): + """ Transforms a tree parsed by Lark to the electro-grammar dict """ + def __init__(self): + self.parsed = {} + # Extra information, not in the original lib and needed for internal purposes + self.extra = {} + + def value3(self, d, type): + """ VALUE [METRIC_PREFIX [MANTISSA]] """ + v = Decimal(d[0]) + c = len(d) + if c >= 3: + # We have something like 2n2 + dec = d[2] + c_dec = len(dec) + v += Decimal(dec)/(Decimal(10)*c_dec) + self.extra['val'] = v + if c >= 2: + # Metric prefix + v *= d[1] + self.extra['mult'] = d[1] + else: + self.extra['mult'] = Decimal(1) + v = float(v) + self.parsed[type] = v + return v + + def value2(self, d, type): + """ VALUE [MANTISSA] """ + v = float(d[0]) + c = len(d) + if c >= 2: + # We have something like 3V3 + dec = d[1] + c_dec = len(dec) + v += float(dec)/(10.0*c_dec) + self.parsed[type] = v + return v + + def value1(self, d, type): + """ VALUE """ + v = float(d[0]) + iv = int(d[0]) + if iv == v: + v = iv + self.parsed[type] = v + return v + + def tolerance(self, d): + return self.value1(d, 'tolerance') + + def voltage_rating(self, d): + return self.value2(d, 'voltage_rating') + + def temp_coef(self, d): + c_len = len(d) + if c_len == 3: + # Class 2: i.e. X7R + v = d[0].value+d[1].value+d[2].value + else: + # Class 1: i.e. C0G + v = d[0].type + self.parsed['characteristic'] = v.upper() + return v + + def power_rating(self, d): + if len(d) == 1: + # 1 W + v = float(d[0]) + elif d[0].type == 'INT': + # 1/4 W + v = float(d[0].value)/float(d[1].value) + else: + # 250 mW + v = float(Decimal(d[0].value)*d[1]) + self.parsed['power_rating'] = v + return v + + def color(self, d): + c = d[0].value.lower() + self.parsed['color'] = c + return c + + def set_type(self, type, d): + self.parsed['type'] = type + return d + + # Package size + def imperial_size(self, d): + s = d[0].value + self.parsed['size'] = s + return s + + def unambigious_metric_size(self, d): + s = TO_IMPERIAL[d[0].value] + self.parsed['size'] = s + return s + + metric_size_base = unambigious_metric_size + + # RLC + def resistance(self, d): + return self.value3(d, 'resistance') + + resistance_no_r = resistance + + def inductance(self, d): + return self.value3(d, 'inductance') + + inductance_no_henry = inductance + + def capacitance(self, d): + return self.value3(d, 'capacitance') + + capacitance_no_farad = capacitance + + # Known components + def inductor(self, d): + return self.set_type('inductor', d) + + def capacitor(self, d): + return self.set_type('capacitor', d) + + def resistor(self, d): + return self.set_type('resistor', d) + + def led(self, d): + return self.set_type('led', d) + + # Metrix prefixes + def giga(self, _): + return Decimal('1e9') + + def mega(self, _): + return Decimal('1e6') + + def kilo(self, _): + return Decimal('1e3') + + def unit(self, _): + return Decimal(1) + + def milli(self, _): + return Decimal('1e-3') + + def nano(self, _): + return Decimal('1e-9') + + def micro(self, _): + return Decimal('1e-6') + + def pico(self, _): + return Decimal('1e-12') + + def femto(self, _): + return Decimal('1e-15') + + def crap(self, v): + if 'discarded' in self.extra: + self.extra['discarded'].append(v[0].value) + else: + self.extra['discarded'] = [v[0].value] + return None + + +def initialize(): + global parser + if parser is not None: + return + with open(os.path.join(GS.get_resource_path('parsers'), 'electro.lark'), 'rt') as f: + g = f.read() + parser = Lark(g, start='main') # , debug=DEBUG) + + +def parse(text, with_extra=False): + initialize() + try: + tree = parser.parse(text) + except Exception as e: + logger.debugl(2, str(e)) + return {} + logger.debugl(3, tree.pretty()) + res_o = ComponentTransformer() + res = res_o.transform(tree) + logger.debugl(3, res) + res = res_o.parsed + if with_extra: + res.update(res_o.extra) + return res diff --git a/kibot/bom/units.py b/kibot/bom/units.py index 06def9af..c0e18456 100644 --- a/kibot/bom/units.py +++ b/kibot/bom/units.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2020 Salvador E. Tropea -# Copyright (c) 2020 Instituto Nacional de Tecnología Industrial +# Copyright (c) 2020-2023 Salvador E. Tropea +# Copyright (c) 2020-2023 Instituto Nacional de Tecnología Industrial # Copyright (c) 2016-2020 Oliver Henry Walters (@SchrodingersGat) # License: MIT # Project: KiBot (formerly KiPlot) @@ -13,10 +13,13 @@ e.g. 0R1 = 0.1Ohm (Unit replaces decimal, different units) Oriented to normalize and sort R, L and C values. """ +from decimal import Decimal import re import locale +from math import log10 from .. import log -from ..misc import W_BADVAL1, W_BADVAL2, W_BADVAL3 +from ..misc import W_BADVAL1, W_BADVAL2, W_BADVAL3, W_BADVAL4 +from .electro_grammar import parse logger = log.get_logger() @@ -30,25 +33,54 @@ PREFIX_GIGA = ["giga", "g"] # All prefixes PREFIX_ALL = PREFIX_PICO + PREFIX_NANO + PREFIX_MICRO + PREFIX_MILLI + PREFIX_KILO + PREFIX_MEGA + PREFIX_GIGA +MAX_POW_PREFIX = 9 +MIN_POW_PREFIX = -12 +PREFIXES = {-15: 'f', -12: 'p', -9: 'n', -6: u"µ", -3: 'm', 0: '', 3: 'k', 6: 'M', 9: 'G'} # Common methods of expressing component units # Note: we match lowercase string, so both: Ω and Ω become the lowercase omega UNIT_R = ["r", "ohms", "ohm", u'\u03c9'] UNIT_C = ["farad", "f"] UNIT_L = ["henry", "h"] +OHMS = u"Ω" UNIT_ALL = UNIT_R + UNIT_C + UNIT_L +GRAM_TYPES = {'inductor': 'L', 'capacitor': 'C', 'resistor': 'R', 'led': ''} # Compiled regex to match the values match = None # Current locale decimal point value decimal_point = None -# Last warning -last_warning = '' +# Parser cache +parser_cache = {} -def get_last_warning(): - return last_warning +class ParsedValue(object): + def __init__(self, v, pow, unit, extra=None): + # From a value that matched the regex + ival = int(v) + self.norm_val = int(v) if v == ival else v + self.exp = pow + self.unit = unit + self.prefix = PREFIXES[pow] + self.extra = extra + + def __str__(self): + return '{} {}{}'.format(self.norm_val, self.prefix, self.unit) + + def get_sortable(self): + mult = pow(10, self.exp) + if self.unit in "FH": + # femto Farads + return "{0:15d}".format(int(self.norm_val * 1e15 * mult + 0.1)) + # milli Ohms + return "{0:15d}".format(int(self.norm_val * 1000 * mult + 0.1)) + + def get_decimal(self): + return Decimal(str(self.norm_val))*pow(10, Decimal(self.exp)) + + def get_extra(self, property): + return self.extra.get(property) if self.extra else None def get_unit(unit, ref_prefix): @@ -58,42 +90,54 @@ def get_unit(unit, ref_prefix): return "H" if ref_prefix == 'C': return "F" - return u"Ω" + return OHMS unit = unit.lower() if unit in UNIT_R: - return u"Ω" + return OHMS if unit in UNIT_C: return "F" if unit in UNIT_L: return "H" -def get_prefix(prefix): +def get_prefix_simple(prefix): """ Return the (numerical) value of a given prefix """ if not prefix: - return 1, '' + return 0 # 'M' is mega, 'm' is milli if prefix != 'M': prefix = prefix.lower() if prefix in PREFIX_PICO: - return 1.0e-12, 'p' + return -12 if prefix in PREFIX_NANO: - return 1.0e-9, 'n' + return -9 if prefix in PREFIX_MICRO: - return 1.0e-6, u"µ" + return -6 if prefix in PREFIX_MILLI: - return 1.0e-3, 'm' + return -3 if prefix in PREFIX_KILO: - return 1.0e3, 'k' + return 3 if prefix in PREFIX_MEGA: - return 1.0e6, 'M' + return 6 if prefix in PREFIX_GIGA: - return 1.0e9, 'G' + return 9 # Unknown, we shouldn't get here because the regex matched # BUT: I found that sometimes unexpected things happen, like mu matching micro and then we reaching this code # Now is fixed, but I can't be sure some bizarre case is overlooked logger.error('Unknown prefix, please report') - return 1, '' + return 0 + + +def get_prefix(val, prefix): + pow = get_prefix_simple(prefix) + # Try to normalize it + while val >= 1000.0 and pow < MAX_POW_PREFIX: + val /= 1000.0 + pow += 3 + while val < 1.0 and pow > MIN_POW_PREFIX: + val *= 1000.0 + pow -= 3 + return val, pow def group_string(group): # Return a reg-ex string for a list of values @@ -104,14 +148,27 @@ def match_string(): return r"(\d*\.?\d*)\s*(" + group_string(PREFIX_ALL) + ")*(" + group_string(UNIT_ALL) + r")*(\d*)$" +def value_from_grammar(r): + """ Convert a result parsed by the Lark grammar to a ParsedResult object """ + val = r.get('val') + if not val: + return None + # Create an object with the result + val, pow = get_prefix(float(val), PREFIXES[int(log10(r['mult']))]) + parsed = ParsedValue(val, pow, get_unit(GRAM_TYPES[r['type']], ''), r) + return parsed + + def comp_match(component, ref_prefix, ref=None): """ Return a normalized value and units for a given component value string - e.g. comp_match('10R2') returns (10, R) - e.g. comp_match('3.3mOhm') returns (0.0033, R) + Also tries to separate extra data, i.e. tolerance, using a complex parser """ - global last_warning original = component + global parser_cache + parsed = parser_cache.get(original+ref_prefix) + if parsed: + return parsed # Remove useless spaces component = component.strip() # ~ is the same as empty for KiCad @@ -128,6 +185,7 @@ def comp_match(component, ref_prefix, ref=None): if decimal_point: component = component.replace(decimal_point, ".") + with_commas = component # Remove any commas component = component.strip().replace(",", "") @@ -140,13 +198,22 @@ def comp_match(component, ref_prefix, ref=None): where = ' in {}'.format(ref) if ref is not None else '' result = match.match(component) if not result: - last_warning = W_BADVAL1 - logger.warning(W_BADVAL1 + "Malformed value: `{}` (no match{})".format(original, where)) - return None + # Failed with the regex, try with the parser + result = parse(ref_prefix[0]+' '+with_commas, with_extra=True) + if result: + result = value_from_grammar(result) + if result and result.get_extra('discarded'): + discarded = " ".join(list(map(lambda x: '`'+x+'`', result.get_extra('discarded')))) + logger.warning(W_BADVAL4 + "Malformed value: `{}` (discarded: {}{})".format(original, discarded, where)) + if not result: + logger.warning(W_BADVAL1 + "Malformed value: `{}` (no match{})".format(original, where)) + return None + # Cache the result + parser_cache[original+ref_prefix] = result + return result value, prefix, units, post = result.groups() if value == '.': - last_warning = W_BADVAL2 logger.warning(W_BADVAL2 + "Malformed value: `{}` (reduced to decimal point{})".format(original, where)) return None if value == '': @@ -158,7 +225,6 @@ def comp_match(component, ref_prefix, ref=None): # We will also have a trailing number if post: if "." in value: - last_warning = W_BADVAL3 logger.warning(W_BADVAL3 + "Malformed value: `{}` (unit split, but contains decimal point{})". format(original, where)) return None @@ -168,35 +234,21 @@ def comp_match(component, ref_prefix, ref=None): else: val = float(value) - # Return all the data, let the caller join it - return (val, get_prefix(prefix), get_unit(units, ref_prefix)) + # Create an object with the result + val, pow = get_prefix(val, prefix) + parsed = ParsedValue(val, pow, get_unit(units, ref_prefix)) + # Cache the result + parser_cache[original+ref_prefix] = parsed + return parsed def compare_values(c1, c2): """ Compare two values """ - # These are the results from comp_match() r1 = c1.value_sort r2 = c2.value_sort - + # If they can't be parsed use the value if not r1 or not r2: - return False - - # Join the data to compare - (v1, (p1, ps1), u1) = r1 - (v2, (p2, ps2), u2) = r2 - - v1 = "{0:.15f}".format(v1 * 1.0 * p1) - v2 = "{0:.15f}".format(v2 * 1.0 * p2) - - if v1 == v2: - # Values match - if u1 == u2: - return True # Units match - # No longer possible because now we use the prefix to determine absent units - # if not u1: - # return True # No units for component 1 - # if not u2: - # return True # No units for component 2 - - return False + return c1.value.strip() == c2.value.strip() + # Compare the normalized representation, i.e. 3300 == 3k3 == 3.3 k + return str(r1) == str(r2) diff --git a/kibot/misc.py b/kibot/misc.py index 40e32b1e..bfde01f0 100644 --- a/kibot/misc.py +++ b/kibot/misc.py @@ -272,6 +272,7 @@ W_BADRES = '(W123) ' W_RESVALISSUE = '(W124) ' W_RES3DNAME = '(W125) ' W_ESCINV = '(W126) ' +W_BADVAL4 = '(W127) ' # Somehow arbitrary, the colors are real, but can be different PCB_MAT_COLORS = {'fr1': "937042", 'fr2': "949d70", 'fr3': "adacb4", 'fr4': "332B16", 'fr5': "6cc290"} PCB_FINISH_COLORS = {'hal': "8b898c", 'hasl': "8b898c", 'imag': "8b898c", 'enig': "cfb96e", 'enepig': "cfb96e", diff --git a/kibot/out_base_3d.py b/kibot/out_base_3d.py index 76066287..c5d7fcd5 100644 --- a/kibot/out_base_3d.py +++ b/kibot/out_base_3d.py @@ -3,6 +3,7 @@ # Copyright (c) 2020-2023 Instituto Nacional de Tecnología Industrial # License: GPL-3.0 # Project: KiBot (formerly KiPlot) +from decimal import Decimal from fnmatch import fnmatch import os import re @@ -341,11 +342,23 @@ class Base3DOptions(VariantOptions): return name r_len = float(m.group(1)) # THT Resistor that we want to add colors - # Check the tolerance + # Check the value + res = comp_match(c.value, c.ref_prefix, c.ref) + if res is None: + return name + val = res.get_decimal() + if val < Decimal('0.01'): + logger.warning(W_BADRES+'Resistor {} out of range, minimum value is 10 mOhms'.format(c.ref)) + return name + val_str = "{0:.0f}".format(val*100) + # Check the tolerance (from the schematic fields) tol = next(filter(lambda x: x, map(c.get_field_value, GS.global_field_tolerance)), None) if not tol: - tol = GS.global_default_resistor_tolerance - logger.warning(W_BADTOL+'Missing tolerance for {}, using {}%'.format(c.ref, tol)) + # Try using the parsed value (i.e. Value="12k 1%") + tol = res.get_extra('tolerance') + if not tol: + tol = GS.global_default_resistor_tolerance + logger.warning(W_BADTOL+'Missing tolerance for {}, using {}%'.format(c.ref, tol)) else: tol = tol.strip() if tol[-1] == '%': @@ -359,15 +372,6 @@ class Base3DOptions(VariantOptions): logger.warning(W_BADTOL+'Unknown tolerance for {}: `{}`'.format(c.ref, tol)) return name tol_color = TOL_COLORS[tol] - # Check the value - res = comp_match(c.value, c.ref_prefix, c.ref) - if res is None: - return name - val = res[0]*res[1][0] - if val < 0.01: - logger.warning(W_BADRES+'Resistor {} out of range, minimum value is 10 mOhms'.format(c.ref)) - return name - val_str = "{0:.0f}".format(val*100) # Find how many bars we'll use if tol < 5: # Use 5 bars for 2 % tol or better diff --git a/kibot/resources/parsers/electro.lark b/kibot/resources/parsers/electro.lark new file mode 100644 index 00000000..b416b113 --- /dev/null +++ b/kibot/resources/parsers/electro.lark @@ -0,0 +1,214 @@ +//***************************************************************************** +// +// Copyright (c) 2023 Salvador E. Tropea +// Copyright (c) 2023 Instituto Nacional de Tecnologia Industrial +// Copyright (c) 2017-2018 Kaspar Emanuel +// +// LICENSE: MIT +// +// Grammar to parse electronic components. +// Can currently parse resistors, capacitors, inductors and LEDs. +// Is strongly based on "electro-grammar" created by Kaspar Emanuel. +// This description was coded to be used with Lark, a Python tool. +// Is an ambiguous description, so it needs the Early algorithm. +// Unlike the original version by Kaspar, implemented using Nearly, this +// grammar handles the stuff to ignore in the grammar description, not retrying +// in the parser. I think Lark can't retry. This makes things a little bit more +// complicated, and harder to debug. +// +//***************************************************************************** +%import common.INT +%import common.NUMBER +%import common.WS +%import common.CNAME + +?main: capacitor | inductor | resistor | led + +// All we ignore +crap: CNAME | NUMBER +// Whitespace and separators +_WS: WS +_SEP: _WS | /[,;]/ + +//************************************ +//************ Capacitors ************ +//************************************ + +// When we know this is a capacitor we don't need the units +capacitor: _c_specs capacitance _c_specs + | _CAP _c_specs (capacitance_no_farad | capacitance)? (_c_specs | c_spec) + +_c_specs: ((c_spec _SEP)+ | (_SEP c_spec)+ | _SEP)* +?c_spec: tolerance | temp_coef | voltage_rating | package_size | crap + +// Give priority to things like "25 V" to avoid separating the "V" as crap +voltage_rating.6: NUMBER _WS? _VOLT INT? + +// See https://en.wikipedia.org/wiki/Ceramic_capacitor#Class_1_ceramic_capacitor +// https://en.wikipedia.org/wiki/Ceramic_capacitor#Class_2_ceramic_capacitor +temp_coef: _class1 | _class2 +_class1: P100 | C0G | N33 | N75 | N150 | N220 | N330 | N470 | N750 | N1000 | N1500 +_class2: /[XYZ]/i "4".."9" /[PRSTUV]/i + +tolerance: (_PLUSMINUS _WS?)? NUMBER _WS? "%" + +capacitance.10: (_capacitance_no_farad | NUMBER) _WS? _FARAD +capacitance_no_farad.10: _capacitance_no_farad +_capacitance_no_farad: INT _c_metric_prefix INT? + | NUMBER _WS? _c_metric_prefix +_c_metric_prefix: milli | micro | nano | pico + +//*********************************** +//************ Inductors ************ +//*********************************** + +// When we know this is an inductor we don't need the units +inductor: _l_specs inductance _l_specs + | _IND _l_specs (inductance_no_henry | inductance)? (_l_specs | l_spec) + +_l_specs: ((l_spec _SEP)+ | (_SEP l_spec)+ | _SEP)* +?l_spec: tolerance | voltage_rating | package_size | crap + +inductance: _inductance_no_henry _WS? _HENRY +inductance_no_henry: _inductance_no_henry +_inductance_no_henry: NUMBER _WS? _l_metric_prefix? INT? +_l_metric_prefix: milli | micro | nano | pico + +//*********************************** +//************ Resistors ************ +//*********************************** + +// When we know this is a resistor we don't need the units +resistor: _r_specs resistance _r_specs + | _RES _r_specs (resistance_no_r | resistance)? (_r_specs | r_spec) + +_r_specs: ((r_spec _SEP)+ | (_SEP r_spec)+ | _SEP)* +?r_spec: tolerance | power_rating | package_size | crap + +power_rating: _power_rating_decimal | _power_rating_fraction +_power_rating_fraction: INT "/" INT _WS? _WATTS +_power_rating_decimal: NUMBER _WS? _power_metric_prefix? _WS? _WATTS +_power_metric_prefix: giga | mega | kilo | milli | micro | nano | pico | femto + +resistance.9: NUMBER _WS? (_r_metric_prefix INT? (_WS? _OHM)? | _OHM) +// Just a number, no R, K, ohm etc. +resistance_no_r.9: NUMBER +_r_metric_prefix: giga | mega | kilo | unit | milli | micro + +//****************************** +//************ LEDs ************ +//****************************** + +led: _led_specs _LED (_SEP _led_specs _led_spec?)? + +_led_specs: ((_led_spec _SEP)+ | (_SEP _led_spec)+ | _SEP)* +_led_spec: package_size | color | crap + +!color: "red"i + | "green"i + | "blue"i + | "yellow"i + | "orange"i + | "white"i + | "amber"i + | "cyan"i + | "purple"i + | "yellow" WS "green" + +//****************************** +//************ Size ************ +//****************************** + +// Sizes looks like numbers and resistors doesn't need units, things like 2512 are hard to differentiate from a +// resistor value. So we use a high priority here +?package_size.11: imperial_size | metric_size +!imperial_size.11: IS01005 | IS0201 | IS0402 | IS0603 | IS0805 | IS1008 | IS1206 | IS1210 | IS1806 | IS2010 | IS2512 +?metric_size.11: metric_size_base _WS _METRIC + | _METRIC _WS metric_size_base + | unambigious_metric_size +// Metric sizes, with names to avoid anonymous +!unambigious_metric_size.11: MS1005 | MS1608 | MS2012 | MS2520 | MS3216 | MS3225 | MS4516 | MS5025 | MS6332 +!metric_size_base.11: unambigious_metric_size | MS0402 | MS0603 + +//****************************** +//****** Metric prefixes ******* +//****************************** + +// !exa: "E" | "exa"i +// !peta: "P" | "peta"i +// !tera: "T" | "tera"i +!giga: "G" | "gig"i "a"i? +!mega: "M" | "meg"i "a"i? +!kilo: "K"i "ilo"i? +!unit: "R"i +// !hecto: "h" | "hecto"i +// !deci: "d" | "deci"i +// !centi: "c" | "centi"i +!milli: "m" | "milli"i +!micro: "U"i + | "\u03BC" + | "\u00B5" + | "𝛍" + | "𝜇" + | "𝝁" + | "𝝻" + | "𝞵" + | /micro/i +!nano: "N"i | "nan"i "o"i? +!pico: "P"i "ico"i? +!femto: "f" | "femto"i +// !atto: "a" | "atto"i + +//****************************** +//****** Named terminals ******* +//****************************** +// Components +_CAP: ("CAPACITOR"i | "CAPA"i | "C"i "AP"i?) _SEP +_RES: ("RESISTOR"i | "RES"i | "R"i) _SEP +_IND: ("IND"i "UCTOR"i? | "L"i) _SEP +_LED: "LED"i +// Units +_FARAD: "F"i "arad"i? +_OHM: "ohm"i "s"i? | "Ω" | "Ω" +_HENRY: "h"i "enry"i? +_VOLT: "Volt"i "s"i? | "V"i +_WATTS: "W"i "atts"i? +// Used for percent +_PLUSMINUS: "+/-" | "±" | "+-" +// Size +IS01005: "01005" +IS0201: "0201" +IS0402: "0402" +IS0603: "0603" +IS0805: "0805" +IS1008: "1008" +IS1206: "1206" +IS1210: "1210" +IS1806: "1806" +IS2010: "2010" +IS2512: "2512" +_METRIC: "METRIC"i +MS1005: "1005" +MS1608: "1608" +MS2012: "2012" +MS2520: "2520" +MS3216: "3216" +MS3225: "3225" +MS4516: "4516" +MS5025: "5025" +MS6332: "6332" +MS0402: "0402" +MS0603: "0603" +// Capacitor temp. coef. classes +P100: "P100"i "/" "M7G"i | "M7G"i "/" "P100"i | "P100"i | "M7G"i +N33: "N33"i "/" "H2G"i | "H2G"i "/" "N33"i | "N33"i | "H2G"i +N75: "N75"i "/" "L2G"i | "L2G"i "/" "N75"i | "N75"i | "L2G"i +N150: "N150"i "/" "P2H"i | "P2H"i "/" "N150"i | "N150"i | "P2H"i +N220: "N220"i "/" "R2H"i | "R2H"i "/" "N220"i | "N220"i | "R2H"i +N330: "N330"i "/" "S2H"i | "S2H"i "/" "N330"i | "N330"i | "S2H"i +N470: "N470"i "/" "T2H"i | "T2H"i "/" "N470"i | "N470"i | "T2H"i +N750: "N750"i "/" "U2J"i | "U2J"i "/" "N750"i | "N750"i | "U2J"i +N1000: "N1000"i "/" "Q3K"i | "Q3K"i "/" "N1000"i | "N1000"i | "Q3K"i +N1500: "N1500"i "/" "P3K"i | "P3K"i "/" "N1500"i | "N1500"i | "P3K"i +C0G: /C[O0]G/i "/" /NP[O0]/i | /NP[O0]/i "/" /C[O0]G/i | /C[O0]G/i | /NP[O0]/i + diff --git a/setup.cfg b/setup.cfg index 15b7139a..63d0c236 100644 --- a/setup.cfg +++ b/setup.cfg @@ -39,6 +39,7 @@ exclude = experiments/kicad/v6/ experiments/JLC/ experiments/resistor_colors/ experiments/EasyEDA/ + experiments/grammar kibot/mcpyrate/ kibot/PcbDraw/ kibot/PyPDF2/ diff --git a/tests/board_samples/kicad_6/resistor_tht.kicad_pcb b/tests/board_samples/kicad_6/resistor_tht.kicad_pcb index d0278823..b4696a33 100644 --- a/tests/board_samples/kicad_6/resistor_tht.kicad_pcb +++ b/tests/board_samples/kicad_6/resistor_tht.kicad_pcb @@ -326,7 +326,7 @@ (effects (font (size 1 1) (thickness 0.15))) (tstamp 8c217b04-361d-48af-b961-1d540df74a9f) ) - (fp_text value "0.01" (at 12.7 3.37) (layer "F.Fab") + (fp_text value "0.01 5%" (at 12.7 3.37) (layer "F.Fab") (effects (font (size 1 1) (thickness 0.15))) (tstamp d228c21f-e790-45ba-aad5-43b7d6020d9a) ) diff --git a/tests/board_samples/kicad_6/resistor_tht.kicad_sch b/tests/board_samples/kicad_6/resistor_tht.kicad_sch index 5e6cd860..6cfd3c3a 100644 --- a/tests/board_samples/kicad_6/resistor_tht.kicad_sch +++ b/tests/board_samples/kicad_6/resistor_tht.kicad_sch @@ -684,7 +684,7 @@ (in_bom yes) (on_board yes) (fields_autoplaced) (uuid bb38c897-d2f8-4276-a663-bb1f35f5db41) (property "Reference" "R26" (id 0) (at 92.71 101.9642 90)) - (property "Value" "0.01" (id 1) (at 92.71 104.5011 90)) + (property "Value" "0.01 5%" (id 1) (at 92.71 104.5011 90)) (property "Footprint" "Resistor_THT:R_Axial_DIN0414_L11.9mm_D4.5mm_P25.40mm_Horizontal" (id 2) (at 92.71 108.458 90) (effects (font (size 1.27 1.27)) hide) ) @@ -1001,7 +1001,7 @@ (reference "R25") (unit 1) (value "33k") (footprint "Resistor_THT:R_Axial_DIN0414_L11.9mm_D4.5mm_P20.32mm_Horizontal") ) (path "/bb38c897-d2f8-4276-a663-bb1f35f5db41" - (reference "R26") (unit 1) (value "0.01") (footprint "Resistor_THT:R_Axial_DIN0414_L11.9mm_D4.5mm_P25.40mm_Horizontal") + (reference "R26") (unit 1) (value "0.01 5%") (footprint "Resistor_THT:R_Axial_DIN0414_L11.9mm_D4.5mm_P25.40mm_Horizontal") ) (path "/9cfcad2d-3334-4d64-b2a9-fb405ef03985" (reference "R27") (unit 1) (value "0.12") (footprint "Resistor_THT:R_Axial_DIN0414_L11.9mm_D4.5mm_P5.08mm_Vertical") diff --git a/tests/board_samples/kicad_7/resistor_tht.kicad_pcb b/tests/board_samples/kicad_7/resistor_tht.kicad_pcb index 4b6ac933..e85d32a9 100644 --- a/tests/board_samples/kicad_7/resistor_tht.kicad_pcb +++ b/tests/board_samples/kicad_7/resistor_tht.kicad_pcb @@ -367,7 +367,7 @@ (effects (font (size 1 1) (thickness 0.15))) (tstamp 8c217b04-361d-48af-b961-1d540df74a9f) ) - (fp_text value "0.01" (at 12.7 3.37) (layer "F.Fab") + (fp_text value "0.01 5%" (at 12.7 3.37) (layer "F.Fab") (effects (font (size 1 1) (thickness 0.15))) (tstamp d228c21f-e790-45ba-aad5-43b7d6020d9a) ) diff --git a/tests/board_samples/kicad_7/resistor_tht.kicad_sch b/tests/board_samples/kicad_7/resistor_tht.kicad_sch index f1a21814..e31d80b2 100644 --- a/tests/board_samples/kicad_7/resistor_tht.kicad_sch +++ b/tests/board_samples/kicad_7/resistor_tht.kicad_sch @@ -1082,7 +1082,7 @@ (property "Reference" "R26" (at 92.71 101.9642 90) (effects (font (size 1.27 1.27))) ) - (property "Value" "0.01" (at 92.71 104.5011 90) + (property "Value" "0.01 5%" (at 92.71 104.5011 90) (effects (font (size 1.27 1.27))) ) (property "Footprint" "Resistor_THT:R_Axial_DIN0414_L11.9mm_D4.5mm_P25.40mm_Horizontal" (at 92.71 108.458 90) diff --git a/tests/test_plot/test_int_bom.py b/tests/test_plot/test_int_bom.py index 11cbe45e..ce6cb121 100644 --- a/tests/test_plot/test_int_bom.py +++ b/tests/test_plot/test_int_bom.py @@ -486,13 +486,13 @@ def int_bom_sort(test_dir, locale, dp): ref_column = header.index(REF_COLUMN_NAME) exp = ['C5', 'C6', 'C7', 'C8', 'C9', 'C10', 'C1', 'C2', 'C3', 'C4', 'C11', 'C12', 'L2', 'L1', 'L3', - 'R5', 'R16', 'R12', 'R4', 'R9', 'R10', 'R3'] + 'R5', 'R16', 'R12', 'R4', 'R13', 'R9', 'R10', 'R3'] if dp == ',': exp += ['R2', 'R1', 'R8'] else: # 8,2 k is interpreted as 82 k exp += ['R1', 'R2', 'R8'] - exp += ['R7', 'R11', 'R14', 'R13', 'R15'] + exp += ['R7', 'R11', 'R14', 'R15'] check_kibom_test_netlist(rows, ref_column, 23, None, exp) # Check the sorting assert get_column(rows, ref_column) == exp diff --git a/tests/test_plot/test_misc_2.py b/tests/test_plot/test_misc_2.py index c935ffea..d4e9860c 100644 --- a/tests/test_plot/test_misc_2.py +++ b/tests/test_plot/test_misc_2.py @@ -16,7 +16,8 @@ from kibot.dep_downloader import search_as_plugin from kibot.registrable import RegOutput, RegFilter from kibot.misc import (WRONG_INSTALL, BOM_ERROR, DRC_ERROR, ERC_ERROR, PDF_PCB_PRINT, KICAD2STEP_ERR) from kibot.bom.columnlist import ColumnList -from kibot.bom.units import get_prefix +from kibot.bom.units import get_prefix, comp_match +from kibot.bom.electro_grammar import parse from kibot.__main__ import detect_kicad from kibot.kicad.config import KiConf from kibot.globals import Globals @@ -291,7 +292,7 @@ def test_step_fail(test_dir, caplog, monkeypatch): def test_unknown_prefix(caplog): with context.cover_it(cov): - get_prefix('y') + get_prefix(1, 'y') assert 'Unknown prefix, please report' in caplog.text @@ -346,21 +347,166 @@ def test_makefile_kibot_sys(test_dir): ctx.clean_up() +def test_units_1(): + with context.cover_it(cov): + assert str(comp_match("1", 'R')) == "1 Ω" + assert str(comp_match("1000", 'R')) == "1 kΩ" + assert str(comp_match("1000000", 'R')) == "1 MΩ" + assert str(comp_match("1000000000", 'R')) == "1 GΩ" + assert str(comp_match("3.3 pF", 'C')) == "3.3 pF" + assert str(comp_match("0.0033 nF", 'C')) == "3.3 pF" + assert str(comp_match("3p3", 'C')) == "3.3 pF" + a = comp_match("3k3 1% 0805", 'R') + assert str(a) == "3.3 kΩ" + assert a.extra['tolerance'] == 1 + assert a.extra['size'] == '0805' + a = comp_match("0.01 1%", 'R') + assert str(a) == "10 mΩ" + assert a.extra['tolerance'] == 1 + + def test_read_resistance(): - assert read_resistance("4k7") == D("4700") - assert read_resistance("4k7") == D("4700") - assert read_resistance("4.7R") == D("4.7") - assert read_resistance("4R7") == D("4.7") - assert read_resistance("0R47") == D("0.47") - assert read_resistance("4700k") == D("4700000") - assert read_resistance("470m") == D("0.47") - assert read_resistance("470M") == D("470000000") - assert read_resistance("4M7") == D("4700000") - assert read_resistance("470") == D("470") - assert read_resistance("470Ω") == D("470") - assert read_resistance("470 Ω") == D("470") - assert read_resistance("470Ohm") == D("470") - assert read_resistance("470 Ohms") == D("470") - assert read_resistance("R47") == D("0.47") - assert read_resistance("1G") == D("1000000000") - assert read_resistance("4k7000") == D("4700") + with context.cover_it(cov): + assert read_resistance("4k7")[0] == D("4700") + assert read_resistance("4k7")[0] == D("4700") + assert read_resistance("4.7R")[0] == D("4.7") + assert read_resistance("4R7")[0] == D("4.7") + assert read_resistance("0R47")[0] == D("0.47") + assert read_resistance("4700k")[0] == D("4700000") + assert read_resistance("470m")[0] == D("0.47") + assert read_resistance("470M")[0] == D("470000000") + assert read_resistance("4M7")[0] == D("4700000") + assert read_resistance("470")[0] == D("470") + assert read_resistance("470Ω")[0] == D("470") + assert read_resistance("470 Ω")[0] == D("470") + assert read_resistance("470Ohm")[0] == D("470") + assert read_resistance("470 Ohms")[0] == D("470") + assert read_resistance("R47")[0] == D("0.47") + assert read_resistance("1G")[0] == D("1000000000") + assert read_resistance("4k7000")[0] == D("4700") + + +def test_electro_grammar_1(): + with context.cover_it(cov): + C2UF_0603_30P = {'type': 'capacitor', 'capacitance': 2e-6, 'size': '0603', 'tolerance': 30} + C2UF_0603 = {'type': 'capacitor', 'capacitance': 2e-6, 'size': '0603'} + C10UF_0402 = {'type': 'capacitor', 'capacitance': 10e-6, 'size': '0402'} + C100NF_0603 = {'type': 'capacitor', 'capacitance': 100e-9, 'size': '0603'} + C100NF_0603_X7R = {'type': 'capacitor', 'capacitance': 100e-9, 'size': '0603', 'characteristic': 'X7R'} + C100NF_0603_Z5U = {'type': 'capacitor', 'capacitance': 100e-9, 'size': '0603', 'characteristic': 'Z5U'} + C100NF_0603_Y5V = {'type': 'capacitor', 'capacitance': 100e-9, 'size': '0603', 'characteristic': 'Y5V'} + C100NF_0603_C0G = {'type': 'capacitor', 'capacitance': 100e-9, 'size': '0603', 'characteristic': 'C0G'} + C100NF_0603_25V = {'type': 'capacitor', 'capacitance': 100e-9, 'size': '0603', 'voltage_rating': 25} + C100NF_0603_6V3 = {'type': 'capacitor', 'capacitance': 100e-9, 'size': '0603', 'voltage_rating': 6.3} + C100UF_0603 = {'type': 'capacitor', 'capacitance': 100e-6, 'size': '0603'} + C100UF_0603_X7R = {'type': 'capacitor', 'capacitance': 100e-6, 'size': '0603', 'characteristic': 'X7R'} + C1N5_0603_X7R = {'type': 'capacitor', 'capacitance': 1.5e-9, 'size': '0603', 'characteristic': 'X7R'} + C1F_0603_25V = {'type': 'capacitor', 'capacitance': 1, 'size': '0603', 'voltage_rating': 25} + C_01005 = {'type': 'capacitor', 'size': '01005'} + C_0201 = {'type': 'capacitor', 'size': '0201'} + C_0402 = {'type': 'capacitor', 'size': '0402'} + C_0603 = {'type': 'capacitor', 'size': '0603'} + C_0805 = {'type': 'capacitor', 'size': '0805'} + C_1206 = {'type': 'capacitor', 'size': '1206'} + C_TESTS = ((('this is total rubbish', ''), {}), + (('2uF 0603',), C2UF_0603), + (('2uF 0603 30%', '2uF 0603 +/-30%', '2uF 0603 ±30%', '2uF 0603 +-30%'), C2UF_0603_30P), + (('10uF 0402', + '10 micro Farad 0402', + '10 \u03BC''F 0402', + '10 \u00B5''F 0402', + '10𝛍F 0402', + '10𝜇F 0402', + '10𝝁 F 0402', + '10 𝝻F 0402', + '10𝞵F 0402'), C10UF_0402), + (('100nF 0603 kajdlkja alkdjlkajd', + 'adjalkjd 100nF akjdlkjda 0603 kajdlkja alkdjlkajd', + 'capacitor 100nF 0603, warehouse 5', + 'adjalkjd 0603 akjdlkjda 100nF kajdlkja alkdjlkajd', + 'C 100n 0603', + 'Capacitor 100n 0603', + 'cap 100n 0603'), C100NF_0603), + (('1n5F 0603 X7R',), C1N5_0603_X7R), + (('100NF 0603 X7R', '100nF 0603 X7R', '100nF 0603 x7r'), C100NF_0603_X7R), + (('100UF 0603 X7R',), C100UF_0603_X7R), + (('100nF 0603 Z5U',), C100NF_0603_Z5U), + (('100nF 0603 Y5V',), C100NF_0603_Y5V), + (('100nF 0603 C0G', + '100nF 0603 NP0', + '100nF 0603 np0', + '100nF 0603 c0g', + '100nF 0603 cog', + '100nF 0603 npO', + '100nF 0603 COG', + '100nF 0603 C0G/NP0'), C100NF_0603_C0G), + (('1F 0603 25V', '1f 0603 25V', '1 Farad 0603 25V'), C1F_0603_25V), + (('100nF 0603 25V', '100nF 0603 25 v'), C100NF_0603_25V), + (('100nF 0603 6v3', '100nF 0603 6V3', '100nF 0603 6.3V', '100nF 0603 6.3v'), C100NF_0603_6V3), + (('0603 0.0001F', '0603 0.0001 F', '0603 0.1mF'), C100UF_0603), + (('capacitor 01005',), C_01005), + (('capacitor 0201',), C_0201), + (('capacitor 0402',), C_0402), + (('capacitor 0603',), C_0603), + (('capacitor 0805',), C_0805), + (('capacitor 1206',), C_1206)) + R1K_0603 = {'type': 'resistor', 'size': '0603', 'resistance': 1000} + R1K_0805_5P = {'type': 'resistor', 'size': '0805', 'resistance': 1000, 'tolerance': 5} + R1K_0805_5P_100MW = {'type': 'resistor', 'size': '0805', 'resistance': 1000, 'tolerance': 5, 'power_rating': 0.1} + R1K_0201_500MW = {'type': 'resistor', 'size': '0201', 'resistance': 1000, 'power_rating': 0.5} + R0_0201_125MW = {'type': 'resistor', 'size': '0201', 'resistance': 0, 'power_rating': 0.125} + R1M_0603 = {'type': 'resistor', 'size': '0603', 'resistance': 1e6} + R1M = {'type': 'resistor', 'resistance': 1e6} + R1M1_0603 = {'type': 'resistor', 'size': '0603', 'resistance': 1.1e6} + R100 = {'type': 'resistor', 'resistance': 100} + R10K_0805 = {'type': 'resistor', 'size': '0805', 'resistance': 10000} + R1 = {'type': 'resistor', 'resistance': 1} + R1_0402 = {'type': 'resistor', 'resistance': 1, 'size': '0402'} + R1_0805 = {'type': 'resistor', 'resistance': 1, 'size': '0805'} + R1K5_0402 = {'type': 'resistor', 'resistance': 1500, 'size': '0402'} + R2_7_0402 = {'type': 'resistor', 'resistance': 2.7, 'size': '0402'} + R1MILI = {'type': 'resistor', 'resistance': 0.001} + R100U = {'type': 'resistor', 'resistance': 0.0001} + R_01005 = {'type': 'resistor', 'size': '01005'} + R_0201 = {'type': 'resistor', 'size': '0201'} + R_0402 = {'type': 'resistor', 'size': '0402'} + R_0603 = {'type': 'resistor', 'size': '0603'} + R_0805 = {'type': 'resistor', 'size': '0805'} + R_1206 = {'type': 'resistor', 'size': '1206'} + R_TESTS = ((('R 0.01 1%',), {'type': 'resistor', 'resistance': 0.01, 'tolerance': 1}), + (('1k 0603', '1k ohm 0603', '1K ohms 0603'), R1K_0603), + (('resistor 100', '100R', '100 R'), R100), + (('r 10000 0805',), R10K_0805), + (('res or whatever 1',), R1), + (('1 ohm 0402',), R1_0402), + (('1Ω 0805', '1Ω 0805'), R1_0805), + (('1MEG 0603', '1M 0603'), R1M_0603), + (('1M1 ohms 0603',), R1M1_0603), + (('1k5 0402', '1.5k 0402'), R1K5_0402), + (('2r7 0402', '2R7 0402'), R2_7_0402), + (('1 mOhm',), R1MILI), + (('1 MOhm',), R1M), + (('100 uΩ',), R100U), + (('1k 0805 5%',), R1K_0805_5P), + (('1k 0805 5% 100mW',), R1K_0805_5P_100MW), + (('0 ohm 0201 0.125W', '0 ohm 0201 1/8W'), R0_0201_125MW), + (('resistor 1k 0201 1/2 watts',), R1K_0201_500MW), + (('resistor 01005',), R_01005), + (('resistor 0201',), R_0201), + (('resistor 0402',), R_0402), + (('resistor 0603',), R_0603), + (('resistor 0805',), R_0805), + (('resistor 1206',), R_1206)) + LED_TEST = ((('led red 0603',), {'type': 'led', 'size': '0603', 'color': 'red'}), + (('SMD LED GREEN 0805', 'GREEN 0805 LED'), {'type': 'led', 'size': '0805', 'color': 'green'})) + L_TEST = ((('L 100 0805', 'IND 100 0805', 'Inductor 100 0805'), {'type': 'inductor', 'inductance': 100, + 'size': '0805'}), + (('3n3 H', '3n3H', '3.3 nH', '3300pH', '3.3 nano Henry', + 'This is a 3.3 nH inductor'), {'type': 'inductor', 'inductance': 3.3e-9})) + TESTS = C_TESTS+R_TESTS+L_TEST+LED_TEST + for test in TESTS: + ref = test[1] + for c in test[0]: + res = parse(c) + assert res == ref, "For `{}` got:\n{}\nExpected:\n{}".format(c, res, ref) + logging.debug(c+" Ok")