225 lines
9.8 KiB
Python
225 lines
9.8 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright (c) 2020-2023 Salvador E. Tropea
|
|
# Copyright (c) 2020-2023 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
|
|
"""
|
|
from collections import namedtuple
|
|
import os
|
|
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 load_board
|
|
from .misc import BOM_ERROR, NETLIST_DIFF, W_PARITY, MISSING_TOOL, KICAD_VERSION_7_0_1, W_NOTINBOM
|
|
from .log import get_logger
|
|
from .optionable import Optionable
|
|
import pcbnew
|
|
|
|
logger = get_logger(__name__)
|
|
Component = namedtuple("Component", "val fp props")
|
|
|
|
|
|
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.
|
|
Not available for KiCad 5. **Important**: when using KiCad 6 and the *Exclude from BoM* attribute
|
|
these components won't be included in the generated XML, so we can't check its parity """
|
|
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()
|
|
excluded = set()
|
|
for m in GS.get_modules():
|
|
ref = m.GetReference()
|
|
pcb_props = m.GetProperties()
|
|
found_comps.add(ref)
|
|
if ref not in comps:
|
|
if GS.ki6_only and pcb_props.get('exclude_from_bom') is not None:
|
|
# KiCad 6 doesn't include the excluded components in the netlist
|
|
logger.warning(W_NOTINBOM+f"{ref} excluded from BoM we can't check its parity, upgrade to KiCad 7")
|
|
excluded.add(ref)
|
|
else:
|
|
errors.append('{} found in PCB, but not in schematic'.format(ref))
|
|
continue
|
|
sch_data = comps[ref]
|
|
pcb_fp = m.GetFPIDAsString()
|
|
if sch_data.fp != pcb_fp:
|
|
errors.append('{} footprint mismatch (PCB: `{}` vs schematic: `{}`)'.format(ref, pcb_fp, sch_data.fp))
|
|
pcb_val = m.GetValue()
|
|
if sch_data.val != pcb_val:
|
|
errors.append('{} value mismatch (PCB: `{}` vs schematic: `{}`)'.format(ref, pcb_val, sch_data.val))
|
|
# Properties
|
|
found_props = set()
|
|
for p, v in sch_data.props.items():
|
|
v_pcb = pcb_props.get(p)
|
|
if v_pcb is None:
|
|
errors.append('{} schematic property `{}` not in PCB'.format(ref, p))
|
|
continue
|
|
found_props.add(p)
|
|
if v_pcb != v:
|
|
if v is None:
|
|
# Things like "exclude_from_bom" has no "value", so we get None, but they have '' in the PCB
|
|
v = ''
|
|
elif p == 'Sheetfile':
|
|
# Sheetfile is really inside the .kicad_pcb, but is just generated by Eeschema
|
|
# This implies that Eeschema can add a path relative to cwd
|
|
# We just check the filename here
|
|
v_pcb = os.path.basename(v_pcb)
|
|
v = os.path.basename(v)
|
|
if v_pcb != v:
|
|
errors.append(f'{ref} property `{p}` mismatch (PCB: `{v_pcb}` vs schematic: `{v}`)')
|
|
# Missing properties
|
|
for p in set(pcb_props.keys()).difference(found_props):
|
|
errors.append('{} PCB property `{}` not in schematic'.format(ref, p))
|
|
for ref in set(comps.keys()).difference(found_comps):
|
|
errors.append('{} found in schematic, but not in PCB'.format(ref))
|
|
return excluded
|
|
|
|
def check_nets(self, net_nodes, errors, excluded):
|
|
# Total count
|
|
con = GS.board.GetConnectivity()
|
|
pcb_net_count = con.GetNetCount()-1 # Removing the bogus net 0
|
|
sch_net_count = len(net_nodes)
|
|
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
|
|
pcb_net_names = set()
|
|
for n in net_info.NetsByNetcode():
|
|
if not n:
|
|
# Bogus net code 0
|
|
continue
|
|
net = net_info.GetNetItem(n)
|
|
net_name = net.GetNetname()
|
|
if net_name not in net_nodes:
|
|
errors.append('Net `{}` not in schematic'.format(net_name))
|
|
continue
|
|
pcb_net_names.add(net_name)
|
|
sch_nodes = net_nodes[net_name]
|
|
pcb_nodes = {pad.GetParent().GetReference()+' pin '+pad.GetNumber()
|
|
for pad in con.GetNetItems(n, pcbnew.PCB_PAD_T)
|
|
if pad.GetParent().GetReference() not in excluded}
|
|
dif = pcb_nodes-sch_nodes
|
|
if dif:
|
|
errors.append('Net `{}` extra PCB connection/s: {}'.format(net_name, ','.join(list(dif))))
|
|
dif = sch_nodes-pcb_nodes
|
|
if dif:
|
|
errors.append('Net `{}` missing PCB connection/s: {}'.format(net_name, ','.join(list(dif))))
|
|
# Now check if the schematic added nets
|
|
for name in net_nodes.keys():
|
|
if name not in pcb_net_names:
|
|
errors.append('Net `{}` not in PCB'.format(name))
|
|
|
|
def check_pcb_parity(self):
|
|
if GS.ki5:
|
|
GS.exit_with_error('PCB vs schematic parity only available for KiCad 6', MISSING_TOOL)
|
|
if GS.ki7 and GS.kicad_version_n < KICAD_VERSION_7_0_1:
|
|
GS.exit_with_error("Connectivity API is broken on KiCad 7.0.0\n"
|
|
"Please upgrade KiCad to 7.0.1 or newer", 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')
|
|
val = c.find('value')
|
|
val = val.text if val is not None else ''
|
|
fp = c.find('footprint')
|
|
fp = fp.text if fp is not None else ''
|
|
props = {p.get('name'): p.get('value') for p in c.iter('property')}
|
|
logger.debugl(2, '- {}: {} {} {}'.format(ref, val, fp, props))
|
|
comps[ref] = Component(val, fp, props)
|
|
netlist = root.find('nets')
|
|
net_nodes = {}
|
|
if netlist is not None:
|
|
for n in netlist.iter('net'):
|
|
# This is a useless number stored there just to use disk space and confuse people:
|
|
# code = int(n.get('code'))
|
|
net_nodes[n.get('name')] = {node.get('ref')+' pin '+node.get('pin') for node in n.iter('node')}
|
|
# Check with the PCB
|
|
errors = []
|
|
load_board()
|
|
# Check components
|
|
excluded = self.check_components(comps, errors)
|
|
# Check the nets
|
|
self.check_nets(net_nodes, errors, excluded)
|
|
# Report errors
|
|
if errors:
|
|
if self.options.as_warnings:
|
|
for e in errors:
|
|
logger.warning(W_PARITY+e)
|
|
else:
|
|
GS.exit_with_error(errors, 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 = self.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')
|
|
if not os.path.isfile(side_effect_file):
|
|
self._files_to_remove.append(side_effect_file)
|
|
logger.info('- Updating BoM in XML format')
|
|
self.exec_with_retry(cmd, BOM_ERROR)
|
|
if self._check_pcb_parity:
|
|
self.check_pcb_parity()
|