KiBot/kibot/pre_annotate_pcb.py

238 lines
9.4 KiB
Python

# -*- coding: utf-8 -*-
# Copyright (c) 2022 Salvador E. Tropea
# Copyright (c) 2022 Instituto Nacional de Tecnología Industrial
# License: GPL-3.0
# Project: KiBot (formerly KiPlot)
from .error import PlotError
from .gs import GS
from .kiplot import load_sch, load_board
from .misc import W_NOANNO
from .kicad.v5_sch import SchematicComponent
from .optionable import Optionable
from .macros import macros, document, pre_class # noqa: F401
from .log import get_logger
logger = get_logger(__name__)
class Annotate_PCBOptions(Optionable):
""" Reference sorting options """
def __init__(self):
super().__init__()
with document:
self.top_main_axis = 'y'
""" [x,y] Use this axis as main sorting criteria for the top layer """
self.top_main_ascending = True
""" Sort the main axis in ascending order for the top layer.
For X this is left to right and for Y top to bottom """
self.top_secondary_ascending = True
""" Sort the secondary axis in ascending order for the top layer.
For X this is left to right and for Y top to bottom """
self.top_start = 1
""" First number for references at the top layer """
self.bottom_main_axis = 'y'
""" [x,y] Use this axis as main sorting criteria for the bottom layer """
self.bottom_main_ascending = True
""" Sort the main axis in ascending order for the bottom layer.
For X this is left to right and for Y top to bottom """
self.bottom_secondary_ascending = True
""" Sort the secondary axis in ascending order for the bottom layer.
For X this is left to right and for Y top to bottom """
self.bottom_start = 101
""" First number for references at the bottom layer.
Use -1 to continue from the last top reference """
self.use_position_of = 'footprint'
""" [footprint,reference] Which coordinate is used """
self.grid = 1.0
""" Grid size in millimeters """
def granular(val, gran):
if val > 0:
return round(round(val/gran+1e-9)*gran, 8)
return round(round(val/gran-1e-9)*gran, 8)
class ModInfo(object):
def __init__(self, m, coord_type, grid):
self.footprint = m
# Get the reference and separate it in prefix (i.e. R) and suffix (i.e. 10)
ref = m.GetReference()
res = SchematicComponent.ref_re.match(ref)
if not res:
raise PlotError('Malformed component reference `{}`'.format(ref))
self.ref_prefix, self.ref_suffix = res.groups()
self.new_ref_suffix = -1
# Get the relevant coordinate
if coord_type == 'footprint':
pos = GS.get_center(m)
else: # Reference
pos = m.Reference().GetPosition()
self.x = pos.x
self.y = pos.y
# Scale and round the coordinates
scale = GS.unit_name_to_scale_factor('millimeters')
self.x = granular(self.x*scale, grid)
self.y = granular(self.y*scale, grid)
# Side
self.is_bottom = m.IsFlipped()
if self.is_bottom:
# Mirror the X axis
self.x = -self.x
def sort_key(ops, obj, top=True):
main_axis = ops.top_main_axis if top else ops.bottom_main_axis
main_ascending = ops.top_main_ascending if top else ops.bottom_main_ascending
secondary_ascending = ops.top_secondary_ascending if top else ops.bottom_secondary_ascending
if main_axis == 'y':
if main_ascending:
if secondary_ascending:
# Top to bottom, left to right (TBLR)
return [obj.ref_prefix, obj.y, obj.x]
else:
# Top to bottom, right to left (TBRL)
return [obj.ref_prefix, obj.y, -obj.x]
else: # not main_ascending
if secondary_ascending:
# Bottom to top, left to right (BTLR)
return [obj.ref_prefix, -obj.y, obj.x]
else:
# Bottom to top, right to left (BTRL)
return [obj.ref_prefix, -obj.y, -obj.x]
else: # main_axis == 'x':
if main_ascending:
if secondary_ascending:
# Left to right, top to bottom (LRTB)
return [obj.ref_prefix, obj.x, obj.y]
else:
# Left to right, bottom to top (LRBT)
return [obj.ref_prefix, obj.x, -obj.y]
else: # not main_ascending
if secondary_ascending:
# Right to left, top to bottom (RLTB)
return [obj.ref_prefix, -obj.x, obj.y]
else:
# Right to left, bottom to top (RLBT)
return [obj.ref_prefix, -obj.x, -obj.y]
@pre_class
class Annotate_PCB(BasePreFlight): # noqa: F821
""" [dict] Annotates the PCB according to physical coordinates.
This preflight modifies the PCB and schematic, use it only in revision control environments.
Used to assign references according to footprint coordinates.
The project must be fully annotated first """
def __init__(self, name, value):
o = Annotate_PCBOptions()
o.set_tree(value)
o.config(self)
super().__init__(name, o)
self._sch_related = True
self._pcb_related = True
def get_example():
""" Returns a YAML value for the example config """
return "\n - top_main_axis: y"\
"\n - top_main_ascending: true"\
"\n - top_secondary_ascending: true"\
"\n - top_start: 1"\
"\n - bottom_main_axis: y"\
"\n - bottom_main_ascending: true"\
"\n - bottom_secondary_ascending: true"\
"\n - bottom_start: 101"\
"\n - use_position_of: 'footprint'"\
"\n - grid: 1.0"
def annotate_ki6(self, changes):
""" Apply changes to the KiCad 6 schematic """
for ins in GS.sch.symbol_instances:
new_ref = changes.get(ins.reference, None)
if new_ref is not None:
c = ins.component
c.set_ref(new_ref)
def annotate_ki5(self, changes):
""" Apply changes to the KiCad 5 schematic """
comps = GS.sch.get_components()
for c in comps:
new_ref_suffix = changes.get(c.f_ref, None)
if new_ref_suffix is not None:
# Force a new number
c.ref = c.f_ref = c.ref_prefix+str(new_ref_suffix)
c.ref_suffix = str(new_ref_suffix)
# Fix the reference field
field = next(filter(lambda x: x.number == 0, c.fields), None)
if field:
field.value = c.ref
# Fix the ARs
if c.ar:
for o in c.ar:
new_ref_suffix = changes.get(o.ref, None)
if new_ref_suffix is not None:
o.ref = c.ref_prefix+str(new_ref_suffix)
def run(self):
o = self._value
#
# PCB part
#
load_board()
logger.debug('- Collecting components')
modules = []
for m in GS.get_modules():
ref = m.GetReference()
if ref[-1] == '?':
center = GS.get_center(m)
scale = GS.unit_name_to_scale_factor('millimeters')
logger.warning(W_NOANNO+'Missing annotation in component at {},{} mm ({})'.
format(center.x*scale, center.y*scale, ref))
else:
modules.append(ModInfo(m, o.use_position_of, o.grid))
modules = sorted(modules, key=lambda x: sort_key(o, x))
if GS.debug_level > 2:
logger.debug('- Components:')
for m in modules:
logger.debug(' '+str(m.__dict__))
prefixes = {x.ref_prefix for x in modules}
logger.debug('- Sorting components')
for p in sorted(prefixes):
n = o.top_start
for m in filter(lambda x: x.ref_prefix == p and not x.is_bottom, modules):
m.new_ref_suffix = n
n = n+1
logger.debug(' - {}{} -> {}{} ({},{})'.format(m.ref_prefix, m.ref_suffix, m.ref_prefix, m.new_ref_suffix, m.x,
m.y))
if o.bottom_start != -1:
n = o.bottom_start
for m in filter(lambda x: x.ref_prefix == p and x.is_bottom, modules):
m.new_ref_suffix = n
n = n+1
logger.debug(' - {}{} -> {}{} ({},{})'.format(m.ref_prefix, m.ref_suffix, m.ref_prefix, m.new_ref_suffix, m.x,
m.y))
logger.debug('- Modifying components')
changes = {}
for m in modules:
old_ref = m.ref_prefix+str(m.ref_suffix)
new_ref = m.ref_prefix+str(m.new_ref_suffix)
if GS.ki6:
changes[old_ref] = new_ref
else:
changes[old_ref] = m.new_ref_suffix
m.footprint.SetReference(new_ref)
logger.debug('- Saving PCB')
GS.make_bkp(GS.pcb_file)
GS.board.Save(GS.pcb_file)
#
# SCH part
#
load_sch()
if not GS.sch:
return
logger.debug('- Transferring changes to the schematic')
if GS.ki5:
self.annotate_ki5(changes)
else:
self.annotate_ki6(changes)
GS.sch.save()