191 lines
7.5 KiB
Python
191 lines
7.5 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright (c) 2020-2022 Salvador E. Tropea
|
|
# Copyright (c) 2020-2022 Instituto Nacional de Tecnología Industrial
|
|
# License: GPL-3.0
|
|
# Project: KiBot (formerly KiPlot)
|
|
"""
|
|
Dependencies:
|
|
- from: KiAuto
|
|
role: mandatory
|
|
command: eeschema_do
|
|
version: 1.5.4
|
|
"""
|
|
import os
|
|
from sys import exit
|
|
import xml.etree.ElementTree as ET
|
|
from .macros import macros, document, pre_class # noqa: F401
|
|
from .error import KiPlotConfigurationError
|
|
from .gs import GS
|
|
from .kiplot import exec_with_retry, add_extra_options, load_board
|
|
from .misc import BOM_ERROR, NETLIST_DIFF, W_PARITY, MISSING_TOOL
|
|
from .log import get_logger
|
|
from .optionable import Optionable
|
|
import pcbnew
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
class Update_XMLOptions(Optionable):
|
|
""" Reference sorting options """
|
|
def __init__(self):
|
|
super().__init__()
|
|
with document:
|
|
self.enabled = True
|
|
""" Enable the update. This is the replacement for the boolean value """
|
|
self.check_pcb_parity = False
|
|
""" *Check if the PCB and Schematic are synchronized.
|
|
This is equivalent to the *Test for parity between PCB and schematic* of the DRC dialog.
|
|
Only available for KiCad 6 """
|
|
self.as_warnings = False
|
|
""" Inform the problems as warnings and don't stop """
|
|
|
|
|
|
@pre_class
|
|
class Update_XML(BasePreFlight): # noqa: F821
|
|
""" [boolean=false|dict] Update the XML version of the BoM (Bill of Materials).
|
|
To ensure our generated BoM is up to date.
|
|
Note that this isn't needed when using the internal BoM generator (`bom`).
|
|
You can compare the PCB and schematic netlists using it """
|
|
def __init__(self, name, value):
|
|
super().__init__(name, value)
|
|
self._check_pcb_parity = False
|
|
if isinstance(value, bool):
|
|
self._enabled = value
|
|
elif isinstance(value, dict):
|
|
f = Update_XMLOptions()
|
|
f.set_tree(value)
|
|
f.config(self)
|
|
self._enabled = f.enabled
|
|
self._check_pcb_parity = f.check_pcb_parity
|
|
self.options = f
|
|
self._pcb_related = True
|
|
else:
|
|
raise KiPlotConfigurationError('must be boolean or dict')
|
|
self._sch_related = True
|
|
|
|
@classmethod
|
|
def get_doc(cls):
|
|
return cls.__doc__, Update_XMLOptions
|
|
|
|
def get_targets(self):
|
|
""" Returns a list of targets generated by this preflight """
|
|
return [GS.sch_no_ext+'.xml']
|
|
|
|
def check_components(self, comps, errors):
|
|
found_comps = set()
|
|
for m in GS.get_modules():
|
|
ref = m.GetReference()
|
|
found_comps.add(ref)
|
|
if ref not in comps:
|
|
errors.append('{} found in PCB, but not in schematic'.format(ref))
|
|
continue
|
|
sch_fp = comps[ref]
|
|
pcb_fp = m.GetFPIDAsString()
|
|
if sch_fp != pcb_fp:
|
|
errors.append('{} footprint mismatch (PCB: {} vs schematic: {})'.format(ref, pcb_fp, sch_fp))
|
|
for ref in set(comps.keys()).difference(found_comps):
|
|
errors.append('{} found in schematic, but not in PCB'.format(ref))
|
|
|
|
def check_nets(self, net_names, net_nodes, errors):
|
|
# Total count
|
|
con = GS.board.GetConnectivity()
|
|
pcb_net_count = con.GetNetCount()-1 # Removing the bogus net 0
|
|
sch_net_count = len(net_names)
|
|
if pcb_net_count != sch_net_count:
|
|
errors.append('Net count mismatch (PCB {} vs schematic {})'.format(pcb_net_count, sch_net_count))
|
|
net_info = GS.board.GetNetInfo()
|
|
# Names and connection
|
|
for n in net_info.NetsByNetcode():
|
|
if not n:
|
|
# Bogus net code 0
|
|
continue
|
|
if n not in net_names:
|
|
errors.append('PCB net code {} not in schematic'.format(n))
|
|
continue
|
|
net = net_info.GetNetItem(n)
|
|
net_name = net.GetNetname()
|
|
sch_name = net_names[n]
|
|
if net_name != sch_name:
|
|
errors.append('PCB net code {} name mismatch ({} vs {})'.format(n, net_name, sch_name))
|
|
sch_nodes = net_nodes[n]
|
|
pcb_nodes = {pad.GetParent().GetReference()+' pin '+pad.GetNumber()
|
|
for pad in con.GetNetItems(n, [pcbnew.PCB_PAD_T])}
|
|
dif = pcb_nodes-sch_nodes
|
|
if dif:
|
|
errors.append('PCB net code {} extra connection/s: {}'.format(n, ','.join(list(dif))))
|
|
dif = sch_nodes-pcb_nodes
|
|
if dif:
|
|
errors.append('PCB net code {} missing connection/s: {}'.format(n, ','.join(list(dif))))
|
|
|
|
def check_pcb_parity(self):
|
|
if GS.ki5:
|
|
logger.error('PCB vs schematic parity only available for KiCad 6')
|
|
exit(MISSING_TOOL)
|
|
fname = GS.sch_no_ext+'.xml'
|
|
logger.debug('Loading XML: '+fname)
|
|
try:
|
|
tree = ET.parse(fname)
|
|
except Exception as e:
|
|
raise KiPlotConfigurationError('Errors parsing {}\n{}'.format(fname, e))
|
|
root = tree.getroot()
|
|
if root.tag != 'export':
|
|
raise KiPlotConfigurationError("{} isn't a valid netlist".format(fname))
|
|
# Check version? root.attrib.get('version')
|
|
components = root.find('components')
|
|
comps = {}
|
|
if components is not None:
|
|
for c in components.iter('comp'):
|
|
ref = c.attrib.get('ref')
|
|
fp = c.find('footprint')
|
|
fp = fp.text if fp is not None else ''
|
|
logger.debugl(2, '- {}: {}'.format(ref, fp))
|
|
comps[ref] = fp
|
|
netlist = root.find('nets')
|
|
net_names = {}
|
|
net_nodes = {}
|
|
if netlist is not None:
|
|
for n in netlist.iter('net'):
|
|
code = int(n.get('code'))
|
|
net_names[code] = n.get('name')
|
|
net_nodes[code] = {node.get('ref')+' pin '+node.get('pin') for node in n.iter('node')}
|
|
# Check with the PCB
|
|
errors = []
|
|
load_board()
|
|
# Check components
|
|
self.check_components(comps, errors)
|
|
# Check the nets
|
|
self.check_nets(net_names, net_nodes, errors)
|
|
# Report errors
|
|
if errors:
|
|
if self.options.as_warnings:
|
|
for e in errors:
|
|
logger.warning(W_PARITY+e)
|
|
else:
|
|
for e in errors:
|
|
logger.error(e)
|
|
exit(NETLIST_DIFF)
|
|
|
|
def run(self):
|
|
command = self.ensure_tool('KiAuto')
|
|
out_dir = self.expand_dirname(GS.out_dir)
|
|
cmd = [command, 'bom_xml', GS.sch_file, out_dir]
|
|
# If we are in verbose mode enable debug in the child
|
|
cmd, video_remove = add_extra_options(cmd)
|
|
# While creating the XML we run a BoM plug-in that creates a useless BoM
|
|
# We remove it, unless this is already there
|
|
side_effect_file = os.path.join(out_dir, GS.sch_basename+'.csv')
|
|
remove_side_effect_file = not os.path.isfile(side_effect_file)
|
|
logger.info('- Updating BoM in XML format')
|
|
ret = exec_with_retry(cmd)
|
|
if remove_side_effect_file and os.path.isfile(side_effect_file):
|
|
os.remove(side_effect_file)
|
|
if ret:
|
|
logger.error('Failed to update the BoM, error %d', ret)
|
|
exit(BOM_ERROR)
|
|
if video_remove:
|
|
video_name = os.path.join(self.expand_dirname(GS.out_dir), 'bom_xml_eeschema_screencast.ogv')
|
|
if os.path.isfile(video_name):
|
|
os.remove(video_name)
|
|
if self._check_pcb_parity:
|
|
self.check_pcb_parity()
|