Compare commits

...

47 Commits

Author SHA1 Message Date
Salvador E. Tropea e49efcf21d Removed manual Digi-Key plug-in installation
- Now in the docker images
2022-03-21 18:38:58 -03:00
Salvador E. Tropea 3896f1b910 Switched to KiCost/master 2022-03-21 18:08:24 -03:00
Salvador E. Tropea dc5924901b Fixed command line for `--list`
- PCB and SCH can be needed
- Adapted the test for --list
2021-11-15 13:12:15 -03:00
Salvador E. Tropea 2e4f5d6596 Internal BoM: the field used for variants doesn't produce conflicts.
- Fixes: #100
2021-11-15 12:14:55 -03:00
Salvador E. Tropea b86b5371d7 `--list`: problems with layers and fields specific for the project.
-  Fixes INTI-CMNB/kibot_variants_arduprog#4
2021-11-15 11:34:57 -03:00
Salvador E. Tropea 3d53b874e2 Fixed problems with schematic fields containing double quotes.
- Fixes #98
2021-11-15 11:12:39 -03:00
Salvador E. Tropea 324b3772e0 Useless change to trigger a test. 2021-10-28 18:42:22 -03:00
Salvador E. Tropea 7bd8c068b8 KiCost refresh 2021-10-28 18:40:21 -03:00
Salvador E. Tropea 4595f2a7f8 dapted to the new KiCost configuration mechanism 2021-10-28 18:10:12 -03:00
Salvador E. Tropea 103a338ece Added `extra_info_display` to the Specs. 2021-10-27 15:58:33 -03:00
Salvador E. Tropea ccfb6ce734 Int BoM + KiCost: Added option to control the distributor description
- Disabled by default
- Also fixed KiCost tests to avoid using Digi-Key plug-in when not
  needed
2021-10-27 15:08:12 -03:00
Salvador E. Tropea 3d6840cd85 Enabled description column. 2021-10-26 13:45:55 -03:00
Salvador E. Tropea bd76069be3 Now Spec columns can be renamed, joined, etc. 2021-10-26 10:46:57 -03:00
Salvador E. Tropea e2d71c38a0 Useless change to trigger a test. 2021-10-25 17:15:17 -03:00
Salvador E. Tropea 0156240a72 Fixed KiCost submodule tracking 2021-10-25 17:12:35 -03:00
Salvador E. Tropea b50bc4f6d9 Fixed extra os imort in xlsx_writer 2021-10-25 16:58:30 -03:00
Salvador E. Tropea ef1097375c Removed DIGIKEY_* debug definitions 2021-10-25 16:56:18 -03:00
Salvador E. Tropea 13d2e43c15 Updated docs (sort_style) 2021-10-25 13:57:59 -03:00
Salvador E. Tropea 8e9d5f69e6 Specs sheet: fixed column width comutation 2021-10-25 13:57:13 -03:00
Salvador E. Tropea 5c8f6334a5 Specs sheet: fixed headings row didn't compute width/height 2021-10-25 13:22:44 -03:00
Salvador E. Tropea d5d4c9f1a6 Updated KiCost submodule 2021-10-25 12:23:01 -03:00
Salvador E. Tropea b895cc7604 Internal BoM: Fixed message about wrong distributor
- The name was `None` instead of the real name
2021-10-25 12:00:57 -03:00
Salvador E. Tropea 1c018e6511 Added "Row" column to the Specs, also differentiated the generated cols 2021-10-20 14:36:10 -03:00
Salvador E. Tropea 20ead17d4a Internal BoM: two other options for the sorting criteria. 2021-10-20 13:55:36 -03:00
Salvador E. Tropea 79a363e49f Unified the BoM XLSX vertical align (centered) 2021-10-20 13:19:49 -03:00
Salvador E. Tropea b2afeadc5c Now KiCost spreadsheet uses our sorting. 2021-10-20 13:13:09 -03:00
Salvador E. Tropea 58fb9fe09a Added Specs columns configuration. 2021-10-20 12:36:08 -03:00
Salvador E. Tropea 7a6a264c5f Adapted to the new Digi-Key cache style and fixed return value 2021-10-20 11:54:33 -03:00
Salvador E. Tropea 616cdcd6bf Added catch for KiCost errors. 2021-10-20 11:50:34 -03:00
Salvador E. Tropea adfc509fdd Added options to control KiCost APIs.
- Removed the hardcoded ones.
2021-10-20 10:08:17 -03:00
Salvador E. Tropea 87bc028362 Internal BoM: Added Specs sheet generation. 2021-10-19 13:52:55 -03:00
Salvador E. Tropea f0384e501b Updated KiCost. 2021-10-19 09:29:00 -03:00
Salvador E. Tropea 51e2b0789a Forced Digi-Key API in the KiCost sheet.
- This is what we want to test
2021-10-19 09:28:14 -03:00
Salvador E. Tropea fcfe64e695 Fixed warning message (missing where info) 2021-10-19 09:27:33 -03:00
Salvador E. Tropea 2ab0e6a489 Removed unused requested constant 2021-10-19 09:26:53 -03:00
Salvador E. Tropea cdc212e89e Generic filter: added options to match if a field is/isn't defined. 2021-10-18 17:04:44 -03:00
Salvador E. Tropea 499b3520a6 Option to hide component from PDF PCB Print
- option `hide_excluded` to hide components marked by the
  `exclude_filter`.
- https://forum.kicad.info/t/fab-drawing-for-only-through-hole-parts/
2021-10-18 16:59:37 -03:00
Salvador E. Tropea e62e38b6ea Added fallback for values+units we can't understand 2021-10-18 16:51:32 -03:00
Salvador E. Tropea 969b3843de Changed the mechanism to avoid merging components with empty fields 2021-10-18 16:42:58 -03:00
Salvador E. Tropea 7187fe5ebc Removed docker image generation for this branch 2021-10-18 15:58:24 -03:00
Salvador E. Tropea 4ead7e6ef5 Avoid grouping components when they lack the grouping fields.
- This is for the internal BoM when `merge_blank_fields` is disabled
2021-10-18 15:55:19 -03:00
Salvador E. Tropea cc272dd31c Added Digi-Key plug-in dependencies installation. 2021-10-18 15:46:52 -03:00
Salvador E. Tropea 48dec2995e Fixed pip command name 2021-10-18 15:27:45 -03:00
Salvador E. Tropea 4d22b52403 Added kicost-digikey-api-v3 installation to the test workflow. 2021-10-18 15:25:56 -03:00
Salvador E. Tropea f752a72bfe Added support for KiCad symbol libs without EOF comment. 2021-10-18 14:45:58 -03:00
Salvador E. Tropea a136c2f7e7 Updated KiCost. 2021-10-18 14:42:31 -03:00
Salvador E. Tropea ea2d7916be Changed KiCost to official repo and the DK plug-in branch. 2021-10-12 10:36:12 -03:00
26 changed files with 446 additions and 75 deletions

View File

@ -53,34 +53,3 @@ jobs:
with: with:
name: Test_Output name: Test_Output
path: output path: output
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
needs: test
steps:
- name: Check out the repo
uses: actions/checkout@v2
- name: Log in to Docker Hub
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
with:
images: setsoft/kicad_auto
- name: Build and push Docker image
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
with:
context: tools/dev_image
push: true
tags: setsoft/kicad_auto:dev
labels: ${{ steps.meta.outputs.labels }}

3
.gitmodules vendored
View File

@ -1,3 +1,4 @@
[submodule "submodules/KiCost"] [submodule "submodules/KiCost"]
path = submodules/KiCost path = submodules/KiCost
url = https://github.com/INTI-CMNB/KiCost.git url = https://github.com/hildogjr/KiCost.git
branch = api_ext_plugin

View File

@ -12,17 +12,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- skip_bottom: bottom components aren't rotated. - skip_bottom: bottom components aren't rotated.
- XLSX BoM: option to control the logo scale (#84) - XLSX BoM: option to control the logo scale (#84)
- Import mechanism for filters and variants (#88) - Import mechanism for filters and variants (#88)
- PDF PCB Print: option `hide_excluded` to hide components marked by the
`exclude_filter`.
https://forum.kicad.info/t/fab-drawing-for-only-through-hole-parts/
- Internal BoM: option to avoid merging components with empty fields.
Is named `merge_both_blank` and defaults to true.
- Internal BoM: when a `Value` field can't be interpreted as a `number+unit`,
and it contain at least one space, now we try to use the text before the
space. This helps for cases like "10K 1%".
- Generic filter: options to match if a field is/isn't defined.
- Internal BoM: two other options for the sorting criteria.
### Changed ### Changed
- Internal BoM: now components with different Tolerance, Voltage, Current - Internal BoM: now components with different Tolerance, Voltage, Current
and/or Power fields aren't grouped together. and/or Power fields aren't grouped together.
These fields are now part of the default `group_fields`. (#79) These fields are now part of the default `group_fields`. (#79)
- Internal BoM: the field used for variants doesn't produce conflicts. (#100)
### Fixed ### Fixed
- Position files now defaults to use the auxiliar origin as KiCad. - Position files now defaults to use the auxiliar origin as KiCad.
Can be disabled to use absolute coordinates. (#87) Can be disabled to use absolute coordinates. (#87)
- Board View: flipped output. (#89) - Board View: flipped output. (#89)
- Board View: problems with netnames using spaces. (#90) - Board View: problems with netnames using spaces. (#90)
- Schematic load: problems with fields containing double quotes. (#98)
- `--list`: problems with layers and fields specific for the project.
(INTI-CMNB/kibot_variants_arduprog#4)
## [0.11.0] - 2021-04-25 ## [0.11.0] - 2021-04-25
### Added ### Added

View File

@ -305,6 +305,8 @@ Currently the only type available is `generic`.
- `column`: [string=''] Name of the column to apply the regular expression. - `column`: [string=''] Name of the column to apply the regular expression.
- *field*: Alias for column. - *field*: Alias for column.
- `invert`: [boolean=false] Invert the regex match result. - `invert`: [boolean=false] Invert the regex match result.
- `match_if_field`: [boolean=false] Match if the field exists, no regex applied. Not affected by `invert`.
- `match_if_no_field`: [boolean=false] Match if the field doesn't exists, no regex applied. Not affected by `invert`.
- `regex`: [string=''] Regular expression to match. - `regex`: [string=''] Regular expression to match.
- *regexp*: Alias for regex. - *regexp*: Alias for regex.
- `skip_if_no_field`: [boolean=false] Skip this test if the field doesn't exist. - `skip_if_no_field`: [boolean=false] Skip this test if the field doesn't exist.
@ -326,6 +328,8 @@ Currently the only type available is `generic`.
- `column`: [string=''] Name of the column to apply the regular expression. - `column`: [string=''] Name of the column to apply the regular expression.
- *field*: Alias for column. - *field*: Alias for column.
- `invert`: [boolean=false] Invert the regex match result. - `invert`: [boolean=false] Invert the regex match result.
- `match_if_field`: [boolean=false] Match if the field exists, no regex applied. Not affected by `invert`.
- `match_if_no_field`: [boolean=false] Match if the field doesn't exists, no regex applied. Not affected by `invert`.
- `regex`: [string=''] Regular expression to match. - `regex`: [string=''] Regular expression to match.
- *regexp*: Alias for regex. - *regexp*: Alias for regex.
- `skip_if_no_field`: [boolean=false] Skip this test if the field doesn't exist. - `skip_if_no_field`: [boolean=false] Skip this test if the field doesn't exist.
@ -713,9 +717,10 @@ Next time you need this list just use an alias, like this:
- `ignore_dnf`: [boolean=true] Exclude DNF (Do Not Fit) components. - `ignore_dnf`: [boolean=true] Exclude DNF (Do Not Fit) components.
- `int_qtys`: [boolean=true] Component quantities are always expressed as integers. Using the ceil() function. - `int_qtys`: [boolean=true] Component quantities are always expressed as integers. Using the ceil() function.
- `merge_blank_fields`: [boolean=true] Component groups with blank fields will be merged into the most compatible group, where possible. - `merge_blank_fields`: [boolean=true] Component groups with blank fields will be merged into the most compatible group, where possible.
- `merge_both_blank`: [boolean=true] When creating groups two components with empty/missing field will be interpreted as with the same value.
- `no_conflict`: [list(string)] List of fields where we tolerate conflicts. - `no_conflict`: [list(string)] List of fields where we tolerate conflicts.
Use it to avoid undesired warnings. Use it to avoid undesired warnings.
By default the field indicated in `fit_field` and the field `part` are excluded. By default the field indicated in `fit_field`, the field used for variants and the field `part` are excluded.
- `no_distributors`: [string|list(string)] Exclude this distributors list. They are removed after computing `distributors`. - `no_distributors`: [string|list(string)] Exclude this distributors list. They are removed after computing `distributors`.
- `normalize_locale`: [boolean=false] When normalizing values use the locale decimal point. - `normalize_locale`: [boolean=false] When normalizing values use the locale decimal point.
- `normalize_values`: [boolean=false] Try to normalize the R, L and C values, producing uniform units and prefixes. - `normalize_values`: [boolean=false] Try to normalize the R, L and C values, producing uniform units and prefixes.
@ -723,6 +728,7 @@ Next time you need this list just use an alias, like this:
- `output`: [string='%f-%i%v.%x'] filename for the output (%i=bom). Affected by global options. - `output`: [string='%f-%i%v.%x'] filename for the output (%i=bom). Affected by global options.
- `ref_id`: [string=''] A prefix to add to all the references from this project. Used for multiple projects. - `ref_id`: [string=''] A prefix to add to all the references from this project. Used for multiple projects.
- `ref_separator`: [string=' '] Separator used for the list of references. - `ref_separator`: [string=' '] Separator used for the list of references.
- `sort_style`: [string='type_value'] [type_value,type_value_ref,ref] Sorting criteria.
- `source_by_id`: [boolean=false] Generate the `Source BoM` column using the reference ID instead of the project name. - `source_by_id`: [boolean=false] Generate the `Source BoM` column using the reference ID instead of the project name.
- `use_alt`: [boolean=false] Print grouped references in the alternate compressed style eg: R1-R7,R18. - `use_alt`: [boolean=false] Print grouped references in the alternate compressed style eg: R1-R7,R18.
- `variant`: [string=''] Board variant, used to determine which components - `variant`: [string=''] Board variant, used to determine which components
@ -737,9 +743,24 @@ Next time you need this list just use an alias, like this:
- `hide_stats_info`: [boolean=false] Hide statistics information. - `hide_stats_info`: [boolean=false] Hide statistics information.
- `highlight_empty`: [boolean=true] Use a color for empty cells. Applies only when `col_colors` is `true`. - `highlight_empty`: [boolean=true] Use a color for empty cells. Applies only when `col_colors` is `true`.
- `kicost`: [boolean=false] Enable KiCost worksheet creation. - `kicost`: [boolean=false] Enable KiCost worksheet creation.
- `kicost_api_disable`: [string|list(string)=''] List of KiCost APIs to disable.
- `kicost_api_enable`: [string|list(string)=''] List of KiCost APIs to enable.
- `kicost_dist_desc`: [boolean=false] Used to add a column with the distributor's description. So you can chek this is the right component.
- `logo`: [string|boolean=''] PNG file to use as logo, use false to remove. - `logo`: [string|boolean=''] PNG file to use as logo, use false to remove.
- `logo_scale`: [number=2] Scaling factor for the logo. Note that this value isn't honored by all spreadsheet software. - `logo_scale`: [number=2] Scaling factor for the logo. Note that this value isn't honored by all spreadsheet software.
- `max_col_width`: [number=60] [20,999] Maximum column width (characters). - `max_col_width`: [number=60] [20,999] Maximum column width (characters).
- `specs`: [boolean=false] Enable Specs worksheet creation. Contains specifications for the components.
Works with only some KiCost APIs.
- `specs_columns`: [list(dict)|list(string)] Which columns are included in the Specs worksheet. Use `References` for the references,
'Row' for the order and 'Sep' to separate groups at the same level. By default all are included.
Column names are distributor specific, the following aren't: '_desc', '_value', '_tolerance', '_footprint',
'_power', '_current', '_voltage', '_frequency', '_temp_coeff', '_manf', '_size'.
* Valid keys:
- `comment`: [string=''] Used as explanation for this column. The XLSX output uses it.
- `field`: [string=''] Name of the field to use for this column.
- `join`: [list(string)|string=''] List of fields to join to this column.
- `level`: [number=0] Used to group columns. The XLSX output uses it to collapse columns.
- `name`: [string=''] Name to display in the header. The field is used when empty.
- `style`: [string='modern-blue'] Head style: modern-blue, modern-green, modern-red and classic. - `style`: [string='modern-blue'] Head style: modern-blue, modern-green, modern-red and classic.
- `title`: [string='KiBot Bill of Materials'] BoM title. - `title`: [string='KiBot Bill of Materials'] BoM title.
@ -1275,6 +1296,7 @@ Next time you need this list just use an alias, like this:
- `dnf_filter`: [string|list(string)=''] Name of the filter to mark components as not fitted. - `dnf_filter`: [string|list(string)=''] Name of the filter to mark components as not fitted.
A short-cut to use for simple cases where a variant is an overkill. A short-cut to use for simple cases where a variant is an overkill.
- `drill_marks`: [string='full'] What to use to indicate the drill places, can be none, small or full (for real scale). - `drill_marks`: [string='full'] What to use to indicate the drill places, can be none, small or full (for real scale).
- `hide_excluded`: [boolean=false] Hide components in the Fab layer that are marked as excluded by a variant.
- `mirror`: [boolean=false] Print mirrored (X axis inverted). ONLY for KiCad 6. - `mirror`: [boolean=false] Print mirrored (X axis inverted). ONLY for KiCad 6.
- `monochrome`: [boolean=false] Print in black and white. - `monochrome`: [boolean=false] Print in black and white.
- `output`: [string='%f-%i%v.%x'] Filename for the output PDF (%i=layers, %x=pdf). Affected by global options. - `output`: [string='%f-%i%v.%x'] Filename for the output PDF (%i=layers, %x=pdf). Affected by global options.
@ -1890,7 +1912,7 @@ KiBot: KiCad automation tool for documents generation
Usage: Usage:
kibot [-b BOARD] [-e SCHEMA] [-c CONFIG] [-d OUT_DIR] [-s PRE] kibot [-b BOARD] [-e SCHEMA] [-c CONFIG] [-d OUT_DIR] [-s PRE]
[-q | -v...] [-i] [-m MKFILE] [-g DEF]... [TARGET...] [-q | -v...] [-i] [-m MKFILE] [-g DEF]... [TARGET...]
kibot [-v...] [-c PLOT_CONFIG] --list kibot [-v...] [-b BOARD] [-e SCHEMA] [-c PLOT_CONFIG] --list
kibot [-v...] [-b BOARD] [-d OUT_DIR] [-p | -P] --example kibot [-v...] [-b BOARD] [-d OUT_DIR] [-p | -P] --example
kibot [-v...] --help-filters kibot [-v...] --help-filters
kibot [-v...] --help-list-outputs kibot [-v...] --help-list-outputs

View File

@ -165,9 +165,11 @@ outputs:
int_qtys: true int_qtys: true
# [boolean=true] Component groups with blank fields will be merged into the most compatible group, where possible # [boolean=true] Component groups with blank fields will be merged into the most compatible group, where possible
merge_blank_fields: true merge_blank_fields: true
# [boolean=true] When creating groups two components with empty/missing field will be interpreted as with the same value
merge_both_blank: true
# [list(string)] List of fields where we tolerate conflicts. # [list(string)] List of fields where we tolerate conflicts.
# Use it to avoid undesired warnings. # Use it to avoid undesired warnings.
# By default the field indicated in `fit_field` and the field `part` are excluded # By default the field indicated in `fit_field`, the field used for variants and the field `part` are excluded
no_conflict: ['Config', 'Part'] no_conflict: ['Config', 'Part']
# [string|list(string)] Exclude this distributors list. They are removed after computing `distributors` # [string|list(string)] Exclude this distributors list. They are removed after computing `distributors`
no_distributors: no_distributors:
@ -183,6 +185,8 @@ outputs:
ref_id: '' ref_id: ''
# [string=' '] Separator used for the list of references # [string=' '] Separator used for the list of references
ref_separator: ' ' ref_separator: ' '
# [string='type_value'] [type_value,type_value_ref,ref] Sorting criteria
sort_style: 'type_value'
# [boolean=false] Generate the `Source BoM` column using the reference ID instead of the project name # [boolean=false] Generate the `Source BoM` column using the reference ID instead of the project name
source_by_id: false source_by_id: false
# [boolean=false] Print grouped references in the alternate compressed style eg: R1-R7,R18 # [boolean=false] Print grouped references in the alternate compressed style eg: R1-R7,R18
@ -208,12 +212,36 @@ outputs:
highlight_empty: true highlight_empty: true
# [boolean=false] Enable KiCost worksheet creation # [boolean=false] Enable KiCost worksheet creation
kicost: false kicost: false
# [string|list(string)=''] List of KiCost APIs to disable
kicost_api_disable: ''
# [string|list(string)=''] List of KiCost APIs to enable
kicost_api_enable: ''
# [boolean=false] Used to add a column with the distributor's description. So you can chek this is the right component
kicost_dist_desc: false
# [string|boolean=''] PNG file to use as logo, use false to remove # [string|boolean=''] PNG file to use as logo, use false to remove
logo: '' logo: ''
# [number=2] Scaling factor for the logo. Note that this value isn't honored by all spreadsheet software # [number=2] Scaling factor for the logo. Note that this value isn't honored by all spreadsheet software
logo_scale: 2 logo_scale: 2
# [number=60] [20,999] Maximum column width (characters) # [number=60] [20,999] Maximum column width (characters)
max_col_width: 60 max_col_width: 60
# [boolean=false] Enable Specs worksheet creation. Contains specifications for the components.
# Works with only some KiCost APIs
specs: false
# [list(dict)|list(string)] Which columns are included in the Specs worksheet. Use `References` for the references,
# 'Row' for the order and 'Sep' to separate groups at the same level. By default all are included.
# Column names are distributor specific, the following aren't: '_desc', '_value', '_tolerance', '_footprint',
# '_power', '_current', '_voltage', '_frequency', '_temp_coeff', '_manf', '_size'
specs_columns:
# [string=''] Used as explanation for this column. The XLSX output uses it
- comment: ''
# [string=''] Name of the field to use for this column
field: 'Row'
# [list(string)|string=''] List of fields to join to this column
join: ''
# [number=0] Used to group columns. The XLSX output uses it to collapse columns
level: 0
# [string=''] Name to display in the header. The field is used when empty
name: 'Line'
# [string='modern-blue'] Head style: modern-blue, modern-green, modern-red and classic # [string='modern-blue'] Head style: modern-blue, modern-green, modern-red and classic
style: 'modern-blue' style: 'modern-blue'
# [string='KiBot Bill of Materials'] BoM title # [string='KiBot Bill of Materials'] BoM title
@ -879,6 +907,8 @@ outputs:
dnf_filter: '' dnf_filter: ''
# [string='full'] What to use to indicate the drill places, can be none, small or full (for real scale) # [string='full'] What to use to indicate the drill places, can be none, small or full (for real scale)
drill_marks: 'full' drill_marks: 'full'
# [boolean=false] Hide components in the Fab layer that are marked as excluded by a variant
hide_excluded: false
# [boolean=false] Print mirrored (X axis inverted). ONLY for KiCad 6 # [boolean=false] Print mirrored (X axis inverted). ONLY for KiCad 6
mirror: false mirror: false
# [boolean=false] Print in black and white # [boolean=false] Print in black and white

View File

@ -10,7 +10,7 @@
Usage: Usage:
kibot [-b BOARD] [-e SCHEMA] [-c CONFIG] [-d OUT_DIR] [-s PRE] kibot [-b BOARD] [-e SCHEMA] [-c CONFIG] [-d OUT_DIR] [-s PRE]
[-q | -v...] [-i] [-m MKFILE] [-g DEF]... [TARGET...] [-q | -v...] [-i] [-m MKFILE] [-g DEF]... [TARGET...]
kibot [-v...] [-c PLOT_CONFIG] --list kibot [-v...] [-b BOARD] [-e SCHEMA] [-c PLOT_CONFIG] --list
kibot [-v...] [-b BOARD] [-d OUT_DIR] [-p | -P] --example kibot [-v...] [-b BOARD] [-d OUT_DIR] [-p | -P] --example
kibot [-v...] --help-filters kibot [-v...] --help-filters
kibot [-v...] --help-list-outputs kibot [-v...] --help-list-outputs
@ -90,7 +90,9 @@ def list_pre_and_outs(logger, outputs):
if len(outputs): if len(outputs):
logger.info('Outputs:') logger.info('Outputs:')
for o in outputs: for o in outputs:
config_output(o, dry=True) # Note: we can't do a `dry` config because some layer and field names can be validated only if we
# load the schematic and the PCB.
config_output(o, dry=False)
logger.info('- '+str(o)) logger.info('- '+str(o))
@ -341,15 +343,16 @@ def main():
with open(plot_config) as cf_file: with open(plot_config) as cf_file:
outputs = cr.read(cf_file) outputs = cr.read(cf_file)
# Determine the SCH file
GS.set_sch(solve_schematic(args.schematic, args.board_file, plot_config))
# Determine the PCB file
GS.set_pcb(solve_board_file(GS.sch_file, args.board_file))
# Is just list the available targets? # Is just list the available targets?
if args.list: if args.list:
list_pre_and_outs(logger, outputs) list_pre_and_outs(logger, outputs)
sys.exit(0) sys.exit(0)
# Determine the SCH file
GS.set_sch(solve_schematic(args.schematic, args.board_file, plot_config))
# Determine the PCB file
GS.set_pcb(solve_board_file(GS.sch_file, args.board_file))
if args.makefile: if args.makefile:
# Only create a makefile # Only create a makefile
generate_makefile(args.makefile, plot_config, outputs) generate_makefile(args.makefile, plot_config, outputs)

View File

@ -12,7 +12,7 @@ All the logic to convert a list of components into the rows and columns used to
import locale import locale
from copy import deepcopy from copy import deepcopy
from math import ceil from math import ceil
from .units import compare_values, comp_match from .units import compare_values, comp_match, get_last_warning
from .bom_writer import write_bom from .bom_writer import write_bom
from .columnlist import ColumnList from .columnlist import ColumnList
from ..misc import DNF, W_FIELDCONF from ..misc import DNF, W_FIELDCONF
@ -70,6 +70,9 @@ def compare_field(c1, c2, field, cfg):
# If blank comparisons are allowed # If blank comparisons are allowed
if (c1_value == "" or c2_value == "") and cfg.merge_blank_fields: if (c1_value == "" or c2_value == "") and cfg.merge_blank_fields:
return True return True
if not cfg.merge_both_blank and c1_value == "" and c2_value == "":
# Avoid merging two components with empty field
return False
return c1_value == c2_value return c1_value == c2_value
@ -346,7 +349,7 @@ class ComponentGroup(object):
return row return row
def get_value_sort(comp): def get_value_sort(comp, fallback_ref=False):
""" Try to better sort R, L and C components """ """ Try to better sort R, L and C components """
res = comp.value_sort res = comp.value_sort
if res: if res:
@ -358,6 +361,8 @@ def get_value_sort(comp):
# milli Ohms # milli Ohms
value = "{0:15d}".format(int(value * 1000 * mult + 0.1)) value = "{0:15d}".format(int(value * 1000 * mult + 0.1))
return value return value
if fallback_ref:
return comp.ref_prefix + "{0:15d}".format(_suffix_to_num(comp.ref_suffix))
return comp.value return comp.value
@ -400,7 +405,14 @@ def group_components(cfg, components):
continue continue
# Cache the value used to sort # Cache the value used to sort
if c.ref_prefix in RLC_PREFIX and c.value.lower() not in DNF: if c.ref_prefix in RLC_PREFIX and c.value.lower() not in DNF:
c.value_sort = comp_match(c.value, c.ref_prefix) c.value_sort = comp_match(c.value, c.ref_prefix, c.ref)
if c.value_sort is None and (' ' in c.value):
# Try with the data before a space
value = c.value.split(' ')[0]
value_sort = comp_match(value, c.ref_prefix)
if value_sort is not None:
c.value_sort = value_sort
logger.warning(get_last_warning() + "Using `{}` for {} instead".format(value, c.ref))
else: else:
c.value_sort = None c.value_sort = None
# Try to add the component to an existing group # Try to add the component to an existing group
@ -429,8 +441,16 @@ def group_components(cfg, components):
if cfg.normalize_values: if cfg.normalize_values:
g.fields[ColumnList.COL_VALUE_L] = normalize_value(g.components[0], decimal_point) g.fields[ColumnList.COL_VALUE_L] = normalize_value(g.components[0], decimal_point)
# Sort the groups # Sort the groups
if cfg.sort_style == 'type_value':
# First priority is the Type of component (e.g. R?, U?, L?) # First priority is the Type of component (e.g. R?, U?, L?)
# Second is the value
groups = sorted(groups, key=lambda g: [g.components[0].ref_prefix, get_value_sort(g.components[0])]) groups = sorted(groups, key=lambda g: [g.components[0].ref_prefix, get_value_sort(g.components[0])])
elif cfg.sort_style == 'type_value_ref':
# First priority is the Type of component (e.g. R?, U?, L?)
# Second is the value, but if we don't have a value we use the reference
groups = sorted(groups, key=lambda g: [g.components[0].ref_prefix, get_value_sort(g.components[0], True)])
else: # ref
groups = sorted(groups, key=lambda g: [g.components[0].ref_prefix, _suffix_to_num(g.components[0].ref_suffix)])
# Enumerate the groups and compute stats # Enumerate the groups and compute stats
n_total = 0 n_total = 0
n_fitted = 0 n_fitted = 0

View File

@ -43,6 +43,12 @@ UNIT_ALL = UNIT_R + UNIT_C + UNIT_L
match = None match = None
# Current locale decimal point value # Current locale decimal point value
decimal_point = None decimal_point = None
# Last warning
last_warning = ''
def get_last_warning():
return last_warning
def get_unit(unit, ref_prefix): def get_unit(unit, ref_prefix):
@ -98,12 +104,13 @@ def match_string():
return r"(\d*\.?\d*)\s*(" + group_string(PREFIX_ALL) + ")*(" + group_string(UNIT_ALL) + r")*(\d*)$" return r"(\d*\.?\d*)\s*(" + group_string(PREFIX_ALL) + ")*(" + group_string(UNIT_ALL) + r")*(\d*)$"
def comp_match(component, ref_prefix): def comp_match(component, ref_prefix, ref=None):
""" """
Return a normalized value and units for a given component value string Return a normalized value and units for a given component value string
e.g. comp_match('10R2') returns (10, R) e.g. comp_match('10R2') returns (10, R)
e.g. comp_match('3.3mOhm') returns (0.0033, R) e.g. comp_match('3.3mOhm') returns (0.0033, R)
""" """
global last_warning
original = component original = component
# Remove useless spaces # Remove useless spaces
component = component.strip() component = component.strip()
@ -130,14 +137,17 @@ def comp_match(component, ref_prefix):
# Ignore case # Ignore case
match = re.compile(match_string(), flags=re.IGNORECASE) match = re.compile(match_string(), flags=re.IGNORECASE)
where = ' in {}'.format(ref) if ref is not None else ''
result = match.match(component) result = match.match(component)
if not result: if not result:
logger.warning(W_BADVAL1 + "Malformed value: `{}` (no match)".format(original)) last_warning = W_BADVAL1
logger.warning(W_BADVAL1 + "Malformed value: `{}` (no match{})".format(original, where))
return None return None
value, prefix, units, post = result.groups() value, prefix, units, post = result.groups()
if value == '.': if value == '.':
logger.warning(W_BADVAL2 + "Malformed value: `{}` (reduced to decimal point)".format(original)) last_warning = W_BADVAL2
logger.warning(W_BADVAL2 + "Malformed value: `{}` (reduced to decimal point{})".format(original, where))
return None return None
if value == '': if value == '':
value = '0' value = '0'
@ -148,7 +158,9 @@ def comp_match(component, ref_prefix):
# We will also have a trailing number # We will also have a trailing number
if post: if post:
if "." in value: if "." in value:
logger.warning(W_BADVAL3 + "Malformed value: `{}` (unit split, but contains decimal point)".format(original)) last_warning = W_BADVAL3
logger.warning(W_BADVAL3 + "Malformed value: `{}` (unit split, but contains decimal point{})".
format(original, where))
return None return None
value = float(value) value = float(value)
postValue = float(post) / (10 ** len(post)) postValue = float(post) / (10 ** len(post))

View File

@ -17,7 +17,8 @@ from base64 import b64decode
from .columnlist import ColumnList from .columnlist import ColumnList
from .kibot_logo import KIBOT_LOGO from .kibot_logo import KIBOT_LOGO
from .. import log from .. import log
from ..misc import W_NOKICOST, W_UNKDIST from ..misc import W_NOKICOST, W_UNKDIST, KICOST_ERROR, W_BADFIELD
from ..error import trace_dump
from ..__main__ import __version__ from ..__main__ import __version__
try: try:
from xlsxwriter import Workbook from xlsxwriter import Workbook
@ -37,14 +38,16 @@ try:
sys.path.insert(0, rel_path) sys.path.insert(0, rel_path)
# Init the logger first # Init the logger first
logger = log.get_logger(__name__) logger = log.get_logger(__name__)
from kicost.global_vars import set_logger from kicost.global_vars import set_logger, KiCostError
set_logger(logger) set_logger(logger)
from kicost import PartGroup from kicost import PartGroup
from kicost.kicost import query_part_info from kicost.kicost import query_part_info
from kicost.spreadsheet import create_worksheet, Spreadsheet from kicost.spreadsheet import create_worksheet, Spreadsheet
from kicost.distributors import (init_distributor_dict, set_distributors_logger, get_distributors_list, from kicost.distributors import (init_distributor_dict, set_distributors_logger, get_distributors_list,
get_dist_name_from_label, set_distributors_progress) get_dist_name_from_label, set_distributors_progress, is_valid_api,
configure_from_environment, configure_apis)
from kicost.edas import set_edas_logger from kicost.edas import set_edas_logger
from kicost.config import load_config
# Progress mechanism: use the one declared in __main__ (TQDM) # Progress mechanism: use the one declared in __main__ (TQDM)
from kicost.__main__ import ProgressConsole from kicost.__main__ import ProgressConsole
set_distributors_progress(ProgressConsole) set_distributors_progress(ProgressConsole)
@ -66,10 +69,11 @@ GREY_L = "#f3f3f3"
HEAD_COLOR_R = "#982020" HEAD_COLOR_R = "#982020"
HEAD_COLOR_G = "#009879" HEAD_COLOR_G = "#009879"
HEAD_COLOR_B = "#0e4e8e" HEAD_COLOR_B = "#0e4e8e"
DEFAULT_FMT = {'text_wrap': True, 'align': 'center_across', 'valign': 'vjustify'} DEFAULT_FMT = {'text_wrap': True, 'align': 'center_across', 'valign': 'vcenter'}
KICOST_COLUMNS = {'refs': ColumnList.COL_REFERENCE, KICOST_COLUMNS = {'refs': ColumnList.COL_REFERENCE,
'desc': ColumnList.COL_DESCRIPTION, 'desc': ColumnList.COL_DESCRIPTION,
'qty': ColumnList.COL_GRP_BUILD_QUANTITY} 'qty': ColumnList.COL_GRP_BUILD_QUANTITY}
SPECS_GENERATED = set((ColumnList.COL_REFERENCE_L, ColumnList.COL_ROW_NUMBER_L, 'sep'))
def bg_color(col): def bg_color(col):
@ -246,6 +250,75 @@ def create_color_ref(workbook, col_colors, hl_empty, fmt_cols, do_kicost, kicost
worksheet.write_string(row, 0, label, format) worksheet.write_string(row, 0, label, format)
def get_spec(part, name):
if name[0] != '_':
return part.specs.get(name, ['', ''])
name = name[1:]
for k, v in part.dd.items():
val = v.extra_info.get(name, None)
if val:
return [name, val]
return ['', '']
def create_meta(workbook, name, columns, parts, fmt_head, fmt_cols, max_w, rename, levels, comments, join):
worksheet = workbook.add_worksheet(name)
col_w = []
row_h = 1
for c, col in enumerate(columns):
name = rename.get(col.lower(), col) if rename else col
worksheet.write_string(0, c, name, fmt_head)
text_l = max(len(col), 6)
if text_l > max_w:
h = len(wrap(col, max_w))
row_h = max(row_h, h)
text_l = max_w
col_w.append(text_l)
if row_h > 1:
worksheet.set_row(0, 15.0*row_h)
for r, part in enumerate(parts):
# Add the references as another spec
part.specs[ColumnList.COL_REFERENCE_L] = (ColumnList.COL_REFERENCE, part.collapsed_refs)
# Also add the row
part.specs[ColumnList.COL_ROW_NUMBER_L] = (ColumnList.COL_ROW_NUMBER, str(r+1))
row_h = 1
for c, col in enumerate(columns):
col_l = col.lower()
if col_l == 'sep':
col_w[c] = 0
continue
v = get_spec(part, col_l)
text = v[1]
# Append text from other fields
if join:
for j in join:
if j[0] == col_l:
for c_join in j[1:]:
v = part.specs.get(c_join, None)
if v:
text += ' ' + v[1]
text_l = len(text)
if not text_l:
continue
fmt_kind = 0 if col_l in SPECS_GENERATED else 2
worksheet.write_string(r+1, c, text, fmt_cols[fmt_kind][r % 2])
if text_l > col_w[c]:
if text_l > max_w:
h = len(wrap(text, max_w))
row_h = max(row_h, h)
text_l = max_w
col_w[c] = text_l
if row_h > 1:
worksheet.set_row(r+1, 15.0*row_h)
for i, width in enumerate(col_w):
ops = {'level': levels[i] if levels else 0}
if not width:
ops['hidden'] = 1
if comments and comments[i]:
worksheet.write_comment(0, i, comments[i])
worksheet.set_column(i, i, width, None, ops)
def adjust_widths(worksheet, column_widths, max_width, levels): def adjust_widths(worksheet, column_widths, max_width, levels):
c_levels = len(levels) c_levels = len(levels)
for i, width in enumerate(column_widths): for i, width in enumerate(column_widths):
@ -375,7 +448,9 @@ def remove_unknown_distributors(distributors, available, silent):
d = d.lower() d = d.lower()
if d not in available: if d not in available:
# Is the label of the column? # Is the label of the column?
d = get_dist_name_from_label(d) new_d = get_dist_name_from_label(d)
if new_d is not None:
d = new_d
if d not in available: if d not in available:
if not silent: if not silent:
logger.warning(W_UNKDIST+'Unknown distributor `{}`'.format(d)) logger.warning(W_UNKDIST+'Unknown distributor `{}`'.format(d))
@ -412,7 +487,40 @@ def compute_qtys(cfg, g):
return [str(g.get_count(sch.name)) for sch in cfg.aggregate] return [str(g.get_count(sch.name)) for sch in cfg.aggregate]
def create_kicost_sheet(workbook, groups, image_data, fmt_title, fmt_info, fmt_subtitle, cfg): def create_meta_sheets(workbook, used_parts, fmt_head, fmt_cols, cfg, ss):
if cfg.xlsx.specs:
meta_names = ['Specs', 'Specs (DNF)']
for ws in range(2):
spec_cols = {}
spec_cols_l = set()
parts = used_parts[ws]
for part in parts:
for name_l, v in part.specs.items():
spec_cols_l.add(name_l)
spec = v[0]
if spec in spec_cols:
spec_cols[spec] += 1
else:
spec_cols[spec] = 1
if len(spec_cols):
columns = cfg.xlsx.s_columns
if columns is None:
# Use all columns, sort them by relevance (most used) and alphabetically
c = len(parts)
columns = sorted(spec_cols, key=lambda k: (c - spec_cols[k], k))
columns.insert(0, ColumnList.COL_REFERENCE)
else:
# Inform about missing columns
for c in columns:
col = c.lower()
if ((col[0] == '_' and col[1:] not in ss.extra_info_display) or
(col[0] != '_' and col not in spec_cols_l and col not in SPECS_GENERATED)):
logger.warning(W_BADFIELD+'Invalid Specs column name `{}` {}'.format(c, col[1:]))
create_meta(workbook, meta_names[ws], columns, parts, fmt_head, fmt_cols, cfg.xlsx.max_col_width,
cfg.xlsx.s_rename, cfg.xlsx.s_levels, cfg.xlsx.s_comments, cfg.xlsx.s_join)
def _create_kicost_sheet(workbook, groups, image_data, fmt_title, fmt_info, fmt_subtitle, fmt_head, fmt_cols, cfg):
if not KICOST_SUPPORT: if not KICOST_SUPPORT:
logger.warning(W_NOKICOST, 'KiCost sheet requested but failed to load KiCost support') logger.warning(W_NOKICOST, 'KiCost sheet requested but failed to load KiCost support')
return return
@ -426,6 +534,19 @@ def create_kicost_sheet(workbook, groups, image_data, fmt_title, fmt_info, fmt_s
# Force KiCost to use our logger # Force KiCost to use our logger
set_distributors_logger(logger) set_distributors_logger(logger)
set_edas_logger(logger) set_edas_logger(logger)
# Load KiCost config (includes APIs config)
api_options = load_config()
# Environment with overwrite
configure_from_environment(api_options, True)
# Filter which APIs we want
for api in cfg.xlsx.kicost_api_disable:
if is_valid_api(api):
api_options[api]['enable'] = False
for api in cfg.xlsx.kicost_api_enable:
if is_valid_api(api):
api_options[api]['enable'] = True
# Configure the APIs
configure_apis(api_options)
# Start with a clean list of available distributors # Start with a clean list of available distributors
init_distributor_dict() init_distributor_dict()
# Create the projects information structure # Create the projects information structure
@ -437,6 +558,13 @@ def create_kicost_sheet(workbook, groups, image_data, fmt_title, fmt_info, fmt_s
Spreadsheet.ADJUST_ROW_AND_COL_SIZE = True Spreadsheet.ADJUST_ROW_AND_COL_SIZE = True
Spreadsheet.MAX_COL_WIDTH = cfg.xlsx.max_col_width Spreadsheet.MAX_COL_WIDTH = cfg.xlsx.max_col_width
Spreadsheet.PART_NSEQ_SEPRTR = cfg.ref_separator Spreadsheet.PART_NSEQ_SEPRTR = cfg.ref_separator
if hasattr(Spreadsheet, 'SUPPRESS_DIST_DESC'):
Spreadsheet.SUPPRESS_DIST_DESC = not cfg.xlsx.kicost_dist_desc
# Keep our sorting
try:
Spreadsheet.SORT_GROUPS = False
except Exception:
pass
# Make the version less intrusive # Make the version less intrusive
Spreadsheet.WRK_FORMATS['about_msg']['font_size'] = 8 Spreadsheet.WRK_FORMATS['about_msg']['font_size'] = 8
# Don 't add project info, we add our own data # Don 't add project info, we add our own data
@ -475,6 +603,7 @@ def create_kicost_sheet(workbook, groups, image_data, fmt_title, fmt_info, fmt_s
id = new_id.lower() id = new_id.lower()
if id in cfg.column_rename: if id in cfg.column_rename:
v['label'] = cfg.column_rename[id] v['label'] = cfg.column_rename[id]
used_parts = []
for ws in range(2): for ws in range(2):
# Second pass is DNF # Second pass is DNF
dnf = ws == 1 dnf = ws == 1
@ -523,6 +652,9 @@ def create_kicost_sheet(workbook, groups, image_data, fmt_title, fmt_info, fmt_s
if cfg.xlsx.title: if cfg.xlsx.title:
wks.set_row(0, 32) wks.set_row(0, 32)
wks.merge_range(0, col1, 0, ss.globals_width, cfg.xlsx.title, fmt_title) wks.merge_range(0, col1, 0, ss.globals_width, cfg.xlsx.title, fmt_title)
used_parts.append(parts)
# Specs sheets
create_meta_sheets(workbook, used_parts, fmt_head, fmt_cols, cfg, ss)
colors = {} colors = {}
colors['Best price'] = ss.wrk_formats['best_price'] colors['Best price'] = ss.wrk_formats['best_price']
colors['No manufacturer or distributor code'] = ss.wrk_formats['not_manf_codes'] colors['No manufacturer or distributor code'] = ss.wrk_formats['not_manf_codes']
@ -536,6 +668,15 @@ def create_kicost_sheet(workbook, groups, image_data, fmt_title, fmt_info, fmt_s
return colors return colors
def create_kicost_sheet(workbook, groups, image_data, fmt_title, fmt_info, fmt_subtitle, fmt_head, fmt_cols, cfg):
try:
return _create_kicost_sheet(workbook, groups, image_data, fmt_title, fmt_info, fmt_subtitle, fmt_head, fmt_cols, cfg)
except KiCostError as e:
trace_dump()
logger.error('KiCost error: `{}` ({})'.format(e.msg, e.id))
exit(KICOST_ERROR)
def write_xlsx(filename, groups, col_fields, head_names, cfg): def write_xlsx(filename, groups, col_fields, head_names, cfg):
""" """
Write BoM out to a XLSX file Write BoM out to a XLSX file
@ -660,7 +801,8 @@ def write_xlsx(filename, groups, col_fields, head_names, cfg):
# Optionally add KiCost information # Optionally add KiCost information
kicost_colors = None kicost_colors = None
if cfg.xlsx.kicost: if cfg.xlsx.kicost:
kicost_colors = create_kicost_sheet(workbook, groups, image_data, fmt_title, fmt_info, fmt_subtitle, cfg) kicost_colors = create_kicost_sheet(workbook, groups, image_data, fmt_title, fmt_info, fmt_subtitle, fmt_head,
fmt_cols, cfg)
# Add a sheet for the color references # Add a sheet for the color references
create_color_ref(workbook, cfg.xlsx.col_colors, hl_empty, fmt_cols, cfg.xlsx.kicost and KICOST_SUPPORT, kicost_colors) create_color_ref(workbook, cfg.xlsx.col_colors, hl_empty, fmt_cols, cfg.xlsx.kicost and KICOST_SUPPORT, kicost_colors)

View File

@ -124,6 +124,10 @@ class Generic(BaseFilter): # noqa: F821
if reg.skip_if_no_field and not c.is_field(reg.column): if reg.skip_if_no_field and not c.is_field(reg.column):
# Skip the check if the field doesn't exist # Skip the check if the field doesn't exist
continue continue
if reg.match_if_field and c.is_field(reg.column):
return True
if reg.match_if_no_field and not c.is_field(reg.column):
return True
field_value = c.get_field_value(reg.column) field_value = c.get_field_value(reg.column)
res = reg.regex.search(field_value) res = reg.regex.search(field_value)
if reg.invert: if reg.invert:
@ -145,6 +149,10 @@ class Generic(BaseFilter): # noqa: F821
if reg.skip_if_no_field and not c.is_field(reg.column): if reg.skip_if_no_field and not c.is_field(reg.column):
# Skip the check if the field doesn't exist # Skip the check if the field doesn't exist
continue continue
if reg.match_if_field and c.is_field(reg.column):
return True
if reg.match_if_no_field and not c.is_field(reg.column):
return True
field_value = c.get_field_value(reg.column) field_value = c.get_field_value(reg.column)
res = reg.regex.search(field_value) res = reg.regex.search(field_value)
if reg.invert: if reg.invert:

View File

@ -18,7 +18,7 @@ from .config import KiConf, un_quote
from ..gs import GS from ..gs import GS
from ..misc import (W_BADPOLI, W_POLICOORDS, W_BADSQUARE, W_BADCIRCLE, W_BADARC, W_BADTEXT, W_BADPIN, W_BADCOMP, W_BADDRAW, from ..misc import (W_BADPOLI, W_POLICOORDS, W_BADSQUARE, W_BADCIRCLE, W_BADARC, W_BADTEXT, W_BADPIN, W_BADCOMP, W_BADDRAW,
W_UNKDCM, W_UNKAR, W_ARNOPATH, W_ARNOREF, W_MISCFLD, W_EXTRASPC, W_NOLIB, W_INCPOS, W_NOANNO, W_MISSLIB, W_UNKDCM, W_UNKAR, W_ARNOPATH, W_ARNOREF, W_MISCFLD, W_EXTRASPC, W_NOLIB, W_INCPOS, W_NOANNO, W_MISSLIB,
W_MISSDCM, W_MISSCMP, W_MISFLDNAME) W_MISSDCM, W_MISSCMP, W_MISFLDNAME, W_NOENDLIB)
from .. import log from .. import log
logger = log.get_logger(__name__) logger = log.get_logger(__name__)
@ -106,7 +106,7 @@ class LibComponentField(object):
Almost the same as a field in the schematic, but incompatible!!! """ Almost the same as a field in the schematic, but incompatible!!! """
# F n "text" posx posy dimension orientation visibility hjustify vjustify/italic/bold "name" # F n "text" posx posy dimension orientation visibility hjustify vjustify/italic/bold "name"
field_re = re.compile(r'F\s*(\d+)\s+' # 0 Field number field_re = re.compile(r'F\s*(\d+)\s+' # 0 Field number
r'"([^"]*)"\s+' # 1 Field value r'"((?:[^\\]|(?:\\.))*)"\s+' # 1 Field value
r'(-?\d+)\s+' # 2 Pos X r'(-?\d+)\s+' # 2 Pos X
r'(-?\d+)\s+' # 3 Pos Y r'(-?\d+)\s+' # 3 Pos Y
r'(\d+)\s+' # 4 Dimension r'(\d+)\s+' # 4 Dimension
@ -114,7 +114,7 @@ class LibComponentField(object):
r'([VI])\s+' # 6 Visibility r'([VI])\s+' # 6 Visibility
r'([LRCBT])\s+' # 7 HJustify r'([LRCBT])\s+' # 7 HJustify
r'([LRCBT][IN][BN])\s*' # 8 VJustify+Italic+Bold r'([LRCBT][IN][BN])\s*' # 8 VJustify+Italic+Bold
r'("[^"]*")?') # 9 Name for user fields r'("(?:[^\\]|(?:\\.))*")?') # 9 Name for user fields
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -510,6 +510,10 @@ class LibComponent(object):
self.draw = [] self.draw = []
line = f.get_line() line = f.get_line()
while not line.startswith('ENDDEF'): while not line.startswith('ENDDEF'):
if len(line) == 0:
# Skip empty lines
line = f.get_line()
continue
if line[0] == 'F': if line[0] == 'F':
# A field # A field
field = LibComponentField.parse(line, lib_name, f) field = LibComponentField.parse(line, lib_name, f)
@ -655,7 +659,11 @@ class SymLib(object):
self.alias[a] = o self.alias[a] = o
else: else:
raise SchLibError('Unknown library entry', line, f) raise SchLibError('Unknown library entry', line, f)
try:
line = f.get_line() line = f.get_line()
except SchLibError:
logger.warning(W_NOENDLIB + 'Library without end of file comment: `{}`'.format(file))
break
class DocLibEntry(object): class DocLibEntry(object):
@ -713,8 +721,8 @@ class DocLib(object):
class SchematicField(object): class SchematicField(object):
# F n "text" orientation posx posy dimension flags hjustify vjustify/italic/bold "name" # F n "text" orientation posx posy dimension flags hjustify vjustify/italic/bold "name"
field_re = re.compile(r'F\s*(\d+)\s+"([^"]*)"\s+([HV])\s+(-?\d+)\s+(-?\d+)\s+(\d+)\s+(\d+)' field_re = re.compile(r'F\s*(\d+)\s+"((?:[^\\]|(?:\\.))*)"\s+([HV])\s+(-?\d+)\s+(-?\d+)\s+(\d+)\s+(\d+)'
r'\s+([LRCBT])\s+([LRCBT][IN][BN])\s*("[^"]*")?') r'\s+([LRCBT])\s+([LRCBT][IN][BN])\s*("(?:[^\\]|(?:\\.))*")?')
def __init__(self): def __init__(self):
super().__init__() super().__init__()

View File

@ -34,6 +34,7 @@ PCBDRAW_ERR = 20
SVG_SCH_PRINT = 21 SVG_SCH_PRINT = 21
CORRUPTED_SCH = 22 CORRUPTED_SCH = 22
WRONG_INSTALL = 23 WRONG_INSTALL = 23
KICOST_ERROR = 24
error_level_to_name = ['NONE', error_level_to_name = ['NONE',
'INTERNAL_ERROR', 'INTERNAL_ERROR',
'WRONG_ARGUMENTS', 'WRONG_ARGUMENTS',
@ -58,6 +59,7 @@ error_level_to_name = ['NONE',
'SVG_SCH_PRINT', 'SVG_SCH_PRINT',
'CORRUPTED_SCH', 'CORRUPTED_SCH',
'WRONG_INSTALL', 'WRONG_INSTALL',
'KICOST_ERROR',
] ]
CMD_EESCHEMA_DO = 'eeschema_do' CMD_EESCHEMA_DO = 'eeschema_do'
URL_EESCHEMA_DO = 'https://github.com/INTI-CMNB/kicad-automation-scripts' URL_EESCHEMA_DO = 'https://github.com/INTI-CMNB/kicad-automation-scripts'
@ -201,6 +203,7 @@ W_NOKICOST = '(W066) '
W_UNKOUT = '(W067) ' W_UNKOUT = '(W067) '
W_NOFILTERS = '(W068) ' W_NOFILTERS = '(W068) '
W_NOVARIANTS = '(W069) ' W_NOVARIANTS = '(W069) '
W_NOENDLIB = '(W070) '
class Rect(object): class Rect(object):

View File

@ -104,6 +104,10 @@ class BoMRegex(Optionable):
""" {regex} """ """ {regex} """
self.skip_if_no_field = False self.skip_if_no_field = False
""" Skip this test if the field doesn't exist """ """ Skip this test if the field doesn't exist """
self.match_if_field = False
""" Match if the field exists, no regex applied. Not affected by `invert` """
self.match_if_no_field = False
""" Match if the field doesn't exists, no regex applied. Not affected by `invert` """
self.invert = False self.invert = False
""" Invert the regex match result """ """ Invert the regex match result """
@ -286,6 +290,38 @@ class VariantOptions(BaseOptions):
for gi in self.old_badhes: for gi in self.old_badhes:
gi.SetLayer(self.badhes) gi.SetLayer(self.badhes)
def remove_fab(self, board, comps_hash):
""" Remove from Fab the excluded components. """
ffab = board.GetLayerID('F.Fab')
bfab = board.GetLayerID('B.Fab')
old_ffab = []
old_bfab = []
rescue = board.GetLayerID('Rescue')
for m in board.GetModules():
ref = m.GetReference()
c = comps_hash.get(ref, None)
if not c.included:
# Remove any graphical item in the *.Fab layers
for gi in m.GraphicalItems():
l_gi = gi.GetLayer()
if l_gi == ffab:
gi.SetLayer(rescue)
old_ffab.append(gi)
if l_gi == bfab:
gi.SetLayer(rescue)
old_bfab.append(gi)
# Store the data to undo the above actions
self.old_ffab = old_ffab
self.old_bfab = old_bfab
self.ffab = ffab
self.bfab = bfab
def restore_fab(self, board, comps_hash):
for gi in self.old_ffab:
gi.SetLayer(self.ffab)
for gi in self.old_bfab:
gi.SetLayer(self.bfab)
def run(self, output_dir): def run(self, output_dir):
""" Makes the list of components available """ """ Makes the list of components available """
if not self.dnf_filter and not self.variant: if not self.dnf_filter and not self.variant:

View File

@ -163,9 +163,60 @@ class BoMXLSX(BoMLinkable):
""" Head style: modern-blue, modern-green, modern-red and classic """ """ Head style: modern-blue, modern-green, modern-red and classic """
self.kicost = False self.kicost = False
""" Enable KiCost worksheet creation """ """ Enable KiCost worksheet creation """
self.kicost_api_enable = Optionable
""" [string|list(string)=''] List of KiCost APIs to enable """
self.kicost_api_disable = Optionable
""" [string|list(string)=''] List of KiCost APIs to disable """
self.kicost_dist_desc = False
""" Used to add a column with the distributor's description. So you can chek this is the right component """
self.specs = False
""" Enable Specs worksheet creation. Contains specifications for the components.
Works with only some KiCost APIs """
self.specs_columns = BoMColumns
""" [list(dict)|list(string)] Which columns are included in the Specs worksheet. Use `References` for the references,
'Row' for the order and 'Sep' to separate groups at the same level. By default all are included.
Column names are distributor specific, the following aren't: '_desc', '_value', '_tolerance', '_footprint',
'_power', '_current', '_voltage', '_frequency', '_temp_coeff', '_manf', '_size' """
self.logo_scale = 2 self.logo_scale = 2
""" Scaling factor for the logo. Note that this value isn't honored by all spreadsheet software """ """ Scaling factor for the logo. Note that this value isn't honored by all spreadsheet software """
def process_columns_config(self, cols):
if isinstance(cols, type):
return (None, None, None, None, None)
columns = []
column_levels = []
column_comments = []
column_rename = {}
join = []
# Create the different lists
for col in cols:
if isinstance(col, str):
# Just a string, add to the list of used
new_col = col
new_col_l = new_col.lower()
level = 0
comment = ''
if new_col_l[0] == '_':
column_rename[new_col_l] = new_col_l[1:].capitalize()
else:
# A complete entry
new_col = col.field
new_col_l = new_col.lower()
# A column rename
if col.name:
column_rename[new_col_l] = col.name
elif new_col_l[0] == '_':
column_rename[new_col_l] = new_col_l[1:].capitalize()
# Attach other columns
if col.join:
join.append(col.join)
level = col.level
comment = col.comment
columns.append(new_col)
column_levels.append(level)
column_comments.append(comment)
return (columns, column_levels, column_comments, column_rename, join)
def config(self, parent): def config(self, parent):
super().config(parent) super().config(parent)
# Style # Style
@ -173,6 +224,18 @@ class BoMXLSX(BoMLinkable):
self.style = 'modern-blue' self.style = 'modern-blue'
if self.style not in VALID_STYLES: if self.style not in VALID_STYLES:
raise KiPlotConfigurationError('Unknown style `{}`'.format(self.style)) raise KiPlotConfigurationError('Unknown style `{}`'.format(self.style))
# KiCost APIs
if isinstance(self.kicost_api_enable, type):
self.kicost_api_enable = []
elif isinstance(self.kicost_api_enable, str):
self.kicost_api_enable = [self.kicost_api_enable]
if isinstance(self.kicost_api_disable, type):
self.kicost_api_disable = []
elif isinstance(self.kicost_api_disable, str):
self.kicost_api_disable = [self.kicost_api_disable]
# Specs columns
(self.s_columns, self.s_levels, self.s_comments, self.s_rename,
self.s_join) = self.process_columns_config(self.specs_columns)
class ComponentAliases(Optionable): class ComponentAliases(Optionable):
@ -270,6 +333,8 @@ class BoMOptions(BaseOptions):
""" Connectors with the same footprints will be grouped together, independent of the name of the connector """ """ Connectors with the same footprints will be grouped together, independent of the name of the connector """
self.merge_blank_fields = True self.merge_blank_fields = True
""" Component groups with blank fields will be merged into the most compatible group, where possible """ """ Component groups with blank fields will be merged into the most compatible group, where possible """
self.merge_both_blank = True
""" When creating groups two components with empty/missing field will be interpreted as with the same value """
self.group_fields = GroupFields self.group_fields = GroupFields
""" [list(string)] List of fields used for sorting individual components into groups. """ [list(string)] List of fields used for sorting individual components into groups.
Components which match (comparing *all* fields) will be grouped together. Components which match (comparing *all* fields) will be grouped together.
@ -293,7 +358,7 @@ class BoMOptions(BaseOptions):
self.no_conflict = NoConflict self.no_conflict = NoConflict
""" [list(string)] List of fields where we tolerate conflicts. """ [list(string)] List of fields where we tolerate conflicts.
Use it to avoid undesired warnings. Use it to avoid undesired warnings.
By default the field indicated in `fit_field` and the field `part` are excluded """ By default the field indicated in `fit_field`, the field used for variants and the field `part` are excluded """
self.aggregate = Aggregate self.aggregate = Aggregate
""" [list(dict)] Add components from other projects """ """ [list(dict)] Add components from other projects """
self.ref_id = '' self.ref_id = ''
@ -306,6 +371,8 @@ class BoMOptions(BaseOptions):
""" [string|list(string)] Include this distributors list. Default is all the available """ """ [string|list(string)] Include this distributors list. Default is all the available """
self.no_distributors = Optionable self.no_distributors = Optionable
""" [string|list(string)] Exclude this distributors list. They are removed after computing `distributors` """ """ [string|list(string)] Exclude this distributors list. They are removed after computing `distributors` """
self.sort_style = 'type_value'
""" [type_value,type_value_ref,ref] Sorting criteria """
self._format_example = 'CSV' self._format_example = 'CSV'
super().__init__() super().__init__()
@ -457,6 +524,9 @@ class BoMOptions(BaseOptions):
if isinstance(self.no_conflict, type): if isinstance(self.no_conflict, type):
no_conflict.add(self.fit_field) no_conflict.add(self.fit_field)
no_conflict.add('part') no_conflict.add('part')
var_field = self.variant.get_variant_field()
if var_field is not None:
no_conflict.add(var_field)
else: else:
for field in self.no_conflict: for field in self.no_conflict:
no_conflict.add(field.lower()) no_conflict.add(field.lower())

View File

@ -41,6 +41,8 @@ class PDF_Pcb_PrintOptions(VariantOptions):
""" Print layers in separated pages """ """ Print layers in separated pages """
self.mirror = False self.mirror = False
""" Print mirrored (X axis inverted). ONLY for KiCad 6 """ """ Print mirrored (X axis inverted). ONLY for KiCad 6 """
self.hide_excluded = False
""" Hide components in the Fab layer that are marked as excluded by a variant """
super().__init__() super().__init__()
self._expand_ext = 'pdf' self._expand_ext = 'pdf'
@ -75,6 +77,8 @@ class PDF_Pcb_PrintOptions(VariantOptions):
comps_hash = self.get_refs_hash() comps_hash = self.get_refs_hash()
self.cross_modules(board, comps_hash) self.cross_modules(board, comps_hash)
self.remove_paste_and_glue(board, comps_hash) self.remove_paste_and_glue(board, comps_hash)
if self.hide_excluded:
self.remove_fab(board, comps_hash)
# Save the PCB to a temporal file # Save the PCB to a temporal file
with NamedTemporaryFile(mode='w', suffix='.kicad_pcb', delete=False) as f: with NamedTemporaryFile(mode='w', suffix='.kicad_pcb', delete=False) as f:
fname = f.name fname = f.name
@ -84,6 +88,8 @@ class PDF_Pcb_PrintOptions(VariantOptions):
fproj = self._copy_project(fname) fproj = self._copy_project(fname)
self.uncross_modules(board, comps_hash) self.uncross_modules(board, comps_hash)
self.restore_paste_and_glue(board, comps_hash) self.restore_paste_and_glue(board, comps_hash)
if self.hide_excluded:
self.restore_fab(board, comps_hash)
return fname, fproj return fname, fproj
def get_targets(self, out_dir): def get_targets(self, out_dir):

View File

@ -39,6 +39,10 @@ class BaseVariant(RegVariant):
""" [string|list(string)=''] Name of the filter to mark components as 'Do Not Change'. """ [string|list(string)=''] Name of the filter to mark components as 'Do Not Change'.
Use '_kibom_dnc' for the default KiBoM behavior """ Use '_kibom_dnc' for the default KiBoM behavior """
def get_variant_field(self):
''' Returns the name of the field used to determine if the component belongs to teh variant '''
return None
def filter(self, comps): def filter(self, comps):
# Apply all the filters # Apply all the filters
comps = apply_pre_transform(comps, self.pre_transform) comps = apply_pre_transform(comps, self.pre_transform)

View File

@ -32,6 +32,10 @@ class IBoM(BaseVariant): # noqa: F821
self.variants_whitelist = Optionable self.variants_whitelist = Optionable
""" [string|list(string)=''] List of board variants to include in the BOM """ """ [string|list(string)=''] List of board variants to include in the BOM """
def get_variant_field(self):
''' Returns the name of the field used to determine if the component belongs to teh variant '''
return self.variant_field
def config(self, parent): def config(self, parent):
super().config(parent) super().config(parent)
self.pre_transform = BaseFilter.solve_filter(self.pre_transform, 'pre_transform', is_transform=True) self.pre_transform = BaseFilter.solve_filter(self.pre_transform, 'pre_transform', is_transform=True)

View File

@ -33,6 +33,10 @@ class KiBoM(BaseVariant): # noqa: F821
self.variant = Optionable self.variant = Optionable
""" [string|list(string)=''] Board variant(s) """ """ [string|list(string)=''] Board variant(s) """
def get_variant_field(self):
''' Returns the name of the field used to determine if the component belongs to teh variant '''
return self.config_field
def set_def_filters(self, exclude_filter, dnf_filter, dnc_filter): def set_def_filters(self, exclude_filter, dnf_filter, dnc_filter):
""" Filters delegated to the variant """ """ Filters delegated to the variant """
self._def_exclude_filter = exclude_filter self._def_exclude_filter = exclude_filter

View File

@ -36,6 +36,10 @@ class KiCost(BaseVariant): # noqa: F821
""" Valid separators for variants in the variant field. """ Valid separators for variants in the variant field.
Each character is a valid separator """ Each character is a valid separator """
def get_variant_field(self):
''' Returns the name of the field used to determine if the component belongs to teh variant '''
return self.variant_field
def config(self, parent): def config(self, parent):
super().config(parent) super().config(parent)
self.pre_transform = BaseFilter.solve_filter(self.pre_transform, 'pre_transform', self.pre_transform = BaseFilter.solve_filter(self.pre_transform, 'pre_transform',

@ -1 +1 @@
Subproject commit 7906366a496dc5cc3549152a16815aa403e0ea61 Subproject commit 28488c34332cdaf83aa159b387a8b75b22af3f58

View File

@ -1915,7 +1915,7 @@ F 0 "R1" V 1093 3450 50 0000 C CNN
F 1 "R" V 1184 3450 50 0000 C CNN F 1 "R" V 1184 3450 50 0000 C CNN
F 2 "" V 1230 3450 50 0001 C CNN F 2 "" V 1230 3450 50 0001 C CNN
F 3 "~" H 1300 3450 50 0001 C CNN F 3 "~" H 1300 3450 50 0001 C CNN
F 4 "Hi!" H 1300 3450 50 0001 C CNN "Test" F 4 "Hi! \"quoted text\"" H 1300 3450 50 0001 C CNN "Test"
1 1300 3450 1 1300 3450
0 1 1 0 0 1 1 0
$EndComp $EndComp

View File

@ -303,7 +303,7 @@ def test_auto_pcb_and_cfg_5(test_dir):
def test_list(test_dir): def test_list(test_dir):
ctx = context.TestContext(test_dir, 'List', '3Rs', 'pre_and_position', POS_DIR) ctx = context.TestContext(test_dir, 'List', '3Rs', 'pre_and_position', POS_DIR)
ctx.run(extra=['--list'], no_verbose=True, no_out_dir=True, no_board_file=True) ctx.run(extra=['--list'], no_verbose=True, no_out_dir=True)
assert ctx.search_out('run_erc: True') assert ctx.search_out('run_erc: True')
assert ctx.search_out('run_drc: True') assert ctx.search_out('run_drc: True')

View File

@ -23,6 +23,13 @@ MODE_SCH = 1
MODE_PCB = 0 MODE_PCB = 0
# Defined as True to collect real world queries # Defined as True to collect real world queries
ADD_QUERY_TO_KNOWN = False ADD_QUERY_TO_KNOWN = False
# ***** DEBUG!!!
# Test Digi-Key API. We need
# os.environ['DIGIKEY_CACHE_TTL'] = '-1'
# os.environ['DIGIKEY_STORAGE_PATH'] = op.abspath(op.join(op.dirname(__file__), '../../submodules/KiCost/tests/digikey'))
# logger.setLevel(1) # Max. KiCost debug
# ***** End of DEBUG!!!
ng_ver = os.environ.get('KIAUS_USE_NIGHTLY') ng_ver = os.environ.get('KIAUS_USE_NIGHTLY')
if ng_ver: if ng_ver:

View File

@ -29,3 +29,4 @@ outputs:
name: Manufacturer P/N name: Manufacturer P/N
xlsx: xlsx:
kicost: true kicost: true
kicost_api_disable: 'Digi-Key'

View File

@ -39,3 +39,4 @@ outputs:
- Digi-Key - Digi-Key
xlsx: xlsx:
kicost: true kicost: true
kicost_api_disable: 'Digi-Key'

View File

@ -36,3 +36,4 @@ outputs:
- 'Voltage' - 'Voltage'
xlsx: xlsx:
kicost: true kicost: true
kicost_api_disable: 'Digi-Key'