# -*- 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()