Added support for KiCost's subparts

This commit is contained in:
Salvador E. Tropea 2021-03-19 19:41:46 -03:00
parent 91dc9c5488
commit 40bd7c24f2
11 changed files with 531 additions and 3 deletions

View File

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- `erc_warnings` pre-flight option to consider ERC warnings as errors.
- Pattern expansion in the `dir` option for outputs (#58)
- New filter type `suparts`. Adds support for KiCost's subparts feature.
### Changed
- Errors and warnings from KiAuto now are printed as errors and warnings.

259
kibot/fil_subparts.py Normal file
View File

@ -0,0 +1,259 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021 Salvador E. Tropea
# Copyright (c) 2021 Instituto Nacional de Tecnología Industrial
# License: GPL-3.0
# Project: KiBot (formerly KiPlot)
"""
Implements the KiCost subparts mechanism.
The 'manf#' field can contain more than one value separated by ;
The result is REF#subpart
"""
import re
from copy import deepcopy
from .gs import GS
from .optionable import Optionable
from .misc import W_NUMSUBPARTS, W_PARTMULT
from .macros import macros, document, filter_class # noqa: F401
from . import log
logger = log.get_logger(__name__)
DISTRIBUTORS = ['digikey#', 'farnell#', 'mouser#', 'newark#', 'rs#', 'arrow#', 'tme#', 'lcsc#']
class DistributorsList(Optionable):
_default = DISTRIBUTORS
@filter_class
class Subparts(BaseFilter): # noqa: F821
""" Subparts
This filter implements the KiCost subparts mechanism """
def __init__(self):
super().__init__()
self._is_transform = True
with document:
self.check_multiplier = Optionable
""" [list(string)] List of fields to include for multiplier computation.
If empty all fields in `split_fields` and `manf_pn_field` are used """
self.manf_field = 'manf'
""" Field for the manufacturer name """
self.manf_pn_field = 'manf#'
""" Field for the manufacturer part number """
self.modify_value = True
""" Add '- p N/M' to the value """
self.modify_first_value = True
""" Modify even the value for the first component in the list (KiCost behavior) """
self.multiplier = True
""" Enables the subpart multiplier mechanism """
self.mult_separators = ':'
""" Separators used for the multiplier. Each character in this string is a valid separator """
self.ref_sep = '#'
""" Separator used in the reference (i.e. R10#1) """
self.separators = ';,'
""" Separators used between subparts. Each character in this string is a valid separator """
self.split_fields = DistributorsList
""" [list(string)] List of fields to split, usually the distributors part numbers """
self.split_fields_expand = False
""" When `true` the fields in `split_fields` are added to the internal names """
self.use_ref_sep_for_first = True
""" Force the reference separator use even for the first component in the list (KiCost behavior) """
self.value_alt_field = 'value_subparts'
""" Field containing replacements for the `Value` field. So we get real values for splitted parts """
def config(self, parent):
super().config(parent)
if not self.separators:
self.separators = ';,'
if not self.mult_separators:
self.mult_separators = ':'
if not self.ref_sep:
self.ref_sep = '#'
if isinstance(self.split_fields, type):
self.split_fields = DISTRIBUTORS
else:
if self.split_fields_expand:
self.split_fields.extend(DISTRIBUTORS)
# (?<!\\) is used to skip \;
self._part_sep = re.compile(r'(?<!\\)\s*['+self.separators+r']\s*')
self._qty_sep = re.compile(r'(?<!\\)\s*['+self.mult_separators+r']\s*')
# TODO: The spaces here seems a bug
self._esc = re.compile(r'\\\s*(['+self.separators+self.mult_separators+r'])\s*')
self._num_format = re.compile(r"^\s*[\-\+]?\s*[0-9]*\s*[\.\/]*\s*?[0-9]*\s*$")
self._remove_sep = re.compile(r'[\.\/]')
# The list of all fields that controls the process
self._fields = self.split_fields
if self.manf_pn_field:
self._fields.append(self.manf_pn_field)
# List of fields that needs qty computation
if isinstance(self.check_multiplier, type) or self.check_multiplier is None:
self.check_multiplier = self._fields
self.check_multiplier = set(self.check_multiplier)
def subpart_list(self, value):
""" Split a field containing self.separators into a list """
return self._part_sep.split(value.strip())
def manf_code_qtypart(self, value):
# Remove any escape backslashes preceding PART_SEPRTR.
value = self._esc.sub(r'\1', value)
strings = self._qty_sep.split(value)
if len(strings) == 2:
# Search for numbers, matching with simple, frac and decimal ones.
string0_test = re.match(self._num_format, strings[0])
string1_test = re.match(self._num_format, strings[1])
if string0_test and not(string1_test):
qty = strings[0].strip()
part = strings[1].strip()
elif not(string0_test) and string1_test:
qty = strings[1].strip()
part = strings[0].strip()
elif string0_test and string1_test:
# May be just a numeric manufacturer/distributor part number,
# in this case, the quantity is the shortest string not
# considering "." and "/" marks.
if len(self._remove_sep.sub('', strings[0])) < len(self._remove_sep.sub('', strings[1])):
qty = strings[0].strip()
part = strings[1].strip()
else:
qty = strings[1].strip()
part = strings[0].strip()
else:
qty = '1'
part = strings[0].strip() + strings[1].strip()
if qty == '':
qty = '1'
else:
qty = '1'
part = ''.join(strings)
if GS.debug_level > 2 and qty != '1':
logger.debug('Subparts filter: `{}` -> part `{}` qty `{}`'.format(value, part, qty))
return qty, part
@staticmethod
def qty_to_float(qty):
try:
if '/' in qty:
vals = qty.split('/')
return float(vals[0])/float(vals[1])
return float(qty)
except ValueError:
logger.error('Internal error qty_to_float("{}"), please report'.format(qty))
def do_split(self, comp, max_num_subparts, splitted_fields):
""" Split `comp` according to the detected subparts """
# Split it
multi_part = max_num_subparts > 1
if multi_part and GS.debug_level > 1:
logger.debug("Splitting {} in {} subparts".format(comp.ref, max_num_subparts))
splitted = []
# Compute the total for the modified value
total_parts = max_num_subparts if self.modify_first_value else max_num_subparts-1
# Check if we have replacements for the `Value` field
alt_values = []
alt_v = comp.get_field_value(self.value_alt_field)
if alt_v:
alt_values = self.subpart_list(alt_v)
alt_values_len = len(alt_values)
for i in range(max_num_subparts):
new_comp = deepcopy(comp)
if multi_part:
# Adjust the reference name
if self.use_ref_sep_for_first:
new_comp.ref = new_comp.ref+self.ref_sep+str(i+1)
elif i > 0:
# I like it better. The first is usually the real component, the rest are accesories.
new_comp.ref = new_comp.ref+self.ref_sep+str(i)
# Adjust the suffix to be "sort friendly"
# Currently useless, but could help in the future
new_comp.ref_suffix = str(int(new_comp.ref_suffix)*100+i)
# Adjust the value field
if i < alt_values_len:
# We have a replacement
new_comp.set_field('value', alt_values[i])
elif self.modify_value:
idx = i
if self.modify_first_value:
idx += 1
if self.modify_first_value or i:
new_comp.set_field('value', new_comp.value+' - p{}/{}'.format(idx, total_parts))
# Adjust the related fields
prev_qty = None
prev_field = None
max_qty = 0
if not self.check_multiplier.intersection(splitted_fields):
# No field to check for qty here, default to 1
max_qty = 1
for field, values in splitted_fields.items():
check_multiplier = field in self.check_multiplier
value = ''
qty = '1'
if len(values) > i:
value = values[i]
if check_multiplier:
# Analyze the multiplier
qty, value = self.manf_code_qtypart(value)
if prev_qty is not None and qty != prev_qty and field != self.manf_field:
logger.warning(W_PARTMULT+'Different part multiplier on {r}, '
'field {c} has {cn} and {lc} has {lcn}.'
.format(r=new_comp.ref, c=prev_field, cn=prev_qty, lc=field, lcn=qty))
prev_qty = qty
prev_field = field
new_comp.set_field(field, value)
if check_multiplier:
new_comp.set_field(field+'_qty', qty)
max_qty = max(max_qty, self.qty_to_float(qty))
new_comp.qty = max_qty
splitted.append(new_comp)
if not multi_part and int(max_qty) == 1:
# No real split and no multiplier
return
if GS.debug_level > 2:
logger.debug('Old component: '+comp.ref+' '+str([str(f) for f in comp.fields]))
logger.debug('Fields to split: '+str(splitted_fields))
logger.debug('New components:')
for c in splitted:
logger.debug(' '+c.ref+' '+str([str(f) for f in c.fields]))
return splitted
def filter(self, comp):
""" Look for fields containing `part1; mult:part2; etc.` """
# Analyze how to split this component
max_num_subparts = 0
splitted_fields = {}
field_max = None
for field in self._fields:
value = comp.get_field_value(field)
if not value:
# Skip it if not used
continue
subparts = self.subpart_list(value)
splitted_fields[field] = subparts
num_subparts = len(subparts)
if num_subparts > max_num_subparts:
field_max = field
max_num_subparts = num_subparts
# Print a warning if this field has a different ammount
if num_subparts != max_num_subparts:
logger.warning(W_NUMSUBPARTS+'Different subparts ammount on {r}, field {c} has {cn} and {lc} has {lcn}.'
.format(r=comp.ref, c=field_max, cn=max_num_subparts, lc=field, lcn=num_subparts))
if len(splitted_fields) == 0:
# Nothing to split
return
# Split the manufacturer name
# It can contain just one name, so we exclude it from the above warning
if self.manf_field:
value = comp.get_field_value(self.manf_field)
if value:
manfs = self.subpart_list(value)
if len(manfs) == 1:
# If just one `manf` apply it to all
manfs = [manfs[0]]*max_num_subparts
else:
# Expand the "repeat" indicator
for i in range(len(manfs)-1):
if manfs[i+1] == '~':
manfs[i+1] = manfs[i]
splitted_fields[self.manf_field] = manfs
# Now do the work
return self.do_split(comp, max_num_subparts, splitted_fields)

View File

@ -746,6 +746,9 @@ class SchematicField(object):
f.write(' "{}"'.format(self.name))
f.write('\n')
def __str__(self):
return self.name+'='+self.value
class SchematicAltRef():
def __init__(self):
@ -842,8 +845,9 @@ class SchematicComponent(object):
def set_field(self, field, value):
""" Change the value for an existing field """
if field in self.dfields:
target = self.dfields[field]
field_lc = field.lower()
if field_lc in self.dfields:
target = self.dfields[field_lc]
target.value = value
# Adjust special fields
if target.number < 4:

View File

@ -176,6 +176,8 @@ W_MUSTBEINT = '(W055) '
W_NOOUTPUTS = '(W056) '
W_NOTASCII = '(W057) '
W_KIAUTO = '(W058) '
W_NUMSUBPARTS = '(W059) '
W_PARTMULT = '(W060) '
class Rect(object):

View File

@ -474,7 +474,7 @@ class BoMOptions(BaseOptions):
apply_fitted_filter(comps, self.dnf_filter)
apply_fixed_filter(comps, self.dnc_filter)
# Apply the variant
self.variant.filter(comps)
comps = self.variant.filter(comps)
# We add the main project to the aggregate list so do_bom sees a complete list
base_sch = Aggregate()
base_sch.file = GS.sch_file

View File

@ -0,0 +1,164 @@
EESchema Schematic File Version 4
EELAYER 30 0
EELAYER END
$Descr A4 11693 8268
encoding utf-8
Sheet 1 1
Title "KiCost subparts test"
Date "2021-03-16"
Rev "r1"
Comp "KiBot"
Comment1 ""
Comment2 ""
Comment3 ""
Comment4 ""
$EndDescr
$Comp
L Mechanical:Heatsink HS1
U 1 1 6063AB02
P 3750 1325
F 0 "HS1" H 3891 1491 50 0000 L CNN
F 1 "322400B00000G" H 3891 1400 50 0000 L CNN
F 2 "" H 3762 1325 50 0001 C CNN
F 3 "~" H 3762 1325 50 0001 C CNN
F 4 "Aavid" H 3750 1325 50 0001 C CNN "manf"
F 5 "322400B00000G" H 3891 1309 50 0000 L CNN "manf#"
F 6 "HS100-ND" H 3750 1325 50 0001 C CNN "digikey#"
F 7 "HEAT SINK TO-18 1W BLK" H 3750 1325 50 0001 C CNN "Description"
1 3750 1325
1 0 0 -1
$EndComp
$Comp
L Device:Q_NPN_BCE Q2
U 1 1 6063D25D
P 3750 1550
F 0 "Q2" V 3675 1350 50 0000 C CNN
F 1 "2N2222A" V 3575 1250 50 0000 C CNN
F 2 "Package_TO_SOT_THT:TO-18-3" H 3950 1650 50 0001 C CNN
F 3 "~" H 3750 1550 50 0001 C CNN
F 4 "Central Semiconductor Corp" V 3750 1550 50 0001 C CNN "manf"
F 5 "2N2222A PBFREE" V 3500 1100 50 0000 C CNN "manf#"
F 6 "TRANS NPN 40V 0.8A TO-18" V 3750 1550 50 0001 C CNN "Description"
F 7 "1514-2N2222APBFREE-ND" V 3750 1550 50 0001 C CNN "digikey#"
1 3750 1550
0 -1 -1 0
$EndComp
Text Notes 3600 1100 0 50 ~ 0
A Transistor and its heatsink\nOnly one goes to the PCB
Text Notes 575 825 0 200 ~ 40
KiCost subparts
$Comp
L Device:Q_NPN_BCE Q1
U 1 1 60640F08
P 1075 1300
F 0 "Q1" V 1000 1100 50 0000 C CNN
F 1 "2N2222A" V 900 1000 50 0000 C CNN
F 2 "Package_TO_SOT_THT:TO-18-3" H 1275 1400 50 0001 C CNN
F 3 "~" H 1075 1300 50 0001 C CNN
F 4 "Central Semiconductor Corp; Aavid" V 1075 1300 50 0001 C CNN "manf"
F 5 "2N2222A PBFREE; 322400B00000G" V 825 500 50 0000 C CNN "manf#"
F 6 "TRANS NPN 40V 0.8A TO-18 + Heatsink" V 1075 1300 50 0001 C CNN "Description"
F 7 "1514-2N2222APBFREE-ND; HS100-ND" V 1075 1300 50 0001 C CNN "digikey#"
F 8 "2N2222A;322400B00000G" V 1150 2600 50 0001 C CNN "Value_Subparts"
1 1075 1300
0 -1 -1 0
$EndComp
Text Notes 2650 1550 0 50 ~ 0
Equivalent to ->
$Comp
L Connector_Generic:Conn_01x06 J1
U 1 1 60649B1F
P 1150 2600
F 0 "J1" V 1114 2212 50 0000 R CNN
F 1 "Conn_01x06" V 1023 2212 50 0000 R CNN
F 2 "Connector_PinHeader_2.54mm:PinHeader_1x06_P2.54mm_Vertical" H 1150 2600 50 0001 C CNN
F 3 "~" H 1150 2600 50 0001 C CNN
F 4 "Molex; Molex; Molex; LEM; LEM" V 1150 2600 50 0001 C CNN "manf"
F 5 "0022232061;0022012067; 6: 08-50-0114; LA 55-P; lv 25-P" V 1150 2600 50 0001 C CNN "manf#"
F 6 "WM4204-ND; WM2015-ND; WM1114-ND; 398-1010-ND; 398-1019-ND" V 1150 2600 50 0001 C CNN "digikey#"
F 7 "CONN HEADER VERT 6POS 2.54MM; CONN HOUSING 6POS .100 W/RAMP; CONN 22-30AWG CRIMP TIN; SENSOR CURRENT HALL 50A AC/DC; TRANSDUCR VOLTAG CLOSE LOOP 10MA" V 1150 2600 50 0001 C CNN "Description"
F 8 "Male_01x06;Female_01x06;Crimp_Pin;LA 55-P;LV 25-P" V 1150 2600 50 0001 C CNN "Value_Subparts"
1 1150 2600
0 -1 -1 0
$EndComp
Text Notes 900 2400 0 50 ~ 0
A male header\nBut we want to include the female, the pins and the two\nsensors wired to the female.
$Comp
L Device:R R1
U 1 1 6064C12F
P 3750 2650
F 0 "R1" H 3820 2696 50 0000 L CNN
F 1 "0" H 3820 2605 50 0000 L CNN
F 2 "" V 3680 2650 50 0001 C CNN
F 3 "~" H 3750 2650 50 0001 C CNN
F 4 "Bourns" H 3750 2650 50 0001 C CNN "manf"
F 5 "CR0603-J/-000ELF:5" H 3750 2650 50 0001 C CNN "manf#"
F 6 "CR0603-J/-000ELFCT-ND: 5" H 3750 2650 50 0001 C CNN "digikey#"
F 7 "RES SMD 0 OHM JUMPER 1/8W 0603" H 3750 2650 50 0001 C CNN "Description"
1 3750 2650
1 0 0 -1
$EndComp
Text Notes 3675 2475 0 50 ~ 0
A simple resistor, \nbut multiplied by 5\nWe add: R4 no mult \n and R5 no manf#\nTotal: 7
$Comp
L Device:R R2
U 1 1 6064CA86
P 3750 3400
F 0 "R2" H 3820 3446 50 0000 L CNN
F 1 "100" H 3820 3355 50 0000 L CNN
F 2 "" V 3680 3400 50 0001 C CNN
F 3 "~" H 3750 3400 50 0001 C CNN
F 4 "Bourns" H 3750 3400 50 0001 C CNN "manf"
F 5 "CR0603-JW-101ELF : 4.5" H 3750 3400 50 0001 C CNN "manf#"
F 6 "CR0603-JW-101ELFCT-ND :4.5" H 3750 3400 50 0001 C CNN "digikey#"
F 7 "RES SMD 100 OHM 5% 1/10W 0603" H 3750 3400 50 0001 C CNN "Description"
1 3750 3400
1 0 0 -1
$EndComp
Text Notes 3675 3225 0 50 ~ 0
A simple resistor, \nbut multiplied by 4.5
$Comp
L Device:R R3
U 1 1 6064D8D1
P 3750 4150
F 0 "R3" H 3820 4196 50 0000 L CNN
F 1 "100k" H 3820 4105 50 0000 L CNN
F 2 "" V 3680 4150 50 0001 C CNN
F 3 "~" H 3750 4150 50 0001 C CNN
F 4 "Bourns" H 3750 4150 50 0001 C CNN "manf"
F 5 "4/5 : CR0603-JW-104ELF " H 3750 4150 50 0001 C CNN "manf#"
F 6 " 4/5 : CR0603-JW-104ELFCT-ND" H 3750 4150 50 0001 C CNN "digikey#"
F 7 "RES SMD 100K OHM 5% 1/10W 0603" H 3750 4150 50 0001 C CNN "Description"
1 3750 4150
1 0 0 -1
$EndComp
Text Notes 3675 3975 0 50 ~ 0
A simple resistor, \nbut multiplied by 4/5
$Comp
L Device:R R4
U 1 1 6064EF62
P 4250 2650
F 0 "R4" H 4320 2696 50 0000 L CNN
F 1 "0" H 4320 2605 50 0000 L CNN
F 2 "" V 4180 2650 50 0001 C CNN
F 3 "~" H 4250 2650 50 0001 C CNN
F 4 "Bourns" H 4250 2650 50 0001 C CNN "manf"
F 5 "CR0603-J/-000ELF" H 4250 2650 50 0001 C CNN "manf#"
F 6 "CR0603-J/-000ELFCT-ND" H 4250 2650 50 0001 C CNN "digikey#"
F 7 "RES SMD 0 OHM JUMPER 1/8W 0603" H 4250 2650 50 0001 C CNN "Description"
1 4250 2650
1 0 0 -1
$EndComp
$Comp
L Device:R R5
U 1 1 6064F812
P 4500 2650
F 0 "R5" H 4570 2696 50 0000 L CNN
F 1 "0" H 4570 2605 50 0000 L CNN
F 2 "" V 4430 2650 50 0001 C CNN
F 3 "~" H 4500 2650 50 0001 C CNN
F 4 "RES SMD 0 OHM JUMPER 1/8W 0603" H 4500 2650 50 0001 C CNN "Description"
1 4500 2650
1 0 0 -1
$EndComp
$EndSCHEMATC

View File

@ -0,0 +1,22 @@
Row,References,Value,Description,manf,manf#,digikey#,Quantity Per PCB,Build Quantity
1,HS1 Q1#2,322400B00000G,HEAT SINK TO-18 1W BLK,Aavid,322400B00000G,HS100-ND,2,200
2,J1#3,Crimp_Pin,CONN 22-30AWG CRIMP TIN,Molex,08-50-0114,WM1114-ND,6,600
3,J1#2,Female_01x06,CONN HOUSING 6POS .100 W/RAMP,Molex,0022012067,WM2015-ND,1,100
4,J1#4,LA 55-P,SENSOR CURRENT HALL 50A AC/DC,LEM,LA 55-P,398-1010-ND,1,100
5,J1#5,LV 25-P,TRANSDUCR VOLTAG CLOSE LOOP 10MA,LEM,lv 25-P,398-1019-ND,1,100
6,J1#1,Male_01x06,CONN HEADER VERT 6POS 2.54MM,Molex,0022232061,WM4204-ND,1,100
7,Q2 Q1#1,2N2222A,TRANS NPN 40V 0.8A TO-18 TRANS NPN 40V 0.8A TO-18 + Heatsink,Central Semiconductor Corp,2N2222A PBFREE,1514-2N2222APBFREE-ND,2,200
8,R1 R4 R5,0,RES SMD 0 OHM JUMPER 1/8W 0603,Bourns,CR0603-J/-000ELF,CR0603-J/-000ELFCT-ND,7,700
9,R2,100,RES SMD 100 OHM 5% 1/10W 0603,Bourns,CR0603-JW-101ELF,CR0603-JW-101ELFCT-ND,5,450
10,R3,100k,RES SMD 100K OHM 5% 1/10W 0603,Bourns,CR0603-JW-104ELF,CR0603-JW-104ELFCT-ND,1,80
Statistics:
Component Groups:,10
Component Count:,27
Fitted Components:,27
Number of PCBs:,100
Total Components:,2630
Can't render this file because it has a wrong number of fields in line 17.

View File

@ -0,0 +1 @@
../5_1_6/subparts-bom.csv
1 ../5_1_6/subparts-bom.csv

View File

@ -0,0 +1 @@
../5_1_6/subparts-bom.csv
1 ../5_1_6/subparts-bom.csv

View File

@ -1506,3 +1506,13 @@ def test_int_bom_merge_xml_1(test_dir):
src_column = header.index(SOURCE_BOM_COLUMN_NAME.replace(' ', '_'))
check_source(rows, 'A:R1', ref_column, src_column, MERGED_R1_SRC)
ctx.clean_up()
def test_int_bom_subparts_1(test_dir):
prj = 'subparts'
ctx = context.TestContextSCH(test_dir, 'test_int_bom_subparts_1', prj, 'int_bom_subparts_1', '')
ctx.run(extra_debug=True)
output = prj+'-bom.csv'
ctx.expect_out_file(output)
ctx.compare_txt(output)
ctx.clean_up()

View File

@ -0,0 +1,64 @@
# Example KiBot config file
kibot:
version: 1
filters:
- name: 'Subparts splitter'
type: subparts
# We want to also split the `Description` field
split_fields: ['Description']
split_fields_expand: true
# We only use the multiplier in `manf#`
check_multiplier: ['manf#', 'digikey#']
variants:
- name: place_holder
comment: 'Just a place holder for the subparts splitter'
type: kibom
pre_transform: 'Subparts splitter'
outputs:
- name: 'bom_internal_subparts'
comment: "Bill of Materials in CSV format, subparts splitted"
type: bom
dir: .
options: &bom_options
variant: place_holder
number: 100
group_fields: ['manf#']
group_fields_fallbacks: ['value']
# int_qtys: false
columns:
- Row
- References
- Value
- Description
- manf
- manf#
- digikey#
- 'Quantity Per PCB'
- 'Build Quantity'
csv:
hide_pcb_info: true
- name: 'bom_html'
comment: "Bill of Materials in HTML format"
type: bom
dir: .
options:
<<: *bom_options
html:
digikey_link: 'digikey#'
highlight_empty: false
- name: 'bom_xlsx'
comment: "Bill of Materials in XLSX format"
type: bom
dir: .
options:
<<: *bom_options
xlsx:
digikey_link: 'digikey#'
highlight_empty: false