diff --git a/CHANGELOG.md b/CHANGELOG.md index 22adcd2b..272ac28b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Now layers can be selected using the default KiCad names. - More control over the name of the drill and gerber files. - More options to customize the excellon output. +- Custom reports for plot outputs (i.e. custom gerber job generation) ### Changed - Now the default output name applies to the DRC and ERC report names. diff --git a/README.md b/README.md index 5301de73..97726f8f 100644 --- a/README.md +++ b/README.md @@ -662,6 +662,11 @@ Next time you need this list just use an alias, like this: - `name`: [string=''] Used to identify this particular output definition. - `options`: [dict] Options for the `dxf` output. * Valid keys: + - `custom_reports`: [list(dict)] A list of customized reports for the manufacturer. + * Valid keys: + - `content`: [string=''] Content for the report. Use ${basename} for the project name without extension. + Use ${filename(LAYER)} for the file corresponding to LAYER. + - `output`: [string='Custom_report.txt'] File name for the custom report. - `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. - `drill_marks`: [string='full'] what to use to indicate the drill places, can be none, small or full (for real scale). @@ -759,6 +764,11 @@ Next time you need this list just use an alias, like this: * Valid keys: - `create_gerber_job_file`: [boolean=true] Creates a file with information about all the generated gerbers. You can use it in gerbview to load all gerbers at once. + - `custom_reports`: [list(dict)] A list of customized reports for the manufacturer. + * Valid keys: + - `content`: [string=''] Content for the report. Use ${basename} for the project name without extension. + Use ${filename(LAYER)} for the file corresponding to LAYER. + - `output`: [string='Custom_report.txt'] File name for the custom report. - `disable_aperture_macros`: [boolean=false] Disable aperture macros (workaround for buggy CAM software) (KiCad 6). - `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. @@ -801,6 +811,11 @@ Next time you need this list just use an alias, like this: - `name`: [string=''] Used to identify this particular output definition. - `options`: [dict] Options for the `hpgl` output. * Valid keys: + - `custom_reports`: [list(dict)] A list of customized reports for the manufacturer. + * Valid keys: + - `content`: [string=''] Content for the report. Use ${basename} for the project name without extension. + Use ${filename(LAYER)} for the file corresponding to LAYER. + - `output`: [string='Custom_report.txt'] File name for the custom report. - `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. - `drill_marks`: [string='full'] what to use to indicate the drill places, can be none, small or full (for real scale). @@ -1025,6 +1040,11 @@ Next time you need this list just use an alias, like this: This output is what you get from the File/Plot menu in pcbnew. * Valid keys: - `comment`: [string=''] A comment for documentation purposes. + - `custom_reports`: [list(dict)] A list of customized reports for the manufacturer. + * Valid keys: + - `content`: [string=''] Content for the report. Use ${basename} for the project name without extension. + Use ${filename(LAYER)} for the file corresponding to LAYER. + - `output`: [string='Custom_report.txt'] File name for the custom report. - `dir`: [string='.'] Output directory for the generated files. - `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. @@ -1045,6 +1065,11 @@ Next time you need this list just use an alias, like this: - `name`: [string=''] Used to identify this particular output definition. - `options`: [dict] Options for the `pdf` output. * Valid keys: + - `custom_reports`: [list(dict)] A list of customized reports for the manufacturer. + * Valid keys: + - `content`: [string=''] Content for the report. Use ${basename} for the project name without extension. + Use ${filename(LAYER)} for the file corresponding to LAYER. + - `output`: [string='Custom_report.txt'] File name for the custom report. - `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. - `drill_marks`: [string='full'] what to use to indicate the drill places, can be none, small or full (for real scale). @@ -1160,6 +1185,11 @@ Next time you need this list just use an alias, like this: - `options`: [dict] Options for the `ps` output. * Valid keys: - `a4_output`: [boolean=true] force A4 paper size. + - `custom_reports`: [list(dict)] A list of customized reports for the manufacturer. + * Valid keys: + - `content`: [string=''] Content for the report. Use ${basename} for the project name without extension. + Use ${filename(LAYER)} for the file corresponding to LAYER. + - `output`: [string='Custom_report.txt'] File name for the custom report. - `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. - `drill_marks`: [string='full'] what to use to indicate the drill places, can be none, small or full (for real scale). @@ -1243,6 +1273,11 @@ Next time you need this list just use an alias, like this: - `name`: [string=''] Used to identify this particular output definition. - `options`: [dict] Options for the `svg` output. * Valid keys: + - `custom_reports`: [list(dict)] A list of customized reports for the manufacturer. + * Valid keys: + - `content`: [string=''] Content for the report. Use ${basename} for the project name without extension. + Use ${filename(LAYER)} for the file corresponding to LAYER. + - `output`: [string='Custom_report.txt'] File name for the custom report. - `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. - `drill_marks`: [string='full'] what to use to indicate the drill places, can be none, small or full (for real scale). diff --git a/docs/samples/generic_plot.kibot.yaml b/docs/samples/generic_plot.kibot.yaml index 409fda2b..e09e1fcf 100644 --- a/docs/samples/generic_plot.kibot.yaml +++ b/docs/samples/generic_plot.kibot.yaml @@ -168,6 +168,13 @@ outputs: type: 'dxf' dir: 'Example/dxf_dir' options: + # [list(dict)] A list of customized reports for the manufacturer + custom_reports: + # [string=''] Content for the report. Use ${basename} for the project name without extension. + # Use ${filename(LAYER)} for the file corresponding to LAYER + - content: '' + # [string='Custom_report.txt'] File name for the custom report + output: 'Custom_report.txt' # [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 dnf_filter: '' @@ -291,6 +298,13 @@ outputs: # [boolean=true] Creates a file with information about all the generated gerbers. # You can use it in gerbview to load all gerbers at once create_gerber_job_file: true + # [list(dict)] A list of customized reports for the manufacturer + custom_reports: + # [string=''] Content for the report. Use ${basename} for the project name without extension. + # Use ${filename(LAYER)} for the file corresponding to LAYER + - content: '' + # [string='Custom_report.txt'] File name for the custom report + output: 'Custom_report.txt' # [boolean=false] Disable aperture macros (workaround for buggy CAM software) (KiCad 6) disable_aperture_macros: false # [string|list(string)=''] Name of the filter to mark components as not fitted. @@ -347,6 +361,13 @@ outputs: type: 'hpgl' dir: 'Example/hpgl_dir' options: + # [list(dict)] A list of customized reports for the manufacturer + custom_reports: + # [string=''] Content for the report. Use ${basename} for the project name without extension. + # Use ${filename(LAYER)} for the file corresponding to LAYER + - content: '' + # [string='Custom_report.txt'] File name for the custom report + output: 'Custom_report.txt' # [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 dnf_filter: '' @@ -663,6 +684,13 @@ outputs: type: 'pdf' dir: 'Example/pdf_dir' options: + # [list(dict)] A list of customized reports for the manufacturer + custom_reports: + # [string=''] Content for the report. Use ${basename} for the project name without extension. + # Use ${filename(LAYER)} for the file corresponding to LAYER + - content: '' + # [string='Custom_report.txt'] File name for the custom report + output: 'Custom_report.txt' # [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 dnf_filter: '' @@ -789,6 +817,13 @@ outputs: options: # [boolean=true] force A4 paper size a4_output: true + # [list(dict)] A list of customized reports for the manufacturer + custom_reports: + # [string=''] Content for the report. Use ${basename} for the project name without extension. + # Use ${filename(LAYER)} for the file corresponding to LAYER + - content: '' + # [string='Custom_report.txt'] File name for the custom report + output: 'Custom_report.txt' # [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 dnf_filter: '' @@ -891,6 +926,13 @@ outputs: type: 'svg' dir: 'Example/svg_dir' options: + # [list(dict)] A list of customized reports for the manufacturer + custom_reports: + # [string=''] Content for the report. Use ${basename} for the project name without extension. + # Use ${filename(LAYER)} for the file corresponding to LAYER + - content: '' + # [string='Custom_report.txt'] File name for the custom report + output: 'Custom_report.txt' # [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 dnf_filter: '' diff --git a/kibot/out_any_layer.py b/kibot/out_any_layer.py index 7b764ec1..5397790b 100644 --- a/kibot/out_any_layer.py +++ b/kibot/out_any_layer.py @@ -6,9 +6,11 @@ # Project: KiBot (formerly KiPlot) # Adapted from: https://github.com/johnbeard/kiplot import os +import re from pcbnew import GERBER_JOBFILE_WRITER, PLOT_CONTROLLER, IsCopperLayer, F_Cu, B_Cu, Edge_Cuts -from .out_base import (BaseOutput) -from .error import (PlotError, KiPlotConfigurationError) +from .optionable import Optionable +from .out_base import BaseOutput +from .error import PlotError, KiPlotConfigurationError from .layer import Layer from .gs import GS from .misc import KICAD_VERSION_5_99, W_NOLAYER @@ -19,6 +21,17 @@ from . import log logger = log.get_logger(__name__) +class CustomReport(Optionable): + def __init__(self): + super().__init__() + with document: + self.output = 'Custom_report.txt' + """ File name for the custom report """ + self.content = '' + """ Content for the report. Use ${basename} for the project name without extension. + Use ${filename(LAYER)} for the file corresponding to LAYER """ + + class AnyLayerOptions(VariantOptions): """ Base class for: DXF, Gerber, HPGL, PDF, PS and SVG """ def __init__(self): @@ -47,8 +60,15 @@ class AnyLayerOptions(VariantOptions): Example '.g%n' """ self.edge_cut_extension = '' """ Used to configure the edge cuts layer extension for Protel mode """ + self.custom_reports = CustomReport + """ [list(dict)] A list of customized reports for the manufacturer """ super().__init__() + def config(self): + super().config() + if isinstance(self.custom_reports, type): + self.custom_reports = [] + def _configure_plot_ctrl(self, po, output_dir): logger.debug("Configuring plot controller for output") po.SetOutputDirectory(output_dir) @@ -93,6 +113,7 @@ class AnyLayerOptions(VariantOptions): # Apply the variants and filters exclude = self.filter_components(board) # Plot every layer in the output + generated = {} layers = Layer.solve(layers) for la in layers: suffix = la.suffix @@ -134,9 +155,29 @@ class AnyLayerOptions(VariantOptions): os.rename(k_filename, filename) if create_job: jobfile_writer.AddGbrFile(id, os.path.basename(filename)) + generated[la.layer] = os.path.basename(filename) # Create the job file if create_job: jobfile_writer.CreateJobFile(self.expand_filename(output_dir, po.gerber_job_file, 'job', 'gbrjob')) + # Custom reports + regex_fname = re.compile(r'\$\{filename\(.*\)\}') + for report in self.custom_reports: + filename = report.output + content = report.content + # Replace special white spaces + content = content.replace('\\r', chr(13)) + content = content.replace('\\n', chr(10)) + content = content.replace('\\t', chr(9)) + # Replace file names, compatible with gerber_zipper_action + content = content.replace('${basename}', GS.pcb_basename) + for name, file in generated.items(): + content = content.replace('${filename('+name+')}', file) + # Replace unused layers + content = regex_fname.sub('', content) + # Create the report + logger.debug('Creating custom report `'+filename+'`') + with open(os.path.join(output_dir, filename), 'wt') as f: + f.write(content) # Restore the eliminated layers if exclude: self.unfilter_components(board)