Added support for KiCad 6 to the `update_qr` preflight.

Related to #93
This commit is contained in:
Salvador E. Tropea 2022-01-04 16:52:12 -03:00
parent 7992fd9888
commit be59ee397e
14 changed files with 9437 additions and 11962 deletions

View File

@ -56,7 +56,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Position files now can include virtual components. (#106)
- Support for variants on KiCost output. (#106)
- `--cli-order` option to generate outputs in arbitrary order. (#106)
- QR codes generation: symbols and footprints. (#93)
- QR codes generation and update: symbols and footprints. (#93)
### Changed
- Internal BoM: now components with different Tolerance, Voltage, Current

View File

@ -1678,6 +1678,11 @@ Next time you need this list just use an alias, like this:
* Type: `qr_lib`
* Description: 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
* Valid keys:
- `comment`: [string=''] A comment for documentation purposes.
- `dir`: [string='./'] Output directory for the generated files. If it starts with `+` the rest is concatenated to the default dir.

View File

@ -1133,6 +1133,11 @@ outputs:
# QR_Lib:
# 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
- name: 'qr_lib_example'
comment: 'Generates a QR code symbol and footprint.'
type: 'qr_lib'

View File

@ -41,6 +41,11 @@ See the source code for more information.
# Copyright (c) 2012 Takafumi Arakaki
# All rights reserved.
# Copyright (c) 2022 Salvador E. Tropea
# Copyright (c) 2022 Instituto Nacional de Tecnología Industrial
# - Adapted to KiCad
# - Added sexp_iter
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
@ -250,7 +255,7 @@ def loads(string, **kwds):
obj = parse(string, **kwds)
if len(obj) != 1:
raise SExpData("Not an s-expression file")
return obj[0]
return obj
def dump(obj, filelike, **kwds):
@ -707,3 +712,18 @@ def parse(string, **kwds):
"""
return Parser(string, **kwds).parse()
def sexp_iter(vect, path):
"""
Returns an iterator to filter all the elements described in the path.
"""
elems = path.split('/')
total = len(elems)
for i, e in enumerate(elems):
iter = filter(lambda x: isinstance(x, list) and isinstance(x[0], Symbol) and x[0].value() == e, vect)
if i == total-1:
return iter
vect = next(iter, None)
if vect is None:
return None

View File

@ -1604,7 +1604,7 @@ class SchematicV6(Schematic):
with open(fname, 'rt') as fh:
error = None
try:
sch = load(fh)
sch = load(fh)[0]
except SExpData as e:
error = str(e)
if error:

View File

@ -5,17 +5,13 @@
# Project: KiBot (formerly KiPlot)
import os
from qrcodegen import QrCode
from tempfile import NamedTemporaryFile
from .gs import GS
if GS.ki6(): # pragma: no cover (Ki6)
from pcbnew import IU_PER_MM, S_POLYGON, wxPoint, ADD_MODE_APPEND
ADD_APPEND = ADD_MODE_APPEND
else:
from pcbnew import IU_PER_MM, S_POLYGON, wxPoint, ADD_APPEND
from .optionable import BaseOptions, Optionable
from .out_base import VariantOptions
from .error import KiPlotConfigurationError
from .kicad.sexpdata import Symbol, dumps, Sep, load, SExpData
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
@ -24,6 +20,46 @@ QR_ECCS = {'low': QrCode.Ecc.LOW,
'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):
@ -85,11 +121,7 @@ class QR_LibOptions(BaseOptions):
def symbol_k5(self, f, qr):
# Compute the size
qrc = qr._code_sch
size = qrc.get_size()
full_size = qr.size_sch * (39.37007874 if qr.size_units == 'millimeters' else 1000)
center = round(full_size/2)
size_rect = full_size/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))
@ -136,7 +168,7 @@ class QR_LibOptions(BaseOptions):
fld.append(Sep())
return fld
def qr_draw_fp(self, size, size_rect, center, qrc, negative, layer):
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):
@ -156,38 +188,11 @@ class QR_LibOptions(BaseOptions):
rect.append([Symbol('layer'), Symbol(layer)])
rect.append([Symbol('width'), 0])
mod.append(rect)
mod.append(Sep())
if do_sep:
mod.append(Sep())
return mod
def qr_draw_fp_memory(self, m, size, size_rect, center, qrc, negative, layer):
""" Create the QR drawings for the board in memory """
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)
# Convert to Internal Units
x_pos *= IU_PER_MM
y_pos *= IU_PER_MM
x_pos2 *= IU_PER_MM
y_pos2 *= IU_PER_MM
# Create a PCB polygon
poly = VariantOptions.create_module_element(m)
poly.SetShape(S_POLYGON)
points = []
points.append(wxPoint(x_pos, y_pos))
points.append(wxPoint(x_pos, y_pos2))
points.append(wxPoint(x_pos2, y_pos2))
points.append(wxPoint(x_pos2, y_pos))
poly.SetPolyPoints(points)
poly.SetLayer(layer)
poly.SetWidth(0)
poly.thisown = 0
m.AddNative(poly, ADD_APPEND)
def qr_draw_sym(self, size, size_rect, center, qrc):
def qr_draw_sym(self, size, size_rect, center, qrc, do_sep=True):
mod = []
for y in range(size):
for x in range(size):
@ -202,18 +207,13 @@ class QR_LibOptions(BaseOptions):
rect.fill = Fill()
rect.fill.type = 'outline'
mod.append(rect.write())
mod.append(Sep())
if do_sep:
mod.append(Sep())
return mod
def footprint(self, dir, qr):
# Compute the size
qrc = qr._code_pcb
size = qrc.get_size()
if qr.pcb_negative:
size += 2
full_size = qr.size_pcb * (1 if qr.size_units == 'millimeters' else 25.4)
center = round(full_size/2, 2)
size_rect = round(full_size/size, 2)
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)]
@ -276,11 +276,7 @@ class QR_LibOptions(BaseOptions):
for qr in self.qrs:
logger.debug('Adding symbol: '+qr.name)
# Compute the size
qrc = qr._code_sch
size = qrc.get_size()
full_size = qr.size_sch * (1 if qr.size_units == 'millimeters' else 25.4)
center = round(full_size/2, 2)
size_rect = round(full_size/size, 2)
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')])
@ -315,52 +311,129 @@ class QR_LibOptions(BaseOptions):
f.write(dumps(lib))
f.write('\n')
def update_footprint(self, name, qr):
logger.debug('Updating QR footprint: '+name)
def update_footprint(self, name, sexp, qr):
logger.debug('- Updating QR footprint: '+name)
# Compute the size
# TODO: don't repeat
qrc = qr._code_pcb
size = qrc.get_size()
if qr.pcb_negative:
size += 2
full_size = qr.size_pcb * (1 if qr.size_units == 'millimeters' else 25.4)
center = round(full_size/2, 2)
size_rect = round(full_size/size, 2)
# Replace any instance
name = name.lower()
for m in GS.get_modules():
id = m.GetFPID()
m_name = ('{}:{}'.format(id.GetLibNickname(), id.GetLibItemName())).lower()
if name == m_name:
ref = m.GetReference()
logger.debug('- Updating '+ref)
# Remove all the drawings
for gi in m.GraphicalItems():
if gi.GetClass() == 'MGRAPHIC':
m.Remove(gi)
# Add the updated version
self.qr_draw_fp_memory(m, size, size_rect, center, qrc, qr.pcb_negative, GS.board.GetLayerID(qr.layer))
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))
def load_k6_sheet(self, fname):
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)
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:
sch = load(fh)
ki_file = load(fh)
except SExpData as e:
error = str(e)
if error:
raise KiPlotConfigurationError(error)
return sch
return ki_file
def load_k6_sheets(self, fname, sheets={}):
assert GS.sch_file is not None
sheet = self.load_k6_sheet(fname)
logger.debug('- Loading '+fname)
sheet = self.load_sexp_file(fname)
sheets[fname] = sheet
if not isinstance(sheet, list) or sheet[0].value() != 'kicad_sch':
if not is_symbol('kicad_sch', sheet[0]):
raise KiPlotConfigurationError('No kicad_sch signature in '+fname)
for e in sheet[1:]:
if isinstance(e, list) and isinstance(e[0], Symbol) and e[0].value == 'sheet':
logger.error(e)
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:
@ -389,28 +462,34 @@ class QR_LibOptions(BaseOptions):
self.footprint(dir_pretty, qr)
# Update the files
if self._parent._update_mode:
# PCB
assert GS.board is not None
logger.debug('Updating the PCB and schematic')
# Create a dict with the known QRs
known_qrs = {}
for qr in self.qrs:
self.update_footprint(self.lib+':'+qr.name, qr)
bkp = GS.pcb_file+'-bak'
if os.path.isfile(bkp):
os.remove(bkp)
os.rename(GS.pcb_file, bkp)
GS.board.Save(GS.pcb_file)
name = self.lib+':'+qr.name
known_qrs[name.lower()] = qr
# TODO: Update fields
# PCB
self.update_footprints(known_qrs)
# Schematic
if GS.ki6():
# KiCad 5 reads the lib, but KiCad 6 is more like the PCB
# sheets = self.load_k6_sheets(GS.sch_file)
pass
# TODO: KiCad 6 is crashing when we delete the graphics
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. """
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:

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"board": {
"active_layer": 0,
"active_layer_preset": "",
"active_layer_preset": "All Layers",
"auto_track_width": true,
"hidden_nets": [],
"high_contrast_mode": 0,
@ -62,7 +62,7 @@
35,
36
],
"visible_layers": "003ffff_80000001",
"visible_layers": "fffffff_ffffffff",
"zone_display_mode": 0
},
"meta": {

View File

@ -2,7 +2,7 @@
"board": {
"design_settings": {
"defaults": {
"board_outline_line_width": 0.09999999999999999,
"board_outline_line_width": 0.049999999999999996,
"copper_line_width": 0.19999999999999998,
"copper_text_italic": false,
"copper_text_size_h": 1.5,
@ -37,7 +37,7 @@
"height": 1.524,
"width": 1.524
},
"silk_line_width": 0.15,
"silk_line_width": 0.12,
"silk_text_italic": false,
"silk_text_size_h": 1.0,
"silk_text_size_v": 1.0,
@ -51,7 +51,6 @@
"diff_pair_dimensions": [],
"drc_exclusions": [],
"meta": {
"filename": "board_design_settings.json",
"version": 2
},
"rule_severities": {
@ -99,7 +98,7 @@
"allow_microvias": false,
"max_error": 0.005,
"min_clearance": 0.0,
"min_copper_edge_clearance": 0.0,
"min_copper_edge_clearance": 0.01,
"min_hole_clearance": 0.25,
"min_hole_to_hole": 0.25,
"min_microvia_diameter": 0.19999999999999998,
@ -122,211 +121,6 @@
"cvpcb": {
"equivalence_files": []
},
"erc": {
"erc_exclusions": [],
"meta": {
"version": 0
},
"pin_map": [
[
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
2,
0,
1,
0,
0,
1,
0,
2,
2,
2,
2
],
[
0,
0,
0,
0,
0,
0,
1,
0,
1,
0,
1,
2
],
[
0,
1,
0,
0,
0,
0,
1,
1,
2,
1,
1,
2
],
[
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
2
],
[
1,
1,
1,
1,
1,
0,
1,
1,
1,
1,
1,
2
],
[
0,
0,
0,
1,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
2,
1,
2,
0,
0,
1,
0,
2,
2,
2,
2
],
[
0,
2,
0,
1,
0,
0,
1,
0,
2,
0,
0,
2
],
[
0,
2,
1,
1,
0,
0,
1,
0,
2,
0,
0,
2
],
[
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2
]
],
"rule_severities": {
"bus_definition_conflict": "error",
"bus_entry_needed": "error",
"bus_label_syntax": "error",
"bus_to_bus_conflict": "error",
"bus_to_net_conflict": "error",
"different_unit_footprint": "error",
"different_unit_net": "error",
"duplicate_reference": "error",
"duplicate_sheet_names": "error",
"extra_units": "error",
"global_label_dangling": "warning",
"hier_label_mismatch": "error",
"label_dangling": "error",
"lib_symbol_issues": "warning",
"multiple_net_names": "warning",
"net_not_bus_member": "warning",
"no_connect_connected": "warning",
"no_connect_dangling": "warning",
"pin_not_connected": "error",
"pin_not_driven": "error",
"pin_to_pin": "warning",
"power_pin_not_driven": "error",
"similar_labels": "warning",
"unannotated": "error",
"unit_value_mismatch": "error",
"unresolved_variable": "error",
"wire_dangling": "error"
}
},
"libraries": {
"pinned_footprint_libs": [],
"pinned_symbol_libs": []
@ -372,43 +166,9 @@
"page_layout_descr_file": ""
},
"schematic": {
"annotate_start_num": 0,
"drawing": {
"default_line_thickness": 6.0,
"default_text_size": 50.0,
"field_names": [],
"intersheets_ref_own_page": false,
"intersheets_ref_prefix": "",
"intersheets_ref_short": false,
"intersheets_ref_show": false,
"intersheets_ref_suffix": "",
"junction_size_choice": 3,
"label_size_ratio": 0.25,
"pin_symbol_size": 25.0,
"text_offset_ratio": 0.08
},
"legacy_lib_dir": "",
"legacy_lib_list": [],
"meta": {
"version": 1
},
"net_format_name": "",
"page_layout_descr_file": "",
"plot_directory": "",
"spice_adjust_passive_values": false,
"spice_external_command": "spice \"%I\"",
"subpart_first_id": 65,
"subpart_id_separator": 0
"legacy_lib_list": []
},
"sheets": [
[
"8efee08b-b92e-4ba6-8722-c058e18114fe",
""
],
[
"00000000-0000-0000-0000-000061d2163a",
"Subsheet"
]
],
"sheets": [],
"text_variables": {}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -980,5 +980,12 @@ def test_qr_lib_1(test_dir):
bogus = os.path.join(bd, 'qr_test/'+f+'.bogus')
if os.path.isfile(bogus):
shutil.copy2(bogus, os.path.join(bd, 'qr_test/'+f))
os.remove(os.path.join(bd, 'qr_test/qr_test.kicad_pcb-bak'))
os.remove(os.path.join(bd, 'qr_test/qr_test.pro-bak'))
if context.ki5():
os.remove(os.path.join(bd, 'qr_test/qr_test.pro-bak'))
else:
os.remove(os.path.join(bd, 'qr_test/qr_test.kicad_sch-bak'))
os.remove(os.path.join(bd, 'qr_test/sub_1.kicad_sch-bak'))
bkp = os.path.join(bd, 'qr_test/qr_test.kicad_pcb-bak')
if os.path.isfile(bkp):
# Not always there, pcbnew_do can remove it
os.remove(bkp)