From b66c02d75310fd3b27ae0efae97fc3256cdd9808 Mon Sep 17 00:00:00 2001 From: Brendan Haines Date: Tue, 4 Feb 2020 17:33:02 -0700 Subject: [PATCH] add KiBoM --- scripts/KiBoM/.gitignore | 18 + scripts/KiBoM/.travis.yml | 14 + scripts/KiBoM/KiBOM_CLI.py | 210 +++++++++ scripts/KiBoM/LICENSE.md | 7 + scripts/KiBoM/README.md | 439 +++++++++++++++++++ scripts/KiBoM/__init__.py | 0 scripts/KiBoM/bomlib/__init__.py | 0 scripts/KiBoM/bomlib/bom_writer.py | 86 ++++ scripts/KiBoM/bomlib/columns.py | 126 ++++++ scripts/KiBoM/bomlib/component.py | 576 +++++++++++++++++++++++++ scripts/KiBoM/bomlib/csv_writer.py | 92 ++++ scripts/KiBoM/bomlib/html_writer.py | 139 ++++++ scripts/KiBoM/bomlib/netlist_reader.py | 484 +++++++++++++++++++++ scripts/KiBoM/bomlib/preferences.py | 299 +++++++++++++ scripts/KiBoM/bomlib/sort.py | 11 + scripts/KiBoM/bomlib/units.py | 171 ++++++++ scripts/KiBoM/bomlib/version.py | 2 + scripts/KiBoM/bomlib/xlsx_writer.py | 144 +++++++ scripts/KiBoM/bomlib/xml_writer.py | 65 +++ scripts/KiBoM/example/bom.png | Bin 0 -> 14454 bytes scripts/KiBoM/example/html.png | Bin 0 -> 80315 bytes scripts/KiBoM/example/html_ex.png | Bin 0 -> 7996 bytes scripts/KiBoM/example/ini.png | Bin 0 -> 42731 bytes scripts/KiBoM/example/schem.png | Bin 0 -> 25215 bytes scripts/KiBoM/example/usage.png | Bin 0 -> 12081 bytes scripts/KiBoM/setup.cfg | 10 + scripts/KiBoM/tests/common.bash | 34 ++ scripts/KiBoM/tests/sanity.bash | 21 + 28 files changed, 2948 insertions(+) create mode 100644 scripts/KiBoM/.gitignore create mode 100644 scripts/KiBoM/.travis.yml create mode 100644 scripts/KiBoM/KiBOM_CLI.py create mode 100644 scripts/KiBoM/LICENSE.md create mode 100644 scripts/KiBoM/README.md create mode 100644 scripts/KiBoM/__init__.py create mode 100644 scripts/KiBoM/bomlib/__init__.py create mode 100644 scripts/KiBoM/bomlib/bom_writer.py create mode 100644 scripts/KiBoM/bomlib/columns.py create mode 100644 scripts/KiBoM/bomlib/component.py create mode 100644 scripts/KiBoM/bomlib/csv_writer.py create mode 100644 scripts/KiBoM/bomlib/html_writer.py create mode 100644 scripts/KiBoM/bomlib/netlist_reader.py create mode 100644 scripts/KiBoM/bomlib/preferences.py create mode 100644 scripts/KiBoM/bomlib/sort.py create mode 100644 scripts/KiBoM/bomlib/units.py create mode 100644 scripts/KiBoM/bomlib/version.py create mode 100644 scripts/KiBoM/bomlib/xlsx_writer.py create mode 100644 scripts/KiBoM/bomlib/xml_writer.py create mode 100644 scripts/KiBoM/example/bom.png create mode 100644 scripts/KiBoM/example/html.png create mode 100644 scripts/KiBoM/example/html_ex.png create mode 100644 scripts/KiBoM/example/ini.png create mode 100644 scripts/KiBoM/example/schem.png create mode 100644 scripts/KiBoM/example/usage.png create mode 100644 scripts/KiBoM/setup.cfg create mode 100644 scripts/KiBoM/tests/common.bash create mode 100644 scripts/KiBoM/tests/sanity.bash diff --git a/scripts/KiBoM/.gitignore b/scripts/KiBoM/.gitignore new file mode 100644 index 0000000..468a5a1 --- /dev/null +++ b/scripts/KiBoM/.gitignore @@ -0,0 +1,18 @@ +__pycache__/ +*.py[cod] +*$py.class +*.pyc + +*.kicad_mod +.idea/ + +.env +.venv +env/ +venv/ +VENV/ +ENV/ +env.bak/ +venv.bak/ + +.python-version diff --git a/scripts/KiBoM/.travis.yml b/scripts/KiBoM/.travis.yml new file mode 100644 index 0000000..4a17dbb --- /dev/null +++ b/scripts/KiBoM/.travis.yml @@ -0,0 +1,14 @@ +# travis-ci integration for KiBOM + +language: + - python + +python: + - "2.7" + - "3.7" + +before_install: + - sudo apt-get install flake8 + +script: + - flake8 . diff --git a/scripts/KiBoM/KiBOM_CLI.py b/scripts/KiBoM/KiBOM_CLI.py new file mode 100644 index 0000000..a273047 --- /dev/null +++ b/scripts/KiBoM/KiBOM_CLI.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" + @package + KiBOM - Bill of Materials generation for KiCad + + Generate BOM in xml, csv, txt, tsv, html or xlsx formats. + + - Components are automatically grouped into BoM rows (grouping is configurable) + - Component groups count number of components and list component designators + - Rows are automatically sorted by component reference(s) + - Supports board variants + + Extended options are available in the "bom.ini" config file in the PCB directory (this file is auto-generated with default options the first time the script is executed). + +""" + +from __future__ import print_function + +import sys +import os + +import argparse + +from bomlib.columns import ColumnList +from bomlib.netlist_reader import netlist +from bomlib.bom_writer import WriteBoM +from bomlib.preferences import BomPref + +try: + import xlsxwriter # noqa: F401 +except: + xlsxwriter_available = False +else: + xlsxwriter_available = True + +here = os.path.abspath(os.path.dirname(sys.argv[0])) + +sys.path.append(here) +sys.path.append(os.path.join(here, "KiBOM")) + + +verbose = False + + +def close(*arg): + print(*arg) + sys.exit(0) + + +def say(*arg): + # Simple debug message handler + if verbose: + print(*arg) + + +def isExtensionSupported(filename): + result = False + extensions = [".xml", ".csv", ".txt", ".tsv", ".html"] + if xlsxwriter_available: + extensions.append(".xlsx") + for e in extensions: + if filename.endswith(e): + result = True + break + return result + + +def writeVariant(variant, subdirectory): + if variant is not None: + pref.pcbConfig = variant.strip().split(',') + + print("PCB variant: ", ", ".join(pref.pcbConfig)) + + # Write preference file back out (first run will generate a file with default preferences) + if not have_cfile: + pref.Write(config_file) + say("Writing preferences file %s" % (config_file,)) + + # Individual components + components = [] + + # Component groups + groups = [] + + # Read out the netlist + net = netlist(input_file, prefs=pref) + + # Extract the components + components = net.getInterestingComponents() + + # Group the components + groups = net.groupComponents(components) + + columns = ColumnList(pref.corder) + + # Read out all available fields + for g in groups: + for f in g.fields: + columns.AddColumn(f) + + # Don't add 'boards' column if only one board is specified + if pref.boards <= 1: + columns.RemoveColumn(ColumnList.COL_GRP_BUILD_QUANTITY) + say("Removing:", ColumnList.COL_GRP_BUILD_QUANTITY) + + # Finally, write the BoM out to file + if write_to_bom: + output_file = args.output + + if output_file is None: + output_file = input_file.replace(".xml", ".csv") + + output_path, output_name = os.path.split(output_file) + output_name, output_ext = os.path.splitext(output_name) + + # KiCad BOM dialog by default passes "%O" without an extension. Append our default + if not isExtensionSupported(output_ext): + output_ext = ".csv" + + # Make replacements to custom file_name. + file_name = pref.outputFileName + + file_name = file_name.replace("%O", output_name) + file_name = file_name.replace("%v", net.getVersion()) + if variant is not None: + file_name = file_name.replace("%V", pref.variantFileNameFormat) + file_name = file_name.replace("%V", variant) + else: + file_name = file_name.replace("%V", "") + + if args.subdirectory is not None: + output_path = os.path.join(output_path, args.subdirectory) + if not os.path.exists(os.path.abspath(output_path)): + os.makedirs(os.path.abspath(output_path)) + + output_file = os.path.join(output_path, file_name + output_ext) + output_file = os.path.abspath(output_file) + + say("Output:", output_file) + + return WriteBoM(output_file, groups, net, columns.columns, pref) + + +parser = argparse.ArgumentParser(description="KiBOM Bill of Materials generator script") + +parser.add_argument("netlist", help='xml netlist file. Use "%%I" when running from within KiCad') +parser.add_argument("output", default="", help='BoM output file name.\nUse "%%O" when running from within KiCad to use the default output name (csv file).\nFor e.g. HTML output, use "%%O.html"') +parser.add_argument("-n", "--number", help="Number of boards to build (default = 1)", type=int, default=None) +parser.add_argument("-v", "--verbose", help="Enable verbose output", action='count') +parser.add_argument("-r", "--variant", help="Board variant(s), used to determine which components are output to the BoM. Comma-separate for multiple.", type=str, default=None) +parser.add_argument("-d", "--subdirectory", help="Subdirectory within which to store the generated BoM files.", type=str, default=None) +parser.add_argument("--cfg", help="BoM config file (script will try to use 'bom.ini' if not specified here)") +parser.add_argument("-s", "--separator", help="CSV Separator (default ',')", type=str, default=None) + +args = parser.parse_args() + +input_file = args.netlist + +if not input_file.endswith(".xml"): + close("{i} is not a valid xml file".format(i=input_file)) + +verbose = args.verbose is not None + +input_file = os.path.abspath(input_file) + +say("Input:", input_file) + +# Look for a config file! +# bom.ini by default +ini = os.path.abspath(os.path.join(os.path.dirname(input_file), "bom.ini")) + +# Default value +config_file = ini + +# User can overwrite with a specific config file +if args.cfg: + config_file = args.cfg + +# Read preferences from file. If file does not exists, default preferences will be used +pref = BomPref() + +have_cfile = os.path.exists(config_file) +if have_cfile: + pref.Read(config_file) + say("Config:", config_file) + +# Pass available modules +pref.xlsxwriter_available = xlsxwriter_available + +# Pass various command-line options through +pref.verbose = verbose +if args.number is not None: + pref.boards = args.number +pref.separatorCSV = args.separator + +write_to_bom = True + +if args.variant is not None: + variants = args.variant.split(';') +else: + variants = [None] + +for variant in variants: + result = writeVariant(variant, args) + if not result: + sys.exit(-1) + +sys.exit(0) diff --git a/scripts/KiBoM/LICENSE.md b/scripts/KiBoM/LICENSE.md new file mode 100644 index 0000000..9ca0e71 --- /dev/null +++ b/scripts/KiBoM/LICENSE.md @@ -0,0 +1,7 @@ +Copyright (c) 2016 KiBOM + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/scripts/KiBoM/README.md b/scripts/KiBoM/README.md new file mode 100644 index 0000000..92edb5b --- /dev/null +++ b/scripts/KiBoM/README.md @@ -0,0 +1,439 @@ +# KiBoM + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Travis Status](https://api.travis-ci.org/SchrodingersGat/KiBoM.svg?branch=master)](https://travis-ci.org/SchrodingersGat/KiBoM) + +Configurable BoM generation tool for KiCad EDA (http://kicad-pcb.org/) + +## Description + +KiBoM is a configurable BOM (Bill of Materials) generation tool for KiCad EDA. Written in Python, it can be used directly with KiCad software without the need for any external libraries or plugins. + +KiBoM intelligently groups components based on multiple factors, and can generate BoM files in multiple output formats. + +BoM options are user-configurable in a per-project configuration file. + +## Usage + +The *KiBOM_CLI* script can be run directly from KiCad or from the command line. For command help, run the script with the *-h* flag e.g. + +`python KiBOM_CLI.py -h` + +~~~~ +usage: KiBOM_CLI.py [-h] [-n NUMBER] [-v] [-r VARIANT] [-d SUBDIRECTORY] + [--cfg CFG] [-s SEPARATOR] + netlist output + +KiBOM Bill of Materials generator script + +positional arguments: + netlist xml netlist file. Use "%I" when running from within + KiCad + output BoM output file name. Use "%O" when running from + within KiCad to use the default output name (csv + file). For e.g. HTML output, use "%O.html" + +optional arguments: + -h, --help show this help message and exit + -n NUMBER, --number NUMBER + Number of boards to build (default = 1) + -v, --verbose Enable verbose output + -r VARIANT, --variant VARIANT + Board variant(s), used to determine which components + are output to the BoM. Comma-separate for multiple. + -d SUBDIRECTORY, --subdirectory SUBDIRECTORY + Subdirectory within which to store the generated BoM + files. + --cfg CFG BoM config file (script will try to use 'bom.ini' if + not specified here) + -s SEPARATOR, --separator SEPARATOR + CSV Separator (default ',') + + +~~~~ + + +**netlist** The netlist must be provided to the script. When running from KiCad use "%I" + +**output** This is the path to the BoM output. When running from KiCad, usage "%O" for the default option + +* If a suffix is not specified, CSV output format will be used +* HTML output can be specified within KiCad as: "%O.html" or "%O_BOM.html" (etc) +* XML output can be specified within KiCad as: "%O.xml" (etc) +* XSLX output can be specified within KiCad as: "%O.xlsx" (etc) + +**-n --number** Specify number of boards for calculating part quantities + +**-v --verbose** Enable extra debugging information + +**-r --variant** Specify the PCB *variant(s)*. Support for arbitrary PCB variants allows individual components to be marked as 'fitted' or 'not fitted' in a given variant. You can provide muliple variants comma-separated. You can generate multiple BoMs at once for different variants by using semicolon-separation. + +**-d --subdirectory** Specify a subdirectory (from the provided **output** file) into which the boms should be generated. + +**--cfg** If provided, this is the BoM config file that will be used. If not provided, options will be loaded from "bom.ini" + +**-s --separator** Override the delimiter for CSV or TSV generation + +-------- +To run from KiCad, simply add the same command line in the *Bill of Materials* script window. e.g. to generate a HTML output: + +![alt tag](example/html_ex.png?raw=True "HTML Example") + +## Quick Start + +Download and unzip the files almost anywhere. + +When you start the KiCad schematic editor and choose *Tools>Generate Bill of Materials* expect a *Bill of Material* dialog. Choose the *Add Plugin* button, expect a file chooser dialog. Navigate to where you unzipped the files, select the KiBOM_CLI.py file, and choose the *Open* button. Expect another confirmation dialog and choose *OK*. Expect the *Command Line:* text box to be filled in, and for a description of the plugin to appear in the *Plugin Info* text box. Choose the *Generate* button. Expect some messages in the *Plugin Info* text box, and for a .csv file to exist in your KiCad project directory. + +If you want other than .csv format, edit the *Command Line*, for example inserting ".html" after the "%O". + +If you want more columns in your BoM, before you generate your BoM, in the schematic editor choose *Preferences>Schematic Editor Options* and create new rows in the *Template Field Names* tab. Then edit your components and fill in the fields. KiBoM will reasonably sum rows in the BoM having the same values in your fields. For example, if you have two components both with Vendor=Digikey and SKU=877-5309 (and value and footprints equal), there will be one row with Quantity "2" and References e.g. "R1, R2." + +## Features + +### Intelligent Component Grouping + +To be useful for ordering components, the BoM output from a KiCad project should be organized into sensible component groups. By default, KiBoM groups components based on the following factors: + +* Part name: (e.g. 'R' for resistors, 'C' for capacitors, or longer part names such as 'MAX232') *note: parts such as {'R','r_small'} (which are different symbol representations for the same component) can also be grouped together* +* Value: Components must have the same value to be grouped together +* Footprint: Components must have the same footprint to be grouped together *(this option can be enabled/disabled in the bom.ini configuration file)* + +#### Custom Column Grouping + +If the user wishes to group components based on additional field values, these can be specified in the preferences (.ini) file + +### Intelligent Value Matching + +Some component values can be expressed in multiple ways (e.g. "0.1uF" === "100n" for a capacitor). KiBoM matches value strings based on their interpreted numerical value, such that components are grouped together even if their values are expressed differently. + +### Field Extraction + +In addition to the default KiCad fields which are assigned to each component, KiBoM extracts and custom fields added to the various components. + +**Default Fields** + +The following default fields are extracted and can be added to the output BoM file: +* `Description` : Part description as per the schematic symbol +* `References` : List of part references included in a particular group +* `Quantity` : Number of components included in a particular group +* `Part` : Part name as per the schematic symbol +* `Part Lib` : Part library for the symbol *(default - not output to BoM file)* +* `Footprint` : Part footprint +* `Footprint Lib` : Part footprint library *(default - not output to BoM file)* +* `Datasheet` : Component datasheet extracted either from user-included data, or from part library + +**User Fields** + +If any components have custom fields added, these are available to the output BoM file. + +### Multiple PCB Configurations + +KiBoM allows for arbitrary PCB configurations, which means that the user can specify that individual components will be included or excluded from the BoM in certain circumstances. + +The preferences (.ini) file provides the *fit_field* option which designates a particular part field (default = "Config") which the user can specify whether or not a part is to be included. + +**DNF Parts** + +To specify a part as DNF (do not fit), the *fit_field* field can be set to one of the following values: (case insensitive) + +* "dnf" +* "do not fit" +* "nofit" +* "not fitted" +* "dnp" +* "do not place" +* "no stuff" +* "nostuff" +* "noload" +* "do not load" + +**Note:** + +If the *Value* field for the component contains any of these values, the component will also not be included + +**PCB Variants** + +To generate a BoM with a custom *Variant*, the --variant flag can be used at the command line to specify which variant is to be used. + +If a variant is specified, the value of the *fit_field* field is used to determine if a component will be included in the BoM, as follows: + +* If the *fit_field* value is empty / blank then it will be loaded in ALL variants. +* If the *fit_field* begins with a '-' character, if will be excluded from the matching variant. +* If the *fit_field* begins with a '+' character, if will ONLY be included in the matching variant. + +Multiple variants can be addressed as the *fit_field* can contain multiple comma-separated values. Multiple BoMs can be generated at once by using semicolon-separated values. + +* If you specify multiple variants + - If the *fit_field* contains the variant beginning with a '-' character, it will be excluded irrespective of any other '+' matches. + - If the *fit_field* contains the variant beginning with a '+' and matches any of the given variants, it will be included. + +e.g. if we have a PCB with three components that have the following values in the *fit_field* field: + +* C1 -> "-production,+test" +* C2 -> "+production,+test" +* R1 -> "" +* R2 -> "-test" + +If the script is run with the flag *--variant production* then C2, R1 and R2 will be loaded. + +If the script is run without the *--variant production* flag, then R1 and R2 will be loaded. + +If the script is run with the flag *--variant test*, then C1, C2 and R1 will be loaded. + +If the script is run with the flags *--variant production,test*, then C2 and R1 will be loaded. + +If the script is run with the flags *--variant production;test;production,test*, then three separate BoMs will be generated one as though it had been run with *--variant production*, one for *--variant test*, and one for *--variant production,test*. + +### Regular Expression Matching + +KiBoM features two types of regex matching : "Include" and "Exclude" (each of these are specified within the preferences (bom.ini) file). + +If the user wishes to include ONLY parts that match one-of-many regular expressions, these can be specified in REGEX_INCLUDE section of the bom.ini file + +If the user wishes to exclude components based on one-of-many regular expressions, these are specified in the REGEX_EXCLUDE section of the bom.ini file + +(Refer to the default bom.ini file for examples) + +### Multiple File Outputs +Multiple BoM output formats are supported: +* CSV (Comma separated values) +* TSV (Tab separated values) +* TXT (Text file output with tab separated values) +* XML +* HTML +* XLSX (Needs XlsxWriter Python module) + +Output file format selection is set by the output filename. e.g. "bom.html" will be written to a HTML file, "bom.csv" will be written to a CSV file. + +### Configuration File +BoM generation options can be configured (on a per-project basis) by editing the *bom.ini* file in the PCB project directory. This file is generated the first time that the KiBoM script is run, and allows configuration of the following options. +* `ignore_dnf` : Component groups designated as 'DNF' (do not fit) will be excluded from the BoM output +* `use_alt` : If this option is set, grouped references will be printed in the alternate compressed style eg: R1-R7,R18 +* `alt_wrap` : If this option is set to an integer `N`, the references field will wrap after `N` entries are printed +* `number_rows` : Add row numbers to the BoM output +* `group_connectors` : If this option is set, connector comparison based on the 'Value' field is ignored. This allows multiple connectors which are named for their function (e.g. "Power", "ICP" etc) can be grouped together. +* `test_regex` : If this option is set, each component group row is test against a list of (user configurable) regular expressions. If any matches are found, that row is excluded from the output BoM file. +* `merge_blank_field` : If this option is set, blank fields are able to be merged with non-blank fields (and do not count as a 'conflict') +* `fit_field` : This is the name of the part field used to determine if the component is fitted, or not. +* `output_file_name` : A string that allows arbitrary specification of the output file name with field replacements. Fields available: + - `%O` : The base output file name (pulled from kicad, or specified on command line when calling script). + - `%v` : version number. + - `%V` : variant name, note that this will be ammended according to `variant_file_name_format`. +* `variant_file_name_format` : A string that defines the variant file format. This is a unique field as the variant is not always used/specified. +* `make_backup` : If this option is set, a backup of the bom created before generating the new one. The option is a string that allows arbitrary specification of the filename. See `output_file_name` for available fields. +* `number_boards` : Specifies the number of boards to produce, if none is specified on CLI with `-n`. +* `board_variant` : Specifies the name of the PCB variant, if none is specified on CLI with `-r`. +* `hide_headers` : If this option is set, the table/column headers and legends are suppressed in the output file. +* `hide_pcb_info` : If this option is set, PCB information (version, component count, etc) are suppressed in the output file. +* `IGNORE_COLUMNS` : A list of columns can be marked as 'ignore', and will not be output to the BoM file. By default, the *Part_Lib* and *Footprint_Lib* columns are ignored. +* `GROUP_FIELDS` : A list of component fields used to group components together. +* `COMPONENT_ALIASES` : A list of space-separated values which allows multiple schematic symbol visualisations to be consolidated. +* `REGEX_INCLUDE` : A list of regular expressions used to explicitly include components. If there are no regex here, all components pass this test. If there are regex here, then a component must match at least one of them to be included in the BoM. +* `REGEX_EXCLUDE` : If a component matches any of these regular expressions, it will *not* be included in the BoM. + +Example configuration file (.ini format) *default values shown* + +~~~~ +[BOM_OPTIONS] +; General BoM options here +; If 'ignore_dnf' option is set to 1, rows that are not to be fitted on the PCB will not be written to the BoM file +ignore_dnf = 1 +; If 'use_alt' option is set to 1, grouped references will be printed in the alternate compressed style eg: R1-R7,R18 +use_alt = 0 +; If 'alt_wrap' option is set to and integer N, the references field will wrap after N entries are printed +alt_wrap = 0 +; If 'number_rows' option is set to 1, each row in the BoM will be prepended with an incrementing row number +number_rows = 1 +; If 'group_connectors' option is set to 1, connectors with the same footprints will be grouped together, independent of the name of the connector +group_connectors = 1 +; If 'test_regex' option is set to 1, each component group will be tested against a number of regular-expressions (specified, per column, below). If any matches are found, the row is ignored in the output file +test_regex = 1 +; If 'merge_blank_fields' option is set to 1, component groups with blank fields will be merged into the most compatible group, where possible +merge_blank_fields = 1 +; Specify output file name format, %O is the defined output name, %v is the version, %V is the variant name which will be ammended according to 'variant_file_name_format'. +output_file_name = %O_bom_%v%V +; Specify the variant file name format, this is a unique field as the variant is not always used/specified. When it is unused you will want to strip all of this. +variant_file_name_format = _(%V) +; Field name used to determine if a particular part is to be fitted +fit_field = Config +; Make a backup of the bom before generating the new one, using the following template +make_backup = %O.tmp +; Default number of boards to produce if none given on CLI with -n +number_boards = 1 +; Default PCB variant if none given on CLI with -r +board_variant = "default" +; When set to 1, suppresses table/column headers and legends in the output file. +; May be useful for testing purposes. +hide_headers = 0 +; When set to 1, PCB information (version, component count, etc) is not shown in the output file. +; Useful for saving space in the HTML output and for ensuring CSV output is machine-parseable. +hide_pcb_info = 0 + +[IGNORE_COLUMNS] +; Any column heading that appears here will be excluded from the Generated BoM +; Titles are case-insensitive +Part Lib +Footprint Lib + +[COLUMN_ORDER] +; Columns will apear in the order they are listed here +; Titles are case-insensitive +Description +Part +Part Lib +References +Value +Footprint +Footprint Lib +Quantity Per PCB +Build Quantity +Datasheet + +[GROUP_FIELDS] +; List of fields used for sorting individual components into groups +; Components which match (comparing *all* fields) will be grouped together +; Field names are case-insensitive +Part +Part Lib +Value +Footprint +Footprint Lib + +[COMPONENT_ALIASES] +; A series of values which are considered to be equivalent for the part name +; Each line represents a tab-separated list of equivalent component name values +; e.g. 'c c_small cap' will ensure the equivalent capacitor symbols can be grouped together +; Aliases are case-insensitive +c c_small cap capacitor +r r_small res resistor +sw switch +l l_small inductor +zener zenersmall +d diode d_small + +[REGEX_INCLUDE] +; A series of regular expressions used to include parts in the BoM +; If there are any regex defined here, only components that match against ANY of them will be included in the BOM +; Column names are case-insensitive +; Format is: "ColumName Regex" (tab-separated) + +[REGEX_EXCLUDE] +; A series of regular expressions used to exclude parts from the BoM +; If a component matches ANY of these, it will be excluded from the BoM +; Column names are case-insensitive +; Format is: "ColumName Regex" (tab-separated) +References ^TP[0-9]* +References ^FID +Part mount.*hole +Part solder.*bridge +Part test.*point +Footprint test.*point +Footprint mount.*hole +Footprint fiducial +~~~~ + +## Example + +A simple schematic is shown below. Here a number of resistors, capacitors, and one IC have been added to demonstrate the BoM output capability. Some of the components have custom fields added ('Vendor', 'Rating', 'Notes') + +![alt tag](example/schem.png?raw=True "Schematic") + +Here, a number of logical groups can be seen: + +**R1 R2** +Resistors R1 and R2 have the same value (470 Ohm) even though the value is expressed differently. +Resistors R1 and R2 have the same footprint + +**R3 R4** +Resistors R3 and R4 have the same value and the same footprint + +**R5** +While R5 has the same value as R3 and R4, it is in a different footprint and thus cannot be placed in the same group. + +**C1 C2** +C1 and C2 have the same value and footprint + +**C3 C5** +C3 and C5 have the same value and footprint + +**C4** +C4 has a different footprint to C3 and C5, and thus is grouped separately + +A HTML BoM file is generated as follows: + +![alt tag](example/bom.png?raw=True "BoM") + +To add the BoM script, the Command Line options should be configured as follows: +* path-to-python-script (KiBOM_CLI.py) +* netlist-file "%I" +* output_path "%O_bom.html" (replace file extension for different output file formats) + +Hit the "Generate" button, and the output window should show that the BoM generation was successful. + +### HTML Output +The output HTML file is generated as follows: + +![alt tag](example/html_ex.png?raw=True "HTML Gen") + +![alt tag](example/html.png?raw=True "HTML Output") + +Here the components are correctly grouped, with links to datasheets where appropriate, and fields color-coded. + +### CSV Output +A CSV file output can be generated simply by changing the file extension + + Component,Description,Part,References,Value,Footprint,Quantity,Datasheet,Rating,Vendor,Notes + 1,Unpolarized capacitor,C,C1 C2,0.1uF,C_0805,2,,,, + 2,Unpolarized capacitor,C,C3 C5,2.2uF,C_0805,2,,,, + 3,Unpolarized capacitor,C,C4,2.2uF,C_0603,1,,100V X7R,, + 4,"Connector, single row, 01x09",CONN_01X09,P2,Comms,JST_XH_S09B-XH-A_09x2.50mm_Angled,1,,,, + 5,"Connector, single row, 01x09",CONN_01X09,P1,Power,JST_XH_S09B-XH-A_09x2.50mm_Angled,1,,,, + 6,Resistor,R,R3 R4,100,R_0805,2,,,, + 7,Resistor,R,R5,100,R_0603,1,,0.5W 0.5%,, + 8,Resistor,R,R1 R2,470R,R_0805,2,,,Digikey, + 9,"Dual RS232 driver/receiver, 5V supply, 120kb/s, 0C-70C",MAX232,U1,MAX232,DIP-16_W7.62mm,1 (DNF),http://www.ti.com/lit/ds/symlink/max232.pdf,,,Do not fit + + Component Count:,13 + Component Groups:,9 + Schematic Version:,A.1 + Schematic Date:,2016-05-15 + BoM Date:,15-May-16 5:27:07 PM + Schematic Source:,C:/bom_test/Bom_Test.sch + KiCad Version:,"Eeschema (2016-05-06 BZR 6776, Git 63decd7)-product" + +### XML Output +An XML file output can be generated simply by changing the file extension + + + + + + + + + + + + + + +### XLSX Output +An XLSX file output can be generated simply by changing the file extension + + +## Contributors + +With thanks to the following contributors: + +* https://github.com/bootchk +* https://github.com/diegoherranz +* https://github.com/kylemanna +* https://github.com/pointhi +* https://github.com/schneidersoft +* https://github.com/suzizecat +* https://github.com/marcelobarrosalmeida +* https://github.com/fauxpark +* https://github.com/Swij +* https://github.com/Ximi1970 +* https://github.com/AngusP +* https://github.com/trentks diff --git a/scripts/KiBoM/__init__.py b/scripts/KiBoM/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/KiBoM/bomlib/__init__.py b/scripts/KiBoM/bomlib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/KiBoM/bomlib/bom_writer.py b/scripts/KiBoM/bomlib/bom_writer.py new file mode 100644 index 0000000..15d5df5 --- /dev/null +++ b/scripts/KiBoM/bomlib/bom_writer.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- + +from bomlib.csv_writer import WriteCSV +from bomlib.xml_writer import WriteXML +from bomlib.html_writer import WriteHTML +from bomlib.xlsx_writer import WriteXLSX + +import bomlib.columns as columns +from bomlib.preferences import BomPref + +import os +import shutil + + +def TmpFileCopy(filename, fmt): + # Make a tmp copy of a given file + + filename = os.path.abspath(filename) + + if os.path.exists(filename) and os.path.isfile(filename): + shutil.copyfile(filename, fmt.replace("%O", filename)) + + +def WriteBoM(filename, groups, net, headings=columns.ColumnList._COLUMNS_DEFAULT, prefs=None): + """ + Write BoM to file + filename = output file path + groups = [list of ComponentGroup groups] + headings = [list of headings to display in the BoM file] + prefs = BomPref object + """ + + filename = os.path.abspath(filename) + + # No preferences supplied, use defaults + if not prefs: + prefs = BomPref() + + # Remove any headings that appear in the ignore[] list + headings = [h for h in headings if not h.lower() in [i.lower() for i in prefs.ignore]] + + # If no extension is given, assume .csv (and append!) + if len(filename.split('.')) < 2: + filename += ".csv" + + # Make a temporary copy of the output file + if prefs.backup is not False: + TmpFileCopy(filename, prefs.backup) + + ext = filename.split('.')[-1].lower() + + result = False + + # CSV file writing + if ext in ["csv", "tsv", "txt"]: + if WriteCSV(filename, groups, net, headings, prefs): + print("CSV Output -> {fn}".format(fn=filename)) + result = True + else: + print("Error writing CSV output") + + elif ext in ["htm", "html"]: + if WriteHTML(filename, groups, net, headings, prefs): + print("HTML Output -> {fn}".format(fn=filename)) + result = True + else: + print("Error writing HTML output") + + elif ext in ["xml"]: + if WriteXML(filename, groups, net, headings, prefs): + print("XML Output -> {fn}".format(fn=filename)) + result = True + else: + print("Error writing XML output") + + elif ext in ["xlsx"] and prefs.xlsxwriter_available: + if WriteXLSX(filename, groups, net, headings, prefs): + print("XLSX Output -> {fn}".format(fn=filename)) + result = True + else: + print("Error writing XLSX output") + + else: + print("Unsupported file extension: {ext}".format(ext=ext)) + + return result diff --git a/scripts/KiBoM/bomlib/columns.py b/scripts/KiBoM/bomlib/columns.py new file mode 100644 index 0000000..b2cb3ba --- /dev/null +++ b/scripts/KiBoM/bomlib/columns.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- + + +class ColumnList: + + # Default columns (immutable) + COL_REFERENCE = 'References' + COL_DESCRIPTION = 'Description' + COL_VALUE = 'Value' + COL_FP = 'Footprint' + COL_FP_LIB = 'Footprint Lib' + COL_PART = 'Part' + COL_PART_LIB = 'Part Lib' + COL_DATASHEET = 'Datasheet' + + # Default columns for groups + COL_GRP_QUANTITY = 'Quantity Per PCB' + COL_GRP_TOTAL_COST = 'Total Cost' + COL_GRP_BUILD_QUANTITY = 'Build Quantity' + + # Generated columns + _COLUMNS_GEN = [ + COL_GRP_QUANTITY, + COL_GRP_BUILD_QUANTITY, + ] + + # Default columns + _COLUMNS_DEFAULT = [ + COL_DESCRIPTION, + COL_PART, + COL_PART_LIB, + COL_REFERENCE, + COL_VALUE, + COL_FP, + COL_FP_LIB, + COL_GRP_QUANTITY, + COL_GRP_BUILD_QUANTITY, + COL_DATASHEET + ] + + # Default columns + # These columns are 'immutable' + _COLUMNS_PROTECTED = [ + COL_REFERENCE, + COL_GRP_QUANTITY, + COL_VALUE, + COL_PART, + COL_PART_LIB, + COL_DESCRIPTION, + COL_DATASHEET, + COL_FP, + COL_FP_LIB + ] + + def __str__(self): + return " ".join(map(str, self.columns)) + + def __repr__(self): + return self.__str__() + + def __init__(self, cols=_COLUMNS_DEFAULT): + + self.columns = [] + + # Make a copy of the supplied columns + for col in cols: + self.AddColumn(col) + + def _hasColumn(self, col): + # Col can either be or + return col.lower() in [c.lower() for c in self.columns] + + """ + Remove a column from the list. Specify either the heading or the index + """ + def RemoveColumn(self, col): + if type(col) is str: + self.RemoveColumnByName(col) + elif type(col) is int and col >= 0 and col < len(self.columns): + self.RemoveColumnByName(self.columns[col]) + + def RemoveColumnByName(self, name): + + # First check if this is in an immutable colum + if name in self._COLUMNS_PROTECTED: + return + + # Column does not exist, return + if name not in self.columns: + return + + try: + index = self.columns.index(name) + del self.columns[index] + except ValueError: + return + + # Add a new column (if it doesn't already exist!) + def AddColumn(self, col, index=None): + + # Already exists? + if self._hasColumn(col): + return + + if type(index) is not int or index < 0 or index >= len(self.columns): + self.columns.append(col) + + # Otherwise, splice the new column in + else: + self.columns = self.columns[0:index] + [col] + self.columns[index:] + + +if __name__ == '__main__': + c = ColumnList() + + c.AddColumn("Test1") + c.AddColumn("Test1") + c.AddColumn("Test2") + c.AddColumn("Test3") + c.AddColumn("Test4") + c.AddColumn("Test2") + + c.RemoveColumn("Test2") + c.RemoveColumn("Part") + c.RemoveColumn(2) + c.RemoveColumn(5) diff --git a/scripts/KiBoM/bomlib/component.py b/scripts/KiBoM/bomlib/component.py new file mode 100644 index 0000000..f9d1217 --- /dev/null +++ b/scripts/KiBoM/bomlib/component.py @@ -0,0 +1,576 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from bomlib.columns import ColumnList +from bomlib.preferences import BomPref +import bomlib.units as units +from bomlib.sort import natural_sort +import re +import sys + +DNF = [ + "dnf", + "dnl", + "dnp", + "do not fit", + "do not place", + "do not load", + "nofit", + "nostuff", + "noplace", + "noload", + "not fitted", + "not loaded", + "not placed", + "no stuff", +] + + +class Component(): + """Class for a component, aka 'comp' in the xml netlist file. + This component class is implemented by wrapping an xmlElement instance + with accessors. The xmlElement is held in field 'element'. + """ + + def __init__(self, xml_element, prefs=None): + self.element = xml_element + self.libpart = None + + if not prefs: + prefs = BomPref() + + self.prefs = prefs + + # Set to true when this component is included in a component group + self.grouped = False + + # Compare the value of this part, to the value of another part (see if they match) + def compareValue(self, other): + # Simple string comparison + if self.getValue().lower() == other.getValue().lower(): + return True + + # Otherwise, perform a more complicated value comparison + if units.compareValues(self.getValue(), other.getValue()): + return True + + # Ignore value if both components are connectors + if self.prefs.groupConnectors: + if 'connector' in self.getLibName().lower() and 'connector' in other.getLibName().lower(): + return True + + # No match, return False + return False + + # Determine if two parts have the same name + def comparePartName(self, other): + pn1 = self.getPartName().lower() + pn2 = other.getPartName().lower() + + # Simple direct match + if pn1 == pn2: + return True + + # Compare part aliases e.g. "c" to "c_small" + for alias in self.prefs.aliases: + if pn1 in alias and pn2 in alias: + return True + + return False + + def compareField(self, other, field): + + this_field = self.getField(field).lower() + other_field = other.getField(field).lower() + + # If blank comparisons are allowed + if this_field == "" or other_field == "": + if not self.prefs.mergeBlankFields: + return False + + if this_field == other_field: + return True + + return False + + def __eq__(self, other): + """ + Equivalency operator is used to determine if two parts are 'equal' + """ + + # 'fitted' value must be the same for both parts + if self.isFitted() != other.isFitted(): + return False + + if len(self.prefs.groups) == 0: + return False + + for c in self.prefs.groups: + # Perform special matches + if c.lower() == ColumnList.COL_VALUE.lower(): + if not self.compareValue(other): + return False + # Match part name + elif c.lower() == ColumnList.COL_PART.lower(): + if not self.comparePartName(other): + return False + + # Generic match + elif not self.compareField(other, c): + return False + + return True + + def setLibPart(self, part): + self.libpart = part + + def getPrefix(self): + """ + Get the reference prefix + e.g. if this component has a reference U12, will return "U" + """ + + prefix = "" + + for c in self.getRef(): + if c.isalpha(): + prefix += c + else: + break + + return prefix + + def getSuffix(self): + """ + Return the reference suffix # + e.g. if this component has a reference U12, will return "12" + """ + + suffix = "" + + for c in self.getRef(): + if c.isalpha(): + suffix = "" + else: + suffix += c + + return int(suffix) + + def getLibPart(self): + return self.libpart + + def getPartName(self): + return self.element.get("libsource", "part") + + def getLibName(self): + return self.element.get("libsource", "lib") + + def getDescription(self): + try: + return self.element.get("libsource", "description") + except: + # Compatibility with old KiCad versions (4.x) + ret = self.element.get("field", "name", "description") + + if ret == "": + ret = self.libpart.getDescription() + + return ret + + def setValue(self, value): + """Set the value of this component""" + v = self.element.getChild("value") + if v: + v.setChars(value) + + def getValue(self): + return self.element.get("value") + + def getField(self, name, ignoreCase=True, libraryToo=True): + """Return the value of a field named name. The component is first + checked for the field, and then the components library part is checked + for the field. If the field doesn't exist in either, an empty string is + returned + + Keywords: + name -- The name of the field to return the value for + libraryToo -- look in the libpart's fields for the same name if not found + in component itself + """ + + fp = self.getFootprint().split(":") + + if name.lower() == ColumnList.COL_REFERENCE.lower(): + return self.getRef().strip() + + elif name.lower() == ColumnList.COL_DESCRIPTION.lower(): + return self.getDescription().strip() + + elif name.lower() == ColumnList.COL_DATASHEET.lower(): + return self.getDatasheet().strip() + + # Footprint library is first element + elif name.lower() == ColumnList.COL_FP_LIB.lower(): + if len(fp) > 1: + return fp[0].strip() + else: + # Explicit empty return + return "" + + elif name.lower() == ColumnList.COL_FP.lower(): + if len(fp) > 1: + return fp[1].strip() + elif len(fp) == 1: + return fp[0] + else: + return "" + + elif name.lower() == ColumnList.COL_VALUE.lower(): + return self.getValue().strip() + + elif name.lower() == ColumnList.COL_PART.lower(): + return self.getPartName().strip() + + elif name.lower() == ColumnList.COL_PART_LIB.lower(): + return self.getLibName().strip() + + # Other fields (case insensitive) + for f in self.getFieldNames(): + if f.lower() == name.lower(): + field = self.element.get("field", "name", f) + + if field == "" and libraryToo: + field = self.libpart.getField(f) + + return field.strip() + + # Could not find a matching field + return "" + + def getFieldNames(self): + """Return a list of field names in play for this component. Mandatory + fields are not included, and they are: Value, Footprint, Datasheet, Ref. + The netlist format only includes fields with non-empty values. So if a field + is empty, it will not be present in the returned list. + """ + + fieldNames = [] + + fields = self.element.getChild('fields') + + if fields: + for f in fields.getChildren(): + fieldNames.append(f.get('field', 'name')) + + return fieldNames + + def getRef(self): + return self.element.get("comp", "ref") + + # Determine if a component is FITTED or not + def isFitted(self): + + check = self.getField(self.prefs.configField).lower() + + # Check the value field first + if self.getValue().lower() in DNF: + return False + + # Empty value means part is fitted + if check == "": + return True + + opts = check.lower().split(",") + + exclude = False + include = True + + for opt in opts: + opt = opt.strip() + # Any option containing a DNF is not fitted + if opt in DNF: + exclude = True + break + + # Options that start with '-' are explicitly removed from certain configurations + if opt.startswith("-") and str(opt[1:]) in [str(cfg) for cfg in self.prefs.pcbConfig]: + exclude = True + break + if opt.startswith("+"): + include = include or opt[1:] in [str(cfg) for cfg in self.prefs.pcbConfig] + + return include and not exclude + + # Test if this part should be included, based on any regex expressions provided in the preferences + def testRegExclude(self): + + for reg in self.prefs.regExcludes: + + if type(reg) == list and len(reg) == 2: + field_name, regex = reg + field_value = self.getField(field_name) + + # Attempt unicode escaping... + # Filthy hack + try: + regex = regex.decode("unicode_escape") + except: + pass + + if re.search(regex, field_value, flags=re.IGNORECASE) is not None: + if self.prefs.verbose: + print("Excluding '{ref}': Field '{field}' ({value}) matched '{reg}'".format( + ref=self.getRef(), + field=field_name, + value=field_value, + reg=regex).encode('utf-8')) + + # Found a match + return True + + # Default, could not find any matches + return False + + def testRegInclude(self): + + if len(self.prefs.regIncludes) == 0: # Nothing to match against + return True + + for reg in self.prefs.regIncludes: + + if type(reg) == list and len(reg) == 2: + field_name, regex = reg + field_value = self.getField(field_name) + + print(field_name, field_value, regex) + + if re.search(regex, field_value, flags=re.IGNORECASE) is not None: + if self.prefs.verbose: + print("") + + # Found a match + return True + + # Default, could not find a match + return False + + def getFootprint(self, libraryToo=True): + ret = self.element.get("footprint") + if ret == "" and libraryToo: + if self.libpart: + ret = self.libpart.getFootprint() + return ret + + def getDatasheet(self, libraryToo=True): + ret = self.element.get("datasheet") + if ret == "" and libraryToo: + ret = self.libpart.getDatasheet() + return ret + + def getTimestamp(self): + return self.element.get("tstamp") + + +class joiner: + def __init__(self): + self.stack = [] + + def add(self, P, N): + + if self.stack == []: + self.stack.append(((P, N), (P, N))) + return + + S, E = self.stack[-1] + + if N == E[1] + 1: + self.stack[-1] = (S, (P, N)) + else: + self.stack.append(((P, N), (P, N))) + + def flush(self, sep, N=None, dash='-'): + + refstr = u'' + c = 0 + + for Q in self.stack: + if bool(N) and c != 0 and c % N == 0: + refstr += u'\n' + elif c != 0: + refstr += sep + + S, E = Q + + if S == E: + refstr += "%s%d" % S + c += 1 + else: + # Do we have space? + if bool(N) and (c + 1) % N == 0: + refstr += u'\n' + c += 1 + + refstr += "%s%d%s%s%d" % (S[0], S[1], dash, E[0], E[1]) + c += 2 + return refstr + + +class ComponentGroup(): + + """ + Initialize the group with no components, and default fields + """ + def __init__(self, prefs=None): + self.components = [] + self.fields = dict.fromkeys(ColumnList._COLUMNS_DEFAULT) # Columns loaded from KiCad + + if not prefs: + prefs = BomPref() + + self.prefs = prefs + + def getField(self, field): + + if field not in self.fields.keys(): + return "" + + if not self.fields[field]: + return "" + + return u''.join((self.fields[field])) + + def getCount(self): + return len(self.components) + + # Test if a given component fits in this group + def matchComponent(self, c): + if len(self.components) == 0: + return True + if c == self.components[0]: + return True + + return False + + def containsComponent(self, c): + # Test if a given component is already contained in this grop + if not self.matchComponent(c): + return False + + for comp in self.components: + if comp.getRef() == c.getRef(): + return True + + return False + + def addComponent(self, c): + # Add a component to the group + + if len(self.components) == 0: + self.components.append(c) + elif self.containsComponent(c): + return + elif self.matchComponent(c): + self.components.append(c) + + def isFitted(self): + return any([c.isFitted() for c in self.components]) + + def getRefs(self): + # Return a list of the components + return " ".join([c.getRef() for c in self.components]) + + def getAltRefs(self, wrapN=None): + S = joiner() + + for n in self.components: + P, N = (n.getPrefix(), n.getSuffix()) + S.add(P, N) + + return S.flush(' ', N=wrapN) + + # Sort the components in correct order + def sortComponents(self): + self.components = sorted(self.components, key=lambda c: natural_sort(c.getRef())) + + # Update a given field, based on some rules and such + def updateField(self, field, fieldData): + + # Protected fields cannot be overwritten + if field in ColumnList._COLUMNS_PROTECTED: + return + + if field is None or field == "": + return + elif fieldData == "" or fieldData is None: + return + + if (field not in self.fields.keys()) or (self.fields[field] is None) or (self.fields[field] == ""): + self.fields[field] = fieldData + elif fieldData.lower() in self.fields[field].lower(): + return + else: + print("Field conflict: ({refs}) [{name}] : '{flds}' <- '{fld}'".format( + refs=self.getRefs(), + name=field, + flds=self.fields[field], + fld=fieldData).encode('utf-8')) + self.fields[field] += " " + fieldData + + def updateFields(self, usealt=False, wrapN=None): + for c in self.components: + for f in c.getFieldNames(): + + # These columns are handled explicitly below + if f in ColumnList._COLUMNS_PROTECTED: + continue + + self.updateField(f, c.getField(f)) + + # Update 'global' fields + if usealt: + self.fields[ColumnList.COL_REFERENCE] = self.getAltRefs(wrapN) + else: + self.fields[ColumnList.COL_REFERENCE] = self.getRefs() + + q = self.getCount() + self.fields[ColumnList.COL_GRP_QUANTITY] = "{n}{dnf}".format( + n=q, + dnf=" (DNF)" if not self.isFitted() else "") + + self.fields[ColumnList.COL_GRP_BUILD_QUANTITY] = str(q * self.prefs.boards) if self.isFitted() else "0" + self.fields[ColumnList.COL_VALUE] = self.components[0].getValue() + self.fields[ColumnList.COL_PART] = self.components[0].getPartName() + self.fields[ColumnList.COL_PART_LIB] = self.components[0].getLibName() + self.fields[ColumnList.COL_DESCRIPTION] = self.components[0].getDescription() + self.fields[ColumnList.COL_DATASHEET] = self.components[0].getDatasheet() + + # Footprint field requires special attention + fp = self.components[0].getFootprint().split(":") + + if len(fp) >= 2: + self.fields[ColumnList.COL_FP_LIB] = fp[0] + self.fields[ColumnList.COL_FP] = fp[1] + elif len(fp) == 1: + self.fields[ColumnList.COL_FP_LIB] = "" + self.fields[ColumnList.COL_FP] = fp[0] + else: + self.fields[ColumnList.COL_FP_LIB] = "" + self.fields[ColumnList.COL_FP] = "" + + # Return a dict of the KiCad data based on the supplied columns + # NOW WITH UNICODE SUPPORT! + def getRow(self, columns): + row = [] + for key in columns: + val = self.getField(key) + + if val is None: + val = "" + else: + val = u'' + val + if sys.version_info[0] < 3: + val = val.encode('utf-8') + + row.append(val) + + return row diff --git a/scripts/KiBoM/bomlib/csv_writer.py b/scripts/KiBoM/bomlib/csv_writer.py new file mode 100644 index 0000000..46622a1 --- /dev/null +++ b/scripts/KiBoM/bomlib/csv_writer.py @@ -0,0 +1,92 @@ +# _*_ coding:latin-1 _*_ + +import csv +import os +import sys + + +def WriteCSV(filename, groups, net, headings, prefs): + """ + Write BoM out to a CSV file + filename = path to output file (must be a .csv, .txt or .tsv file) + groups = [list of ComponentGroup groups] + net = netlist object + headings = [list of headings to display in the BoM file] + prefs = BomPref object + """ + + filename = os.path.abspath(filename) + + # Delimeter is assumed from file extension + # Override delimiter if separator specified + if prefs.separatorCSV is not None: + delimiter = prefs.separatorCSV + else: + if filename.endswith(".csv"): + delimiter = "," + elif filename.endswith(".tsv") or filename.endswith(".txt"): + delimiter = "\t" + else: + return False + + nGroups = len(groups) + nTotal = sum([g.getCount() for g in groups]) + nFitted = sum([g.getCount() for g in groups if g.isFitted()]) + nBuild = nFitted * prefs.boards + + if (sys.version_info[0] >= 3): + f = open(filename, "w", encoding='utf-8') + else: + f = open(filename, "w") + + writer = csv.writer(f, delimiter=delimiter, lineterminator="\n") + + if not prefs.hideHeaders: + if prefs.numberRows: + writer.writerow(["Component"] + headings) + else: + writer.writerow(headings) + + count = 0 + rowCount = 1 + + for group in groups: + if prefs.ignoreDNF and not group.isFitted(): + continue + + row = group.getRow(headings) + + if prefs.numberRows: + row = [str(rowCount)] + row + + # Deal with unicode characters + # Row = [el.decode('latin-1') for el in row] + writer.writerow(row) + + try: + count += group.getCount() + except: + pass + + rowCount += 1 + + if not prefs.hidePcbInfo: + # Add some blank rows + for i in range(5): + writer.writerow([]) + + writer.writerow(["Component Groups:", nGroups]) + writer.writerow(["Component Count:", nTotal]) + writer.writerow(["Fitted Components:", nFitted]) + writer.writerow(["Number of PCBs:", prefs.boards]) + writer.writerow(["Total components:", nBuild]) + writer.writerow(["Schematic Version:", net.getVersion()]) + writer.writerow(["Schematic Date:", net.getSheetDate()]) + writer.writerow(["PCB Variant:", ' + '.join(prefs.pcbConfig)]) + writer.writerow(["BoM Date:", net.getDate()]) + writer.writerow(["Schematic Source:", net.getSource()]) + writer.writerow(["KiCad Version:", net.getTool()]) + + f.close() + + return True diff --git a/scripts/KiBoM/bomlib/html_writer.py b/scripts/KiBoM/bomlib/html_writer.py new file mode 100644 index 0000000..6945e8d --- /dev/null +++ b/scripts/KiBoM/bomlib/html_writer.py @@ -0,0 +1,139 @@ +# -*- coding: utf-8 -*- + +from bomlib.component import ColumnList + +BG_GEN = "#E6FFEE" +BG_KICAD = "#FFE6B3" +BG_USER = "#E6F9FF" +BG_EMPTY = "#FF8080" + + +def bgColor(col): + """ Return a background color for a given column title """ + + # Auto-generated columns + if col in ColumnList._COLUMNS_GEN: + return BG_GEN + # KiCad protected columns + elif col in ColumnList._COLUMNS_PROTECTED: + return BG_KICAD + # Additional user columns + else: + return BG_USER + + +def link(text): + + for t in ["http", "https", "ftp", "www"]: + if text.startswith(t): + return '{t}'.format(t=text) + + return text + + +def WriteHTML(filename, groups, net, headings, prefs): + """ + Write BoM out to a HTML file + filename = path to output file (must be a .htm or .html file) + groups = [list of ComponentGroup groups] + net = netlist object + headings = [list of headings to display in the BoM file] + prefs = BomPref object + """ + + if not filename.endswith(".html") and not filename.endswith(".htm"): + print("{fn} is not a valid html file".format(fn=filename)) + return False + + nGroups = len(groups) + nTotal = sum([g.getCount() for g in groups]) + nFitted = sum([g.getCount() for g in groups if g.isFitted()]) + nBuild = nFitted * prefs.boards + + with open(filename, "w") as html: + + # HTML Header + html.write("\n") + html.write("\n") + html.write('\t\n') # UTF-8 encoding for unicode support + html.write("\n") + html.write("\n") + + # PCB info + if not prefs.hideHeaders: + html.write("

KiBoM PCB Bill of Materials

\n") + if not prefs.hidePcbInfo: + html.write('\n') + html.write("\n".format(source=net.getSource())) + html.write("\n".format(date=net.getDate())) + html.write("\n".format(version=net.getVersion())) + html.write("\n".format(date=net.getSheetDate())) + html.write("\n".format(variant=', '.join(prefs.pcbConfig))) + html.write("\n".format(version=net.getTool())) + html.write("\n".format(n=nGroups)) + html.write("\n".format(n=nTotal)) + html.write("\n".format(n=nFitted)) + html.write("\n".format(n=prefs.boards)) + html.write("\n".format(n=prefs.boards, t=nBuild)) + html.write("
Source File{source}
BoM Date{date}
Schematic Version{version}
Schematic Date{date}
PCB Variant{variant}
KiCad Version{version}
Component Groups{n}
Component Count (per PCB){n}
Fitted Components (per PCB){n}
Number of PCBs{n}
Total Component Count
(for {n} PCBs)
{t}
\n") + html.write("
\n") + + if not prefs.hideHeaders: + html.write("

Component Groups

\n") + html.write('

KiCad Fields (default)

\n'.format(bg=BG_KICAD)) + html.write('

Generated Fields

\n'.format(bg=BG_GEN)) + html.write('

User Fields

\n'.format(bg=BG_USER)) + html.write('

Empty Fields

\n'.format(bg=BG_EMPTY)) + + # Component groups + html.write('\n') + + # Row titles: + html.write("\n") + + if prefs.numberRows: + html.write("\t\n") + + for i, h in enumerate(headings): + # Cell background color + bg = bgColor(h) + html.write('\t\n'.format( + h=h, + bg=' bgcolor="{c}"'.format(c=bg) if bg else '') + ) + + html.write("\n") + + rowCount = 0 + + for i, group in enumerate(groups): + + if prefs.ignoreDNF and not group.isFitted(): + continue + + row = group.getRow(headings) + + rowCount += 1 + + html.write("\n") + + if prefs.numberRows: + html.write('\t\n'.format(n=rowCount)) + + for n, r in enumerate(row): + + if (len(r) == 0) or (r.strip() == "~"): + bg = BG_EMPTY + else: + bg = bgColor(headings[n]) + + html.write('\t\n'.format(bg=' bgcolor={c}'.format(c=bg) if bg else '', val=link(r))) + + html.write("\n") + + html.write("
{h}
{n}{val}
\n") + html.write("

\n") + + html.write("") + + return True diff --git a/scripts/KiBoM/bomlib/netlist_reader.py b/scripts/KiBoM/bomlib/netlist_reader.py new file mode 100644 index 0000000..1664e08 --- /dev/null +++ b/scripts/KiBoM/bomlib/netlist_reader.py @@ -0,0 +1,484 @@ +# -*- coding: utf-8 -*- + +""" + @package + Generate a HTML BOM list. + Components are sorted and grouped by value + Any existing fields are read +""" + + +from __future__ import print_function +import sys +import xml.sax as sax + +from bomlib.component import (Component, ComponentGroup) + +from bomlib.preferences import BomPref + +# -------------------------------------------------------------------- + + +class xmlElement(): + """xml element which can represent all nodes of the netlist tree. It can be + used to easily generate various output formats by propogating format + requests to children recursively. + """ + def __init__(self, name, parent=None): + self.name = name + self.attributes = {} + self.parent = parent + self.chars = "" + self.children = [] + + def __str__(self): + """String representation of this netlist element + + """ + return self.name + "[" + self.chars + "]" + " attr_count:" + str(len(self.attributes)) + + def formatXML(self, nestLevel=0, amChild=False): + """Return this element formatted as XML + + Keywords: + nestLevel -- increases by one for each level of nesting. + amChild -- If set to True, the start of document is not returned. + + """ + s = "" + indent = " " * nestLevel + + if not amChild: + s = "\n" + + s += indent + "<" + self.name + for a in self.attributes: + s += " " + a + "=\"" + self.attributes[a] + "\"" + + if (len(self.chars) == 0) and (len(self.children) == 0): + s += "/>" + else: + s += ">" + self.chars + + for c in self.children: + s += "\n" + s += c.formatXML(nestLevel + 1, True) + + if (len(self.children) > 0): + s += "\n" + indent + + if (len(self.children) > 0) or (len(self.chars) > 0): + s += "" + + return s + + def formatHTML(self, amChild=False): + """Return this element formatted as HTML + + Keywords: + amChild -- If set to True, the start of document is not returned + + """ + s = "" + + if not amChild: + s = """ + + + + + + + + """ + + s += "\n" + + for c in self.children: + s += c.formatHTML(True) + + if not amChild: + s += """
" + self.name + "
" + self.chars + "
    " + for a in self.attributes: + s += "
  • " + a + " = " + self.attributes[a] + "
  • " + + s += "
+ + """ + + return s + + def addAttribute(self, attr, value): + """Add an attribute to this element""" + self.attributes[attr] = value + + def setAttribute(self, attr, value): + """Set an attributes value - in fact does the same thing as add + attribute + + """ + self.attributes[attr] = value + + def setChars(self, chars): + """Set the characters for this element""" + self.chars = chars + + def addChars(self, chars): + """Add characters (textual value) to this element""" + self.chars += chars + + def addChild(self, child): + """Add a child element to this element""" + self.children.append(child) + return self.children[len(self.children) - 1] + + def getParent(self): + """Get the parent of this element (Could be None)""" + return self.parent + + def getChild(self, name): + """Returns the first child element named 'name' + + Keywords: + name -- The name of the child element to return""" + for child in self.children: + if child.name == name: + return child + return None + + def getChildren(self, name=None): + if name: + # return _all_ children named "name" + ret = [] + for child in self.children: + if child.name == name: + ret.append(child) + return ret + else: + return self.children + + def get(self, elemName, attribute="", attrmatch=""): + """Return the text data for either an attribute or an xmlElement + """ + if (self.name == elemName): + if attribute != "": + try: + if attrmatch != "": + if self.attributes[attribute] == attrmatch: + return self.chars + else: + return self.attributes[attribute] + except AttributeError: + return "" + else: + return self.chars + + for child in self.children: + ret = child.get(elemName, attribute, attrmatch) + if ret != "": + return ret + + return "" + + +class libpart(): + """Class for a library part, aka 'libpart' in the xml netlist file. + (Components in eeschema are instantiated from library parts.) + This part class is implemented by wrapping an xmlElement with accessors. + This xmlElement instance is held in field 'element'. + """ + def __init__(self, xml_element): + # + self.element = xml_element + + def getLibName(self): + return self.element.get("libpart", "lib") + + def getPartName(self): + return self.element.get("libpart", "part") + + # For backwards Compatibility with v4.x only + def getDescription(self): + return self.element.get("description") + + def getDocs(self): + return self.element.get("docs") + + def getField(self, name): + return self.element.get("field", "name", name) + + def getFieldNames(self): + """Return a list of field names in play for this libpart. + """ + fieldNames = [] + fields = self.element.getChild('fields') + if fields: + for f in fields.getChildren(): + fieldNames.append(f.get('field', 'name')) + return fieldNames + + def getDatasheet(self): + + datasheet = self.getField("Datasheet") + + if not datasheet or datasheet == "": + docs = self.getDocs() + + if "http" in docs or ".pdf" in docs: + datasheet = docs + + return datasheet + + def getFootprint(self): + return self.getField("Footprint") + + def getAliases(self): + """Return a list of aliases or None""" + aliases = self.element.getChild("aliases") + if aliases: + ret = [] + children = aliases.getChildren() + # grab the text out of each child: + for child in children: + ret.append(child.get("alias")) + return ret + return None + + +class netlist(): + """ KiCad generic netlist class. Generally loaded from a KiCad generic + netlist file. Includes several helper functions to ease BOM creating + scripts + + """ + def __init__(self, fname="", prefs=None): + """Initialiser for the genericNetlist class + + Keywords: + fname -- The name of the generic netlist file to open (Optional) + + """ + self.design = None + self.components = [] + self.libparts = [] + self.libraries = [] + self.nets = [] + + # The entire tree is loaded into self.tree + self.tree = [] + + self._curr_element = None + + if not prefs: + prefs = BomPref() # Default values + + self.prefs = prefs + + if fname != "": + self.load(fname) + + def addChars(self, content): + """Add characters to the current element""" + self._curr_element.addChars(content) + + def addElement(self, name): + """Add a new KiCad generic element to the list""" + if self._curr_element is None: + self.tree = xmlElement(name) + self._curr_element = self.tree + else: + self._curr_element = self._curr_element.addChild( + xmlElement(name, self._curr_element)) + + # If this element is a component, add it to the components list + if self._curr_element.name == "comp": + self.components.append(Component(self._curr_element, prefs=self.prefs)) + + # Assign the design element + if self._curr_element.name == "design": + self.design = self._curr_element + + # If this element is a library part, add it to the parts list + if self._curr_element.name == "libpart": + self.libparts.append(libpart(self._curr_element)) + + # If this element is a net, add it to the nets list + if self._curr_element.name == "net": + self.nets.append(self._curr_element) + + # If this element is a library, add it to the libraries list + if self._curr_element.name == "library": + self.libraries.append(self._curr_element) + + return self._curr_element + + def endDocument(self): + """Called when the netlist document has been fully parsed""" + # When the document is complete, the library parts must be linked to + # the components as they are seperate in the tree so as not to + # duplicate library part information for every component + for c in self.components: + for p in self.libparts: + if p.getLibName() == c.getLibName(): + if p.getPartName() == c.getPartName(): + c.setLibPart(p) + break + else: + aliases = p.getAliases() + if aliases and self.aliasMatch(c.getPartName(), aliases): + c.setLibPart(p) + break + + if not c.getLibPart(): + print('missing libpart for ref:', c.getRef(), c.getPartName(), c.getLibName()) + + def aliasMatch(self, partName, aliasList): + for alias in aliasList: + if partName == alias: + return True + return False + + def endElement(self): + """End the current element and switch to its parent""" + self._curr_element = self._curr_element.getParent() + + def getDate(self): + """Return the date + time string generated by the tree creation tool""" + if (sys.version_info[0] >= 3): + return self.design.get("date") + else: + return self.design.get("date").encode('ascii', 'ignore') + + def getSource(self): + """Return the source string for the design""" + if (sys.version_info[0] >= 3): + return self.design.get("source") + else: + return self.design.get("source").encode('ascii', 'ignore') + + def getTool(self): + """Return the tool string which was used to create the netlist tree""" + if (sys.version_info[0] >= 3): + return self.design.get("tool") + else: + return self.design.get("tool").encode('ascii', 'ignore') + + def getSheet(self): + return self.design.getChild("sheet") + + def getSheetDate(self): + sheet = self.getSheet() + if sheet is None: + return "" + return sheet.get("date") + + def getVersion(self): + """Return the verison of the sheet info""" + + sheet = self.getSheet() + + if sheet is None: + return "" + + return sheet.get("rev") + + def getInterestingComponents(self): + + # Copy out the components + ret = [c for c in self.components] + + # Sort first by ref as this makes for easier to read BOM's + ret.sort(key=lambda g: g.getRef()) + + return ret + + def groupComponents(self, components): + + groups = [] + + # Iterate through each component, and test whether a group for these already exists + for c in components: + + if self.prefs.useRegex: + # Skip components if they do not meet regex requirements + if not c.testRegInclude(): + continue + if c.testRegExclude(): + continue + + found = False + + for g in groups: + if g.matchComponent(c): + g.addComponent(c) + found = True + break + + if not found: + g = ComponentGroup(prefs=self.prefs) # Pass down the preferences + g.addComponent(c) + groups.append(g) + + # Sort the references within each group + for g in groups: + g.sortComponents() + g.updateFields(self.prefs.useAlt, self.prefs.altWrap) + + # Sort the groups + # First priority is the Type of component (e.g. R?, U?, L?) + groups = sorted(groups, key=lambda g: [g.components[0].getPrefix(), g.components[0].getValue()]) + + return groups + + def formatXML(self): + """Return the whole netlist formatted in XML""" + return self.tree.formatXML() + + def formatHTML(self): + """Return the whole netlist formatted in HTML""" + return self.tree.formatHTML() + + def load(self, fname): + """Load a KiCad generic netlist + + Keywords: + fname -- The name of the generic netlist file to open + + """ + try: + self._reader = sax.make_parser() + self._reader.setContentHandler(_gNetReader(self)) + self._reader.parse(fname) + except IOError as e: + print(__file__, ":", e, file=sys.stderr) + sys.exit(-1) + + +class _gNetReader(sax.handler.ContentHandler): + """SAX KiCad generic netlist content handler - passes most of the work back + to the 'netlist' class which builds a complete tree in RAM for the design + + """ + def __init__(self, aParent): + self.parent = aParent + + def startElement(self, name, attrs): + """Start of a new XML element event""" + element = self.parent.addElement(name) + + for name in attrs.getNames(): + element.addAttribute(name, attrs.getValue(name)) + + def endElement(self, name): + self.parent.endElement() + + def characters(self, content): + # Ignore erroneous white space - ignoreableWhitespace does not get rid + # of the need for this! + if not content.isspace(): + self.parent.addChars(content) + + def endDocument(self): + """End of the XML document event""" + self.parent.endDocument() diff --git a/scripts/KiBoM/bomlib/preferences.py b/scripts/KiBoM/bomlib/preferences.py new file mode 100644 index 0000000..1a4733e --- /dev/null +++ b/scripts/KiBoM/bomlib/preferences.py @@ -0,0 +1,299 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import sys +import re +import os + +from bomlib.columns import ColumnList + +# Check python version to determine which version of ConfirParser to import +if sys.version_info.major >= 3: + import configparser as ConfigParser +else: + import ConfigParser + + +class BomPref: + + SECTION_IGNORE = "IGNORE_COLUMNS" + SECTION_COLUMN_ORDER = "COLUMN_ORDER" + SECTION_GENERAL = "BOM_OPTIONS" + SECTION_ALIASES = "COMPONENT_ALIASES" + SECTION_GROUPING_FIELDS = "GROUP_FIELDS" + SECTION_REGEXCLUDES = "REGEX_EXCLUDE" + SECTION_REGINCLUDES = "REGEX_INCLUDE" + + OPT_PCB_CONFIG = "pcb_configuration" + OPT_NUMBER_ROWS = "number_rows" + OPT_GROUP_CONN = "group_connectors" + OPT_USE_REGEX = "test_regex" + OPT_USE_ALT = "use_alt" + OPT_ALT_WRAP = "alt_wrap" + OPT_MERGE_BLANK = "merge_blank_fields" + OPT_IGNORE_DNF = "ignore_dnf" + OPT_BACKUP = "make_backup" + OPT_OUTPUT_FILE_NAME = "output_file_name" + OPT_VARIANT_FILE_NAME_FORMAT = "variant_file_name_format" + OPT_DEFAULT_BOARDS = "number_boards" + OPT_DEFAULT_PCBCONFIG = "board_variant" + OPT_CONFIG_FIELD = "fit_field" + OPT_HIDE_HEADERS = "hide_headers" + OPT_HIDE_PCB_INFO = "hide_pcb_info" + + def __init__(self): + # List of headings to ignore in BoM generation + self.ignore = [ + ColumnList.COL_PART_LIB, + ColumnList.COL_FP_LIB, + ] + + self.corder = ColumnList._COLUMNS_DEFAULT + self.useAlt = False # Use alternate reference representation + self.altWrap = None # Wrap to n items when using alt representation + self.ignoreDNF = True # Ignore rows for do-not-fit parts + self.numberRows = True # Add row-numbers to BoM output + self.groupConnectors = True # Group connectors and ignore component value + self.useRegex = True # Test various columns with regex + + self.boards = 1 # Quantity of boards to be made + self.mergeBlankFields = True # Blanks fields will be merged when possible + self.hideHeaders = False + self.hidePcbInfo = False + self.verbose = False # By default, is not verbose + self.configField = "Config" # Default field used for part fitting config + self.pcbConfig = ["default"] + + self.backup = "%O.tmp" + + self.separatorCSV = None + self.outputFileName = "%O_bom_%v%V" + self.variantFileNameFormat = "_(%V)" + + self.xlsxwriter_available = False + self.xlsxwriter2_available = False + + # Default fields used to group components + self.groups = [ + ColumnList.COL_PART, + ColumnList.COL_PART_LIB, + ColumnList.COL_VALUE, + ColumnList.COL_FP, + ColumnList.COL_FP_LIB, + # User can add custom grouping columns in bom.ini + ] + + self.regIncludes = [] # None by default + + self.regExcludes = [ + [ColumnList.COL_REFERENCE, '^TP[0-9]*'], + [ColumnList.COL_REFERENCE, '^FID'], + [ColumnList.COL_PART, 'mount.*hole'], + [ColumnList.COL_PART, 'solder.*bridge'], + [ColumnList.COL_PART, 'test.*point'], + [ColumnList.COL_FP, 'test.*point'], + [ColumnList.COL_FP, 'mount.*hole'], + [ColumnList.COL_FP, 'fiducial'], + ] + + # Default component groupings + self.aliases = [ + ["c", "c_small", "cap", "capacitor"], + ["r", "r_small", "res", "resistor"], + ["sw", "switch"], + ["l", "l_small", "inductor"], + ["zener", "zenersmall"], + ["d", "diode", "d_small"] + ] + + # Check an option within the SECTION_GENERAL group + def checkOption(self, parser, opt, default=False): + if parser.has_option(self.SECTION_GENERAL, opt): + return parser.get(self.SECTION_GENERAL, opt).lower() in ["1", "true", "yes"] + else: + return default + + def checkInt(self, parser, opt, default=False): + if parser.has_option(self.SECTION_GENERAL, opt): + return int(parser.get(self.SECTION_GENERAL, opt).lower()) + else: + return default + + # Read KiBOM preferences from file + def Read(self, file, verbose=False): + file = os.path.abspath(file) + if not os.path.exists(file) or not os.path.isfile(file): + print("{f} is not a valid file!".format(f=file)) + return + + cf = ConfigParser.RawConfigParser(allow_no_value=True) + cf.optionxform = str + + cf.read(file) + + # Read general options + if self.SECTION_GENERAL in cf.sections(): + self.ignoreDNF = self.checkOption(cf, self.OPT_IGNORE_DNF, default=True) + self.useAlt = self.checkOption(cf, self.OPT_USE_ALT, default=False) + self.altWrap = self.checkInt(cf, self.OPT_ALT_WRAP, default=None) + self.numberRows = self.checkOption(cf, self.OPT_NUMBER_ROWS, default=True) + self.groupConnectors = self.checkOption(cf, self.OPT_GROUP_CONN, default=True) + self.useRegex = self.checkOption(cf, self.OPT_USE_REGEX, default=True) + self.mergeBlankFields = self.checkOption(cf, self.OPT_MERGE_BLANK, default=True) + self.outputFileName = cf.get(self.SECTION_GENERAL, self.OPT_OUTPUT_FILE_NAME) + self.variantFileNameFormat = cf.get(self.SECTION_GENERAL, self.OPT_VARIANT_FILE_NAME_FORMAT) + + if cf.has_option(self.SECTION_GENERAL, self.OPT_CONFIG_FIELD): + self.configField = cf.get(self.SECTION_GENERAL, self.OPT_CONFIG_FIELD) + + if cf.has_option(self.SECTION_GENERAL, self.OPT_DEFAULT_BOARDS): + self.boards = self.checkInt(cf, self.OPT_DEFAULT_BOARDS, default=None) + + if cf.has_option(self.SECTION_GENERAL, self.OPT_DEFAULT_PCBCONFIG): + self.pcbConfig = cf.get(self.SECTION_GENERAL, self.OPT_DEFAULT_PCBCONFIG).strip().split(",") + + if cf.has_option(self.SECTION_GENERAL, self.OPT_BACKUP): + self.backup = cf.get(self.SECTION_GENERAL, self.OPT_BACKUP) + else: + self.backup = False + + if cf.has_option(self.SECTION_GENERAL, self.OPT_HIDE_HEADERS): + self.hideHeaders = cf.get(self.SECTION_GENERAL, self.OPT_HIDE_HEADERS) == '1' + + if cf.has_option(self.SECTION_GENERAL, self.OPT_HIDE_PCB_INFO): + self.hidePcbInfo = cf.get(self.SECTION_GENERAL, self.OPT_HIDE_PCB_INFO) == '1' + + # Read out grouping colums + if self.SECTION_GROUPING_FIELDS in cf.sections(): + self.groups = [i for i in cf.options(self.SECTION_GROUPING_FIELDS)] + + # Read out ignored-rows + if self.SECTION_IGNORE in cf.sections(): + self.ignore = [i for i in cf.options(self.SECTION_IGNORE)] + + # Read out column order + if self.SECTION_COLUMN_ORDER in cf.sections(): + self.corder = [i for i in cf.options(self.SECTION_COLUMN_ORDER)] + + # Read out component aliases + if self.SECTION_ALIASES in cf.sections(): + self.aliases = [re.split('[ \t]+', a) for a in cf.options(self.SECTION_ALIASES)] + + if self.SECTION_REGEXCLUDES in cf.sections(): + self.regExcludes = [] + for pair in cf.options(self.SECTION_REGEXCLUDES): + if len(re.split('[ \t]+', pair)) == 2: + self.regExcludes.append(re.split('[ \t]+', pair)) + + if self.SECTION_REGINCLUDES in cf.sections(): + self.regIncludes = [] + for pair in cf.options(self.SECTION_REGINCLUDES): + if len(re.split('[ \t]+', pair)) == 2: + self.regIncludes.append(re.split('[ \t]+', pair)) + + # Add an option to the SECTION_GENRAL group + def addOption(self, parser, opt, value, comment=None): + if comment: + if not comment.startswith(";"): + comment = "; " + comment + parser.set(self.SECTION_GENERAL, comment) + parser.set(self.SECTION_GENERAL, opt, "1" if value else "0") + + # Write KiBOM preferences to file + def Write(self, file): + file = os.path.abspath(file) + + cf = ConfigParser.RawConfigParser(allow_no_value=True) + cf.optionxform = str + + cf.add_section(self.SECTION_GENERAL) + cf.set(self.SECTION_GENERAL, "; General BoM options here") + self.addOption(cf, self.OPT_IGNORE_DNF, self.ignoreDNF, comment="If '{opt}' option is set to 1, rows that are not to be fitted on the PCB will not be written to the BoM file".format(opt=self.OPT_IGNORE_DNF)) + self.addOption(cf, self.OPT_USE_ALT, self.useAlt, comment="If '{opt}' option is set to 1, grouped references will be printed in the alternate compressed style eg: R1-R7,R18".format(opt=self.OPT_USE_ALT)) + self.addOption(cf, self.OPT_ALT_WRAP, self.altWrap, comment="If '{opt}' option is set to and integer N, the references field will wrap after N entries are printed".format(opt=self.OPT_ALT_WRAP)) + self.addOption(cf, self.OPT_NUMBER_ROWS, self.numberRows, comment="If '{opt}' option is set to 1, each row in the BoM will be prepended with an incrementing row number".format(opt=self.OPT_NUMBER_ROWS)) + self.addOption(cf, self.OPT_GROUP_CONN, self.groupConnectors, comment="If '{opt}' option is set to 1, connectors with the same footprints will be grouped together, independent of the name of the connector".format(opt=self.OPT_GROUP_CONN)) + self.addOption(cf, self.OPT_USE_REGEX, self.useRegex, comment="If '{opt}' option is set to 1, each component group will be tested against a number of regular-expressions (specified, per column, below). If any matches are found, the row is ignored in the output file".format(opt=self.OPT_USE_REGEX)) + self.addOption(cf, self.OPT_MERGE_BLANK, self.mergeBlankFields, comment="If '{opt}' option is set to 1, component groups with blank fields will be merged into the most compatible group, where possible".format(opt=self.OPT_MERGE_BLANK)) + + cf.set(self.SECTION_GENERAL, "; Specify output file name format, %O is the defined output name, %v is the version, %V is the variant name which will be ammended according to 'variant_file_name_format'.") + cf.set(self.SECTION_GENERAL, self.OPT_OUTPUT_FILE_NAME, self.outputFileName) + + cf.set(self.SECTION_GENERAL, "; Specify the variant file name format, this is a unique field as the variant is not always used/specified. When it is unused you will want to strip all of this.") + cf.set(self.SECTION_GENERAL, self.OPT_VARIANT_FILE_NAME_FORMAT, self.variantFileNameFormat) + + cf.set(self.SECTION_GENERAL, '; Field name used to determine if a particular part is to be fitted') + cf.set(self.SECTION_GENERAL, self.OPT_CONFIG_FIELD, self.configField) + + cf.set(self.SECTION_GENERAL, '; Make a backup of the bom before generating the new one, using the following template') + cf.set(self.SECTION_GENERAL, self.OPT_BACKUP, self.backup) + + cf.set(self.SECTION_GENERAL, '; Default number of boards to produce if none given on CLI with -n') + cf.set(self.SECTION_GENERAL, self.OPT_DEFAULT_BOARDS, self.boards) + + cf.set(self.SECTION_GENERAL, '; Default PCB variant if none given on CLI with -r') + cf.set(self.SECTION_GENERAL, self.OPT_DEFAULT_PCBCONFIG, self.pcbConfig) + + cf.set(self.SECTION_GENERAL, '; Whether to hide headers from output file') + cf.set(self.SECTION_GENERAL, self.OPT_HIDE_HEADERS, self.hideHeaders) + + cf.set(self.SECTION_GENERAL, '; Whether to hide PCB info from output file') + cf.set(self.SECTION_GENERAL, self.OPT_HIDE_PCB_INFO, self.hidePcbInfo) + + cf.add_section(self.SECTION_IGNORE) + cf.set(self.SECTION_IGNORE, "; Any column heading that appears here will be excluded from the Generated BoM") + cf.set(self.SECTION_IGNORE, "; Titles are case-insensitive") + + for i in self.ignore: + cf.set(self.SECTION_IGNORE, i) + + cf.add_section(self.SECTION_COLUMN_ORDER) + cf.set(self.SECTION_COLUMN_ORDER, "; Columns will apear in the order they are listed here") + cf.set(self.SECTION_COLUMN_ORDER, "; Titles are case-insensitive") + + for i in self.corder: + cf.set(self.SECTION_COLUMN_ORDER, i) + + # Write the component grouping fields + cf.add_section(self.SECTION_GROUPING_FIELDS) + cf.set(self.SECTION_GROUPING_FIELDS, '; List of fields used for sorting individual components into groups') + cf.set(self.SECTION_GROUPING_FIELDS, '; Components which match (comparing *all* fields) will be grouped together') + cf.set(self.SECTION_GROUPING_FIELDS, '; Field names are case-insensitive') + + for i in self.groups: + cf.set(self.SECTION_GROUPING_FIELDS, i) + + cf.add_section(self.SECTION_ALIASES) + cf.set(self.SECTION_ALIASES, "; A series of values which are considered to be equivalent for the part name") + cf.set(self.SECTION_ALIASES, "; Each line represents a list of equivalent component name values separated by white space") + cf.set(self.SECTION_ALIASES, "; e.g. 'c c_small cap' will ensure the equivalent capacitor symbols can be grouped together") + cf.set(self.SECTION_ALIASES, '; Aliases are case-insensitive') + + for a in self.aliases: + cf.set(self.SECTION_ALIASES, "\t".join(a)) + + cf.add_section(self.SECTION_REGINCLUDES) + cf.set(self.SECTION_REGINCLUDES, '; A series of regular expressions used to include parts in the BoM') + cf.set(self.SECTION_REGINCLUDES, '; If there are any regex defined here, only components that match against ANY of them will be included in the BOM') + cf.set(self.SECTION_REGINCLUDES, '; Column names are case-insensitive') + cf.set(self.SECTION_REGINCLUDES, '; Format is: "[ColumName] [Regex]" (white-space separated)') + + for i in self.regIncludes: + if not len(i) == 2: + continue + cf.set(self.SECTION_REGINCLUDES, i[0] + "\t" + i[1]) + + cf.add_section(self.SECTION_REGEXCLUDES) + cf.set(self.SECTION_REGEXCLUDES, '; A series of regular expressions used to exclude parts from the BoM') + cf.set(self.SECTION_REGEXCLUDES, '; If a component matches ANY of these, it will be excluded from the BoM') + cf.set(self.SECTION_REGEXCLUDES, '; Column names are case-insensitive') + cf.set(self.SECTION_REGEXCLUDES, '; Format is: "[ColumName] [Regex]" (white-space separated)') + + for i in self.regExcludes: + if not len(i) == 2: + continue + + cf.set(self.SECTION_REGEXCLUDES, i[0] + "\t" + i[1]) + + with open(file, 'wb') as configfile: + cf.write(configfile) diff --git a/scripts/KiBoM/bomlib/sort.py b/scripts/KiBoM/bomlib/sort.py new file mode 100644 index 0000000..08f365a --- /dev/null +++ b/scripts/KiBoM/bomlib/sort.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- + +import re + + +def natural_sort(string): + """ + Natural sorting function which sorts by numerical value of a string, + rather than raw ASCII value. + """ + return [int(s) if s.isdigit() else s for s in re.split(r'(\d+)', string)] diff --git a/scripts/KiBoM/bomlib/units.py b/scripts/KiBoM/bomlib/units.py new file mode 100644 index 0000000..da68c4d --- /dev/null +++ b/scripts/KiBoM/bomlib/units.py @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- + +""" + +This file contains a set of functions for matching values which may be written in different formats +e.g. +0.1uF = 100n (different suffix specified, one has missing unit) +0R1 = 0.1Ohm (Unit replaces decimal, different units) + +""" + +from __future__ import unicode_literals +import re + +PREFIX_MICRO = [u"μ", "u", "micro"] +PREFIX_MILLI = ["milli", "m"] +PREFIX_NANO = ["nano", "n"] +PREFIX_PICO = ["pico", "p"] +PREFIX_KILO = ["kilo", "k"] +PREFIX_MEGA = ["mega", "meg"] +PREFIX_GIGA = ["giga", "g"] + +# All prefixes +PREFIX_ALL = PREFIX_PICO + PREFIX_NANO + PREFIX_MICRO + PREFIX_MILLI + PREFIX_KILO + PREFIX_MEGA + PREFIX_GIGA + +# Common methods of expressing component units +UNIT_R = ["r", "ohms", "ohm", u"Ω"] +UNIT_C = ["farad", "f"] +UNIT_L = ["henry", "h"] + +UNIT_ALL = UNIT_R + UNIT_C + UNIT_L + + +def getUnit(unit): + """ + Return a simplified version of a units string, for comparison purposes + """ + + if not unit: + return None + + unit = unit.lower() + + if unit in UNIT_R: + return "R" + if unit in UNIT_C: + return "F" + if unit in UNIT_L: + return "H" + + return None + + +def getPrefix(prefix): + """ + Return the (numerical) value of a given prefix + """ + + if not prefix: + return 1 + + prefix = prefix.lower() + + if prefix in PREFIX_PICO: + return 1.0e-12 + if prefix in PREFIX_NANO: + return 1.0e-9 + if prefix in PREFIX_MICRO: + return 1.0e-6 + if prefix in PREFIX_MILLI: + return 1.0e-3 + if prefix in PREFIX_KILO: + return 1.0e3 + if prefix in PREFIX_MEGA: + return 1.0e6 + if prefix in PREFIX_GIGA: + return 1.0e9 + + return 1 + + +def groupString(group): # Return a reg-ex string for a list of values + return "|".join(group) + + +def matchString(): + return "^([0-9\.]+)(" + groupString(PREFIX_ALL) + ")*(" + groupString(UNIT_ALL) + ")*(\d*)$" + + +def compMatch(component): + """ + Return a normalized value and units for a given component value string + e.g. compMatch("10R2") returns (10, R) + e.g. compMatch("3.3mOhm") returns (0.0033, R) + """ + + # Remove any commas + component = component.strip().replace(",", "").lower() + + match = matchString() + + result = re.search(match, component) + + if not result: + return None + + if not len(result.groups()) == 4: + return None + + value, prefix, units, post = result.groups() + + # Special case where units is in the middle of the string + # e.g. "0R05" for 0.05Ohm + # In this case, we will NOT have a decimal + # We will also have a trailing number + + if post and "." not in value: + try: + value = float(int(value)) + postValue = float(int(post)) / (10 ** len(post)) + value = value * 1.0 + postValue + except: + return None + + try: + val = float(value) + except: + return None + + val = "{0:.15f}".format(val * 1.0 * getPrefix(prefix)) + + return (val, getUnit(units)) + + +def componentValue(valString): + + result = compMatch(valString) + + if not result: + return valString # Return the same string back + + if not len(result) == 2: # Result length is incorrect + return valString + + val = result[0] + + return val + + +def compareValues(c1, c2): + """ Compare two values """ + + r1 = compMatch(c1) + r2 = compMatch(c2) + + if not r1 or not r2: + return False + + (v1, u1) = r1 + (v2, u2) = r2 + + if v1 == v2: + # Values match + if u1 == u2: + return True # Units match + if not u1: + return True # No units for component 1 + if not u2: + return True # No units for component 2 + + return False diff --git a/scripts/KiBoM/bomlib/version.py b/scripts/KiBoM/bomlib/version.py new file mode 100644 index 0000000..8ecfdea --- /dev/null +++ b/scripts/KiBoM/bomlib/version.py @@ -0,0 +1,2 @@ +KIBOM_VERSION = "1.52" +KIBOM_DATE = "2018-9-16" diff --git a/scripts/KiBoM/bomlib/xlsx_writer.py b/scripts/KiBoM/bomlib/xlsx_writer.py new file mode 100644 index 0000000..43018e5 --- /dev/null +++ b/scripts/KiBoM/bomlib/xlsx_writer.py @@ -0,0 +1,144 @@ +# _*_ coding:latin-1 _*_ + +try: + import xlsxwriter +except: + def WriteXLSX(filename, groups, net, headings, prefs): + return False +else: + import os + + """ + Write BoM out to a XLSX file + filename = path to output file (must be a .xlsx file) + groups = [list of ComponentGroup groups] + net = netlist object + headings = [list of headings to display in the BoM file] + prefs = BomPref object + """ + + def WriteXLSX(filename, groups, net, headings, prefs): + + filename = os.path.abspath(filename) + + if not filename.endswith(".xlsx"): + return False + + nGroups = len(groups) + nTotal = sum([g.getCount() for g in groups]) + nFitted = sum([g.getCount() for g in groups if g.isFitted()]) + nBuild = nFitted * prefs.boards + + workbook = xlsxwriter.Workbook(filename) + worksheet = workbook.add_worksheet() + + if prefs.numberRows: + row_headings = ["Component"] + headings + else: + row_headings = headings + + cellformats = {} + column_widths = {} + for i in range(len(row_headings)): + cellformats[i] = workbook.add_format({'align': 'center_across'}) + column_widths[i] = len(row_headings[i]) + 10 + + if not prefs.hideHeaders: + worksheet.write_string(0, i, row_headings[i], cellformats[i]) + + count = 0 + rowCount = 1 + + for i, group in enumerate(groups): + if prefs.ignoreDNF and not group.isFitted(): + continue + + row = group.getRow(headings) + + if prefs.numberRows: + row = [str(rowCount)] + row + + for columnCount in range(len(row)): + + cell = row[columnCount].decode('utf-8') + + worksheet.write_string(rowCount, columnCount, cell, cellformats[columnCount]) + + if len(cell) > column_widths[columnCount] - 5: + column_widths[columnCount] = len(cell) + 5 + + try: + count += group.getCount() + except: + pass + + rowCount += 1 + + if not prefs.hidePcbInfo: + # Add a few blank rows + for i in range(5): + rowCount += 1 + + cellformat_left = workbook.add_format({'align': 'left'}) + + worksheet.write_string(rowCount, 0, "Component Groups:", cellformats[0]) + worksheet.write_number(rowCount, 1, nGroups, cellformat_left) + rowCount += 1 + + worksheet.write_string(rowCount, 0, "Component Count:", cellformats[0]) + worksheet.write_number(rowCount, 1, nTotal, cellformat_left) + rowCount += 1 + + worksheet.write_string(rowCount, 0, "Fitted Components:", cellformats[0]) + worksheet.write_number(rowCount, 1, nFitted, cellformat_left) + rowCount += 1 + + worksheet.write_string(rowCount, 0, "Number of PCBs:", cellformats[0]) + worksheet.write_number(rowCount, 1, prefs.boards, cellformat_left) + rowCount += 1 + + worksheet.write_string(rowCount, 0, "Total components:", cellformats[0]) + worksheet.write_number(rowCount, 1, nBuild, cellformat_left) + rowCount += 1 + + worksheet.write_string(rowCount, 0, "Schematic Version:", cellformats[0]) + worksheet.write_string(rowCount, 1, net.getVersion(), cellformat_left) + rowCount += 1 + + if len(net.getVersion()) > column_widths[1]: + column_widths[1] = len(net.getVersion()) + + worksheet.write_string(rowCount, 0, "Schematic Date:", cellformats[0]) + worksheet.write_string(rowCount, 1, net.getSheetDate(), cellformat_left) + rowCount += 1 + + if len(net.getSheetDate()) > column_widths[1]: + column_widths[1] = len(net.getSheetDate()) + + worksheet.write_string(rowCount, 0, "BoM Date:", cellformats[0]) + worksheet.write_string(rowCount, 1, net.getDate(), cellformat_left) + rowCount += 1 + + if len(net.getDate()) > column_widths[1]: + column_widths[1] = len(net.getDate()) + + worksheet.write_string(rowCount, 0, "Schematic Source:", cellformats[0]) + worksheet.write_string(rowCount, 1, net.getSource(), cellformat_left) + rowCount += 1 + + if len(net.getSource()) > column_widths[1]: + column_widths[1] = len(net.getSource()) + + worksheet.write_string(rowCount, 0, "KiCad Version:", cellformats[0]) + worksheet.write_string(rowCount, 1, net.getTool(), cellformat_left) + rowCount += 1 + + if len(net.getTool()) > column_widths[1]: + column_widths[1] = len(net.getTool()) + + for i in range(len(column_widths)): + worksheet.set_column(i, i, column_widths[i]) + + workbook.close() + + return True diff --git a/scripts/KiBoM/bomlib/xml_writer.py b/scripts/KiBoM/bomlib/xml_writer.py new file mode 100644 index 0000000..b72b3b5 --- /dev/null +++ b/scripts/KiBoM/bomlib/xml_writer.py @@ -0,0 +1,65 @@ +""" +Write BoM out to an XML file +filename = path to output file (must be a .xml) +groups = [list of ComponentGroup groups] +net = netlist object +headings = [list of headings to display in the BoM file] +prefs = BomPref object +""" + +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from xml.etree import ElementTree +from xml.dom import minidom + + +def WriteXML(filename, groups, net, headings, prefs): + + if not filename.endswith(".xml"): + return False + + nGroups = len(groups) + nTotal = sum([g.getCount() for g in groups]) + nFitted = sum([g.getCount() for g in groups if g.isFitted()]) + nBuild = nFitted * prefs.boards + + attrib = {} + + attrib['Schematic_Source'] = net.getSource() + attrib['Schematic_Version'] = net.getVersion() + attrib['Schematic_Date'] = net.getSheetDate() + attrib['PCB_Variant'] = ', '.join(prefs.pcbConfig) + attrib['BOM_Date'] = net.getDate() + attrib['KiCad_Version'] = net.getTool() + attrib['Component_Groups'] = str(nGroups) + attrib['Component_Count'] = str(nTotal) + attrib['Fitted_Components'] = str(nFitted) + + attrib['Number_of_PCBs'] = str(prefs.boards) + attrib['Total_Components'] = str(nBuild) + + xml = ElementTree.Element('KiCad_BOM', attrib=attrib, encoding='utf-8') + + for group in groups: + if prefs.ignoreDNF and not group.isFitted(): + continue + + row = group.getRow(headings) + + attrib = {} + + for i, h in enumerate(headings): + h = h.replace(' ', '_') # Replace spaces, xml no likey + h = h.replace('"', '') + h = h.replace("'", '') + + attrib[h] = str(row[i]).decode('ascii', errors='ignore') + + # sub = ElementTree.SubElement(xml, "group", attrib=attrib) + + with open(filename, "w") as output: + out = ElementTree.tostring(xml, encoding="utf-8") + output.write(minidom.parseString(out).toprettyxml(indent="\t")) + + return True diff --git a/scripts/KiBoM/example/bom.png b/scripts/KiBoM/example/bom.png new file mode 100644 index 0000000000000000000000000000000000000000..775221c04f7215aedd83875f1055090f6ddd9298 GIT binary patch literal 14454 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 LMX6FrBme&eMx+04 literal 0 HcmV?d00001 diff --git a/scripts/KiBoM/example/html.png b/scripts/KiBoM/example/html.png new file mode 100644 index 0000000000000000000000000000000000000000..f0a8fb14c6dd3bccc34555dfc3c7451e919349bd GIT binary patch literal 80315 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

$4{?m2`h-Jb>-8YBV<=A&$?B;aF^Q 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 literal 0 HcmV?d00001 diff --git a/scripts/KiBoM/example/html_ex.png b/scripts/KiBoM/example/html_ex.png new file mode 100644 index 0000000000000000000000000000000000000000..035b8140b4b0f28d3d464deade7f2708fd693a78 GIT binary patch literal 7996 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 b-rmoaB>>gppMMEXya7#B-9OPP)=&Qn^wg5t literal 0 HcmV?d00001 diff --git a/scripts/KiBoM/example/ini.png b/scripts/KiBoM/example/ini.png new file mode 100644 index 0000000000000000000000000000000000000000..c6e671a59939647b3a7abd0a2beb92c62026ca9d GIT binary patch literal 42731 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 literal 0 HcmV?d00001 diff --git a/scripts/KiBoM/example/usage.png b/scripts/KiBoM/example/usage.png new file mode 100644 index 0000000000000000000000000000000000000000..960e9b1d196e83f38bd604e9ac5bc0701e9f80c2 GIT binary patch literal 12081 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 b3%&?9|E1(;_Cqk{E^y*ki(`4lE|LESjDVja literal 0 HcmV?d00001 diff --git a/scripts/KiBoM/setup.cfg b/scripts/KiBoM/setup.cfg new file mode 100644 index 0000000..61a1b69 --- /dev/null +++ b/scripts/KiBoM/setup.cfg @@ -0,0 +1,10 @@ +[flake8] +ignore = + # - W293 - blank lines contain whitespace + W293, + # - E501 - line too long (82 characters) + E501, E722, + # - C901 - function is too complex + C901, +exclude = .git,__pycache__,*/migrations/* +max-complexity = 20 diff --git a/scripts/KiBoM/tests/common.bash b/scripts/KiBoM/tests/common.bash new file mode 100644 index 0000000..109928e --- /dev/null +++ b/scripts/KiBoM/tests/common.bash @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Common functions for running various tests + +PASSED=0 +FAILED=0 + +# Results +result() { + echo "--- Results ---" + echo "${PASSED}/$((PASSED+FAILED))" + if (( FAILED == 0 )); then + return 0 + else + return 1 + fi +} + +# Runs a command, increments test passed/failed +# Args: Command +cmd() { + # Run command + echo "CMD: $@" + $@ + local RET=$? + + # Check command + if [[ ${RET} -ne 0 ]]; then + ((FAILED++)) + else + ((PASSED++)) + fi + + return ${RET} +} diff --git a/scripts/KiBoM/tests/sanity.bash b/scripts/KiBoM/tests/sanity.bash new file mode 100644 index 0000000..9edede2 --- /dev/null +++ b/scripts/KiBoM/tests/sanity.bash @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Basic run-time sanity check for KiBoM + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Common functions +source ${SCRIPT_DIR}/common.bash + +# Start in kll top-level directory +cd ${SCRIPT_DIR}/.. + + +## Tests + +cmd ./KiBOM_CLI.py --help + +## Tests complete + + +result +exit $?