Names holding both a rail token (VCC/VDD/PWR/...) and a control token (SENSE, EN, PG, FB, OK, FAULT, ...) are signals ABOUT a rail - feedback, enable, power-good - so their non-Power classification is confident. They used to land in the Suspect bucket, drowning the genuine ambiguities. - classify_signal_name(): 3-state name verdict (Rail / PowerAdjacent / GndShield / Other) with whole-token matching (trailing digits stripped, long lexemes also match as suffix: VSENSE, PWRGOOD, NFAULT). infer_signal_type() becomes a thin wrapper, so the dashboard suspect count and the export suspect column shrink automatically. - infer_signal_types(): PowerAdjacent -> Other + new `adjacent` stat, before the structural gate (a big-fanout sense net stays Other). - LoadResult.adjacent rendered by all three consumers (TUI command, script engine, wx log) - outputs kept in sync. - analyze Types tab: new [Pwr-adjacent] rows with the deciding token, deliberate sort order (Power, Suspect, Adjacent, Gnd), glossary entry. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
192 lines
6.8 KiB
C++
192 lines
6.8 KiB
C++
|
||
#include "signals.hpp"
|
||
#include "parts.hpp"
|
||
|
||
#include <algorithm>
|
||
#include <cctype>
|
||
#include <cstring>
|
||
#include <vector>
|
||
|
||
const char *signal_type_name(SignalType t) {
|
||
switch (t) {
|
||
case SignalType::Power: return "power";
|
||
case SignalType::GndShield: return "gnd";
|
||
case SignalType::Other: return "other";
|
||
}
|
||
return "other";
|
||
}
|
||
|
||
SignalType next_signal_type(SignalType t) {
|
||
switch (t) {
|
||
case SignalType::Power: return SignalType::GndShield;
|
||
case SignalType::GndShield: return SignalType::Other;
|
||
case SignalType::Other: return SignalType::Power;
|
||
}
|
||
return SignalType::Other;
|
||
}
|
||
|
||
bool signal_type_from_name(const std::string &s, SignalType &out) {
|
||
std::string l = s;
|
||
std::transform(l.begin(), l.end(), l.begin(),
|
||
[](unsigned char c) { return std::tolower(c); });
|
||
if (l == "power" || l == "p") { out = SignalType::Power; return true; }
|
||
if (l == "gnd" || l == "g" || l == "shield" || l == "ground")
|
||
{ out = SignalType::GndShield; return true; }
|
||
if (l == "other" || l == "o" || l == "signal")
|
||
{ out = SignalType::Other; return true; }
|
||
return false;
|
||
}
|
||
|
||
namespace {
|
||
|
||
// Control/monitoring vocabulary: a name holding both a rail token and one of
|
||
// these is a signal ABOUT a rail (feedback, enable, power-good, fault, …) —
|
||
// not the rail itself. Matched against whole separator-delimited tokens
|
||
// (uppercase, trailing digits stripped so EN1/PG0 still hit). Entries of
|
||
// length ≥ 4 also match as a token *suffix*, catching fused (VSENSE, PWRGOOD)
|
||
// and active-low (NFAULT) forms; short entries match exactly, so GREEN or
|
||
// SENSOR never trip on EN / SENSE.
|
||
const char *const kPowerControlTokens[] = {
|
||
"SENSE", "SNS", "KELVIN", // remote / Kelvin sense
|
||
"FB", "FDBK", "FEEDBACK", // regulator feedback
|
||
"EN", "ENA", "ENABLE", "INH", "INHIBIT", // enable / inhibit
|
||
"PG", "PGOOD", "PWRGD", "PWROK", // power-good
|
||
"GOOD", "OK", "FAIL", "FAULT", "FLT", // status / fault
|
||
"ALERT", "ALRT", "WARN",
|
||
"MON", "IMON", "VMON", "PMON", // monitoring
|
||
"DET", "DETECT", "PRSNT", "PRESENT", // presence detection
|
||
"OC", "OCP", "OV", "OVP", "UV", "UVP", // protection trips
|
||
"TRIP", "SHDN", "SHUTDOWN",
|
||
"SEQ", "CTRL", "CTL", // sequencing / control
|
||
"STAT", "STATUS",
|
||
"ON", "OFF", "BTN", // on/off request
|
||
"CS", "IRQ",
|
||
};
|
||
|
||
bool is_power_control_token(std::string tok) {
|
||
while (!tok.empty() && std::isdigit((unsigned char)tok.back()))
|
||
tok.pop_back(); // EN1, PG0, FB2 …
|
||
if (tok.empty()) return false;
|
||
for (const char *lex : kPowerControlTokens) {
|
||
size_t n = std::strlen(lex);
|
||
if (tok == lex) return true;
|
||
if (n >= 4 && tok.size() > n
|
||
&& tok.compare(tok.size() - n, n, lex) == 0)
|
||
return true; // VSENSE, PWRGOOD, NFAULT …
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// Split on every non-alphanumeric character. `u` is already uppercase.
|
||
std::vector<std::string> alnum_tokens(const std::string &u) {
|
||
std::vector<std::string> out;
|
||
std::string cur;
|
||
for (char c : u) {
|
||
if (std::isalnum((unsigned char)c)) { cur += c; continue; }
|
||
if (!cur.empty()) { out.push_back(std::move(cur)); cur.clear(); }
|
||
}
|
||
if (!cur.empty()) out.push_back(std::move(cur));
|
||
return out;
|
||
}
|
||
|
||
} // namespace
|
||
|
||
// Heuristic. Names like GND, GNDA, SHIELD, CHASSIS → GndShield (name alone is
|
||
// reliable there — left out of the control-token logic on purpose). Names
|
||
// containing PWR/POWER/VCC/VDD/VEE/VSS, or starting with VS_/VBAT/+/− → rail
|
||
// candidates; a rail candidate whose tokens include a control word (SENSE,
|
||
// EN, PG, …) is downgraded to PowerAdjacent. Else Other.
|
||
NameClassification classify_signal_name(const std::string &name) {
|
||
NameClassification out;
|
||
if (name.empty()) return out;
|
||
std::string u = name;
|
||
std::transform(u.begin(), u.end(), u.begin(),
|
||
[](unsigned char c) { return std::toupper(c); });
|
||
|
||
auto contains = [&](const char *needle) {
|
||
return u.find(needle) != std::string::npos;
|
||
};
|
||
auto starts_with = [&](const char *needle) {
|
||
return u.rfind(needle, 0) == 0;
|
||
};
|
||
|
||
if (u == "GND" || u == "GROUND"
|
||
|| starts_with("GND_")
|
||
|| (starts_with("GND") && u.size() >= 4
|
||
&& std::isalpha((unsigned char)u[3]))
|
||
|| contains("SHIELD") || contains("CHASSIS") || contains("EARTH")) {
|
||
out.verdict = NameVerdict::GndShield;
|
||
return out;
|
||
}
|
||
|
||
if (contains("PWR") || contains("POWER")
|
||
|| contains("VCC") || contains("VDD") || contains("VEE") || contains("VSS")
|
||
|| starts_with("VS_") || starts_with("VS1_") || starts_with("VS2_")
|
||
|| starts_with("VS3_") || starts_with("VS4_")
|
||
|| starts_with("VBAT") || starts_with("VBUS")
|
||
|| starts_with("+") || starts_with("-")) {
|
||
for (const std::string &tok : alnum_tokens(u)) {
|
||
if (is_power_control_token(tok)) {
|
||
out.verdict = NameVerdict::PowerAdjacent;
|
||
out.token = tok;
|
||
return out;
|
||
}
|
||
}
|
||
out.verdict = NameVerdict::Rail;
|
||
return out;
|
||
}
|
||
return out;
|
||
}
|
||
|
||
SignalType infer_signal_type(const std::string &name) {
|
||
switch (classify_signal_name(name).verdict) {
|
||
case NameVerdict::Rail: return SignalType::Power;
|
||
case NameVerdict::GndShield: return SignalType::GndShield;
|
||
case NameVerdict::PowerAdjacent:
|
||
case NameVerdict::Other: break;
|
||
}
|
||
return SignalType::Other;
|
||
}
|
||
|
||
Signal::Signal(std::string name)
|
||
: SystemElementContainer<Pin>(name), prnt(nullptr),
|
||
type(SignalType::Other) {};
|
||
|
||
void Signal::add(Pin *pin)
|
||
{
|
||
string pname = pin->prnt->name + "." + pin->name;
|
||
SystemElementContainer<Pin>::add(pname, pin);
|
||
}
|
||
|
||
Signals::Signals(void): SystemElementContainer<Signal>("signals") {}
|
||
|
||
Signals::Signals(std::vector<Signal *> signals): SystemElementContainer<Signal>("signals", signals) {}
|
||
|
||
Signals::~Signals() {
|
||
for (const auto& [key, value] : content) {
|
||
delete value;
|
||
}
|
||
}
|
||
|
||
int drop_singleton_signals(Signals *signals) {
|
||
if (!signals) return 0;
|
||
std::vector<Signal *> doomed;
|
||
for (auto &kv : *signals)
|
||
if (kv.second->size() == 1) doomed.push_back(kv.second);
|
||
for (Signal *s : doomed) {
|
||
// Detach the lone pin so it surfaces as `(NC)` in views.
|
||
for (auto &pkv : *s) {
|
||
pkv.second->connect(nullptr);
|
||
pkv.second->nc_origin = NcOrigin::DroppedSingleton;
|
||
}
|
||
signals->remove(s->name);
|
||
delete s;
|
||
}
|
||
return (int)doomed.size();
|
||
}
|
||
|
||
void Signals::add(Signal *signal)
|
||
{
|
||
SystemElementContainer<Signal>::add(signal);
|
||
signal->prnt = this;
|
||
} |