Now you can consolidate more than one project in one BoM.
The basic idea comes from pimpmykicadbom by Anton Savov (@antto)
This commit is contained in:
parent
66e342e36d
commit
15474ae4d7
|
|
@ -8,8 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
## [Unreleased]
|
||||
### Added
|
||||
- The multipart id to references of multipart components others than part 1.
|
||||
- Internal BoM: `no_conflict` option to exclude fields from conflict detection.
|
||||
- Internal BoM: HTML tables can be sorted selecting a column (Java Script).
|
||||
- Internal BoM:
|
||||
- `no_conflict` option to exclude fields from conflict detection.
|
||||
- HTML tables can be sorted selecting a column (Java Script).
|
||||
- You can consolidate more than one project in one BoM.
|
||||
- Support for KICAD_CONFIG_HOME defined from inside KiCad.
|
||||
- Now layers can be selected using the default KiCad names.
|
||||
- More control over the name of the drill and gerber files.
|
||||
|
|
|
|||
12
README.md
12
README.md
|
|
@ -570,6 +570,12 @@ Next time you need this list just use an alias, like this:
|
|||
- `name`: [string=''] Used to identify this particular output definition.
|
||||
- `options`: [dict] Options for the `bom` output.
|
||||
* Valid keys:
|
||||
- `aggregate`: [list(dict)] Add components from other projects.
|
||||
* Valid keys:
|
||||
- `file`: [string=''] Name of the schematic to aggregate.
|
||||
- `name`: [string=''] Name to identify this source. If empty we use the name of the schematic.
|
||||
- `number`: [number=1] Number of boards to build (components multiplier). Use negative to substract.
|
||||
- `ref_id`: [string=''] A prefix to add to all the references from this project.
|
||||
- `columns`: [list(dict)|list(string)] List of columns to display.
|
||||
Can be just the name of the field.
|
||||
* Valid keys:
|
||||
|
|
@ -628,7 +634,9 @@ Next time you need this list just use an alias, like this:
|
|||
- `normalize_values`: [boolean=false] Try to normalize the R, L and C values, producing uniform units and prefixes.
|
||||
- `number`: [number=1] Number of boards to build (components multiplier).
|
||||
- `output`: [string='%f-%i%v.%x'] filename for the output (%i=bom). Affected by global options.
|
||||
- `ref_id`: [string=''] A prefix to add to all the references from this project. Used for multiple projects.
|
||||
- `ref_separator`: [string=' '] Separator used for the list of references.
|
||||
- `source_by_id`: [boolean=false] Generate the `Source BoM` column using the reference ID instead of the project name.
|
||||
- `use_alt`: [boolean=false] Print grouped references in the alternate compressed style eg: R1-R7,R18.
|
||||
- `variant`: [string=''] Board variant, used to determine which components
|
||||
are output to the BoM..
|
||||
|
|
@ -1846,9 +1854,11 @@ The internal list of rotations is:
|
|||
- **KiBoM**: Oliver Henry Walters (@SchrodingersGat)
|
||||
- **Interactive HTML BoM**: @qu1ck
|
||||
- **PcbDraw**: Jan Mrázek (@yaqwsx)
|
||||
- **KiCad Gerber Zipper**: @g200kg
|
||||
- **Contributors**:
|
||||
- **Error filters ideas**: Leandro Heck (@leoheck)
|
||||
- **GitHub Actions Integration/SVG output**: @nerdyscout
|
||||
- **Sources of inspiration and good ideas**:
|
||||
- **KiCad Gerber Zipper**: @g200kg
|
||||
- **pimpmykicadbom **: Anton Savov (@antto)
|
||||
- **Others**:
|
||||
- **Robot in the logo**: Christian Plaza (from pixabay)
|
||||
|
|
|
|||
|
|
@ -942,9 +942,11 @@ The internal list of rotations is:
|
|||
- **KiBoM**: Oliver Henry Walters (@SchrodingersGat)
|
||||
- **Interactive HTML BoM**: @qu1ck
|
||||
- **PcbDraw**: Jan Mrázek (@yaqwsx)
|
||||
- **KiCad Gerber Zipper**: @g200kg
|
||||
- **Contributors**:
|
||||
- **Error filters ideas**: Leandro Heck (@leoheck)
|
||||
- **GitHub Actions Integration/SVG output**: @nerdyscout
|
||||
- **Sources of inspiration and good ideas**:
|
||||
- **KiCad Gerber Zipper**: @g200kg
|
||||
- **pimpmykicadbom **: Anton Savov (@antto)
|
||||
- **Others**:
|
||||
- **Robot in the logo**: Christian Plaza (from pixabay)
|
||||
|
|
|
|||
|
|
@ -39,6 +39,16 @@ outputs:
|
|||
type: 'bom'
|
||||
dir: 'Example/bom_dir'
|
||||
options:
|
||||
# [list(dict)] Add components from other projects
|
||||
aggregate:
|
||||
# [string=''] Name of the schematic to aggregate
|
||||
- file: ''
|
||||
# [string=''] Name to identify this source. If empty we use the name of the schematic
|
||||
name: ''
|
||||
# [number=1] Number of boards to build (components multiplier). Use negative to substract
|
||||
number: 1
|
||||
# [string=''] A prefix to add to all the references from this project
|
||||
ref_id: ''
|
||||
# [list(dict)|list(string)] List of columns to display.
|
||||
# Can be just the name of the field
|
||||
columns:
|
||||
|
|
@ -129,8 +139,12 @@ outputs:
|
|||
number: 1
|
||||
# [string='%f-%i%v.%x'] filename for the output (%i=bom). Affected by global options
|
||||
output: '%f-%i%v.%x'
|
||||
# [string=''] A prefix to add to all the references from this project. Used for multiple projects
|
||||
ref_id: ''
|
||||
# [string=' '] Separator used for the list of references
|
||||
ref_separator: ' '
|
||||
# [boolean=false] Generate the `Source BoM` column using the reference ID instead of the project name
|
||||
source_by_id: false
|
||||
# [boolean=false] Print grouped references in the alternate compressed style eg: R1-R7,R18
|
||||
use_alt: false
|
||||
# [string=''] Board variant, used to determine which components
|
||||
|
|
|
|||
|
|
@ -166,8 +166,38 @@ class ComponentGroup(object):
|
|||
self.components.append(c)
|
||||
self.refs[c.ref] = c
|
||||
|
||||
def get_count(self):
|
||||
return len(self.components)
|
||||
def get_count(self, project=None):
|
||||
if project is None:
|
||||
# Total components
|
||||
return len(self.components)
|
||||
# Only for the specified project
|
||||
return sum(map(lambda c: c.project == project, self.components))
|
||||
|
||||
def get_build_count(self):
|
||||
if not self.is_fitted():
|
||||
# Not fitted -> 0
|
||||
return 0
|
||||
if len(self.cfg.aggregate) == 1:
|
||||
# Just one project
|
||||
return len(self.components)*self.cfg.number
|
||||
# Multiple projects, count them using the number of board for each project
|
||||
return sum(map(lambda c: self.cfg.qtys[c.project], self.components))
|
||||
|
||||
def get_sources(self):
|
||||
sources = {}
|
||||
for c in self.components:
|
||||
if c.project in sources:
|
||||
sources[c.project] += 1
|
||||
else:
|
||||
sources[c.project] = 1
|
||||
field = ''
|
||||
for prj, n in sources.items():
|
||||
if len(field):
|
||||
field += ' '
|
||||
if self.cfg.source_by_id:
|
||||
prj = self.cfg.source_to_id[prj]
|
||||
field += prj+'('+str(n)+')'
|
||||
return field
|
||||
|
||||
def is_fitted(self):
|
||||
# compare_components ensures all has the same status
|
||||
|
|
@ -196,7 +226,7 @@ class ComponentGroup(object):
|
|||
""" Alternative list of references using ranges """
|
||||
S = Joiner()
|
||||
for n in self.components:
|
||||
P, N = (n.ref_prefix, _suffix_to_num(n.ref_suffix))
|
||||
P, N = (n.ref_id+n.ref_prefix, _suffix_to_num(n.ref_suffix))
|
||||
S.add(P, N)
|
||||
return S.flush(self.cfg.ref_separator)
|
||||
|
||||
|
|
@ -232,9 +262,10 @@ class ComponentGroup(object):
|
|||
else:
|
||||
self.fields[ColumnList.COL_REFERENCE_L] = self.get_refs()
|
||||
# Quantity
|
||||
q = self.get_count()
|
||||
self.fields[ColumnList.COL_GRP_QUANTITY_L] = str(q)
|
||||
self.fields[ColumnList.COL_GRP_BUILD_QUANTITY_L] = str(q * self.cfg.number) if self.is_fitted() else "0"
|
||||
self.fields[ColumnList.COL_GRP_QUANTITY_L] = str(self.get_count())
|
||||
self.total = self.get_build_count()
|
||||
self.fields[ColumnList.COL_GRP_BUILD_QUANTITY_L] = str(self.total)
|
||||
self.fields[ColumnList.COL_SOURCE_BOM_L] = self.get_sources()
|
||||
# Group status
|
||||
status = ' '
|
||||
if not self.is_fitted():
|
||||
|
|
@ -301,6 +332,25 @@ def normalize_value(c, decimal_point):
|
|||
return '{} {}{}'.format(value, mult_s, unit)
|
||||
|
||||
|
||||
def compute_multiple_stats(cfg, groups):
|
||||
for sch in cfg.aggregate:
|
||||
sch.comp_total = 0
|
||||
sch.comp_fitted = 0
|
||||
sch.comp_build = 0
|
||||
sch.comp_groups = 0
|
||||
for g in groups:
|
||||
g_l = g.get_count(sch.name)
|
||||
if g_l:
|
||||
sch.comp_groups = sch.comp_groups+1
|
||||
sch.comp_total += g_l
|
||||
if g.is_fitted():
|
||||
sch.comp_fitted += g_l
|
||||
sch.comp_build = sch.comp_fitted*sch.number
|
||||
if cfg.debug_level > 1:
|
||||
logger.debug('Stats for {}: total {} fitted {} build {}'.
|
||||
format(sch.name, sch.comp_total, sch.comp_fitted, sch.comp_build))
|
||||
|
||||
|
||||
def group_components(cfg, components):
|
||||
groups = []
|
||||
# Iterate through each component, and test whether a group for these already exists
|
||||
|
|
@ -343,6 +393,7 @@ def group_components(cfg, components):
|
|||
# Enumerate the groups and compute stats
|
||||
n_total = 0
|
||||
n_fitted = 0
|
||||
n_build = 0
|
||||
c = 1
|
||||
dnf = 1
|
||||
cfg.n_groups = len(groups)
|
||||
|
|
@ -359,9 +410,15 @@ def group_components(cfg, components):
|
|||
n_total += g_l
|
||||
if is_fitted:
|
||||
n_fitted += g_l
|
||||
n_build += g.total
|
||||
cfg.n_total = n_total
|
||||
cfg.n_fitted = n_fitted
|
||||
cfg.n_build = n_fitted * cfg.number
|
||||
cfg.n_build = n_build
|
||||
if cfg.debug_level > 1:
|
||||
logger.debug('Global stats: total {} fitted {} build {}'.format(n_total, n_fitted, n_build))
|
||||
# Compute stats for multiple schematics
|
||||
if len(cfg.aggregate) > 1:
|
||||
compute_multiple_stats(cfg, groups)
|
||||
return groups
|
||||
|
||||
|
||||
|
|
@ -370,4 +427,7 @@ def do_bom(file_name, ext, comps, cfg):
|
|||
groups = group_components(cfg, comps)
|
||||
# Create the BoM
|
||||
logger.debug("Saving BOM File: "+file_name)
|
||||
number = cfg.number
|
||||
cfg.number = sum(map(lambda prj: prj.number, cfg.aggregate))
|
||||
write_bom(file_name, ext, groups, cfg.columns, cfg)
|
||||
cfg.number = number
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ class ColumnList:
|
|||
COL_GRP_QUANTITY_L = COL_GRP_QUANTITY.lower()
|
||||
COL_GRP_BUILD_QUANTITY = 'Build Quantity'
|
||||
COL_GRP_BUILD_QUANTITY_L = COL_GRP_BUILD_QUANTITY.lower()
|
||||
COL_SOURCE_BOM = 'Source BoM'
|
||||
COL_SOURCE_BOM_L = COL_SOURCE_BOM.lower()
|
||||
|
||||
# Generated columns
|
||||
COLUMNS_GEN_L = {
|
||||
|
|
@ -56,6 +58,7 @@ class ColumnList:
|
|||
COL_GRP_BUILD_QUANTITY_L: 1,
|
||||
COL_ROW_NUMBER_L: 1,
|
||||
COL_STATUS_L: 1,
|
||||
COL_SOURCE_BOM_L: 1,
|
||||
}
|
||||
|
||||
# Default columns
|
||||
|
|
@ -72,7 +75,8 @@ class ColumnList:
|
|||
COL_GRP_BUILD_QUANTITY,
|
||||
COL_STATUS,
|
||||
COL_DATASHEET,
|
||||
COL_SHEETPATH
|
||||
COL_SHEETPATH,
|
||||
COL_SOURCE_BOM,
|
||||
]
|
||||
|
||||
# Default columns
|
||||
|
|
|
|||
|
|
@ -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-2021 Salvador E. Tropea
|
||||
# Copyright (c) 2020-2021 Instituto Nacional de Tecnología Industrial
|
||||
# Copyright (c) 2016-2020 Oliver Henry Walters (@SchrodingersGat)
|
||||
# License: MIT
|
||||
# Project: KiBot (formerly KiPlot)
|
||||
|
|
@ -11,6 +11,58 @@ CSV Writer: Generates a CSV, TSV or TXT BoM file.
|
|||
import csv
|
||||
|
||||
|
||||
def write_stats(writer, cfg):
|
||||
if len(cfg.aggregate) == 1:
|
||||
# Only one project
|
||||
if not cfg.csv.hide_pcb_info:
|
||||
prj = cfg.aggregate[0]
|
||||
writer.writerow(["Project info:"])
|
||||
writer.writerow(["Schematic:", prj.name])
|
||||
writer.writerow(["Variant:", cfg.variant.name])
|
||||
writer.writerow(["Revision:", prj.sch.revision])
|
||||
writer.writerow(["Date:", prj.sch.date])
|
||||
writer.writerow(["KiCad Version:", cfg.kicad_version])
|
||||
if not cfg.csv.hide_stats_info:
|
||||
writer.writerow(["Statistics:"])
|
||||
writer.writerow(["Component Groups:", cfg.n_groups])
|
||||
writer.writerow(["Component Count:", cfg.n_total])
|
||||
writer.writerow(["Fitted Components:", cfg.n_fitted])
|
||||
writer.writerow(["Number of PCBs:", cfg.number])
|
||||
writer.writerow(["Total Components:", cfg.n_build])
|
||||
else:
|
||||
# Multiple projects
|
||||
if not cfg.csv.hide_pcb_info:
|
||||
prj = cfg.aggregate[0]
|
||||
writer.writerow(["Project info:"])
|
||||
writer.writerow(["Variant:", cfg.variant.name])
|
||||
writer.writerow(["KiCad Version:", cfg.kicad_version])
|
||||
if not cfg.csv.hide_stats_info:
|
||||
writer.writerow(["Global statistics:"])
|
||||
writer.writerow(["Component Groups:", cfg.n_groups])
|
||||
writer.writerow(["Component Count:", cfg.n_total])
|
||||
writer.writerow(["Fitted Components:", cfg.n_fitted])
|
||||
writer.writerow(["Number of PCBs:", cfg.number])
|
||||
writer.writerow(["Total Components:", cfg.n_build])
|
||||
# Individual stats
|
||||
for prj in cfg.aggregate:
|
||||
if not cfg.csv.hide_pcb_info:
|
||||
writer.writerow(["Project info:", prj.sch.title])
|
||||
writer.writerow(["Schematic:", prj.name])
|
||||
writer.writerow(["Revision:", prj.sch.revision])
|
||||
writer.writerow(["Date:", prj.sch.date])
|
||||
if prj.sch.company:
|
||||
writer.writerow(["Company:", prj.sch.company])
|
||||
if prj.ref_id:
|
||||
writer.writerow(["ID", prj.ref_id])
|
||||
if not cfg.csv.hide_stats_info:
|
||||
writer.writerow(["Statistics:", prj.sch.title])
|
||||
writer.writerow(["Component Groups:", prj.comp_groups])
|
||||
writer.writerow(["Component Count:", prj.comp_total])
|
||||
writer.writerow(["Fitted Components:", prj.comp_fitted])
|
||||
writer.writerow(["Number of PCBs:", prj.number])
|
||||
writer.writerow(["Total Components:", prj.comp_build])
|
||||
|
||||
|
||||
def write_csv(filename, ext, groups, headings, head_names, cfg):
|
||||
"""
|
||||
Write BoM out to a CSV file
|
||||
|
|
@ -50,19 +102,6 @@ def write_csv(filename, ext, groups, headings, head_names, cfg):
|
|||
for i in range(5):
|
||||
writer.writerow([])
|
||||
# The info
|
||||
if not cfg.csv.hide_pcb_info:
|
||||
writer.writerow(["Project info:"])
|
||||
writer.writerow(["Schematic:", cfg.source])
|
||||
writer.writerow(["Variant:", cfg.variant.name])
|
||||
writer.writerow(["Revision:", cfg.revision])
|
||||
writer.writerow(["Date:", cfg.date])
|
||||
writer.writerow(["KiCad Version:", cfg.kicad_version])
|
||||
if not cfg.csv.hide_stats_info:
|
||||
writer.writerow(["Statistics:"])
|
||||
writer.writerow(["Component Groups:", cfg.n_groups])
|
||||
writer.writerow(["Component Count:", cfg.n_total])
|
||||
writer.writerow(["Fitted Components:", cfg.n_fitted])
|
||||
writer.writerow(["Number of PCBs:", cfg.number])
|
||||
writer.writerow(["Total Components:", cfg.n_build])
|
||||
write_stats(writer, cfg)
|
||||
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ STYLE_COMMON = (" .cell-title { vertical-align: bottom; }\n"
|
|||
" .cell-info { vertical-align: top; padding: 1em;}\n"
|
||||
" .cell-stats { vertical-align: top; padding: 1em;}\n"
|
||||
" .title { font-size:2.5em; font-weight: bold; }\n"
|
||||
" .subtitle { font-size:1.5em; font-weight: bold; }\n"
|
||||
" .h2 { font-size:1.5em; font-weight: bold; }\n"
|
||||
" .td-empty0 { text-align: center; background-color: "+BG_EMPTY+";}\n"
|
||||
" .td-gen0 { text-align: center; background-color: "+BG_GEN+";}\n"
|
||||
|
|
@ -274,6 +275,75 @@ def embed_image(file):
|
|||
return int(w), int(h), 'data:image/png;base64,'+b64encode(s).decode('ascii')
|
||||
|
||||
|
||||
def write_stats(html, cfg):
|
||||
if len(cfg.aggregate) == 1:
|
||||
# Only one project
|
||||
html.write('<tr>\n')
|
||||
html.write(' <td class="cell-info">\n')
|
||||
if not cfg.html.hide_pcb_info:
|
||||
prj = cfg.aggregate[0]
|
||||
html.write(" <b>Schematic</b>: {}<br>\n".format(prj.name))
|
||||
html.write(" <b>Variant</b>: {}<br>\n".format(cfg.variant.name))
|
||||
html.write(" <b>Revision</b>: {}<br>\n".format(prj.sch.revision))
|
||||
html.write(" <b>Date</b>: {}<br>\n".format(prj.sch.date))
|
||||
html.write(" <b>KiCad Version</b>: {}<br>\n".format(cfg.kicad_version))
|
||||
html.write(' </td>\n')
|
||||
html.write(' <td class="cell-stats">\n')
|
||||
if not cfg.html.hide_stats_info:
|
||||
html.write(" <b>Component Groups</b>: {}<br>\n".format(cfg.n_groups))
|
||||
html.write(" <b>Component Count</b>: {} (per PCB)<br>\n\n".format(cfg.n_total))
|
||||
html.write(" <b>Fitted Components</b>: {} (per PCB)<br>\n".format(cfg.n_fitted))
|
||||
html.write(" <b>Number of PCBs</b>: {}<br>\n".format(cfg.number))
|
||||
html.write(" <b>Total Components</b>: {t} (for {n} PCBs)<br>\n".format(n=cfg.number, t=cfg.n_build))
|
||||
html.write(' </td>\n')
|
||||
html.write('</tr>\n')
|
||||
else:
|
||||
# Multiple projects
|
||||
# Global stats
|
||||
html.write('<tr>\n')
|
||||
html.write(' <td class="cell-info">\n')
|
||||
if not cfg.html.hide_pcb_info:
|
||||
html.write(" <b>Variant</b>: {}<br>\n".format(cfg.variant.name))
|
||||
html.write(" <b>KiCad Version</b>: {}<br>\n".format(cfg.kicad_version))
|
||||
html.write(' </td>\n')
|
||||
html.write(' <td class="cell-stats">\n')
|
||||
if not cfg.html.hide_stats_info:
|
||||
html.write(" <b>Component Groups</b>: {}<br>\n".format(cfg.n_groups))
|
||||
html.write(" <b>Component Count</b>: {} (per PCB)<br>\n\n".format(cfg.n_total))
|
||||
html.write(" <b>Fitted Components</b>: {} (per PCB)<br>\n".format(cfg.n_fitted))
|
||||
html.write(" <b>Number of PCBs</b>: {}<br>\n".format(cfg.number))
|
||||
html.write(" <b>Total Components</b>: {t} (for {n} PCBs)<br>\n".format(n=cfg.number, t=cfg.n_build))
|
||||
html.write(' </td>\n')
|
||||
html.write('</tr>\n')
|
||||
# Individual stats
|
||||
for prj in cfg.aggregate:
|
||||
html.write('<tr>\n')
|
||||
html.write(' <td colspan="2" class="cell-title">\n')
|
||||
html.write(' <div class="subtitle">'+prj.sch.title+'</div>\n')
|
||||
html.write(' </td>\n')
|
||||
html.write('</tr>\n')
|
||||
html.write('<tr>\n')
|
||||
html.write(' <td class="cell-info">\n')
|
||||
if not cfg.html.hide_pcb_info:
|
||||
html.write(" <b>Schematic</b>: {}<br>\n".format(prj.name))
|
||||
html.write(" <b>Revision</b>: {}<br>\n".format(prj.sch.revision))
|
||||
html.write(" <b>Date</b>: {}<br>\n".format(prj.sch.date))
|
||||
if prj.sch.company:
|
||||
html.write(" <b>Company</b>: {}<br>\n".format(prj.sch.company))
|
||||
if prj.ref_id:
|
||||
html.write(" <b>ID</b>: {}<br>\n".format(prj.ref_id))
|
||||
html.write(' </td>\n')
|
||||
html.write(' <td class="cell-stats">\n')
|
||||
if not cfg.html.hide_stats_info:
|
||||
html.write(" <b>Component Groups</b>: {}<br>\n".format(prj.comp_groups))
|
||||
html.write(" <b>Component Count</b>: {} (per PCB)<br>\n\n".format(prj.comp_total))
|
||||
html.write(" <b>Fitted Components</b>: {} (per PCB)<br>\n".format(prj.comp_fitted))
|
||||
html.write(" <b>Number of PCBs</b>: {}<br>\n".format(prj.number))
|
||||
html.write(" <b>Total Components</b>: {t} (for {n} PCBs)<br>\n".format(n=prj.number, t=prj.comp_build))
|
||||
html.write(' </td>\n')
|
||||
html.write('</tr>\n')
|
||||
|
||||
|
||||
def write_html(filename, groups, headings, head_names, cfg):
|
||||
"""
|
||||
Write BoM out to a HTML file
|
||||
|
|
@ -340,7 +410,10 @@ def write_html(filename, groups, headings, head_names, cfg):
|
|||
if img or not cfg.html.hide_pcb_info or not cfg.html.hide_stats_info or cfg.html.title:
|
||||
html.write('<table class="head-table">\n')
|
||||
html.write('<tr>\n')
|
||||
html.write(' <td rowspan="2">\n')
|
||||
n = 2
|
||||
if len(cfg.aggregate) > 1:
|
||||
n += 2*len(cfg.aggregate)
|
||||
html.write(' <td rowspan="{}">\n'.format(n))
|
||||
if img:
|
||||
html.write(' <img src="'+img+'" alt="Logo" width="'+str(img_w)+'" height="'+str(img_h)+'">\n')
|
||||
html.write(' </td>\n')
|
||||
|
|
@ -349,24 +422,7 @@ def write_html(filename, groups, headings, head_names, cfg):
|
|||
html.write(' <div class="title">'+cfg.html.title+'</div>\n')
|
||||
html.write(' </td>\n')
|
||||
html.write('</tr>\n')
|
||||
html.write('<tr>\n')
|
||||
html.write(' <td class="cell-info">\n')
|
||||
if not cfg.html.hide_pcb_info:
|
||||
html.write(" <b>Schematic</b>: {}<br>\n".format(cfg.source))
|
||||
html.write(" <b>Variant</b>: {}<br>\n".format(cfg.variant.name))
|
||||
html.write(" <b>Revision</b>: {}<br>\n".format(cfg.revision))
|
||||
html.write(" <b>Date</b>: {}<br>\n".format(cfg.date))
|
||||
html.write(" <b>KiCad Version</b>: {}<br>\n".format(cfg.kicad_version))
|
||||
html.write(' </td>\n')
|
||||
html.write(' <td class="cell-stats">\n')
|
||||
if not cfg.html.hide_stats_info:
|
||||
html.write(" <b>Component Groups</b>: {}<br>\n".format(cfg.n_groups))
|
||||
html.write(" <b>Component Count</b>: {} (per PCB)<br>\n\n".format(cfg.n_total))
|
||||
html.write(" <b>Fitted Components</b>: {} (per PCB)<br>\n".format(cfg.n_fitted))
|
||||
html.write(" <b>Number of PCBs</b>: {}<br>\n".format(cfg.number))
|
||||
html.write(" <b>Total Components</b>: {t} (for {n} PCBs)<br>\n".format(n=cfg.number, t=cfg.n_build))
|
||||
html.write(' </td>\n')
|
||||
html.write('</tr>\n')
|
||||
write_stats(html, cfg)
|
||||
html.write('</table>\n')
|
||||
|
||||
# Fitted groups
|
||||
|
|
|
|||
|
|
@ -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-2021 Salvador E. Tropea
|
||||
# Copyright (c) 2020-2021 Instituto Nacional de Tecnología Industrial
|
||||
# Copyright (c) 2016-2020 Oliver Henry Walters (@SchrodingersGat)
|
||||
# License: MIT
|
||||
# Project: KiBot (formerly KiPlot)
|
||||
|
|
@ -67,14 +67,16 @@ def add_info(worksheet, column_widths, row, col_offset, formats, text, value):
|
|||
|
||||
|
||||
def compute_head_size(cfg):
|
||||
head_size = 7
|
||||
if cfg.xlsx.logo is None:
|
||||
if not cfg.xlsx.title:
|
||||
head_size -= 1
|
||||
if cfg.xlsx.hide_pcb_info and cfg.xlsx.hide_stats_info:
|
||||
head_size -= 5
|
||||
if head_size == 1:
|
||||
head_size = 0
|
||||
col_logo = 0 if cfg.xlsx.logo is None else 6
|
||||
col_info = 1 if cfg.xlsx.title else 0
|
||||
if not (cfg.xlsx.hide_pcb_info and cfg.xlsx.hide_stats_info):
|
||||
col_info += 5
|
||||
if len(cfg.aggregate) > 1:
|
||||
col_info += 6*len(cfg.aggregate)
|
||||
head_size = max(col_logo, col_info)
|
||||
if head_size:
|
||||
# To separate
|
||||
head_size += 1
|
||||
return head_size
|
||||
|
||||
|
||||
|
|
@ -116,6 +118,15 @@ def create_fmt_title(workbook, title):
|
|||
return fmt_title
|
||||
|
||||
|
||||
def create_fmt_subtitle(workbook):
|
||||
fmt_title = workbook.add_format(DEFAULT_FMT)
|
||||
fmt_title.set_font_size(18)
|
||||
fmt_title.set_bold()
|
||||
fmt_title.set_font_name('Arial')
|
||||
fmt_title.set_align('left')
|
||||
return fmt_title
|
||||
|
||||
|
||||
def create_fmt_cols(workbook, col_colors):
|
||||
""" Create the possible column formats """
|
||||
fmt_cols = []
|
||||
|
|
@ -202,6 +213,67 @@ def adjust_heights(worksheet, rows, max_width, head_size):
|
|||
worksheet.set_row(head_size+rn, 15.0*max_h)
|
||||
|
||||
|
||||
def write_info(cfg, r_info_start, worksheet, column_widths, col1, fmt_info, fmt_subtitle):
|
||||
if len(cfg.aggregate) == 1:
|
||||
# Only one project
|
||||
rc = r_info_start
|
||||
if not cfg.xlsx.hide_pcb_info:
|
||||
prj = cfg.aggregate[0]
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Schematic:", prj.name)
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Variant:", cfg.variant.name)
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Revision:", prj.sch.revision)
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Date:", prj.sch.date)
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "KiCad Version:", cfg.kicad_version)
|
||||
col1 += 2
|
||||
rc = r_info_start
|
||||
if not cfg.xlsx.hide_stats_info:
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Component Groups:", cfg.n_groups)
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Component Count:", cfg.n_total)
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Fitted Components:", cfg.n_fitted)
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Number of PCBs:", cfg.number)
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Total Components:", cfg.n_build)
|
||||
else:
|
||||
# Multiple projects
|
||||
# Global stats
|
||||
old_col1 = col1
|
||||
rc = r_info_start
|
||||
if not cfg.xlsx.hide_pcb_info:
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Variant:", cfg.variant.name)
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "KiCad Version:", cfg.kicad_version)
|
||||
col1 += 2
|
||||
rc = r_info_start
|
||||
if not cfg.xlsx.hide_stats_info:
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Component Groups:", cfg.n_groups)
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Component Count:", cfg.n_total)
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Fitted Components:", cfg.n_fitted)
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Number of PCBs:", cfg.number)
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Total Components:", cfg.n_build)
|
||||
# Individual stats
|
||||
for prj in cfg.aggregate:
|
||||
r_info_start += 5
|
||||
col1 = old_col1
|
||||
worksheet.set_row(r_info_start, 24)
|
||||
worksheet.merge_range(r_info_start, col1, r_info_start, len(column_widths)-1, prj.sch.title, fmt_subtitle)
|
||||
r_info_start += 1
|
||||
rc = r_info_start
|
||||
if not cfg.xlsx.hide_pcb_info:
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Schematic:", prj.name)
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Revision:", prj.sch.revision)
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Date:", prj.sch.date)
|
||||
if prj.sch.company:
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Company:", prj.sch.company)
|
||||
if prj.ref_id:
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "ID:", prj.ref_id)
|
||||
col1 += 2
|
||||
rc = r_info_start
|
||||
if not cfg.xlsx.hide_stats_info:
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Component Groups:", prj.comp_groups)
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Component Count:", prj.comp_total)
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Fitted Components:", prj.comp_fitted)
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Number of PCBs:", prj.number)
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Total Components:", prj.comp_build)
|
||||
|
||||
|
||||
def write_xlsx(filename, groups, col_fields, head_names, cfg):
|
||||
"""
|
||||
Write BoM out to a XLSX file
|
||||
|
|
@ -245,6 +317,7 @@ def write_xlsx(filename, groups, col_fields, head_names, cfg):
|
|||
image_data = get_logo_data(cfg.xlsx.logo)
|
||||
# Title
|
||||
fmt_title = create_fmt_title(workbook, cfg.xlsx.title)
|
||||
fmt_subtitle = create_fmt_subtitle(workbook)
|
||||
# Info
|
||||
fmt_info = create_fmt_info(workbook, cfg)
|
||||
|
||||
|
|
@ -310,21 +383,7 @@ def write_xlsx(filename, groups, col_fields, head_names, cfg):
|
|||
worksheet.merge_range(0, col1, 0, len(column_widths)-1, cfg.xlsx.title, fmt_title)
|
||||
# PCB & Stats Info
|
||||
if not (cfg.xlsx.hide_pcb_info and cfg.xlsx.hide_stats_info):
|
||||
rc = r_info_start
|
||||
if not cfg.xlsx.hide_pcb_info:
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Schematic:", cfg.source)
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Variant:", cfg.variant.name)
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Revision:", cfg.revision)
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Date:", cfg.date)
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "KiCad Version:", cfg.kicad_version)
|
||||
col1 += 2
|
||||
rc = r_info_start
|
||||
if not cfg.xlsx.hide_stats_info:
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Component Groups:", cfg.n_groups)
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Component Count:", cfg.n_total)
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Fitted Components:", cfg.n_fitted)
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Number of PCBs:", cfg.number)
|
||||
rc = add_info(worksheet, column_widths, rc, col1, fmt_info, "Total Components:", cfg.n_build)
|
||||
write_info(cfg, r_info_start, worksheet, column_widths, col1, fmt_info, fmt_subtitle)
|
||||
|
||||
# Adjust cols and rows
|
||||
adjust_widths(worksheet, column_widths, max_width)
|
||||
|
|
|
|||
|
|
@ -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-2021 Salvador E. Tropea
|
||||
# Copyright (c) 2020-2021 Instituto Nacional de Tecnología Industrial
|
||||
# Copyright (c) 2016-2020 Oliver Henry Walters (@SchrodingersGat)
|
||||
# License: MIT
|
||||
# Project: KiBot (formerly KiPlot)
|
||||
|
|
@ -23,9 +23,6 @@ def write_xml(filename, groups, headings, head_names, cfg):
|
|||
cfg = BoMOptions object with all the configuration
|
||||
"""
|
||||
attrib = {}
|
||||
attrib['Schematic_Source'] = cfg.source
|
||||
attrib['Schematic_Revision'] = cfg.revision
|
||||
attrib['Schematic_Date'] = cfg.date
|
||||
attrib['PCB_Variant'] = cfg.variant.name
|
||||
attrib['KiCad_Version'] = cfg.kicad_version
|
||||
attrib['Component_Groups'] = str(cfg.n_groups)
|
||||
|
|
@ -33,7 +30,22 @@ def write_xml(filename, groups, headings, head_names, cfg):
|
|||
attrib['Fitted_Components'] = str(cfg.n_fitted)
|
||||
attrib['Number_of_PCBs'] = str(cfg.number)
|
||||
attrib['Total_Components'] = str(cfg.n_build)
|
||||
|
||||
if len(cfg.aggregate) == 1:
|
||||
prj = cfg.aggregate[0]
|
||||
attrib['Schematic_Source'] = prj.name
|
||||
attrib['Schematic_Revision'] = prj.sch.revision
|
||||
attrib['Schematic_Date'] = prj.sch.date
|
||||
else:
|
||||
for n, prj in enumerate(cfg.aggregate):
|
||||
attrib['Schematic{}_Source'.format(n)] = prj.name
|
||||
attrib['Schematic{}_Revision'.format(n)] = prj.sch.revision
|
||||
attrib['Schematic{}_Date'.format(n)] = prj.sch.date
|
||||
attrib['Schematic{}_ID'.format(n)] = prj.ref_id
|
||||
attrib['Component_Groups{}'.format(n)] = str(prj.comp_groups)
|
||||
attrib['Component_Count{}'.format(n)] = str(prj.comp_total)
|
||||
attrib['Fitted_Components{}'.format(n)] = str(prj.comp_fitted)
|
||||
attrib['Number_of_PCBs{}'.format(n)] = str(prj.number)
|
||||
attrib['Total_Components{}'.format(n)] = str(prj.comp_build)
|
||||
xml = ElementTree.Element('KiCad_BOM', attrib=attrib, encoding='utf-8')
|
||||
for group in groups:
|
||||
if cfg.ignore_dnf and not group.is_fitted():
|
||||
|
|
|
|||
37
kibot/gs.py
37
kibot/gs.py
|
|
@ -4,7 +4,6 @@
|
|||
# License: GPL-3.0
|
||||
# Project: KiBot (formerly KiPlot)
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
from sys import exit
|
||||
from .misc import (EXIT_BAD_ARGS)
|
||||
|
|
@ -94,37 +93,11 @@ class GS(object):
|
|||
def load_sch_title_block():
|
||||
if GS.sch_title is not None:
|
||||
return
|
||||
GS.sch_title = ''
|
||||
GS.sch_date = ''
|
||||
GS.sch_rev = ''
|
||||
GS.sch_comp = ''
|
||||
re_val = re.compile(r"(\w+)\s+\"([^\"]+)\"")
|
||||
with open(GS.sch_file) as f:
|
||||
for line in f:
|
||||
m = re_val.match(line)
|
||||
if not m:
|
||||
if line.startswith("$EndDescr"):
|
||||
break
|
||||
# This line is executed, but coverage fails to detect it
|
||||
continue # pragma: no cover
|
||||
name, val = m.groups()
|
||||
if name == "Title":
|
||||
GS.sch_title = val
|
||||
elif name == "Date":
|
||||
GS.sch_date = val
|
||||
elif name == "Rev":
|
||||
GS.sch_rev = val
|
||||
elif name == "Comp":
|
||||
GS.sch_comp = val
|
||||
if not GS.sch_date:
|
||||
file_mtime = os.path.getmtime(GS.sch_file)
|
||||
GS.sch_date = datetime.fromtimestamp(file_mtime).strftime('%Y-%m-%d_%H-%M-%S')
|
||||
if not GS.sch_title:
|
||||
GS.sch_title = GS.sch_basename
|
||||
logger.debug("SCH title: `{}`".format(GS.sch_title))
|
||||
logger.debug("SCH date: `{}`".format(GS.sch_date))
|
||||
logger.debug("SCH revision: `{}`".format(GS.sch_rev))
|
||||
logger.debug("SCH company: `{}`".format(GS.sch_comp))
|
||||
assert GS.sch is not None
|
||||
GS.sch_title = GS.sch.title
|
||||
GS.sch_date = GS.sch.date
|
||||
GS.sch_rev = GS.sch.revision
|
||||
GS.sch_comp = GS.sch.company
|
||||
|
||||
@staticmethod
|
||||
def load_pcb_title_block():
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ Currently oriented to collect the components for the BoM.
|
|||
# Encapsulate file/line
|
||||
import re
|
||||
import os
|
||||
from datetime import datetime
|
||||
from copy import deepcopy
|
||||
from collections import OrderedDict
|
||||
from .config import KiConf, un_quote
|
||||
|
|
@ -807,6 +808,10 @@ class SchematicComponent(object):
|
|||
self.footprint = ''
|
||||
self.datasheet = ''
|
||||
self.desc = ''
|
||||
self.fields = []
|
||||
self.dfields = {}
|
||||
self.fields_bkp = None
|
||||
self.dfields_bkp = None
|
||||
# Will be computed
|
||||
self.fitted = True
|
||||
self.included = True
|
||||
|
|
@ -941,7 +946,7 @@ class SchematicComponent(object):
|
|||
return '{} ({} {})'.format(ref, self.name, self.value)
|
||||
|
||||
@staticmethod
|
||||
def load(f, sheet_path, sheet_path_h, libs, fields, fields_lc):
|
||||
def load(f, project, sheet_path, sheet_path_h, libs, fields, fields_lc):
|
||||
# L lib:name reference
|
||||
line = f.get_line()
|
||||
if not line or line[0] != 'L':
|
||||
|
|
@ -950,6 +955,7 @@ class SchematicComponent(object):
|
|||
if len(res) != 2:
|
||||
raise SchFileError('Malformed component label', line, f)
|
||||
comp = SchematicComponent()
|
||||
comp.project = project
|
||||
comp.name, comp.f_ref = res
|
||||
res = comp.name.split(':')
|
||||
comp.lib = None
|
||||
|
|
@ -985,10 +991,6 @@ class SchematicComponent(object):
|
|||
comp.ar.append(SchematicAltRef.parse(line))
|
||||
line = f.get_line()
|
||||
# F field_number "text" orientation posX posY size Flags (see below) hjustify vjustify/italic/bold "name"
|
||||
comp.fields = []
|
||||
comp.dfields = {}
|
||||
comp.fields_bkp = None
|
||||
comp.dfields_bkp = None
|
||||
while line[0] == 'F':
|
||||
field = SchematicField.parse(line, f)
|
||||
name_lc = field.name.lower()
|
||||
|
|
@ -1275,7 +1277,7 @@ class SchematicSheet(object):
|
|||
self.sheet = None
|
||||
self.id = ''
|
||||
|
||||
def load_sheet(self, parent, sheet_path, sheet_path_h, libs, fields, fields_lc):
|
||||
def load_sheet(self, project, parent, sheet_path, sheet_path_h, libs, fields, fields_lc):
|
||||
assert self.name
|
||||
self.sheet = Schematic()
|
||||
parent_dir = os.path.dirname(parent)
|
||||
|
|
@ -1283,7 +1285,7 @@ class SchematicSheet(object):
|
|||
if len(sheet_path_h) > 1:
|
||||
sheet_path_h += '/'
|
||||
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, libs, fields, fields_lc)
|
||||
self.sheet.load(os.path.join(parent_dir, self.file), project, sheet_path, sheet_path_h, libs, fields, fields_lc)
|
||||
return self.sheet
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -1366,6 +1368,10 @@ class Schematic(object):
|
|||
while True:
|
||||
line = f.get_line()
|
||||
if line.startswith('$EndDescr'):
|
||||
self.title = self.title_block['Title'] if 'Title' in self.title_block else ''
|
||||
self.date = self.title_block['Date'] if 'Date' in self.title_block else ''
|
||||
self.revision = self.title_block['Rev'] if 'Rev' in self.title_block else ''
|
||||
self.company = self.title_block['Comp'] if 'Comp' in self.title_block else ''
|
||||
return
|
||||
elif line.startswith('encoding'):
|
||||
if line[9:14] != 'utf-8':
|
||||
|
|
@ -1382,7 +1388,7 @@ class Schematic(object):
|
|||
raise SchFileError('Wrong entry in title block', line, f)
|
||||
self.title_block[m.group(1)] = m.group(2)
|
||||
|
||||
def load(self, fname, sheet_path='', sheet_path_h='/', libs={}, fields=[], fields_lc=set()):
|
||||
def load(self, fname, project, sheet_path='', sheet_path_h='/', libs={}, fields=[], fields_lc=set()):
|
||||
""" Load a v5.x KiCad Schematic.
|
||||
The caller must be sure the file exists.
|
||||
Only the schematics are loaded not the libs. """
|
||||
|
|
@ -1391,6 +1397,7 @@ class Schematic(object):
|
|||
self.libs = libs
|
||||
self.fields = fields
|
||||
self.fields_lc = fields_lc
|
||||
self.project = project
|
||||
with open(fname, 'rt') as fh:
|
||||
f = SCHLineReader(fh, fname)
|
||||
line = f.get_line()
|
||||
|
|
@ -1410,7 +1417,18 @@ class Schematic(object):
|
|||
line = f.get_line()
|
||||
if not line.startswith('EELAYER END'):
|
||||
raise SchFileError('Missing EELAYER END', line, f)
|
||||
# Load the title block
|
||||
self._get_title_block(f)
|
||||
# Fill in some missing info
|
||||
if not self.date:
|
||||
file_mtime = os.path.getmtime(fname)
|
||||
self.date = datetime.fromtimestamp(file_mtime).strftime('%Y-%m-%d_%H-%M-%S')
|
||||
if not self.title:
|
||||
self.title = os.path.splitext(os.path.basename(fname))[0]
|
||||
logger.debug("SCH title: `{}`".format(self.title))
|
||||
logger.debug("SCH date: `{}`".format(self.date))
|
||||
logger.debug("SCH revision: `{}`".format(self.revision))
|
||||
logger.debug("SCH company: `{}`".format(self.company))
|
||||
line = f.get_line()
|
||||
self.all = []
|
||||
self.components = []
|
||||
|
|
@ -1421,7 +1439,7 @@ class Schematic(object):
|
|||
self.sheets = []
|
||||
while not line.startswith('$EndSCHEMATC'):
|
||||
if line.startswith('$Comp'):
|
||||
obj = SchematicComponent.load(f, sheet_path, sheet_path_h, libs, fields, fields_lc)
|
||||
obj = SchematicComponent.load(f, project, sheet_path, sheet_path_h, libs, fields, fields_lc)
|
||||
self.components.append(obj)
|
||||
elif line.startswith('NoConn'):
|
||||
obj = SchematicConnection.parse(False, line[7:], f)
|
||||
|
|
@ -1448,7 +1466,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, libs, fields, fields_lc))
|
||||
self.sub_sheets.append(sch.load_sheet(project, fname, sheet_path, sheet_path_h, libs, fields, fields_lc))
|
||||
|
||||
def get_files(self):
|
||||
""" A list of the names for all the sheets, including this one. """
|
||||
|
|
|
|||
|
|
@ -145,19 +145,12 @@ def load_board(pcb_file=None):
|
|||
return board
|
||||
|
||||
|
||||
def load_sch():
|
||||
if GS.sch: # Already loaded
|
||||
return
|
||||
GS.check_sch()
|
||||
# We can't yet load the new format
|
||||
if GS.sch_file[-9:] == 'kicad_sch':
|
||||
return
|
||||
GS.sch = Schematic()
|
||||
def load_any_sch(sch, file, project):
|
||||
try:
|
||||
GS.sch.load(GS.sch_file)
|
||||
GS.sch.load_libs(GS.sch_file)
|
||||
sch.load(file, project)
|
||||
sch.load_libs(file)
|
||||
if GS.debug_level > 1:
|
||||
logger.debug('Schematic dependencies: '+str(GS.sch.get_files()))
|
||||
logger.debug('Schematic dependencies: '+str(sch.get_files()))
|
||||
except SchFileError as e:
|
||||
trace_dump()
|
||||
logger.error('At line {} of `{}`: {}'.format(e.line, e.file, e.msg))
|
||||
|
|
@ -170,6 +163,17 @@ def load_sch():
|
|||
exit(EXIT_BAD_CONFIG)
|
||||
|
||||
|
||||
def load_sch():
|
||||
if GS.sch: # Already loaded
|
||||
return
|
||||
GS.check_sch()
|
||||
# We can't yet load the new format
|
||||
if GS.sch_file[-9:] == 'kicad_sch':
|
||||
return
|
||||
GS.sch = Schematic()
|
||||
load_any_sch(GS.sch, GS.sch_file, GS.sch_basename)
|
||||
|
||||
|
||||
def get_board_comps_data(comps):
|
||||
""" Add information from the PCB to the list of components from the schematic.
|
||||
Note that we do it every time the function is called to reset transformation filters like rot_footprint. """
|
||||
|
|
|
|||
|
|
@ -12,12 +12,13 @@ from .gs import GS
|
|||
from .optionable import Optionable, BaseOptions
|
||||
from .registrable import RegOutput
|
||||
from .error import KiPlotConfigurationError
|
||||
from .kiplot import get_board_comps_data
|
||||
from .macros import macros, document, output_class # noqa: F401
|
||||
from .kiplot import get_board_comps_data, load_any_sch
|
||||
from .bom.columnlist import ColumnList, BoMError
|
||||
from .bom.bom import do_bom
|
||||
from .var_kibom import KiBoM
|
||||
from .kicad.v5_sch import Schematic
|
||||
from .fil_base import BaseFilter, apply_exclude_filter, apply_fitted_filter, apply_fixed_filter, reset_filters
|
||||
from .macros import macros, document, output_class # noqa: F401
|
||||
from . import log
|
||||
# To debug the `with document` we can use:
|
||||
# from .mcpyrate.debug import macros, step_expansion
|
||||
|
|
@ -186,6 +187,27 @@ class NoConflict(Optionable):
|
|||
super().__init__()
|
||||
|
||||
|
||||
class Aggregate(Optionable):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
with document:
|
||||
self.file = ''
|
||||
""" Name of the schematic to aggregate """
|
||||
self.name = ''
|
||||
""" Name to identify this source. If empty we use the name of the schematic """
|
||||
self.ref_id = ''
|
||||
""" A prefix to add to all the references from this project """
|
||||
self.number = 1
|
||||
""" Number of boards to build (components multiplier). Use negative to substract """
|
||||
|
||||
def config(self):
|
||||
super().config()
|
||||
if not self.file:
|
||||
raise KiPlotConfigurationError("Missing or empty `file` in aggregate list ({})".format(str(self._tree)))
|
||||
if not self.name:
|
||||
self.name = os.path.splitext(os.path.basename(self.file))[0]
|
||||
|
||||
|
||||
class BoMOptions(BaseOptions):
|
||||
def __init__(self):
|
||||
with document:
|
||||
|
|
@ -256,6 +278,12 @@ class BoMOptions(BaseOptions):
|
|||
""" [list(string)] List of fields where we tolerate conflicts.
|
||||
Use it to avoid undesired warnings.
|
||||
By default the field indicated in `fit_field` and the field `part` are excluded """
|
||||
self.aggregate = Aggregate
|
||||
""" [list(dict)] Add components from other projects """
|
||||
self.ref_id = ''
|
||||
""" A prefix to add to all the references from this project. Used for multiple projects """
|
||||
self.source_by_id = False
|
||||
""" Generate the `Source BoM` column using the reference ID instead of the project name """
|
||||
self._format_example = 'CSV'
|
||||
super().__init__()
|
||||
|
||||
|
|
@ -341,6 +369,9 @@ class BoMOptions(BaseOptions):
|
|||
for field in self.no_conflict:
|
||||
no_conflict.add(field.lower())
|
||||
self.no_conflict = no_conflict
|
||||
# Make sure aggregate is a list
|
||||
if isinstance(self.aggregate, type):
|
||||
self.aggregate = []
|
||||
# Columns
|
||||
self.column_rename = {}
|
||||
self.join = []
|
||||
|
|
@ -351,9 +382,11 @@ class BoMOptions(BaseOptions):
|
|||
# Ignore the part and footprint library, also sheetpath and the Reference in singular
|
||||
ignore = [ColumnList.COL_PART_LIB_L, ColumnList.COL_FP_LIB_L, ColumnList.COL_SHEETPATH_L,
|
||||
ColumnList.COL_REFERENCE_L[:-1]]
|
||||
if self.number <= 1:
|
||||
# For one board avoid COL_GRP_BUILD_QUANTITY
|
||||
ignore.append(ColumnList.COL_GRP_BUILD_QUANTITY_L)
|
||||
if len(self.aggregate) == 0:
|
||||
ignore.append(ColumnList.COL_SOURCE_BOM_L)
|
||||
if self.number == 1:
|
||||
# For one board avoid COL_GRP_BUILD_QUANTITY
|
||||
ignore.append(ColumnList.COL_GRP_BUILD_QUANTITY_L)
|
||||
# Exclude the particular columns
|
||||
self.columns = [h for h in valid_columns if not h.lower() in ignore]
|
||||
else:
|
||||
|
|
@ -384,6 +417,24 @@ class BoMOptions(BaseOptions):
|
|||
# This is the ordered list with the case style defined by the user
|
||||
self.columns = columns
|
||||
|
||||
def aggregate_comps(self, comps):
|
||||
self.qtys = {GS.sch_basename: self.number}
|
||||
for prj in self.aggregate:
|
||||
if not os.path.isfile(prj.file):
|
||||
raise KiPlotConfigurationError("Missing `{}`".format(prj.file))
|
||||
logger.debug('Adding components from project {} ({}) using reference id `{}`'.
|
||||
format(prj.name, prj.file, prj.ref_id))
|
||||
self.qtys[prj.name] = prj.number
|
||||
prj.sch = Schematic()
|
||||
load_any_sch(prj.sch, prj.file, prj.name)
|
||||
new_comps = prj.sch.get_components()
|
||||
if prj.ref_id:
|
||||
for c in new_comps:
|
||||
c.ref = prj.ref_id+c.ref
|
||||
c.ref_id = prj.ref_id
|
||||
comps.extend(new_comps)
|
||||
prj.source = os.path.basename(prj.file)
|
||||
|
||||
def run(self, output_dir):
|
||||
format = self.format.lower()
|
||||
output = self.expand_filename_sch(output_dir, self.output, 'bom', format)
|
||||
|
|
@ -397,6 +448,12 @@ class BoMOptions(BaseOptions):
|
|||
# Get the components list from the schematic
|
||||
comps = GS.sch.get_components()
|
||||
get_board_comps_data(comps)
|
||||
# Apply the reference prefix
|
||||
for c in comps:
|
||||
c.ref = self.ref_id+c.ref
|
||||
c.ref_id = self.ref_id
|
||||
# Aggregate components from other projects
|
||||
self.aggregate_comps(comps)
|
||||
# Apply all the filters
|
||||
reset_filters(comps)
|
||||
apply_exclude_filter(comps, self.exclude_filter)
|
||||
|
|
@ -404,10 +461,27 @@ class BoMOptions(BaseOptions):
|
|||
apply_fixed_filter(comps, self.dnc_filter)
|
||||
# Apply the variant
|
||||
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
|
||||
base_sch.name = GS.sch_basename
|
||||
base_sch.ref_id = self.ref_id
|
||||
base_sch.number = self.number
|
||||
base_sch.sch = GS.sch
|
||||
self.aggregate.insert(0, base_sch)
|
||||
# To translate project to ID
|
||||
if self.source_by_id:
|
||||
self.source_to_id = {prj.name: prj.ref_id for prj in self.aggregate}
|
||||
try:
|
||||
do_bom(output, format, comps, self)
|
||||
except BoMError as e:
|
||||
raise KiPlotConfigurationError(str(e))
|
||||
# Undo the reference prefix
|
||||
if self.ref_id:
|
||||
l_id = len(self.ref_id)
|
||||
for c in filter(lambda c: c.project == GS.sch_basename, comps):
|
||||
c.ref = c.ref[l_id:]
|
||||
c.ref_id = ''
|
||||
|
||||
def get_targets(self, parent, out_dir):
|
||||
return [self.expand_filename_sch(out_dir, self.output, 'bom', self.format.lower())]
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from sys import (exit)
|
|||
from .macros import macros, pre_class # noqa: F401
|
||||
from .gs import (GS)
|
||||
from .optionable import Optionable
|
||||
from .kiplot import check_eeschema_do, exec_with_retry
|
||||
from .kiplot import check_eeschema_do, exec_with_retry, load_sch
|
||||
from .error import (KiPlotConfigurationError)
|
||||
from .misc import (CMD_EESCHEMA_DO, ERC_ERROR)
|
||||
from .log import (get_logger)
|
||||
|
|
@ -28,6 +28,9 @@ class Run_ERC(BasePreFlight): # noqa: F821
|
|||
|
||||
def run(self):
|
||||
check_eeschema_do()
|
||||
# The schematic is loaded only before executing an output related to it.
|
||||
# But here we need data from it.
|
||||
load_sch()
|
||||
output = Optionable.expand_filename_sch(None, GS.out_dir, GS.def_global_output, 'erc', 'txt')
|
||||
logger.debug('ERC report: '+output)
|
||||
cmd = [CMD_EESCHEMA_DO, 'run_erc', '-o', output]
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ U 1 1 5E6A448B
|
|||
P 2200 3650
|
||||
F 0 "R9" V 2280 3650 50 0000 C CNN
|
||||
F 1 "100R" V 2200 3650 50 0000 C CNN
|
||||
F 2 "Resistor_SMD:R_0603_1608Metric" V 2130 3650 50 0001 C CNN
|
||||
F 2 "Resistor_SMD:R_0805_2012Metric" V 2130 3650 50 0001 C CNN
|
||||
F 3 "~" H 2200 3650 50 0001 C CNN
|
||||
1 2200 3650
|
||||
1 0 0 -1
|
||||
|
|
@ -98,7 +98,7 @@ U 1 1 5E6A491A
|
|||
P 2500 3650
|
||||
F 0 "R10" V 2580 3650 50 0000 C CNN
|
||||
F 1 "100" V 2500 3650 50 0000 C CNN
|
||||
F 2 "Resistor_SMD:R_0603_1608Metric" V 2430 3650 50 0001 C CNN
|
||||
F 2 "Resistor_SMD:R_0805_2012Metric" V 2430 3650 50 0001 C CNN
|
||||
F 3 "~" H 2500 3650 50 0001 C CNN
|
||||
1 2500 3650
|
||||
1 0 0 -1
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ if prev_dir not in sys.path:
|
|||
sys.path.insert(0, prev_dir)
|
||||
# Utils import
|
||||
from utils import context
|
||||
from kibot.misc import (DRC_ERROR, ERC_ERROR, BOM_ERROR, CORRUPTED_PCB)
|
||||
from kibot.misc import (DRC_ERROR, ERC_ERROR, BOM_ERROR, CORRUPTED_PCB, CORRUPTED_SCH)
|
||||
|
||||
|
||||
def test_erc_1():
|
||||
|
|
@ -49,7 +49,7 @@ def test_erc_fail_2():
|
|||
""" Using a dummy SCH """
|
||||
prj = '3Rs'
|
||||
ctx = context.TestContext('ERCFail2', prj, 'erc', '')
|
||||
ctx.run(ERC_ERROR)
|
||||
ctx.run(CORRUPTED_SCH)
|
||||
ctx.clean_up()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ def check_l1(ctx):
|
|||
ctx.expect_out_file(o_name)
|
||||
sch = Schematic()
|
||||
try:
|
||||
sch.load(ctx.get_out_path(o_name))
|
||||
sch.load(ctx.get_out_path(o_name), 'no_project')
|
||||
except SchFileError as e:
|
||||
logging.error('At line {} of `{}`: {}'.format(e.line, e.file, e.msg))
|
||||
logging.error('Line content: `{}`'.format(e.code))
|
||||
|
|
|
|||
Loading…
Reference in New Issue