├── .github ├── FUNDING.yml └── workflows │ └── go.yml ├── .gitignore ├── .goreleaser.yml ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets ├── image-20230103181918906.png └── image-20230104145925988.png ├── bom.go ├── envsetup.sh ├── example ├── .gitignore ├── ASY-001-0000.zip ├── ASY-001-0000 │ ├── ASY-001-0000-all.csv │ ├── ASY-001-0000.csv │ ├── ASY-001-0000.html │ ├── ASY-002-0001 │ ├── CHANGELOG.md │ ├── MFG.md │ ├── PCA-019-0000 │ └── timestamp.txt ├── ASY-001.csv ├── ASY-001.log ├── ASY-001.yml ├── CHANGELOG.md ├── MFG.md ├── electrical │ └── pcb-design │ │ ├── PCA-019-0000 │ │ ├── PCA-019-0000.csv │ │ └── PCB-019-0001 │ │ ├── PCA-019.csv │ │ ├── PCA-019.log │ │ ├── PCA-019.yml │ │ ├── PCB-019-0001 │ │ ├── README.md │ │ ├── gerber.txt │ │ ├── gerber │ │ │ ├── bottom.grb │ │ │ ├── drill.grb │ │ │ └── top.grb │ │ ├── mfg │ │ │ ├── bottom.pos │ │ │ └── top.pos │ │ └── pcb.schematic │ │ ├── PCB-019.log │ │ ├── PCB-019.yml │ │ ├── gerber │ │ ├── bottom.grb │ │ ├── drill.grb │ │ └── top.grb │ │ ├── mfg │ │ ├── bottom.pos │ │ └── top.pos │ │ └── pcb.schematic ├── mechanical │ └── enclosure │ │ ├── ASY-002-0001 │ │ ├── ASY-002-0001-all.csv │ │ ├── ASY-002-0001.csv │ │ ├── ASY-012-0012 │ │ └── enclosure-drawing.dwg │ │ ├── ASY-002.csv │ │ ├── ASY-002.log │ │ └── endcap │ │ ├── ASY-012-0012 │ │ ├── ASY-012-0012.csv │ │ └── endcap-drawing.dwg │ │ ├── ASY-012.csv │ │ └── ASY-012.log └── partmaster.csv ├── file.go ├── flow1.png ├── flow2.png ├── gitplm-logo.png ├── gitplm-logo.svg ├── go.mod ├── go.sum ├── ipn.go ├── ipn_test.go ├── main.go ├── partmaster.go ├── partmaster_test.go ├── partnumbers.md ├── rel-script.go ├── rel-script_test.go ├── release.go └── tools └── gitplm_bom.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: cbrake 4 | custom: https://paypal.me/becsystems 5 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Build 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Set up Go 1.19 9 | uses: actions/setup-go@v1 10 | with: 11 | go-version: 1.19 12 | id: go 13 | 14 | - name: Check out code into the Go module directory 15 | uses: actions/checkout@v2 16 | 17 | - name: Test 18 | run: | 19 | go test ./... 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | gitplm 2 | dist 3 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is an example goreleaser.yaml file with some sane defaults. 2 | # Make sure to check the documentation at http://goreleaser.com 3 | before: 4 | hooks: 5 | # You may remove this if you don't use go modules. 6 | - go mod download 7 | # you may remove this if you don't need go generate 8 | - go generate ./... 9 | changelog: 10 | skip: true 11 | builds: 12 | - main: . 13 | id: gitplm 14 | binary: gitplm 15 | env: 16 | - CGO_ENABLED=0 17 | goos: 18 | - linux 19 | - darwin 20 | - windows 21 | goarch: 22 | - amd64 23 | - arm 24 | - arm64 25 | - 386 26 | goarm: 27 | - 6 28 | - 7 29 | ignore: 30 | - goos: darwin 31 | goarch: 386 32 | hooks: 33 | pre: 34 | #- /bin/sh -c '. ./envsetup.sh && siot_setup && siot_build_dependencies' 35 | archives: 36 | - name_template: 37 | "{{.ProjectName}}-{{.Tag}}-{{.Os}}-{{.Arch}}{{if .Arm}}{{.Arm}}{{end}}" 38 | wrap_in_directory: true 39 | format: tar.gz 40 | format_overrides: 41 | - goos: windows 42 | format: zip 43 | files: 44 | - README.md 45 | - LICENSE 46 | - CHANGELOG.md 47 | replacements: 48 | 386: i386 49 | amd64: x86_64 50 | darwin: macos 51 | 52 | checksum: 53 | name_template: "checksums.txt" 54 | snapshot: 55 | name_template: "{{ .Tag }}-next" 56 | #env_files: 57 | #github_token: GITHUB_TOKEN 58 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | printWidth: 80 2 | proseWrap: always 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to 7 | [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 8 | 9 | For more details or to discuss releases, please visit the 10 | [GitPLM community forum](https://community.tmpdir.org/t/gitplm-releases/365) 11 | 12 | ## [Unreleased] 13 | 14 | ## [[0.4.0] - 2024-02-02](https://github.com/git-plm/gitplm/releases/tag/v0.4.0) 15 | 16 | - output hook stdout/err to gitplm stdout/err 17 | - add IPN template variable in hooks 18 | 19 | ## [[0.3.0] - 2023-01-04](https://github.com/git-plm/gitplm/releases/tag/v0.3.0) 20 | 21 | - support hooks, and required sections in release configuration (yml) file. 22 | - rewrite README.md 23 | 24 | ## [[0.2.0] - 2023-01-04](https://github.com/git-plm/gitplm/releases/tag/v0.2.0) 25 | 26 | - make tool a more general release tool -- CCC-NNN.csv or CCC-NNN.yml will now 27 | trigger a release. 28 | - support `copy` operation in YML file 29 | 30 | ## [[0.1.1] - 2023-01-03](https://github.com/git-plm/gitplm/releases/tag/v0.1.1) 31 | 32 | - merge partmaster into combined BOM and add description from partmaster 33 | 34 | ## [[0.1.0] - 2023-01-03](https://github.com/git-plm/gitplm/releases/tag/v0.1.0) 35 | 36 | - gather up manufacturing assets for all parts we produce. 37 | 38 | ## [[0.0.14] - 2022-09-08](https://github.com/git-plm/gitplm/releases/tag/v0.0.14) 39 | 40 | - support PCA assemblies 41 | 42 | ## [[0.0.13] - 2022-03-24](https://github.com/git-plm/gitplm/releases/tag/v0.0.13) 43 | 44 | - input output BOMs, move MPN and Manufactuer columns left. This makes it easier 45 | to import BOMs into distributor web sites like Mouser. (#30) 46 | 47 | ## [[0.0.12] - 2022-03-18](https://github.com/git-plm/gitplm/releases/tag/v0.0.12) 48 | 49 | - fix issue BOM lines with zero qty not being deleted (#28) 50 | 51 | ## [[0.0.11] - 2022-01-22](https://github.com/git-plm/gitplm/releases/tag/v0.0.11) 52 | 53 | - add support for checked column. This value now gets propogated from the 54 | partmaster to all BOMs and can be used for a process where a part information 55 | is double checked for accuracy. 56 | 57 | ## [[0.0.10] - 2022-01-13](https://github.com/git-plm/gitplm/releases/tag/v0.0.10) 58 | 59 | - allow partmaster.csv to life in any subdirectory instead of having to be at 60 | top level. This allows parmaster to live in a Git submodule. 61 | 62 | ## [[0.0.9] - 2022-01-12](https://github.com/git-plm/gitplm/releases/tag/v0.0.9) 63 | 64 | - fix bug in log file name -- should sit next to source BOM so we can track 65 | changes 66 | 67 | ## [[0.0.8] - 2022-01-12](https://github.com/git-plm/gitplm/releases/tag/v0.0.8) 68 | 69 | - if BOM includes subassemblies (ASY, or PCB IPNs), also create a purchase BOM 70 | that is a recursive agregate of all parts used in the design. This BOM is 71 | named `CCC-NNN-VVVV-all.csv` 72 | 73 | ## [[0.0.7] - 2022-01-06](https://github.com/git-plm/gitplm/releases/tag/v0.0.7) 74 | 75 | - support multiple sources of parts in partmaster -- simply put on separate 76 | lines. GitPLM will select the part with lowest priority field value. Other 77 | fields like `Description` are merged -- only need to be entered on one line. 78 | See `CAP-000-1001` in `examples/partmaster.csv` for an example of how to do 79 | this. 80 | 81 | ## [[0.0.6] - 2021-12-03](https://github.com/git-plm/gitplm/releases/tag/v0.0.6) 82 | 83 | - print out version more concisely so it is easier to use in scripts 84 | 85 | ## [[0.0.5] - 2021-12-02](https://github.com/git-plm/gitplm/releases/tag/v0.0.5) 86 | 87 | - add badges in readme 88 | - fix missed error check 89 | 90 | ## [[0.0.4] - 2021-12-02](https://github.com/git-plm/gitplm/releases/tag/v0.0.4) 91 | 92 | - switch from HPN (house part number) to IPN (internal part number) (#11) 93 | - implement Github CI (runs tests in PRs) (#13) 94 | - change `-version` commandline switch to print application version 95 | - add `-bomVersion` to specify BOM version to generate (used to be `-version`) 96 | 97 | ## [[0.0.3] - 2021-12-01](https://github.com/git-plm/gitplm/releases/tag/v0.0.3) 98 | 99 | - write log file when processing BOM (see 100 | [PCB-019.log](example/cad-design/PCB-019.log)). This ensures any errors are 101 | captured in a file that is automatically generated and can be stored in Git. 102 | 103 | ## [[0.0.2] - 2021-11-30](https://github.com/git-plm/gitplm/releases/tag/v0.0.2) 104 | 105 | - support for adding/removing KiCad BOM items. See 106 | [PCB-019.yml](example/cad-design/PCB-019.yml) for an example of syntax. 107 | - misc cleanup 108 | - output BOMs are sorted by HPN 109 | 110 | ## [[0.0.1] - 2021-11-22](https://github.com/git-plm/gitplm/releases/tag/v0.0.1) 111 | 112 | - initial release that can populate KiCad BOMs with parts from partmaster 113 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![gitplm logo](gitplm-logo.png) 2 | 3 | [![Go](https://github.com/git-plm/gitplm/workflows/Go/badge.svg?branch=main)](https://github.com/git-plm/gitplm/actions) 4 | ![code stats](https://tokei.rs/b1/github/git-plm/gitplm?category=code) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/git-plm/gitplm)](https://goreportcard.com/report/github.com/git-plm/gitplm) 6 | 7 | ## Product Lifecycle Management (PLM) in Git. 8 | 9 | Additional documents: 10 | 11 | - [Part numbers](https://github.com/git-plm/parts/blob/main/partnumbers.md) 12 | - [Changelog](CHANGELOG.md) 13 | 14 | GitPLM is a tool and a collection of best practices for managing information 15 | needed to manufacture products. 16 | 17 | **The fundamental thing you want to avoid in any workflow is tedious manual 18 | operations that need to made over and over. You want to do something once, and 19 | then your tools do it for you from then on. This is the problem that GitPLM 20 | solves.** 21 | 22 | GitPLM does several things: 23 | 24 | - combines source BOMs with the partmaster to generate BOMs with manufacturing 25 | information. 26 | - automate the generation of release/manufacturing information 27 | - create combined BOMs that include parts from all sub-assemblies 28 | - gathers release data for all custom components in the design into one 29 | directory for release to manufacturing. 30 | 31 | An example output is shown below: 32 | 33 | image-20230104145925988 34 | 35 | GitPLM is designed for small teams building products. We leverage Git to track 36 | changes and use simple file formats like CSV to store BOMs, partmaster, etc. 37 | 38 | ## Video overview 39 | 40 | [GitPLM overview](https://youtu.be/rSGHQXkrZmc) 41 | 42 | ## Installation 43 | 44 | You can [download a release](https://github.com/git-plm/gitplm/releases) for 45 | your favorite platform. This tool is a self-contained binary with no 46 | dependencies. 47 | 48 | Alternatively, you can: 49 | 50 | - `go intstall github.com/git-plm/gitplm@latest` 51 | 52 | or 53 | 54 | - clone the Git repo and run: `go run .` 55 | 56 | ## Usage 57 | 58 | Type `gitplm` from a shell to see commandline options: 59 | 60 | ``` 61 | Usage of gitplm: 62 | -release string 63 | Process release for IPN (ex: PCB-056-0005, ASY-002-0023) 64 | -version int 65 | display version of this application 66 | ``` 67 | 68 | ## Part Numbers 69 | 70 | Each part used to make a product is defined by a 71 | [IPN (Internal Part Number)](https://github.com/git-plm/parts/blob/main/partnumbers.md). 72 | The convention used by GitPLM is: `CCC-NNN-VVVV` 73 | 74 | - `CCC`: major category (RES, CAP, DIO, etc) 75 | - `NNN`: incrementing sequential number for each part 76 | - `VVVV`: variation to code variations of a parts typically with the **same 77 | datasheet** (resistance, capacitance, regulator voltage, IC package, etc.) 78 | Also used to encode the version of custom parts or assemblies. 79 | 80 | ## Partmaster 81 | 82 | A single [`partmaster.csv`](example/partmaster.csv) file is used for the entire 83 | organization and contains internal part numbers (IPN) for all assets used to 84 | build a product. For externally sourced parts, purchasing information such as 85 | manufacturer part number (MPN) is also included. 86 | 87 | If multiple sources are available for a part, these can be entered on additional 88 | lines with the same IPN, and different Manufacturer/MPN specified. GitPLM will 89 | merge other fields like Description, Value, etc so these only need to be 90 | specified on one of the lines. The `Priority` column is used to select the 91 | preferred part (lowest number wins). If no `Priority` is set, it defaults to 0 92 | (highest priority). Currently, GitPLM picks the highest priority part and 93 | populates that in the output BOM. In the future, we could add additional columns 94 | for multiple sources. 95 | 96 | CAD tool libraries should contain IPNs, not MPNs. _Why not just put MPNs in the 97 | CAD database?_ The fundamental reason is that a single part may be used in 100's 98 | of different places and dozens of assemblies. If you need to change a supplier 99 | for a part, you don't want to manually modify a dozen designs, generate new 100 | BOMs, etc. This is manual, tedious, and error prone. What you want to do is 101 | change the manufacturer information in the partmaster and then automatically 102 | generate new BOMs for all affected products. Because the BOMs are stored in Git, 103 | it is easy to review what changed. 104 | 105 | ## Components you manufacture 106 | 107 | A product is typically a collection of custom parts you manufacture and 108 | off-the-shelf parts you purchase. Custom parts are identified by the following 109 | `CCC`s: 110 | 111 | | Code | Description | 112 | | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | 113 | | PCA | Printed Circuit Assembly. The version is incremented any time the BOM for the assembly changes. | 114 | | PCB | Printed Circuit board. This category identifies the bare PCB board. | 115 | | ASY | Assembly (can be mechanical or top level subassembly -- typically represented by BOM and documentation). Again, the variation is incremented any time a BOM line item changes. You can also use product specific prefixes such as GTW (gateway). | 116 | | DOC | standalone documents | 117 | | DFW | data -- firmware to be loaded on MCUs, etc | 118 | | DSW | data -- software (images for embedded Linux systems, applications, programming utilities, etc) | 119 | | DCL | data -- calibration data for a design | 120 | | FIX | manufacturing fixtures | 121 | 122 | If IPN with the above category codes are found in a BOM, GitPLM looks for 123 | release directory that matches the IPN and then soft-links from the release 124 | directory to the sub component release directory. In this way we build up a 125 | hierarchy of release directories for the entire product. 126 | 127 | ## Source and Release directories 128 | 129 | For parts you produce, GitPLM scans the directory tree looking for source 130 | directories which are identified by one or both of the following files: 131 | 132 | - an input BOM. Ex: `ASY-023.csv` 133 | - a release configuration file. Ex: `PCB-019.yml` 134 | 135 | If either of these is found, GitPLM considers this a source directory and will 136 | use this directory to generate release directories. 137 | 138 | A source directory might contain: 139 | 140 | - A PCB designs 141 | - Application source code 142 | - Firmware 143 | - Mechanical design files 144 | - Test procedures 145 | - User documentation 146 | - Test Fixures/Procedures 147 | 148 | Release directories are identified by a full IPN. Examples: 149 | 150 | - `PCA-019-0012` 151 | - `ASY-012-0002` 152 | - `DOC-055-0006` 153 | 154 | ## Special Files 155 | 156 | The following files will be copied into the release directory if found in the 157 | project directory: 158 | 159 | - `MFG.md`: contains notes for manufacturing 160 | - `CHANGELOG.md`: contains a list of changes for each version. See 161 | [keep a changelog](https://keepachangelog.com) for ideas on how to structure 162 | this file. Every source directory should have a `CHANGELOG.md`. 163 | 164 | ## Release configuration 165 | 166 | A release configuration file (`CCC-NNN.yml`) in the source directory can be used 167 | to customize the release process. 168 | 169 | The file format is [YAML](https://yaml.org/), and an example is shown below: 170 | 171 | ``` 172 | remove: 173 | - cmpName: Test point 174 | - cmpName: Test point 2 175 | - ref: D12 176 | add: 177 | - cmpName: "screw #4,2" 178 | ref: S3 179 | ipn: SCR-002-0002 180 | hooks: 181 | - date -Iseconds > {{ .RelDir }}/timestamp.txt 182 | - | 183 | echo "processing {{ .SrcDir }}" 184 | echo "hi #1" 185 | echo "hi #2" 186 | copy: 187 | - gerber 188 | - mfg 189 | - pcb.schematic 190 | required: 191 | - PCA-019-0002_ibom.html 192 | ``` 193 | 194 | The following template variables are available: 195 | 196 | - `RelDir`: the release directory that GitPLM is generating 197 | - `SrcDir`: the source directory GitPLM is pulling information from 198 | 199 | Supported operations: 200 | 201 | - `remove`: remove a part from a BOM 202 | - `add`: add a part to a BOM 203 | - `copy`: copy a file or dir to the release directory 204 | - `hooks`: run shell scripts (currently Linux/MacOS only). Can be used to build 205 | software, generate PDFs, etc. 206 | - `required`: looks for required files in the release directory and stops with 207 | an error if they are not found. This is used to check that manually generated 208 | files have been populated. 209 | 210 | The release process should be automated as much as possible to process the 211 | source files and generate the release information with no manual steps. 212 | 213 | ## Examples 214 | 215 | See the examples folder. You can run commands like to exercise GitPLM: 216 | 217 | - `go run . -release ASY-001-0000` 218 | - `go run . -release PCB-019-0001` 219 | 220 | `go run .` is used when working in the source directory. You can replace this 221 | with `gitplm` if you have it installed. 222 | 223 | ## Principles 224 | 225 | - manual operations/tweaks to machine generated files are bad. If changes are 226 | made (example a BOM line item add/removed/changed), this needs to be defined 227 | declaratively and then this change applied by a program. Ideally this 228 | mechanism is also idempotent, so we describe where we want to end up, not 229 | steps to get there. The program can determine how to get there. 230 | - the number of parts used in a product is bounded, and can easily fit in 231 | computer memory (IE, we probably don't need a database for small/mid sized 232 | companies) 233 | - the total number of parts a company may use (partmaster) is also bounded, and 234 | will likely fit in memory for most small/mid sized companies. 235 | - tracking changes is important 236 | - review is important, thus Git workflow is beneficial 237 | - ASCII (text) files are preferred as they can be manually edited and changes 238 | easily review in Git workflows. 239 | - versions are cheap -- `VVVV` should be incremented liberally. 240 | - PLM software should not be tied to any one CAD tool, but should be flexible 241 | enough to work with any CAD output. 242 | 243 | ## Additional notes 244 | 245 | - use CSV files for partmaster and all BOMs. 246 | - _rational: can be read and written by excel, libreoffice, or by machine_ 247 | - _rational: easy to get started_ 248 | - versions in part numbers are sequential numbers: (0, 1, 2, 3, 4) 249 | - _rational: easy to use in programs, sorting, etc_ 250 | - CAD BOMs are never manually "scrubbed". If additional parts are needed in the 251 | assembly, create a higher level BOM that includes the CAD generated BOM, or 252 | create a `*.yml` file to declaratively describe modifications to the BOM. 253 | - _rational: since the CAD program generates the BOM in the first place, any 254 | manual processing of this BOM will only lead to mistakes._ 255 | - CSV files should be delimited with ';' instead of ','. 256 | - _rational: comma is useful in lists, descriptions, etc._ 257 | - Tooling is written in Go. 258 | - _rational:_ 259 | - _Go programs are reasonably 260 | [reliable](http://bec-systems.com/site/1625/why-are-go-applications-so-reliable)_ 261 | - _it is easy to generate standalone binaries for most platforms with no 262 | dependencies_ 263 | - _Go is fast_ 264 | - _Go is easy to read and learn._ 265 | - _The Go [package ecosystem](https://pkg.go.dev/) is quite extensive. 266 | [go-git](https://pkg.go.dev/github.com/go-git/go-git/v5) may be useful for 267 | tight integration with Git._ 268 | - _Program can be started as a command line program, but eventually grow 269 | into a full-blown web application._ 270 | 271 | ## Reference Information 272 | 273 | - https://www.awkwardengineer.com/pages/writing 274 | - https://github.com/jaredwolff/eagle-plm 275 | - https://www.jaredwolff.com/five-reasons-your-company-needs-an-item-master/ 276 | - https://www.aligni.com/ 277 | - https://kitspace.org/ 278 | - https://www.buyplm.com/plm-good-practice/part-numbering-system-software.aspx 279 | - https://github.com/Gasman2014/KC2PK 280 | -------------------------------------------------------------------------------- /assets/image-20230103181918906.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-plm/gitplm/0a092c4a9369c5941f82dd72f2bcacceecfe7978/assets/image-20230103181918906.png -------------------------------------------------------------------------------- /assets/image-20230104145925988.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-plm/gitplm/0a092c4a9369c5941f82dd72f2bcacceecfe7978/assets/image-20230104145925988.png -------------------------------------------------------------------------------- /bom.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | ) 8 | 9 | type bomLine struct { 10 | IPN ipn `csv:"IPN" yaml:"ipn"` 11 | Qnty int `csv:"Qnty" yaml:"qnty"` 12 | MPN string `csv:"MPN" yaml:"mpn"` 13 | Manufacturer string `csv:"Manufacturer" yaml:"manufacturer"` 14 | Ref string `csv:"Ref" yaml:"ref"` 15 | Value string `csv:"Value" yaml:"value"` 16 | CmpName string `csv:"Cmp name" yaml:"cmpName"` 17 | Footprint string `csv:"Footprint" yaml:"footprint"` 18 | Description string `csv:"Description" yaml:"description"` 19 | Vendor string `csv:"Vendor" yaml:"vendor"` 20 | Datasheet string `csv:"Datasheet" yaml:"datasheet"` 21 | Checked string `csv:"Checked" yaml:"checked"` 22 | } 23 | 24 | func (bl *bomLine) String() string { 25 | return fmt.Sprintf("%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v", 26 | bl.Ref, 27 | bl.Qnty, 28 | bl.Value, 29 | bl.CmpName, 30 | bl.Footprint, 31 | bl.Description, 32 | bl.Vendor, 33 | bl.IPN, 34 | bl.Datasheet, 35 | bl.Manufacturer, 36 | bl.MPN, 37 | bl.Checked) 38 | } 39 | 40 | func (bl *bomLine) removeRef(ref string) { 41 | refs := strings.Split(bl.Ref, ",") 42 | refsOut := []string{} 43 | for _, r := range refs { 44 | r = strings.Trim(r, " ") 45 | if r != ref && r != "" { 46 | refsOut = append(refsOut, r) 47 | } 48 | } 49 | bl.Ref = strings.Join(refsOut, ", ") 50 | bl.Qnty = len(refsOut) 51 | } 52 | 53 | type bom []*bomLine 54 | 55 | func (b bom) String() string { 56 | ret := "\n" 57 | for _, l := range b { 58 | ret += fmt.Sprintf("%v\n", l) 59 | } 60 | return ret 61 | } 62 | 63 | // merge can be used to merge partmaster attributes into a BOM 64 | func (b *bom) mergePartmaster(p partmaster, logErr func(string)) { 65 | // populate MPN info in our BOM 66 | for i, l := range *b { 67 | pmPart, err := p.findPart(l.IPN) 68 | if err != nil { 69 | logErr(fmt.Sprintf("Error finding part (%v:%v) on bom line #%v in pm: %v\n", l.CmpName, l.IPN, i+2, err)) 70 | continue 71 | } 72 | l.Manufacturer = pmPart.Manufacturer 73 | l.MPN = pmPart.MPN 74 | l.Datasheet = pmPart.Datasheet 75 | l.Checked = pmPart.Checked 76 | l.Description = pmPart.Description 77 | } 78 | } 79 | 80 | func (b *bom) copy() bom { 81 | ret := make([]*bomLine, len(*b)) 82 | 83 | for i, l := range *b { 84 | ret[i] = &(*l) 85 | } 86 | 87 | return ret 88 | } 89 | 90 | func (b *bom) processOurIPN(pn ipn, qty int) error { 91 | log.Println("processing our IPN: ", pn, qty) 92 | 93 | // check if BOM exists 94 | bomPath, err := findFile(pn.String() + ".csv") 95 | if err != nil { 96 | return fmt.Errorf("Error finding sub assy BOM: %v", err) 97 | } 98 | 99 | subBom := bom{} 100 | 101 | err = loadCSV(bomPath, &subBom) 102 | if err != nil { 103 | return fmt.Errorf("Error parsing CSV for %v: %v", pn, err) 104 | } 105 | 106 | for _, l := range subBom { 107 | isSub, _ := l.IPN.hasBOM() 108 | if isSub { 109 | err := b.processOurIPN(l.IPN, l.Qnty*qty) 110 | if err != nil { 111 | return fmt.Errorf("Error processing sub %v: %v", l.IPN, err) 112 | } 113 | } 114 | n := *l 115 | n.Qnty *= qty 116 | b.addItem(&n) 117 | } 118 | 119 | return nil 120 | } 121 | 122 | func (b *bom) addItem(newItem *bomLine) { 123 | for i, l := range *b { 124 | if newItem.IPN == l.IPN { 125 | (*b)[i].Qnty += newItem.Qnty 126 | return 127 | } 128 | } 129 | 130 | n := *newItem 131 | // clear refs 132 | n.Ref = "" 133 | *b = append(*b, &n) 134 | } 135 | 136 | // sort methods 137 | func (b bom) Len() int { return len(b) } 138 | func (b bom) Swap(i, j int) { b[i], b[j] = b[j], b[i] } 139 | func (b bom) Less(i, j int) bool { return strings.Compare(string(b[i].IPN), string(b[j].IPN)) < 0 } 140 | -------------------------------------------------------------------------------- /envsetup.sh: -------------------------------------------------------------------------------- 1 | # download goreleaser from https://github.com/goreleaser/goreleaser/releases/ 2 | # and put in /usr/local/bin 3 | # This can be useful to test/debug the release process locally 4 | gitplm_goreleaser_build() { 5 | goreleaser build --skip-validate --rm-dist 6 | } 7 | 8 | # before releasing, you need to tag the release 9 | # you need to provide GITHUB_TOKEN in env or ~/.config/goreleaser/github_token 10 | # generate tokens: https://github.com/settings/tokens/new 11 | # enable repo and workflow sections 12 | gitplm_goreleaser_release() { 13 | goreleaser release --rm-dist 14 | } 15 | 16 | gitplm_update_examples() { 17 | for bom in ASY-012-0012 ASY-002-0001 PCA-019-0000 ASY-001-0000; do 18 | go run . -bom $bom || return 19 | done 20 | } 21 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-plm/gitplm/0a092c4a9369c5941f82dd72f2bcacceecfe7978/example/.gitignore -------------------------------------------------------------------------------- /example/ASY-001-0000.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-plm/gitplm/0a092c4a9369c5941f82dd72f2bcacceecfe7978/example/ASY-001-0000.zip -------------------------------------------------------------------------------- /example/ASY-001-0000/ASY-001-0000-all.csv: -------------------------------------------------------------------------------- 1 | IPN;Qnty;MPN;Manufacturer;Ref;Value;Cmp name;Footprint;Description;Vendor;Datasheet;Checked 2 | ANA-000-0000;16;LT1716IS5#TRPBF;Linear Technology;;LT1716;LT1716;Package_TO_SOT_SMD:SOT-23-5;LT1716;;https://www.analog.com/media/en/technical-documentation/data-sheets/LT1716.pdf; 3 | ASY-002-0001;3;;mycompany;;;;;endplate;;;Y 4 | ASY-012-0012;6;;mycompany;;;;;endcap assembly;;; 5 | CAP-000-1001;36;08055C102JAT2A;AVX;;10nF_50V;10nF_50V;Capacitor_SMD:C_0603_1608Metric;1nF, 50V cap;;http://datasheets.avx.com/X7RDielectric.pdf;Y 6 | DIO-002-0000;14;MMBZ5245B-7-F;Diodes Incorporated;;MMBZ5245B-7-F;MMBZ5245B-7-F;Diode_SMD:D_SOT-23_ANK;SMD diode;;https://www.diodes.com/assets/Datasheets/MMBZ5221B-MMBZ5259B.pdf;Y 7 | MCH-001-0001;6;1051023;bracketsRus;;;;;bracket;;https://www.brackets.com/pdfs/21523.pdf;Y 8 | PCA-019-0000;2;;mycompany;;;;;PCB assembly;;; 9 | PCB-019-0001;2;;mycompany;;;Design XYZ PCB;;PCB;;; 10 | RES-008-220K;8;HV732HTTE2203F;KOA SPEER Electronics;;220k_500mW;220k_500mW;Resistor_SMD:R_2010_5025Metric;220k, Resistor;;https://www.koaspeer.com/pdfs/HV73.pdf; 11 | SCR-002-0002;56;18a02SDF;screwsRus;;;;;#4 screw;;https://www.screws.com/pdfs/abc.pdf; 12 | -------------------------------------------------------------------------------- /example/ASY-001-0000/ASY-001-0000.csv: -------------------------------------------------------------------------------- 1 | IPN;Qnty;MPN;Manufacturer;Ref;Value;Cmp name;Footprint;Description;Vendor;Datasheet;Checked 2 | ASY-002-0001;3;;mycompany;;;;;endplate;;;Y 3 | PCA-019-0000;2;;mycompany;;;;;PCB assembly;;; 4 | -------------------------------------------------------------------------------- /example/ASY-001-0000/ASY-001-0000.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-plm/gitplm/0a092c4a9369c5941f82dd72f2bcacceecfe7978/example/ASY-001-0000/ASY-001-0000.html -------------------------------------------------------------------------------- /example/ASY-001-0000/ASY-002-0001: -------------------------------------------------------------------------------- 1 | ../mechanical/enclosure/ASY-002-0001 -------------------------------------------------------------------------------- /example/ASY-001-0000/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | The format of this changelog roughly follows 4 | [keep a changelog](https://keepachangelog.com) 5 | 6 | ## [ASY-001-0000] - 2022-10-23 7 | 8 | - first release of product XYZ 9 | -------------------------------------------------------------------------------- /example/ASY-001-0000/MFG.md: -------------------------------------------------------------------------------- 1 | # Product XYZ Manufacturing notes 2 | 3 | ## Testing 4 | 5 | - point a 6 | - point b 7 | - point c 8 | 9 | ## Certifications 10 | -------------------------------------------------------------------------------- /example/ASY-001-0000/PCA-019-0000: -------------------------------------------------------------------------------- 1 | ../electrical/pcb-design/PCA-019-0000 -------------------------------------------------------------------------------- /example/ASY-001-0000/timestamp.txt: -------------------------------------------------------------------------------- 1 | 2023-01-04T15:02:52-05:00 2 | -------------------------------------------------------------------------------- /example/ASY-001.csv: -------------------------------------------------------------------------------- 1 | IPN;Qnty 2 | PCA-019-0000;2 3 | ASY-002-0001;3 4 | -------------------------------------------------------------------------------- /example/ASY-001.log: -------------------------------------------------------------------------------- 1 | release ASY-001-0000 updated 2 | -------------------------------------------------------------------------------- /example/ASY-001.yml: -------------------------------------------------------------------------------- 1 | hooks: 2 | - date -Iseconds > {{ .RelDir }}/timestamp.txt 3 | - | 4 | echo "processing {{ .SrcDir }}" 5 | echo "IPN {{ .IPN }}" 6 | echo "hi #1" 7 | echo "hi #2" 8 | required: 9 | - ASY-001-0000.html 10 | -------------------------------------------------------------------------------- /example/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | The format of this changelog roughly follows 4 | [keep a changelog](https://keepachangelog.com) 5 | 6 | ## [ASY-001-0000] - 2022-10-23 7 | 8 | - first release of product XYZ 9 | -------------------------------------------------------------------------------- /example/MFG.md: -------------------------------------------------------------------------------- 1 | # Product XYZ Manufacturing notes 2 | 3 | ## Testing 4 | 5 | - point a 6 | - point b 7 | - point c 8 | 9 | ## Certifications 10 | -------------------------------------------------------------------------------- /example/electrical/pcb-design/PCA-019-0000/PCA-019-0000.csv: -------------------------------------------------------------------------------- 1 | IPN;Qnty;MPN;Manufacturer;Ref;Value;Cmp name;Footprint;Description;Vendor;Datasheet;Checked 2 | ANA-000-0000;8;LT1716IS5#TRPBF;Linear Technology;U1, U2, U4, U5, U7, U8, U10, U11;LT1716;LT1716;Package_TO_SOT_SMD:SOT-23-5;;;https://www.analog.com/media/en/technical-documentation/data-sheets/LT1716.pdf; 3 | CAP-000-1001;18;08055C102JAT2A;AVX;C2, C7, C8, C13, C16, C21, C22, C27, C31, C36, C37, C42, C45, C50, C51, C56, C86, C91;10nF_50V;10nF_50V;Capacitor_SMD:C_0603_1608Metric;;;http://datasheets.avx.com/X7RDielectric.pdf;Y 4 | DIO-002-0000;7;MMBZ5245B-7-F;Diodes Incorporated;D2, D8, D18, D22, D28, D32, D38;MMBZ5245B-7-F;MMBZ5245B-7-F;Diode_SMD:D_SOT-23_ANK;;;https://www.diodes.com/assets/Datasheets/MMBZ5221B-MMBZ5259B.pdf;Y 5 | PCB-019-0001;1;;;;;Design XYZ PCB;;;;; 6 | RES-008-220K;4;HV732HTTE2203F;KOA SPEER Electronics;R1, R15, R29, R43;220k_500mW;220k_500mW;Resistor_SMD:R_2010_5025Metric;;;https://www.koaspeer.com/pdfs/HV73.pdf; 7 | SCR-002-0002;1;18a02SDF;screwsRus;S3;;screw #4,2;;;;https://www.screws.com/pdfs/abc.pdf; 8 | -------------------------------------------------------------------------------- /example/electrical/pcb-design/PCA-019-0000/PCB-019-0001: -------------------------------------------------------------------------------- 1 | ../PCB-019-0001 -------------------------------------------------------------------------------- /example/electrical/pcb-design/PCA-019.csv: -------------------------------------------------------------------------------- 1 | "Ref";"Qnty";"Value";"Cmp name";"Footprint";"Description";"Vendor";"IPN";"Datasheet" 2 | "U1, U2, U4, U5, U7, U8, U10, U11, ";"8";"LT1716";"LT1716";"Package_TO_SOT_SMD:SOT-23-5";"";"";"ANA-000-0000";"https://www.analog.com/media/en/technical-documentation/data-sheets/LT1716.pdf" 3 | "D2, D8, D12, D18, D22, D28, D32, D38, ";"8";"MMBZ5245B-7-F";"MMBZ5245B-7-F";"Diode_SMD:D_SOT-23_ANK";"";"";"DIO-002-0000";"https://www.diodes.com/assets/Datasheets/MMBZ5221B-MMBZ5259B.pdf" 4 | "R1, R15, R29, R43, ";"4";"220k_500mW";"220k_500mW";"Resistor_SMD:R_2010_5025Metric";"";"";"RES-008-220K";"https://www.koaspeer.com/pdfs/HV73.pdf" 5 | "C2, C7, C8, C13, C16, C21, C22, C27, C31, C36, C37, C42, C45, C50, C51, C56, C86, C91, ";"18";"10nF_50V";"10nF_50V";"Capacitor_SMD:C_0603_1608Metric";"";"";"CAP-000-1001";"http://datasheets.avx.com/X7RDielectric.pdf" 6 | "TP1, TP2, TP3, ";"3";"";"Test point";;;;; 7 | "TP4, TP5, ";"2";"";"Test point 2";;;;; 8 | -------------------------------------------------------------------------------- /example/electrical/pcb-design/PCA-019.log: -------------------------------------------------------------------------------- 1 | BOM PCA-019-0000 updated 2 | -------------------------------------------------------------------------------- /example/electrical/pcb-design/PCA-019.yml: -------------------------------------------------------------------------------- 1 | # this file is used by GitPLM to describe modifications to a BOM or copy files/dirs to the output directory 2 | remove: 3 | - cmpName: Test point 4 | - cmpName: Test point 2 5 | - ref: D12 6 | add: 7 | - cmpName: "screw #4,2" 8 | ref: S3 9 | ipn: SCR-002-0002 10 | - cmpName: "Design XYZ PCB" 11 | ipn: PCB-019-0001 12 | -------------------------------------------------------------------------------- /example/electrical/pcb-design/PCB-019-0001/README.md: -------------------------------------------------------------------------------- 1 | This directory simulates the PCB output required to manufacture a bare PCB board 2 | -- typically the gerbers, etc. 3 | -------------------------------------------------------------------------------- /example/electrical/pcb-design/PCB-019-0001/gerber.txt: -------------------------------------------------------------------------------- 1 | dummy gerber file 2 | -------------------------------------------------------------------------------- /example/electrical/pcb-design/PCB-019-0001/gerber/bottom.grb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-plm/gitplm/0a092c4a9369c5941f82dd72f2bcacceecfe7978/example/electrical/pcb-design/PCB-019-0001/gerber/bottom.grb -------------------------------------------------------------------------------- /example/electrical/pcb-design/PCB-019-0001/gerber/drill.grb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-plm/gitplm/0a092c4a9369c5941f82dd72f2bcacceecfe7978/example/electrical/pcb-design/PCB-019-0001/gerber/drill.grb -------------------------------------------------------------------------------- /example/electrical/pcb-design/PCB-019-0001/gerber/top.grb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-plm/gitplm/0a092c4a9369c5941f82dd72f2bcacceecfe7978/example/electrical/pcb-design/PCB-019-0001/gerber/top.grb -------------------------------------------------------------------------------- /example/electrical/pcb-design/PCB-019-0001/mfg/bottom.pos: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-plm/gitplm/0a092c4a9369c5941f82dd72f2bcacceecfe7978/example/electrical/pcb-design/PCB-019-0001/mfg/bottom.pos -------------------------------------------------------------------------------- /example/electrical/pcb-design/PCB-019-0001/mfg/top.pos: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-plm/gitplm/0a092c4a9369c5941f82dd72f2bcacceecfe7978/example/electrical/pcb-design/PCB-019-0001/mfg/top.pos -------------------------------------------------------------------------------- /example/electrical/pcb-design/PCB-019-0001/pcb.schematic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-plm/gitplm/0a092c4a9369c5941f82dd72f2bcacceecfe7978/example/electrical/pcb-design/PCB-019-0001/pcb.schematic -------------------------------------------------------------------------------- /example/electrical/pcb-design/PCB-019.log: -------------------------------------------------------------------------------- 1 | BOM PCB-019-0001 updated 2 | -------------------------------------------------------------------------------- /example/electrical/pcb-design/PCB-019.yml: -------------------------------------------------------------------------------- 1 | # this file is used by GitPLM to describe modifications to a BOM or copy files/dirs to the output directory 2 | copy: 3 | - gerber 4 | - mfg 5 | - pcb.schematic 6 | -------------------------------------------------------------------------------- /example/electrical/pcb-design/gerber/bottom.grb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-plm/gitplm/0a092c4a9369c5941f82dd72f2bcacceecfe7978/example/electrical/pcb-design/gerber/bottom.grb -------------------------------------------------------------------------------- /example/electrical/pcb-design/gerber/drill.grb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-plm/gitplm/0a092c4a9369c5941f82dd72f2bcacceecfe7978/example/electrical/pcb-design/gerber/drill.grb -------------------------------------------------------------------------------- /example/electrical/pcb-design/gerber/top.grb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-plm/gitplm/0a092c4a9369c5941f82dd72f2bcacceecfe7978/example/electrical/pcb-design/gerber/top.grb -------------------------------------------------------------------------------- /example/electrical/pcb-design/mfg/bottom.pos: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-plm/gitplm/0a092c4a9369c5941f82dd72f2bcacceecfe7978/example/electrical/pcb-design/mfg/bottom.pos -------------------------------------------------------------------------------- /example/electrical/pcb-design/mfg/top.pos: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-plm/gitplm/0a092c4a9369c5941f82dd72f2bcacceecfe7978/example/electrical/pcb-design/mfg/top.pos -------------------------------------------------------------------------------- /example/electrical/pcb-design/pcb.schematic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-plm/gitplm/0a092c4a9369c5941f82dd72f2bcacceecfe7978/example/electrical/pcb-design/pcb.schematic -------------------------------------------------------------------------------- /example/mechanical/enclosure/ASY-002-0001/ASY-002-0001-all.csv: -------------------------------------------------------------------------------- 1 | IPN;Qnty;MPN;Manufacturer;Ref;Value;Cmp name;Footprint;Description;Vendor;Datasheet;Checked 2 | ASY-012-0012;2;;mycompany;;;;;endcap assembly;;; 3 | MCH-001-0001;2;1051023;bracketsRus;;;;;bracket;;https://www.brackets.com/pdfs/21523.pdf;Y 4 | SCR-002-0002;18;18a02SDF;screwsRus;;;;;#4 screw;;https://www.screws.com/pdfs/abc.pdf; 5 | -------------------------------------------------------------------------------- /example/mechanical/enclosure/ASY-002-0001/ASY-002-0001.csv: -------------------------------------------------------------------------------- 1 | IPN;Qnty;MPN;Manufacturer;Ref;Value;Cmp name;Footprint;Description;Vendor;Datasheet;Checked 2 | ASY-012-0012;2;;mycompany;;;;;endcap assembly;;; 3 | SCR-002-0002;10;18a02SDF;screwsRus;;;;;#4 screw;;https://www.screws.com/pdfs/abc.pdf; 4 | -------------------------------------------------------------------------------- /example/mechanical/enclosure/ASY-002-0001/ASY-012-0012: -------------------------------------------------------------------------------- 1 | ../endcap/ASY-012-0012 -------------------------------------------------------------------------------- /example/mechanical/enclosure/ASY-002-0001/enclosure-drawing.dwg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-plm/gitplm/0a092c4a9369c5941f82dd72f2bcacceecfe7978/example/mechanical/enclosure/ASY-002-0001/enclosure-drawing.dwg -------------------------------------------------------------------------------- /example/mechanical/enclosure/ASY-002.csv: -------------------------------------------------------------------------------- 1 | IPN;Qnty 2 | SCR-002-0002;10 3 | ASY-012-0012;2 4 | -------------------------------------------------------------------------------- /example/mechanical/enclosure/ASY-002.log: -------------------------------------------------------------------------------- 1 | BOM ASY-002-0001 updated 2 | -------------------------------------------------------------------------------- /example/mechanical/enclosure/endcap/ASY-012-0012/ASY-012-0012.csv: -------------------------------------------------------------------------------- 1 | IPN;Qnty;MPN;Manufacturer;Ref;Value;Cmp name;Footprint;Description;Vendor;Datasheet;Checked 2 | MCH-001-0001;1;1051023;bracketsRus;;;;;;;https://www.brackets.com/pdfs/21523.pdf;Y 3 | SCR-002-0002;4;18a02SDF;screwsRus;;;;;;;https://www.screws.com/pdfs/abc.pdf; 4 | -------------------------------------------------------------------------------- /example/mechanical/enclosure/endcap/ASY-012-0012/endcap-drawing.dwg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-plm/gitplm/0a092c4a9369c5941f82dd72f2bcacceecfe7978/example/mechanical/enclosure/endcap/ASY-012-0012/endcap-drawing.dwg -------------------------------------------------------------------------------- /example/mechanical/enclosure/endcap/ASY-012.csv: -------------------------------------------------------------------------------- 1 | IPN;Qnty 2 | MCH-001-0001;1 3 | SCR-002-0002;4 4 | -------------------------------------------------------------------------------- /example/mechanical/enclosure/endcap/ASY-012.log: -------------------------------------------------------------------------------- 1 | BOM ASY-012-0012 updated 2 | -------------------------------------------------------------------------------- /example/partmaster.csv: -------------------------------------------------------------------------------- 1 | IPN;Description;Footprint;Value;Manufacturer;MPN;Datasheet;Priority;Checked 2 | ANA-000-0000;LT1716;Package_TO_SOT_SMD:SOT-23-5;;Linear Technology;LT1716IS5#TRPBF;https://www.analog.com/media/en/technical-documentation/data-sheets/LT1716.pdf;; 3 | ASY-001-0000;productA;;productA;mycompany;;;; 4 | ASY-002-0001;endplate;;endplate;mycompany;;;;Y 5 | ASY-012-0012;endcap assembly;;;mycompany;;;; 6 | CAP-000-1001;1nF, 50V cap;;1nF_50V;Bogus Caps, Inc;1234;;2; 7 | CAP-000-1001;1nF, 50V cap;Capacitor_SMD:C_0805_2012Metric;;AVX;08055C102JAT2A;http://datasheets.avx.com/X7RDielectric.pdf;1;Y 8 | CAP-000-1002;10nF, 50V;Capacitor_SMD:C_0805_2012Metric;10nF_50V;AVX;08055C103JAT2A;http://datasheets.avx.com/X7RDielectric.pdf;; 9 | CAP-000-1005;1.0uF;Capacitor_SMD:C_0805_2012Metric;1.0uF_50V;AVX;08055C105KAT2A;http://datasheets.avx.com/X7RDielectric.pdf;; 10 | CAP-000-470R;470pF, 50V;Capacitor_SMD:C_0603_1608Metric;470pF_50V;AVX;06035C471JAT2A;https://datasheets.avx.com/X7RDielectric.pdf;;Y 11 | CAP-015-1002;10nF/50V;Capacitor_SMD:C_0603_1608Metric;10nF_50V;AVX;06035C103JAT2A;http://datasheets.avx.com/X7RDielectric.pdf;; 12 | DIO-002-0000;SMD diode;Diode_SMD:D_SOT-23_ANK;MMBZ5245B-7-F;Diodes Incorporated;MMBZ5245B-7-F;https://www.diodes.com/assets/Datasheets/MMBZ5221B-MMBZ5259B.pdf;;Y 13 | MCH-001-0001;bracket;;bracket;bracketsRus;1051023;https://www.brackets.com/pdfs/21523.pdf;;Y 14 | PCA-019-0000;PCB assembly;;;mycompany;;;; 15 | PCB-019-0001;PCB;;;mycompany;;;; 16 | RES-008-220K;220k, Resistor;Resistor_SMD:R_2010_5025Metric;220k_500mW;KOA SPEER Electronics;HV732HTTE2203F;https://www.koaspeer.com/pdfs/HV73.pdf;; 17 | SCR-002-0002;#4 screw;;screw #4,2;screwsRus;18a02SDF;https://www.screws.com/pdfs/abc.pdf;; 18 | -------------------------------------------------------------------------------- /file.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/csv" 5 | "fmt" 6 | "io" 7 | "io/fs" 8 | "os" 9 | 10 | "github.com/gocarina/gocsv" 11 | ) 12 | 13 | // load CSV into target data structure. target is modified 14 | func loadCSV(fileName string, target interface{}) error { 15 | file, err := os.OpenFile(fileName, os.O_RDONLY, 0644) 16 | if err != nil { 17 | return err 18 | } 19 | defer file.Close() 20 | 21 | return gocsv.UnmarshalFile(file, target) 22 | } 23 | 24 | func saveCSV(filename string, data interface{}) error { 25 | file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) 26 | if err != nil { 27 | return err 28 | } 29 | defer file.Close() 30 | 31 | return gocsv.MarshalFile(data, file) 32 | } 33 | 34 | // findDir recursively searches the directory tree for a directory name. This skips soft links. 35 | func findDir(name string) (string, error) { 36 | retPath := "" 37 | // WalkDir does not follown symbolic links 38 | err := fs.WalkDir(os.DirFS("./"), ".", func(path string, d fs.DirEntry, err error) error { 39 | if err != nil { 40 | return err 41 | } 42 | if d.IsDir() { 43 | if name == d.Name() { 44 | // found it 45 | retPath = path 46 | } 47 | } 48 | return nil 49 | }) 50 | 51 | if err != nil { 52 | return "", err 53 | } 54 | 55 | if retPath == "" { 56 | return retPath, fmt.Errorf("Dir not found: %v", name) 57 | } 58 | 59 | return retPath, nil 60 | } 61 | 62 | // findFile recursively searches the directory tree to find a file and returns the path 63 | func findFile(name string) (string, error) { 64 | retPath := "" 65 | // WalkDir does not follown symbolic links 66 | err := fs.WalkDir(os.DirFS("./"), ".", func(path string, d fs.DirEntry, err error) error { 67 | if err != nil { 68 | return err 69 | } 70 | if !d.IsDir() { 71 | if name == d.Name() { 72 | // found it 73 | retPath = path 74 | } 75 | } 76 | return nil 77 | }) 78 | 79 | if err != nil { 80 | return "", err 81 | } 82 | 83 | if retPath == "" { 84 | return retPath, fmt.Errorf("File not found: %v", name) 85 | } 86 | 87 | return retPath, nil 88 | } 89 | 90 | func initCSV() { 91 | gocsv.SetCSVReader(func(in io.Reader) gocsv.CSVReader { 92 | r := csv.NewReader(in) 93 | r.Comma = ';' 94 | return r 95 | }) 96 | 97 | gocsv.SetCSVWriter(func(out io.Writer) *gocsv.SafeCSVWriter { 98 | writer := csv.NewWriter(out) 99 | writer.Comma = ';' 100 | return gocsv.NewSafeCSVWriter(writer) 101 | }) 102 | } 103 | 104 | func exists(path string) (bool, error) { 105 | _, err := os.Stat(path) 106 | if err == nil { 107 | return true, nil 108 | } 109 | if os.IsNotExist(err) { 110 | return false, nil 111 | } 112 | return false, err 113 | } 114 | -------------------------------------------------------------------------------- /flow1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-plm/gitplm/0a092c4a9369c5941f82dd72f2bcacceecfe7978/flow1.png -------------------------------------------------------------------------------- /flow2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-plm/gitplm/0a092c4a9369c5941f82dd72f2bcacceecfe7978/flow2.png -------------------------------------------------------------------------------- /gitplm-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-plm/gitplm/0a092c4a9369c5941f82dd72f2bcacceecfe7978/gitplm-logo.png -------------------------------------------------------------------------------- /gitplm-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 40 | 42 | 46 | Git 57 | PLM 68 | 79 | 90 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/git-plm/gitplm 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/gocarina/gocsv v0.0.0-20211020200912-82fc2684cc48 7 | github.com/samber/lo v1.33.0 8 | gopkg.in/yaml.v2 v2.4.0 9 | ) 10 | 11 | require ( 12 | github.com/otiai10/copy v1.9.0 // indirect 13 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect 14 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/gocarina/gocsv v0.0.0-20211020200912-82fc2684cc48 h1:hLeicZW4XBuaISuJPfjkprg0SP0xxsQmb31aJZ6lnIw= 3 | github.com/gocarina/gocsv v0.0.0-20211020200912-82fc2684cc48/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= 4 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 5 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 6 | github.com/otiai10/copy v1.9.0 h1:7KFNiCgZ91Ru4qW4CWPf/7jqtxLagGRmIxWldPP9VY4= 7 | github.com/otiai10/copy v1.9.0/go.mod h1:hsfX19wcn0UWIHUQ3/4fHuehhk2UyArQ9dVFAn3FczI= 8 | github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= 9 | github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= 10 | github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= 11 | github.com/otiai10/mint v1.4.0/go.mod h1:gifjb2MYOoULtKLqUAEILUG/9KONW6f7YsJ6vQLTlFI= 12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 13 | github.com/samber/lo v1.33.0 h1:2aKucr+rQV6gHpY3bpeZu69uYoQOzVhGT3J22Op6Cjk= 14 | github.com/samber/lo v1.33.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8= 15 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 16 | github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= 17 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= 18 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= 19 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= 20 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 21 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 22 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 23 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 24 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 25 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 26 | -------------------------------------------------------------------------------- /ipn.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "regexp" 7 | "strconv" 8 | 9 | "github.com/samber/lo" 10 | ) 11 | 12 | type ipn string 13 | 14 | var reIpn = regexp.MustCompile(`^([A-Z][A-Z][A-Z])-(\d\d\d)-(\d\d\d\d)$`) 15 | var reC = regexp.MustCompile(`^[A-Z][A-Z][A-Z]$`) 16 | 17 | func newIpn(s string) (ipn, error) { 18 | _, _, _, err := ipn(s).parse() 19 | return ipn(s), err 20 | } 21 | 22 | func newIpnParts(c string, n, v int) (ipn, error) { 23 | if n < 0 || n > 999 { 24 | return "", errors.New("N out of range") 25 | } 26 | 27 | if v < 0 || v > 9999 { 28 | return "", errors.New("V out of range") 29 | } 30 | 31 | if len(c) != 3 { 32 | return "", errors.New("C must be 3 chars") 33 | } 34 | 35 | if reC.FindString(c) == "" { 36 | return "", errors.New("C must be in format CCC") 37 | } 38 | 39 | return ipn(fmt.Sprintf("%v-%03v-%04v", c, n, v)), nil 40 | } 41 | 42 | func (i ipn) String() string { 43 | return string(i) 44 | } 45 | 46 | // parse() returns C (category), N (number), V (variation) 47 | func (i ipn) parse() (string, int, int, error) { 48 | groups := reIpn.FindStringSubmatch(string(i)) 49 | if len(groups) < 4 { 50 | return "", 0, 0, errors.New("Error parsing ipn") 51 | } 52 | 53 | c := groups[1] 54 | n, err := strconv.Atoi(groups[2]) 55 | if err != nil { 56 | return "", 0, 0, fmt.Errorf("Error parsing N: %v", err) 57 | } 58 | v, err := strconv.Atoi(groups[3]) 59 | if err != nil { 60 | return "", 0, 0, fmt.Errorf("Error parsing V: %v", err) 61 | } 62 | 63 | return c, n, v, nil 64 | } 65 | 66 | func (i ipn) c() (string, error) { 67 | c, _, _, err := i.parse() 68 | return c, err 69 | } 70 | 71 | func (i ipn) n() (int, error) { 72 | _, n, _, err := i.parse() 73 | return n, err 74 | } 75 | 76 | func (i ipn) v() (int, error) { 77 | _, _, v, err := i.parse() 78 | return v, err 79 | } 80 | 81 | var ourIPNs = []string{"PCA", "PCB", "ASY", "DOC", "DFW", "DSW", "DCL", "FIX"} 82 | 83 | func (i ipn) isOurIPN() (bool, error) { 84 | c, _, _, err := i.parse() 85 | if err != nil { 86 | return false, err 87 | } 88 | return lo.Contains(ourIPNs, c), nil 89 | } 90 | 91 | var boms = []string{"PCA", "ASY"} 92 | 93 | func (i ipn) hasBOM() (bool, error) { 94 | c, _, _, err := i.parse() 95 | if err != nil { 96 | return false, err 97 | } 98 | return lo.Contains(boms, c), nil 99 | } 100 | -------------------------------------------------------------------------------- /ipn_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | type ipnTest struct { 6 | ipn string 7 | c string 8 | n int 9 | v int 10 | valid bool 11 | } 12 | 13 | func TestIpn(t *testing.T) { 14 | tests := []ipnTest{ 15 | {"PCB-001-0500", "PCB", 1, 500, true}, 16 | {"ASY-200-1000", "ASY", 200, 1000, true}, 17 | {"SY-200-1000", "", 0, 0, false}, 18 | {"ASY-20-1000", "", 0, 0, false}, 19 | {"ASY-200-100", "", 0, 0, false}, 20 | } 21 | 22 | for _, test := range tests { 23 | c, n, v, err := ipn(test.ipn).parse() 24 | if test.valid && err != nil { 25 | t.Errorf("%v error: %v", test.ipn, err) 26 | } else if !test.valid && err == nil { 27 | t.Errorf("%v expected error but got none", test.ipn) 28 | } 29 | 30 | if c != test.c { 31 | t.Errorf("%v, C failed, exp %v, got %v", 32 | test.ipn, test.c, c) 33 | } 34 | if n != test.n { 35 | t.Errorf("%v, N failed, exp %v, got %v", 36 | test.ipn, test.n, n) 37 | } 38 | if v != test.v { 39 | t.Errorf("%v, V failed, exp %v, got %v", 40 | test.ipn, test.v, v) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | ) 12 | 13 | var version = "Development" 14 | 15 | func main() { 16 | initCSV() 17 | 18 | flagRelease := flag.String("release", "", "Process release for IPN (ex: PCB-056-0005, ASY-002-0023)") 19 | flagVersion := flag.Bool("version", false, "display version of this application") 20 | flag.Parse() 21 | 22 | if *flagVersion { 23 | if version == "" { 24 | version = "Development" 25 | } 26 | fmt.Printf("%v\n", version) 27 | os.Exit(0) 28 | } 29 | 30 | var gLog strings.Builder 31 | logMsg := func(s string) { 32 | _, err := gLog.Write([]byte(s)) 33 | if err != nil { 34 | log.Println("Error writing to gLog: ", err) 35 | } 36 | log.Println(s) 37 | } 38 | 39 | if *flagRelease != "" { 40 | relPath, err := processRelease(*flagRelease, &gLog) 41 | if err != nil { 42 | logMsg(fmt.Sprintf("release error: %v\n", err)) 43 | } else { 44 | logMsg(fmt.Sprintf("release %v updated\n", *flagRelease)) 45 | } 46 | 47 | if relPath != "" { 48 | // write out log file 49 | c, n, _, err := ipn(*flagRelease).parse() 50 | if err != nil { 51 | log.Fatal("Error parsing bom IPN: ", err) 52 | } 53 | fn := fmt.Sprintf("%v-%03v.log", c, n) 54 | logFilePath := filepath.Join(relPath, fn) 55 | err = ioutil.WriteFile(logFilePath, []byte(gLog.String()), 0644) 56 | if err != nil { 57 | log.Println("Error writing log file: ", err) 58 | } 59 | } 60 | 61 | return 62 | } 63 | 64 | fmt.Println("Error, please specify an action") 65 | flag.Usage() 66 | } 67 | -------------------------------------------------------------------------------- /partmaster.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | ) 7 | 8 | type partmasterLine struct { 9 | IPN ipn `csv:"IPN"` 10 | Description string `csv:"Description"` 11 | Footprint string `csv:"Footprint"` 12 | Value string `csv:"Value"` 13 | Manufacturer string `csv:"Manufacturer"` 14 | MPN string `csv:"MPN"` 15 | Datasheet string `csv:"Datasheet"` 16 | Priority int `csv:"Priority"` 17 | Checked string `csv:"Checked"` 18 | } 19 | 20 | type partmaster []*partmasterLine 21 | 22 | // findPart returns part with highest priority 23 | func (p *partmaster) findPart(pn ipn) (*partmasterLine, error) { 24 | found := []*partmasterLine{} 25 | for _, l := range *p { 26 | if l.IPN == pn { 27 | found = append(found, l) 28 | } 29 | } 30 | 31 | if len(found) <= 0 { 32 | return nil, fmt.Errorf("Part not found") 33 | } 34 | 35 | sort.Sort(byPriority(found)) 36 | 37 | if len(found) > 1 { 38 | // fill in blank fields with values from other items 39 | for i := 1; i < len(found); i++ { 40 | if found[0].Description == "" && found[i].Description != "" { 41 | found[0].Description = found[i].Description 42 | } 43 | if found[0].Footprint == "" && found[i].Footprint != "" { 44 | found[0].Footprint = found[i].Footprint 45 | } 46 | if found[0].Value == "" && found[i].Value != "" { 47 | found[0].Value = found[i].Value 48 | } 49 | } 50 | } 51 | 52 | return found[0], nil 53 | } 54 | 55 | // type for sorting 56 | type byPriority []*partmasterLine 57 | 58 | func (p byPriority) Len() int { return len(p) } 59 | func (p byPriority) Swap(i, j int) { p[i], p[j] = p[j], p[i] } 60 | func (p byPriority) Less(i, j int) bool { return p[i].Priority < p[j].Priority } 61 | -------------------------------------------------------------------------------- /partmaster_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gocarina/gocsv" 7 | ) 8 | 9 | var pmIn = ` 10 | IPN;Description;Value;Manufacturer;MPN;Priority 11 | CAP-001-1001;superduper cap;;CapsInc;10045;2 12 | CAP-001-1001;;10k;MaxCaps;abc2322;1 13 | CAP-001-1002;;;MaxCaps;abc2323; 14 | ` 15 | 16 | func TestPartmaster(t *testing.T) { 17 | initCSV() 18 | pm := partmaster{} 19 | err := gocsv.UnmarshalBytes([]byte(pmIn), &pm) 20 | if err != nil { 21 | t.Fatalf("Error parsing pmIn: %v", err) 22 | } 23 | 24 | p, err := pm.findPart("CAP-001-1001") 25 | if err != nil { 26 | t.Fatalf("Error finding part CAP-001-1001: %v", err) 27 | } 28 | 29 | if p.MPN != "abc2322" { 30 | t.Errorf("Got wrong part for CAP-001-1001, %v", p.MPN) 31 | } 32 | 33 | if p.Description != "superduper cap" { 34 | t.Errorf("Got wrong description for CAP-001-1001: %v", p.Description) 35 | } 36 | 37 | if p.Value != "10k" { 38 | t.Errorf("Got wrong value for CAP-001-1001: %v", p.Value) 39 | } 40 | 41 | p, err = pm.findPart("CAP-001-1002") 42 | if err != nil { 43 | t.Fatalf("Error finding part CAP-001-1002: %v", err) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /partnumbers.md: -------------------------------------------------------------------------------- 1 | This document has been moved to: 2 | https://github.com/git-plm/parts/blob/main/partnumbers.md 3 | -------------------------------------------------------------------------------- /rel-script.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "os" 8 | "os/exec" 9 | "path" 10 | "sort" 11 | "strings" 12 | "text/template" 13 | 14 | "github.com/otiai10/copy" 15 | ) 16 | 17 | type relScript struct { 18 | Description string 19 | Remove []bomLine 20 | Add []bomLine 21 | Copy []string 22 | Hooks []string 23 | Required []string 24 | } 25 | 26 | func (rs *relScript) processBom(b bom) (bom, error) { 27 | ret := b 28 | for _, r := range rs.Remove { 29 | if r.CmpName != "" { 30 | retM := bom{} 31 | for _, l := range ret { 32 | if l.CmpName != r.CmpName { 33 | retM = append(retM, l) 34 | } 35 | } 36 | ret = retM 37 | } 38 | 39 | if r.Ref != "" { 40 | retM := bom{} 41 | for _, l := range ret { 42 | l.removeRef(r.Ref) 43 | if l.Qnty > 0 { 44 | retM = append(retM, l) 45 | } 46 | } 47 | ret = retM 48 | } 49 | } 50 | 51 | for _, a := range rs.Add { 52 | refs := strings.Split(a.Ref, ",") 53 | a.Qnty = len(refs) 54 | if a.Qnty < 0 { 55 | a.Qnty = 1 56 | } 57 | // for some reason we need to make a copy or it 58 | // will alias the last one 59 | c := a 60 | ret = append(ret, &c) 61 | } 62 | 63 | sort.Sort(ret) 64 | 65 | return ret, nil 66 | } 67 | 68 | func (rs *relScript) copy(srcDir, destDir string) error { 69 | for _, c := range rs.Copy { 70 | opts := copy.Options{ 71 | OnSymlink: func(src string) copy.SymlinkAction { 72 | return copy.Deep 73 | }, 74 | OnDirExists: func(src, dest string) copy.DirExistsAction { 75 | return copy.Replace 76 | }, 77 | } 78 | srcPath := path.Join(srcDir, c) 79 | destPath := path.Join(destDir, c) 80 | err := copy.Copy(srcPath, destPath, opts) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | log.Printf("%v copied to release dir\n", c) 86 | } 87 | 88 | return nil 89 | } 90 | 91 | func (rs *relScript) hooks(pn string, srcDir, destDir string) error { 92 | data := struct { 93 | SrcDir string 94 | RelDir string 95 | IPN string 96 | }{ 97 | SrcDir: srcDir, 98 | RelDir: destDir, 99 | IPN: pn, 100 | } 101 | 102 | for _, h := range rs.Hooks { 103 | t, err := template.New("hook").Parse(h) 104 | if err != nil { 105 | return fmt.Errorf("Error parsing hook: %v: %v", h, err) 106 | } 107 | 108 | var out strings.Builder 109 | 110 | err = t.Execute(&out, data) 111 | if err != nil { 112 | return fmt.Errorf("Error parsing hook: %v: %v", h, err) 113 | } 114 | 115 | cmd := exec.Command("/bin/sh", "-c", out.String()) 116 | 117 | stdout, err := cmd.StdoutPipe() 118 | if err != nil { 119 | log.Fatal(err) 120 | } 121 | stderr, err := cmd.StderrPipe() 122 | if err != nil { 123 | log.Fatal(err) 124 | } 125 | 126 | // Start the command 127 | if err := cmd.Start(); err != nil { 128 | log.Fatal(err) 129 | } 130 | 131 | // Copy the command's stdout and stderr to the Go program's stdout 132 | go func() { 133 | _, _ = io.Copy(os.Stdout, stdout) 134 | }() 135 | 136 | go func() { 137 | _, _ = io.Copy(os.Stderr, stderr) 138 | }() 139 | 140 | // Wait for the command to exit 141 | if err := cmd.Wait(); err != nil { 142 | log.Println("Error running hook: ", err) 143 | log.Println("Hook contents: ") 144 | fmt.Print(out.String()) 145 | } 146 | } 147 | return nil 148 | } 149 | 150 | func (rs *relScript) required(destDir string) error { 151 | for _, r := range rs.Required { 152 | p := path.Join(destDir, r) 153 | e, err := exists(p) 154 | if err != nil { 155 | return fmt.Errorf("Error looking for required file: %v: %v", p, err) 156 | } 157 | 158 | if !e { 159 | return fmt.Errorf("Required file does not exist, please generate it: %v", p) 160 | } 161 | } 162 | 163 | return nil 164 | } 165 | -------------------------------------------------------------------------------- /rel-script_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/gocarina/gocsv" 9 | "gopkg.in/yaml.v2" 10 | ) 11 | 12 | var modFile = ` 13 | # yaml file 14 | description: modify bom 15 | remove: 16 | - cmpName: Test point 2 17 | - ref: D13 18 | - ref: R11 19 | add: 20 | - cmpName: "screw #4,2" 21 | ref: S3 22 | ipn: SCR-002-0002 23 | ` 24 | 25 | var bomIn = ` 26 | Ref;Qnty;Value;Cmp name;Footprint;Description;Vendor;IPN;Datasheet 27 | TP4, TP5;2;;Test point 2;;;;; 28 | R1, R2, ;2;;100K_100mw;;;;RES-006-0232; 29 | D1, D2, D13, D14;4;;diode;;;;DIO-023-0023; 30 | "R11, ";"1";"2010_500mW_1%_3000V_10M";"2010_500mW_1%_3000V_10M";"Resistor_SMD:R_2010_5025Metric";"";"";"RES-008-1005";"https://www.bourns.com/docs/Product-Datasheets/CHV.pdf" 31 | ` 32 | 33 | var bomExp = ` 34 | Ref;Qnty;Value;Cmp name;Footprint;Description;Vendor;IPN;Datasheet 35 | D1, D2, D14;3;;diode;;;;DIO-023-0023; 36 | R1, R2;2;;100K_100mw;;;;RES-006-0232; 37 | S3;1;;screw #4,2;;;;SCR-002-0002; 38 | ` 39 | 40 | func TestRelScript(t *testing.T) { 41 | initCSV() 42 | bIn := bom{} 43 | err := gocsv.UnmarshalBytes([]byte(bomIn), &bIn) 44 | if err != nil { 45 | t.Errorf("error parsing bomIn: %v", err) 46 | } 47 | 48 | bExp := bom{} 49 | err = gocsv.UnmarshalBytes([]byte(bomExp), &bExp) 50 | if err != nil { 51 | t.Errorf("error parsing bomExp: %v", err) 52 | } 53 | 54 | rs := relScript{} 55 | 56 | err = yaml.Unmarshal([]byte(modFile), &rs) 57 | if err != nil { 58 | t.Errorf("error parsing yaml: %v", err) 59 | } 60 | 61 | bModified, err := rs.processBom(bIn) 62 | if err != nil { 63 | t.Errorf("error processing bom: %v", err) 64 | } 65 | 66 | if reflect.DeepEqual(bExp, bModified) != true { 67 | t.Error("bExp not the same as bModified") 68 | fmt.Printf("bExp: %v", bExp) 69 | fmt.Printf("bModified: %v", bModified) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /release.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "os" 8 | "path" 9 | "path/filepath" 10 | "sort" 11 | "strings" 12 | 13 | "gopkg.in/yaml.v2" 14 | ) 15 | 16 | func processRelease(relPn string, relLog *strings.Builder) (string, error) { 17 | c, n, _, err := ipn(relPn).parse() 18 | if err != nil { 19 | return "", fmt.Errorf("error parsing bom %v IPN : %v", relPn, err) 20 | } 21 | 22 | relPnBase := fmt.Sprintf("%v-%03v", c, n) 23 | 24 | bomFile := relPnBase + ".csv" 25 | ymlFile := relPnBase + ".yml" 26 | bomExists := false 27 | ymlExists := false 28 | sourceDir := "" 29 | 30 | bomFilePath, err := findFile(bomFile) 31 | if err == nil { 32 | bomExists = true 33 | sourceDir = filepath.Dir(bomFilePath) 34 | } 35 | 36 | ymlFilePath, err := findFile(ymlFile) 37 | if err == nil { 38 | ymlExists = true 39 | sourceDir = filepath.Dir(ymlFilePath) 40 | } 41 | 42 | if !ymlExists && !bomExists { 43 | return "", errors.New("Could not find BOM or YML file for release IPN") 44 | } 45 | 46 | if bomExists && ymlExists { 47 | bomDir := filepath.Dir(bomFilePath) 48 | ymlDir := filepath.Dir(ymlFilePath) 49 | 50 | if bomDir != ymlDir { 51 | return "", fmt.Errorf("BOM and YML files should be in the same directory: %v %v", bomFilePath, ymlFilePath) 52 | } 53 | } 54 | 55 | // Create output release dir 56 | releaseDir := filepath.Join(sourceDir, relPn) 57 | 58 | dirExists, err := exists(releaseDir) 59 | if err != nil { 60 | return sourceDir, err 61 | } 62 | 63 | if !dirExists { 64 | err = os.Mkdir(releaseDir, 0755) 65 | if err != nil { 66 | return sourceDir, err 67 | } 68 | } 69 | 70 | writeFilePath := filepath.Join(releaseDir, relPn+".csv") 71 | 72 | logErr := func(s string) { 73 | _, err := relLog.Write([]byte(s)) 74 | if err != nil { 75 | log.Println("Error writing to relLog: ", err) 76 | } 77 | log.Println(s) 78 | } 79 | 80 | partmasterPath, err := findFile("partmaster.csv") 81 | if err != nil { 82 | return sourceDir, fmt.Errorf("Error, partmaster.csv not found in any dir") 83 | } 84 | 85 | p := partmaster{} 86 | err = loadCSV(partmasterPath, &p) 87 | if err != nil { 88 | return sourceDir, err 89 | } 90 | 91 | b := bom{} 92 | 93 | if bomExists { 94 | err = loadCSV(bomFilePath, &b) 95 | if err != nil { 96 | return sourceDir, err 97 | } 98 | } 99 | 100 | if ymlExists { 101 | ymlBytes, err := os.ReadFile(ymlFilePath) 102 | if err != nil { 103 | return sourceDir, fmt.Errorf("Error loading yml file: %v", err) 104 | } 105 | 106 | rs := relScript{} 107 | err = yaml.Unmarshal(ymlBytes, &rs) 108 | if err != nil { 109 | return sourceDir, fmt.Errorf("Error parsing yml: %v", err) 110 | } 111 | 112 | if bomExists { 113 | b, err = rs.processBom(b) 114 | if err != nil { 115 | return sourceDir, fmt.Errorf("Error processing bom with yml file: %v", err) 116 | } 117 | } 118 | 119 | // run hooks 120 | err = rs.hooks(relPn, sourceDir, releaseDir) 121 | if err != nil { 122 | return sourceDir, fmt.Errorf("Error running hooks specified in YML: %v", err) 123 | } 124 | 125 | // copy stuff to release dir specified in YML file 126 | err = rs.copy(sourceDir, releaseDir) 127 | if err != nil { 128 | return sourceDir, fmt.Errorf("Error copying files specified in YML: %v", err) 129 | } 130 | 131 | // check if required files are present in release 132 | err = rs.required(releaseDir) 133 | if err != nil { 134 | return sourceDir, err 135 | } 136 | } 137 | 138 | if !bomExists { 139 | // nothing else to do 140 | return sourceDir, nil 141 | } 142 | 143 | // always sort BOM for good measure 144 | sort.Sort(b) 145 | 146 | // merge in partmaster info into BOM 147 | b.mergePartmaster(p, logErr) 148 | 149 | err = saveCSV(writeFilePath, b) 150 | if err != nil { 151 | return sourceDir, fmt.Errorf("Error writing BOM: %v", err) 152 | } 153 | 154 | // copy MFG.md and CHANGELOG.md if they exist 155 | assetsToCopy := []string{"MFG.md", "CHANGELOG.md"} 156 | for _, a := range assetsToCopy { 157 | aPath := path.Join(sourceDir, a) 158 | aPathExists, err := exists(aPath) 159 | if err != nil { 160 | return sourceDir, err 161 | } 162 | if aPathExists { 163 | aDest := path.Join(releaseDir, a) 164 | data, err := os.ReadFile(aPath) 165 | if err != nil { 166 | return sourceDir, fmt.Errorf("Error reading %v: %v", aPath, err) 167 | } 168 | 169 | err = os.WriteFile(aDest, data, 0644) 170 | if err != nil { 171 | return sourceDir, fmt.Errorf("Error writing %v: %v", aDest, err) 172 | } 173 | } 174 | } 175 | 176 | // create combined BOM with all sub assemblies if we have any PCB or ASY line items 177 | // process all special IPNS 178 | // if BOM is found, then include in roll-up BOM 179 | // create soft link to release directory 180 | foundSub := false 181 | for _, l := range b { 182 | // clear refs in purchase bom 183 | l.Ref = "" 184 | isOurs, _ := l.IPN.isOurIPN() 185 | if isOurs { 186 | // look for release package 187 | dir, err := findDir(l.IPN.String()) 188 | if err != nil { 189 | return sourceDir, fmt.Errorf("Missing release package: %v", err) 190 | } 191 | // soft link to that package 192 | dirRel, err := filepath.Rel(releaseDir, dir) 193 | if err != nil { 194 | return sourceDir, fmt.Errorf("Error creating rel path for %v: %v", 195 | dir, err) 196 | } 197 | linkPath := path.Join(releaseDir, l.IPN.String()) 198 | os.Remove(linkPath) 199 | err = os.Symlink(dirRel, linkPath) 200 | if err != nil { 201 | return sourceDir, fmt.Errorf("Error creating symlink %v: %v", 202 | dir, err) 203 | } 204 | hasBOM, _ := l.IPN.hasBOM() 205 | if hasBOM { 206 | foundSub = true 207 | err = b.processOurIPN(l.IPN, l.Qnty) 208 | if err != nil { 209 | return sourceDir, fmt.Errorf("Error proccessing sub %v: %v", l.IPN, err) 210 | } 211 | } 212 | } 213 | } 214 | 215 | if foundSub { 216 | // merge in partmaster info into BOM 217 | b.mergePartmaster(p, logErr) 218 | // write out combined BOM 219 | sort.Sort(b) 220 | writePath := filepath.Join(releaseDir, relPn+"-all.csv") 221 | // write out purchase bom 222 | err := saveCSV(writePath, b) 223 | if err != nil { 224 | return sourceDir, fmt.Errorf("Error writing purchase bom %v", err) 225 | } 226 | } 227 | 228 | return sourceDir, nil 229 | } 230 | -------------------------------------------------------------------------------- /tools/gitplm_bom.py: -------------------------------------------------------------------------------- 1 | # 2 | # Example python script to generate a BOM from a KiCad generic netlist 3 | # 4 | # Example: Sorted and Grouped CSV BOM 5 | # 6 | 7 | """ 8 | @package 9 | Output: CSV (comma-separated) 10 | Grouped By: Value, Footprint 11 | Sorted By: Ref 12 | Fields: Ref, Qnty, Value, Cmp name, Footprint, Description, Vendor 13 | 14 | Command line: 15 | python "pathToFile/bom_csv_grouped_by_value_with_fp.py" "%I" "%O.csv" 16 | """ 17 | 18 | # Import the KiCad python helper module and the csv formatter 19 | import kicad_netlist_reader 20 | import csv 21 | import sys 22 | 23 | # Generate an instance of a generic netlist, and load the netlist tree from 24 | # the command line option. If the file doesn't exist, execution will stop 25 | net = kicad_netlist_reader.netlist(sys.argv[1]) 26 | 27 | # Open a file to write to, if the file cannot be opened output to stdout 28 | # instead 29 | 30 | # look for PCB-NNN.yml file in output directory and then name output file to 31 | # PCB-NNN.csv. If PCB-NNN.csv file exists, then replace it. 32 | 33 | from pathlib import Path 34 | 35 | bomFile = Path(sys.argv[2]) 36 | bomDir = bomFile.parent 37 | 38 | ymlFiles = sorted(Path(bomDir).glob('PCA-*.yml')) 39 | if len(ymlFiles) == 1: 40 | print("Found yml file: ", ymlFiles[0].name) 41 | bomFile = bomDir / Path(ymlFiles[0].stem + ".csv") 42 | else: 43 | # look for existing BOM and overwite it 44 | pcbCsvFiles = sorted(Path(bomDir).glob('PCA-*.csv')) 45 | if len(pcbCsvFiles) == 1: 46 | print("Found existing bom file: ", pcbCsvFiles[0].name) 47 | bomFile = pcbCsvFiles[0] 48 | else: 49 | print() 50 | print("WARNING: Did not find yml config file, please create a " 51 | + "PCA-NNN.yml config file so we " 52 | + "know the part number for this PCB.") 53 | print() 54 | 55 | print("Outputing bomfile: ", str(bomFile)) 56 | 57 | try: 58 | f = open(str(bomFile), 'w') 59 | except IOError: 60 | e = "Can't open output file for writing: " + sys.argv[2] 61 | print(__file__, ":", e, sys.stderr) 62 | f = sys.stdout 63 | 64 | # Create a new csv writer object to use as the output formatter 65 | out = csv.writer(f, lineterminator='\n', delimiter=';', quotechar='\"', quoting=csv.QUOTE_ALL) 66 | 67 | # Output a set of rows for a header providing general information 68 | # don't write header info as that breaks automatic parsing by CSV tools 69 | #out.writerow(['Source:', net.getSource()]) 70 | #out.writerow(['Date:', net.getDate()]) 71 | #out.writerow(['Tool:', net.getTool()]) 72 | #out.writerow( ['Generator:', sys.argv[0]] ) 73 | #out.writerow(['Component Count:', len(net.components)]) 74 | out.writerow(['Ref', 'Qnty', 'Value', 'Cmp name', 'Footprint', 'Description', 75 | 'Vendor', 'IPN', 'Datasheet']) 76 | 77 | # Get all of the components in groups of matching parts + values 78 | # (see ky_generic_netlist_reader.py) 79 | grouped = net.groupComponents() 80 | 81 | # Output all of the component information 82 | for group in grouped: 83 | refs = "" 84 | 85 | # Add the reference of every component in the group and keep a reference 86 | # to the component so that the other data can be filled in once per group 87 | for component in group: 88 | refs += component.getRef() + ", " 89 | c = component 90 | 91 | # Fill in the component groups common data 92 | out.writerow([refs, len(group), c.getValue(), c.getPartName(), c.getFootprint(), 93 | c.getDescription(), c.getField("Vendor"), c.getField("IPN"), c.getDatasheet()]) 94 | --------------------------------------------------------------------------------