From 56030c5dc9917efbbab1bebd8d82e755c397be2d Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Tue, 26 Apr 2022 09:51:14 -0300 Subject: [PATCH] Added the first stepof the new `--quick-start` option - Should be a way to quickly start using KiBot without any config --- MANIFEST.in | 3 + README.md | 5 +- docs/Makefile | 13 +- docs/samples/generic_plot.kibot.yaml | 5 +- kibot/__main__.py | 109 +-------- .../bom/MacroFab_XYRS.kibot.yaml | 65 ++++++ .../gerber/Elecrow.kibot.yaml | 59 +++++ .../gerber/FusionPCB.kibot.yaml | 59 +++++ .../config_templates/gerber/JLCPCB.kibot.yaml | 133 +++++++++++ .../config_templates/gerber/PCBWay.kibot.yaml | 69 ++++++ kibot/gs.py | 13 ++ kibot/kiplot.py | 208 +++++++++++++++++- kibot/layer.py | 23 +- kibot/misc.py | 1 + kibot/out_any_layer.py | 16 ++ kibot/out_base.py | 17 +- kibot/out_boardview.py | 4 + kibot/out_bom.py | 131 ++++++++++- kibot/out_download_datasheets.py | 17 +- kibot/out_excellon.py | 12 + kibot/out_gencad.py | 4 + kibot/out_gerb_drill.py | 12 + kibot/out_gerber.py | 34 +++ kibot/out_ibom.py | 24 +- kibot/out_pcb_print.py | 99 ++++++++- kibot/out_pcbdraw.py | 34 ++- kibot/out_pdf_sch_print.py | 4 + kibot/out_pdfunite.py | 1 + kibot/out_position.py | 23 ++ kibot/out_render_3d.py | 37 ++++ kibot/out_report.py | 29 ++- kibot/out_step.py | 6 +- kibot/out_svg_sch_print.py | 4 + kibot/report_templates/report_full_svg.txt | 123 +++++++++++ 34 files changed, 1270 insertions(+), 126 deletions(-) create mode 100644 kibot/config_templates/bom/MacroFab_XYRS.kibot.yaml create mode 100644 kibot/config_templates/gerber/Elecrow.kibot.yaml create mode 100644 kibot/config_templates/gerber/FusionPCB.kibot.yaml create mode 100644 kibot/config_templates/gerber/JLCPCB.kibot.yaml create mode 100644 kibot/config_templates/gerber/PCBWay.kibot.yaml create mode 100644 kibot/report_templates/report_full_svg.txt diff --git a/MANIFEST.in b/MANIFEST.in index 31b3599c..9cbff360 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,3 +3,6 @@ include LICENSE include README.md include kibot/report_templates/*.txt include kibot/kicad_colors/*.json +include kibot/kicad_layouts/*.kicad_wks +include kibot/config_templates/gerbers/*.yaml +include kibot/config_templates/bom/*.yaml diff --git a/README.md b/README.md index fc99258e..257b0023 100644 --- a/README.md +++ b/README.md @@ -1705,6 +1705,7 @@ Next time you need this list just use an alias, like this: * Valid keys: - `add_background`: [boolean=false] Add a background to the pages, see `background_color`. - `background_color`: [string='#FFFFFF'] Color for the background when `add_background` is enabled. + - `background_image`: [string=''] Background image, must be an SVG, only when `add_background` is enabled. - `blind_via_color`: [string=''] Color used for blind/buried `colored_vias`. - `color_theme`: [string='_builtin_classic'] Selects the color theme. Only applies to KiCad 6. To use the KiCad 6 default colors select `_builtin_default`. @@ -2195,7 +2196,8 @@ Next time you need this list just use an alias, like this: In Debian/Ubuntu environments: install `pandoc`, `texlive-latex-base` and `texlive-latex-recommended`. - `eurocircuits_class_target`: [string='10F'] Which Eurocircuits class are we aiming at. - `output`: [string='%f-%i%I%v.%x'] Output file name (%i='report', %x='txt'). Affected by global options. - - `template`: [string='full'] Name for one of the internal templates (full, simple) or a custom template file. + - `template`: [string='full'] Name for one of the internal templates (full, full_svg, simple) or a custom template file. + Note: when converting to PDF PanDoc can fail on some Unicode values (use `simple_ASCII`). - `output_id`: [string=''] Text to use for the %I expansion content. To differentiate variations of this output. - `run_by_default`: [boolean=true] When enabled this output will be created when no specific outputs are requested. @@ -2812,6 +2814,7 @@ Usage: [-q | -v...] [-i] [-C] [-m MKFILE] [-g DEF]... [TARGET...] kibot [-v...] [-b BOARD] [-e SCHEMA] [-c PLOT_CONFIG] --list kibot [-v...] [-b BOARD] [-d OUT_DIR] [-p | -P] --example + kibot [-v...] --quick-start kibot [-v...] --help-filters kibot [-v...] --help-global-options kibot [-v...] --help-list-outputs diff --git a/docs/Makefile b/docs/Makefile index a02923e1..7b9d938e 100755 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,6 +1,14 @@ #!/usr/bin/make -all: ../README.md samples/generic_plot.kibot.yaml +all: ../README.md samples/generic_plot.kibot.yaml ../kibot/report_templates/report_full_svg.txt ../kibot/config_templates/bom/MacroFab_XYRS.kibot.yaml \ + ../kibot/config_templates/gerber/Elecrow.kibot.yaml ../kibot/config_templates/gerber/FusionPCB.kibot.yaml \ + ../kibot/config_templates/gerber/JLCPCB.kibot.yaml ../kibot/config_templates/gerber/PCBWay.kibot.yaml + +../kibot/config_templates/gerber/%.kibot.yaml: samples/%.kibot.yaml + cp $< $@ + +../kibot/config_templates/bom/%.kibot.yaml: samples/%.kibot.yaml + cp $< $@ ../README.md: README.in replace_tags.pl ../kibot/out_*.py ../kibot/pre_*.py ../kibot/fil_*.py ../kibot/__main__.py ../kibot/config_reader.py cat README.in | perl replace_tags.pl > ../README.md @@ -9,3 +17,6 @@ samples/generic_plot.kibot.yaml: ../kibot/out_*.py ../kibot/pre_*.py ../kibot/co rm -f example_template.kibot.yaml ../src/kibot -v --example mv example_template.kibot.yaml $@ + +../kibot/report_templates/report_full_svg.txt: ../kibot/report_templates/report_full.txt + sed -e 's/layer_pdfs/layer_svgs/' $< > $@ diff --git a/docs/samples/generic_plot.kibot.yaml b/docs/samples/generic_plot.kibot.yaml index 121ea4cf..0295d359 100644 --- a/docs/samples/generic_plot.kibot.yaml +++ b/docs/samples/generic_plot.kibot.yaml @@ -970,6 +970,8 @@ outputs: add_background: false # [string='#FFFFFF'] Color for the background when `add_background` is enabled background_color: '#FFFFFF' + # [string=''] Background image, must be an SVG, only when `add_background` is enabled + background_image: '' # [string=''] Color used for blind/buried `colored_vias` blind_via_color: '' # [string='_builtin_classic'] Selects the color theme. Only applies to KiCad 6. @@ -1514,7 +1516,8 @@ outputs: eurocircuits_class_target: '10F' # [string='%f-%i%I%v.%x'] Output file name (%i='report', %x='txt'). Affected by global options output: '%f-%i%I%v.%x' - # [string='full'] Name for one of the internal templates (full, simple) or a custom template file + # [string='full'] Name for one of the internal templates (full, full_svg, simple) or a custom template file. + # Note: when converting to PDF PanDoc can fail on some Unicode values (use `simple_ASCII`) template: 'full' # Schematic with variant generator: # This copy isn't intended for development. diff --git a/kibot/__main__.py b/kibot/__main__.py index 965c3efc..a6372f7f 100644 --- a/kibot/__main__.py +++ b/kibot/__main__.py @@ -12,6 +12,7 @@ Usage: [-q | -v...] [-i] [-C] [-m MKFILE] [-g DEF]... [TARGET...] kibot [-v...] [-b BOARD] [-e SCHEMA] [-c PLOT_CONFIG] --list kibot [-v...] [-b BOARD] [-d OUT_DIR] [-p | -P] --example + kibot [-v...] --quick-start kibot [-v...] --help-filters kibot [-v...] --help-global-options kibot [-v...] --help-list-outputs @@ -87,12 +88,12 @@ if os.environ.get('KIAUS_USE_NIGHTLY'): # pragma: no cover (nightly) os.environ['PYTHONPATH'] = pcbnew_path nightly = True from .gs import (GS) -from .misc import (NO_PCB_FILE, NO_SCH_FILE, EXIT_BAD_ARGS, W_VARSCH, W_VARCFG, W_VARPCB, NO_PCBNEW_MODULE, - W_NOKIVER, hide_stderr) +from .misc import (EXIT_BAD_ARGS, W_VARCFG, NO_PCBNEW_MODULE, W_NOKIVER, hide_stderr) from .pre_base import (BasePreFlight) from .config_reader import (CfgYamlReader, print_outputs_help, print_output_help, print_preflights_help, create_example, print_filters_help, print_global_options_help) -from .kiplot import (generate_outputs, load_actions, config_output, generate_makefile) +from .kiplot import (generate_outputs, load_actions, config_output, generate_makefile, generate_examples, solve_schematic, + solve_board_file, solve_project_file, check_board_file) GS.kibot_version = __version__ @@ -112,59 +113,6 @@ def list_pre_and_outs(logger, outputs): logger.info('- '+str(o)) -def solve_schematic(a_schematic, a_board_file, config): - schematic = a_schematic - if not schematic and a_board_file: - base = os.path.splitext(a_board_file)[0] - sch = base+'.sch' - if os.path.isfile(sch): - schematic = sch - else: - sch = base+'.kicad_sch' - if os.path.isfile(sch): - schematic = sch - if not schematic: - schematics = glob('*.sch')+glob('*.kicad_sch') - if len(schematics) == 1: - schematic = schematics[0] - logger.info('Using SCH file: '+schematic) - elif len(schematics) > 1: - # Look for a schematic with the same name as the config - if config[0] == '.': - # Unhide hidden config - config = config[1:] - while '.' in config: - config = os.path.splitext(config)[0] - sch = config+'.sch' - if os.path.isfile(sch): - schematic = sch - else: - sch = config+'.kicad_sch' - if os.path.isfile(sch): - schematic = sch - else: - # Look for a schematic with a PCB and/or project - for sch in schematics: - base = os.path.splitext(sch)[0] - if (os.path.isfile(base+'.pro') or os.path.isfile(base+'.kicad_pro') or - os.path.isfile(base+'.kicad_pcb')): - schematic = sch - break - else: - schematic = schematics[0] - logger.warning(W_VARSCH + 'More than one SCH file found in current directory.\n' - ' Using '+schematic+' if you want to use another use -e option.') - if schematic and not os.path.isfile(schematic): - logger.error("Schematic file not found: "+schematic) - sys.exit(NO_SCH_FILE) - if schematic: - schematic = os.path.abspath(schematic) - logger.debug('Using schematic: `{}`'.format(schematic)) - else: - logger.debug('No schematic file found') - return schematic - - def solve_config(a_plot_config): plot_config = a_plot_config if not plot_config: @@ -186,35 +134,6 @@ def solve_config(a_plot_config): return plot_config -def check_board_file(board_file): - if board_file and not os.path.isfile(board_file): - logger.error("Board file not found: "+board_file) - sys.exit(NO_PCB_FILE) - - -def solve_board_file(schematic, a_board_file): - board_file = a_board_file - if not board_file and schematic: - pcb = os.path.splitext(schematic)[0]+'.kicad_pcb' - if os.path.isfile(pcb): - board_file = pcb - if not board_file: - board_files = glob('*.kicad_pcb') - if len(board_files) == 1: - board_file = board_files[0] - logger.info('Using PCB file: '+board_file) - elif len(board_files) > 1: - board_file = board_files[0] - logger.warning(W_VARPCB + 'More than one PCB file found in current directory.\n' - ' Using '+board_file+' if you want to use another use -b option.') - check_board_file(board_file) - if board_file: - logger.debug('Using PCB: `{}`'.format(board_file)) - else: - logger.debug('No PCB file found') - return board_file - - def set_locale(): """ Try to set the locale for all the cataegories. If it fails try with LC_NUMERIC (the one we need for tests). """ @@ -305,18 +224,6 @@ def detect_kicad(): logger.debug('KiCad config path {}'.format(GS.kicad_conf_path)) -def solve_project_file(): - if GS.pcb_file: - pro_name = GS.pcb_no_ext+GS.pro_ext - if os.path.isfile(pro_name): - return pro_name - if GS.sch_file: - pro_name = GS.sch_no_ext+GS.pro_ext - if os.path.isfile(pro_name): - return pro_name - return None - - def main(): set_locale() ver = 'KiBot '+__version__+' - '+__copyright__+' - License: '+__license__ @@ -366,13 +273,17 @@ def main(): sys.exit(EXIT_BAD_ARGS) create_example(args.board_file, GS.out_dir, args.copy_options, args.copy_and_expand) sys.exit(0) + if args.quick_start: + # Some kind of wizard to get usable examples + generate_examples() + sys.exit(0) # Determine the YAML file plot_config = solve_config(args.plot_config) # Determine the SCH file - GS.set_sch(solve_schematic(args.schematic, args.board_file, plot_config)) + 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)) + GS.set_pcb(solve_board_file('.', args.board_file)) # Determine the project file GS.set_pro(solve_project_file()) diff --git a/kibot/config_templates/bom/MacroFab_XYRS.kibot.yaml b/kibot/config_templates/bom/MacroFab_XYRS.kibot.yaml new file mode 100644 index 00000000..aa92f41d --- /dev/null +++ b/kibot/config_templates/bom/MacroFab_XYRS.kibot.yaml @@ -0,0 +1,65 @@ +# MacroFab compatible XYRS +# https://help.macrofab.com/knowledge/macrofab-required-design-files +kibot: + version: 1 + +filters: + - name: fix_rotation + comment: 'Adjust rotation for JLC' + type: rot_footprint + + - name: only_smd + comment: 'Only SMD parts' + type: generic + exclude_virtual: true + exclude_tht: true + +variants: + - name: rotated + comment: 'Just a place holder for the rotation filter' + type: kibom + variant: rotated + pre_transform: fix_rotation + dnf_filter: only_smd + +outputs: + - name: 'macrofab_xyrs' + comment: "Pick and place file, XYRS style" + type: bom + options: + variant: rotated + output: '%f_MacroFab.XYRS' + units: mils + group_fields: [] + sort_style: ref + use_aux_axis_as_origin: true + ignore_dnf: false + footprint_populate_values: '0,1' + footprint_type_values: '1,2,0' + csv: + separator: ' ' + hide_pcb_info: true + hide_stats_info: true + hide_header: true + columns: + - field: References + name: Designator + - field: Footprint X + name: X-Loc + - field: Footprint Y + name: Y-Loc + - field: Footprint Rot + name: Rotation + - field: Footprint Side + name: Side + - field: Footprint Type + name: Type + - field: Footprint X-Size + name: X-Size + - field: Footprint Y-Size + name: Y-Size + - field: Value + - field: Footprint + - field: Footprint Populate + name: Populate + - field: MPN diff --git a/kibot/config_templates/gerber/Elecrow.kibot.yaml b/kibot/config_templates/gerber/Elecrow.kibot.yaml new file mode 100644 index 00000000..5fdb551e --- /dev/null +++ b/kibot/config_templates/gerber/Elecrow.kibot.yaml @@ -0,0 +1,59 @@ +# Gerber and drill files for Elecrow, without stencil +# URL: https://www.elecrow.com/ +# Based on setting used by Gerber Zipper (https://github.com/g200kg/kicad-gerberzipper) +kibot: + version: 1 + +outputs: + - name: Elecrow_gerbers + comment: Gerbers compatible with Elecrow + type: gerber + dir: Elecrow + options: &gerber_options + exclude_edge_layer: true + exclude_pads_from_silkscreen: true + plot_sheet_reference: false + plot_footprint_refs: true + plot_footprint_values: true + force_plot_invisible_refs_vals: false + tent_vias: true + use_protel_extensions: true + create_gerber_job_file: false + output: "%f.%x" + gerber_precision: 4.6 + use_gerber_x2_attributes: false + use_gerber_net_attributes: false + disable_aperture_macros: true + line_width: 0.1 + uppercase_extensions: true + subtract_mask_from_silk: true + inner_extension_pattern: '.g%n' + edge_cut_extension: '.gml' + layers: + - copper + - F.SilkS + - B.SilkS + - F.Mask + - B.Mask + - Edge.Cuts + + - name: Elecrow_drill + comment: Drill files compatible with Elecrow + type: excellon + dir: Elecrow + options: + pth_and_npth_single_file: false + pth_id: '' + npth_id: '-NPTH' + output: "%f%i.TXT" + + - name: Elecrow + comment: ZIP file for Elecrow + type: compress + dir: Elecrow + options: + files: + - from_output: Elecrow_gerbers + dest: / + - from_output: Elecrow_drill + dest: / diff --git a/kibot/config_templates/gerber/FusionPCB.kibot.yaml b/kibot/config_templates/gerber/FusionPCB.kibot.yaml new file mode 100644 index 00000000..fd49fab3 --- /dev/null +++ b/kibot/config_templates/gerber/FusionPCB.kibot.yaml @@ -0,0 +1,59 @@ +# Gerber and drill files for FusionPCB, without stencil +# URL: https://www.seeedstudio.io/fusion.html +# Based on setting used by Gerber Zipper (https://github.com/g200kg/kicad-gerberzipper) +kibot: + version: 1 + +outputs: + - name: FusionPCB_gerbers + comment: Gerbers compatible with FusionPCB + type: gerber + dir: FusionPCB + options: &gerber_options + exclude_edge_layer: true + exclude_pads_from_silkscreen: true + plot_sheet_reference: false + plot_footprint_refs: true + plot_footprint_values: true + force_plot_invisible_refs_vals: false + tent_vias: true + use_protel_extensions: true + create_gerber_job_file: false + output: "%f.%x" + gerber_precision: 4.6 + use_gerber_x2_attributes: false + use_gerber_net_attributes: false + disable_aperture_macros: true + line_width: 0.1 + uppercase_extensions: true + subtract_mask_from_silk: false + use_aux_axis_as_origin: true + inner_extension_pattern: '.gl%N' + edge_cut_extension: '.gml' + layers: + - copper + - F.SilkS + - B.SilkS + - F.Mask + - B.Mask + - Edge.Cuts + + - name: FusionPCB_drill + comment: Drill files compatible with FusionPCB + type: excellon + dir: FusionPCB + options: + pth_and_npth_single_file: true + use_aux_axis_as_origin: true + output: "%f.TXT" + + - name: FusionPCB + comment: ZIP file for FusionPCB + type: compress + dir: FusionPCB + options: + files: + - from_output: FusionPCB_gerbers + dest: / + - from_output: FusionPCB_drill + dest: / diff --git a/kibot/config_templates/gerber/JLCPCB.kibot.yaml b/kibot/config_templates/gerber/JLCPCB.kibot.yaml new file mode 100644 index 00000000..fb0cfd91 --- /dev/null +++ b/kibot/config_templates/gerber/JLCPCB.kibot.yaml @@ -0,0 +1,133 @@ +# Gerber and drill files for JLCPCB, without stencil +# URL: https://jlcpcb.com/ +# Based on setting used by Gerber Zipper (https://github.com/g200kg/kicad-gerberzipper) +kibot: + version: 1 + +filters: + - name: only_jlc_parts + comment: 'Only parts with JLC (LCSC) code' + type: generic + include_only: + - column: 'LCSC#' + regex: '^C\d+' + +variants: + - name: rotated + comment: 'Just a place holder for the rotation filter' + type: kibom + variant: rotated + pre_transform: _rot_footprint + +outputs: + - name: JLCPCB_gerbers + comment: Gerbers compatible with JLCPCB + type: gerber + dir: JLCPCB + options: &gerber_options + exclude_edge_layer: true + exclude_pads_from_silkscreen: true + plot_sheet_reference: false + plot_footprint_refs: true + plot_footprint_values: false + force_plot_invisible_refs_vals: false + tent_vias: true + use_protel_extensions: true + create_gerber_job_file: false + disable_aperture_macros: true + gerber_precision: 4.6 + use_gerber_x2_attributes: false + use_gerber_net_attributes: false + line_width: 0.1 + subtract_mask_from_silk: true + inner_extension_pattern: '.gp%n' + layers: + # Note: a more generic approach is to use 'copper' but then the filenames + # are slightly different. + - F.Cu + - B.Cu + - In1.Cu + - In2.Cu + - In3.Cu + - In4.Cu + - In5.Cu + - In6.Cu + - F.SilkS + - B.SilkS + - F.Mask + - B.Mask + - Edge.Cuts + + - name: JLCPCB_drill + comment: Drill files compatible with JLCPCB + type: excellon + dir: JLCPCB + options: + pth_and_npth_single_file: false + pth_id: '-PTH' + npth_id: '-NPTH' + metric_units: true + map: gerber + route_mode_for_oval_holes: false + output: "%f%i.%x" + + - name: 'JLCPCB_position' + comment: "Pick and place file, JLCPCB style" + type: position + dir: JLCPCB + options: + variant: rotated + output: '%f_cpl_jlc.%x' + format: CSV + units: millimeters + separate_files_for_front_and_back: false + only_smd: true + columns: + - id: Ref + name: Designator + - Val + - Package + - id: PosX + name: "Mid X" + - id: PosY + name: "Mid Y" + - id: Rot + name: Rotation + - id: Side + name: Layer + + - name: 'JLCPCB_bom' + comment: "BoM for JLCPCB" + type: bom + dir: JLCPCB + options: + output: '%f_%i_jlc.%x' + exclude_filter: 'only_jlc_parts' + ref_separator: ',' + columns: + - field: Value + name: Comment + - field: References + name: Designator + - Footprint + - field: 'LCSC#' + name: 'LCSC Part #' + csv: + hide_pcb_info: true + hide_stats_info: true + quote_all: true + + - name: JLCPCB + comment: ZIP file for JLCPCB + type: compress + dir: JLCPCB + options: + files: + - from_output: JLCPCB_gerbers + dest: / + - from_output: JLCPCB_drill + dest: / + - from_output: JLCPCB_position + dest: / + - from_output: JLCPCB_bom + dest: / diff --git a/kibot/config_templates/gerber/PCBWay.kibot.yaml b/kibot/config_templates/gerber/PCBWay.kibot.yaml new file mode 100644 index 00000000..12670b0a --- /dev/null +++ b/kibot/config_templates/gerber/PCBWay.kibot.yaml @@ -0,0 +1,69 @@ +# Gerber and drill files for PCBWay, with stencil (solder paste) +# URL: https://www.pcbway.com +# Based on setting used by Gerber Zipper (https://github.com/g200kg/kicad-gerberzipper) +kibot: + version: 1 + +outputs: + - name: PCBWay_gerbers + comment: Gerbers compatible with PCBWay + type: gerber + dir: PCBWay + options: &gerber_options + exclude_edge_layer: true + exclude_pads_from_silkscreen: true + plot_sheet_reference: false + plot_footprint_refs: true + plot_footprint_values: true + force_plot_invisible_refs_vals: false + tent_vias: true + use_protel_extensions: true + create_gerber_job_file: false + output: "%f.%x" + gerber_precision: 4.6 + use_gerber_x2_attributes: false + use_gerber_net_attributes: false + disable_aperture_macros: true + line_width: 0.1 + subtract_mask_from_silk: false + inner_extension_pattern: '.gl%N' + layers: + - copper + - F.SilkS + - B.SilkS + - F.Mask + - B.Mask + - F.Paste + - B.Paste + - Edge.Cuts + + - name: PCBWay_drill + comment: Drill files compatible with PCBWay + type: excellon + dir: PCBWay + options: + metric_units: false + minimal_header: true + zeros_format: SUPPRESS_LEADING + # left_digits: 3 + # right_digits: 3 + # See https://github.com/INTI-CMNB/kicad-ci-test-spora/issues/1 + # and https://docs.oshpark.com/design-tools/gerbv/fix-drill-format/ + left_digits: 2 + right_digits: 4 + pth_and_npth_single_file: false + pth_id: '' + npth_id: '-NPTH' + output: "%f%i.drl" + + - name: PCBWay + comment: ZIP file for PCBWay + type: compress + dir: PCBWay + options: + format: ZIP + files: + - from_output: PCBWay_gerbers + dest: / + - from_output: PCBWay_drill + dest: / diff --git a/kibot/gs.py b/kibot/gs.py index d6ecdc32..240fc38a 100644 --- a/kibot/gs.py +++ b/kibot/gs.py @@ -350,3 +350,16 @@ class GS(object): def load_sch(): """ Will be repplaced by kiplot.py """ raise AssertionError() + + @staticmethod + def get_useful_layers(useful, layers, include_copper=False): + """ Filters layers selecting the ones from useful """ + from .layer import Layer + if include_copper: + # This is a list of layers that we could add + useful = {la._id for la in Layer.solve(useful)} + # Now filter the list of layers using the ones we are interested on + return [la for la in layers if (include_copper and la.is_copper()) or la._id in useful] + # Similar but keeping the sorting of useful + use = {la._id for la in layers} + return [la for la in Layer.solve(useful) if la._id in use] diff --git a/kibot/kiplot.py b/kibot/kiplot.py index 216a8dbb..fdc01f60 100644 --- a/kibot/kiplot.py +++ b/kibot/kiplot.py @@ -11,6 +11,7 @@ Main KiBot code import os import re +import yaml from sys import exit from sys import path as sys_path from shutil import which @@ -25,7 +26,7 @@ from .registrable import RegOutput from .misc import (PLOT_ERROR, MISSING_TOOL, CMD_EESCHEMA_DO, URL_EESCHEMA_DO, CORRUPTED_PCB, EXIT_BAD_ARGS, CORRUPTED_SCH, EXIT_BAD_CONFIG, WRONG_INSTALL, UI_SMD, UI_VIRTUAL, MOD_SMD, MOD_THROUGH_HOLE, MOD_VIRTUAL, W_PCBNOSCH, W_NONEEDSKIP, W_WRONGCHAR, name2make, W_TIMEOUT, - W_KIAUTO) + W_KIAUTO, W_VARSCH, NO_SCH_FILE, NO_PCB_FILE, W_VARPCB) from .error import PlotError, KiPlotConfigurationError, config_error, trace_dump from .pre_base import BasePreFlight from .kicad.v5_sch import Schematic, SchFileError, SchError @@ -564,6 +565,211 @@ def generate_makefile(makefile, cfg_file, outputs, kibot_sys=False): f.write('.PHONY: '+' '.join(extra_targets+list(targets.keys()))+'\n') +def solve_schematic(base_dir, a_schematic=None, a_board_file=None, config=None): + schematic = a_schematic + if not schematic and a_board_file: + base = os.path.splitext(a_board_file)[0] + sch = os.path.join(base_dir, base+'.sch') + if os.path.isfile(sch): + schematic = sch + else: + sch = os.path.join(base_dir, base+'.kicad_sch') + if os.path.isfile(sch): + schematic = sch + if not schematic: + schematics = glob(os.path.join(base_dir, '*.sch'))+glob(os.path.join(base_dir, '*.kicad_sch')) + if len(schematics) == 1: + schematic = schematics[0] + logger.info('Using SCH file: '+schematic) + elif len(schematics) > 1: + # Look for a schematic with the same name as the config + if config: + if config[0] == '.': + # Unhide hidden config + config = config[1:] + # Remove any extension + while '.' in config: + config = os.path.splitext(config)[0] + # Try KiCad 5 + sch = os.path.join(base_dir, config+'.sch') + if os.path.isfile(sch): + schematic = sch + else: + # Try KiCad 6 + sch = os.path.join(base_dir, config+'.kicad_sch') + if os.path.isfile(sch): + schematic = sch + if not schematic: + # Look for a schematic with a PCB and/or project + for sch in schematics: + base = os.path.splitext(sch)[0] + if (os.path.isfile(os.path.join(base_dir, base+'.pro')) or + os.path.isfile(os.path.join(base_dir, base+'.kicad_pro')) or + os.path.isfile(os.path.join(base_dir, base+'.kicad_pcb'))): + schematic = sch + break + else: + # No way to select one, just take the first + schematic = schematics[0] + logger.warning(W_VARSCH + 'More than one SCH file found in current directory.\n' + ' Using '+schematic+' if you want to use another use -e option.') + if schematic and not os.path.isfile(schematic): + logger.error("Schematic file not found: "+schematic) + exit(NO_SCH_FILE) + if schematic: + schematic = os.path.abspath(schematic) + logger.debug('Using schematic: `{}`'.format(schematic)) + else: + logger.debug('No schematic file found') + return schematic + + +def check_board_file(board_file): + if board_file and not os.path.isfile(board_file): + logger.error("Board file not found: "+board_file) + exit(NO_PCB_FILE) + + +def solve_board_file(base_dir, a_board_file=None): + schematic = GS.sch_file + board_file = a_board_file + if not board_file and schematic: + pcb = os.path.join(base_dir, os.path.splitext(schematic)[0]+'.kicad_pcb') + if os.path.isfile(pcb): + board_file = pcb + if not board_file: + board_files = glob(os.path.join(base_dir, '*.kicad_pcb')) + if len(board_files) == 1: + board_file = board_files[0] + logger.info('Using PCB file: '+board_file) + elif len(board_files) > 1: + board_file = board_files[0] + logger.warning(W_VARPCB + 'More than one PCB file found in current directory.\n' + ' Using '+board_file+' if you want to use another use -b option.') + check_board_file(board_file) + if board_file: + logger.debug('Using PCB: `{}`'.format(board_file)) + else: + logger.debug('No PCB file found') + return board_file + + +def solve_project_file(): + if GS.pcb_file: + pro_name = GS.pcb_no_ext+GS.pro_ext + if os.path.isfile(pro_name): + return pro_name + if GS.sch_file: + pro_name = GS.sch_no_ext+GS.pro_ext + if os.path.isfile(pro_name): + return pro_name + return None + + +def look_for_used_layers(): + layers = set() + components = {} + # Look inside the modules + for m in GS.get_modules(): + layer = m.GetLayer() + components[layer] = components.get(layer, 0)+1 + for gi in m.GraphicalItems(): + layers.add(gi.GetLayer()) + for pad in m.Pads(): + for id in pad.GetLayerSet().Seq(): + layers.add(id) + # All drawings in the PCB + for e in GS.board.GetDrawings(): + layers.add(e.GetLayer()) + # Zones + for e in list(GS.board.Zones()): + layers.add(e.GetLayer()) + # Tracks and vias + via_type = 'VIA' if GS.ki5() else 'PCB_VIA' + for e in GS.board.GetTracks(): + if e.GetClass() == via_type: + for id in e.GetLayerSet().Seq(): + layers.add(id) + else: + layers.add(e.GetLayer()) + # Now filter the pads and vias potential layers + from .layer import Layer + declared_layers = {la._id for la in Layer.solve('all')} + layers = sorted(declared_layers.intersection(layers)) + logger.debug('- Detected layers: {}'.format(layers)) + layers = Layer.solve(layers) + for la in layers: + la.components = components.get(la._id, 0) + return layers + + +def generate_examples(): + # Set default global options + glb = GS.global_opts_class() + glb.set_tree({}) + glb.config(None) + + outs = RegOutput.get_registered() + fname = 'kibot_generated.kibot.yaml' + GS.set_sch(solve_schematic('.')) + GS.set_pcb(solve_board_file('.')) + GS.set_pro(solve_project_file()) + if not GS.pcb_file and not GS.sch_file: + return + GS.board = None + GS.sch = None + with open(fname, 'wt') as f: + logger.info('Creating {} example configuration'.format(fname)) + f.write("# This is a working example.\n") + f.write("# For a more complete reference use `--example`\n") + f.write('kibot:\n version: 1\n\n') + # Outputs + outs = RegOutput.get_registered() + # List of layers + layers = [] + if GS.pcb_file: + load_board(GS.pcb_file) + layers = look_for_used_layers() + if GS.sch_file: + load_sch() + # A helper for the JLCPCB stuff + fil = {'name': 'only_jlc_parts'} + fil['comment'] = 'Only parts with JLC (LCSC) code' + fil['type'] = 'generic' + fil['include_only'] = [{'column': 'LCSC#', 'regex': r'^C\d+'}] + f.write(yaml.dump({'filters': [fil]}, sort_keys=False)) + f.write('\n') + # A helper for KiCost demo + var = {'name': 'place_holder'} + var['comment'] = 'Just a place holder for pre_transform filters' + var['type'] = 'kicost' + var['pre_transform'] = ['_kicost_rename', '_rot_footprint'] + f.write(yaml.dump({'variants': [var]}, sort_keys=False)) + f.write('\n') + # All the outputs + outputs = [] + for n, cls in OrderedDict(sorted(outs.items())).items(): + o = cls() + if not(o.is_pcb() and GS.pcb_file) and not(o.is_sch() and GS.sch_file): + logger.debug('- {}, skipped (PCB: {} SCH: {})'.format(n, o.is_pcb(), o.is_sch())) + continue + # Look for templates + tpls = glob(os.path.join(os.path.dirname(__file__), 'config_templates', n, '*.kibot.yaml')) + if tpls: + # Load the templates + tpl_names = tpls + tpls = [yaml.safe_load(open(t))['outputs'] for t in tpls] + tree = cls.get_conf_examples(n, layers, tpls) + if tree: + logger.debug('- {}, generated'.format(n)) + if tpls: + logger.debug(' - Templates: {}'.format(tpl_names)) + outputs.extend(tree) + else: + logger.debug('- {}, nothing to do'.format(n)) + f.write(yaml.dump({'outputs': outputs}, sort_keys=False)) + + # To avoid circular dependencies: Optionable needs it, but almost everything needs Optionable GS.load_board = load_board GS.load_sch = load_sch diff --git a/kibot/layer.py b/kibot/layer.py index 1a6b7922..bcc3c287 100644 --- a/kibot/layer.py +++ b/kibot/layer.py @@ -219,6 +219,8 @@ class Layer(Optionable): raise PlotError("Inner layer `{}` is not valid for this board".format(layer)) layer.fix_protel_ext() new_vals.append(layer) + elif isinstance(layer, int): + new_vals.append(cls.create_layer(layer)) else: # A string ext = None if layer == 'all': @@ -263,10 +265,16 @@ class Layer(Optionable): @classmethod def create_layer(cls, name): layer = cls() - layer.layer = name + if isinstance(name, str): + layer.layer = name + layer._get_layer_id_from_name() + else: + layer._id = name + layer._is_inner = name > pcbnew.F_Cu and name < pcbnew.B_Cu + name = GS.board.GetLayerName(name) + layer.layer = name layer.suffix = name.replace('.', '_') - layer.description = Layer.DEFAULT_LAYER_DESC.get(name) - layer._get_layer_id_from_name() + layer.description = Layer.DEFAULT_LAYER_DESC.get(name, '') layer.fix_protel_ext() layer.clean_suffix() return layer @@ -310,6 +318,15 @@ class Layer(Optionable): raise KiPlotConfigurationError("Unknown layer name: `{}`".format(self.layer)) return self._id + def is_copper(self): + return self._id >= pcbnew.F_Cu and self._id <= pcbnew.B_Cu + + def is_top(self): + return self._id == pcbnew.F_Cu + + def is_bottom(self): + return self._id == pcbnew.B_Cu + def __str__(self): if hasattr(self, '_id'): return "{} ({} '{}' {})".format(self.layer, self._id, self.description, self.suffix) diff --git a/kibot/misc.py b/kibot/misc.py index 42e30a78..26121e73 100644 --- a/kibot/misc.py +++ b/kibot/misc.py @@ -236,6 +236,7 @@ W_WKSVERSION = '(W086) ' W_WRONGOAR = '(W087) ' W_ECCLASST = '(W088) ' W_PDMASKFAIL = '(W089) ' +W_MISSTOOL = '(W090) ' # Somehow arbitrary, the colors are real, but can be different PCB_MAT_COLORS = {'fr1': "937042", 'fr2': "949d70", 'fr3': "adacb4", 'fr4': "332B16", 'fr5': "6cc290"} PCB_FINISH_COLORS = {'hal': "8b898c", 'hasl': "8b898c", 'imag': "8b898c", 'enig': "cfb96e", 'enepig': "cfb96e", diff --git a/kibot/out_any_layer.py b/kibot/out_any_layer.py index eebed086..ee6112eb 100644 --- a/kibot/out_any_layer.py +++ b/kibot/out_any_layer.py @@ -253,5 +253,21 @@ class AnyLayer(BaseOutput): def get_targets(self, out_dir): return self.options.get_targets(out_dir, self.layers) + @staticmethod + def layer2dict(la): + return {'layer': la.layer, 'suffix': la.suffix, 'description': la.description} + + @staticmethod + def get_conf_examples(name, layers, templates): + gb = {} + outs = [gb] + name_u = name.upper() + gb['name'] = 'basic_'+name + gb['comment'] = 'Individual layers in '+name_u+' format' + gb['type'] = name + gb['dir'] = os.path.join('Individual_Layers', name_u) + gb['layers'] = [AnyLayer.layer2dict(la) for la in layers] + return outs + def run(self, output_dir): self.options.run(output_dir, self.layers) diff --git a/kibot/out_base.py b/kibot/out_base.py index 1023f2c9..b88033c5 100644 --- a/kibot/out_base.py +++ b/kibot/out_base.py @@ -56,6 +56,7 @@ class BaseOutput(RegOutput): self.dir = GS.global_dir self._sch_related = False self._both_related = False + self._none_related = False self._unkown_is_error = True self._done = False @@ -69,7 +70,7 @@ class BaseOutput(RegOutput): def is_pcb(self): """ True for outputs that works on the PCB """ - return (not self._sch_related) or self._both_related + return (not(self._sch_related) and not(self._none_related)) or self._both_related def get_targets(self, out_dir): """ Returns a list of targets generated by this output """ @@ -128,6 +129,20 @@ class BaseOutput(RegOutput): name = self.options.expand_filename_both(name, is_sch=self._sch_related) return os.path.abspath(os.path.join(out_dir, name)) + @staticmethod + def get_conf_examples(name, layers, templates): + return None + + @staticmethod + def simple_conf_examples(name, comment, dir): + gb = {} + outs = [gb] + gb['name'] = 'basic_'+name + gb['comment'] = comment + gb['type'] = name + gb['dir'] = dir + return outs + def run(self, output_dir): self.output_dir = output_dir self.options.run(self.expand_filename(output_dir, self.options.output)) diff --git a/kibot/out_boardview.py b/kibot/out_boardview.py index d7fc5227..7b7e04e2 100644 --- a/kibot/out_boardview.py +++ b/kibot/out_boardview.py @@ -178,3 +178,7 @@ class BoardView(BaseOutput): # noqa: F821 with document: self.options = BoardViewOptions """ [dict] Options for the `boardview` output """ + + @staticmethod + def get_conf_examples(name, layers, templates): + return BaseOutput.simple_conf_examples(name, 'Board View export', 'Assembly') # noqa: F821 diff --git a/kibot/out_bom.py b/kibot/out_bom.py index a246d05c..f4b74acd 100644 --- a/kibot/out_bom.py +++ b/kibot/out_bom.py @@ -9,16 +9,19 @@ This is somehow compatible with KiBoM. """ import os import re +from copy import deepcopy from .gs import GS -from .misc import W_BADFIELD, W_NEEDSPCB +from .misc import W_BADFIELD, W_NEEDSPCB, DISTRIBUTORS from .optionable import Optionable, BaseOptions from .registrable import RegOutput from .error import KiPlotConfigurationError from .kiplot import get_board_comps_data, load_any_sch from .bom.columnlist import ColumnList, BoMError from .bom.bom import do_bom +from .bom.xlsx_writer import KICOST_SUPPORT from .var_kibom import KiBoM -from .fil_base import BaseFilter, apply_exclude_filter, apply_fitted_filter, apply_fixed_filter, reset_filters +from .fil_base import (BaseFilter, apply_exclude_filter, apply_fitted_filter, apply_fixed_filter, reset_filters, + KICOST_NAME_TRANSLATIONS) from .macros import macros, document, output_class # noqa: F401 from . import log # To debug the `with document` we can use: @@ -479,7 +482,8 @@ class BoMOptions(BaseOptions): def _get_columns(): """ Create a list of valid columns """ if GS.sch: - return (GS.sch.get_field_names(ColumnList.COLUMNS_DEFAULT), ColumnList.COLUMNS_EXTRA) + cols = deepcopy(ColumnList.COLUMNS_DEFAULT) + return (GS.sch.get_field_names(cols), ColumnList.COLUMNS_EXTRA) return (ColumnList.COLUMNS_DEFAULT, ColumnList.COLUMNS_EXTRA) def _guess_format(self): @@ -744,3 +748,124 @@ class BoM(BaseOutput): # noqa: F821 self.options = BoMOptions """ [dict] Options for the `bom` output """ self._sch_related = True + + @staticmethod + def create_bom(fmt, subd, group_fields, join_fields, fld_names, cols=None): + gb = {} + gb['name'] = subd.lower()+'_bom_'+fmt.lower() + gb['comment'] = '{} Bill of Materials in {} format'.format(subd, fmt) + gb['type'] = 'bom' + gb['dir'] = os.path.join('BoM', subd) + ops = {'format': fmt} + if group_fields: + ops['group_fields'] = group_fields + if join_fields: + columns = [] + for c in fld_names: + if c.lower() == 'value': + columns.append({'field': c, 'join': list(join_fields)}) + else: + columns.append(c) + ops['columns'] = columns + if cols: + ops['columns'] = cols + if GS.board: + ops['count_smd_tht'] = True + gb['options'] = ops + return gb + + @staticmethod + def process_templates(templates, outs, mpn_fields, dists): + for tpl in templates: + for out in tpl: + if out['type'] == 'bom': + # Use the KiCost + rotate variant + out['options']['variant'] = 'place_holder' + columns = out['options'].get('columns', None) + if columns: + # Rename MPN for something we have, or just remove it + to_remove = None + for c in columns: + fld = c.get('field', '') + if fld.lower() == 'mpn': + if mpn_fields: + c['field'] = 'manf#' + elif dists: + c['field'] = list(dists)[0]+'#' + else: + to_remove = c + if to_remove: + columns.remove(to_remove) + # Currently we have a position example (XYRS) + out['dir'] = 'Position' + outs.append(out) + + @staticmethod + def get_conf_examples(name, layers, templates): + outs = [] + # Make a list of available fields + fld_names, extra_names = BoMOptions._get_columns() + fld_names_l = [f.lower() for f in fld_names] + fld_set = set(fld_names_l) + logger.debug(' - Available fields {}'.format(fld_names_l)) + # Look for the manufaturer part number + mpn_set = {k for k, v in KICOST_NAME_TRANSLATIONS.items() if v == 'manf#'} + mpn_set.add('manf#') + mpn_fields = fld_set.intersection(mpn_set) + # Look for distributor part number + dpn_set = set() + for stub in ['part#', '#', 'p#', 'pn', 'vendor#', 'vp#', 'vpn', 'num']: + for dist in DISTRIBUTORS: + dpn_set.add(dist+stub) + if stub != '#': + dpn_set.add(dist+'_'+stub) + dpn_set.add(dist+'-'+stub) + dpn_fields = fld_set.intersection(dpn_set) + # Collect the used distributors + dists = set() + for dist in DISTRIBUTORS: + for fld in dpn_fields: + if dist in fld: + dists.add(dist) + break + # Add it to the group_fields + xpn_fields = mpn_fields | dpn_fields + group_fields = None + if xpn_fields: + group_fields = GroupFields.get_default().copy() + group_fields.extend(list(xpn_fields)) + logger.debug(' - Adding grouping fields {}'.format(xpn_fields)) + # Look for fields to join to the value + joinable_set = {'tolerance', 'voltage', 'power', 'current'} + join_fields = fld_set.intersection(joinable_set) + if join_fields: + logger.debug(' - Fields to join with Value: {}'.format(join_fields)) + # Create a generic version + for fmt in ['HTML', 'CSV', 'TXT', 'TSV', 'XML', 'XLSX']: + outs.append(BoM.create_bom(fmt, 'Generic', group_fields, join_fields, fld_names)) + if GS.board: + # Create an example showing the positional fields + cols = ColumnList.COLUMNS_DEFAULT + ColumnList.COLUMNS_EXTRA + for fmt in ['HTML', 'XLSX']: + gb = BoM.create_bom(fmt, 'Positional', group_fields, None, fld_names, cols) + gb['options'][fmt.lower()] = {'style': 'modern-red'} + outs.append(gb) + # Create a costs version + if KICOST_SUPPORT: # and dists? + logger.debug(' - KiCost distributors {}'.format(dists)) + grp = group_fields + if group_fields: + # We will apply KiCost rename rules, so we must use their names + grp = GroupFields.get_default().copy() + if mpn_fields: + grp.append('manf#') + for d in dists: + grp.append(d+'#') + gb = BoM.create_bom('XLSX', 'Costs', grp, join_fields, fld_names) + gb['options']['xlsx'] = {'style': 'modern-green', 'kicost': True, 'specs': True} + gb['options']['variant'] = 'place_holder' + # gb['options']['distributors'] = list(dists) + outs.append(gb) + # Add the list of layers to the templates + BoM.process_templates(templates, outs, mpn_fields, dists) + return outs diff --git a/kibot/out_download_datasheets.py b/kibot/out_download_datasheets.py index f28315e4..f78b5d88 100644 --- a/kibot/out_download_datasheets.py +++ b/kibot/out_download_datasheets.py @@ -17,6 +17,10 @@ logger = log.get_logger() USER_AGENT = 'Mozilla/5.0 (Windows NT 5.2; rv:2.0.1) Gecko/20100101 Firefox/4.0.1' +def is_url(ds): + return ds.startswith('http://') or ds.startswith('https://') + + class Download_Datasheets_Options(VariantOptions): _vars_regex = re.compile(r'\$\{([^\}]+)\}') @@ -110,7 +114,7 @@ class Download_Datasheets_Options(VariantOptions): field_used = True if not c.included or (not c.fitted and not self.dnf): continue - if ds: + if ds and is_url(ds): known = self._urls.get(ds, None) if known is None or self.repeated: name = self.out_name(c) @@ -150,3 +154,14 @@ class Download_Datasheets(BaseOutput): # noqa: F821 def run(self, output_dir): # No output member, just a dir self.options.run(output_dir) + + @staticmethod + def get_conf_examples(name, layers, templates): + has_urls = False + for c in GS.sch.get_components(): + if c.datasheet and is_url(c.datasheet): + has_urls = True + break + if not has_urls: + return None + return BaseOutput.simple_conf_examples(name, 'Download the datasheets', 'Datasheets') # noqa: F821 diff --git a/kibot/out_excellon.py b/kibot/out_excellon.py index 03e64999..fa8fa475 100644 --- a/kibot/out_excellon.py +++ b/kibot/out_excellon.py @@ -55,3 +55,15 @@ class Excellon(BaseOutput): # noqa: F821 with document: self.options = ExcellonOptions """ [dict] Options for the `excellon` output """ + + @staticmethod + def get_conf_examples(name, layers, templates): + gb = {} + outs = [gb] + name_u = name.upper() + gb['name'] = 'basic_'+name + gb['comment'] = 'Drill files in '+name_u+' format' + gb['type'] = name + gb['dir'] = 'Gerbers_and_Drill' + gb['options'] = {'map': 'pdf'} + return outs diff --git a/kibot/out_gencad.py b/kibot/out_gencad.py index 062f8804..da9a0126 100644 --- a/kibot/out_gencad.py +++ b/kibot/out_gencad.py @@ -71,3 +71,7 @@ class GenCAD(BaseOutput): # noqa: F821 with document: self.options = GenCADOptions """ [dict] Options for the `gencad` output """ + + @staticmethod + def get_conf_examples(name, layers, templates): + return BaseOutput.simple_conf_examples(name, 'PCB in GenCAD format', 'Export') # noqa: F821 diff --git a/kibot/out_gerb_drill.py b/kibot/out_gerb_drill.py index a2e35220..26121437 100644 --- a/kibot/out_gerb_drill.py +++ b/kibot/out_gerb_drill.py @@ -32,3 +32,15 @@ class Gerb_Drill(BaseOutput): # noqa: F821 with document: self.options = Gerb_DrillOptions """ [dict] Options for the `gerb_drill` output """ + + @staticmethod + def get_conf_examples(name, layers, templates): + gb = {} + outs = [gb] + name_u = name.upper() + gb['name'] = 'basic_'+name + gb['comment'] = 'Drill files in '+name_u+' format' + gb['type'] = name + gb['dir'] = 'Gerbers_and_Drill' + gb['options'] = {'map': 'gerber'} + return outs diff --git a/kibot/out_gerber.py b/kibot/out_gerber.py index ba385e20..6b22a9c2 100644 --- a/kibot/out_gerber.py +++ b/kibot/out_gerber.py @@ -5,11 +5,16 @@ # License: GPL-3.0 # Project: KiBot (formerly KiPlot) # Adapted from: https://github.com/johnbeard/kiplot +import os from pcbnew import (PLOT_FORMAT_GERBER, FromMM, ToMM) from .gs import GS from .out_any_layer import (AnyLayer, AnyLayerOptions) from .error import KiPlotConfigurationError from .macros import macros, document, output_class # noqa: F401 +from . import log + +logger = log.get_logger() +USEFUL_LAYERS = ['F.SilkS', 'B.SilkS', 'F.Mask', 'B.Mask', 'F.Paste', 'B.Paste', 'Edge.Cuts'] class GerberOptions(AnyLayerOptions): @@ -101,3 +106,32 @@ class Gerber(AnyLayer): with document: self.options = GerberOptions """ [dict] Options for the `gerber` output """ + + @staticmethod + def get_conf_examples(name, layers, templates): + gb = {} + outs = [gb] + # Create a generic version + gb['name'] = 'gerber_modern' + gb['comment'] = 'Gerbers in modern format, recommended by the standard' + gb['type'] = 'gerber' + gb['dir'] = 'Gerbers_and_Drill' + gb['layers'] = [AnyLayer.layer2dict(la) for la in layers] + # Process the templates + # Filter the list of layers using the ones we are interested on + useful = GS.get_useful_layers(USEFUL_LAYERS, layers, include_copper=True) + tpl_layers = [AnyLayer.layer2dict(la) for la in useful] + # Add the list of layers to the templates + for tpl in templates: + for out in tpl: + if out['type'] == 'gerber': + out['layers'] = tpl_layers + elif out['type'] == 'position': + out['options']['variant'] = 'place_holder' + if out['type'] == 'compress': + out['dir'] = 'Manufacturers' + out['options']['move_files'] = True + else: + out['dir'] = os.path.join('Manufacturers', out['dir']) + outs.append(out) + return outs diff --git a/kibot/out_ibom.py b/kibot/out_ibom.py index 60f229d8..f3a9826c 100644 --- a/kibot/out_ibom.py +++ b/kibot/out_ibom.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2020-2021 Salvador E. Tropea -# Copyright (c) 2020-2021 Instituto Nacional de Tecnología Industrial +# Copyright (c) 2020-2022 Salvador E. Tropea +# Copyright (c) 2020-2022 Instituto Nacional de Tecnología Industrial # License: GPL-3.0 # Project: KiBot (formerly KiPlot) import os @@ -16,6 +16,12 @@ logger = log.get_logger() WARNING_MIX = "Avoid using it in conjunction with with IBoM native filtering options" +def check_tool(): + tool = search_as_plugin(CMD_IBOM, ['InteractiveHtmlBom', 'InteractiveHtmlBom/InteractiveHtmlBom']) + check_script(tool, URL_IBOM) + return tool + + class IBoMOptions(VariantOptions): def __init__(self): with document: @@ -136,8 +142,7 @@ class IBoMOptions(VariantOptions): def run(self, name): super().run(name) - tool = search_as_plugin(CMD_IBOM, ['InteractiveHtmlBom', 'InteractiveHtmlBom/InteractiveHtmlBom']) - check_script(tool, URL_IBOM) + tool = check_tool() logger.debug('Doing Interactive BoM') # Tell ibom we don't want to use the screen os.environ['INTERACTIVE_HTML_BOM_NO_DISPLAY'] = '' @@ -205,3 +210,14 @@ class IBoM(BaseOutput): # noqa: F821 def get_dependencies(self): return self.options.get_dependencies() + + @staticmethod + def get_conf_examples(name, layers, templates): + enabled = True + try: + check_tool() + except SystemExit: + enabled = False + if not enabled: + return None + return BaseOutput.simple_conf_examples(name, 'Interactive HTML BoM', 'Assembly') # noqa: F821 diff --git a/kibot/out_pcb_print.py b/kibot/out_pcb_print.py index 994fc82e..3d9aaa03 100644 --- a/kibot/out_pcb_print.py +++ b/kibot/out_pcb_print.py @@ -8,7 +8,8 @@ import re import os import subprocess -from pcbnew import B_Cu, F_Cu, FromMM, IsCopperLayer, PLOT_CONTROLLER, PLOT_FORMAT_SVG, wxSize, F_Mask, B_Mask +from pcbnew import (B_Cu, F_Cu, FromMM, IsCopperLayer, PLOT_CONTROLLER, PLOT_FORMAT_SVG, wxSize, F_Mask, B_Mask, ZONE_FILLER, + ZONES) from shutil import rmtree, which from tempfile import NamedTemporaryFile, mkdtemp from .svgutils.transform import fromstring, RectElement, fromfile @@ -23,7 +24,7 @@ from .kicad.config import KiConf from .kicad.v5_sch import SchError from .kicad.pcb import PCB from .misc import (CMD_PCBNEW_PRINT_LAYERS, URL_PCBNEW_PRINT_LAYERS, PDF_PCB_PRINT, MISSING_TOOL, W_PDMASKFAIL, - KICAD5_SVG_SCALE) + KICAD5_SVG_SCALE, W_MISSTOOL) from .kiplot import check_script, exec_with_retry, add_extra_options from .macros import macros, document, output_class # noqa: F401 from .layer import Layer, get_priority @@ -39,6 +40,8 @@ VIATYPE_BLIND_BURIED = 2 VIATYPE_MICROVIA = 1 POLY_FILL_STYLE = ("fill:{0}; fill-opacity:1.0; stroke:{0}; stroke-width:1; stroke-opacity:1; stroke-linecap:round; " "stroke-linejoin:round;fill-rule:evenodd;") +DRAWING_LAYERS = ['Dwgs.User', 'Cmts.User', 'Eco1.User', 'Eco2.User'] +EXTRA_LAYERS = ['F.Fab', 'B.Fab', 'F.CrtYd', 'B.CrtYd'] def _run_command(cmd): @@ -518,6 +521,7 @@ class PCB_PrintOptions(VariantOptions): moved = [] removed = [] vias = [] + zones = ZONES() wxSize(0, 0) for m in GS.get_modules(): for gi in m.GraphicalItems(): @@ -529,9 +533,10 @@ class PCB_PrintOptions(VariantOptions): if dr.x: continue layers = pad.GetLayerSet() - layers.removeLayer(id) - pad.SetLayerSet(layers) - removed.append(pad) + if layers.Contains(id): + layers.removeLayer(id) + pad.SetLayerSet(layers) + removed.append(pad) for e in GS.board.GetDrawings(): if e.GetLayer() == id: e.SetLayer(tmp_layer) @@ -540,6 +545,7 @@ class PCB_PrintOptions(VariantOptions): if e.GetLayer() == id: e.SetLayer(tmp_layer) moved.append(e) + zones.append(e) via_type = 'VIA' if GS.ki5() else 'PCB_VIA' for e in GS.board.GetTracks(): if e.GetClass() == via_type: @@ -565,6 +571,8 @@ class PCB_PrintOptions(VariantOptions): for (via, drill, width) in vias: via.SetDrill(drill) via.SetWidth(width) + if len(zones): + ZONE_FILLER(GS.board).Fill(zones) # Add it to the list filelist.append((GS.pcb_basename+"-"+suffix+".svg", self.pad_color)) @@ -576,6 +584,7 @@ class PCB_PrintOptions(VariantOptions): moved = [] removed = [] vias = [] + zones = ZONES() wxSize(0, 0) for m in GS.get_modules(): for gi in m.GraphicalItems(): @@ -584,9 +593,10 @@ class PCB_PrintOptions(VariantOptions): moved.append(gi) for pad in m.Pads(): layers = pad.GetLayerSet() - layers.removeLayer(id) - pad.SetLayerSet(layers) - removed.append(pad) + if layers.Contains(id): + layers.removeLayer(id) + pad.SetLayerSet(layers) + removed.append(pad) for e in GS.board.GetDrawings(): if e.GetLayer() == id: e.SetLayer(tmp_layer) @@ -595,6 +605,7 @@ class PCB_PrintOptions(VariantOptions): if e.GetLayer() == id: e.SetLayer(tmp_layer) moved.append(e) + zones.append(e) via_type = 'VIA' if GS.ki5() else 'PCB_VIA' for e in GS.board.GetTracks(): if e.GetClass() == via_type: @@ -640,6 +651,8 @@ class PCB_PrintOptions(VariantOptions): via.SetWidth(width) via.SetTopLayer(top) via.SetBottomLayer(bottom) + if len(zones): + ZONE_FILLER(GS.board).Fill(zones) # Add it to the list filelist.append((GS.pcb_basename+"-"+suffix+".svg", via_c)) @@ -771,6 +784,7 @@ class PCB_PrintOptions(VariantOptions): if id >= F_Cu and id <= B_Cu: if self.colored_pads: self.plot_pads(la, pc, p, filelist) + return if self.colored_vias: self.plot_vias(la, pc, p, filelist, VIATYPE_THROUGH, self.via_color) self.plot_vias(la, pc, p, filelist, VIATYPE_BLIND_BURIED, self.blind_via_color) @@ -1014,3 +1028,72 @@ class PCB_Print(BaseOutput): # noqa: F821 with document: self.options = PCB_PrintOptions """ [dict] Options for the `pcb_print` output """ + + @staticmethod + def get_conf_examples(name, layers, templates): + outs = [] + if len(DRAWING_LAYERS) < 10 and GS.ki6(): + DRAWING_LAYERS.extend(['User.'+str(c+1) for c in range(9)]) + extra = {la._id for la in Layer.solve(EXTRA_LAYERS)} + disabled = set() + # Check we can use PcbDraw + realistic_solder_mask = which('pcbdraw') is not None + if not realistic_solder_mask: + logger.warning(W_MISSTOOL+'Missing PcbDraw tool, disabling `realistic_solder_mask`') + # Check we can convert SVGs + if which(SVG2PDF) is None: + logger.warning(W_MISSTOOL+'Missing {} tool, disabling most printed formats'.format(SVG2PDF)) + disabled |= {'PDF', 'PNG', 'EPS', 'PS'} + # Check we can convert to PS + if which(PDF2PS) is None: + logger.warning(W_MISSTOOL+'Missing {} tool, disabling postscript printed format'.format(PDF2PS)) + disabled.add('PS') + # Generate one output for each format + for fmt in ['PDF', 'SVG', 'PNG', 'EPS', 'PS']: + if fmt in disabled: + continue + gb = {} + gb['name'] = 'basic_{}_{}'.format(name, fmt.lower()) + gb['comment'] = 'PCB' + gb['type'] = name + gb['dir'] = os.path.join('PCB', fmt) + pages = [] + # One page for each Cu layer + for la in layers: + page = None + mirror = False + if la.is_copper(): + if la.is_top(): + use_layers = ['F.Cu', 'F.Mask', 'F.Paste', 'F.SilkS', 'Edge.Cuts'] + elif la.is_bottom(): + use_layers = ['B.Cu', 'B.Mask', 'B.Paste', 'B.SilkS', 'Edge.Cuts'] + mirror = True + else: + use_layers = [la.layer, 'Edge.Cuts'] + useful = GS.get_useful_layers(use_layers+DRAWING_LAYERS, layers) + page = {} + page['layers'] = [{'layer': la.layer} for la in useful] + elif la._id in extra: + useful = GS.get_useful_layers([la, 'Edge.Cuts']+DRAWING_LAYERS, layers) + page = {} + page['layers'] = [{'layer': la.layer} for la in useful] + mirror = la.layer.startswith('B.') + if page: + if mirror: + page['mirror'] = True + if la.description: + page['sheet'] = la.description + if realistic_solder_mask: + # Change the color of the masks + for ly in page['layers']: + if ly['layer'].endswith('.Mask'): + ly['color'] = '#14332440' + pages.append(page) + ops = {'format': fmt, 'pages': pages, 'keep_temporal_files': True} + if fmt in ['PNG', 'SVG']: + ops['add_background'] = True + if not realistic_solder_mask: + ops['realistic_solder_mask'] = False + gb['options'] = ops + outs.append(gb) + return outs diff --git a/kibot/out_pcbdraw.py b/kibot/out_pcbdraw.py index d2de708e..e7340fb5 100644 --- a/kibot/out_pcbdraw.py +++ b/kibot/out_pcbdraw.py @@ -20,6 +20,7 @@ from . import log logger = log.get_logger() SVG2PNG = 'rsvg-convert' CONVERT = 'convert' +MIN_VERSION = '0.6.0' class PcbDrawStyle(Optionable): @@ -256,7 +257,7 @@ class PcbDrawOptions(VariantOptions): def run(self, name): super().run(name) - check_script(PCBDRAW, URL_PCBDRAW, '0.6.0') + check_script(PCBDRAW, URL_PCBDRAW, MIN_VERSION) # Base command with overwrite cmd = [PCBDRAW] # Add user options @@ -331,3 +332,34 @@ class PcbDraw(BaseOutput): # noqa: F821 if isinstance(self.options.style, str) and os.path.isfile(self.options.style): files.append(self.options.style) return files + + @staticmethod + def get_conf_examples(name, layers, templates): + enabled = True + try: + check_script(PCBDRAW, URL_PCBDRAW, MIN_VERSION) + except SystemExit: + enabled = False + if not enabled: + return None + outs = [] + for la in layers: + is_top = la.is_top() + is_bottom = la.is_bottom() + if not is_top and not is_bottom: + continue + id = 'top' if is_top else 'bottom' + for style in ['jlcpcb-green-enig', 'set-blue-enig', 'set-red-hasl']: + style_2 = style.replace('-', '_') + for fmt in ['svg', 'png', 'jpg']: + gb = {} + gb['name'] = 'basic_{}_{}_{}_{}'.format(name, fmt, style_2, id) + gb['comment'] = 'PCB 2D render in {} format, using {} style'.format(fmt.upper(), style) + gb['type'] = name + gb['dir'] = os.path.join('PCB', '2D_render', style_2) + ops = {'style': style, 'format': fmt} + if is_bottom: + ops['bottom'] = True + gb['options'] = ops + outs.append(gb) + return outs diff --git a/kibot/out_pdf_sch_print.py b/kibot/out_pdf_sch_print.py index 9042bfaf..93efa20f 100644 --- a/kibot/out_pdf_sch_print.py +++ b/kibot/out_pdf_sch_print.py @@ -34,3 +34,7 @@ class PDF_SCH_Print(BaseOutput): # noqa: F821 self.options = PDF_SCH_PrintOptions """ [dict] Options for the `pdf_sch_print` output """ self._sch_related = True + + @staticmethod + def get_conf_examples(name, layers, templates): + return BaseOutput.simple_conf_examples(name, 'Schematic in PDF format', 'Schematic') # noqa: F821 diff --git a/kibot/out_pdfunite.py b/kibot/out_pdfunite.py index 6826254a..bb8bfa86 100644 --- a/kibot/out_pdfunite.py +++ b/kibot/out_pdfunite.py @@ -147,6 +147,7 @@ class PDFUnite(BaseOutput): # noqa: F821 with document: self.options = PDFUniteOptions """ [dict] Options for the `pdfunite` output """ + self._none_related = True def get_dependencies(self): return self.options.get_dependencies() diff --git a/kibot/out_position.py b/kibot/out_position.py index 95fdef2c..05a37278 100644 --- a/kibot/out_position.py +++ b/kibot/out_position.py @@ -300,3 +300,26 @@ class Position(BaseOutput): # noqa: F821 with document: self.options = PositionOptions """ [dict] Options for the `position` output """ + + @staticmethod + def get_conf_examples(name, layers, templates): + outs = [] + has_top = False + has_bottom = False + for la in layers: + if la.is_top(): + has_top = la.components + elif la.is_bottom(): + has_bottom = la.components + for fmt in ['ASCII', 'CSV']: + gb = {} + gb['name'] = 'basic_position_{}'.format(fmt) + gb['comment'] = 'Components position for Pick & Place' + gb['type'] = name + gb['dir'] = 'Position' + ops = {'format': fmt, 'only_smd': False} + if not has_top or not has_bottom: + ops['separate_files_for_front_and_back'] = False + gb['options'] = ops + outs.append(gb) + return outs diff --git a/kibot/out_render_3d.py b/kibot/out_render_3d.py index 0e1d81dd..e43739e4 100644 --- a/kibot/out_render_3d.py +++ b/kibot/out_render_3d.py @@ -199,3 +199,40 @@ class Render_3D(Base3D): # noqa: F821 with document: self.options = Render3DOptions """ [dict] Options for the `render_3d` output """ + + @staticmethod + def get_conf_examples(name, layers, templates): + outs = [] + has_top = False + has_bottom = False + for la in layers: + if la.is_top() or la.layer.startswith('F.'): + has_top = True + elif la.is_bottom() or la.layer.startswith('B.'): + has_bottom = True + if has_top: + gb = {} + gb['name'] = 'basic_{}_top'.format(name) + gb['comment'] = '3D view from top' + gb['type'] = name + gb['dir'] = '3D' + gb['options'] = {'ray_tracing': True, 'orthographic': True} + outs.append(gb) + if GS.ki6(): + gb = {} + gb['name'] = 'basic_{}_30deg'.format(name) + gb['comment'] = '3D view from 30 degrees' + gb['type'] = name + gb['dir'] = '3D' + gb['output_id'] = '30deg' + gb['options'] = {'ray_tracing': True, 'rotate_x': 3, 'rotate_z': -2} + outs.append(gb) + if has_bottom: + gb = {} + gb['name'] = 'basic_{}_bottom'.format(name) + gb['comment'] = '3D view from bottom' + gb['type'] = name + gb['dir'] = '3D' + gb['options'] = {'ray_tracing': True, 'orthographic': True, 'view': 'bottom'} + outs.append(gb) + return outs diff --git a/kibot/out_report.py b/kibot/out_report.py index 30c5ae1a..cf6caf07 100644 --- a/kibot/out_report.py +++ b/kibot/out_report.py @@ -7,10 +7,11 @@ import os import re import pcbnew from subprocess import check_output, STDOUT, CalledProcessError +from shutil import which from .gs import GS from .misc import (UI_SMD, UI_VIRTUAL, MOD_THROUGH_HOLE, MOD_SMD, MOD_EXCLUDE_FROM_POS_FILES, PANDOC, MISSING_TOOL, - FAILED_EXECUTE, W_WRONGEXT, W_WRONGOAR, W_ECCLASST) + FAILED_EXECUTE, W_WRONGEXT, W_WRONGOAR, W_ECCLASST, W_MISSTOOL) from .registrable import RegOutput from .out_base import BaseOptions from .error import KiPlotConfigurationError @@ -788,3 +789,29 @@ class Report(BaseOutput): # noqa: F821 with document: self.options = ReportOptions """ [dict] Options for the `report` output """ + + @staticmethod + def get_conf_examples(name, layers, templates): + if which(PANDOC) is None: + logger.warning((W_MISSTOOL+'Missing {} tool, disabling report in PDF format\n'+PANDOC_INSTALL).format(PANDOC)) + pandoc = False + else: + pandoc = True + gb = {} + outs = [gb] + gb['name'] = 'report_simple' + gb['comment'] = 'Simple design report' + gb['type'] = name + gb['output_id'] = '_simple' + gb['options'] = {'template': 'simple_ASCII'} + if pandoc: + gb['options']['do_convert'] = True + gb = {} + gb['name'] = 'report_full' + gb['comment'] = 'Full design report' + gb['type'] = name + gb['options'] = {'template': 'full_SVG'} + if pandoc: + gb['options']['do_convert'] = True + outs.append(gb) + return outs diff --git a/kibot/out_step.py b/kibot/out_step.py index e4d3bc44..ac6f0952 100644 --- a/kibot/out_step.py +++ b/kibot/out_step.py @@ -106,7 +106,7 @@ class STEPOptions(Base3DOptions): @output_class -class STEP(Base3D): # noqa: F821 +class STEP(Base3D): """ STEP (ISO 10303-21 Clear Text Encoding of the Exchange Structure) Exports the PCB as a 3D model. This is the most common 3D format for exchange purposes. @@ -116,3 +116,7 @@ class STEP(Base3D): # noqa: F821 with document: self.options = STEPOptions """ [dict] Options for the `step` output """ + + @staticmethod + def get_conf_examples(name, layers, templates): + return Base3D.simple_conf_examples(name, '3D model in STEP format', '3D') diff --git a/kibot/out_svg_sch_print.py b/kibot/out_svg_sch_print.py index d7760969..47d4075e 100644 --- a/kibot/out_svg_sch_print.py +++ b/kibot/out_svg_sch_print.py @@ -34,3 +34,7 @@ class SVG_SCH_Print(BaseOutput): # noqa: F821 self.options = SVG_SCH_PrintOptions """ [dict] Options for the `svg_sch_print` output """ self._sch_related = True + + @staticmethod + def get_conf_examples(name, layers, templates): + return BaseOutput.simple_conf_examples(name, 'Schematic in SVG format', 'Schematic') # noqa: F821 diff --git a/kibot/report_templates/report_full_svg.txt b/kibot/report_templates/report_full_svg.txt new file mode 100644 index 00000000..0ff4e6b5 --- /dev/null +++ b/kibot/report_templates/report_full_svg.txt @@ -0,0 +1,123 @@ +# PCB + +Board size: ${bb_w_mm}x${bb_h_mm} mm (${bb_w_in}x${bb_h_in} inches) + +- This is the size of the rectangle that contains the board +- Thickness: ${thickness_mm} mm (${thickness_mils} mils) +- Material: ${pcb_material} +- Finish: ${pcb_finish} +- Layers: ${layers} +- Copper thickness: ${copper_thickness} µm + +Solder mask: ${solder_mask} + +- Color: ${solder_mask_color_text} + +Silk screen: ${silk_screen} + +- Color: ${silk_screen_color_text} + +#?edge_connector or castellated_pads or edge_plating +Special features: +#?edge_connector or castellated_pads or edge_plating + +#?edge_connector +- Edge connector: ${edge_connector} +#?castellated_pads +- Castellated pads +#?edge_plating +- Edge plating + +#?stackup +Stackup: +#?stackup and impedance_controlled + +#?stackup and impedance_controlled +Impedance controlled: YES +#?stackup + +#?stackup +| Name | Type | Color | Thickness | Material | Epsilon_r | Loss tangent | +#?stackup +|----------------------|----------------------|----------|-----------|-----------------|-----------|--------------| +#?stackup +#stackup:| ${%-20s,name} | ${%-20s,type} | ${%-8s,color} | ${%9d,thickness} | ${%-15s,material} | ${%9.1f,epsilon_r} | ${%12.2f,loss_tangent} | +#?stackup + +# Important sizes + +Clearance: ${clearance_mm} mm (${clearance_mils} mils) + +Track width: ${track_mm} mm (${track_mils} mils) + +- By design rules: ${track_d_mm} mm (${track_d_mils} mils) + +Drill: ${drill_real_mm} mm (${drill_real_mils} mils) + +- Vias: ${via_drill_real_mm} mm (${via_drill_real_mils} mils) [Design: ${via_drill_real_d_mm} mm (${via_drill_real_d_mils} mils)] +- Pads: ${pad_drill_real_mm} mm (${pad_drill_real_mils} mils) +- The above values are real drill sizes, they add ${extra_pth_drill_mm} mm (${extra_pth_drill_mils} mils) to plated holes (PTH) + +Via: ${via_pad_mm}/${via_drill_mm} mm (${via_pad_mils}/${via_drill_mils} mils) + +- By design rules: ${via_pad_d_mm}/${via_drill_d_mm} mm (${via_pad_d_mils}/${via_drill_d_mils} mils) +- Micro via: ${micro_vias} [${uvia_pad_mm}/${uvia_drill_mm} mm (${uvia_pad_mils}/${uvia_drill_mils} mils)] +- Burried/blind via: ${blind_vias} + +Outer Annular Ring: ${oar_mm} mm (${oar_mils} mils) + +- By design rules: ${oar_d_mm} mm (${oar_d_mils} mils) + +Eurocircuits class: ${pattern_class}${drill_class} + + +# General stats + +Components count: (SMD/THT) + +- Top: ${top_smd}/${top_tht} (${top_comp_type}) +- Bottom: ${bot_smd}/${bot_tht} (${bot_comp_type}) + +Defined tracks: + +#defined_tracks:- ${track_mm} mm (${track_mils} mils) + +Used tracks: + +#used_tracks:- ${track_mm} mm (${track_mils} mils) (${count}) defined: ${defined} + +Defined vias: + +#defined_vias:- ${pad_mm}/${drill_mm} mm (${pad_mils}/${drill_mils} mils) + +Used vias: + +#used_vias:- ${pad_mm}/${drill_mm} mm (${pad_mils}/${drill_mils} mils) (Count: ${count}, Aspect: ${aspect} ${producibility_level}) defined: ${defined} + +Holes (excluding vias): + +#hole_sizes_no_vias:- ${drill_mm} mm (${drill_mils} mils) (${count}) + +Oval holes: + +#oval_hole_sizes:- ${drill_1_mm}x${drill_2_mm} mm (${drill_1_mils}x${drill_2_mils} mils) (${count}) + +Drill tools (including vias and computing adjusts and rounding): + +#drill_tools:- ${drill_mm} mm (${drill_mils} mils) (${count}) + + +#?schematic_svgs +# Schematic +#?schematic_svgs + +#?schematic_svgs +#schematic_svgs:![${comment}](${path}){ width=16.5cm height=11.7cm }${new_line} + + +#?layer_svgs +# PCB Layers +#?layer_svgs + +#?layer_svgs +#layer_svgs:![${comment}](${path}){ width=16.5cm height=11.7cm }${new_line}