├── .coveragerc ├── kibom ├── __init__.py ├── version.py ├── sort.py ├── debug.py ├── xml_writer.py ├── bom_writer.py ├── csv_writer.py ├── columns.py ├── units.py ├── xlsx_writer.py ├── html_writer.py ├── __main__.py ├── netlist_reader.py ├── preferences.py └── component.py ├── test ├── kibom-test.kicad_pcb ├── .gitignore ├── requirements.txt ├── kibom-test-cache.lib ├── kibom-test.pro ├── test_bom.py ├── bom.ini ├── kibom-test.sch-bak ├── kibom-test.sch ├── kibom-test.net ├── kibom-test.xml └── fp-info-cache ├── example ├── ini.png ├── html.png ├── schem.png ├── usage.png └── html_ex.png ├── setup.cfg ├── .gitignore ├── tests ├── sanity.bash └── common.bash ├── .travis.yml ├── .github ├── release.yml └── workflows │ ├── build.yaml │ ├── pep.yaml │ └── pypi.yaml ├── run-tests.sh ├── KiBOM_CLI.py ├── LICENSE.md ├── setup.py └── README.md /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = ./kibom -------------------------------------------------------------------------------- /kibom/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /kibom/version.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | KIBOM_VERSION = "1.9.1" 4 | -------------------------------------------------------------------------------- /test/kibom-test.kicad_pcb: -------------------------------------------------------------------------------- 1 | (kicad_pcb (version 4) (host kicad "dummy file") ) 2 | -------------------------------------------------------------------------------- /example/ini.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SchrodingersGat/KiBoM/HEAD/example/ini.png -------------------------------------------------------------------------------- /example/html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SchrodingersGat/KiBoM/HEAD/example/html.png -------------------------------------------------------------------------------- /example/schem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SchrodingersGat/KiBoM/HEAD/example/schem.png -------------------------------------------------------------------------------- /example/usage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SchrodingersGat/KiBoM/HEAD/example/usage.png -------------------------------------------------------------------------------- /example/html_ex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SchrodingersGat/KiBoM/HEAD/example/html_ex.png -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore generated BOM files 2 | 3 | bom.ini 4 | 5 | *.csv 6 | *.html 7 | *.tmp 8 | *.xls 9 | *.xlsx 10 | *.xml -------------------------------------------------------------------------------- /test/requirements.txt: -------------------------------------------------------------------------------- 1 | # Python packages required for unit testing 2 | 3 | flake8>=6.0.0 # PEP checking 4 | coverage # Unit test coverage 5 | coveralls>=3.3.0 # Coveralls.io linking 6 | xlsxwriter>=3.0.0 # Excel file writing 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = 3 | # - W293 - blank lines contain whitespace 4 | W293, 5 | # - E501 - line too long (82 characters) 6 | E501, E722, 7 | # - C901 - function is too complex 8 | C901, 9 | exclude = .git,__pycache__,*/migrations/*,./build, ./test, 10 | max-complexity = 20 11 | -------------------------------------------------------------------------------- /kibom/sort.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import re 4 | 5 | 6 | def natural_sort(string): 7 | """ 8 | Natural sorting function which sorts by numerical value of a string, 9 | rather than raw ASCII value. 10 | """ 11 | return [int(s) if s.isdigit() else s for s in re.split(r'(\d+)', string)] 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | *$py.class 4 | *.pyc 5 | 6 | *.patch 7 | .idea/ 8 | 9 | .env 10 | .venv 11 | env/ 12 | venv/ 13 | VENV/ 14 | ENV/ 15 | env.bak/ 16 | venv.bak/ 17 | 18 | # Coverage reports 19 | .coverage 20 | htmlcov/ 21 | 22 | # PIP build 23 | build/ 24 | dist/ 25 | kibom.egg-info/ 26 | 27 | .python-version 28 | -------------------------------------------------------------------------------- /tests/sanity.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Basic run-time sanity check for KiBoM 3 | 4 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 5 | 6 | # Common functions 7 | source ${SCRIPT_DIR}/common.bash 8 | 9 | # Start in kll top-level directory 10 | cd ${SCRIPT_DIR}/.. 11 | 12 | 13 | ## Tests 14 | 15 | cmd ./KiBOM_CLI.py --help 16 | 17 | ## Tests complete 18 | 19 | 20 | result 21 | exit $? 22 | -------------------------------------------------------------------------------- /tests/common.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Common functions for running various tests 3 | 4 | PASSED=0 5 | FAILED=0 6 | 7 | # Results 8 | result() { 9 | echo "--- Results ---" 10 | echo "${PASSED}/$((PASSED+FAILED))" 11 | if (( FAILED == 0 )); then 12 | return 0 13 | else 14 | return 1 15 | fi 16 | } 17 | 18 | # Runs a command, increments test passed/failed 19 | # Args: Command 20 | cmd() { 21 | # Run command 22 | echo "CMD: $@" 23 | $@ 24 | local RET=$? 25 | 26 | # Check command 27 | if [[ ${RET} -ne 0 ]]; then 28 | ((FAILED++)) 29 | else 30 | ((PASSED++)) 31 | fi 32 | 33 | return ${RET} 34 | } 35 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # travis-ci integration for KiBOM 2 | 3 | dist: xenial 4 | 5 | language: python 6 | 7 | python: 8 | - 2.7 9 | - 3.7 10 | 11 | install: 12 | - pip install -r test/requirements.txt 13 | - pip install wheel 14 | 15 | addons: 16 | apt: 17 | update: true 18 | 19 | before_install: 20 | - pip install coverage 21 | - pip install xlsxwriter 22 | 23 | script: 24 | # Check Python code for style-guide 25 | - flake8 . 26 | # Run the coverage tests 27 | - bash ./run-tests.sh 28 | # Ensure the module can actually build 29 | - python setup.py bdist_wheel --universal 30 | 31 | after_success: 32 | - coveralls -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | # .github/release.yml 2 | 3 | changelog: 4 | exclude: 5 | labels: 6 | - translation 7 | - documentation 8 | categories: 9 | - title: Breaking Changes 10 | labels: 11 | - Semver-Major 12 | - breaking 13 | - title: Security Patches 14 | labels: 15 | - security 16 | - title: New Features 17 | labels: 18 | - Semver-Minor 19 | - enhancement 20 | - title: Bug Fixes 21 | labels: 22 | - Semver-Patch 23 | - bug 24 | - title: Devops / Setup Changes 25 | labels: 26 | - docker 27 | - setup 28 | - demo 29 | - CI 30 | - title: Other Changes 31 | labels: 32 | - "*" 33 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build Package 2 | 3 | on: ["push", "pull_request"] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | max-parallel: 4 11 | matrix: 12 | python-version: [3.8] 13 | 14 | steps: 15 | - name: Checkout Code 16 | uses: actions/checkout@v2 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | - name: Install Deps 22 | run: | 23 | pip install -U -r test/requirements.txt 24 | - name: Build Python Package 25 | run: | 26 | pip install --upgrade pip wheel setuptools 27 | python setup.py bdist_wheel --universal 28 | -------------------------------------------------------------------------------- /test/kibom-test-cache.lib: -------------------------------------------------------------------------------- 1 | EESchema-LIBRARY Version 2.4 2 | #encoding utf-8 3 | # 4 | # Device_C 5 | # 6 | DEF Device_C C 0 10 N Y 1 F N 7 | F0 "C" 25 100 50 H V L CNN 8 | F1 "Device_C" 25 -100 50 H V L CNN 9 | F2 "" 38 -150 50 H I C CNN 10 | F3 "" 0 0 50 H I C CNN 11 | $FPLIST 12 | C_* 13 | $ENDFPLIST 14 | DRAW 15 | P 2 0 1 20 -80 -30 80 -30 N 16 | P 2 0 1 20 -80 30 80 30 N 17 | X ~ 1 0 150 110 D 50 50 1 1 P 18 | X ~ 2 0 -150 110 U 50 50 1 1 P 19 | ENDDRAW 20 | ENDDEF 21 | # 22 | # Device_R 23 | # 24 | DEF Device_R R 0 0 N Y 1 F N 25 | F0 "R" 80 0 50 V V C CNN 26 | F1 "Device_R" 0 0 50 V V C CNN 27 | F2 "" -70 0 50 V I C CNN 28 | F3 "" 0 0 50 H I C CNN 29 | $FPLIST 30 | R_* 31 | $ENDFPLIST 32 | DRAW 33 | S -40 -100 40 100 0 1 10 N 34 | X ~ 1 0 150 50 D 50 50 1 1 P 35 | X ~ 2 0 -150 50 U 50 50 1 1 P 36 | ENDDRAW 37 | ENDDEF 38 | # 39 | #End Library 40 | -------------------------------------------------------------------------------- /.github/workflows/pep.yaml: -------------------------------------------------------------------------------- 1 | name: PEP Style Checks 2 | 3 | on: ["push", "pull_request"] 4 | 5 | jobs: 6 | style: 7 | runs-on: ubuntu-latest 8 | 9 | env: 10 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 11 | 12 | strategy: 13 | max-parallel: 4 14 | matrix: 15 | python-version: [3.8] 16 | 17 | steps: 18 | - name: Checkout Code 19 | uses: actions/checkout@v2 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install Deps 25 | run: | 26 | pip install -U -r test/requirements.txt 27 | - name: Style Checks 28 | run: | 29 | flake8 . 30 | bash ./run-tests.sh 31 | - name: Upload Report 32 | run: | 33 | coveralls --service=github 34 | -------------------------------------------------------------------------------- /run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # delete the 'default' BOM file so it gets created 4 | rm test/bom.ini 5 | 6 | coverage erase 7 | 8 | # Run a simple test 9 | coverage run -a -m kibom test/kibom-test.xml test/bom-out.csv 10 | 11 | # Generate a html file 12 | coverage run -a -m kibom test/kibom-test.xml test/bom-out.html 13 | 14 | # Generate an XML file 15 | coverage run -a -m kibom test/kibom-test.xml test/bom-out.xml 16 | 17 | # Generate an XLSX file 18 | coverage run -a -m kibom test/kibom-test.xml test/bom-out.xlsx 19 | # Generate a BOM file in a subdirectory 20 | coverage run -a -m kibom test/kibom-test.xml bom-dir.csv -d bomsubdir -vvv 21 | coverage run -a -m kibom test/kibom-test.xml bom-dir2.html -d bomsubdir/secondsubdir -vvv 22 | 23 | 24 | # Run the sanity checker on the output BOM files 25 | coverage run -a test/test_bom.py 26 | 27 | # Generate HTML code coverage output 28 | coverage html 29 | 30 | -------------------------------------------------------------------------------- /KiBOM_CLI.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | @package 4 | KiBOM - Bill of Materials generation for KiCad 5 | 6 | Generate BOM in xml, csv, txt, tsv, html or xlsx formats. 7 | 8 | - Components are automatically grouped into BoM rows (grouping is configurable) 9 | - Component groups count number of components and list component designators 10 | - Rows are automatically sorted by component reference(s) 11 | - Supports board variants 12 | 13 | Extended options are available in the "bom.ini" config file in the PCB directory 14 | (this file is auto-generated with default options the first time the script is executed). 15 | 16 | For usage help: 17 | python KiBOM_CLI.py -h 18 | """ 19 | 20 | import sys 21 | import os 22 | 23 | here = os.path.abspath(os.path.dirname(__file__)) 24 | sys.path.insert(0, here) 25 | 26 | from kibom.__main__ import main # noqa: E402 27 | 28 | main() 29 | -------------------------------------------------------------------------------- /.github/workflows/pypi.yaml: -------------------------------------------------------------------------------- 1 | # Publish to PyPi package index 2 | 3 | name: PIP Publish 4 | 5 | on: 6 | release: 7 | types: [published] 8 | 9 | jobs: 10 | 11 | publish: 12 | name: Publish to PyPi 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout Code 17 | uses: actions/checkout@v2 18 | - name: Setup Python 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: 3.8 22 | - name: Install Dependencies 23 | run: | 24 | pip install -U -r test/requirements.txt 25 | pip install -U wheel setuptools twine 26 | - name: Build Release 27 | run: | 28 | python setup.py sdist bdist_wheel --universal 29 | - name: Publish 30 | run: | 31 | python -m twine upload dist/* 32 | env: 33 | TWINE_USERNAME: __token__ 34 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} 35 | TWINE_REPOSITORY: pypi 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 KiBOM 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. -------------------------------------------------------------------------------- /test/kibom-test.pro: -------------------------------------------------------------------------------- 1 | update=12/03/2020 11:21:57 PM 2 | version=1 3 | last_client=kicad 4 | [general] 5 | version=1 6 | RootSch= 7 | BoardNm= 8 | [pcbnew] 9 | version=1 10 | LastNetListRead= 11 | UseCmpFile=1 12 | PadDrill=0.600000000000 13 | PadDrillOvalY=0.600000000000 14 | PadSizeH=1.500000000000 15 | PadSizeV=1.500000000000 16 | PcbTextSizeV=1.500000000000 17 | PcbTextSizeH=1.500000000000 18 | PcbTextThickness=0.300000000000 19 | ModuleTextSizeV=1.000000000000 20 | ModuleTextSizeH=1.000000000000 21 | ModuleTextSizeThickness=0.150000000000 22 | SolderMaskClearance=0.000000000000 23 | SolderMaskMinWidth=0.000000000000 24 | DrawSegmentWidth=0.200000000000 25 | BoardOutlineThickness=0.100000000000 26 | ModuleOutlineThickness=0.150000000000 27 | [cvpcb] 28 | version=1 29 | NetIExt=net 30 | [eeschema] 31 | version=1 32 | LibDir= 33 | [eeschema/libraries] 34 | [schematic_editor] 35 | version=1 36 | PageLayoutDescrFile= 37 | PlotDirectoryName= 38 | SubpartIdSeparator=0 39 | SubpartFirstId=65 40 | NetFmtName=Pcbnew 41 | SpiceAjustPassiveValues=0 42 | LabSize=50 43 | ERC_TestSimilarLabels=1 44 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import setuptools 4 | 5 | from kibom.version import KIBOM_VERSION 6 | 7 | long_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. For futher information see the KiBom project page" 8 | 9 | 10 | setuptools.setup( 11 | name="kibom", 12 | version=KIBOM_VERSION, 13 | author="Oliver Walters", 14 | author_email="oliver.henry.walters@gmail.com", 15 | description="Bill of Materials generation tool for KiCad EDA", 16 | long_description=long_description, 17 | keywords="kicad, bom, electronics, schematic, bill of materials", 18 | url="https://github.com/SchrodingersGat/KiBom", 19 | license="MIT", 20 | packages=setuptools.find_packages(), 21 | scripts=['KiBOM_CLI.py'], 22 | entry_points={ 23 | 'console_scripts': ['kibom = kibom.__main__:main'] 24 | }, 25 | install_requires=[ 26 | "xlsxwriter", 27 | ], 28 | python_requires=">=2.7" 29 | ) 30 | -------------------------------------------------------------------------------- /test/test_bom.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import csv 4 | import os 5 | 6 | 7 | def check_files_exist(): 8 | """ 9 | Test that all expected generated BOM files are present 10 | """ 11 | 12 | print("Checking output files...") 13 | 14 | assert(os.path.exists('test/bom-out_bom_A.csv')) 15 | assert(os.path.exists('test/bom-out_bom_A.xlsx')) 16 | assert(os.path.exists('test/bom-out_bom_A.xml')) 17 | assert(os.path.exists('test/bom-out_bom_A.html')) 18 | 19 | assert(os.path.exists('test/bomsubdir/bom-dir_bom_A.csv')) 20 | assert(os.path.exists('test/bomsubdir/secondsubdir/bom-dir2_bom_A.html')) 21 | 22 | 23 | def check_csv_data(): 24 | """ 25 | Test the generated CSV data 26 | """ 27 | 28 | print("Checking generated BOM...") 29 | 30 | BOM_FILE = "bom-out_bom_A.csv" 31 | 32 | BOM_FILE = os.path.join(os.path.dirname(__file__), BOM_FILE) 33 | 34 | lines = [] 35 | 36 | with open(BOM_FILE, 'r') as bom_file: 37 | reader = csv.reader(bom_file, delimiter=',') 38 | 39 | lines = [line for line in reader] 40 | 41 | # Check that the header row contains the expected information 42 | assert 'Component' in lines[0] 43 | 44 | component_rows = [] 45 | 46 | idx = 1 47 | 48 | while idx < len(lines): 49 | row = lines[idx] 50 | 51 | # Break on the first 'empty' row 52 | if len(row) == 0: 53 | break 54 | 55 | component_rows.append(row) 56 | 57 | idx += 1 58 | 59 | # We know how many component rows there should be 60 | assert len(component_rows) == 5 61 | 62 | # Create a list of components 63 | component_refs = [] 64 | 65 | for row in component_rows: 66 | refs = row[3].split(" ") 67 | 68 | for ref in refs: 69 | # Ensure no component is duplicated in the BOM! 70 | if ref in component_refs: 71 | raise AssertionError("Component {ref} is duplicated".format(ref=ref)) 72 | 73 | # R6 should be excluded from the BOM (marked as DNF) 74 | assert 'R6' not in component_refs 75 | 76 | 77 | if __name__ == '__main__': 78 | 79 | print("Running BOM tests") 80 | 81 | check_files_exist() 82 | check_csv_data() 83 | 84 | print("All tests passed... OK...") 85 | -------------------------------------------------------------------------------- /kibom/debug.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import print_function 4 | 5 | import sys 6 | 7 | # Various msg levels 8 | MSG_MESSAGE = -1 # Display generic message (always displayed) 9 | MSG_ERROR = 0 # Display error messages 10 | MSG_WARN = 1 # Display warning messages 11 | MSG_INFO = 2 # Display information messages 12 | MSG_DEBUG = 3 # Display debug messages 13 | 14 | MSG_CODES = { 15 | MSG_ERROR: "ERROR", 16 | MSG_WARN: "WARNING", 17 | MSG_INFO: "INFO", 18 | MSG_DEBUG: "DEBUG", 19 | } 20 | 21 | # By default, only display error messages 22 | MSG_LEVEL = MSG_ERROR 23 | 24 | # Keep track of accumulated errorsh 25 | ERR_COUNT = 0 26 | 27 | 28 | def setDebugLevel(level): 29 | global MSG_LEVEL 30 | MSG_LEVEL = int(level) 31 | 32 | 33 | def getErrorCount(): 34 | global ERR_COUNT 35 | return ERR_COUNT 36 | 37 | 38 | def _msg(prefix, *arg): 39 | """ 40 | Display a message with the given color. 41 | """ 42 | 43 | msg = "" 44 | 45 | if prefix: 46 | msg += prefix 47 | 48 | print(msg, *arg) 49 | 50 | 51 | def message(*arg): 52 | """ 53 | Display a message 54 | """ 55 | 56 | _msg("", *arg) 57 | 58 | 59 | def debug(*arg): 60 | """ 61 | Display a debug message. 62 | """ 63 | 64 | global MSG_LEVEL 65 | if MSG_LEVEL < MSG_DEBUG: 66 | return 67 | 68 | _msg(MSG_CODES[MSG_DEBUG], *arg) 69 | 70 | 71 | def info(*arg): 72 | """ 73 | Display an info message. 74 | """ 75 | 76 | global MSG_LEVEL 77 | if MSG_LEVEL < MSG_INFO: 78 | return 79 | 80 | _msg(MSG_CODES[MSG_INFO], *arg) 81 | 82 | 83 | def warning(*arg): 84 | """ 85 | Display a warning message 86 | """ 87 | 88 | global MSG_LEVEL 89 | if MSG_LEVEL < MSG_WARN: 90 | return 91 | 92 | _msg(MSG_CODES[MSG_WARN], *arg) 93 | 94 | 95 | def error(*arg, **kwargs): 96 | """ 97 | Display an error message 98 | """ 99 | 100 | global MSG_LEVEL 101 | global ERR_COUNT 102 | 103 | if MSG_LEVEL < MSG_ERROR: 104 | return 105 | 106 | _msg(MSG_CODES[MSG_ERROR], *arg) 107 | 108 | ERR_COUNT += 1 109 | 110 | fail = kwargs.get('fail', False) 111 | 112 | if fail: 113 | sys.exit(ERR_COUNT) 114 | -------------------------------------------------------------------------------- /kibom/xml_writer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Write BoM out to an XML file 3 | filename = path to output file (must be a .xml) 4 | groups = [list of ComponentGroup groups] 5 | net = netlist object 6 | headings = [list of headings to display in the BoM file] 7 | prefs = BomPref object 8 | """ 9 | 10 | # -*- coding: utf-8 -*- 11 | from __future__ import unicode_literals 12 | 13 | from xml.etree import ElementTree 14 | from xml.dom import minidom 15 | 16 | 17 | def WriteXML(filename, groups, net, headings, head_names, prefs): 18 | 19 | if not filename.endswith(".xml"): 20 | return False 21 | 22 | nGroups = len(groups) 23 | nTotal = sum([g.getCount() for g in groups]) 24 | nFitted = sum([g.getCount() for g in groups if g.isFitted()]) 25 | nBuild = nFitted * prefs.boards 26 | 27 | attrib = {} 28 | 29 | attrib['Schematic_Source'] = net.getSource() 30 | attrib['Schematic_Version'] = net.getVersion() 31 | attrib['Schematic_Date'] = net.getSheetDate() 32 | attrib['PCB_Variant'] = ', '.join(prefs.pcbConfig) 33 | attrib['BOM_Date'] = net.getDate() 34 | attrib['KiCad_Version'] = net.getTool() 35 | attrib['Component_Groups'] = str(nGroups) 36 | attrib['Component_Count'] = str(nTotal) 37 | attrib['Fitted_Components'] = str(nFitted) 38 | 39 | attrib['Number_of_PCBs'] = str(prefs.boards) 40 | attrib['Total_Components'] = str(nBuild) 41 | 42 | xml = ElementTree.Element('KiCad_BOM', attrib=attrib, encoding='utf-8') 43 | 44 | for group in groups: 45 | if prefs.ignoreDNF and not group.isFitted(): 46 | continue 47 | 48 | row = group.getRow(headings) 49 | 50 | attrib = {} 51 | 52 | for i, h in enumerate(head_names): 53 | h = h.replace(' ', '_') # Replace spaces, xml no likey 54 | h = h.replace('"', '') 55 | h = h.replace("'", '') 56 | 57 | attrib[h] = str(row[i]) 58 | 59 | ElementTree.SubElement(xml, "group", attrib=attrib) 60 | 61 | with open(filename, "w", encoding="utf-8") as output: 62 | out = ElementTree.tostring(xml, encoding="utf-8") 63 | # There is probably a better way to write the data to file (without so many encoding/decoding steps), 64 | # but toprettyxml() without specifying UTF-8 will chew up non-ASCII chars. Perhaps revisit if performance here 65 | # is ever a concern 66 | output.write(minidom.parseString(out).toprettyxml(indent="\t", encoding="utf-8").decode("utf-8")) 67 | 68 | return True 69 | -------------------------------------------------------------------------------- /kibom/bom_writer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .csv_writer import WriteCSV 4 | from .xml_writer import WriteXML 5 | from .html_writer import WriteHTML 6 | from .xlsx_writer import WriteXLSX 7 | 8 | from . import columns 9 | from . import debug 10 | from .preferences import BomPref 11 | 12 | import os 13 | import shutil 14 | 15 | 16 | def TmpFileCopy(filename, fmt): 17 | # Make a tmp copy of a given file 18 | 19 | filename = os.path.abspath(filename) 20 | 21 | if os.path.exists(filename) and os.path.isfile(filename): 22 | shutil.copyfile(filename, fmt.replace("%O", filename)) 23 | 24 | 25 | def WriteBoM(filename, groups, net, headings=columns.ColumnList._COLUMNS_DEFAULT, prefs=None): 26 | """ 27 | Write BoM to file 28 | filename = output file path 29 | groups = [list of ComponentGroup groups] 30 | headings = [list of headings to display in the BoM file] 31 | prefs = BomPref object 32 | """ 33 | 34 | filename = os.path.abspath(filename) 35 | 36 | # No preferences supplied, use defaults 37 | if not prefs: 38 | prefs = BomPref() 39 | 40 | # Remove any headings that appear in the ignore[] list 41 | headings = [h for h in headings if not h.lower() in prefs.ignore] 42 | # Allow renaming the columns 43 | head_names = [h if h.lower() not in prefs.colRename else prefs.colRename[h.lower()] for h in headings] 44 | 45 | # If no extension is given, assume .csv (and append!) 46 | if len(filename.split('.')) < 2: 47 | filename += ".csv" 48 | 49 | # Make a temporary copy of the output file 50 | if prefs.backup is not False: 51 | TmpFileCopy(filename, prefs.backup) 52 | 53 | ext = filename.split('.')[-1].lower() 54 | 55 | result = False 56 | 57 | # CSV file writing 58 | if ext in ["csv", "tsv", "txt"]: 59 | if WriteCSV(filename, groups, net, headings, head_names, prefs): 60 | debug.info("CSV Output -> {fn}".format(fn=filename)) 61 | result = True 62 | else: 63 | debug.error("Error writing CSV output") 64 | 65 | elif ext in ["htm", "html"]: 66 | if WriteHTML(filename, groups, net, headings, head_names, prefs): 67 | debug.info("HTML Output -> {fn}".format(fn=filename)) 68 | result = True 69 | else: 70 | debug.error("Error writing HTML output") 71 | 72 | elif ext in ["xml"]: 73 | if WriteXML(filename, groups, net, headings, head_names, prefs): 74 | debug.info("XML Output -> {fn}".format(fn=filename)) 75 | result = True 76 | else: 77 | debug.error("Error writing XML output") 78 | 79 | elif ext in ["xlsx"]: 80 | if WriteXLSX(filename, groups, net, headings, head_names, prefs): 81 | debug.info("XLSX Output -> {fn}".format(fn=filename)) 82 | result = True 83 | else: 84 | debug.error("Error writing XLSX output") 85 | 86 | else: 87 | debug.error("Unsupported file extension: {ext}".format(ext=ext)) 88 | 89 | return result 90 | -------------------------------------------------------------------------------- /kibom/csv_writer.py: -------------------------------------------------------------------------------- 1 | # _*_ coding:latin-1 _*_ 2 | 3 | import csv 4 | import os 5 | import sys 6 | 7 | 8 | def WriteCSV(filename, groups, net, headings, head_names, prefs): 9 | """ 10 | Write BoM out to a CSV file 11 | filename = path to output file (must be a .csv, .txt or .tsv file) 12 | groups = [list of ComponentGroup groups] 13 | net = netlist object 14 | headings = [list of headings to search for data in the BoM file] 15 | head_names = [list of headings to display in the BoM file] 16 | prefs = BomPref object 17 | """ 18 | 19 | filename = os.path.abspath(filename) 20 | 21 | # Delimeter is assumed from file extension 22 | # Override delimiter if separator specified 23 | if prefs.separatorCSV is not None: 24 | delimiter = prefs.separatorCSV 25 | else: 26 | if filename.endswith(".csv"): 27 | delimiter = "," 28 | elif filename.endswith(".tsv") or filename.endswith(".txt"): 29 | delimiter = "\t" 30 | else: 31 | return False 32 | 33 | nGroups = len(groups) 34 | nTotal = sum([g.getCount() for g in groups]) 35 | nFitted = sum([g.getCount() for g in groups if g.isFitted()]) 36 | nBuild = nFitted * prefs.boards 37 | 38 | if (sys.version_info[0] >= 3): 39 | f = open(filename, "w", encoding='utf-8') 40 | else: 41 | f = open(filename, "w") 42 | 43 | writer = csv.writer(f, delimiter=delimiter, lineterminator="\n") 44 | 45 | if not prefs.hideHeaders: 46 | if prefs.numberRows: 47 | comp = "Component" 48 | if comp.lower() in prefs.colRename: 49 | comp = prefs.colRename[comp.lower()] 50 | writer.writerow([comp] + head_names) 51 | else: 52 | writer.writerow(head_names) 53 | 54 | count = 0 55 | rowCount = 1 56 | 57 | for group in groups: 58 | if prefs.ignoreDNF and not group.isFitted(): 59 | continue 60 | 61 | row = group.getRow(headings) 62 | 63 | if prefs.numberRows: 64 | row = [str(rowCount)] + row 65 | 66 | # Deal with unicode characters 67 | # Row = [el.decode('latin-1') for el in row] 68 | writer.writerow(row) 69 | 70 | try: 71 | count += group.getCount() 72 | except: 73 | pass 74 | 75 | rowCount += 1 76 | 77 | if not prefs.hidePcbInfo: 78 | # Add some blank rows 79 | for i in range(5): 80 | writer.writerow([]) 81 | 82 | writer.writerow(["Component Groups:", nGroups]) 83 | writer.writerow(["Component Count:", nTotal]) 84 | writer.writerow(["Fitted Components:", nFitted]) 85 | writer.writerow(["Number of PCBs:", prefs.boards]) 86 | writer.writerow(["Total components:", nBuild]) 87 | writer.writerow(["Schematic Version:", net.getVersion()]) 88 | writer.writerow(["Schematic Date:", net.getSheetDate()]) 89 | writer.writerow(["PCB Variant:", ' + '.join(prefs.pcbConfig)]) 90 | writer.writerow(["BoM Date:", net.getDate()]) 91 | writer.writerow(["Schematic Source:", net.getSource()]) 92 | writer.writerow(["KiCad Version:", net.getTool()]) 93 | 94 | f.close() 95 | 96 | return True 97 | -------------------------------------------------------------------------------- /kibom/columns.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class ColumnList: 5 | 6 | # Default columns (immutable) 7 | COL_REFERENCE = 'References' 8 | COL_DESCRIPTION = 'Description' 9 | COL_VALUE = 'Value' 10 | COL_FP = 'Footprint' 11 | COL_FP_LIB = 'Footprint Lib' 12 | COL_PART = 'Part' 13 | COL_PART_LIB = 'Part Lib' 14 | COL_SHEETPATH = 'Sheetpath' 15 | COL_DATASHEET = 'Datasheet' 16 | 17 | # Default columns for groups 18 | COL_GRP_QUANTITY = 'Quantity Per PCB' 19 | COL_GRP_TOTAL_COST = 'Total Cost' 20 | COL_GRP_BUILD_QUANTITY = 'Build Quantity' 21 | 22 | # Generated columns 23 | _COLUMNS_GEN = [ 24 | COL_GRP_QUANTITY, 25 | COL_GRP_BUILD_QUANTITY, 26 | ] 27 | 28 | # Default columns 29 | _COLUMNS_DEFAULT = [ 30 | COL_DESCRIPTION, 31 | COL_PART, 32 | COL_PART_LIB, 33 | COL_REFERENCE, 34 | COL_VALUE, 35 | COL_FP, 36 | COL_FP_LIB, 37 | COL_SHEETPATH, 38 | COL_GRP_QUANTITY, 39 | COL_GRP_BUILD_QUANTITY, 40 | COL_DATASHEET 41 | ] 42 | 43 | # Default columns 44 | # These columns are 'immutable' 45 | _COLUMNS_PROTECTED = [ 46 | COL_REFERENCE, 47 | COL_GRP_QUANTITY, 48 | COL_VALUE, 49 | COL_PART, 50 | COL_PART_LIB, 51 | COL_DESCRIPTION, 52 | COL_DATASHEET, 53 | COL_FP, 54 | COL_FP_LIB, 55 | COL_SHEETPATH 56 | ] 57 | 58 | def __str__(self): 59 | return " ".join(map(str, self.columns)) 60 | 61 | def __repr__(self): 62 | return self.__str__() 63 | 64 | def __init__(self, cols=_COLUMNS_DEFAULT): 65 | 66 | self.columns = [] 67 | 68 | # Make a copy of the supplied columns 69 | for col in cols: 70 | self.AddColumn(col) 71 | 72 | def _hasColumn(self, col): 73 | # Col can either be or 74 | return str(col) in [str(c) for c in self.columns] 75 | 76 | """ 77 | Remove a column from the list. Specify either the heading or the index 78 | """ 79 | def RemoveColumn(self, col): 80 | if type(col) is str: 81 | self.RemoveColumnByName(col) 82 | elif type(col) is int and col >= 0 and col < len(self.columns): 83 | self.RemoveColumnByName(self.columns[col]) 84 | 85 | def RemoveColumnByName(self, name): 86 | 87 | # First check if this is in an immutable colum 88 | if name in self._COLUMNS_PROTECTED: 89 | return 90 | 91 | # Column does not exist, return 92 | if name not in self.columns: 93 | return 94 | 95 | try: 96 | index = self.columns.index(name) 97 | del self.columns[index] 98 | except ValueError: 99 | return 100 | 101 | # Add a new column (if it doesn't already exist!) 102 | def AddColumn(self, col, index=None): 103 | 104 | # Already exists? 105 | if self._hasColumn(col): 106 | return 107 | 108 | if type(index) is not int or index < 0 or index >= len(self.columns): 109 | self.columns.append(col) 110 | 111 | # Otherwise, splice the new column in 112 | else: 113 | self.columns = self.columns[0:index] + [col] + self.columns[index:] 114 | -------------------------------------------------------------------------------- /test/bom.ini: -------------------------------------------------------------------------------- 1 | [BOM_OPTIONS] 2 | ; General BoM options here 3 | ; 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 4 | ignore_dnf = 1 5 | ; If 'html_generate_dnf' option is set to 1, also generate a list of components not fitted on the PCB (HTML only) 6 | html_generate_dnf = 1 7 | ; If 'use_alt' option is set to 1, grouped references will be printed in the alternate compressed style eg: R1-R7,R18 8 | use_alt = 0 9 | ; If 'alt_wrap' option is set to and integer N, the references field will wrap after N entries are printed 10 | alt_wrap = 0 11 | ; If 'number_rows' option is set to 1, each row in the BoM will be prepended with an incrementing row number 12 | number_rows = 1 13 | ; If 'group_connectors' option is set to 1, connectors with the same footprints will be grouped together, independent of the name of the connector 14 | group_connectors = 1 15 | ; 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 16 | test_regex = 1 17 | ; If 'merge_blank_fields' option is set to 1, component groups with blank fields will be merged into the most compatible group, where possible 18 | merge_blank_fields = 1 19 | ; 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'. 20 | output_file_name = %O_bom_%v%V 21 | ; 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. 22 | variant_file_name_format = _(%V) 23 | ; Field name used to determine if a particular part is to be fitted 24 | fit_field = Config 25 | ; Make a backup of the bom before generating the new one, using the following template 26 | make_backup = %O.tmp 27 | ; Default number of boards to produce if none given on CLI with -n 28 | number_boards = 1 29 | ; Default PCB variant if none given on CLI with -r 30 | board_variant = ['default'] 31 | ; Whether to hide headers from output file 32 | hide_headers = False 33 | ; Whether to hide PCB info from output file 34 | hide_pcb_info = False 35 | 36 | [IGNORE_COLUMNS] 37 | ; Any column heading that appears here will be excluded from the Generated BoM 38 | ; Titles are case-insensitive 39 | Part Lib 40 | Footprint Lib 41 | 42 | [COLUMN_ORDER] 43 | ; Columns will apear in the order they are listed here 44 | ; Titles are case-insensitive 45 | Description 46 | Part 47 | Part Lib 48 | References 49 | Value 50 | Footprint 51 | Footprint Lib 52 | Quantity Per PCB 53 | Build Quantity 54 | Datasheet 55 | 56 | [GROUP_FIELDS] 57 | ; List of fields used for sorting individual components into groups 58 | ; Components which match (comparing *all* fields) will be grouped together 59 | ; Field names are case-insensitive 60 | Part 61 | Part Lib 62 | Value 63 | Footprint 64 | Footprint Lib 65 | 66 | [COMPONENT_ALIASES] 67 | ; A series of values which are considered to be equivalent for the part name 68 | ; Each line represents a list of equivalent component name values separated by white space 69 | ; e.g. 'c c_small cap' will ensure the equivalent capacitor symbols can be grouped together 70 | ; Aliases are case-insensitive 71 | c c_small cap capacitor 72 | r r_small res resistor 73 | sw switch 74 | l l_small inductor 75 | zener zenersmall 76 | d diode d_small 77 | 78 | [REGEX_INCLUDE] 79 | ; A series of regular expressions used to include parts in the BoM 80 | ; If there are any regex defined here, only components that match against ANY of them will be included in the BOM 81 | ; Column names are case-insensitive 82 | ; Format is: "[ColumName] [Regex]" (white-space separated) 83 | 84 | [REGEX_EXCLUDE] 85 | ; A series of regular expressions used to exclude parts from the BoM 86 | ; If a component matches ANY of these, it will be excluded from the BoM 87 | ; Column names are case-insensitive 88 | ; Format is: "[ColumName] [Regex]" (white-space separated) 89 | References ^TP[0-9]* 90 | References ^FID 91 | Part mount.*hole 92 | Part solder.*bridge 93 | Part test.*point 94 | Footprint test.*point 95 | Footprint mount.*hole 96 | Footprint fiducial 97 | 98 | -------------------------------------------------------------------------------- /test/kibom-test.sch-bak: -------------------------------------------------------------------------------- 1 | EESchema Schematic File Version 4 2 | EELAYER 30 0 3 | EELAYER END 4 | $Descr A4 11693 8268 5 | encoding utf-8 6 | Sheet 1 1 7 | Title "KiBom Test Schematic" 8 | Date "2020-03-12" 9 | Rev "A" 10 | Comp "https://github.com/SchrodingersGat/KiBom" 11 | Comment1 "" 12 | Comment2 "" 13 | Comment3 "" 14 | Comment4 "" 15 | $EndDescr 16 | $Comp 17 | L Device:R R1 18 | U 1 1 5E6A2873 19 | P 2200 2550 20 | F 0 "R1" V 2280 2550 50 0000 C CNN 21 | F 1 "10K" V 2200 2550 50 0000 C CNN 22 | F 2 "Resistor_SMD:R_0805_2012Metric" V 2130 2550 50 0001 C CNN 23 | F 3 "~" H 2200 2550 50 0001 C CNN 24 | 1 2200 2550 25 | 1 0 0 -1 26 | $EndComp 27 | $Comp 28 | L Device:R R2 29 | U 1 1 5E6A330D 30 | P 2500 2550 31 | F 0 "R2" V 2580 2550 50 0000 C CNN 32 | F 1 "10K" V 2500 2550 50 0000 C CNN 33 | F 2 "Resistor_SMD:R_0805_2012Metric" V 2430 2550 50 0001 C CNN 34 | F 3 "~" H 2500 2550 50 0001 C CNN 35 | 1 2500 2550 36 | 1 0 0 -1 37 | $EndComp 38 | $Comp 39 | L Device:R R3 40 | U 1 1 5E6A35E1 41 | P 2750 2550 42 | F 0 "R3" V 2830 2550 50 0000 C CNN 43 | F 1 "10K" V 2750 2550 50 0000 C CNN 44 | F 2 "Resistor_SMD:R_0805_2012Metric" V 2680 2550 50 0001 C CNN 45 | F 3 "~" H 2750 2550 50 0001 C CNN 46 | 1 2750 2550 47 | 1 0 0 -1 48 | $EndComp 49 | $Comp 50 | L Device:R R4 51 | U 1 1 5E6A37B2 52 | P 3000 2550 53 | F 0 "R4" V 3080 2550 50 0000 C CNN 54 | F 1 "10K" V 3000 2550 50 0000 C CNN 55 | F 2 "Resistor_SMD:R_0805_2012Metric" V 2930 2550 50 0001 C CNN 56 | F 3 "~" H 3000 2550 50 0001 C CNN 57 | 1 3000 2550 58 | 1 0 0 -1 59 | $EndComp 60 | $Comp 61 | L Device:R R5 62 | U 1 1 5E6A39EB 63 | P 3250 2550 64 | F 0 "R5" V 3330 2550 50 0000 C CNN 65 | F 1 "10K" V 3250 2550 50 0000 C CNN 66 | F 2 "Resistor_SMD:R_0805_2012Metric" V 3180 2550 50 0001 C CNN 67 | F 3 "~" H 3250 2550 50 0001 C CNN 68 | 1 3250 2550 69 | 1 0 0 -1 70 | $EndComp 71 | Text Notes 3500 2550 0 50 ~ 0 72 | 5 x 10K resistors in 0805 package 73 | $Comp 74 | L Device:R R6 75 | U 1 1 5E6A3CA0 76 | P 2200 3100 77 | F 0 "R6" V 2280 3100 50 0000 C CNN 78 | F 1 "4K7" V 2200 3100 50 0000 C CNN 79 | F 2 "Resistor_SMD:R_0805_2012Metric" V 2130 3100 50 0001 C CNN 80 | F 3 "~" H 2200 3100 50 0001 C CNN 81 | 1 2200 3100 82 | 1 0 0 -1 83 | $EndComp 84 | $Comp 85 | L Device:R R7 86 | U 1 1 5E6A3F38 87 | P 2500 3100 88 | F 0 "R7" V 2580 3100 50 0000 C CNN 89 | F 1 "4700" V 2500 3100 50 0000 C CNN 90 | F 2 "Resistor_SMD:R_0805_2012Metric" V 2430 3100 50 0001 C CNN 91 | F 3 "~" H 2500 3100 50 0001 C CNN 92 | 1 2500 3100 93 | 1 0 0 -1 94 | $EndComp 95 | $Comp 96 | L Device:R R8 97 | U 1 1 5E6A4181 98 | P 2750 3100 99 | F 0 "R8" V 2830 3100 50 0000 C CNN 100 | F 1 "4.7K" V 2750 3100 50 0000 C CNN 101 | F 2 "Resistor_SMD:R_0805_2012Metric" V 2680 3100 50 0001 C CNN 102 | F 3 "~" H 2750 3100 50 0001 C CNN 103 | 1 2750 3100 104 | 1 0 0 -1 105 | $EndComp 106 | Text Notes 3500 3150 0 50 ~ 0 107 | 3 x 4K7 resistors in 0805 package\nNote: Values are identical even if specified differently 108 | $Comp 109 | L Device:R R9 110 | U 1 1 5E6A448B 111 | P 2200 3650 112 | F 0 "R9" V 2280 3650 50 0000 C CNN 113 | F 1 "4K7" V 2200 3650 50 0000 C CNN 114 | F 2 "Resistor_SMD:R_0603_1608Metric" V 2130 3650 50 0001 C CNN 115 | F 3 "~" H 2200 3650 50 0001 C CNN 116 | 1 2200 3650 117 | 1 0 0 -1 118 | $EndComp 119 | $Comp 120 | L Device:R R10 121 | U 1 1 5E6A491A 122 | P 2500 3650 123 | F 0 "R10" V 2580 3650 50 0000 C CNN 124 | F 1 "4K7" V 2500 3650 50 0000 C CNN 125 | F 2 "Resistor_SMD:R_0603_1608Metric" V 2430 3650 50 0001 C CNN 126 | F 3 "~" H 2500 3650 50 0001 C CNN 127 | 1 2500 3650 128 | 1 0 0 -1 129 | $EndComp 130 | Text Notes 3500 3650 0 50 ~ 0 131 | 3 x 4K7 resistors in 0603 package 132 | Text Notes 550 950 0 50 ~ 0 133 | This schematic serves as a test-file for the KiBom export script.\n\nAfter making a change to the schematic, remember to re-export the netlist\n\n(The testing framework cannot perform the netlist-export step!) 134 | $Comp 135 | L Device:C C1 136 | U 1 1 5E6A62CC 137 | P 6650 2550 138 | F 0 "C1" H 6675 2650 50 0000 L CNN 139 | F 1 "10nF" H 6675 2450 50 0000 L CNN 140 | F 2 "Capacitor_SMD:C_0603_1608Metric" H 6688 2400 50 0001 C CNN 141 | F 3 "~" H 6650 2550 50 0001 C CNN 142 | 1 6650 2550 143 | 1 0 0 -1 144 | $EndComp 145 | $Comp 146 | L Device:C C2 147 | U 1 1 5E6A6854 148 | P 7050 2550 149 | F 0 "C2" H 7075 2650 50 0000 L CNN 150 | F 1 "10n" H 7075 2450 50 0000 L CNN 151 | F 2 "Capacitor_SMD:C_0603_1608Metric" H 7088 2400 50 0001 C CNN 152 | F 3 "~" H 7050 2550 50 0001 C CNN 153 | 1 7050 2550 154 | 1 0 0 -1 155 | $EndComp 156 | $Comp 157 | L Device:C C3 158 | U 1 1 5E6A6A34 159 | P 7450 2550 160 | F 0 "C3" H 7475 2650 50 0000 L CNN 161 | F 1 "0.01uF" H 7475 2450 50 0000 L CNN 162 | F 2 "Capacitor_SMD:C_0603_1608Metric" H 7488 2400 50 0001 C CNN 163 | F 3 "~" H 7450 2550 50 0001 C CNN 164 | 1 7450 2550 165 | 1 0 0 -1 166 | $EndComp 167 | $Comp 168 | L Device:C C4 169 | U 1 1 5E6A6CB6 170 | P 7900 2550 171 | F 0 "C4" H 7925 2650 50 0000 L CNN 172 | F 1 "0.01uf" H 7925 2450 50 0000 L CNN 173 | F 2 "Capacitor_SMD:C_0603_1608Metric" H 7938 2400 50 0001 C CNN 174 | F 3 "~" H 7900 2550 50 0001 C CNN 175 | 1 7900 2550 176 | 1 0 0 -1 177 | $EndComp 178 | $EndSCHEMATC 179 | -------------------------------------------------------------------------------- /test/kibom-test.sch: -------------------------------------------------------------------------------- 1 | EESchema Schematic File Version 4 2 | EELAYER 30 0 3 | EELAYER END 4 | $Descr A4 11693 8268 5 | encoding utf-8 6 | Sheet 1 1 7 | Title "KiBom Test Schematic" 8 | Date "2020-03-12" 9 | Rev "A" 10 | Comp "https://github.com/SchrodingersGat/KiBom" 11 | Comment1 "" 12 | Comment2 "" 13 | Comment3 "" 14 | Comment4 "" 15 | $EndDescr 16 | $Comp 17 | L Device:R R1 18 | U 1 1 5E6A2873 19 | P 2200 2550 20 | F 0 "R1" V 2280 2550 50 0000 C CNN 21 | F 1 "10K" V 2200 2550 50 0000 C CNN 22 | F 2 "Resistor_SMD:R_0805_2012Metric" V 2130 2550 50 0001 C CNN 23 | F 3 "~" H 2200 2550 50 0001 C CNN 24 | 1 2200 2550 25 | 1 0 0 -1 26 | $EndComp 27 | $Comp 28 | L Device:R R2 29 | U 1 1 5E6A330D 30 | P 2500 2550 31 | F 0 "R2" V 2580 2550 50 0000 C CNN 32 | F 1 "10K" V 2500 2550 50 0000 C CNN 33 | F 2 "Resistor_SMD:R_0805_2012Metric" V 2430 2550 50 0001 C CNN 34 | F 3 "~" H 2500 2550 50 0001 C CNN 35 | 1 2500 2550 36 | 1 0 0 -1 37 | $EndComp 38 | $Comp 39 | L Device:R R3 40 | U 1 1 5E6A35E1 41 | P 2750 2550 42 | F 0 "R3" V 2830 2550 50 0000 C CNN 43 | F 1 "10K" V 2750 2550 50 0000 C CNN 44 | F 2 "Resistor_SMD:R_0805_2012Metric" V 2680 2550 50 0001 C CNN 45 | F 3 "~" H 2750 2550 50 0001 C CNN 46 | 1 2750 2550 47 | 1 0 0 -1 48 | $EndComp 49 | $Comp 50 | L Device:R R4 51 | U 1 1 5E6A37B2 52 | P 3000 2550 53 | F 0 "R4" V 3080 2550 50 0000 C CNN 54 | F 1 "10K" V 3000 2550 50 0000 C CNN 55 | F 2 "Resistor_SMD:R_0805_2012Metric" V 2930 2550 50 0001 C CNN 56 | F 3 "~" H 3000 2550 50 0001 C CNN 57 | 1 3000 2550 58 | 1 0 0 -1 59 | $EndComp 60 | $Comp 61 | L Device:R R5 62 | U 1 1 5E6A39EB 63 | P 3250 2550 64 | F 0 "R5" V 3330 2550 50 0000 C CNN 65 | F 1 "10K" V 3250 2550 50 0000 C CNN 66 | F 2 "Resistor_SMD:R_0805_2012Metric" V 3180 2550 50 0001 C CNN 67 | F 3 "~" H 3250 2550 50 0001 C CNN 68 | 1 3250 2550 69 | 1 0 0 -1 70 | $EndComp 71 | Text Notes 3500 2550 0 50 ~ 0 72 | 5 x 10K resistors in 0805 package 73 | $Comp 74 | L Device:R R6 75 | U 1 1 5E6A3CA0 76 | P 2200 3100 77 | F 0 "R6" V 2280 3100 50 0000 C CNN 78 | F 1 "4K7" V 2200 3100 50 0000 C CNN 79 | F 2 "Resistor_SMD:R_0805_2012Metric" V 2130 3100 50 0001 C CNN 80 | F 3 "~" H 2200 3100 50 0001 C CNN 81 | 1 2200 3100 82 | 1 0 0 -1 83 | $EndComp 84 | $Comp 85 | L Device:R R7 86 | U 1 1 5E6A3F38 87 | P 2500 3100 88 | F 0 "R7" V 2580 3100 50 0000 C CNN 89 | F 1 "4700" V 2500 3100 50 0000 C CNN 90 | F 2 "Resistor_SMD:R_0805_2012Metric" V 2430 3100 50 0001 C CNN 91 | F 3 "~" H 2500 3100 50 0001 C CNN 92 | 1 2500 3100 93 | 1 0 0 -1 94 | $EndComp 95 | $Comp 96 | L Device:R R8 97 | U 1 1 5E6A4181 98 | P 2750 3100 99 | F 0 "R8" V 2830 3100 50 0000 C CNN 100 | F 1 "4.7K" V 2750 3100 50 0000 C CNN 101 | F 2 "Resistor_SMD:R_0805_2012Metric" V 2680 3100 50 0001 C CNN 102 | F 3 "~" H 2750 3100 50 0001 C CNN 103 | 1 2750 3100 104 | 1 0 0 -1 105 | $EndComp 106 | Text Notes 3500 3150 0 50 ~ 0 107 | 3 x 4K7 resistors in 0805 package\nNote: Values are identical even if specified differently 108 | $Comp 109 | L Device:R R9 110 | U 1 1 5E6A448B 111 | P 2200 3650 112 | F 0 "R9" V 2280 3650 50 0000 C CNN 113 | F 1 "4K7" V 2200 3650 50 0000 C CNN 114 | F 2 "Resistor_SMD:R_0603_1608Metric" V 2130 3650 50 0001 C CNN 115 | F 3 "~" H 2200 3650 50 0001 C CNN 116 | 1 2200 3650 117 | 1 0 0 -1 118 | $EndComp 119 | $Comp 120 | L Device:R R10 121 | U 1 1 5E6A491A 122 | P 2500 3650 123 | F 0 "R10" V 2580 3650 50 0000 C CNN 124 | F 1 "4K7" V 2500 3650 50 0000 C CNN 125 | F 2 "Resistor_SMD:R_0603_1608Metric" V 2430 3650 50 0001 C CNN 126 | F 3 "~" H 2500 3650 50 0001 C CNN 127 | 1 2500 3650 128 | 1 0 0 -1 129 | $EndComp 130 | Text Notes 3500 3650 0 50 ~ 0 131 | 3 x 4K7 resistors in 0603 package 132 | Text Notes 550 950 0 50 ~ 0 133 | This schematic serves as a test-file for the KiBom export script.\n\nAfter making a change to the schematic, remember to re-export the BOM to generate the intermediate .xml file\n\n(The testing framework cannot perform the netlist-export step!) 134 | $Comp 135 | L Device:C C1 136 | U 1 1 5E6A62CC 137 | P 6650 2550 138 | F 0 "C1" H 6675 2650 50 0000 L CNN 139 | F 1 "10nF" H 6675 2450 50 0000 L CNN 140 | F 2 "Capacitor_SMD:C_0603_1608Metric" H 6688 2400 50 0001 C CNN 141 | F 3 "~" H 6650 2550 50 0001 C CNN 142 | 1 6650 2550 143 | 1 0 0 -1 144 | $EndComp 145 | $Comp 146 | L Device:C C2 147 | U 1 1 5E6A6854 148 | P 7050 2550 149 | F 0 "C2" H 7075 2650 50 0000 L CNN 150 | F 1 "10n" H 7075 2450 50 0000 L CNN 151 | F 2 "Capacitor_SMD:C_0603_1608Metric" H 7088 2400 50 0001 C CNN 152 | F 3 "~" H 7050 2550 50 0001 C CNN 153 | 1 7050 2550 154 | 1 0 0 -1 155 | $EndComp 156 | $Comp 157 | L Device:C C3 158 | U 1 1 5E6A6A34 159 | P 7450 2550 160 | F 0 "C3" H 7475 2650 50 0000 L CNN 161 | F 1 "0.01uF" H 7475 2450 50 0000 L CNN 162 | F 2 "Capacitor_SMD:C_0603_1608Metric" H 7488 2400 50 0001 C CNN 163 | F 3 "~" H 7450 2550 50 0001 C CNN 164 | 1 7450 2550 165 | 1 0 0 -1 166 | $EndComp 167 | $Comp 168 | L Device:C C4 169 | U 1 1 5E6A6CB6 170 | P 7900 2550 171 | F 0 "C4" H 7925 2650 50 0000 L CNN 172 | F 1 "0.01uf" H 7925 2450 50 0000 L CNN 173 | F 2 "Capacitor_SMD:C_0603_1608Metric" H 7938 2400 50 0001 C CNN 174 | F 3 "~" H 7900 2550 50 0001 C CNN 175 | 1 7900 2550 176 | 1 0 0 -1 177 | $EndComp 178 | $EndSCHEMATC 179 | -------------------------------------------------------------------------------- /kibom/units.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | This file contains a set of functions for matching values which may be written in different formats 6 | e.g. 7 | 0.1uF = 100n (different suffix specified, one has missing unit) 8 | 0R1 = 0.1Ohm (Unit replaces decimal, different units) 9 | 10 | """ 11 | 12 | from __future__ import unicode_literals 13 | import re 14 | import locale 15 | 16 | PREFIX_MICRO = [u"μ", u"µ", "u", "micro"] 17 | PREFIX_MILLI = ["milli", "m"] 18 | PREFIX_NANO = ["nano", "n"] 19 | PREFIX_PICO = ["pico", "p"] 20 | PREFIX_KILO = ["kilo", "k"] 21 | PREFIX_MEGA = ["mega", "meg", "M"] 22 | PREFIX_GIGA = ["giga", "g"] 23 | 24 | # All prefixes 25 | PREFIX_ALL = PREFIX_PICO + PREFIX_NANO + PREFIX_MICRO + PREFIX_MILLI + PREFIX_KILO + PREFIX_MEGA + PREFIX_GIGA 26 | 27 | # Common methods of expressing component units 28 | # Note: we match lowercase string, so both: Ω and Ω become the lowercase omega 29 | UNIT_R = ["r", "ohms", "ohm", u'\u03c9'] 30 | UNIT_C = ["farad", "f"] 31 | UNIT_L = ["henry", "h"] 32 | 33 | UNIT_ALL = UNIT_R + UNIT_C + UNIT_L 34 | 35 | # Compiled regex to match the values 36 | match = None 37 | # Current locale decimal point value 38 | decimal_point = None 39 | 40 | 41 | def getUnit(unit): 42 | """ 43 | Return a simplified version of a units string, for comparison purposes 44 | """ 45 | 46 | if not unit: 47 | return None 48 | 49 | unit = unit.lower() 50 | 51 | if unit in UNIT_R: 52 | return "R" 53 | if unit in UNIT_C: 54 | return "F" 55 | if unit in UNIT_L: 56 | return "H" 57 | 58 | return None 59 | 60 | 61 | def getPrefix(prefix): 62 | """ 63 | Return the (numerical) value of a given prefix 64 | """ 65 | 66 | if not prefix: 67 | return 1 68 | 69 | # 'M' is mega, 'm' is milli 70 | if prefix != 'M': 71 | prefix = prefix.lower() 72 | 73 | if prefix in PREFIX_PICO: 74 | return 1.0e-12 75 | if prefix in PREFIX_NANO: 76 | return 1.0e-9 77 | if prefix in PREFIX_MICRO: 78 | return 1.0e-6 79 | if prefix in PREFIX_MILLI: 80 | return 1.0e-3 81 | if prefix in PREFIX_KILO: 82 | return 1.0e3 83 | if prefix in PREFIX_MEGA: 84 | return 1.0e6 85 | if prefix in PREFIX_GIGA: 86 | return 1.0e9 87 | 88 | return 1 89 | 90 | 91 | def groupString(group): # Return a reg-ex string for a list of values 92 | return "|".join(group) 93 | 94 | 95 | def matchString(): 96 | return r"^([0-9\.]+)\s*(" + groupString(PREFIX_ALL) + ")*(" + groupString(UNIT_ALL) + r")*(\d*)$" 97 | 98 | 99 | def compMatch(component): 100 | """ 101 | Return a normalized value and units for a given component value string 102 | e.g. compMatch('10R2') returns (10, R) 103 | e.g. compMatch('3.3mOhm') returns (0.0033, R) 104 | """ 105 | 106 | # Convert the decimal point from the current locale to a '.' 107 | global decimal_point 108 | if not decimal_point: 109 | decimal_point = locale.localeconv()['decimal_point'] 110 | if decimal_point and decimal_point != '.': 111 | component = component.replace(decimal_point, ".") 112 | 113 | # Remove any commas 114 | component = component.strip().replace(",", "") 115 | 116 | # Get the compiled regex 117 | global match 118 | if not match: 119 | match = re.compile(matchString(), flags=re.IGNORECASE) 120 | 121 | # Not lower, but ignore case 122 | result = match.search(component) 123 | 124 | if not result: 125 | return None 126 | 127 | if not len(result.groups()) == 4: 128 | return None 129 | 130 | value, prefix, units, post = result.groups() 131 | 132 | # Special case where units is in the middle of the string 133 | # e.g. "0R05" for 0.05Ohm 134 | # In this case, we will NOT have a decimal 135 | # We will also have a trailing number 136 | 137 | if post and "." not in value: 138 | try: 139 | value = float(int(value)) 140 | postValue = float(int(post)) / (10 ** len(post)) 141 | value = value * 1.0 + postValue 142 | except: 143 | return None 144 | 145 | try: 146 | val = float(value) 147 | except: 148 | return None 149 | 150 | # Return all the data, let the caller join it 151 | return (val, getPrefix(prefix), getUnit(units)) 152 | 153 | 154 | def componentValue(valString): 155 | 156 | result = compMatch(valString) 157 | 158 | if not result: 159 | return valString # Return the same string back 160 | 161 | if not len(result) == 2: # Result length is incorrect 162 | return valString 163 | 164 | val = result[0] 165 | 166 | return val 167 | 168 | 169 | def compareValues(c1, c2): 170 | """ Compare two values """ 171 | 172 | r1 = compMatch(c1) 173 | r2 = compMatch(c2) 174 | 175 | if not r1 or not r2: 176 | return False 177 | 178 | # Join the data to compare 179 | (v1, p1, u1) = r1 180 | (v2, p2, u2) = r2 181 | 182 | v1 = "{0:.15f}".format(v1 * 1.0 * p1) 183 | v2 = "{0:.15f}".format(v2 * 1.0 * p2) 184 | 185 | if v1 == v2: 186 | # Values match 187 | if u1 == u2: 188 | return True # Units match 189 | if not u1: 190 | return True # No units for component 1 191 | if not u2: 192 | return True # No units for component 2 193 | 194 | return False 195 | -------------------------------------------------------------------------------- /kibom/xlsx_writer.py: -------------------------------------------------------------------------------- 1 | # _*_ coding:latin-1 _*_ 2 | 3 | try: 4 | import xlsxwriter 5 | except: 6 | def WriteXLSX(filename, groups, net, headings, head_names, prefs): 7 | return False 8 | else: 9 | import os 10 | 11 | """ 12 | Write BoM out to a XLSX file 13 | filename = path to output file (must be a .xlsx file) 14 | groups = [list of ComponentGroup groups] 15 | net = netlist object 16 | headings = [list of headings to search for data in the BoM file] 17 | head_names = [list of headings to display in the BoM file] 18 | prefs = BomPref object 19 | """ 20 | 21 | def WriteXLSX(filename, groups, net, headings, head_names, prefs): 22 | 23 | filename = os.path.abspath(filename) 24 | 25 | if not filename.endswith(".xlsx"): 26 | return False 27 | 28 | nGroups = len(groups) 29 | nTotal = sum([g.getCount() for g in groups]) 30 | nFitted = sum([g.getCount() for g in groups if g.isFitted()]) 31 | nBuild = nFitted * prefs.boards 32 | 33 | workbook = xlsxwriter.Workbook(filename) 34 | worksheet = workbook.add_worksheet() 35 | 36 | if prefs.numberRows: 37 | comp = "Component" 38 | if comp.lower() in prefs.colRename: 39 | comp = prefs.colRename[comp.lower()] 40 | row_headings = [comp] + head_names 41 | else: 42 | row_headings = head_names 43 | 44 | link_datasheet = prefs.as_link 45 | link_digikey = None 46 | if prefs.digikey_link: 47 | link_digikey = prefs.digikey_link.split("\t") 48 | 49 | cellformats = {} 50 | column_widths = {} 51 | for i in range(len(row_headings)): 52 | cellformats[i] = workbook.add_format({'align': 'left'}) 53 | column_widths[i] = len(row_headings[i]) + 10 54 | 55 | if not prefs.hideHeaders: 56 | worksheet.write_string(0, i, row_headings[i], cellformats[i]) 57 | 58 | count = 0 59 | rowCount = 1 60 | 61 | for i, group in enumerate(groups): 62 | 63 | if prefs.ignoreDNF and not group.isFitted(): 64 | continue 65 | 66 | row = group.getRow(headings) 67 | 68 | if prefs.numberRows: 69 | row = [str(rowCount)] + row 70 | 71 | for columnCount in range(len(row)): 72 | cell = row[columnCount] 73 | if link_datasheet and row_headings[columnCount] == link_datasheet: 74 | worksheet.write_url(rowCount, columnCount, cell, cellformats[columnCount]) 75 | elif link_digikey and row_headings[columnCount] in link_digikey: 76 | url = "https://www.digikey.com/en/products?mpart=" + cell 77 | worksheet.write_url(rowCount, columnCount, url, cellformats[columnCount], cell) 78 | else: 79 | worksheet.write_string(rowCount, columnCount, cell, cellformats[columnCount]) 80 | 81 | # if len(cell) > column_widths[columnCount] - 5: 82 | # column_widths[columnCount] = len(cell) + 5 83 | 84 | try: 85 | count += group.getCount() 86 | except: 87 | pass 88 | 89 | rowCount += 1 90 | 91 | if not prefs.hidePcbInfo: 92 | # Add a few blank rows 93 | for i in range(5): 94 | rowCount += 1 95 | 96 | cellformat_left = workbook.add_format({'align': 'left'}) 97 | 98 | worksheet.write_string(rowCount, 0, "Component Groups:", cellformats[0]) 99 | worksheet.write_number(rowCount, 1, nGroups, cellformat_left) 100 | rowCount += 1 101 | 102 | worksheet.write_string(rowCount, 0, "Component Count:", cellformats[0]) 103 | worksheet.write_number(rowCount, 1, nTotal, cellformat_left) 104 | rowCount += 1 105 | 106 | worksheet.write_string(rowCount, 0, "Fitted Components:", cellformats[0]) 107 | worksheet.write_number(rowCount, 1, nFitted, cellformat_left) 108 | rowCount += 1 109 | 110 | worksheet.write_string(rowCount, 0, "Number of PCBs:", cellformats[0]) 111 | worksheet.write_number(rowCount, 1, prefs.boards, cellformat_left) 112 | rowCount += 1 113 | 114 | worksheet.write_string(rowCount, 0, "Total components:", cellformats[0]) 115 | worksheet.write_number(rowCount, 1, nBuild, cellformat_left) 116 | rowCount += 1 117 | 118 | worksheet.write_string(rowCount, 0, "Schematic Version:", cellformats[0]) 119 | worksheet.write_string(rowCount, 1, net.getVersion(), cellformat_left) 120 | rowCount += 1 121 | 122 | if len(net.getVersion()) > column_widths[1]: 123 | column_widths[1] = len(net.getVersion()) 124 | 125 | worksheet.write_string(rowCount, 0, "Schematic Date:", cellformats[0]) 126 | worksheet.write_string(rowCount, 1, net.getSheetDate(), cellformat_left) 127 | rowCount += 1 128 | 129 | if len(net.getSheetDate()) > column_widths[1]: 130 | column_widths[1] = len(net.getSheetDate()) 131 | 132 | worksheet.write_string(rowCount, 0, "BoM Date:", cellformats[0]) 133 | worksheet.write_string(rowCount, 1, net.getDate(), cellformat_left) 134 | rowCount += 1 135 | 136 | if len(net.getDate()) > column_widths[1]: 137 | column_widths[1] = len(net.getDate()) 138 | 139 | worksheet.write_string(rowCount, 0, "Schematic Source:", cellformats[0]) 140 | worksheet.write_string(rowCount, 1, net.getSource(), cellformat_left) 141 | rowCount += 1 142 | 143 | if len(net.getSource()) > column_widths[1]: 144 | column_widths[1] = len(net.getSource()) 145 | 146 | worksheet.write_string(rowCount, 0, "KiCad Version:", cellformats[0]) 147 | worksheet.write_string(rowCount, 1, net.getTool(), cellformat_left) 148 | rowCount += 1 149 | 150 | if len(net.getTool()) > column_widths[1]: 151 | column_widths[1] = len(net.getTool()) 152 | 153 | for i in range(len(column_widths)): 154 | worksheet.set_column(i, i, column_widths[i]) 155 | 156 | workbook.close() 157 | 158 | return True 159 | -------------------------------------------------------------------------------- /test/kibom-test.net: -------------------------------------------------------------------------------- 1 | (export (version D) 2 | (design 3 | (source C:\KiCad\KiBoM\test\kibom-test.sch) 4 | (date "12/03/2020 11:25:40 PM") 5 | (tool "Eeschema (5.1.5)-2") 6 | (sheet (number 1) (name /) (tstamps /) 7 | (title_block 8 | (title "KiBom Test Schematic") 9 | (company https://github.com/SchrodingersGat/KiBom) 10 | (rev A) 11 | (date 2020-03-12) 12 | (source kibom-test.sch) 13 | (comment (number 1) (value "")) 14 | (comment (number 2) (value "")) 15 | (comment (number 3) (value "")) 16 | (comment (number 4) (value ""))))) 17 | (components 18 | (comp (ref R1) 19 | (value 10K) 20 | (footprint Resistor_SMD:R_0805_2012Metric) 21 | (datasheet ~) 22 | (libsource (lib Device) (part R) (description Resistor)) 23 | (sheetpath (names /) (tstamps /)) 24 | (tstamp 5E6A2873)) 25 | (comp (ref R2) 26 | (value 10K) 27 | (footprint Resistor_SMD:R_0805_2012Metric) 28 | (datasheet ~) 29 | (libsource (lib Device) (part R) (description Resistor)) 30 | (sheetpath (names /) (tstamps /)) 31 | (tstamp 5E6A330D)) 32 | (comp (ref R3) 33 | (value 10K) 34 | (footprint Resistor_SMD:R_0805_2012Metric) 35 | (datasheet ~) 36 | (libsource (lib Device) (part R) (description Resistor)) 37 | (sheetpath (names /) (tstamps /)) 38 | (tstamp 5E6A35E1)) 39 | (comp (ref R4) 40 | (value 10K) 41 | (footprint Resistor_SMD:R_0805_2012Metric) 42 | (datasheet ~) 43 | (libsource (lib Device) (part R) (description Resistor)) 44 | (sheetpath (names /) (tstamps /)) 45 | (tstamp 5E6A37B2)) 46 | (comp (ref R5) 47 | (value 10K) 48 | (footprint Resistor_SMD:R_0805_2012Metric) 49 | (datasheet ~) 50 | (libsource (lib Device) (part R) (description Resistor)) 51 | (sheetpath (names /) (tstamps /)) 52 | (tstamp 5E6A39EB)) 53 | (comp (ref R6) 54 | (value 4K7) 55 | (footprint Resistor_SMD:R_0805_2012Metric) 56 | (datasheet ~) 57 | (libsource (lib Device) (part R) (description Resistor)) 58 | (sheetpath (names /) (tstamps /)) 59 | (tstamp 5E6A3CA0)) 60 | (comp (ref R7) 61 | (value 4700) 62 | (footprint Resistor_SMD:R_0805_2012Metric) 63 | (datasheet ~) 64 | (libsource (lib Device) (part R) (description Resistor)) 65 | (sheetpath (names /) (tstamps /)) 66 | (tstamp 5E6A3F38)) 67 | (comp (ref R8) 68 | (value 4.7K) 69 | (footprint Resistor_SMD:R_0805_2012Metric) 70 | (datasheet ~) 71 | (libsource (lib Device) (part R) (description Resistor)) 72 | (sheetpath (names /) (tstamps /)) 73 | (tstamp 5E6A4181)) 74 | (comp (ref R9) 75 | (value 4K7) 76 | (footprint Resistor_SMD:R_0603_1608Metric) 77 | (datasheet ~) 78 | (libsource (lib Device) (part R) (description Resistor)) 79 | (sheetpath (names /) (tstamps /)) 80 | (tstamp 5E6A448B)) 81 | (comp (ref R10) 82 | (value 4K7) 83 | (footprint Resistor_SMD:R_0603_1608Metric) 84 | (datasheet ~) 85 | (libsource (lib Device) (part R) (description Resistor)) 86 | (sheetpath (names /) (tstamps /)) 87 | (tstamp 5E6A491A)) 88 | (comp (ref C1) 89 | (value 10nF) 90 | (footprint Capacitor_SMD:C_0603_1608Metric) 91 | (datasheet ~) 92 | (libsource (lib Device) (part C) (description "Unpolarized capacitor")) 93 | (sheetpath (names /) (tstamps /)) 94 | (tstamp 5E6A62CC)) 95 | (comp (ref C2) 96 | (value 10n) 97 | (footprint Capacitor_SMD:C_0603_1608Metric) 98 | (datasheet ~) 99 | (libsource (lib Device) (part C) (description "Unpolarized capacitor")) 100 | (sheetpath (names /) (tstamps /)) 101 | (tstamp 5E6A6854)) 102 | (comp (ref C3) 103 | (value 0.01uF) 104 | (footprint Capacitor_SMD:C_0603_1608Metric) 105 | (datasheet ~) 106 | (libsource (lib Device) (part C) (description "Unpolarized capacitor")) 107 | (sheetpath (names /) (tstamps /)) 108 | (tstamp 5E6A6A34)) 109 | (comp (ref C4) 110 | (value 0.01uf) 111 | (footprint Capacitor_SMD:C_0603_1608Metric) 112 | (datasheet ~) 113 | (libsource (lib Device) (part C) (description "Unpolarized capacitor")) 114 | (sheetpath (names /) (tstamps /)) 115 | (tstamp 5E6A6CB6))) 116 | (libparts 117 | (libpart (lib Device) (part C) 118 | (description "Unpolarized capacitor") 119 | (docs ~) 120 | (footprints 121 | (fp C_*)) 122 | (fields 123 | (field (name Reference) C) 124 | (field (name Value) C)) 125 | (pins 126 | (pin (num 1) (name ~) (type passive)) 127 | (pin (num 2) (name ~) (type passive)))) 128 | (libpart (lib Device) (part R) 129 | (description Resistor) 130 | (docs ~) 131 | (footprints 132 | (fp R_*)) 133 | (fields 134 | (field (name Reference) R) 135 | (field (name Value) R)) 136 | (pins 137 | (pin (num 1) (name ~) (type passive)) 138 | (pin (num 2) (name ~) (type passive))))) 139 | (libraries 140 | (library (logical Device) 141 | (uri C:\KiCad\share\kicad-symbols/Device.lib))) 142 | (nets 143 | (net (code 1) (name "Net-(R8-Pad1)") 144 | (node (ref R8) (pin 1))) 145 | (net (code 2) (name "Net-(C4-Pad2)") 146 | (node (ref C4) (pin 2))) 147 | (net (code 3) (name "Net-(C4-Pad1)") 148 | (node (ref C4) (pin 1))) 149 | (net (code 4) (name "Net-(C3-Pad2)") 150 | (node (ref C3) (pin 2))) 151 | (net (code 5) (name "Net-(C3-Pad1)") 152 | (node (ref C3) (pin 1))) 153 | (net (code 6) (name "Net-(C2-Pad2)") 154 | (node (ref C2) (pin 2))) 155 | (net (code 7) (name "Net-(C2-Pad1)") 156 | (node (ref C2) (pin 1))) 157 | (net (code 8) (name "Net-(C1-Pad2)") 158 | (node (ref C1) (pin 2))) 159 | (net (code 9) (name "Net-(C1-Pad1)") 160 | (node (ref C1) (pin 1))) 161 | (net (code 10) (name "Net-(R10-Pad2)") 162 | (node (ref R10) (pin 2))) 163 | (net (code 11) (name "Net-(R10-Pad1)") 164 | (node (ref R10) (pin 1))) 165 | (net (code 12) (name "Net-(R9-Pad2)") 166 | (node (ref R9) (pin 2))) 167 | (net (code 13) (name "Net-(R9-Pad1)") 168 | (node (ref R9) (pin 1))) 169 | (net (code 14) (name "Net-(R8-Pad2)") 170 | (node (ref R8) (pin 2))) 171 | (net (code 15) (name "Net-(R1-Pad1)") 172 | (node (ref R1) (pin 1))) 173 | (net (code 16) (name "Net-(R7-Pad2)") 174 | (node (ref R7) (pin 2))) 175 | (net (code 17) (name "Net-(R7-Pad1)") 176 | (node (ref R7) (pin 1))) 177 | (net (code 18) (name "Net-(R6-Pad2)") 178 | (node (ref R6) (pin 2))) 179 | (net (code 19) (name "Net-(R6-Pad1)") 180 | (node (ref R6) (pin 1))) 181 | (net (code 20) (name "Net-(R5-Pad2)") 182 | (node (ref R5) (pin 2))) 183 | (net (code 21) (name "Net-(R5-Pad1)") 184 | (node (ref R5) (pin 1))) 185 | (net (code 22) (name "Net-(R4-Pad2)") 186 | (node (ref R4) (pin 2))) 187 | (net (code 23) (name "Net-(R4-Pad1)") 188 | (node (ref R4) (pin 1))) 189 | (net (code 24) (name "Net-(R3-Pad2)") 190 | (node (ref R3) (pin 2))) 191 | (net (code 25) (name "Net-(R3-Pad1)") 192 | (node (ref R3) (pin 1))) 193 | (net (code 26) (name "Net-(R2-Pad2)") 194 | (node (ref R2) (pin 2))) 195 | (net (code 27) (name "Net-(R2-Pad1)") 196 | (node (ref R2) (pin 1))) 197 | (net (code 28) (name "Net-(R1-Pad2)") 198 | (node (ref R1) (pin 2))))) -------------------------------------------------------------------------------- /kibom/html_writer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .component import ColumnList 4 | from . import debug 5 | 6 | BG_GEN = "#E6FFEE" 7 | BG_KICAD = "#FFE6B3" 8 | BG_USER = "#E6F9FF" 9 | BG_EMPTY = "#FF8080" 10 | 11 | 12 | def bgColor(col): 13 | """ Return a background color for a given column title """ 14 | 15 | # Auto-generated columns 16 | if col in ColumnList._COLUMNS_GEN: 17 | return BG_GEN 18 | # KiCad protected columns 19 | elif col in ColumnList._COLUMNS_PROTECTED: 20 | return BG_KICAD 21 | # Additional user columns 22 | else: 23 | return BG_USER 24 | 25 | 26 | def link(text): 27 | 28 | for t in ["http", "https", "ftp", "www"]: 29 | if text.startswith(t): 30 | return '{t}'.format(t=text) 31 | 32 | return text 33 | 34 | 35 | def WriteHTML(filename, groups, net, headings, head_names, prefs): 36 | """ 37 | Write BoM out to a HTML file 38 | filename = path to output file (must be a .htm or .html file) 39 | groups = [list of ComponentGroup groups] 40 | net = netlist object 41 | headings = [list of headings to search for data in the BoM file] 42 | head_names = [list of headings to display in the BoM file] 43 | prefs = BomPref object 44 | """ 45 | 46 | if not filename.endswith(".html") and not filename.endswith(".htm"): 47 | debug.error("{fn} is not a valid html file".format(fn=filename)) 48 | return False 49 | 50 | nGroups = len(groups) 51 | nTotal = sum([g.getCount() for g in groups]) 52 | nFitted = sum([g.getCount() for g in groups if g.isFitted()]) 53 | nBuild = nFitted * prefs.boards 54 | 55 | link_datasheet = prefs.as_link 56 | link_digikey = None 57 | if prefs.digikey_link: 58 | link_digikey = prefs.digikey_link.split("\t") 59 | 60 | link_mouser = None 61 | if prefs.mouser_link: 62 | link_mouser = prefs.mouser_link.split("\t") 63 | 64 | link_lcsc = None 65 | if prefs.lcsc_link: 66 | link_lcsc = prefs.lcsc_link.split("\t") 67 | with open(filename, "w") as html: 68 | 69 | # HTML Header 70 | html.write("\n") 71 | html.write("\n") 72 | html.write('\t\n') # UTF-8 encoding for unicode support 73 | html.write("\n") 74 | html.write("\n") 75 | 76 | # PCB info 77 | if not prefs.hideHeaders: 78 | html.write("

KiBoM PCB Bill of Materials

\n") 79 | if not prefs.hidePcbInfo: 80 | html.write('\n') 81 | html.write("\n".format(source=net.getSource())) 82 | html.write("\n".format(date=net.getDate())) 83 | html.write("\n".format(version=net.getVersion())) 84 | html.write("\n".format(date=net.getSheetDate())) 85 | html.write("\n".format(variant=', '.join(prefs.pcbConfig))) 86 | html.write("\n".format(version=net.getTool())) 87 | html.write("\n".format(n=nGroups)) 88 | html.write("\n".format(n=nTotal)) 89 | html.write("\n".format(n=nFitted)) 90 | html.write("\n".format(n=prefs.boards)) 91 | html.write("\n".format(n=prefs.boards, t=nBuild)) 92 | 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") 93 | html.write("
\n") 94 | 95 | if not prefs.hideHeaders: 96 | html.write("

Component Groups

\n") 97 | html.write('

KiCad Fields (default)

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

Generated Fields

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

User Fields

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

Empty Fields

\n'.format(bg=BG_EMPTY)) 101 | 102 | # Component groups 103 | html.write('\n') 104 | 105 | # Row titles: 106 | html.write("\n") 107 | 108 | if prefs.numberRows: 109 | html.write("\t\n") 110 | 111 | for i, h in enumerate(head_names): 112 | # Cell background color 113 | bg = bgColor(headings[i]) 114 | html.write('\t\n'.format( 115 | h=h, 116 | bg=' bgcolor="{c}"'.format(c=bg) if bg else '') 117 | ) 118 | 119 | html.write("\n") 120 | 121 | rowCount = 0 122 | 123 | for i, group in enumerate(groups): 124 | 125 | if prefs.ignoreDNF and not group.isFitted(): 126 | continue 127 | 128 | row = group.getRow(headings) 129 | 130 | rowCount += 1 131 | 132 | html.write("\n") 133 | 134 | if prefs.numberRows: 135 | html.write('\t\n'.format(n=rowCount)) 136 | 137 | for n, r in enumerate(row): 138 | if link_digikey and headings[n] in link_digikey: 139 | r = '' + r + '' 140 | 141 | if link_mouser and headings[n] in link_mouser: 142 | r = '' + r + '' 143 | 144 | if link_lcsc and headings[n] in link_lcsc: 145 | r = '' + r + '' 146 | 147 | # Link this column to the datasheet? 148 | if link_datasheet and headings[n] == link_datasheet: 149 | r = '' + r + '' 150 | 151 | if (len(r) == 0) or (r.strip() == "~"): 152 | bg = BG_EMPTY 153 | else: 154 | bg = bgColor(headings[n]) 155 | 156 | html.write('\t\n'.format(bg=' bgcolor={c}'.format(c=bg) if bg else '', val=link(r))) 157 | 158 | html.write("\n") 159 | 160 | html.write("
{h}
{n}{val}
\n") 161 | html.write("

\n") 162 | 163 | if prefs.generateDNF and rowCount != len(groups): 164 | html.write("

Optional components (DNF=Do Not Fit)

\n") 165 | 166 | # DNF component groups 167 | html.write('\n') 168 | 169 | # Row titles: 170 | html.write("\n") 171 | if prefs.numberRows: 172 | html.write("\t\n") 173 | for i, h in enumerate(head_names): 174 | # Cell background color 175 | bg = bgColor(headings[i]) 176 | html.write('\t\n'.format(h=h, bg=' bgcolor="{c}"'.format(c=bg) if bg else '')) 177 | html.write("\n") 178 | 179 | rowCount = 0 180 | 181 | for i, group in enumerate(groups): 182 | 183 | if not (prefs.ignoreDNF and not group.isFitted()): 184 | continue 185 | 186 | row = group.getRow(headings) 187 | rowCount += 1 188 | html.write("\n") 189 | 190 | if prefs.numberRows: 191 | html.write('\t\n'.format(n=rowCount)) 192 | 193 | for n, r in enumerate(row): 194 | 195 | # Link this column to the datasheet? 196 | if link_datasheet and headings[n] == link_datasheet: 197 | r = '' + r + '' 198 | 199 | if link_digikey and headings[n] in link_digikey: 200 | r = '' + r + '' 201 | 202 | if link_mouser and headings[n] in link_mouser: 203 | r = '' + r + '' 204 | 205 | if link_lcsc and headings[n] in link_lcsc: 206 | r = '' + r + '' 207 | 208 | if (len(r) == 0) or (r.strip() == "~"): 209 | bg = BG_EMPTY 210 | else: 211 | bg = bgColor(headings[n]) 212 | 213 | html.write('\t\n'.format(bg=' bgcolor={c}'.format(c=bg) if bg else '', val=link(r))) 214 | 215 | html.write("\n") 216 | 217 | html.write("
{h}
{n}{val}
\n") 218 | html.write("

\n") 219 | 220 | html.write("") 221 | 222 | return True 223 | -------------------------------------------------------------------------------- /test/kibom-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | C:\KiCad\KiBoM\test\kibom-test.sch 5 | 13/03/2020 12:28:18 PM 6 | Eeschema (5.1.5)-2 7 | 8 | 9 | KiBom Test Schematic 10 | https://github.com/SchrodingersGat/KiBom 11 | A 12 | 2020-03-12 13 | kibom-test.sch 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 10000 24 | Resistor_SMD:R_0805_2012Metric 25 | ~ 26 | 27 | 28 | 5E6A2873 29 | 30 | 31 | 10K 32 | Resistor_SMD:R_0805_2012Metric 33 | ~ 34 | 35 | 36 | 5E6A330D 37 | 38 | 39 | 10K 40 | Resistor_SMD:R_0805_2012Metric 41 | ~ 42 | 43 | 44 | 5E6A35E1 45 | 46 | 47 | 10K 48 | Resistor_SMD:R_0805_2012Metric 49 | ~ 50 | 51 | 52 | 5E6A37B2 53 | 54 | 55 | 10K 56 | Resistor_SMD:R_0805_2012Metric 57 | ~ 58 | 59 | 60 | 5E6A39EB 61 | 62 | 63 | 4K7 64 | Resistor_SMD:R_0805_2012Metric 65 | ~ 66 | 67 | DNF 68 | 69 | 70 | 71 | 5E6A3CA0 72 | 73 | 74 | 4700 75 | Resistor_SMD:R_0805_2012Metric 76 | ~ 77 | 78 | DNC 79 | 80 | 81 | 82 | 5E6A3F38 83 | 84 | 85 | 4.7K 86 | Resistor_SMD:R_0805_2012Metric 87 | ~ 88 | 89 | 90 | 5E6A4181 91 | 92 | 93 | 4K7 94 | Resistor_SMD:R_0603_1608Metric 95 | ~ 96 | 97 | 98 | 5E6A448B 99 | 100 | 101 | 4K7 102 | Resistor_SMD:R_0603_1608Metric 103 | ~ 104 | 105 | 106 | 5E6A491A 107 | 108 | 109 | 10nF 110 | Capacitor_SMD:C_0603_1608Metric 111 | ~ 112 | 113 | 114 | 5E6A62CC 115 | 116 | 117 | 10n 118 | Capacitor_SMD:C_0603_1608Metric 119 | ~ 120 | 121 | 122 | 5E6A6854 123 | 124 | 125 | 0.01uF 126 | Capacitor_SMD:C_0603_1608Metric 127 | ~ 128 | 129 | 130 | 5E6A6A34 131 | 132 | 133 | 0.01uf 134 | Capacitor_SMD:C_0603_1608Metric 135 | ~ 136 | 137 | 138 | 5E6A6CB6 139 | 140 | 141 | 142 | 143 | Unpolarized capacitor 144 | ~ 145 | 146 | C_* 147 | 148 | 149 | C 150 | C 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | Resistor 159 | ~ 160 | 161 | R_* 162 | 163 | 164 | R 165 | R 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | C:\KiCad\share\kicad-symbols/Device.lib 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | -------------------------------------------------------------------------------- /kibom/__main__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | @package 5 | KiBOM - Bill of Materials generation for KiCad 6 | 7 | Generate BOM in xml, csv, txt, tsv, html or xlsx formats. 8 | 9 | - Components are automatically grouped into BoM rows (grouping is configurable) 10 | - Component groups count number of components and list component designators 11 | - Rows are automatically sorted by component reference(s) 12 | - Supports board variants 13 | 14 | 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). 15 | 16 | For usage help: 17 | python -m kibom -h 18 | """ 19 | 20 | from __future__ import print_function 21 | 22 | import sys 23 | import os 24 | import argparse 25 | import locale 26 | 27 | from .columns import ColumnList 28 | from .netlist_reader import netlist 29 | from .bom_writer import WriteBoM 30 | from .preferences import BomPref 31 | from .version import KIBOM_VERSION 32 | from . import debug 33 | from .component import DNF 34 | 35 | VARIANT_FIELD_SEPARATOR = ':' 36 | 37 | 38 | def writeVariant(input_file, output_dir, output_file, variant, preferences): 39 | 40 | if variant is not None: 41 | preferences.pcbConfig = variant.strip().lower().split(',') 42 | 43 | debug.message("PCB variant:", ", ".join(preferences.pcbConfig)) 44 | 45 | # Individual components 46 | components = [] 47 | 48 | # Component groups 49 | groups = [] 50 | 51 | # Read out the netlist 52 | net = netlist(input_file, prefs=preferences) 53 | 54 | # Extract the components 55 | components = net.getInterestingComponents() 56 | 57 | # Check if complex variant processing is enabled 58 | if preferences.complexVariant: 59 | # Process the variant fields 60 | do_not_populate = [] 61 | for component in components: 62 | fields = component.getFieldNames() 63 | for field in fields: 64 | try: 65 | # Find fields used for variant 66 | [variant_name, field_name] = field.split(VARIANT_FIELD_SEPARATOR) 67 | except ValueError: 68 | [variant_name, field_name] = [field, ''] 69 | 70 | if variant_name.lower() in preferences.pcbConfig: 71 | # Variant exist for component 72 | variant_field_value = component.getField(field) 73 | 74 | # Process no loaded option 75 | if variant_field_value.lower() in DNF and not field_name: 76 | do_not_populate.append(component) 77 | break 78 | 79 | # Write variant value to target field 80 | component.setField(field_name, variant_field_value) 81 | 82 | # Process component dnp for specified variant 83 | if do_not_populate: 84 | updated_components = [] 85 | for component in components: 86 | keep = True 87 | for dnp in do_not_populate: 88 | # If component reference if found in dnp list: set for removal 89 | if component.getRef() == dnp.getRef(): 90 | keep = False 91 | break 92 | 93 | if keep: 94 | # Component not in dnp list 95 | updated_components.append(component) 96 | else: 97 | # Component found in dnp list 98 | do_not_populate.remove(component) 99 | 100 | # Finally update components list 101 | components = updated_components 102 | 103 | # Group the components 104 | groups = net.groupComponents(components) 105 | 106 | columns = ColumnList(preferences.corder) 107 | 108 | # Read out all available fields 109 | for g in groups: 110 | for f in g.fields: 111 | columns.AddColumn(f) 112 | 113 | # Don't add 'boards' column if only one board is specified 114 | if preferences.boards <= 1: 115 | columns.RemoveColumn(ColumnList.COL_GRP_BUILD_QUANTITY) 116 | debug.info("Removing:", ColumnList.COL_GRP_BUILD_QUANTITY) 117 | 118 | if output_file is None: 119 | output_file = input_file.replace(".xml", ".csv") 120 | 121 | output_name = os.path.basename(output_file) 122 | output_name, output_ext = os.path.splitext(output_name) 123 | 124 | # KiCad BOM dialog by default passes "%O" without an extension. Append our default 125 | if output_ext == "": 126 | output_ext = ".csv" 127 | debug.info("No extension supplied for output file - using .csv") 128 | elif output_ext not in [".xml", ".csv", ".txt", ".tsv", ".html", ".xlsx"]: 129 | output_ext = ".csv" 130 | debug.warning("Unknown extension '{e}' supplied - using .csv".format(e=output_ext)) 131 | 132 | # Make replacements to custom file_name. 133 | file_name = preferences.outputFileName 134 | 135 | file_name = file_name.replace("%O", output_name) 136 | file_name = file_name.replace("%v", net.getVersion()) 137 | 138 | if variant is not None: 139 | file_name = file_name.replace("%V", preferences.variantFileNameFormat) 140 | file_name = file_name.replace("%V", variant) 141 | else: 142 | file_name = file_name.replace("%V", "") 143 | 144 | output_file = os.path.join(output_dir, file_name + output_ext) 145 | output_file = os.path.abspath(output_file) 146 | 147 | debug.message("Saving BOM File:", output_file) 148 | 149 | return WriteBoM(output_file, groups, net, columns.columns, preferences) 150 | 151 | 152 | def main(): 153 | locale.setlocale(locale.LC_ALL, '') 154 | 155 | prog = 'KiBOM_CLI.py' 156 | if __name__ == '__main__': 157 | prog = "python -m kibom" 158 | parser = argparse.ArgumentParser(prog=prog, description="KiBOM Bill of Materials generator script") 159 | 160 | parser.add_argument("netlist", help='xml netlist file. Use "%%I" when running from within KiCad') 161 | 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"') 162 | parser.add_argument("-n", "--number", help="Number of boards to build (default = 1)", type=int, default=None) 163 | parser.add_argument("-v", "--verbose", help="Enable verbose output", action='count') 164 | parser.add_argument("-r", "--variant", help="Board variant(s), used to determine which components are output to the BoM. To specify multiple variants, with a BOM file exported for each variant, separate variants with the ';' (semicolon) character.", type=str, default=None) 165 | parser.add_argument("-d", "--subdirectory", help="Subdirectory within which to store the generated BoM files.", type=str, default=None) 166 | parser.add_argument("--cfg", help="BoM config file (script will try to use 'bom.ini' if not specified here)") 167 | parser.add_argument("-s", "--separator", help="CSV Separator (default ',')", type=str, default=None) 168 | parser.add_argument("-k", "--no-colon-sep", help="Don't use : as delimiter in the config file", action='store_true') 169 | parser.add_argument('--version', action='version', version="KiBOM Version: {v}".format(v=KIBOM_VERSION)) 170 | 171 | args = parser.parse_args() 172 | 173 | # Set the global debugging level 174 | debug.setDebugLevel(int(args.verbose) if args.verbose is not None else debug.MSG_ERROR) 175 | 176 | debug.message("KiBOM version {v}".format(v=KIBOM_VERSION)) 177 | 178 | input_file = os.path.abspath(args.netlist) 179 | 180 | input_dir = os.path.abspath(os.path.dirname(input_file)) 181 | 182 | output_file = os.path.basename(args.output) 183 | 184 | if args.subdirectory is not None: 185 | output_dir = args.subdirectory 186 | 187 | if not os.path.isabs(output_dir): 188 | output_dir = os.path.join(input_dir, output_dir) 189 | 190 | # Make the directory if it does not exist 191 | if not os.path.exists(output_dir): 192 | os.makedirs(output_dir) 193 | 194 | debug.message("Creating subdirectory: '{d}'".format(d=output_dir)) 195 | else: 196 | output_dir = os.path.abspath(os.path.dirname(input_file)) 197 | 198 | debug.message("Output directory: '{d}'".format(d=output_dir)) 199 | 200 | if not input_file.endswith(".xml"): 201 | debug.error("Input file '{f}' is not an xml file".format(f=input_file), fail=True) 202 | 203 | if not os.path.exists(input_file) or not os.path.isfile(input_file): 204 | debug.error("Input file '{f}' does not exist".format(f=input_file), fail=True) 205 | 206 | debug.message("Input:", input_file) 207 | 208 | # Look for a config file! 209 | # bom.ini by default 210 | ini = os.path.abspath(os.path.join(os.path.dirname(input_file), "bom.ini")) 211 | 212 | # Default value 213 | config_file = ini 214 | 215 | # User can overwrite with a specific config file 216 | if args.cfg: 217 | config_file = args.cfg 218 | 219 | # Read preferences from file. If file does not exists, default preferences will be used 220 | pref = BomPref() 221 | 222 | have_cfile = os.path.exists(config_file) 223 | 224 | if have_cfile: 225 | pref.Read(config_file, no_colon_sep=args.no_colon_sep) 226 | debug.message("Configuration file:", config_file) 227 | else: 228 | pref.Write(config_file) 229 | debug.info("Writing configuration file:", config_file) 230 | 231 | # Pass various command-line options through 232 | if args.number is not None: 233 | pref.boards = args.number 234 | 235 | pref.separatorCSV = args.separator 236 | 237 | if args.variant is not None: 238 | variants = args.variant.split(';') 239 | else: 240 | # Check if variants were defined in configuration 241 | if pref.pcbConfig != ['default']: 242 | variants = pref.pcbConfig 243 | else: 244 | variants = [None] 245 | 246 | # Generate BOMs for each specified variant 247 | for variant in variants: 248 | result = writeVariant(input_file, output_dir, output_file, variant, pref) 249 | if not result: 250 | debug.error("Error writing variant '{v}'".format(v=variant)) 251 | sys.exit(-1) 252 | 253 | sys.exit(debug.getErrorCount()) 254 | 255 | 256 | if __name__ == '__main__': 257 | main() 258 | -------------------------------------------------------------------------------- /kibom/netlist_reader.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | @package 5 | Generate a HTML BOM list. 6 | Components are sorted and grouped by value 7 | Any existing fields are read 8 | """ 9 | 10 | 11 | from __future__ import print_function 12 | import sys 13 | import os.path 14 | import xml.sax as sax 15 | 16 | from .component import Component, ComponentGroup 17 | from .preferences import BomPref 18 | from . import debug 19 | 20 | 21 | class xmlElement(): 22 | """xml element which can represent all nodes of the netlist tree. It can be 23 | used to easily generate various output formats by propogating format 24 | requests to children recursively. 25 | """ 26 | def __init__(self, name, parent=None): 27 | self.name = name 28 | self.attributes = {} 29 | self.parent = parent 30 | self.chars = "" 31 | self.children = [] 32 | 33 | def __str__(self): 34 | """String representation of this netlist element 35 | 36 | """ 37 | return self.name + "[" + self.chars + "]" + " attr_count:" + str(len(self.attributes)) 38 | 39 | def addAttribute(self, attr, value): 40 | """Add an attribute to this element""" 41 | self.attributes[attr] = value 42 | 43 | def setAttribute(self, attr, value): 44 | """Set an attributes value - in fact does the same thing as add 45 | attribute 46 | 47 | """ 48 | self.attributes[attr] = value 49 | 50 | def setChars(self, chars): 51 | """Set the characters for this element""" 52 | self.chars = chars 53 | 54 | def addChars(self, chars): 55 | """Add characters (textual value) to this element""" 56 | self.chars += chars 57 | 58 | def addChild(self, child): 59 | """Add a child element to this element""" 60 | self.children.append(child) 61 | return self.children[len(self.children) - 1] 62 | 63 | def getParent(self): 64 | """Get the parent of this element (Could be None)""" 65 | return self.parent 66 | 67 | def getChild(self, name): 68 | """Returns the first child element named 'name' 69 | 70 | Keywords: 71 | name -- The name of the child element to return""" 72 | for child in self.children: 73 | if child.name == name: 74 | return child 75 | return None 76 | 77 | def getChildren(self, name=None): 78 | if name: 79 | # return _all_ children named "name" 80 | ret = [] 81 | for child in self.children: 82 | if child.name == name: 83 | ret.append(child) 84 | return ret 85 | else: 86 | return self.children 87 | 88 | def get(self, elemName, attribute="", attrmatch=""): 89 | """Return the text data for either an attribute or an xmlElement 90 | """ 91 | if (self.name == elemName): 92 | if attribute != "": 93 | try: 94 | if attrmatch != "": 95 | if self.attributes[attribute] == attrmatch: 96 | return self.chars 97 | else: 98 | return self.attributes[attribute] 99 | except AttributeError: 100 | return "" 101 | else: 102 | return self.chars 103 | 104 | for child in self.children: 105 | ret = child.get(elemName, attribute, attrmatch) 106 | if ret != "": 107 | return ret 108 | 109 | return "" 110 | 111 | 112 | class libpart(): 113 | """Class for a library part, aka 'libpart' in the xml netlist file. 114 | (Components in eeschema are instantiated from library parts.) 115 | This part class is implemented by wrapping an xmlElement with accessors. 116 | This xmlElement instance is held in field 'element'. 117 | """ 118 | def __init__(self, xml_element): 119 | # 120 | self.element = xml_element 121 | 122 | def getLibName(self): 123 | return self.element.get("libpart", "lib") 124 | 125 | def getPartName(self): 126 | return self.element.get("libpart", "part") 127 | 128 | # For backwards Compatibility with v4.x only 129 | def getDescription(self): 130 | return self.element.get("description") 131 | 132 | def getDocs(self): 133 | return self.element.get("docs") 134 | 135 | def getField(self, name): 136 | return self.element.get("field", "name", name) 137 | 138 | def getFieldNames(self): 139 | """Return a list of field names in play for this libpart. 140 | """ 141 | fieldNames = [] 142 | fields = self.element.getChild('fields') 143 | if fields: 144 | for f in fields.getChildren(): 145 | fieldNames.append(f.get('field', 'name')) 146 | return fieldNames 147 | 148 | def getDatasheet(self): 149 | 150 | datasheet = self.getField("Datasheet") 151 | 152 | if not datasheet or datasheet == "": 153 | docs = self.getDocs() 154 | 155 | if "http" in docs or ".pdf" in docs: 156 | datasheet = docs 157 | 158 | return datasheet 159 | 160 | def getFootprint(self): 161 | return self.getField("Footprint") 162 | 163 | def getAliases(self): 164 | """Return a list of aliases or None""" 165 | aliases = self.element.getChild("aliases") 166 | if aliases: 167 | ret = [] 168 | children = aliases.getChildren() 169 | # grab the text out of each child: 170 | for child in children: 171 | ret.append(child.get("alias")) 172 | return ret 173 | return None 174 | 175 | 176 | class netlist(): 177 | """ KiCad generic netlist class. Generally loaded from a KiCad generic 178 | netlist file. Includes several helper functions to ease BOM creating 179 | scripts 180 | 181 | """ 182 | def __init__(self, fname="", prefs=None): 183 | """Initialiser for the genericNetlist class 184 | 185 | Keywords: 186 | fname -- The name of the generic netlist file to open (Optional) 187 | 188 | """ 189 | self.design = None 190 | self.components = [] 191 | self.libparts = [] 192 | self.libraries = [] 193 | self.nets = [] 194 | 195 | # The entire tree is loaded into self.tree 196 | self.tree = [] 197 | 198 | self._curr_element = None 199 | 200 | if not prefs: 201 | prefs = BomPref() # Default values 202 | 203 | self.prefs = prefs 204 | 205 | if fname != "": 206 | self.load(fname) 207 | 208 | def addChars(self, content): 209 | """Add characters to the current element""" 210 | self._curr_element.addChars(content) 211 | 212 | def addElement(self, name): 213 | """Add a new KiCad generic element to the list""" 214 | if self._curr_element is None: 215 | self.tree = xmlElement(name) 216 | self._curr_element = self.tree 217 | else: 218 | self._curr_element = self._curr_element.addChild( 219 | xmlElement(name, self._curr_element)) 220 | 221 | # If this element is a component, add it to the components list 222 | if self._curr_element.name == "comp": 223 | self.components.append(Component(self._curr_element, prefs=self.prefs)) 224 | 225 | # Assign the design element 226 | if self._curr_element.name == "design": 227 | self.design = self._curr_element 228 | 229 | # If this element is a library part, add it to the parts list 230 | if self._curr_element.name == "libpart": 231 | self.libparts.append(libpart(self._curr_element)) 232 | 233 | # If this element is a net, add it to the nets list 234 | if self._curr_element.name == "net": 235 | self.nets.append(self._curr_element) 236 | 237 | # If this element is a library, add it to the libraries list 238 | if self._curr_element.name == "library": 239 | self.libraries.append(self._curr_element) 240 | 241 | return self._curr_element 242 | 243 | def endDocument(self): 244 | """Called when the netlist document has been fully parsed""" 245 | # When the document is complete, the library parts must be linked to 246 | # the components as they are seperate in the tree so as not to 247 | # duplicate library part information for every component 248 | for c in self.components: 249 | for p in self.libparts: 250 | if p.getLibName() == c.getLibName(): 251 | if p.getPartName() == c.getPartName(): 252 | c.setLibPart(p) 253 | break 254 | else: 255 | aliases = p.getAliases() 256 | if aliases and self.aliasMatch(c.getPartName(), aliases): 257 | c.setLibPart(p) 258 | break 259 | 260 | if not c.getLibPart(): 261 | debug.warning('missing libpart for ref:', c.getRef(), c.getPartName(), c.getLibName()) 262 | 263 | def aliasMatch(self, partName, aliasList): 264 | for alias in aliasList: 265 | if partName == alias: 266 | return True 267 | return False 268 | 269 | def endElement(self): 270 | """End the current element and switch to its parent""" 271 | self._curr_element = self._curr_element.getParent() 272 | 273 | def getDate(self): 274 | """Return the date + time string generated by the tree creation tool""" 275 | if (sys.version_info[0] >= 3): 276 | return self.design.get("date") 277 | else: 278 | return self.design.get("date").encode('ascii', 'ignore') 279 | 280 | def getSource(self): 281 | 282 | path = self.design.get("source").replace("\\", "/") 283 | path = os.path.basename(path) 284 | 285 | """Return the source string for the design""" 286 | if (sys.version_info[0] >= 3): 287 | return path 288 | else: 289 | return path.encode('ascii', 'ignore') 290 | 291 | def getTool(self): 292 | """Return the tool string which was used to create the netlist tree""" 293 | if (sys.version_info[0] >= 3): 294 | return self.design.get("tool") 295 | else: 296 | return self.design.get("tool").encode('ascii', 'ignore') 297 | 298 | def getSheet(self): 299 | return self.design.getChild("sheet") 300 | 301 | def getSheetDate(self): 302 | sheet = self.getSheet() 303 | if sheet is None: 304 | return "" 305 | return sheet.get("date") 306 | 307 | def getVersion(self): 308 | """Return the verison of the sheet info""" 309 | 310 | sheet = self.getSheet() 311 | 312 | if sheet is None: 313 | return "" 314 | 315 | return sheet.get("rev") 316 | 317 | def getInterestingComponents(self): 318 | 319 | # Copy out the components 320 | ret = [c for c in self.components] 321 | 322 | # Sort first by ref as this makes for easier to read BOM's 323 | ret.sort(key=lambda g: g.getRef()) 324 | 325 | return ret 326 | 327 | def groupComponents(self, components): 328 | 329 | groups = [] 330 | 331 | # Iterate through each component, and test whether a group for these already exists 332 | for c in components: 333 | 334 | if self.prefs.useRegex: 335 | # Skip components if they do not meet regex requirements 336 | if not c.testRegInclude(): 337 | continue 338 | if c.testRegExclude(): 339 | continue 340 | 341 | found = False 342 | 343 | for g in groups: 344 | if g.matchComponent(c): 345 | g.addComponent(c) 346 | found = True 347 | break 348 | 349 | if not found: 350 | g = ComponentGroup(prefs=self.prefs) # Pass down the preferences 351 | g.addComponent(c) 352 | groups.append(g) 353 | 354 | # Sort the references within each group 355 | for g in groups: 356 | g.sortComponents() 357 | g.updateFields(self.prefs.useAlt) 358 | 359 | # Sort the groups 360 | # First priority is the Type of component (e.g. R?, U?, L?) 361 | groups = sorted(groups, key=lambda g: [g.components[0].getPrefix(), g.components[0].getValueSort()]) 362 | 363 | return groups 364 | 365 | def load(self, fname): 366 | """Load a KiCad generic netlist 367 | 368 | Keywords: 369 | fname -- The name of the generic netlist file to open 370 | 371 | """ 372 | try: 373 | self._reader = sax.make_parser() 374 | self._reader.setContentHandler(_gNetReader(self)) 375 | self._reader.parse(fname) 376 | except IOError as e: 377 | debug.error(__file__, ":", e) 378 | sys.exit(-1) 379 | 380 | 381 | class _gNetReader(sax.handler.ContentHandler): 382 | """SAX KiCad generic netlist content handler - passes most of the work back 383 | to the 'netlist' class which builds a complete tree in RAM for the design 384 | 385 | """ 386 | def __init__(self, aParent): 387 | self.parent = aParent 388 | 389 | def startElement(self, name, attrs): 390 | """Start of a new XML element event""" 391 | element = self.parent.addElement(name) 392 | 393 | for name in attrs.getNames(): 394 | element.addAttribute(name, attrs.getValue(name)) 395 | 396 | def endElement(self, name): 397 | self.parent.endElement() 398 | 399 | def characters(self, content): 400 | # Ignore erroneous white space - ignoreableWhitespace does not get rid 401 | # of the need for this! 402 | if not content.isspace(): 403 | self.parent.addChars(content) 404 | 405 | def endDocument(self): 406 | """End of the XML document event""" 407 | self.parent.endDocument() 408 | -------------------------------------------------------------------------------- /kibom/preferences.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import sys 5 | import re 6 | import os 7 | 8 | from .columns import ColumnList 9 | from . import debug 10 | 11 | # Check python version to determine which version of ConfirParser to import 12 | if sys.version_info.major >= 3: 13 | import configparser as ConfigParser 14 | else: 15 | import ConfigParser 16 | 17 | 18 | class BomPref: 19 | 20 | SECTION_IGNORE = "IGNORE_COLUMNS" 21 | SECTION_COLUMN_ORDER = "COLUMN_ORDER" 22 | SECTION_GENERAL = "BOM_OPTIONS" 23 | SECTION_ALIASES = "COMPONENT_ALIASES" 24 | SECTION_GROUPING_FIELDS = "GROUP_FIELDS" 25 | SECTION_REGEXCLUDES = "REGEX_EXCLUDE" 26 | SECTION_REGINCLUDES = "REGEX_INCLUDE" 27 | SECTION_JOIN = "JOIN" # (#81) 28 | SECTION_COLUMN_RENAME = "COLUMN_RENAME" 29 | 30 | OPT_DIGIKEY_LINK = "digikey_link" 31 | OPT_MOUSER_LINK = "mouser_link" 32 | OPT_LCSC_LINK = "lcsc_link" 33 | OPT_PCB_CONFIG = "pcb_configuration" 34 | OPT_NUMBER_ROWS = "number_rows" 35 | OPT_GROUP_CONN = "group_connectors" 36 | OPT_USE_REGEX = "test_regex" 37 | OPT_USE_ALT = "use_alt" 38 | OPT_MERGE_BLANK = "merge_blank_fields" 39 | OPT_IGNORE_DNF = "ignore_dnf" 40 | OPT_GENERATE_DNF = "html_generate_dnf" 41 | OPT_BACKUP = "make_backup" 42 | OPT_OUTPUT_FILE_NAME = "output_file_name" 43 | OPT_VARIANT_FILE_NAME_FORMAT = "variant_file_name_format" 44 | OPT_DEFAULT_BOARDS = "number_boards" 45 | OPT_DEFAULT_PCBCONFIG = "board_variant" 46 | OPT_CONFIG_FIELD = "fit_field" 47 | OPT_COMPLEX_VARIANT = "complex_variant" 48 | OPT_HIDE_HEADERS = "hide_headers" 49 | OPT_HIDE_PCB_INFO = "hide_pcb_info" 50 | OPT_REF_SEPARATOR = "ref_separator" 51 | OPT_DATASHEET_AS_LINK = "datasheet_as_link" 52 | 53 | def __init__(self): 54 | # List of headings to ignore in BoM generation 55 | self.ignore = [ 56 | ColumnList.COL_PART_LIB.lower(), 57 | ColumnList.COL_FP_LIB.lower(), 58 | ColumnList.COL_SHEETPATH.lower(), 59 | ] 60 | 61 | self.corder = ColumnList._COLUMNS_DEFAULT 62 | self.useAlt = False # Use alternate reference representation 63 | self.ignoreDNF = True # Ignore rows for do-not-fit parts 64 | self.generateDNF = True # Generate a list of do-not-fit parts 65 | self.numberRows = True # Add row-numbers to BoM output 66 | self.groupConnectors = True # Group connectors and ignore component value 67 | self.useRegex = True # Test various columns with regex 68 | 69 | self.digikey_link = False # Columns to link to Digi-Key 70 | self.mouser_link = False # Columns to link to Mouser (requires Mouser-PartNO) 71 | self.lcsc_link = False # Columns to link to LCSC 72 | self.boards = 1 # Quantity of boards to be made 73 | self.mergeBlankFields = True # Blanks fields will be merged when possible 74 | self.hideHeaders = False 75 | self.hidePcbInfo = False 76 | self.configField = "Config" # Default field used for part fitting config 77 | self.pcbConfig = ["default"] 78 | self.complexVariant = False # To enable complex variant processing 79 | self.refSeparator = " " 80 | 81 | self.backup = "%O.tmp" 82 | self.as_link = False 83 | 84 | self.separatorCSV = None 85 | self.outputFileName = "%O_bom_%v%V" 86 | self.variantFileNameFormat = "_(%V)" 87 | 88 | # Default fields used to group components 89 | self.groups = [ 90 | ColumnList.COL_PART, 91 | ColumnList.COL_PART_LIB, 92 | ColumnList.COL_VALUE, 93 | ColumnList.COL_FP, 94 | ColumnList.COL_FP_LIB, 95 | # User can add custom grouping columns in bom.ini 96 | ] 97 | 98 | self.colRename = {} # None by default 99 | 100 | self.regIncludes = [] # None by default 101 | 102 | self.regExcludes = [ 103 | [ColumnList.COL_REFERENCE, '^TP[0-9]*'], 104 | [ColumnList.COL_REFERENCE, '^FID'], 105 | [ColumnList.COL_PART, 'mount.*hole'], 106 | [ColumnList.COL_PART, 'solder.*bridge'], 107 | [ColumnList.COL_PART, 'test.*point'], 108 | [ColumnList.COL_FP, 'test.*point'], 109 | [ColumnList.COL_FP, 'mount.*hole'], 110 | [ColumnList.COL_FP, 'fiducial'], 111 | ] 112 | 113 | # Default component groupings 114 | self.aliases = [ 115 | ["c", "c_small", "cap", "capacitor"], 116 | ["r", "r_small", "res", "resistor"], 117 | ["sw", "switch"], 118 | ["l", "l_small", "inductor"], 119 | ["zener", "zenersmall"], 120 | ["d", "diode", "d_small"] 121 | ] 122 | 123 | # Nothing to join by default (#81) 124 | self.join = [] 125 | 126 | # Check an option within the SECTION_GENERAL group 127 | def checkOption(self, parser, opt, default=False): 128 | if parser.has_option(self.SECTION_GENERAL, opt): 129 | return parser.get(self.SECTION_GENERAL, opt).lower() in ["1", "true", "yes"] 130 | else: 131 | return default 132 | 133 | def checkInt(self, parser, opt, default=False): 134 | if parser.has_option(self.SECTION_GENERAL, opt): 135 | return int(parser.get(self.SECTION_GENERAL, opt).lower()) 136 | else: 137 | return default 138 | 139 | def checkStr(self, opt, default=False): 140 | if self.parser.has_option(self.SECTION_GENERAL, opt): 141 | return self.parser.get(self.SECTION_GENERAL, opt) 142 | else: 143 | return default 144 | 145 | # Read KiBOM preferences from file 146 | def Read(self, file, verbose=False, no_colon_sep=False): 147 | file = os.path.abspath(file) 148 | if not os.path.exists(file) or not os.path.isfile(file): 149 | debug.error("{f} is not a valid file!".format(f=file)) 150 | return 151 | 152 | cf = ConfigParser.RawConfigParser(allow_no_value=True, delimiters=('=') if no_colon_sep else ('=', ':')) 153 | self.parser = cf 154 | cf.optionxform = str 155 | 156 | cf.read(file) 157 | 158 | # Read general options 159 | if self.SECTION_GENERAL in cf.sections(): 160 | self.ignoreDNF = self.checkOption(cf, self.OPT_IGNORE_DNF, default=True) 161 | self.generateDNF = self.checkOption(cf, self.OPT_GENERATE_DNF, default=True) 162 | self.useAlt = self.checkOption(cf, self.OPT_USE_ALT, default=False) 163 | self.numberRows = self.checkOption(cf, self.OPT_NUMBER_ROWS, default=True) 164 | self.groupConnectors = self.checkOption(cf, self.OPT_GROUP_CONN, default=True) 165 | self.useRegex = self.checkOption(cf, self.OPT_USE_REGEX, default=True) 166 | self.mergeBlankFields = self.checkOption(cf, self.OPT_MERGE_BLANK, default=True) 167 | self.outputFileName = self.checkStr(self.OPT_OUTPUT_FILE_NAME, default=self.outputFileName) 168 | self.variantFileNameFormat = self.checkStr(self.OPT_VARIANT_FILE_NAME_FORMAT, 169 | default=self.variantFileNameFormat) 170 | self.refSeparator = self.checkStr(self.OPT_REF_SEPARATOR, default=self.refSeparator).strip("\'\"") 171 | 172 | if cf.has_option(self.SECTION_GENERAL, self.OPT_CONFIG_FIELD): 173 | self.configField = cf.get(self.SECTION_GENERAL, self.OPT_CONFIG_FIELD) 174 | 175 | if cf.has_option(self.SECTION_GENERAL, self.OPT_COMPLEX_VARIANT): 176 | self.complexVariant = self.checkOption(cf, self.OPT_COMPLEX_VARIANT, default=False) 177 | 178 | if cf.has_option(self.SECTION_GENERAL, self.OPT_DEFAULT_BOARDS): 179 | self.boards = self.checkInt(cf, self.OPT_DEFAULT_BOARDS, default=None) 180 | 181 | if cf.has_option(self.SECTION_GENERAL, self.OPT_DEFAULT_PCBCONFIG): 182 | char_remove = [' ', '[', ']', '\'', '"'] 183 | self.pcbConfig = cf.get(self.SECTION_GENERAL, self.OPT_DEFAULT_PCBCONFIG) 184 | for char in char_remove: 185 | self.pcbConfig = self.pcbConfig.replace(char, '') 186 | self.pcbConfig = self.pcbConfig.split(",") 187 | 188 | if cf.has_option(self.SECTION_GENERAL, self.OPT_BACKUP): 189 | self.backup = cf.get(self.SECTION_GENERAL, self.OPT_BACKUP) 190 | else: 191 | self.backup = False 192 | 193 | if cf.has_option(self.SECTION_GENERAL, self.OPT_DATASHEET_AS_LINK): 194 | self.as_link = cf.get(self.SECTION_GENERAL, self.OPT_DATASHEET_AS_LINK) 195 | else: 196 | self.as_link = False 197 | 198 | if cf.has_option(self.SECTION_GENERAL, self.OPT_HIDE_HEADERS): 199 | self.hideHeaders = cf.get(self.SECTION_GENERAL, self.OPT_HIDE_HEADERS) == '1' 200 | 201 | if cf.has_option(self.SECTION_GENERAL, self.OPT_HIDE_PCB_INFO): 202 | self.hidePcbInfo = cf.get(self.SECTION_GENERAL, self.OPT_HIDE_PCB_INFO) == '1' 203 | 204 | if cf.has_option(self.SECTION_GENERAL, self.OPT_DIGIKEY_LINK): 205 | self.digikey_link = cf.get(self.SECTION_GENERAL, self.OPT_DIGIKEY_LINK) 206 | else: 207 | self.digikey_link = False 208 | 209 | if cf.has_option(self.SECTION_GENERAL, self.OPT_MOUSER_LINK): 210 | self.mouser_link = cf.get(self.SECTION_GENERAL, self.OPT_MOUSER_LINK) 211 | else: 212 | self.mouser_link = False 213 | 214 | if cf.has_option(self.SECTION_GENERAL, self.OPT_LCSC_LINK): 215 | self.lcsc_link = cf.get(self.SECTION_GENERAL, self.OPT_LCSC_LINK) 216 | else: 217 | self.lcsc_link = False 218 | 219 | # Read out grouping colums 220 | if self.SECTION_GROUPING_FIELDS in cf.sections(): 221 | self.groups = [i for i in cf.options(self.SECTION_GROUPING_FIELDS)] 222 | 223 | # Read out ignored-rows 224 | if self.SECTION_IGNORE in cf.sections(): 225 | self.ignore = [i.lower() for i in cf.options(self.SECTION_IGNORE)] 226 | 227 | # Read out column order 228 | if self.SECTION_COLUMN_ORDER in cf.sections(): 229 | self.corder = [i for i in cf.options(self.SECTION_COLUMN_ORDER)] 230 | 231 | # Read out component aliases 232 | if self.SECTION_ALIASES in cf.sections(): 233 | self.aliases = [re.split('[ \t]+', a) for a in cf.options(self.SECTION_ALIASES)] 234 | 235 | # Read out join rules (#81) 236 | if self.SECTION_JOIN in cf.sections(): 237 | self.join = [a.split('\t') for a in cf.options(self.SECTION_JOIN)] 238 | 239 | if self.SECTION_REGEXCLUDES in cf.sections(): 240 | self.regExcludes = [] 241 | for pair in cf.options(self.SECTION_REGEXCLUDES): 242 | if len(re.split('[ \t]+', pair)) == 2: 243 | self.regExcludes.append(re.split('[ \t]+', pair)) 244 | 245 | if self.SECTION_REGINCLUDES in cf.sections(): 246 | self.regIncludes = [] 247 | for pair in cf.options(self.SECTION_REGINCLUDES): 248 | if len(re.split('[ \t]+', pair)) == 2: 249 | self.regIncludes.append(re.split('[ \t]+', pair)) 250 | 251 | if self.SECTION_COLUMN_RENAME in cf.sections(): 252 | self.colRename = {} 253 | for pair in cf.options(self.SECTION_COLUMN_RENAME): 254 | pair = re.split('\t', pair) 255 | if len(pair) == 2: 256 | self.colRename[pair[0].lower()] = pair[1] 257 | 258 | # Add an option to the SECTION_GENRAL group 259 | def addOption(self, parser, opt, value, comment=None): 260 | if comment: 261 | if not comment.startswith(";"): 262 | comment = "; " + comment 263 | parser.set(self.SECTION_GENERAL, comment) 264 | parser.set(self.SECTION_GENERAL, opt, "1" if value else "0") 265 | 266 | # Write KiBOM preferences to file 267 | def Write(self, file): 268 | file = os.path.abspath(file) 269 | 270 | cf = ConfigParser.RawConfigParser(allow_no_value=True) 271 | cf.optionxform = str 272 | 273 | cf.add_section(self.SECTION_GENERAL) 274 | cf.set(self.SECTION_GENERAL, "; General BoM options here") 275 | 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)) 276 | self.addOption(cf, self.OPT_GENERATE_DNF, self.generateDNF, comment="If '{opt}' option is set to 1, also generate a list of components not fitted on the PCB (HTML only)".format(opt=self.OPT_GENERATE_DNF)) 277 | 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)) 278 | 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)) 279 | 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)) 280 | 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)) 281 | 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)) 282 | 283 | 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'.") 284 | cf.set(self.SECTION_GENERAL, self.OPT_OUTPUT_FILE_NAME, self.outputFileName) 285 | 286 | 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.") 287 | cf.set(self.SECTION_GENERAL, self.OPT_VARIANT_FILE_NAME_FORMAT, self.variantFileNameFormat) 288 | 289 | cf.set(self.SECTION_GENERAL, '; Field name used to determine if a particular part is to be fitted') 290 | cf.set(self.SECTION_GENERAL, self.OPT_CONFIG_FIELD, self.configField) 291 | 292 | cf.set(self.SECTION_GENERAL, '; Complex variant processing (disabled by default)') 293 | cf.set(self.SECTION_GENERAL, self.OPT_COMPLEX_VARIANT, self.complexVariant) 294 | 295 | cf.set(self.SECTION_GENERAL, '; Character used to separate reference designators in output') 296 | cf.set(self.SECTION_GENERAL, self.OPT_REF_SEPARATOR, "'" + self.refSeparator + "'") 297 | 298 | cf.set(self.SECTION_GENERAL, '; Make a backup of the bom before generating the new one, using the following template') 299 | cf.set(self.SECTION_GENERAL, self.OPT_BACKUP, self.backup) 300 | 301 | cf.set(self.SECTION_GENERAL, '; Put the datasheet as a link for the following field') 302 | cf.set(self.SECTION_GENERAL, self.OPT_DATASHEET_AS_LINK, self.as_link) 303 | 304 | cf.set(self.SECTION_GENERAL, '; Default number of boards to produce if none given on CLI with -n') 305 | cf.set(self.SECTION_GENERAL, self.OPT_DEFAULT_BOARDS, self.boards) 306 | 307 | cf.set(self.SECTION_GENERAL, '; Default PCB variant if none given on CLI with -r') 308 | cf.set(self.SECTION_GENERAL, self.OPT_DEFAULT_PCBCONFIG, self.pcbConfig) 309 | 310 | cf.set(self.SECTION_GENERAL, '; Whether to hide headers from output file') 311 | cf.set(self.SECTION_GENERAL, self.OPT_HIDE_HEADERS, self.hideHeaders) 312 | 313 | cf.set(self.SECTION_GENERAL, '; Whether to hide PCB info from output file') 314 | cf.set(self.SECTION_GENERAL, self.OPT_HIDE_PCB_INFO, self.hidePcbInfo) 315 | 316 | cf.set(self.SECTION_GENERAL, '; Interpret as a Digikey P/N and link the following field') 317 | cf.set(self.SECTION_GENERAL, self.OPT_DIGIKEY_LINK, self.digikey_link) 318 | 319 | cf.set(self.SECTION_GENERAL, '; Interpret as a MOUSER P/N and link the following field') 320 | cf.set(self.SECTION_GENERAL, self.OPT_MOUSER_LINK, self.mouser_link) 321 | 322 | cf.set(self.SECTION_GENERAL, '; Interpret as a LCSC P/N and link the following field') 323 | cf.set(self.SECTION_GENERAL, self.OPT_LCSC_LINK, self.lcsc_link) 324 | 325 | cf.add_section(self.SECTION_IGNORE) 326 | cf.set(self.SECTION_IGNORE, "; Any column heading that appears here will be excluded from the Generated BoM") 327 | cf.set(self.SECTION_IGNORE, "; Titles are case-insensitive") 328 | 329 | for i in self.ignore: 330 | cf.set(self.SECTION_IGNORE, i) 331 | 332 | cf.add_section(self.SECTION_COLUMN_ORDER) 333 | cf.set(self.SECTION_COLUMN_ORDER, "; Columns will apear in the order they are listed here") 334 | cf.set(self.SECTION_COLUMN_ORDER, "; Titles are case-insensitive") 335 | 336 | for i in self.corder: 337 | cf.set(self.SECTION_COLUMN_ORDER, i) 338 | 339 | # Write the component grouping fields 340 | cf.add_section(self.SECTION_GROUPING_FIELDS) 341 | cf.set(self.SECTION_GROUPING_FIELDS, '; List of fields used for sorting individual components into groups') 342 | cf.set(self.SECTION_GROUPING_FIELDS, '; Components which match (comparing *all* fields) will be grouped together') 343 | cf.set(self.SECTION_GROUPING_FIELDS, '; Field names are case-insensitive') 344 | 345 | for i in self.groups: 346 | cf.set(self.SECTION_GROUPING_FIELDS, i) 347 | 348 | cf.add_section(self.SECTION_ALIASES) 349 | cf.set(self.SECTION_ALIASES, "; A series of values which are considered to be equivalent for the part name") 350 | cf.set(self.SECTION_ALIASES, "; Each line represents a list of equivalent component name values separated by a tab character") 351 | cf.set(self.SECTION_ALIASES, "; e.g. 'c\tc_small\tcap' will ensure the equivalent capacitor symbols can be grouped together") 352 | cf.set(self.SECTION_ALIASES, '; Aliases are case-insensitive') 353 | 354 | for a in self.aliases: 355 | cf.set(self.SECTION_ALIASES, "\t".join(a)) 356 | 357 | # (#81) 358 | cf.add_section(self.SECTION_JOIN) 359 | cf.set(self.SECTION_JOIN, '; A list of rules to join the content of fields') 360 | cf.set(self.SECTION_JOIN, '; Each line is a rule, the first name is the field that will receive the data') 361 | cf.set(self.SECTION_JOIN, '; from the other fields') 362 | cf.set(self.SECTION_JOIN, '; Use tab (ASCII 9) as separator') 363 | cf.set(self.SECTION_JOIN, '; Field names are case sensitive') 364 | 365 | for a in self.join: 366 | cf.set(self.SECTION_JOIN, "\t".join(a)) 367 | 368 | cf.add_section(self.SECTION_REGINCLUDES) 369 | cf.set(self.SECTION_REGINCLUDES, '; A series of regular expressions used to include parts in the BoM') 370 | 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') 371 | cf.set(self.SECTION_REGINCLUDES, '; Column names are case-insensitive') 372 | cf.set(self.SECTION_REGINCLUDES, '; Format is: "[ColumName] [Regex]" (separated by a tab)') 373 | 374 | for i in self.regIncludes: 375 | if not len(i) == 2: 376 | continue 377 | cf.set(self.SECTION_REGINCLUDES, i[0] + "\t" + i[1]) 378 | 379 | cf.add_section(self.SECTION_COLUMN_RENAME) 380 | cf.set(self.SECTION_COLUMN_RENAME, '; A list of columns to be renamed') 381 | cf.set(self.SECTION_COLUMN_RENAME, '; Format is: "[ColumName] [NewName]" (separated by a tab)') 382 | 383 | for k, v in self.colRename.items(): 384 | cf.set(self.SECTION_COLUMN_RENAME, k + "\t" + v) 385 | 386 | cf.add_section(self.SECTION_REGEXCLUDES) 387 | cf.set(self.SECTION_REGEXCLUDES, '; A series of regular expressions used to exclude parts from the BoM') 388 | cf.set(self.SECTION_REGEXCLUDES, '; If a component matches ANY of these, it will be excluded from the BoM') 389 | cf.set(self.SECTION_REGEXCLUDES, '; Important: the [' + self.SECTION_REGINCLUDES + '] section has more precedence') 390 | cf.set(self.SECTION_REGEXCLUDES, '; Column names are case-insensitive') 391 | cf.set(self.SECTION_REGEXCLUDES, '; Format is: "[ColumName] [Regex]" (separated by a tab') 392 | 393 | for i in self.regExcludes: 394 | if not len(i) == 2: 395 | continue 396 | 397 | cf.set(self.SECTION_REGEXCLUDES, i[0] + "\t" + i[1]) 398 | 399 | with open(file, 'w') as configfile: 400 | cf.write(configfile) 401 | -------------------------------------------------------------------------------- /test/fp-info-cache: -------------------------------------------------------------------------------- 1 | 158334035666645 2 | Capacitor_SMD 3 | CP_Elec_3x5.3 4 | SMT capacitor, aluminium electrolytic, 3x5.3, Cornell Dubilier Electronics 5 | Capacitor Electrolytic 6 | 0 7 | 2 8 | 2 9 | Capacitor_SMD 10 | CP_Elec_3x5.4 11 | SMD capacitor, aluminum electrolytic, Nichicon, 3.0x5.4mm 12 | capacitor electrolytic 13 | 0 14 | 2 15 | 2 16 | Capacitor_SMD 17 | CP_Elec_4x3 18 | SMD capacitor, aluminum electrolytic, Nichicon, 4.0x3mm 19 | capacitor electrolytic 20 | 0 21 | 2 22 | 2 23 | Capacitor_SMD 24 | CP_Elec_4x3.9 25 | SMD capacitor, aluminum electrolytic, Nichicon, 4.0x3.9mm 26 | capacitor electrolytic 27 | 0 28 | 2 29 | 2 30 | Capacitor_SMD 31 | CP_Elec_4x4.5 32 | SMD capacitor, aluminum electrolytic, Nichicon, 4.0x4.5mm 33 | capacitor electrolytic 34 | 0 35 | 2 36 | 2 37 | Capacitor_SMD 38 | CP_Elec_4x5.3 39 | SMD capacitor, aluminum electrolytic, Vishay, 4.0x5.3mm 40 | capacitor electrolytic 41 | 0 42 | 2 43 | 2 44 | Capacitor_SMD 45 | CP_Elec_4x5.4 46 | SMD capacitor, aluminum electrolytic, Panasonic A5 / Nichicon, 4.0x5.4mm 47 | capacitor electrolytic 48 | 0 49 | 2 50 | 2 51 | Capacitor_SMD 52 | CP_Elec_4x5.7 53 | SMD capacitor, aluminum electrolytic, United Chemi-Con, 4.0x5.7mm 54 | capacitor electrolytic 55 | 0 56 | 2 57 | 2 58 | Capacitor_SMD 59 | CP_Elec_4x5.8 60 | SMD capacitor, aluminum electrolytic, Panasonic, 4.0x5.8mm 61 | capacitor electrolytic 62 | 0 63 | 2 64 | 2 65 | Capacitor_SMD 66 | CP_Elec_5x3 67 | SMD capacitor, aluminum electrolytic, Nichicon, 5.0x3.0mm 68 | capacitor electrolytic 69 | 0 70 | 2 71 | 2 72 | Capacitor_SMD 73 | CP_Elec_5x3.9 74 | SMD capacitor, aluminum electrolytic, Nichicon, 5.0x3.9mm 75 | capacitor electrolytic 76 | 0 77 | 2 78 | 2 79 | Capacitor_SMD 80 | CP_Elec_5x4.4 81 | SMD capacitor, aluminum electrolytic, Panasonic B45, 5.0x4.4mm 82 | capacitor electrolytic 83 | 0 84 | 2 85 | 2 86 | Capacitor_SMD 87 | CP_Elec_5x4.5 88 | SMD capacitor, aluminum electrolytic, Nichicon, 5.0x4.5mm 89 | capacitor electrolytic 90 | 0 91 | 2 92 | 2 93 | Capacitor_SMD 94 | CP_Elec_5x5.3 95 | SMD capacitor, aluminum electrolytic, Nichicon, 5.0x5.3mm 96 | capacitor electrolytic 97 | 0 98 | 2 99 | 2 100 | Capacitor_SMD 101 | CP_Elec_5x5.4 102 | SMD capacitor, aluminum electrolytic, Nichicon, 5.0x5.4mm 103 | capacitor electrolytic 104 | 0 105 | 2 106 | 2 107 | Capacitor_SMD 108 | CP_Elec_5x5.7 109 | SMD capacitor, aluminum electrolytic, United Chemi-Con, 5.0x5.7mm 110 | capacitor electrolytic 111 | 0 112 | 2 113 | 2 114 | Capacitor_SMD 115 | CP_Elec_5x5.8 116 | SMD capacitor, aluminum electrolytic, Panasonic, 5.0x5.8mm 117 | capacitor electrolytic 118 | 0 119 | 2 120 | 2 121 | Capacitor_SMD 122 | CP_Elec_5x5.9 123 | SMD capacitor, aluminum electrolytic, Panasonic B6, 5.0x5.9mm 124 | capacitor electrolytic 125 | 0 126 | 2 127 | 2 128 | Capacitor_SMD 129 | CP_Elec_6.3x3 130 | SMD capacitor, aluminum electrolytic, Nichicon, 6.3x3.0mm 131 | capacitor electrolytic 132 | 0 133 | 2 134 | 2 135 | Capacitor_SMD 136 | CP_Elec_6.3x3.9 137 | SMD capacitor, aluminum electrolytic, Nichicon, 6.3x3.9mm 138 | capacitor electrolytic 139 | 0 140 | 2 141 | 2 142 | Capacitor_SMD 143 | CP_Elec_6.3x4.5 144 | SMD capacitor, aluminum electrolytic, Nichicon, 6.3x4.5mm 145 | capacitor electrolytic 146 | 0 147 | 2 148 | 2 149 | Capacitor_SMD 150 | CP_Elec_6.3x4.9 151 | SMD capacitor, aluminum electrolytic, Panasonic C5, 6.3x4.9mm 152 | capacitor electrolytic 153 | 0 154 | 2 155 | 2 156 | Capacitor_SMD 157 | CP_Elec_6.3x5.2 158 | SMD capacitor, aluminum electrolytic, United Chemi-Con, 6.3x5.2mm 159 | capacitor electrolytic 160 | 0 161 | 2 162 | 2 163 | Capacitor_SMD 164 | CP_Elec_6.3x5.3 165 | SMD capacitor, aluminum electrolytic, Cornell Dubilier, 6.3x5.3mm 166 | capacitor electrolytic 167 | 0 168 | 2 169 | 2 170 | Capacitor_SMD 171 | CP_Elec_6.3x5.4 172 | SMD capacitor, aluminum electrolytic, Panasonic C55, 6.3x5.4mm 173 | capacitor electrolytic 174 | 0 175 | 2 176 | 2 177 | Capacitor_SMD 178 | CP_Elec_6.3x5.4_Nichicon 179 | SMD capacitor, aluminum electrolytic, Nichicon, 6.3x5.4mm 180 | capacitor electrolytic 181 | 0 182 | 2 183 | 2 184 | Capacitor_SMD 185 | CP_Elec_6.3x5.7 186 | SMD capacitor, aluminum electrolytic, United Chemi-Con, 6.3x5.7mm 187 | capacitor electrolytic 188 | 0 189 | 2 190 | 2 191 | Capacitor_SMD 192 | CP_Elec_6.3x5.8 193 | SMD capacitor, aluminum electrolytic, Nichicon, 6.3x5.8mm 194 | capacitor electrolytic 195 | 0 196 | 2 197 | 2 198 | Capacitor_SMD 199 | CP_Elec_6.3x5.9 200 | SMD capacitor, aluminum electrolytic, Panasonic C6, 6.3x5.9mm 201 | capacitor electrolytic 202 | 0 203 | 2 204 | 2 205 | Capacitor_SMD 206 | CP_Elec_6.3x7.7 207 | SMD capacitor, aluminum electrolytic, Nichicon, 6.3x7.7mm 208 | capacitor electrolytic 209 | 0 210 | 2 211 | 2 212 | Capacitor_SMD 213 | CP_Elec_6.3x9.9 214 | SMD capacitor, aluminum electrolytic, Panasonic C10, 6.3x9.9mm 215 | capacitor electrolytic 216 | 0 217 | 2 218 | 2 219 | Capacitor_SMD 220 | CP_Elec_8x5.4 221 | SMD capacitor, aluminum electrolytic, Nichicon, 8.0x5.4mm 222 | capacitor electrolytic 223 | 0 224 | 2 225 | 2 226 | Capacitor_SMD 227 | CP_Elec_8x6.2 228 | SMD capacitor, aluminum electrolytic, Nichicon, 8.0x6.2mm 229 | capacitor electrolytic 230 | 0 231 | 2 232 | 2 233 | Capacitor_SMD 234 | CP_Elec_8x6.5 235 | SMD capacitor, aluminum electrolytic, Rubycon, 8.0x6.5mm 236 | capacitor electrolytic 237 | 0 238 | 2 239 | 2 240 | Capacitor_SMD 241 | CP_Elec_8x6.7 242 | SMD capacitor, aluminum electrolytic, United Chemi-Con, 8.0x6.7mm 243 | capacitor electrolytic 244 | 0 245 | 2 246 | 2 247 | Capacitor_SMD 248 | CP_Elec_8x6.9 249 | SMD capacitor, aluminum electrolytic, Panasonic E7, 8.0x6.9mm 250 | capacitor electrolytic 251 | 0 252 | 2 253 | 2 254 | Capacitor_SMD 255 | CP_Elec_8x10 256 | SMD capacitor, aluminum electrolytic, Nichicon, 8.0x10mm 257 | capacitor electrolytic 258 | 0 259 | 2 260 | 2 261 | Capacitor_SMD 262 | CP_Elec_8x10.5 263 | SMD capacitor, aluminum electrolytic, Vishay 0810, 8.0x10.5mm, http://www.vishay.com/docs/28395/150crz.pdf 264 | capacitor electrolytic 265 | 0 266 | 2 267 | 2 268 | Capacitor_SMD 269 | CP_Elec_8x11.9 270 | SMD capacitor, aluminum electrolytic, Panasonic E12, 8.0x11.9mm 271 | capacitor electrolytic 272 | 0 273 | 2 274 | 2 275 | Capacitor_SMD 276 | CP_Elec_10x7.7 277 | SMD capacitor, aluminum electrolytic, Nichicon, 10.0x7.7mm 278 | capacitor electrolytic 279 | 0 280 | 2 281 | 2 282 | Capacitor_SMD 283 | CP_Elec_10x7.9 284 | SMD capacitor, aluminum electrolytic, Panasonic F8, 10.0x7.9mm 285 | capacitor electrolytic 286 | 0 287 | 2 288 | 2 289 | Capacitor_SMD 290 | CP_Elec_10x10 291 | SMD capacitor, aluminum electrolytic, Nichicon, 10.0x10.0mm 292 | capacitor electrolytic 293 | 0 294 | 2 295 | 2 296 | Capacitor_SMD 297 | CP_Elec_10x10.5 298 | SMD capacitor, aluminum electrolytic, Vishay 1010, 10.0x10.5mm, http://www.vishay.com/docs/28395/150crz.pdf 299 | capacitor electrolytic 300 | 0 301 | 2 302 | 2 303 | Capacitor_SMD 304 | CP_Elec_10x12.5 305 | SMD capacitor, aluminum electrolytic, Vishay 1012, 10.0x12.5mm, http://www.vishay.com/docs/28395/150crz.pdf 306 | capacitor electrolytic 307 | 0 308 | 2 309 | 2 310 | Capacitor_SMD 311 | CP_Elec_10x12.6 312 | SMD capacitor, aluminum electrolytic, Panasonic F12, 10.0x12.6mm 313 | capacitor electrolytic 314 | 0 315 | 2 316 | 2 317 | Capacitor_SMD 318 | CP_Elec_10x14.3 319 | SMD capacitor, aluminum electrolytic, Vishay 1014, 10.0x14.3mm, http://www.vishay.com/docs/28395/150crz.pdf 320 | capacitor electrolytic 321 | 0 322 | 2 323 | 2 324 | Capacitor_SMD 325 | CP_Elec_16x17.5 326 | SMD capacitor, aluminum electrolytic, Vishay 1616, 16.0x17.5mm, http://www.vishay.com/docs/28395/150crz.pdf 327 | capacitor electrolytic 328 | 0 329 | 2 330 | 2 331 | Capacitor_SMD 332 | CP_Elec_16x22 333 | SMD capacitor, aluminum electrolytic, Vishay 1621, 16.0x22.0mm, http://www.vishay.com/docs/28395/150crz.pdf 334 | capacitor electrolytic 335 | 0 336 | 2 337 | 2 338 | Capacitor_SMD 339 | CP_Elec_18x17.5 340 | SMD capacitor, aluminum electrolytic, Vishay 1816, 18.0x17.5mm, http://www.vishay.com/docs/28395/150crz.pdf 341 | capacitor electrolytic 342 | 0 343 | 2 344 | 2 345 | Capacitor_SMD 346 | CP_Elec_18x22 347 | SMD capacitor, aluminum electrolytic, Vishay 1821, 18.0x22.0mm, http://www.vishay.com/docs/28395/150crz.pdf 348 | capacitor electrolytic 349 | 0 350 | 2 351 | 2 352 | Capacitor_SMD 353 | C_0201_0603Metric 354 | Capacitor SMD 0201 (0603 Metric), square (rectangular) end terminal, IPC_7351 nominal, (Body size source: https://www.vishay.com/docs/20052/crcw0201e3.pdf), generated with kicad-footprint-generator 355 | capacitor 356 | 0 357 | 4 358 | 2 359 | Capacitor_SMD 360 | C_0402_1005Metric 361 | Capacitor SMD 0402 (1005 Metric), square (rectangular) end terminal, IPC_7351 nominal, (Body size source: http://www.tortai-tech.com/upload/download/2011102023233369053.pdf), generated with kicad-footprint-generator 362 | capacitor 363 | 0 364 | 2 365 | 2 366 | Capacitor_SMD 367 | C_0603_1608Metric 368 | Capacitor SMD 0603 (1608 Metric), square (rectangular) end terminal, IPC_7351 nominal, (Body size source: http://www.tortai-tech.com/upload/download/2011102023233369053.pdf), generated with kicad-footprint-generator 369 | capacitor 370 | 0 371 | 2 372 | 2 373 | Capacitor_SMD 374 | C_0603_1608Metric_Pad1.05x0.95mm_HandSolder 375 | Capacitor SMD 0603 (1608 Metric), square (rectangular) end terminal, IPC_7351 nominal with elongated pad for handsoldering. (Body size source: http://www.tortai-tech.com/upload/download/2011102023233369053.pdf), generated with kicad-footprint-generator 376 | capacitor handsolder 377 | 0 378 | 2 379 | 2 380 | Capacitor_SMD 381 | C_0805_2012Metric 382 | Capacitor SMD 0805 (2012 Metric), square (rectangular) end terminal, IPC_7351 nominal, (Body size source: https://docs.google.com/spreadsheets/d/1BsfQQcO9C6DZCsRaXUlFlo91Tg2WpOkGARC1WS5S8t0/edit?usp=sharing), generated with kicad-footprint-generator 383 | capacitor 384 | 0 385 | 2 386 | 2 387 | Capacitor_SMD 388 | C_0805_2012Metric_Pad1.15x1.40mm_HandSolder 389 | Capacitor SMD 0805 (2012 Metric), square (rectangular) end terminal, IPC_7351 nominal with elongated pad for handsoldering. (Body size source: https://docs.google.com/spreadsheets/d/1BsfQQcO9C6DZCsRaXUlFlo91Tg2WpOkGARC1WS5S8t0/edit?usp=sharing), generated with kicad-footprint-generator 390 | capacitor handsolder 391 | 0 392 | 2 393 | 2 394 | Capacitor_SMD 395 | C_01005_0402Metric 396 | Capacitor SMD 01005 (0402 Metric), square (rectangular) end terminal, IPC_7351 nominal, (Body size source: http://www.vishay.com/docs/20056/crcw01005e3.pdf), generated with kicad-footprint-generator 397 | capacitor 398 | 0 399 | 4 400 | 2 401 | Capacitor_SMD 402 | C_1206_3216Metric 403 | Capacitor SMD 1206 (3216 Metric), square (rectangular) end terminal, IPC_7351 nominal, (Body size source: http://www.tortai-tech.com/upload/download/2011102023233369053.pdf), generated with kicad-footprint-generator 404 | capacitor 405 | 0 406 | 2 407 | 2 408 | Capacitor_SMD 409 | C_1206_3216Metric_Pad1.42x1.75mm_HandSolder 410 | Capacitor SMD 1206 (3216 Metric), square (rectangular) end terminal, IPC_7351 nominal with elongated pad for handsoldering. (Body size source: http://www.tortai-tech.com/upload/download/2011102023233369053.pdf), generated with kicad-footprint-generator 411 | capacitor handsolder 412 | 0 413 | 2 414 | 2 415 | Capacitor_SMD 416 | C_1210_3225Metric 417 | Capacitor SMD 1210 (3225 Metric), square (rectangular) end terminal, IPC_7351 nominal, (Body size source: http://www.tortai-tech.com/upload/download/2011102023233369053.pdf), generated with kicad-footprint-generator 418 | capacitor 419 | 0 420 | 2 421 | 2 422 | Capacitor_SMD 423 | C_1210_3225Metric_Pad1.42x2.65mm_HandSolder 424 | Capacitor SMD 1210 (3225 Metric), square (rectangular) end terminal, IPC_7351 nominal with elongated pad for handsoldering. (Body size source: http://www.tortai-tech.com/upload/download/2011102023233369053.pdf), generated with kicad-footprint-generator 425 | capacitor handsolder 426 | 0 427 | 2 428 | 2 429 | Capacitor_SMD 430 | C_1806_4516Metric 431 | Capacitor SMD 1806 (4516 Metric), square (rectangular) end terminal, IPC_7351 nominal, (Body size source: https://www.modelithics.com/models/Vendor/MuRata/BLM41P.pdf), generated with kicad-footprint-generator 432 | capacitor 433 | 0 434 | 2 435 | 2 436 | Capacitor_SMD 437 | C_1806_4516Metric_Pad1.57x1.80mm_HandSolder 438 | Capacitor SMD 1806 (4516 Metric), square (rectangular) end terminal, IPC_7351 nominal with elongated pad for handsoldering. (Body size source: https://www.modelithics.com/models/Vendor/MuRata/BLM41P.pdf), generated with kicad-footprint-generator 439 | capacitor handsolder 440 | 0 441 | 2 442 | 2 443 | Capacitor_SMD 444 | C_1812_4532Metric 445 | Capacitor SMD 1812 (4532 Metric), square (rectangular) end terminal, IPC_7351 nominal, (Body size source: https://www.nikhef.nl/pub/departments/mt/projects/detectorR_D/dtddice/ERJ2G.pdf), generated with kicad-footprint-generator 446 | capacitor 447 | 0 448 | 2 449 | 2 450 | Capacitor_SMD 451 | C_1812_4532Metric_Pad1.30x3.40mm_HandSolder 452 | Capacitor SMD 1812 (4532 Metric), square (rectangular) end terminal, IPC_7351 nominal with elongated pad for handsoldering. (Body size source: https://www.nikhef.nl/pub/departments/mt/projects/detectorR_D/dtddice/ERJ2G.pdf), generated with kicad-footprint-generator 453 | capacitor handsolder 454 | 0 455 | 2 456 | 2 457 | Capacitor_SMD 458 | C_1825_4564Metric 459 | Capacitor SMD 1825 (4564 Metric), square (rectangular) end terminal, IPC_7351 nominal, (Body size from: http://datasheets.avx.com/AVX-HV_MLCC.pdf), generated with kicad-footprint-generator 460 | capacitor 461 | 0 462 | 2 463 | 2 464 | Capacitor_SMD 465 | C_1825_4564Metric_Pad1.88x6.70mm_HandSolder 466 | Capacitor SMD 1825 (4564 Metric), square (rectangular) end terminal, IPC_7351 nominal with elongated pad for handsoldering. (Body size from: http://datasheets.avx.com/AVX-HV_MLCC.pdf), generated with kicad-footprint-generator 467 | capacitor handsolder 468 | 0 469 | 2 470 | 2 471 | Capacitor_SMD 472 | C_2010_5025Metric 473 | Capacitor SMD 2010 (5025 Metric), square (rectangular) end terminal, IPC_7351 nominal, (Body size source: http://www.tortai-tech.com/upload/download/2011102023233369053.pdf), generated with kicad-footprint-generator 474 | capacitor 475 | 0 476 | 2 477 | 2 478 | Capacitor_SMD 479 | C_2010_5025Metric_Pad1.52x2.65mm_HandSolder 480 | Capacitor SMD 2010 (5025 Metric), square (rectangular) end terminal, IPC_7351 nominal with elongated pad for handsoldering. (Body size source: http://www.tortai-tech.com/upload/download/2011102023233369053.pdf), generated with kicad-footprint-generator 481 | capacitor handsolder 482 | 0 483 | 2 484 | 2 485 | Capacitor_SMD 486 | C_2220_5650Metric 487 | Capacitor SMD 2220 (5650 Metric), square (rectangular) end terminal, IPC_7351 nominal, (Body size from: http://datasheets.avx.com/AVX-HV_MLCC.pdf), generated with kicad-footprint-generator 488 | capacitor 489 | 0 490 | 2 491 | 2 492 | Capacitor_SMD 493 | C_2220_5650Metric_Pad1.97x5.40mm_HandSolder 494 | Capacitor SMD 2220 (5650 Metric), square (rectangular) end terminal, IPC_7351 nominal with elongated pad for handsoldering. (Body size from: http://datasheets.avx.com/AVX-HV_MLCC.pdf), generated with kicad-footprint-generator 495 | capacitor handsolder 496 | 0 497 | 2 498 | 2 499 | Capacitor_SMD 500 | C_2225_5664Metric 501 | Capacitor SMD 2225 (5664 Metric), square (rectangular) end terminal, IPC_7351 nominal, (Body size from: http://datasheets.avx.com/AVX-HV_MLCC.pdf), generated with kicad-footprint-generator 502 | capacitor 503 | 0 504 | 2 505 | 2 506 | Capacitor_SMD 507 | C_2225_5664Metric_Pad1.80x6.60mm_HandSolder 508 | Capacitor SMD 2225 (5664 Metric), square (rectangular) end terminal, IPC_7351 nominal with elongated pad for handsoldering. (Body size from: http://datasheets.avx.com/AVX-HV_MLCC.pdf), generated with kicad-footprint-generator 509 | capacitor handsolder 510 | 0 511 | 2 512 | 2 513 | Capacitor_SMD 514 | C_2512_6332Metric 515 | Capacitor SMD 2512 (6332 Metric), square (rectangular) end terminal, IPC_7351 nominal, (Body size source: http://www.tortai-tech.com/upload/download/2011102023233369053.pdf), generated with kicad-footprint-generator 516 | capacitor 517 | 0 518 | 2 519 | 2 520 | Capacitor_SMD 521 | C_2512_6332Metric_Pad1.52x3.35mm_HandSolder 522 | Capacitor SMD 2512 (6332 Metric), square (rectangular) end terminal, IPC_7351 nominal with elongated pad for handsoldering. (Body size source: http://www.tortai-tech.com/upload/download/2011102023233369053.pdf), generated with kicad-footprint-generator 523 | capacitor handsolder 524 | 0 525 | 2 526 | 2 527 | Capacitor_SMD 528 | C_2816_7142Metric 529 | Capacitor SMD 2816 (7142 Metric), square (rectangular) end terminal, IPC_7351 nominal, (Body size from: https://www.vishay.com/docs/30100/wsl.pdf), generated with kicad-footprint-generator 530 | capacitor 531 | 0 532 | 2 533 | 2 534 | Capacitor_SMD 535 | C_2816_7142Metric_Pad3.20x4.45mm_HandSolder 536 | Capacitor SMD 2816 (7142 Metric), square (rectangular) end terminal, IPC_7351 nominal with elongated pad for handsoldering. (Body size from: https://www.vishay.com/docs/30100/wsl.pdf), generated with kicad-footprint-generator 537 | capacitor handsolder 538 | 0 539 | 2 540 | 2 541 | Capacitor_SMD 542 | C_3640_9110Metric 543 | Capacitor SMD 3640 (9110 Metric), square (rectangular) end terminal, IPC_7351 nominal, (Body size from: http://datasheets.avx.com/AVX-HV_MLCC.pdf), generated with kicad-footprint-generator 544 | capacitor 545 | 0 546 | 2 547 | 2 548 | Capacitor_SMD 549 | C_3640_9110Metric_Pad2.10x10.45mm_HandSolder 550 | Capacitor SMD 3640 (9110 Metric), square (rectangular) end terminal, IPC_7351 nominal with elongated pad for handsoldering. (Body size from: http://datasheets.avx.com/AVX-HV_MLCC.pdf), generated with kicad-footprint-generator 551 | capacitor handsolder 552 | 0 553 | 2 554 | 2 555 | Capacitor_SMD 556 | C_Elec_3x5.4 557 | SMD capacitor, aluminum electrolytic nonpolar, 3.0x5.4mm 558 | capacitor electrolyic nonpolar 559 | 0 560 | 2 561 | 2 562 | Capacitor_SMD 563 | C_Elec_4x5.4 564 | SMD capacitor, aluminum electrolytic nonpolar, 4.0x5.4mm 565 | capacitor electrolyic nonpolar 566 | 0 567 | 2 568 | 2 569 | Capacitor_SMD 570 | C_Elec_4x5.8 571 | SMD capacitor, aluminum electrolytic nonpolar, 4.0x5.8mm 572 | capacitor electrolyic nonpolar 573 | 0 574 | 2 575 | 2 576 | Capacitor_SMD 577 | C_Elec_5x5.4 578 | SMD capacitor, aluminum electrolytic nonpolar, 5.0x5.4mm 579 | capacitor electrolyic nonpolar 580 | 0 581 | 2 582 | 2 583 | Capacitor_SMD 584 | C_Elec_5x5.8 585 | SMD capacitor, aluminum electrolytic nonpolar, 5.0x5.8mm 586 | capacitor electrolyic nonpolar 587 | 0 588 | 2 589 | 2 590 | Capacitor_SMD 591 | C_Elec_6.3x5.4 592 | SMD capacitor, aluminum electrolytic nonpolar, 6.3x5.4mm 593 | capacitor electrolyic nonpolar 594 | 0 595 | 2 596 | 2 597 | Capacitor_SMD 598 | C_Elec_6.3x5.8 599 | SMD capacitor, aluminum electrolytic nonpolar, 6.3x5.8mm 600 | capacitor electrolyic nonpolar 601 | 0 602 | 2 603 | 2 604 | Capacitor_SMD 605 | C_Elec_6.3x7.7 606 | SMD capacitor, aluminum electrolytic nonpolar, 6.3x7.7mm 607 | capacitor electrolyic nonpolar 608 | 0 609 | 2 610 | 2 611 | Capacitor_SMD 612 | C_Elec_8x5.4 613 | SMD capacitor, aluminum electrolytic nonpolar, 8.0x5.4mm 614 | capacitor electrolyic nonpolar 615 | 0 616 | 2 617 | 2 618 | Capacitor_SMD 619 | C_Elec_8x6.2 620 | SMD capacitor, aluminum electrolytic nonpolar, 8.0x6.2mm 621 | capacitor electrolyic nonpolar 622 | 0 623 | 2 624 | 2 625 | Capacitor_SMD 626 | C_Elec_8x10.2 627 | SMD capacitor, aluminum electrolytic nonpolar, 8.0x10.2mm 628 | capacitor electrolyic nonpolar 629 | 0 630 | 2 631 | 2 632 | Capacitor_SMD 633 | C_Elec_10x10.2 634 | SMD capacitor, aluminum electrolytic nonpolar, 10.0x10.2mm 635 | capacitor electrolyic nonpolar 636 | 0 637 | 2 638 | 2 639 | Capacitor_SMD 640 | C_Trimmer_Murata_TZB4-A 641 | trimmer capacitor SMD horizontal, http://www.murata.com/~/media/webrenewal/support/library/catalog/products/capacitor/trimmer/t13e.ashx?la=en-gb 642 | Murata TZB4 TZB4-A 643 | 0 644 | 2 645 | 2 646 | Capacitor_SMD 647 | C_Trimmer_Murata_TZB4-B 648 | trimmer capacitor SMD horizontal, http://www.murata.com/~/media/webrenewal/support/library/catalog/products/capacitor/trimmer/t13e.ashx?la=en-gb 649 | Murata TZB4 TZB4-A 650 | 0 651 | 2 652 | 2 653 | Capacitor_SMD 654 | C_Trimmer_Murata_TZC3 655 | trimmer capacitor SMD horizontal, http://www.murata.com/~/media/webrenewal/support/library/catalog/products/capacitor/trimmer/t13e.ashx?la=en-gb 656 | Murata TZC3 657 | 0 658 | 2 659 | 2 660 | Capacitor_SMD 661 | C_Trimmer_Murata_TZR1 662 | trimmer capacitor SMD horizontal, http://www.murata.com/~/media/webrenewal/support/library/catalog/products/capacitor/trimmer/t13e.ashx?la=en-gb 663 | Murata TZR1 664 | 0 665 | 2 666 | 2 667 | Capacitor_SMD 668 | C_Trimmer_Murata_TZW4 669 | trimmer capacitor SMD horizontal, http://www.murata.com/~/media/webrenewal/support/library/catalog/products/capacitor/trimmer/t13e.ashx?la=en-gb 670 | Murata TZW4 671 | 0 672 | 2 673 | 2 674 | Capacitor_SMD 675 | C_Trimmer_Murata_TZY2 676 | trimmer capacitor SMD horizontal, http://www.murata.com/~/media/webrenewal/support/library/catalog/products/capacitor/trimmer/t13e.ashx?la=en-gb 677 | Murata TZY2 678 | 0 679 | 2 680 | 2 681 | Capacitor_SMD 682 | C_Trimmer_Sprague-Goodman_SGC3 683 | trimmer capacitor SMD horizontal, http://media.wix.com/ugd/d86717_38d9821e12823a7aa9cef38c6c2a73cc.pdf 684 | Sprague Goodman SGC3 685 | 0 686 | 2 687 | 2 688 | Capacitor_SMD 689 | C_Trimmer_Voltronics_JN 690 | trimmer capacitor SMD horizontal, http://www.knowlescapacitors.com/File%20Library/Voltronics/English/GlobalNavigation/Products/Trimmer%20Capacitors/CerChipTrimCap.pdf 691 | Voltronics JN 692 | 0 693 | 2 694 | 2 695 | Capacitor_SMD 696 | C_Trimmer_Voltronics_JQ 697 | trimmer capacitor SMD horizontal, http://www.knowlescapacitors.com/File%20Library/Voltronics/English/GlobalNavigation/Products/Trimmer%20Capacitors/CerChipTrimCap.pdf 698 | Voltronics JQ 699 | 0 700 | 2 701 | 2 702 | Capacitor_SMD 703 | C_Trimmer_Voltronics_JR 704 | trimmer capacitor SMD horizontal, http://www.knowlescapacitors.com/File%20Library/Voltronics/English/GlobalNavigation/Products/Trimmer%20Capacitors/CerChipTrimCap.pdf 705 | Voltronics JR 706 | 0 707 | 2 708 | 2 709 | Capacitor_SMD 710 | C_Trimmer_Voltronics_JV 711 | trimmer capacitor SMD horizontal, http://www.knowlescapacitors.com/File%20Library/Voltronics/English/GlobalNavigation/Products/Trimmer%20Capacitors/CerChipTrimCap.pdf 712 | Voltronics JV 713 | 0 714 | 2 715 | 2 716 | Capacitor_SMD 717 | C_Trimmer_Voltronics_JZ 718 | trimmer capacitor SMD horizontal, http://www.knowlescapacitors.com/File%20Library/Voltronics/English/GlobalNavigation/Products/Trimmer%20Capacitors/CerChipTrimCap.pdf 719 | Voltronics JR 720 | 0 721 | 2 722 | 2 723 | -------------------------------------------------------------------------------- /kibom/component.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import re 5 | import sys 6 | 7 | from .columns import ColumnList 8 | from .preferences import BomPref 9 | from . import units 10 | from . import debug 11 | from .sort import natural_sort 12 | 13 | # String matches for marking a component as "do not fit" 14 | DNF = [ 15 | "dnf", 16 | "dnl", 17 | "dnp", 18 | "do not fit", 19 | "do not place", 20 | "do not load", 21 | "nofit", 22 | "nostuff", 23 | "noplace", 24 | "noload", 25 | "not fitted", 26 | "not loaded", 27 | "not placed", 28 | "no stuff", 29 | ] 30 | 31 | # String matches for marking a component as "do not change" or "fixed" 32 | DNC = [ 33 | "dnc", 34 | "do not change", 35 | "no change", 36 | "fixed" 37 | ] 38 | 39 | 40 | class Component(): 41 | """Class for a component, aka 'comp' in the xml netlist file. 42 | This component class is implemented by wrapping an xmlElement instance 43 | with accessors. The xmlElement is held in field 'element'. 44 | """ 45 | 46 | def __init__(self, xml_element, prefs=None): 47 | self.element = xml_element 48 | self.libpart = None 49 | 50 | if not prefs: 51 | prefs = BomPref() 52 | 53 | self.prefs = prefs 54 | 55 | # Set to true when this component is included in a component group 56 | self.grouped = False 57 | 58 | # Compare the value of this part, to the value of another part (see if they match) 59 | def compareValue(self, other): 60 | # Simple string comparison 61 | if self.getValue().lower() == other.getValue().lower(): 62 | return True 63 | 64 | # Otherwise, perform a more complicated value comparison 65 | if units.compareValues(self.getValue(), other.getValue()): 66 | return True 67 | 68 | # Ignore value if both components are connectors 69 | if self.prefs.groupConnectors: 70 | if 'connector' in self.getLibName().lower() and 'connector' in other.getLibName().lower(): 71 | return True 72 | 73 | # No match, return False 74 | return False 75 | 76 | # Determine if two parts have the same name 77 | def comparePartName(self, other): 78 | pn1 = self.getPartName().lower() 79 | pn2 = other.getPartName().lower() 80 | 81 | # Simple direct match 82 | if pn1 == pn2: 83 | return True 84 | 85 | # Compare part aliases e.g. "c" to "c_small" 86 | for alias in self.prefs.aliases: 87 | if pn1 in alias and pn2 in alias: 88 | return True 89 | 90 | return False 91 | 92 | def compareField(self, other, field): 93 | 94 | this_field = self.getField(field).lower() 95 | other_field = other.getField(field).lower() 96 | 97 | # If blank comparisons are allowed 98 | if this_field == "" or other_field == "": 99 | if not self.prefs.mergeBlankFields: 100 | return False 101 | 102 | if this_field == other_field: 103 | return True 104 | 105 | return False 106 | 107 | def __eq__(self, other): 108 | """ 109 | Equivalency operator is used to determine if two parts are 'equal' 110 | """ 111 | 112 | # 'fitted' value must be the same for both parts 113 | if self.isFitted() != other.isFitted(): 114 | return False 115 | 116 | # 'fixed' value must be the same for both parts 117 | if self.isFixed() != other.isFixed(): 118 | return False 119 | 120 | if len(self.prefs.groups) == 0: 121 | return False 122 | 123 | for c in self.prefs.groups: 124 | # Perform special matches 125 | if c.lower() == ColumnList.COL_VALUE.lower(): 126 | if not self.compareValue(other): 127 | return False 128 | # Match part name 129 | elif c.lower() == ColumnList.COL_PART.lower(): 130 | if not self.comparePartName(other): 131 | return False 132 | 133 | # Generic match 134 | elif not self.compareField(other, c): 135 | return False 136 | 137 | return True 138 | 139 | def setLibPart(self, part): 140 | self.libpart = part 141 | 142 | def getPrefix(self): 143 | """ 144 | Get the reference prefix 145 | e.g. if this component has a reference U12, will return "U" 146 | """ 147 | 148 | prefix = "" 149 | 150 | for c in self.getRef(): 151 | if c.isalpha(): 152 | prefix += c 153 | else: 154 | break 155 | 156 | return prefix 157 | 158 | def getSuffix(self): 159 | """ 160 | Return the reference suffix # 161 | e.g. if this component has a reference U12, will return "12" 162 | """ 163 | 164 | suffix = "" 165 | 166 | for c in self.getRef(): 167 | if c.isalpha(): 168 | suffix = "" 169 | else: 170 | suffix += c 171 | 172 | return int(suffix) 173 | 174 | def getLibPart(self): 175 | return self.libpart 176 | 177 | def getPartName(self): 178 | return self.element.get("libsource", "part") 179 | 180 | def getLibName(self): 181 | return self.element.get("libsource", "lib") 182 | 183 | def getSheetpathNames(self): 184 | return self.element.get("sheetpath", "names") 185 | 186 | def getDescription(self): 187 | """Extract the 'description' field for this component""" 188 | 189 | # Give priority to a user "description" field 190 | ret = self.element.get("field", "name", "description") 191 | if ret: 192 | return ret 193 | 194 | ret = self.element.get("field", "name", "Description") 195 | if ret: 196 | return ret 197 | 198 | try: 199 | ret = self.element.get("libsource", "description") 200 | except: 201 | # Compatibility with old KiCad versions (4.x) 202 | ret = self.element.get("field", "name", "description") 203 | 204 | if ret == "": 205 | try: 206 | ret = self.libpart.getDescription() 207 | except AttributeError: 208 | # Raise a good error description here, so the user knows what the culprit component is. 209 | # (sometimes libpart is None) 210 | raise AttributeError('Could not get description for part {}{}.'.format(self.getPrefix(), 211 | self.getSuffix())) 212 | 213 | return ret 214 | 215 | def setValue(self, value): 216 | """Set the value of this component""" 217 | v = self.element.getChild("value") 218 | if v: 219 | v.setChars(value) 220 | 221 | def getValue(self): 222 | return self.element.get("value") 223 | 224 | # Try to better sort R, L and C components 225 | def getValueSort(self): 226 | pref = self.getPrefix() 227 | if pref in 'RLC' or pref == 'RV': 228 | res = units.compMatch(self.getValue()) 229 | if res: 230 | value, mult, unit = res 231 | if pref in "CL": 232 | # fempto Farads 233 | value = "{0:15d}".format(int(value * 1e15 * mult + 0.1)) 234 | else: 235 | # milli Ohms 236 | value = "{0:15d}".format(int(value * 1000 * mult + 0.1)) 237 | return value 238 | return self.element.get("value") 239 | 240 | def setField(self, name, value): 241 | """ Set the value of the specified field """ 242 | 243 | name = name.lower() 244 | # Description field 245 | doc = self.element.getChild('libsource') 246 | if doc: 247 | for att_name, att_value in doc.attributes.items(): 248 | if att_name.lower() == name: 249 | doc.attributes[att_name] = value 250 | return value 251 | 252 | # Common fields 253 | field = self.element.getChild(name) 254 | if field: 255 | field.setChars(value) 256 | return value 257 | 258 | # Other fields 259 | fields = self.element.getChild('fields') 260 | if fields: 261 | for field in fields.getChildren(): 262 | if field.get('field', 'name').lower() == name: 263 | field.setChars(value) 264 | return value 265 | 266 | return None 267 | 268 | def getField(self, name, ignoreCase=True, libraryToo=True): 269 | """Return the value of a field named name. The component is first 270 | checked for the field, and then the components library part is checked 271 | for the field. If the field doesn't exist in either, an empty string is 272 | returned 273 | 274 | Keywords: 275 | name -- The name of the field to return the value for 276 | libraryToo -- look in the libpart's fields for the same name if not found 277 | in component itself 278 | """ 279 | 280 | fp = self.getFootprint().split(":") 281 | 282 | if name.lower() == ColumnList.COL_REFERENCE.lower(): 283 | return self.getRef().strip() 284 | 285 | elif name.lower() == ColumnList.COL_DESCRIPTION.lower(): 286 | return self.getDescription().strip() 287 | 288 | elif name.lower() == ColumnList.COL_DATASHEET.lower(): 289 | return self.getDatasheet().strip() 290 | 291 | # Footprint library is first element 292 | elif name.lower() == ColumnList.COL_FP_LIB.lower(): 293 | if len(fp) > 1: 294 | return fp[0].strip() 295 | else: 296 | # Explicit empty return 297 | return "" 298 | 299 | elif name.lower() == ColumnList.COL_FP.lower(): 300 | if len(fp) > 1: 301 | return fp[1].strip() 302 | elif len(fp) == 1: 303 | return fp[0] 304 | else: 305 | return "" 306 | 307 | elif name.lower() == ColumnList.COL_VALUE.lower(): 308 | return self.getValue().strip() 309 | 310 | elif name.lower() == ColumnList.COL_PART.lower(): 311 | return self.getPartName().strip() 312 | 313 | elif name.lower() == ColumnList.COL_PART_LIB.lower(): 314 | return self.getLibName().strip() 315 | 316 | elif name.lower() == ColumnList.COL_SHEETPATH.lower(): 317 | return self.getSheetpathNames().strip() 318 | 319 | # Other fields (case insensitive) 320 | for f in self.getFieldNames(): 321 | if f.lower() == name.lower(): 322 | field = self.element.get("field", "name", f) 323 | 324 | if field == "" and libraryToo: 325 | field = self.libpart.getField(f) 326 | 327 | return field.strip() 328 | 329 | # Could not find a matching field 330 | return "" 331 | 332 | def getFieldNames(self): 333 | """Return a list of field names in play for this component. Mandatory 334 | fields are not included, and they are: Value, Footprint, Datasheet, Ref. 335 | The netlist format only includes fields with non-empty values. So if a field 336 | is empty, it will not be present in the returned list. 337 | """ 338 | 339 | fieldNames = [] 340 | 341 | fields = self.element.getChild('fields') 342 | 343 | if fields: 344 | for f in fields.getChildren(): 345 | fieldNames.append(f.get('field', 'name')) 346 | 347 | return fieldNames 348 | 349 | def getRef(self): 350 | return self.element.get("comp", "ref") 351 | 352 | def isFitted(self): 353 | """ Determine if a component is FITTED or not """ 354 | 355 | # First, check for the 'dnp' attribute (added in KiCad 7.0) 356 | for child in self.element.getChildren(): 357 | if child.name == 'property': 358 | name = child.attributes.get('name', '').lower() 359 | if name == 'dnp' or name == 'exclude_from_bom': 360 | return False 361 | 362 | # Check the value field first 363 | if self.getValue().lower() in DNF: 364 | return False 365 | 366 | check = self.getField(self.prefs.configField).lower() 367 | # Empty value means part is fitted 368 | if check == "": 369 | return True 370 | 371 | # Also support space separated list (simple cases) 372 | opts = check.split(" ") 373 | for opt in opts: 374 | if opt.lower() in DNF: 375 | return False 376 | 377 | # Variants logic 378 | opts = check.split(",") 379 | # Exclude components that match a -VARIANT 380 | for opt in opts: 381 | opt = opt.strip() 382 | # Any option containing a DNF is not fitted 383 | if opt in DNF: 384 | return False 385 | # Options that start with '-' are explicitly removed from certain configurations 386 | if opt.startswith("-") and opt[1:] in self.prefs.pcbConfig: 387 | return False 388 | # Include components that match +VARIANT 389 | exclusive = False 390 | for opt in opts: 391 | # Options that start with '+' are fitted only for certain configurations 392 | if opt.startswith("+"): 393 | exclusive = True 394 | if opt[1:] in self.prefs.pcbConfig: 395 | return True 396 | # No match 397 | return not exclusive 398 | 399 | def isFixed(self): 400 | """ Determine if a component is FIXED or not. 401 | Fixed components shouldn't be replaced without express authorization """ 402 | 403 | # Check the value field first 404 | if self.getValue().lower() in DNC: 405 | return True 406 | 407 | check = self.getField(self.prefs.configField).lower() 408 | # Empty is not fixed 409 | if check == "": 410 | return False 411 | 412 | opts = check.split(" ") 413 | for opt in opts: 414 | if opt.lower() in DNC: 415 | return True 416 | 417 | opts = check.split(",") 418 | for opt in opts: 419 | if opt.lower() in DNC: 420 | return True 421 | 422 | return False 423 | 424 | # Test if this part should be included, based on any regex expressions provided in the preferences 425 | def testRegExclude(self): 426 | 427 | for reg in self.prefs.regExcludes: 428 | 429 | if type(reg) is list and len(reg) == 2: 430 | field_name, regex = reg 431 | field_value = self.getField(field_name) 432 | 433 | # Attempt unicode escaping... 434 | # Filthy hack 435 | try: 436 | regex = regex.decode("unicode_escape") 437 | except: 438 | pass 439 | 440 | if re.search(regex, field_value, flags=re.IGNORECASE) is not None: 441 | debug.info("Excluding '{ref}': Field '{field}' ({value}) matched '{reg}'".format( 442 | ref=self.getRef(), 443 | field=field_name, 444 | value=field_value, 445 | reg=regex).encode('utf-8') 446 | ) 447 | 448 | # Found a match 449 | return True 450 | 451 | # Default, could not find any matches 452 | return False 453 | 454 | def testRegInclude(self): 455 | 456 | if len(self.prefs.regIncludes) == 0: # Nothing to match against 457 | return True 458 | 459 | for reg in self.prefs.regIncludes: 460 | 461 | if type(reg) is list and len(reg) == 2: 462 | field_name, regex = reg 463 | field_value = self.getField(field_name) 464 | 465 | debug.info(field_name, field_value, regex) 466 | 467 | if re.search(regex, field_value, flags=re.IGNORECASE) is not None: 468 | 469 | # Found a match 470 | return True 471 | 472 | # Default, could not find a match 473 | return False 474 | 475 | def getFootprint(self, libraryToo=True): 476 | ret = self.element.get("footprint") 477 | if ret == "" and libraryToo: 478 | if self.libpart: 479 | ret = self.libpart.getFootprint() 480 | return ret 481 | 482 | def getDatasheet(self, libraryToo=True): 483 | ret = self.element.get("datasheet") 484 | if ret == "" and libraryToo: 485 | ret = self.libpart.getDatasheet() 486 | return ret 487 | 488 | def getTimestamp(self): 489 | return self.element.get("tstamp") 490 | 491 | 492 | class joiner: 493 | def __init__(self): 494 | self.stack = [] 495 | 496 | def add(self, P, N): 497 | 498 | if self.stack == []: 499 | self.stack.append(((P, N), (P, N))) 500 | return 501 | 502 | S, E = self.stack[-1] 503 | 504 | if N == E[1] + 1: 505 | self.stack[-1] = (S, (P, N)) 506 | else: 507 | self.stack.append(((P, N), (P, N))) 508 | 509 | def flush(self, sep, N=None, dash='-'): 510 | 511 | refstr = u'' 512 | c = 0 513 | 514 | for Q in self.stack: 515 | if bool(N) and c != 0 and c % N == 0: 516 | refstr += u'\n' 517 | elif c != 0: 518 | refstr += sep + " " 519 | 520 | S, E = Q 521 | 522 | if S == E: 523 | refstr += "%s%d" % S 524 | c += 1 525 | else: 526 | # Do we have space? 527 | if bool(N) and (c + 1) % N == 0: 528 | refstr += u'\n' 529 | c += 1 530 | 531 | refstr += "%s%d%s%s%d" % (S[0], S[1], dash, E[0], E[1]) 532 | c += 2 533 | return refstr 534 | 535 | 536 | class ComponentGroup(): 537 | 538 | """ 539 | Initialize the group with no components, and default fields 540 | """ 541 | def __init__(self, prefs=None): 542 | self.components = [] 543 | self.fields = dict.fromkeys(ColumnList._COLUMNS_DEFAULT) # Columns loaded from KiCad 544 | 545 | if not prefs: 546 | prefs = BomPref() 547 | 548 | self.prefs = prefs 549 | 550 | def getField(self, field): 551 | 552 | if field not in self.fields.keys(): 553 | return "" 554 | 555 | if not self.fields[field]: 556 | return "" 557 | 558 | return u''.join((self.fields[field])) 559 | 560 | def getCount(self): 561 | return len(self.components) 562 | 563 | # Test if a given component fits in this group 564 | def matchComponent(self, c): 565 | if len(self.components) == 0: 566 | return True 567 | if c == self.components[0]: 568 | return True 569 | 570 | return False 571 | 572 | def containsComponent(self, c): 573 | # Test if a given component is already contained in this grop 574 | if not self.matchComponent(c): 575 | return False 576 | 577 | for comp in self.components: 578 | if comp.getRef() == c.getRef(): 579 | return True 580 | 581 | return False 582 | 583 | def addComponent(self, c): 584 | # Add a component to the group 585 | 586 | if len(self.components) == 0: 587 | self.components.append(c) 588 | elif self.containsComponent(c): 589 | return 590 | elif self.matchComponent(c): 591 | self.components.append(c) 592 | 593 | def isFitted(self): 594 | return any([c.isFitted() for c in self.components]) 595 | 596 | def isFixed(self): 597 | return any([c.isFixed() for c in self.components]) 598 | 599 | def getRefs(self): 600 | # Return a list of the components 601 | separator = self.prefs.refSeparator 602 | return separator.join([c.getRef() for c in self.components]) 603 | 604 | def getAltRefs(self): 605 | S = joiner() 606 | 607 | for n in self.components: 608 | P, N = (n.getPrefix(), n.getSuffix()) 609 | S.add(P, N) 610 | 611 | return S.flush(self.prefs.refSeparator) 612 | 613 | # Sort the components in correct order 614 | def sortComponents(self): 615 | self.components = sorted(self.components, key=lambda c: natural_sort(c.getRef())) 616 | 617 | # Update a given field, based on some rules and such 618 | def updateField(self, field, fieldData): 619 | 620 | # Protected fields cannot be overwritten 621 | if field in ColumnList._COLUMNS_PROTECTED: 622 | return 623 | 624 | if field is None or field == "": 625 | return 626 | elif fieldData == "" or fieldData is None: 627 | return 628 | 629 | if (field not in self.fields.keys()) or (self.fields[field] is None) or (self.fields[field] == ""): 630 | self.fields[field] = fieldData 631 | elif fieldData.lower() in self.fields[field].lower(): 632 | return 633 | else: 634 | if field != self.prefs.configField: 635 | debug.warning("Field conflict: ({refs}) [{name}] : '{flds}' <- '{fld}'".format( 636 | refs=self.getRefs(), 637 | name=field, 638 | flds=self.fields[field], 639 | fld=fieldData).encode('utf-8')) 640 | self.fields[field] += " " + fieldData 641 | 642 | def updateFields(self, usealt=False, wrapN=None): 643 | for c in self.components: 644 | for f in c.getFieldNames(): 645 | 646 | # These columns are handled explicitly below 647 | if f in ColumnList._COLUMNS_PROTECTED: 648 | continue 649 | 650 | self.updateField(f, c.getField(f)) 651 | 652 | # Update 'global' fields 653 | if usealt: 654 | self.fields[ColumnList.COL_REFERENCE] = self.getAltRefs() 655 | else: 656 | self.fields[ColumnList.COL_REFERENCE] = self.getRefs() 657 | 658 | q = self.getCount() 659 | self.fields[ColumnList.COL_GRP_QUANTITY] = "{n}{dnf}{dnc}".format( 660 | n=q, 661 | dnf=" (DNF)" if not self.isFitted() else "", 662 | dnc=" (DNC)" if self.isFixed() else "") 663 | 664 | self.fields[ColumnList.COL_GRP_BUILD_QUANTITY] = str(q * self.prefs.boards) if self.isFitted() else "0" 665 | self.fields[ColumnList.COL_VALUE] = self.components[0].getValue() 666 | self.fields[ColumnList.COL_PART] = self.components[0].getPartName() 667 | self.fields[ColumnList.COL_PART_LIB] = self.components[0].getLibName() 668 | self.fields[ColumnList.COL_DESCRIPTION] = self.components[0].getDescription() 669 | self.fields[ColumnList.COL_DATASHEET] = self.components[0].getDatasheet() 670 | self.fields[ColumnList.COL_SHEETPATH] = self.components[0].getSheetpathNames() 671 | 672 | # Footprint field requires special attention 673 | fp = self.components[0].getFootprint().split(":") 674 | 675 | if len(fp) >= 2: 676 | self.fields[ColumnList.COL_FP_LIB] = fp[0] 677 | self.fields[ColumnList.COL_FP] = fp[1] 678 | elif len(fp) == 1: 679 | self.fields[ColumnList.COL_FP_LIB] = "" 680 | self.fields[ColumnList.COL_FP] = fp[0] 681 | else: 682 | self.fields[ColumnList.COL_FP_LIB] = "" 683 | self.fields[ColumnList.COL_FP] = "" 684 | 685 | # Return a dict of the KiCad data based on the supplied columns 686 | # NOW WITH UNICODE SUPPORT! 687 | def getRow(self, columns): 688 | row = [] 689 | for key in columns: 690 | val = self.getField(key) 691 | 692 | # Join fields (appending to current value) (#81) 693 | for join_l in self.prefs.join: 694 | # Each list is "target, source..." so we need at least 2 elements 695 | elements = len(join_l) 696 | target = join_l[0] 697 | if elements > 1 and target == key: 698 | # Append data from the other fields 699 | for source in join_l[1:]: 700 | v = self.getField(source) 701 | if v: 702 | val = val + ' ' + v 703 | 704 | if val is None: 705 | val = "" 706 | else: 707 | val = u'' + val 708 | if sys.version_info[0] < 3: 709 | val = val.encode('utf-8') 710 | 711 | row.append(val) 712 | 713 | return row 714 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KiBoM 2 | 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 | [![PyPI](https://img.shields.io/pypi/v/kibom)](https://pypi.org/project/kibom/) 5 | [![Coverage Status](https://coveralls.io/repos/github/SchrodingersGat/KiBoM/badge.svg?branch=master)](https://coveralls.io/github/SchrodingersGat/KiBoM?branch=master) 6 | 7 | Configurable BoM generation tool for KiCad EDA (http://kicad.org/) 8 | 9 | ## Description 10 | 11 | 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. 12 | 13 | KiBoM intelligently groups components based on multiple factors, and can generate BoM files in multiple output formats. 14 | 15 | BoM options are user-configurable in a per-project configuration file. 16 | 17 | ## Installation 18 | 19 | KiBoM can be installed via multiple methods: 20 | 21 | **A. Download** 22 | 23 | Download the KiBoM [package from github](https://github.com/SchrodingersGat/KiBoM/archive/master.zip) and extract the .zip archive to a location on your computer. 24 | 25 | **B. Git Clone** 26 | 27 | Use git to clone the source code to your computer: 28 | 29 | `git clone https://github.com/SchrodingersGat/kibom` 30 | 31 | **C. PIP** 32 | 33 | KiBom can also be installed through the PIP package manager: 34 | 35 | ```pip install kibom``` 36 | 37 | *Note: Take note of which python executable you use when installing kibom - this is the same executable you must use when running the KiBom script from KiCAD (more details below under "Usage")* 38 | 39 | Installing under PIP is recommended for advanced users only, as the exact location of the installed module must be known to properly run the script from within KiCad. 40 | 41 | ## Usage 42 | 43 | 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. 44 | 45 | `python KiBOM_CLI.py -h` 46 | 47 | ~~~~ 48 | usage: KiBOM_CLI.py [-h] [-n NUMBER] [-v] [-r VARIANT] [--cfg CFG] 49 | [-s SEPARATOR] 50 | netlist output 51 | 52 | KiBOM Bill of Materials generator script 53 | 54 | positional arguments: 55 | netlist xml netlist file. Use "%I" when running from within 56 | KiCad 57 | output BoM output file name. Use "%O" when running from 58 | within KiCad to use the default output name (csv 59 | file). For e.g. HTML output, use "%O.html" 60 | 61 | optional arguments: 62 | -h, --help show this help message and exit 63 | -n NUMBER, --number NUMBER 64 | Number of boards to build (default = 1) 65 | -v, --verbose Enable verbose output 66 | -r VARIANT, --variant VARIANT 67 | Board variant(s), used to determine which components are 68 | output to the BoM 69 | -d SUBDIRECTORY, --subdirectory SUBDIRECTORY 70 | Subdirectory (relative to output file) within which the 71 | BoM(s) should be written. 72 | --cfg CFG BoM config file (script will try to use 'bom.ini' if 73 | not specified here) 74 | -s SEPARATOR, --separator SEPARATOR 75 | CSV Separator (default ',') 76 | -k, --no-colon-sep Don't use : as delimiter in the config file 77 | --version show program's version number and exit 78 | 79 | 80 | ~~~~ 81 | 82 | **netlist** The netlist must be provided to the script. When running from KiCad use "%I" 83 | 84 | **output** This is the path to the BoM output. When running from KiCad, usage "%O" for the default option 85 | 86 | * If a suffix is not specified, CSV output format will be used 87 | * HTML output can be specified within KiCad as: "%O.html" or "%O_BOM.html" (etc) 88 | * XML output can be specified within KiCad as: "%O.xml" (etc) 89 | * XSLX output can be specified within KiCad as: "%O.xlsx" (etc) 90 | 91 | **-n --number** Specify number of boards for calculating part quantities 92 | 93 | **-v --verbose** Enable extra debugging information 94 | 95 | **-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. 96 | 97 | **-d --subdirectory** Specify a subdirectory (from the provided **output** file) into which the boms should be generated. 98 | 99 | **--cfg** If provided, this is the BoM config file that will be used. If not provided, options will be loaded from "bom.ini" 100 | 101 | **-s --separator** Override the delimiter for CSV or TSV generation 102 | 103 | **-k --no-colon-sep** Only accept `=` as a delimiter for KEY/VALUE pairs in the config file, enables the use of `:` in field names 104 | 105 | -------- 106 | To run from KiCad, simply add the same command line in the *Bill of Materials* script window. e.g. to generate a HTML output: 107 | 108 | ![alt tag](example/html_ex.png?raw=True "HTML Example") 109 | 110 | ## Quick Start 111 | 112 | Download and unzip the files almost anywhere. 113 | 114 | 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. 115 | 116 | If you want other than .csv format, edit the *Command Line*, for example inserting ".html" after the "%O". 117 | 118 | 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." 119 | 120 | ## Features 121 | 122 | ### Intelligent Component Grouping 123 | 124 | 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: 125 | 126 | * 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* 127 | * Value: Components must have the same value to be grouped together 128 | * Footprint: Components must have the same footprint to be grouped together *(this option can be enabled/disabled in the bom.ini configuration file)* 129 | 130 | #### Custom Column Grouping 131 | 132 | If the user wishes to group components based on additional field values, these can be specified in the preferences (.ini) file 133 | 134 | ### Intelligent Value Matching 135 | 136 | 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. 137 | 138 | ### Field Extraction 139 | 140 | In addition to the default KiCad fields which are assigned to each component, KiBoM extracts and custom fields added to the various components. 141 | 142 | **Default Fields** 143 | 144 | The following default fields are extracted and can be added to the output BoM file: 145 | * `Description` : Part description as per the schematic symbol 146 | * `References` : List of part references included in a particular group 147 | * `Quantity` : Number of components included in a particular group 148 | * `Part` : Part name as per the schematic symbol 149 | * `Part Lib` : Part library for the symbol *(default - not output to BoM file)* 150 | * `Footprint` : Part footprint 151 | * `Footprint Lib` : Part footprint library *(default - not output to BoM file)* 152 | * `Datasheet` : Component datasheet extracted either from user-included data, or from part library 153 | 154 | **User Fields** 155 | 156 | If any components have custom fields added, these are available to the output BoM file. 157 | 158 | **Joining Fields** 159 | 160 | The user may wish to have separate fields in the output BOM file. For example, multiple component parameters such as [Voltage / Current / Tolerance] could be joined into the *Value* field in the generated BOM file. 161 | 162 | Field joining is configured in the `bom.ini` file. Under the `[JOIN]` section in the file, multiple join entries can be specified by the user to be joined. Each line is a separate entry, which contains two or more tab-separated field names. 163 | 164 | The first name specifies the primary field which be displayed in the output file. The following names specifiy fields which will be joined into the primary field. 165 | 166 | Example: 167 | 168 | ``` 169 | [JOIN] 170 | Value Voltage Current Tolerance 171 | ``` 172 | 173 | This entry will append the `voltage`, `current` and `tolerance` values into the `value` field. 174 | 175 | ### Multiple PCB Configurations 176 | 177 | 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. 178 | 179 | 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. 180 | 181 | **DNF Parts** 182 | 183 | To specify a part as DNF (do not fit), the *fit_field* field can be set to one of the following values: (case insensitive) 184 | 185 | * "dnf" 186 | * "do not fit" 187 | * "nofit" 188 | * "not fitted" 189 | * "dnp" 190 | * "do not place" 191 | * "no stuff" 192 | * "nostuff" 193 | * "noload" 194 | * "do not load" 195 | 196 | **DNC Parts** 197 | 198 | Parts can be marked as *do not change* or *fixed* by specifying the `dnc` attribute in the *fit_field* field. 199 | 200 | **Note:** 201 | 202 | If the *Value* field for the component contains any of these values, the component will also not be included 203 | 204 | **PCB Variants** 205 | 206 | 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. 207 | 208 | 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: 209 | 210 | * If the *fit_field* value is empty / blank then it will be loaded in ALL variants. 211 | * If the *fit_field* begins with a '-' character, if will be excluded from the matching variant. 212 | * If the *fit_field* begins with a '+' character, if will ONLY be included in the matching variant. 213 | 214 | 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. 215 | 216 | * If you specify multiple variants 217 | - If the *fit_field* contains the variant beginning with a '-' character, it will be excluded irrespective of any other '+' matches. 218 | - If the *fit_field* contains the variant beginning with a '+' and matches any of the given variants, it will be included. 219 | 220 | e.g. if we have a PCB with three components that have the following values in the *fit_field* field: 221 | 222 | * C1 -> "-production,+test" 223 | * C2 -> "+production,+test" 224 | * R1 -> "" 225 | * R2 -> "-test" 226 | 227 | If the script is run with the flag *--variant production* then C2, R1 and R2 will be loaded. 228 | 229 | If the script is run without the *--variant production* flag, then R1 and R2 will be loaded. 230 | 231 | If the script is run with the flag *--variant test*, then C1, C2 and R1 will be loaded. 232 | 233 | If the script is run with the flags *--variant production,test*, then C2 and R1 will be loaded. 234 | 235 | 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*. 236 | 237 | ### Regular Expression Matching 238 | 239 | KiBoM features two types of regex matching : "Include" and "Exclude" (each of these are specified within the preferences (bom.ini) file). 240 | 241 | 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 242 | 243 | 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 244 | 245 | (Refer to the default bom.ini file for examples) 246 | 247 | ### Multiple File Outputs 248 | Multiple BoM output formats are supported: 249 | * CSV (Comma separated values) 250 | * TSV (Tab separated values) 251 | * TXT (Text file output with tab separated values) 252 | * XML 253 | * HTML 254 | * XLSX (Needs XlsxWriter Python module) 255 | 256 | 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. 257 | 258 | ### Digi-Key Linking 259 | 260 | If you have a field containing the Digi-Key part number you can make its column to contain links to the Digi-Key web page for this component. (*Note: Digi-Key links will only be generated for the HTML output format*). 261 | 262 | **Instructions** 263 | 264 | Specify a column (field) to use as the `digikey_link` field in the configuration file (ie. `bom.ini`). The value for this option is the column you want to convert into a link to the Digi-Key. Note that this field must contian a valid Digi-Key part number in each row. 265 | 266 | For example: 267 | 268 | `digikey_link = digikeypn` 269 | 270 | This will render entries in the column *digikeypn* as hyperlinks to the component webpage on the Digi-Key website. 271 | 272 | **Limitations** 273 | 274 | Note that Digi-Key URL rendering will only be rendered for HTML file outputs. 275 | 276 | ### Configuration File 277 | 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. 278 | * `ignore_dnf` : Component groups designated as 'DNF' (do not fit) will be excluded from the BoM output 279 | * `use_alt` : If this option is set, grouped references will be printed in the alternate compressed style eg: R1-R7,R18 280 | * `number_rows` : Add row numbers to the BoM output 281 | * `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. 282 | * `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. 283 | * `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') 284 | * `ref_separator` : This is the character used to separate reference designators in the output, when grouped. Defaults to " ". 285 | * `fit_field` : This is the name of the part field used to determine if the component is fitted, or not. 286 | * `complex_variant` : This enable a more complex processing of variant fields using the `VARIANT:FIELD` format for the name of symbol properties 287 | * `output_file_name` : A string that allows arbitrary specification of the output file name with field replacements. Fields available: 288 | - `%O` : The base output file name (pulled from kicad, or specified on command line when calling script). 289 | - `%v` : version number. 290 | - `%V` : variant name, note that this will be ammended according to `variant_file_name_format`. 291 | * `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. 292 | * `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. 293 | * `number_boards` : Specifies the number of boards to produce, if none is specified on CLI with `-n`. 294 | * `board_variant` : Specifies the name of the PCB variant, if none is specified on CLI with `-r`. 295 | * `hide_headers` : If this option is set, the table/column headers and legends are suppressed in the output file. 296 | * `hide_pcb_info` : If this option is set, PCB information (version, component count, etc) are suppressed in the output file. 297 | * `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. 298 | * `GROUP_FIELDS` : A list of component fields used to group components together. 299 | * `COMPONENT_ALIASES` : A list of space-separated values which allows multiple schematic symbol visualisations to be consolidated. 300 | * `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. 301 | * `REGEX_EXCLUDE` : If a component matches any of these regular expressions, it will *not* be included in the BoM. Important: the `REGEX_INCLUDE` section has more precedence. 302 | 303 | 304 | Example configuration file (.ini format) *default values shown* 305 | 306 | ~~~~ 307 | [BOM_OPTIONS] 308 | ; General BoM options here 309 | ; 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 310 | ignore_dnf = 1 311 | ; If 'use_alt' option is set to 1, grouped references will be printed in the alternate compressed style eg: R1-R7,R18 312 | use_alt = 0 313 | ; If 'number_rows' option is set to 1, each row in the BoM will be prepended with an incrementing row number 314 | number_rows = 1 315 | ; If 'group_connectors' option is set to 1, connectors with the same footprints will be grouped together, independent of the name of the connector 316 | group_connectors = 1 317 | ; 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 318 | test_regex = 1 319 | ; If 'merge_blank_fields' option is set to 1, component groups with blank fields will be merged into the most compatible group, where possible 320 | merge_blank_fields = 1 321 | ; 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'. 322 | output_file_name = %O_bom_%v%V 323 | ; 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. 324 | variant_file_name_format = _(%V) 325 | ; Field name used to determine if a particular part is to be fitted 326 | fit_field = Config 327 | ; Make a backup of the bom before generating the new one, using the following template 328 | make_backup = %O.tmp 329 | ; Default number of boards to produce if none given on CLI with -n 330 | number_boards = 1 331 | ; Default PCB variant if none given on CLI with -r 332 | board_variant = "default" 333 | ; Complex variant field processing (disabled by default) 334 | complex_variant = 0 335 | ; When set to 1, suppresses table/column headers and legends in the output file. 336 | ; May be useful for testing purposes. 337 | hide_headers = 0 338 | ; When set to 1, PCB information (version, component count, etc) is not shown in the output file. 339 | ; Useful for saving space in the HTML output and for ensuring CSV output is machine-parseable. 340 | hide_pcb_info = 0 341 | 342 | [IGNORE_COLUMNS] 343 | ; Any column heading that appears here will be excluded from the Generated BoM 344 | ; Titles are case-insensitive 345 | Part Lib 346 | Footprint Lib 347 | 348 | [COLUMN_ORDER] 349 | ; Columns will apear in the order they are listed here 350 | ; Titles are case-insensitive 351 | Description 352 | Part 353 | Part Lib 354 | References 355 | Value 356 | Footprint 357 | Footprint Lib 358 | Quantity Per PCB 359 | Build Quantity 360 | Datasheet 361 | 362 | [GROUP_FIELDS] 363 | ; List of fields used for sorting individual components into groups 364 | ; Components which match (comparing *all* fields) will be grouped together 365 | ; Field names are case-insensitive 366 | Part 367 | Part Lib 368 | Value 369 | Footprint 370 | Footprint Lib 371 | 372 | [COMPONENT_ALIASES] 373 | ; A series of values which are considered to be equivalent for the part name 374 | ; Each line represents a tab-separated list of equivalent component name values 375 | ; e.g. 'c c_small cap' will ensure the equivalent capacitor symbols can be grouped together 376 | ; Aliases are case-insensitive 377 | c c_small cap capacitor 378 | r r_small res resistor 379 | sw switch 380 | l l_small inductor 381 | zener zenersmall 382 | d diode d_small 383 | 384 | [REGEX_INCLUDE] 385 | ; A series of regular expressions used to include parts in the BoM 386 | ; If there are any regex defined here, only components that match against ANY of them will be included in the BOM 387 | ; Column names are case-insensitive 388 | ; Format is: "ColumName Regex" (tab-separated) 389 | 390 | [REGEX_EXCLUDE] 391 | ; A series of regular expressions used to exclude parts from the BoM 392 | ; If a component matches ANY of these, it will be excluded from the BoM 393 | ; Column names are case-insensitive 394 | ; Format is: "ColumName Regex" (tab-separated) 395 | References ^TP[0-9]* 396 | References ^FID 397 | Part mount.*hole 398 | Part solder.*bridge 399 | Part test.*point 400 | Footprint test.*point 401 | Footprint mount.*hole 402 | Footprint fiducial 403 | ~~~~ 404 | 405 | ## Example 406 | 407 | 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') 408 | 409 | ![alt tag](example/schem.png?raw=True "Schematic") 410 | 411 | Here, a number of logical groups can be seen: 412 | 413 | **R1 R2** 414 | Resistors R1 and R2 have the same value (470 Ohm) even though the value is expressed differently. 415 | Resistors R1 and R2 have the same footprint 416 | 417 | **R3 R4** 418 | Resistors R3 and R4 have the same value and the same footprint 419 | 420 | **R5** 421 | 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. 422 | 423 | **C1 C2** 424 | C1 and C2 have the same value and footprint 425 | 426 | **C3 C5** 427 | C3 and C5 have the same value and footprint 428 | 429 | **C4** 430 | C4 has a different footprint to C3 and C5, and thus is grouped separately 431 | 432 | ### HTML Output 433 | The output HTML file is generated as follows: 434 | 435 | ![alt tag](example/html_ex.png?raw=True "HTML Gen") 436 | 437 | ![alt tag](example/html.png?raw=True "HTML Output") 438 | 439 | Here the components are correctly grouped, with links to datasheets where appropriate, and fields color-coded. 440 | 441 | ### CSV Output 442 | A CSV file output can be generated simply by changing the file extension 443 | 444 | Component,Description,Part,References,Value,Footprint,Quantity,Datasheet,Rating,Vendor,Notes 445 | 1,Unpolarized capacitor,C,C1 C2,0.1uF,C_0805,2,,,, 446 | 2,Unpolarized capacitor,C,C3 C5,2.2uF,C_0805,2,,,, 447 | 3,Unpolarized capacitor,C,C4,2.2uF,C_0603,1,,100V X7R,, 448 | 4,"Connector, single row, 01x09",CONN_01X09,P2,Comms,JST_XH_S09B-XH-A_09x2.50mm_Angled,1,,,, 449 | 5,"Connector, single row, 01x09",CONN_01X09,P1,Power,JST_XH_S09B-XH-A_09x2.50mm_Angled,1,,,, 450 | 6,Resistor,R,R3 R4,100,R_0805,2,,,, 451 | 7,Resistor,R,R5,100,R_0603,1,,0.5W 0.5%,, 452 | 8,Resistor,R,R1 R2,470R,R_0805,2,,,Digikey, 453 | 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 454 | 455 | Component Count:,13 456 | Component Groups:,9 457 | Schematic Version:,A.1 458 | Schematic Date:,2016-05-15 459 | BoM Date:,15-May-16 5:27:07 PM 460 | Schematic Source:,C:/bom_test/Bom_Test.sch 461 | KiCad Version:,"Eeschema (2016-05-06 BZR 6776, Git 63decd7)-product" 462 | 463 | ### XML Output 464 | An XML file output can be generated simply by changing the file extension 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | ### XLSX Output 480 | An XLSX file output can be generated simply by changing the file extension 481 | 482 | 483 | ## Contributors 484 | 485 | With thanks to the following contributors: 486 | 487 | * https://github.com/set-soft 488 | * https://github.com/bootchk 489 | * https://github.com/diegoherranz 490 | * https://github.com/kylemanna 491 | * https://github.com/pointhi 492 | * https://github.com/schneidersoft 493 | * https://github.com/suzizecat 494 | * https://github.com/marcelobarrosalmeida 495 | * https://github.com/fauxpark 496 | * https://github.com/Swij 497 | * https://github.com/Ximi1970 498 | * https://github.com/AngusP 499 | * https://github.com/trentks 500 | * https://github.com/set-soft 501 | --------------------------------------------------------------------------------