import os import operator from datetime import datetime from pcbnew import (IU_PER_MM, IU_PER_MILS) from .error import KiPlotConfigurationError from kiplot.macros import macros, document, output_class # noqa: F401 @output_class class Position(BaseOutput): # noqa: F821 """ Pick & place Generates the file with position information for the PCB components, used by the pick and place machine. This output is what you get from the 'File/Fabrication output/Footprint poistion (.pos) file' menu in pcbnew. """ def __init__(self, name, type, description): super(Position, self).__init__(name, type, description) # Options with document: self._format = 'ASCII' """ can be ASCII or CSV """ self.separate_files_for_front_and_back = True """ generate two separated files, one for the top and another for the bottom """ self.only_smd = True """ only include the surface mount components """ self._units = 'millimeters' """ can be millimeters or inches """ # pragma: no cover @property def format(self): return self._format @format.setter def format(self, val): if val not in ['ASCII', 'CSV']: raise KiPlotConfigurationError("`format` must be either `ASCII` or `CSV`") self._format = val @property def units(self): return self._units @units.setter def units(self, val): if val not in ['millimeters', 'inches']: raise KiPlotConfigurationError("`units` must be either `millimeters` or `inches`") self._units = val def _do_position_plot_ascii(self, board, output_dir, columns, modulesStr, maxSizes): name = os.path.splitext(os.path.basename(board.GetFileName()))[0] topf = None botf = None bothf = None if self.separate_files_for_front_and_back: topf = open(os.path.join(output_dir, "{}-top.pos".format(name)), 'w') botf = open(os.path.join(output_dir, "{}-bottom.pos".format(name)), 'w') else: bothf = open(os.path.join(output_dir, "{}-both.pos").format(name), 'w') files = [f for f in [topf, botf, bothf] if f is not None] for f in files: f.write('### Module positions - created on {} ###\n'.format(datetime.now().strftime("%a %d %b %Y %X %Z"))) f.write('### Printed by KiPlot\n') unit = {'millimeters': 'mm', 'inches': 'in'}[self.units] f.write('## Unit = {}, Angle = deg.\n'.format(unit)) if topf is not None: topf.write('## Side : top\n') if botf is not None: botf.write('## Side : bottom\n') if bothf is not None: bothf.write('## Side : both\n') for f in files: f.write('# ') for idx, col in enumerate(columns): if idx > 0: f.write(" ") f.write("{0: <{width}}".format(col, width=maxSizes[idx])) f.write('\n') # Account for the "# " at the start of the comment column maxSizes[0] = maxSizes[0] + 2 for m in modulesStr: fle = bothf if fle is None: if m[-1] == "top": fle = topf else: fle = botf for idx, col in enumerate(m): if idx > 0: fle.write(" ") fle.write("{0: <{width}}".format(col, width=maxSizes[idx])) fle.write("\n") for f in files: f.write("## End\n") if topf is not None: topf.close() if botf is not None: botf.close() if bothf is not None: bothf.close() def _do_position_plot_csv(self, board, output_dir, columns, modulesStr): name = os.path.splitext(os.path.basename(board.GetFileName()))[0] topf = None botf = None bothf = None if self.separate_files_for_front_and_back: topf = open(os.path.join(output_dir, "{}-top-pos.csv".format(name)), 'w') botf = open(os.path.join(output_dir, "{}-bottom-pos.csv".format(name)), 'w') else: bothf = open(os.path.join(output_dir, "{}-both-pos.csv").format(name), 'w') files = [f for f in [topf, botf, bothf] if f is not None] for f in files: f.write(",".join(columns)) f.write("\n") for m in modulesStr: fle = bothf if fle is None: if m[-1] == "top": fle = topf else: fle = botf fle.write(",".join('"{}"'.format(e) for e in m)) fle.write("\n") if topf is not None: topf.close() if botf is not None: botf.close() if bothf is not None: bothf.close() def run(self, output_dir, board): columns = ["Ref", "Val", "Package", "PosX", "PosY", "Rot", "Side"] colcount = len(columns) # Note: the parser already checked the units are milimeters or inches conv = 1.0 if self.units == 'millimeters': conv = 1.0 / IU_PER_MM else: # self.units == 'inches': conv = 0.001 / IU_PER_MILS # Format all strings modules = [] for m in sorted(board.GetModules(), key=operator.methodcaller('GetReference')): if (self.only_smd and m.GetAttributes() == 1) or not self.only_smd: center = m.GetCenter() # See PLACE_FILE_EXPORTER::GenPositionData() in # export_footprints_placefile.cpp for C++ version of this. modules.append([ "{}".format(m.GetReference()), "{}".format(m.GetValue()), "{}".format(m.GetFPID().GetLibItemName()), "{:.4f}".format(center.x * conv), "{:.4f}".format(-center.y * conv), "{:.4f}".format(m.GetOrientationDegrees()), "{}".format("bottom" if m.IsFlipped() else "top") ]) # Find max width for all columns maxlengths = [0] * colcount for row in range(len(modules)): for col in range(colcount): maxlengths[col] = max(maxlengths[col], len(modules[row][col])) # Note: the parser already checked the format is ASCII or CSV if self.format == 'ASCII': self._do_position_plot_ascii(board, output_dir, columns, modules, maxlengths) else: # if self.format == 'CSV': self._do_position_plot_csv(board, output_dir, columns, modules)