# -*- 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) import os from qrcodegen import QrCode from tempfile import NamedTemporaryFile from .gs import GS from .optionable import BaseOptions, Optionable from .error import KiPlotConfigurationError from .kicad.sexpdata import Symbol, dumps, Sep, load, SExpData, sexp_iter from .kicad.v6_sch import DrawRectangleV6, PointXY, Stroke, Fill, SchematicFieldV6, FontEffects from .kiplot import load_board from .macros import macros, document, output_class # noqa: F401 from . import log QR_ECCS = {'low': QrCode.Ecc.LOW, 'medium': QrCode.Ecc.MEDIUM, 'quartile': QrCode.Ecc.QUARTILE, 'high': QrCode.Ecc.HIGH} logger = log.get_logger() TO_SEPARATE = set(['kicad_pcb', 'general', 'title_block', 'layers', 'setup', 'pcbplotparams', 'net_class', 'module', 'kicad_sch', 'lib_symbols', 'symbol', 'sheet', 'sheet_instances', 'symbol_instances']) def is_symbol(name, sexp): return isinstance(sexp, list) and len(sexp) >= 1 and isinstance(sexp[0], Symbol) and sexp[0].value() == name def make_separated(sexp): if not isinstance(sexp, list): return sexp if not isinstance(sexp[0], Symbol) or sexp[0].value() not in TO_SEPARATE: return sexp separated = [] for s in sexp: separated.append(make_separated(s)) if isinstance(s, list): separated.append(Sep()) return separated def compute_size(qr, is_sch=True, use_mm=True): if is_sch: qrc = qr._code_sch full_size = qr.size_sch else: qrc = qr._code_pcb full_size = qr.size_pcb size = qrc.get_size() if not is_sch and qr.pcb_negative: size += 2 if use_mm: full_size *= 1 if qr.size_units == 'millimeters' else 25.4 center = round(full_size/2, 2) size_rect = round(full_size/size, 2) else: full_size *= 39.37007874 if qr.size_units == 'millimeters' else 1000 center = round(full_size/2) size_rect = full_size/size return qrc, size, full_size, center, size_rect class QRCodeOptions(Optionable): """ A QR code """ def __init__(self, field=None): super().__init__() with document: self.name = 'QR' """ Name for the symbol/footprint """ self.text = '%p %r' """ Text to encode as QR """ self.correction_level = 'low' """ [low,medium,quartile,high] Error correction level """ self.size_sch = 15 """ Size of the QR symbol """ self.size_pcb = 15 """ Size of the QR footprint """ self.size_units = 'millimeters' """ [millimeters,inches] Units used for the size """ self.layer = 'silk' """ [silk,copper] Layer for the footprint """ self.pcb_negative = False """ Generate a negative image for the PCB """ self._unkown_is_error = True self._update_mode = False def config(self, parent): super().config(parent) self.correction_level = QR_ECCS[self.correction_level] self.layer = 'F.SilkS' if self.layer == 'silk' else 'F.Cu' class QR_LibOptions(BaseOptions): def __init__(self): with document: self.output = GS.def_global_output """ Filename for the output (%i=qr, %x=lib) """ self.lib = 'QR' """ Short name for the library """ self.reference = 'QR' """ The reference prefix """ self.use_sch_dir = True """ Generate the libs relative to the schematic/PCB dir """ self.qrs = QRCodeOptions """ [list(dict)] QR codes to include in the library """ super().__init__() self._expand_id = 'qr' self._expand_ext = 'lib' def config(self, parent): super().config(parent) if isinstance(self.qrs, type): raise KiPlotConfigurationError("You must specify at least one QR code") names = set() for qr in self.qrs: if qr.name in names: raise KiPlotConfigurationError("QR code name `{}` repeated".format(qr.name)) names.add(qr.name) def symbol_k5(self, f, qr): # Compute the size qrc, size, full_size, center, size_rect = compute_size(qr, use_mm=False) # Generate the symbol f.write("#\n# {}\n#\n".format(qr.name)) f.write("DEF {} {} 0 {} N N 1 F N\n".format(qr.name, '#'+self.reference, 0)) # Reference f.write('F0 "{}" {} {} 50 H I L BNN\n'.format('#'+self.reference, -center, center+60)) # Value f.write('F1 "{}" {} {} 50 H I L TNN\n'.format(qr.name, -center, -center)) # Footprint f.write('F2 "{}:{}" 0 150 50 H I C CNN\n'.format(self.lib, qr.name)) # Datasheet f.write('F3 "" 0 0 50 H I C CNN\n') # QR information f.write('F4 "{}" 0 0 50 H I C CNN "qr_version"\n'.format(qrc.get_version())) f.write('F5 "{}" 0 0 50 H I C CNN "qr_size"\n'.format(size)) ecc = qrc.get_error_correction_level() f.write('F6 "{},{}" 0 0 50 H I C CNN "qr_ecc"\n'.format(ecc.ordinal, ecc.formatbits)) f.write('F7 "{}" 0 0 50 H I C CNN "qr_mask"\n'.format(qrc.get_mask())) f.write('F8 "{}" 0 0 50 H I C CNN "qr_text"\n'.format(qr._text_sch.replace('"', '\"'))) f.write("DRAW\n") for y in range(size): for x in range(size): if qrc.get_module(x, y): x_pos = round(x*size_rect-center) y_pos = round(center-y*size_rect) f.write('S {} {} {} {} 0 0 1 F\n'.format(x_pos, y_pos, round(x_pos+size_rect), round(y_pos+size_rect))) f.write("ENDDRAW\n") f.write("ENDDEF\n") def fp_field(self, center, name, value, layer, id): if id == 0: pos_y = center+1.25 else: pos_y = -(center+1.25+1.7*(id-1)) fld = [Symbol('fp_text'), Symbol(name), value] fld.append([Symbol('at'), 0, pos_y]) fld.append([Symbol('layer'), Symbol(layer)]) if name == 'user': fld.append(Symbol('hide')) fld.append(Sep()) font = [Symbol('font')] font.append([Symbol('size'), 1, 1]) font.append([Symbol('thickness'), 0.15]) fld.append([Symbol('effects'), font]) fld.append(Sep()) return fld def qr_draw_fp(self, size, size_rect, center, qrc, negative, layer, do_sep=True): mod = [] for y in range(size): for x in range(size): if qrc.get_module(x-negative, y-negative) ^ negative: x_pos = round(x*size_rect-center, 2) y_pos = round(y*size_rect-center, 2) x_pos2 = round(x_pos+size_rect, 2) y_pos2 = round(y_pos+size_rect, 2) rect = [Symbol('fp_poly')] # fp_rect not in v5 pts = [Symbol('pts')] pts.append([Symbol('xy'), x_pos, y_pos]) pts.append([Symbol('xy'), x_pos, y_pos2]) pts.append([Symbol('xy'), x_pos2, y_pos2]) pts.append([Symbol('xy'), x_pos2, y_pos]) rect.append(pts) if layer: rect.append([Symbol('layer'), Symbol(layer)]) rect.append([Symbol('width'), 0]) mod.append(rect) if do_sep: mod.append(Sep()) return mod def qr_draw_sym(self, size, size_rect, center, qrc, do_sep=True): mod = [] for y in range(size): for x in range(size): if qrc.get_module(x, y): x_pos = round(x*size_rect-center, 2) y_pos = round(center-y*size_rect, 2) rect = DrawRectangleV6() rect.start = PointXY(x_pos, y_pos) rect.end = PointXY(round(x_pos+size_rect, 2), round(y_pos-size_rect, 2)) rect.stroke = Stroke() rect.stroke.width = 0.001 rect.fill = Fill() rect.fill.type = 'outline' mod.append(rect.write()) if do_sep: mod.append(Sep()) return mod def footprint(self, dir, qr): # Compute the size qrc, size, full_size, center, size_rect = compute_size(qr, is_sch=False) # Generate the footprint fname = os.path.join(dir, qr.name+'.kicad_mod') mod = [Symbol('module'), Symbol(qr.name)] mod.append([Symbol('layer'), Symbol(qr.layer)]) mod.append([Symbol('tedit'), 0]) mod.append(Sep()) mod.append([Symbol('attr'), Symbol('virtual')]) mod.append(Sep()) mod.append(self.fp_field(center, 'reference', self.reference+'***', qr.layer, 0)) mod.append(Sep()) mod.append(self.fp_field(center, 'value', qr.name, qr.layer, 1)) mod.append(Sep()) mod.append(self.fp_field(center, 'user', 'qr_version: '+str(qrc.get_version()), qr.layer, 2)) mod.append(Sep()) mod.append(self.fp_field(center, 'user', 'qr_size: '+str(size), qr.layer, 3)) mod.append(Sep()) ecc = qrc.get_error_correction_level() mod.append(self.fp_field(center, 'user', 'qr_ecc: {},{}'.format(ecc.ordinal, ecc.formatbits), qr.layer, 4)) mod.append(Sep()) mod.append(self.fp_field(center, 'user', 'qr_mask: '+str(qrc.get_mask()), qr.layer, 5)) mod.append(Sep()) mod.append(self.fp_field(center, 'user', qr._text_pcb, qr.layer, 6)) mod.append(Sep()) # The QR itself mod.extend(self.qr_draw_fp(size, size_rect, center, qrc, qr.pcb_negative, qr.layer)) with open(fname, 'wt') as f: f.write(dumps(mod)) f.write('\n') def symbol_lib_k5(self): self._expand_ext = 'lib' output = os.path.join(self._odir_sch, self.expand_filename_sch(self.output)) logger.debug('Creating KiCad 5 symbols library: '+output) with open(output, 'wt') as f: f.write("EESchema-LIBRARY Version 2.4\n") f.write("#encoding utf-8\n") for qr in self.qrs: logger.debug('Adding symbol: '+qr.name) self.symbol_k5(f, qr) f.write("#\n#End Library\n") def sym_field(self, center, name, value, id): if id == 0: pos_y = center+1.25 else: pos_y = -(center+1.25+1.7*(id-1)) f = SchematicFieldV6(name, str(value), id, 0, round(pos_y, 2)) if id > 1: f.effects = FontEffects() f.effects.hide = True return f.write()+[Sep()] def symbol_lib_k6(self): self._expand_ext = 'kicad_sym' output = os.path.join(self._odir_sch, self.expand_filename_sch(self.output)) logger.debug('Creating KiCad 6 symbols library: '+output) # Lib header lib = [Symbol('kicad_symbol_lib')] lib.append([Symbol('version'), 20211014]) lib.append([Symbol('generator'), Symbol('kibot')]) lib.append(Sep()) for qr in self.qrs: logger.debug('Adding symbol: '+qr.name) # Compute the size qrc, size, full_size, center, size_rect = compute_size(qr) # Symbol main attributes sym = [Symbol('symbol'), qr.name] sym.append([Symbol('pin_numbers'), Symbol('hide')]) sym.append([Symbol('pin_names'), Symbol('hide')]) sym.append([Symbol('in_bom'), Symbol('no')]) sym.append([Symbol('on_board'), Symbol('yes')]) sym.append(Sep()) # Properties (Fields) sym.append(self.sym_field(center, 'Reference', '#'+self.reference, 0)) sym.append(Sep()) sym.append(self.sym_field(center, 'Value', qr.name, 1)) sym.append(Sep()) sym.append(self.sym_field(center, 'Footprint', self.lib+':'+qr.name, 2)) sym.append(Sep()) sym.append(self.sym_field(center, 'Datasheet', '', 3)) sym.append(Sep()) sym.append(self.sym_field(center, 'qr_version', qrc.get_version(), 4)) sym.append(Sep()) sym.append(self.sym_field(center, 'qr_size', size, 5)) sym.append(Sep()) ecc = qrc.get_error_correction_level() sym.append(self.sym_field(center, 'qr_ecc', '{},{}'.format(ecc.ordinal, ecc.formatbits), 6)) sym.append(Sep()) sym.append(self.sym_field(center, 'qr_mask', qrc.get_mask(), 7)) sym.append(Sep()) sym.append(self.sym_field(center, 'qr_text', qr._text_sch, 8)) sym.append(Sep()) sym.extend(self.qr_draw_sym(size, size_rect, center, qrc)) lib.append(sym) lib.append(Sep()) with open(output, 'wt') as f: f.write(dumps(lib)) f.write('\n') def update_footprint(self, name, sexp, qr): logger.debug('- Updating QR footprint: '+name) # Compute the size qrc, size, full_size, center, size_rect = compute_size(qr, is_sch=False) # Remove old drawing sexp[:] = list(filter(lambda s: not is_symbol('fp_poly', s), sexp)) # Add the new drawings sexp.extend(self.qr_draw_fp(size, size_rect, center, qrc, qr.pcb_negative, qr.layer, do_sep=False)) # Update the fields for s in sexp: if (is_symbol('fp_text', s) and len(s) > 2 and isinstance(s[1], Symbol) and s[1].value() == 'user' and isinstance(s[2], str)): res = s[2].split(':') if len(res) > 1: logger.debug('- Updating field `{}`'.format(res[0])) if res[0] == 'qr_version': s[2] = 'qr_version: '+str(qrc.get_version()) elif res[0] == 'qr_size': s[2] = 'qr_size: '+str(size) elif res[0] == 'qr_ecc': ecc = qrc.get_error_correction_level() s[2] = 'qr_ecc: {},{}'.format(ecc.ordinal, ecc.formatbits) elif res[0] == 'qr_mask': s[2] = 'qr_mask: '+str(qrc.get_mask()) elif s[2][0] == ' ': logger.debug('- Updating text `{}`'.format(qr._text_pcb)) s[2] = ' '+qr._text_pcb def update_footprints(self, known_qrs): # Replace known QRs in the PCB updated = False pcb = self.load_sexp_file(GS.pcb_file) for iter in [sexp_iter(pcb, 'kicad_pcb/module'), sexp_iter(pcb, 'kicad_pcb/footprint')]: for s in iter: if len(s) < 2: continue if isinstance(s[1], Symbol): name = s[1].value().lower() else: name = s[1].lower() if name in known_qrs: updated = True self.update_footprint(name, s, known_qrs[name]) # Save the resulting PCB if updated: # Make it readable separated = make_separated(pcb[0]) # Save it to a temporal with NamedTemporaryFile(mode='wt', suffix='.kicad_pcb', delete=False) as f: logger.debug('- Saving updated PCB to: '+f.name) f.write(dumps(separated)) f.write('\n') tmp_pcb = f.name # Reload it GS.board = None logger.debug('- Loading the temporal PCB') load_board(tmp_pcb) # Create a back-up and save it in the original place logger.debug('- Replacing the old PCB') os.remove(tmp_pcb) bkp = GS.pcb_file+'-bak' if os.path.isfile(bkp): os.remove(bkp) os.rename(GS.pcb_file, bkp) prl = None if GS.ki6(): # KiCad 6 is destroying the PRL ... prl_name = GS.pcb_no_ext+'.kicad_prl' if os.path.isfile(prl_name): with open(prl_name, 'rt') as f: prl = f.read() GS.board.Save(GS.pcb_file) if prl: with open(prl_name, 'wt') as f: f.write(prl) def update_symbol(self, name, c_name, sexp, qr): logger.debug('- Updating QR symbol: '+name) # Compute the size qrc, size, full_size, center, size_rect = compute_size(qr) # Create the new drawings sub_unit_name = c_name+"_1_1" sub_unit_sexp = [Symbol('symbol'), sub_unit_name] sub_unit_sexp.extend(self.qr_draw_sym(size, size_rect, center, qrc, do_sep=False)) # Replace the old one for s in sexp_iter(sexp, 'symbol'): if len(s) >= 2 and isinstance(s[1], str) and s[1] == sub_unit_name: s[:] = list(sub_unit_sexp) # Update the fields for s in sexp: if is_symbol('property', s) and len(s) > 2 and isinstance(s[1], str) and isinstance(s[2], str): new_val = None field = s[1] if field == 'qr_version': new_val = str(qrc.get_version()) elif field == 'qr_size': new_val = str(size) elif field == 'qr_ecc': ecc = qrc.get_error_correction_level() new_val = '{},{}'.format(ecc.ordinal, ecc.formatbits) elif field == 'qr_mask': new_val = str(qrc.get_mask()) elif field == 'qr_text': new_val = qr._text_sch if new_val is not None: logger.debug('- Updating field `{}` {} -> {}'.format(field, s[2], new_val)) s[2] = new_val def update_symbols(self, fname, sexp, known_qrs): # Replace known QRs in the Schematic updated = False for s in sexp_iter(sexp, 'kicad_sch/lib_symbols/symbol'): if len(s) < 2 or not isinstance(s[1], str): continue name = s[1].lower() c_name = s[1].split(':')[1] if name in known_qrs: updated = True self.update_symbol(name, c_name, s, known_qrs[name]) # Save the resulting Schematic if updated: # Make it readable separated = make_separated(sexp[0]) # Create a back-up and save it in the original place logger.debug('- Replacing the old SCH') bkp = fname+'-bak' if os.path.isfile(bkp): os.remove(bkp) os.rename(fname, bkp) with open(fname, 'wt') as f: f.write(dumps(separated)) f.write('\n') def load_sexp_file(self, fname): with open(fname, 'rt') as fh: error = None try: ki_file = load(fh) except SExpData as e: error = str(e) if error: raise KiPlotConfigurationError(error) return ki_file def load_k6_sheets(self, fname, sheets={}): logger.debug('- Loading '+fname) sheet = self.load_sexp_file(fname) sheets[fname] = sheet if not is_symbol('kicad_sch', sheet[0]): raise KiPlotConfigurationError('No kicad_sch signature in '+fname) path = os.path.dirname(fname) for s in sexp_iter(sheet, 'kicad_sch/sheet'): sub_name = None for prop in sexp_iter(s, 'property'): if len(prop) > 2 and isinstance(prop[1], str) and isinstance(prop[2], str) and prop[1] == 'Sheet file': sub_name = prop[2] if sub_name is not None: sub_name = os.path.abspath(os.path.join(path, sub_name)) if sub_name not in sheets: self.load_k6_sheets(os.path.join(path, sub_name), sheets) return sheets def run(self, output): if self.use_sch_dir: self._odir_sch = GS.sch_dir self._odir_pcb = GS.pcb_dir else: self._odir_pcb = self._odir_sch = self._parent.output_dir # Create the QR codes for qr in self.qrs: qr._text_sch = self.expand_filename_both(qr.text, make_safe=False) qr._code_sch = QrCode.encode_text(qr._text_sch, qr.correction_level) qr._text_pcb = self.expand_filename_both(qr.text, is_sch=False, make_safe=False) qr._code_pcb = QrCode.encode_text(qr._text_pcb, qr.correction_level) # Create the symbols if GS.ki5(): self.symbol_lib_k5() else: self.symbol_lib_k6() # Create the footprints self._expand_ext = 'pretty' dir_pretty = os.path.join(self._odir_pcb, self.expand_filename_pcb(self.output)) logger.debug('Creating footprints library: '+dir_pretty) os.makedirs(dir_pretty, exist_ok=True) for qr in self.qrs: logger.debug('Adding footprint: '+qr.name) self.footprint(dir_pretty, qr) # Update the files if self._parent._update_mode: logger.debug('Updating the PCB and schematic') # Create a dict with the known QRs known_qrs = {} for qr in self.qrs: name = self.lib+':'+qr.name known_qrs[name.lower()] = qr # PCB self.update_footprints(known_qrs) # Schematic if GS.ki6(): # KiCad 5 reads the lib, but KiCad 6 is more like the PCB assert GS.sch_file is not None sheets = self.load_k6_sheets(GS.sch_file) for k, v in sheets.items(): self.update_symbols(k, v, known_qrs) @output_class class QR_Lib(BaseOutput): # noqa: F821 """ QR_Lib Generates a QR code symbol and footprint. This output creates a library containing a symbol and footprint for a QR code. To refresh the generated symbols and footprints use the `update_qr` preflight. The workflow is as follows: - Create the symbol and footprints using this output. - Use them in your schematic and PCB. - To keep them updated add the `update_qr` preflight """ def __init__(self): super().__init__() with document: self.options = QR_LibOptions """ [dict] Options for the `boardview` output """ self._both_related = True