Add electrical continuity checking to the scripting API

qet.checkContinuity(folioIndex) runs two structural checks against the
live Terminal/Conductor object graph -- Terminal::conductors() and
Conductor::relatedPotentialConductors(), the same primitive
setConductorProperty() already uses -- rather than a heuristic read of
the saved XML:

- unconnected_terminal (info): a terminal with no conductor at all.
  Deliberately low severity -- routine (a spare relay contact, an
  unused optional pin), not necessarily a mistake.
- potential_mismatch (error): two conductors electrically on the same
  potential (following bridged terminal strips and linked report
  elements, matching setConductorProperty()'s own scope) disagreeing
  on num/conductor_color/conductor_section/function/bus/cable.
  QElectroTech's own edits always keep every member of a potential
  identical, so any divergence found here came from hand-edited XML,
  a legacy file, or an external tool -- verified with a test that
  patches a saved file's XML directly to introduce exactly that.

Documented plainly what this does NOT check and why: pin electrical
direction/power conflicts and No/Nc/Common contact shorts, since
QElectroTech's terminal data model (Generic/Inner/Outer/No/Nc/Common --
contact role within one relay, not signal direction) does not carry
the information either would need. This is continuity/consistency
checking, not full ERC.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-09-22 16:28:04 +12:00
parent a542b911e9
commit 2906a48aab
2 changed files with 125 additions and 0 deletions
+120
View File
@@ -3837,6 +3837,126 @@ int QetScriptApi::searchAndReplace(const QString &kind, const QString &field,
return changed;
}
/**
@brief QetScriptApi::checkContinuity
Structural electrical checks against the live scene graph -- Terminal/
Conductor/relatedPotentialConductors() -- rather than a heuristic read
of the saved XML the way qet_check's Python side works. Two checks:
1. unconnected_terminal (severity "info"): a terminal with no
conductor at all. Reported at low confidence deliberately -- an
unconnected terminal is routine (a spare relay contact, an unused
optional pin), not necessarily a mistake, so this is a prompt to
look, not a claim that something is wrong.
2. potential_mismatch (severity "error"): two conductors that
electrically belong to the same potential (connected transitively
through shared terminals, following bridged terminal strips and
linked report elements the same way setConductorProperty() does)
but disagree on num, conductor_color, conductor_section, function,
bus or cable. QElectroTech's own setConductorProperty() always
writes every member of a potential identically, so any divergence
found here did not come from this API or the GUI's equivalent
action -- it came from hand-edited XML, a legacy file, or an
external tool, and it is a real defect: two wire numbers on what
is electrically one node is exactly the kind of thing a human
reading the schematic would get wrong from.
What this deliberately does NOT check, because QElectroTech's own
terminal data model does not carry the information a real check would
need: pin electrical direction/power conflicts (no terminal in this
model is marked input/output/power the way a KiCad pin is -- only
Generic/Inner/Outer/No/Nc/Common, which describe contact role within
one relay/switch, not signal direction), and short circuits between a
contact's No and Nc terminals sharing a Common (would need per-
contact-group semantics this does not attempt). Treat this as
continuity/consistency checking, not full ERC.
@param folioIndex a single folio, or -1 for the whole project
@return a list of {kind, severity, folio, message, ...} objects;
kind-specific keys: unconnected_terminal has element/elementLabel/
terminal/terminalName, potential_mismatch has property/values
*/
QVariantList QetScriptApi::checkContinuity(int folioIndex)
{
QVariantList findings;
if (!m_project) return findings;
const QList<Diagram *> diagrams = m_project->diagrams();
if (folioIndex >= diagrams.count()) {
log(QStringLiteral("qet.checkContinuity: folio %1 does not exist (%2 folio(s))")
.arg(folioIndex).arg(diagrams.count()));
return findings;
}
for (int f = 0; f < diagrams.count(); ++f) {
if (folioIndex >= 0 && f != folioIndex) continue;
DiagramContent content(diagrams.at(f), false);
for (Element *elmt : std::as_const(content.m_elements)) {
const QList<Terminal *> terminals = elmt->terminals();
for (int ti = 0; ti < terminals.count(); ++ti) {
Terminal *t = terminals.at(ti);
if (!t->conductors().isEmpty()) continue;
QVariantMap finding;
finding.insert(QStringLiteral("kind"), QStringLiteral("unconnected_terminal"));
finding.insert(QStringLiteral("severity"), QStringLiteral("info"));
finding.insert(QStringLiteral("folio"), f);
finding.insert(QStringLiteral("element"), elmt->uuid().toString());
finding.insert(QStringLiteral("elementLabel"), elmt->actualLabel());
finding.insert(QStringLiteral("terminal"), ti);
finding.insert(QStringLiteral("terminalName"), t->name());
finding.insert(QStringLiteral("message"),
QStringLiteral("terminal %1 ('%2') of %3 has no conductor")
.arg(ti).arg(t->name(), elmt->actualLabel()));
findings << finding;
}
}
}
static const QStringList checked_properties = {
QStringLiteral("num"), QStringLiteral("conductor_color"),
QStringLiteral("conductor_section"), QStringLiteral("function"),
QStringLiteral("bus"), QStringLiteral("cable")};
QSet<Conductor *> visited;
for (int f = 0; f < diagrams.count(); ++f) {
if (folioIndex >= 0 && f != folioIndex) continue;
DiagramContent content(diagrams.at(f), false);
const QList<Conductor *> all = content.conductors(DiagramContent::AnyConductor);
for (Conductor *c : all) {
if (visited.contains(c)) continue;
QSet<Conductor *> potential = c->relatedPotentialConductors(true);
potential << c;
visited += potential;
if (potential.count() < 2) continue;
QHash<QString, QSet<QString>> distinct_values;
for (Conductor *pc : std::as_const(potential)) {
for (const QString &prop : checked_properties) {
distinct_values[prop].insert(conductorPropertyValue(pc->properties(), prop));
}
}
for (const QString &prop : checked_properties) {
if (distinct_values.value(prop).count() <= 1) continue;
QStringList values(distinct_values.value(prop).begin(),
distinct_values.value(prop).end());
values.sort();
QVariantMap finding;
finding.insert(QStringLiteral("kind"), QStringLiteral("potential_mismatch"));
finding.insert(QStringLiteral("severity"), QStringLiteral("error"));
finding.insert(QStringLiteral("folio"), f);
finding.insert(QStringLiteral("property"), prop);
finding.insert(QStringLiteral("values"), values);
finding.insert(QStringLiteral("message"),
QStringLiteral("conductors on the same electrical potential disagree "
"on %1: %2").arg(prop, values.join(QStringLiteral(", "))));
findings << finding;
}
}
}
return findings;
}
bool QetScriptApi::selectElement(const QString &elementUuid)
{
if (!m_project) return false;