├── .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 | 
2 |
3 | [](https://github.com/git-plm/gitplm/actions)
4 | 
5 | [](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 |
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 |
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 |
--------------------------------------------------------------------------------