Files
Fabrication-Toolkit/plugins/process.py
T
2023-03-14 11:48:19 +02:00

302 lines
12 KiB
Python

# For better annotation.
from __future__ import annotations
# System base libraries
import os
import csv
import math
import shutil
from collections import defaultdict
import re
# Interaction with KiCad.
import pcbnew
# Application definitions.
from .config import *
class ProcessManager:
def __init__(self):
self.board = pcbnew.GetBoard()
self.bom = []
self.components = []
self.__rotation_db = self.__read_rotation_db()
@staticmethod
def __read_rotation_db(filename: str = os.path.join(os.path.dirname(__file__), 'rotations.cf')) -> dict[str, float]:
'''Read the rotations.cf config file so we know what rotations
to apply later.
'''
db = {}
with open(filename, 'r') as fh:
for line in fh:
line = line.rstrip()
line = re.sub('#.*$', '', line) # remove anything after a comment
line = re.sub('\s*$', '', line) # remove all trailing space
if (line == ""):
continue
match = re.match('^([^\s]+)\s+(\d+)$', line)
if match:
db.update({ match.group(1): int(match.group(2)) })
return db
def _get_rotation_from_db(self, footprint: str) -> float:
'''Get the rotation to be added from the database file.'''
# Look for regular expression math of the footprint name and not its root library.
fpshort = footprint.split(':')[-1]
for expression, delta in self.db.items():
fp = fpshort
if (re.search(':', expression)):
fp = footprint
if(re.search(expression, fp)):
return delta
return 0.0
def generate_gerber(self, temp_dir):
'''Generate the Gerber files.'''
settings = self.board.GetDesignSettings()
settings.m_SolderMaskMargin = 0
settings.m_SolderMaskMinWidth = 0
plot_controller = pcbnew.PLOT_CONTROLLER(self.board)
plot_options = plot_controller.GetPlotOptions()
plot_options.SetOutputDirectory(temp_dir)
plot_options.SetPlotFrameRef(False)
plot_options.SetSketchPadLineWidth(pcbnew.FromMM(0.1))
plot_options.SetAutoScale(False)
plot_options.SetScale(1)
plot_options.SetMirror(False)
plot_options.SetUseGerberAttributes(True)
plot_options.SetUseGerberProtelExtensions(False)
plot_options.SetUseAuxOrigin(True)
plot_options.SetSubtractMaskFromSilk(True)
plot_options.SetDrillMarksType(0) # NO_DRILL_SHAPE
if hasattr(plot_options, "SetExcludeEdgeLayer"):
plot_options.SetExcludeEdgeLayer(True)
for layer_info in plotPlan:
if self.board.IsLayerEnabled(layer_info[1]):
plot_controller.SetLayer(layer_info[1])
plot_controller.OpenPlotfile(
layer_info[0],
pcbnew.PLOT_FORMAT_GERBER,
layer_info[2])
plot_controller.PlotLayer()
plot_controller.ClosePlot()
def generate_drills(self, temp_dir):
'''Generate the drill file.'''
drill_writer = pcbnew.EXCELLON_WRITER(self.board)
drill_writer.SetOptions(
False,
True,
self.board.GetDesignSettings().GetAuxOrigin(),
False)
drill_writer.SetFormat(False)
drill_writer.CreateDrillandMapFilesSet(temp_dir, True, False)
def generate_netlist(self, temp_dir):
'''Generate the connection netlist.'''
netlist_writer = pcbnew.IPC356D_WRITER(self.board)
netlist_writer.Write(os.path.join(temp_dir, netlistFileName))
def generate_positions(self, temp_dir):
'''Generate the position files.'''
if hasattr(self.board, 'GetModules'):
footprints = list(self.board.GetModules())
else:
footprints = list(self.board.GetFootprints())
# sort footprint after designator
footprints.sort(key=lambda x: x.GetReference())
# unique designator dictionary
footprint_designators = defaultdict(int)
for i, footprint in enumerate(footprints):
# count unique designators
footprint_designators[footprint.GetReference()] += 1
bom_designators = footprint_designators.copy()
if len(footprint_designators.items()) > 0:
with open((os.path.join(temp_dir, designatorsFileName)), 'w', encoding='utf-8') as f:
for key, value in footprint_designators.items():
f.write('%s:%s\n' % (key, value))
for i, footprint in enumerate(footprints):
try:
footprint_name = str(footprint.GetFPID().GetFootprintName())
except AttributeError:
footprint_name = str(footprint.GetFPID().GetLibItemName())
layer = {
pcbnew.F_Cu: 'top',
pcbnew.B_Cu: 'bottom',
}.get(footprint.GetLayer())
# mount_type = {
# 0: 'smt',
# 1: 'tht',
# 2: 'smt'
# }.get(footprint.GetAttributes())
if not footprint.GetAttributes() & pcbnew.FP_EXCLUDE_FROM_POS_FILES:
# append unique ID if duplicate footprint designator
unique_id = ""
if footprint_designators[footprint.GetReference()] > 1:
unique_id = str(footprint_designators[footprint.GetReference()])
footprint_designators[footprint.GetReference()] -= 1
designator = "{}{}{}".format(footprint.GetReference(), "" if unique_id == "" else "_", unique_id)
mid_x = (footprint.GetPosition()[0] - self.board.GetDesignSettings().GetAuxOrigin()[0]) / 1000000.0
mid_y = (footprint.GetPosition()[1] - self.board.GetDesignSettings().GetAuxOrigin()[1]) * -1.0 / 1000000.0
rotation = footprint.GetOrientation().AsDegrees() if hasattr(footprint.GetOrientation(), 'AsDegrees') else footprint.GetOrientation() / 10.0
# Get the rotation offset to be added to the actual rotation prioritizing the explicated by the
# designer at the standards symbol fields. If not specified use the internal database.
rotation_offset = self._get_rotation_offset_from_footprint(footprint) #or self._get_rotation_from_db(footprint)
rotation = (rotation + rotation_offset) % 360.0
# position offset needs to take rotation into account
pos_offset = self._get_position_offset_from_footprint(footprint)
rsin = math.sin(rotation / 180 * math.pi)
rcos = math.cos(rotation / 180 * math.pi)
pos_offset = ( pos_offset[0] * rcos - pos_offset[1] * rsin, pos_offset[0] * rsin + pos_offset[1] * rcos )
mid_x, mid_y = tuple(map(sum,zip((mid_x, mid_y), pos_offset)))
self.components.append({
'Designator': designator,
'Mid X': mid_x,
'Mid Y': mid_y,
'Rotation': rotation,
'Layer': layer,
})
if not footprint.GetAttributes() & pcbnew.FP_EXCLUDE_FROM_BOM:
# append unique ID if we are dealing with duplicate bom designator
unique_id = ""
if bom_designators[footprint.GetReference()] > 1:
unique_id = str(bom_designators[footprint.GetReference()])
bom_designators[footprint.GetReference()] -= 1
# merge similar parts into single entry
insert = True
for component in self.bom:
if component['Footprint'] == self._normalize_footprint_name(footprint_name) and component['Value'].upper() == footprint.GetValue().upper():
component['Designator'] += ", " + "{}{}{}".format(footprint.GetReference(), "" if unique_id == "" else "_", unique_id)
component['Quantity'] += 1
insert = False
# add component to BOM
if insert:
self.bom.append({
'Designator': "{}{}{}".format(footprint.GetReference(), "" if unique_id == "" else "_", unique_id),
'Footprint': self._normalize_footprint_name(footprint_name),
'Quantity': 1,
'Value': footprint.GetValue(),
# 'Mount': mount_type,
'LCSC Part #': self._get_mpn_from_footprint(footprint),
})
if len(self.components) > 0:
with open((os.path.join(temp_dir, placementFileName)), 'w', newline='', encoding='utf-8') as outfile:
csv_writer = csv.writer(outfile)
# writing headers of CSV file
csv_writer.writerow(self.components[0].keys())
for component in self.components:
# writing data of CSV file
if ('**' not in component['Designator']):
csv_writer.writerow(component.values())
def generate_bom(self, temp_dir):
if len(self.bom) > 0:
with open((os.path.join(temp_dir, bomFileName)), 'w', newline='', encoding='utf-8') as outfile:
csv_writer = csv.writer(outfile)
# writing headers of CSV file
csv_writer.writerow(self.bom[0].keys())
# Output all of the component information
for component in self.bom:
# writing data of CSV file
if ('**' not in component['Designator']):
csv_writer.writerow(component.values())
def generate_archive(self, temp_dir, temp_file):
'''Generate the files.'''
temp_file = shutil.make_archive(temp_file, 'zip', temp_dir)
temp_file = shutil.move(temp_file, temp_dir)
# remove non essential files
for item in os.listdir(temp_dir):
if not item.endswith(".zip") and not item.endswith(".csv") and not item.endswith(".ipc"):
os.remove(os.path.join(temp_dir, item))
return temp_file
def _get_mpn_from_footprint(self, footprint: str):
''''Get the MPN/LCSC stock code from standard symbol fields.'''
keys = ['LCSC Part #', 'JLCPCB Part #']
fallback_keys = ['LCSC Part', 'JLC Part', 'LCSC', 'JLC', 'MPN', 'Mpn', 'mpn']
for key in keys + fallback_keys:
if footprint.HasProperty(key):
return footprint.GetProperty(key)
def _get_rotation_offset_from_footprint(self, footprint: str) -> float:
'''Get the rotation from standard symbol fields.'''
keys = ['JLCPCB Rotation Offset']
fallback_keys = ['JlcRotOffset', 'JLCRotOffset']
offset = None
for key in keys + fallback_keys:
if footprint.HasProperty(key):
offset = footprint.GetProperty(key)
break
if offset is None or offset == "":
return 0
else:
try:
return float(offset)
except ValueError:
raise RuntimeError("Rotation offset of {} is not a valid number".format(footprint.GetReference()))
def _get_position_offset_from_footprint(self, footprint):
keys = ['JLCPCB Position Offset']
fallback_keys = ['JlcPosOffset', 'JLCPosOffset']
offset = None
for key in keys + fallback_keys:
if footprint.HasProperty(key):
offset = footprint.GetProperty(key)
break
if offset is None or offset == "":
return (0, 0)
else:
try:
return ( float(offset.split(",")[0]), float(offset.split(",")[1]) )
except ValueError:
raise RuntimeError("Position offset of {} is not a valid pair of numbers".format(footprint.GetReference()))
def _normalize_footprint_name(self, footprint):
# replace footprint names of resistors, capacitors, inductors, diodes, LEDs, fuses etc, with the footprint size only
pattern = re.compile(r'^(\w*_SMD:)?\w{1,4}_(\d+)_\d+Metric.*$')
return pattern.sub(r'\2', footprint)