Altium import, nets, canonical pins, component kinds, set/$var, scrollback, source modal.

Major additions, all wired end-to-end with doctest coverage:

- Altium netlist importer (`imports/import_altium.{hpp,cpp}`): two-pass
  parser for `[ ]` parts and `( )` signals; `System::Load` no longer has
  the IMPORT_ALTIUM hole.
- `duplicate <src> <dst>` deep-copies a module (signals, parts, pins,
  rewired signals); connections excluded by design.
- Nets (`system/nets.{hpp,cpp}`): BFS over `Connection::pin_map` to
  return the transitive (Module, Signal) closure. `verify` extended with
  a second pass flagging Power↔GndShield inconsistencies in bridged
  nets; new `net <module> <signal>` command for inspection.
- Canonical pin names (`system/pin_name.{hpp,cpp}`): zero-padded digit
  suffix lets A1 ↔ A001 pair via `IdentityTransform` and
  `CheckIdentityCompatible` without losing the imported notation.
- Component classification (`system/component_kind.{hpp,cpp}`):
  `Part::kind` inferred at construction from the reference-designator
  prefix (longest-match: LED/TP/SW/FB/MK/MP/MH/HS/RA/RN/RP/RV first,
  then R/C/L/F/D/Q/U/J/P/Y/X/S).
- Identity wiring tolerance: `CheckIdentityCompatible` accepts the
  subset case (typical when one importer drops NC pins, e.g. Altium)
  and surfaces orphans as an info string. `FillIdentityNCs`
  materialises orphan canonical positions as NC pins on the missing
  side at connect time.
- Connector layout preparation: `pin_layout(kind)` and
  `FillPartFromLayout(part, kind)` stubs in `pin_role`, called from
  `set-type`. Empty today; populate alongside `vpx_3u_role`.
- TUI scrollback: PageUp/PageDown step 10 lines, Home/End jump to
  ends; `Print()` snaps back to the tail.
- `set <name> <value>` declares session variables; `$name` / `${name}`
  expanded inside `Finalize` between canonical-form recording and the
  action call — history and script-save preserve `$var` references.
- Long `source` scripts now show a centred "Computing…" modal with a
  N/M progress counter. Driven by a ticker thread that posts one
  paced `Event::Special` per processed line, ack'd by the main thread,
  so heavy lines don't backlog ticks and freeze the counter.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-09 20:28:21 +02:00
parent 477f3abd40
commit c3bb00cb4d
24 changed files with 1163 additions and 61 deletions

View File

@@ -23,15 +23,25 @@ src/
system/ -- domain model
syselmts.hpp SystemElement + SystemElementContainer<T> (templated, get/merge/iterate)
modules.{hpp,cpp} Module, Modules
parts.{hpp,cpp} Part, Parts
parts.{hpp,cpp} Part (carries `kind` + `connector_type`), Parts
pins.{hpp,cpp} Pin, Pins
signals.{hpp,cpp} Signal, Signals
signal_type.hpp SignalType + helpers
component_kind.{hpp,cpp} ComponentKind enum + infer_component_kind(name)
pin_name.{hpp,cpp} canonical_pin_name(s) — zero-pad digit suffix to 3
connect.{hpp,cpp} Connection, Connections
transform.{hpp,cpp} transforms applied to the model
transform.{hpp,cpp} Transform / IdentityTransform / TransformRegistry +
CheckIdentityCompatible + FillIdentityNCs
pin_role.{hpp,cpp} pin_role(kind, name), pin_layout(kind),
FillPartFromLayout(part, kind)
nets.{hpp,cpp} find_net / compute_all_nets / net_type_consistent
persist.{hpp,cpp} save / restore (tab-delimited)
system.{hpp,cpp} System: owns Modules + Connections, exposes Load()
imports/ -- adapters that populate the domain
import_base.hpp ImportBase interface
import_mentor.{hpp,cpp} Mentor Graphics netlist parser (done)
import_mentor.{hpp,cpp} Mentor Graphics netlist parser
import_altium.{hpp,cpp} Altium netlist parser (`[ ]` parts, `( )` signals)
import_ods.{hpp,cpp} ODS spreadsheet pinout (libzip + pugixml)
tui/ -- FTXUI shell, split by responsibility
tui.{hpp,cpp} Class Tui (state + Run() orchestrator + screen-mode event dispatcher)
tui_helpers.{hpp,cpp} Free helpers: ToLower, NaturalLess, LongestCommonPrefix
@@ -71,7 +81,11 @@ doc/classes.puml -- PlantUML class diagram
- Multi-step prompts work via a `std::deque<Prompt>` queue. `Submit()` pops them one by one before falling back to dispatch. Adding a new command = one entry in `RegisterCommands()`; the prompt-flow and inline-flow are both handled automatically.
- Tab completion: at the top-level prompt (no `pending`), completes built-in command names. Inside a prompt with `path_completion = true` (e.g. the `filename` step of `load`), completes file paths via `std::filesystem::directory_iterator` (handles `~/`, dirs get a trailing `/`). Logic: 1 match → replace; multiple with progress on the longest common prefix → extend; multiple stuck at LCP → list candidates in the visualisation area.
Built-in commands: `new`, `load`, `save`, `restore`, `source`, `script-save`, `connect`, `set-type`, `search`, `explore`, `clear`, `help`, `quit`/`exit`. `Esc` cancels an in-progress multi-step prompt.
Built-in commands: `new`, `set`, `load`, `duplicate`, `save`, `restore`, `source`, `script-save`, `connect`, `set-type`, `set-signal-type`, `search`, `explore`, `verify`, `net`, `clear`, `help`, `quit`/`exit`. `Esc` cancels an in-progress multi-step prompt.
`set <name> <value>` declares a session-scoped variable. Subsequent commands expand `$name` and `${name}` in their args (substitution happens in `Finalize` between canonical-form recording and `spec.action(args)` — so `history` and `script-save` keep the **unexpanded** form, while the action sees resolved values). Unknown variables are left literal. `vars` is reset by `new`. Validation: `[A-Za-z_][A-Za-z0-9_]*`.
`duplicate <source> <newname>` deep-copies a module: signals (with type overrides), parts (with `connector_type` and `kind`), pins (with `expected_signal_type`), and rewires each pin to the equivalent same-named signal in the new module. Connections are NOT copied (they're cross-module topology).
`script-save <file>` writes a replay-ready script of every command issued since the last `new` (the `recorded` buffer is cleared inside the `new` action). The buffer is appended to inside `Finalize()` after the action runs, with a denylist of commands that aren't useful in a replay: `clear`, `help`, `quit`, `exit`, `source`, `script-save`. Note the `source` exclusion: when a script is sourced, the *individual lines inside* the script are recorded (because each goes through `Finalize`), so the saved script reproduces the same end state without the indirection.
@@ -85,15 +99,27 @@ Pending prompts (from incomplete inline commands) are NOT considered interactive
**Signals** carry a `type` (`SignalType::Power | GndShield | Other`) auto-inferred from the name in `Signal::Signal` via `infer_signal_type` (heuristic: GND/GROUND/SHIELD/CHASSIS → GndShield; PWR/VCC/VDD/VEE/VSS/VBAT/VS_/VS3_*/+/- prefixes → Power; else Other). Override with `set-signal-type <module> <signal> <power|gnd|other>`. The explore screen shows the type in the signal detail header.
**Pin role expectations**: every Pin carries an `expected_signal_type` populated by `set-type` from a per-(connector_type, pin_name) lookup (`src/system/pin_role.{hpp,cpp}`). The framework is wired end-to-end; the actual VPX 3U lookup table is currently a stub returning Other for all positions — fill in `vpx_3u_role(col, row, idx)` with the real VITA 46 layout when needed. The `verify` command walks all typed parts and reports pins whose connected signal's type doesn't match the expectation.
**Pin role expectations**: every Pin carries an `expected_signal_type` populated by `set-type` from a per-(connector_type, pin_name) lookup (`src/system/pin_role.{hpp,cpp}`). The framework is wired end-to-end; the actual VPX 3U lookup table is currently a stub returning Other for all positions — fill in `vpx_3u_role(col, row, idx)` with the real VITA 46 layout when needed.
**Connector pin layout (preparation)**: `pin_layout(connector_type)` returns the canonical full pin-name list for a known connector kind, and `FillPartFromLayout(part, kind)` materialises NC pins for any layout position absent from the imported netlist. `set-type` calls it after setting `connector_type` (no-op today since `pin_layout` is a stub returning `{}` for everything — populate alongside `vpx_3u_role`). End-to-end chain in place: `set-type → FillPartFromLayout → pin_role`.
**`verify` (two passes)**: walks all typed pins and reports local mismatches between `expected_signal_type` and the actual signal type, AND walks all bridged nets reporting Power↔GndShield inconsistencies. `net <module> <signal>` prints the BFS-reached `(module, signal)` set with types and an `[INCONSISTENT]` flag.
**Component classification**: every `Part` carries a `ComponentKind kind` (`Passive | Semiconductor | IntegratedCircuit | Connector | TestPoint | Switch | Crystal | Mechanical | Other`) inferred at construction by `infer_component_kind(name)` from the leading reference-designator letter(s) (longest-match: `LED/TP/SW/FB/MK/MP/MH/HS/RA/RN/RP/RV` first, then single-letter R/C/L/F/D/Q/U/J/P/Y/X/S). Recomputed on `restore` (no persistence tag). Not yet exposed in TUI commands — branchpoints will be `search` filter, `set-type` guard, and `explore` header.
`SignalType` lives in its own header `src/system/signal_type.hpp` (extracted from signals to avoid a pins↔signals include cycle).
**Pins** are either NC (`signal() == nullptr`) or connected to exactly one signal. The ODS importer creates a Pin for every row that has a non-empty pin name, even when the signal column is empty or `"NC"` — the pin stays in the Part as NC. `restore` replaces `Tui::sys` entirely (`unique_ptr::reset`). Names are stored as-is — must not contain TAB or newline (true for the EE netlists we ingest). Format is versioned by the `# essim system snapshot v1` header for future compatibility.
**Connector types & transforms**: every `Part` carries a `connector_type` string (default `""`, set via the `set-type` command — inline `set-type m p kind` or bare which opens a TUI screen with module menu, part filter+menu, type input, list of types already in use, and an Apply button). When `connect` validates a pair, it consults `TransformRegistry::lookup(p1->connector_type, p2->connector_type)` (defined in `src/system/transform.{hpp,cpp}`) — both directions of the pair are tried. If neither is registered, an `IdentityTransform` fallback wires each pin of A to the same-name pin of B (when present). The resulting `(Pin*, Pin*)` list and the transform's name are stored on the `Connection` (`pin_map`, `transform_name`). To register a real transform: define a `Transform` subclass in `transform.cpp` and call `TransformRegistry::get().add("kindA", "kindB", new MyTransform())` at init — there's no startup hook for this yet, so a small `RegisterBuiltinTransforms()` helper is the natural place to add when more types appear.
**Connector types & transforms**: every `Part` carries a `connector_type` string (default `""`, set via the `set-type` command — inline `set-type m p kind` or bare which opens a TUI screen with module menu, part filter+menu, type input, list of types already in use, and an Apply button). When `connect` validates a pair, it consults `TransformRegistry::lookup(p1->connector_type, p2->connector_type)` (defined in `src/system/transform.{hpp,cpp}`) — both directions of the pair are tried. If neither is registered, an `IdentityTransform` fallback wires each pin of A to the canonical-equivalent pin of B (when present). The resulting `(Pin*, Pin*)` list and the transform's name are stored on the `Connection` (`pin_map`, `transform_name`). To register a real transform: define a `Transform` subclass in `transform.cpp` and call `TransformRegistry::get().add("kindA", "kindB", new MyTransform())` at init — there's no startup hook for this yet, so a small `RegisterBuiltinTransforms()` helper is the natural place to add when more types appear.
`screen_idx` mapping: 0 = main TUI, 1 = search, 2 = connect, 3 = set-type. Adding a new screen mode is the same recipe each time: state members, `Container::Vertical` of focusable components, a `Renderer` lambda that recomputes derived state per frame (e.g. filtered part lists), an entry in `Container::Tab`, and Tab/Esc handling in the outer `CatchEvent`.
**Identity wiring uses canonical names**: `IdentityTransform::apply` builds `unordered_map<canonical, Pin*>` for side B and looks up each side-A pin by its canonical form. So `A1` (one card) auto-pairs with `A001` (the other) thanks to `canonical_pin_name` (`pre + zero-padded(3) digit suffix`; mixed/non-numeric returns the original). Same canonicalisation in `CheckIdentityCompatible`. **`pin_role` doesn't need canonicalisation** because `parse_pin` extracts `(col, row)` via `stoi` which already strips leading zeros.
**Subset wiring + NC backfill**: `CheckIdentityCompatible(a, b, info=&s)` accepts the case where one side's canonical pin set is a subset of the other's — typical when one importer drops NC pins (Altium) and the other doesn't (Mentor). It populates `info` with a non-fatal "N pin(s) only on '<part>'" message. Bidirectional mismatch (both sides have orphans) is still refused. After acceptance, `connect` calls `FillIdentityNCs(p1, p2)` which materialises the orphan canonical positions on the missing side as NC pins (`new Pin(other_side_name)`) — so `Connection::pin_map.size()` matches the larger side's count. Idempotent.
`screen_idx` mapping: 0 = main TUI, 1 = search, 2 = connect, 3 = set-type, 4 = explore. Adding a new screen mode is the same recipe each time: state members, `Container::Vertical` of focusable components, a `Renderer` lambda that recomputes derived state per frame (e.g. filtered part lists), an entry in `Container::Tab`, and Tab/Esc handling in the outer `CatchEvent`.
**Main-screen scrollback**: the visualisation area lets you scroll through past output. State is `Tui::scroll_offset` (0 = follow tail). Keys (default branch of the outer `CatchEvent` in `Run()`): `PageUp` / `PageDown` step 10 lines, `Home` jumps to top, `End` returns to tail. `Print()` resets `scroll_offset = 0` so any new output snaps the view back to the tail (otherwise late errors would be hidden). Render: instead of `focusPositionRelative(0, 1)` always anchoring to the bottom, the screen places `| focus` on `output[size - 1 - scroll_offset]` and shows a `[scroll: -N / PgUp PgDn Home End]` indicator next to the prompt when offset > 0. `vscroll_indicator | yframe` for the FTXUI-side scroll bar.
`connect` is dual-mode (`prompt_for_missing = false`):
- Inline: `connect m1 p1 m2 p2`. Each part argument is resolved as exact name first, then case-insensitive substring; ambiguous → lists candidates and aborts.
@@ -119,7 +145,8 @@ Each successful submission appends a single line to the file (so a crash doesn't
## Gotchas
- `System::Load` for `IMPORT_ALTIUM`: the corresponding constructor line is still commented out, so `imp` stays uninitialised → UB on `imp->parse(...)`. `IMPORT_MENTOR` and `IMPORT_ODS` are wired. Wrap calls in `try/catch` (the TUI does).
- All three importers (`IMPORT_MENTOR`, `IMPORT_ALTIUM`, `IMPORT_ODS`) are wired in `System::Load`. Wrap calls in `try/catch` (the TUI does).
- **Altium importer drops NC pins entirely**: the source format only enumerates pins inside `(signal …)` blocks, so positions not connected to any signal on this card never become `Pin`s. Mentor (via `Explicit Pin:`) and ODS (one row per pin) materialise NC. This is the asymmetry that motivates `FillIdentityNCs` at `connect` time and (eventually) `FillPartFromLayout` at `set-type` time.
- ODS importer: each spreadsheet sheet becomes a `Part` (sheet name = part name). Rows are pin/signal pairs; the **first non-empty row of each sheet is dropped as a header** (no validation of header content). Empty cells skip the row; `"NC"` keeps the pin in the part but doesn't connect it to a signal. Pins or parts whose name collides (rare in well-formed sheets) are silently dropped.
- `System::Load` throws `std::runtime_error("Unknown import type")` for any value outside the three enum cases.
- `Modules`/`Parts`/etc. have no const-correct iteration on the `*` accessor; iterators on `SystemElementContainer<T>` are available but `begin()`/`end()` are non-const-safe in some places.

View File

@@ -0,0 +1,93 @@
#include "import_altium.hpp"
#include "system/parts.hpp"
#include "system/pins.hpp"
#include "system/signals.hpp"
#include <cctype>
#include <string>
#include <vector>
namespace {
std::string strip(const std::string &s) {
size_t i = 0, j = s.size();
while (i < j && std::isspace((unsigned char)s[i])) ++i;
while (j > i && std::isspace((unsigned char)s[j - 1])) --j;
return s.substr(i, j - i);
}
} // namespace
ImportAltium::ImportAltium(std::string filename) : ImportBase(filename) {}
// Altium netlist text format:
// * Parts section: blocks delimited by `[` and `]`. First non-empty line
// inside a block is the part name.
// * Signals section: blocks delimited by `(` and `)`. First non-empty line
// is the signal name; subsequent lines are `partname-pinname` entries.
//
// Both sections may interleave; we make two passes — parts first so signals
// can resolve their part references.
void ImportAltium::parse(Signals *signals) {
std::vector<std::string> lines;
std::string raw;
while (std::getline(file_lines, raw)) lines.push_back(raw);
enum class State { Out, In };
// Pass 1: parts.
{
State sta = State::Out;
int lnum = 0;
for (const auto &l : lines) {
std::string t = strip(l);
if (t == "]") { sta = State::Out; continue; }
if (sta == State::Out) {
if (t == "[") { sta = State::In; lnum = 0; }
continue;
}
++lnum;
if (lnum == 1 && !t.empty() && !prts->exists(t)) {
prts->add(new Part(t));
}
}
}
// Pass 2: signals + pins.
{
State sta = State::Out;
int lnum = 0;
Signal *sig = nullptr;
for (const auto &l : lines) {
std::string t = strip(l);
if (t == ")") { sta = State::Out; sig = nullptr; continue; }
if (sta == State::Out) {
if (t == "(") { sta = State::In; lnum = 0; sig = nullptr; }
continue;
}
++lnum;
if (t.empty()) continue;
if (lnum == 1) {
sig = signals->merge(t);
continue;
}
if (!sig) continue;
// Split on first '-' so pin names containing dashes survive.
auto dash = t.find('-');
if (dash == std::string::npos) continue;
std::string pname = strip(t.substr(0, dash));
std::string pinname = strip(t.substr(dash + 1));
if (pname.empty() || pinname.empty()) continue;
Part *prt = nullptr;
try { prt = prts->get(pname); }
catch (...) { continue; }
if (prt->exists(pinname)) continue;
Pin *pin = new Pin(pinname);
try { prt->add(pin); }
catch (...) { delete pin; continue; }
sig->add(pin);
pin->connect(sig);
}
}
}

View File

@@ -0,0 +1,14 @@
#ifndef _IMPORT_ALTIUM_HPP_
#define _IMPORT_ALTIUM_HPP_
#include <string>
#include "import_base.hpp"
class ImportAltium : public ImportBase {
public:
ImportAltium(std::string filename);
void parse(Signals *signals) override;
};
#endif // _IMPORT_ALTIUM_HPP_

View File

@@ -0,0 +1,76 @@
#include "system/component_kind.hpp"
#include <cctype>
#include <string>
const char *component_kind_name(ComponentKind k) {
switch (k) {
case ComponentKind::Passive: return "passive";
case ComponentKind::Semiconductor: return "semiconductor";
case ComponentKind::IntegratedCircuit: return "ic";
case ComponentKind::Connector: return "connector";
case ComponentKind::TestPoint: return "testpoint";
case ComponentKind::Switch: return "switch";
case ComponentKind::Crystal: return "crystal";
case ComponentKind::Mechanical: return "mechanical";
case ComponentKind::Other: return "other";
}
return "other";
}
bool component_kind_from_name(const std::string &s, ComponentKind &out) {
std::string lo;
lo.reserve(s.size());
for (char c : s) lo += (char)std::tolower((unsigned char)c);
if (lo == "passive") { out = ComponentKind::Passive; return true; }
if (lo == "semiconductor" || lo == "semi") { out = ComponentKind::Semiconductor; return true; }
if (lo == "ic" || lo == "integratedcircuit") { out = ComponentKind::IntegratedCircuit; return true; }
if (lo == "connector" || lo == "conn") { out = ComponentKind::Connector; return true; }
if (lo == "testpoint" || lo == "tp") { out = ComponentKind::TestPoint; return true; }
if (lo == "switch" || lo == "sw") { out = ComponentKind::Switch; return true; }
if (lo == "crystal" || lo == "xtal" || lo == "y") { out = ComponentKind::Crystal; return true; }
if (lo == "mechanical" || lo == "mech") { out = ComponentKind::Mechanical; return true; }
if (lo == "other" || lo == "unknown") { out = ComponentKind::Other; return true; }
return false;
}
ComponentKind infer_component_kind(const std::string &part_name) {
if (part_name.empty()) return ComponentKind::Other;
// Extract the leading letter run (the reference-designator prefix).
std::string pre;
for (char c : part_name) {
if (std::isalpha((unsigned char)c)) pre += (char)std::toupper((unsigned char)c);
else break;
}
if (pre.empty()) return ComponentKind::Other;
// Multi-letter prefixes first (longest match).
if (pre == "LED") return ComponentKind::Semiconductor;
if (pre == "TP") return ComponentKind::TestPoint;
if (pre == "SW") return ComponentKind::Switch;
if (pre == "FB") return ComponentKind::Passive;
if (pre == "MK" || pre == "MP" || pre == "MH"
|| pre == "HS") return ComponentKind::Mechanical;
if (pre == "RA" || pre == "RN" || pre == "RP"
|| pre == "RV") return ComponentKind::Passive;
// Single-letter prefixes.
char c = pre[0];
switch (c) {
case 'R': case 'C': case 'L': case 'F':
return ComponentKind::Passive;
case 'D': case 'Q':
return ComponentKind::Semiconductor;
case 'U':
return ComponentKind::IntegratedCircuit;
case 'J': case 'P':
return ComponentKind::Connector;
case 'Y': case 'X':
return ComponentKind::Crystal;
case 'S':
return ComponentKind::Switch;
default:
return ComponentKind::Other;
}
}

View File

@@ -0,0 +1,26 @@
#ifndef _COMPONENT_KIND_HPP_
#define _COMPONENT_KIND_HPP_
#include <string>
// Reference-designator-derived component category.
// Inferred at Part construction from the leading letter(s) of the part name
// (e.g. "R12" → Passive, "U5" → IntegratedCircuit, "J1" → Connector).
// Preserved on save/restore as part of the part name (re-derived, not stored).
enum class ComponentKind {
Passive, // R, C, L, FB, F
Semiconductor, // D, LED, Q
IntegratedCircuit, // U
Connector, // J, P
TestPoint, // TP
Switch, // SW, S
Crystal, // Y, X
Mechanical, // MK, MP, MH, HS
Other, // unknown / unclassified
};
const char *component_kind_name(ComponentKind k);
bool component_kind_from_name(const std::string &s, ComponentKind &out);
ComponentKind infer_component_kind(const std::string &part_name);
#endif // _COMPONENT_KIND_HPP_

117
src/system/nets.cpp Normal file
View File

@@ -0,0 +1,117 @@
#include "system/nets.hpp"
#include "system/connect.hpp"
#include "system/modules.hpp"
#include "system/parts.hpp"
#include "system/pins.hpp"
#include "system/signals.hpp"
#include "system/system.hpp"
#include <queue>
#include <unordered_map>
#include <unordered_set>
namespace {
using SigKey = std::pair<Module *, Signal *>;
struct SigKeyHash {
size_t operator()(const SigKey &k) const noexcept {
return std::hash<void *>()(k.first) ^ (std::hash<void *>()(k.second) << 1);
}
};
std::unordered_map<Pin *, std::vector<Pin *>>
build_bridges(System *sys) {
std::unordered_map<Pin *, std::vector<Pin *>> br;
if (!sys || !sys->connections()) return br;
for (auto &kv : *sys->connections()) {
for (auto &wp : kv.second->pin_map) {
br[wp.first].push_back(wp.second);
br[wp.second].push_back(wp.first);
}
}
return br;
}
void bfs_net(const std::unordered_map<Pin *, std::vector<Pin *>> &bridges,
Module *start_m, Signal *start_s,
std::unordered_set<SigKey, SigKeyHash> &visited,
Net &out) {
if (!start_m || !start_s) return;
SigKey start{start_m, start_s};
if (!visited.insert(start).second) return;
std::queue<SigKey> q;
q.push(start);
while (!q.empty()) {
auto [m, s] = q.front();
q.pop();
out.members.emplace_back(m, s);
for (auto &pkv : *s) {
auto it = bridges.find(pkv.second);
if (it == bridges.end()) continue;
for (Pin *other : it->second) {
Signal *os = other->signal();
if (!os) continue;
Part *opart = other->prnt;
if (!opart) continue;
Module *om = opart->prnt;
if (!om) continue;
SigKey k{om, os};
if (visited.insert(k).second) q.push(k);
}
}
}
}
} // namespace
Net find_net(System *sys, Module *m, Signal *s) {
Net n;
if (!sys || !m || !s) return n;
auto bridges = build_bridges(sys);
std::unordered_set<SigKey, SigKeyHash> visited;
bfs_net(bridges, m, s, visited, n);
return n;
}
Net find_net(System *sys, Pin *pin) {
if (!sys || !pin || !pin->signal() || !pin->prnt) return {};
return find_net(sys, pin->prnt->prnt, pin->signal());
}
std::vector<Net> compute_all_nets(System *sys) {
std::vector<Net> nets;
if (!sys || !sys->modules()) return nets;
auto bridges = build_bridges(sys);
std::unordered_set<SigKey, SigKeyHash> visited;
for (auto &mkv : *sys->modules()) {
Module *m = mkv.second;
if (!m->signals) continue;
for (auto &skv : *m->signals) {
SigKey k{m, skv.second};
if (visited.count(k)) continue;
Net n;
bfs_net(bridges, m, skv.second, visited, n);
if (!n.members.empty()) nets.push_back(std::move(n));
}
}
return nets;
}
bool net_type_consistent(const Net &net, SignalType &dominant) {
bool seen_power = false, seen_gnd = false;
for (auto &mp : net.members) {
if (!mp.second) continue;
switch (mp.second->type) {
case SignalType::Power: seen_power = true; break;
case SignalType::GndShield: seen_gnd = true; break;
default: break;
}
}
if (seen_power && seen_gnd) { dominant = SignalType::Power; return false; }
dominant = seen_power ? SignalType::Power
: seen_gnd ? SignalType::GndShield
: SignalType::Other;
return true;
}

34
src/system/nets.hpp Normal file
View File

@@ -0,0 +1,34 @@
#ifndef _NETS_HPP_
#define _NETS_HPP_
#include "signal_type.hpp"
#include <utility>
#include <vector>
class System;
class Module;
class Signal;
class Pin;
// A net is the transitive closure of (Module, Signal) pairs linked through
// Connection::pin_map. Computed on demand; not persisted.
struct Net {
std::vector<std::pair<Module *, Signal *>> members;
};
// BFS the net containing (m, s). Returns an empty Net on null inputs.
Net find_net(System *sys, Module *m, Signal *s);
// Net containing the signal the pin is wired to. Empty if pin is NC.
Net find_net(System *sys, Pin *pin);
// All distinct nets; each (module, signal) appears in exactly one net.
// Singletons (signals not bridged anywhere) are included.
std::vector<Net> compute_all_nets(System *sys);
// Returns false if the net mixes Power and GndShield typed signals.
// `dominant` is set to Power, GndShield, or Other depending on the net's content.
bool net_type_consistent(const Net &net, SignalType &dominant);
#endif // _NETS_HPP_

View File

@@ -1,6 +1,10 @@
#include "parts.hpp"
Part::Part(std::string name) : SystemElementContainer<Pin>(name), prnt(nullptr), connector_type() {};
Part::Part(std::string name)
: SystemElementContainer<Pin>(name),
prnt(nullptr),
connector_type(),
kind(infer_component_kind(name)) {};
void Part::add(Pin *pin)
{

View File

@@ -1,6 +1,7 @@
#ifndef _PARTS_HPP_
#define _PARTS_HPP_
#include "component_kind.hpp"
#include "syselmts.hpp"
#include "pins.hpp"
@@ -14,6 +15,7 @@ public:
~Part();
Module *prnt; ///< Pointer to the parent module.
std::string connector_type; ///< Tag used by the transform registry; empty = untyped.
ComponentKind kind; ///< Inferred from the part name's reference-designator prefix.
void add(Pin *pin) override;
};

26
src/system/pin_name.cpp Normal file
View File

@@ -0,0 +1,26 @@
#include "system/pin_name.hpp"
#include <cstdio>
#include <stdexcept>
#include <string>
std::string canonical_pin_name(const std::string &name) {
if (name.empty()) return name;
std::string pre, suf;
for (char c : name) {
if (suf.empty() && !(c >= '0' && c <= '9')) pre += c;
else suf += c;
}
if (suf.empty()) return name;
for (char c : suf) {
if (!(c >= '0' && c <= '9')) return name;
}
try {
int n = std::stoi(suf);
char buf[16];
std::snprintf(buf, sizeof(buf), "%03d", n);
return pre + buf;
} catch (const std::exception &) {
return name;
}
}

25
src/system/pin_name.hpp Normal file
View File

@@ -0,0 +1,25 @@
#ifndef _PIN_NAME_HPP_
#define _PIN_NAME_HPP_
#include <string>
// Canonical form of a pin name for cross-card matching: <leading non-digits>
// + <pure-digit suffix zero-padded to width 3>. Returns the original name
// unchanged when no digit suffix is present, or when the suffix mixes digits
// with other characters.
//
// Examples:
// "A1" -> "A001"
// "A001" -> "A001"
// "AB12" -> "AB012"
// "12" -> "012"
// "VCC" -> "VCC"
// "A1B" -> "A1B" (mixed suffix, not canonicalised)
// "P3.3V" -> "P3.3V" (mixed suffix)
//
// Used by IdentityTransform and CheckIdentityCompatible to match pin names
// that differ only in zero-padding. Pin::name itself is preserved as-imported
// so the original schematic notation survives in the UI and on disk.
std::string canonical_pin_name(const std::string &name);
#endif // _PIN_NAME_HPP_

View File

@@ -1,8 +1,14 @@
#include "pin_role.hpp"
#include "parts.hpp"
#include "pin_name.hpp"
#include "pins.hpp"
#include <cctype>
#include <exception>
#include <string>
#include <unordered_set>
#include <vector>
// VPX 3U built-in pin role tables.
//
@@ -53,3 +59,30 @@ SignalType pin_role(const std::string &kind, const std::string &pin_name)
return SignalType::Other;
}
std::vector<std::string> pin_layout(const std::string &kind)
{
// TODO: enumerate the canonical pin set for known connector types,
// alongside `vpx_3u_role`. Empty for now — `FillPartFromLayout` becomes
// a no-op and the rest of the pipeline (verify, explore, identity wiring)
// works on whatever pins were imported.
(void)kind;
return {};
}
int FillPartFromLayout(Part *p, const std::string &kind)
{
if (!p) return 0;
auto layout = pin_layout(kind);
if (layout.empty()) return 0;
std::unordered_set<std::string> existing;
for (auto &kv : *p) existing.insert(canonical_pin_name(kv.first));
int added = 0;
for (const auto &name : layout) {
if (existing.count(canonical_pin_name(name))) continue;
if (p->exists(name)) continue;
p->add(new Pin(name));
++added;
}
return added;
}

View File

@@ -4,6 +4,9 @@
#include "signal_type.hpp"
#include <string>
#include <vector>
class Part;
// For a given connector type and pin position, return the expected SignalType
// (Power / GndShield / Other). Used at `set-type` to populate each pin's
@@ -15,4 +18,19 @@
SignalType pin_role(const std::string &connector_type,
const std::string &pin_name);
// Canonical full pin-name list for the connector type (e.g. for VPX 3U,
// every (col, row) position the connector physically has). Returns an empty
// vector for connector types that don't have a registered layout — callers
// must treat that as "unknown, do not auto-fill".
//
// Used at `set-type` to materialise NC pins for positions absent from the
// imported netlist (Altium drops NC, Mentor doesn't). Stub today: every
// known kind returns {} — populate alongside `vpx_3u_role`.
std::vector<std::string> pin_layout(const std::string &connector_type);
// For each canonical pin in `pin_layout(kind)` not already present on `p`,
// add a NC pin using the canonical name. Returns the number created.
// No-op when the layout is empty (unknown kind).
int FillPartFromLayout(Part *p, const std::string &connector_type);
#endif // _PIN_ROLE_HPP_

View File

@@ -3,6 +3,7 @@
#include "connect.hpp"
#include "modules.hpp"
#include "imports/import_altium.hpp"
#include "imports/import_mentor.hpp"
#include "imports/import_ods.hpp"
@@ -33,7 +34,7 @@ void System::Load(std::string module_name, std::string file_name, ImportType typ
imp = new ImportMentor(file_name);
} else if (type == ImportType::IMPORT_ALTIUM)
{
// imp = new ImportAltium(file_name);
imp = new ImportAltium(file_name);
} else if (type == ImportType::IMPORT_ODS)
{
imp = new ImportOds(file_name);

View File

@@ -1,10 +1,12 @@
#include "transform.hpp"
#include "parts.hpp"
#include "pin_name.hpp"
#include "pins.hpp"
#include "transform_vpx.hpp"
#include <set>
#include <unordered_map>
#include <vector>
#include <exception>
@@ -12,40 +14,83 @@
Transform::Transform(std::string name) : name(std::move(name)) {}
std::string CheckIdentityCompatible(const Part *a, const Part *b)
std::string CheckIdentityCompatible(const Part *a, const Part *b, std::string *info)
{
if (!a || !b) return "missing part";
// Compare on canonical names so that A1 ↔ A001 etc. count as the same pin.
std::set<std::string> a_pins, b_pins;
for (auto &kv : *a) a_pins.insert(kv.first);
for (auto &kv : *b) b_pins.insert(kv.first);
std::unordered_map<std::string, std::string> a_orig, b_orig;
for (auto &kv : *a) {
std::string c = canonical_pin_name(kv.first);
a_pins.insert(c);
a_orig.emplace(c, kv.first);
}
for (auto &kv : *b) {
std::string c = canonical_pin_name(kv.first);
b_pins.insert(c);
b_orig.emplace(c, kv.first);
}
if (a_pins == b_pins) return "";
std::vector<std::string> only_a, only_b;
for (const auto &n : a_pins) if (!b_pins.count(n)) only_a.push_back(n);
for (const auto &n : b_pins) if (!a_pins.count(n)) only_b.push_back(n);
for (const auto &n : a_pins) if (!b_pins.count(n)) only_a.push_back(a_orig[n]);
for (const auto &n : b_pins) if (!a_pins.count(n)) only_b.push_back(b_orig[n]);
std::string msg = "identity wiring requires same pin names on both sides";
if (!only_a.empty())
// True bidirectional mismatch — refuse.
if (!only_a.empty() && !only_b.empty()) {
std::string msg = "identity wiring requires the pin sets to be related";
msg += "; only on '" + a->name + "': "
+ std::to_string(only_a.size()) + " (e.g. " + only_a.front() + ")";
if (!only_b.empty())
msg += "; only on '" + b->name + "': "
+ std::to_string(only_b.size()) + " (e.g. " + only_b.front() + ")";
return msg;
return msg;
}
// One side is a (strict) subset of the other — accept, surface as info.
if (info) {
const auto &orphans = only_a.empty() ? only_b : only_a;
const std::string &side = only_a.empty() ? b->name : a->name;
*info = std::to_string(orphans.size()) + " pin(s) only on '" + side
+ "' (e.g. " + orphans.front() + ") — wiring intersection";
}
return "";
}
int FillIdentityNCs(Part *a, Part *b) {
if (!a || !b) return 0;
std::unordered_map<std::string, std::string> a_canon, b_canon;
for (auto &kv : *a) a_canon.emplace(canonical_pin_name(kv.first), kv.first);
for (auto &kv : *b) b_canon.emplace(canonical_pin_name(kv.first), kv.first);
auto fill = [](Part *dst,
const std::unordered_map<std::string, std::string> &src,
const std::unordered_map<std::string, std::string> &dst_canon) {
int n = 0;
for (const auto &kv : src) {
if (dst_canon.count(kv.first)) continue; // already there canonically
if (dst->exists(kv.second)) continue; // safety net for exotic clashes
dst->add(new Pin(kv.second));
++n;
}
return n;
};
int added = 0;
added += fill(a, b_canon, a_canon);
added += fill(b, a_canon, b_canon);
return added;
}
IdentityTransform::IdentityTransform() : Transform("identity") {}
std::vector<std::pair<Pin *, Pin *>> IdentityTransform::apply(Part *a, Part *b) const
{
// Match pins on canonical name so A1 (one card) wires to A001 (the other).
std::vector<std::pair<Pin *, Pin *>> out;
std::unordered_map<std::string, Pin *> b_canon;
for (auto &kv : *b) b_canon.emplace(canonical_pin_name(kv.first), kv.second);
for (auto &kv : *a) {
try {
Pin *pb = b->get(kv.first);
out.emplace_back(kv.second, pb);
} catch (const std::exception &) {
// No same-name pin on the other side — skip.
}
auto it = b_canon.find(canonical_pin_name(kv.first));
if (it != b_canon.end()) out.emplace_back(kv.second, it->second);
}
return out;
}

View File

@@ -9,12 +9,24 @@
class Part;
class Pin;
// A Transform describes how a connector pair maps pins between two Parts.
// Returning the list of (pin on side A, pin on side B) wired by this connection
// is enough to record the wiring on the Connection.
// Returns "" if a and b have identical pin name sets (so the identity
// fallback would wire every pin), otherwise a description of the mismatch.
std::string CheckIdentityCompatible(const Part *a, const Part *b);
// Returns "" when identity wiring is acceptable for the pair:
// * pin sets are identical (after canonicalisation), OR
// * one side's pin set is a subset of the other's (the typical case when a
// netlist format omits NC pins — Altium does, Mentor doesn't).
// Returns a non-empty error string only on a *true* bidirectional mismatch
// where both sides have pins absent from the other.
//
// On the subset path, when `info` is non-null, it is filled with a short
// human-readable description of the orphan pins on the larger side (count +
// example) so the caller can surface a non-fatal warning.
std::string CheckIdentityCompatible(const Part *a, const Part *b,
std::string *info = nullptr);
// For each canonical pin present on one side but missing on the other,
// materialise a NC pin (no signal) on the missing side, using the original
// pin name from the side that has it. Returns the number of pins created.
// Idempotent — subsequent calls add nothing.
int FillIdentityNCs(Part *a, Part *b);
class Transform
{

View File

@@ -3,6 +3,7 @@
#include "system/connect.hpp"
#include "system/modules.hpp"
#include "system/nets.hpp"
#include "system/parts.hpp"
#include "system/persist.hpp"
#include "system/pin_role.hpp"
@@ -32,7 +33,8 @@ void Tui::RegisterCommands() {
+ std::string(maxw - kv.first.size() + 2, ' ')
+ kv.second.description);
}
Print("Keys: Esc cancels a multi-step prompt; Tab completes commands or paths.");
Print("Keys: Esc cancels a multi-step prompt; Tab completes commands or paths;");
Print(" PageUp/PageDown scroll output (10 lines), Home/End jump to top/bottom.");
return;
}
const std::string &name = args[0];
@@ -66,8 +68,42 @@ void Tui::RegisterCommands() {
commands["new"] = { {}, [this](auto &) {
sys = std::make_unique<System>();
recorded.clear();
vars.clear();
Print("system created.");
}, true, "create a new (empty) system; resets the script-save buffer" };
}, true, "create a new (empty) system; resets the script-save buffer and $vars" };
commands["set"] = {
{{"name", Completion::None},
{"value", Completion::None}},
[this](const std::vector<std::string> &args) {
if (args.empty()) {
if (vars.empty()) { Print("(no variables defined)"); return; }
for (const auto &kv : vars)
Print(" $" + kv.first + " = " + kv.second);
return;
}
if (args.size() != 2) {
Print("usage: set <name> <value> (or no args to list)");
return;
}
const std::string &name = args[0];
if (name.empty()) { Print("set: empty name"); return; }
for (size_t i = 0; i < name.size(); ++i) {
char c = name[i];
bool ok = std::isalnum((unsigned char)c) || c == '_';
bool first_ok = i == 0 ? !std::isdigit((unsigned char)c) : true;
if (!ok || !first_ok) {
Print("set: invalid name '" + name
+ "' (must match [A-Za-z_][A-Za-z0-9_]*)");
return;
}
}
vars[name] = args[1];
},
/*prompt_for_missing=*/ false,
"define a $variable for substitution in subsequent commands "
"(no args = list defined vars)",
};
commands["load"] = {
{{"module name", Completion::None},
@@ -180,10 +216,60 @@ void Tui::RegisterCommands() {
}
}
}
Print("verify: " + std::to_string(mismatches) + " mismatch(es) over "
Print("verify: " + std::to_string(mismatches) + " local mismatch(es) over "
+ std::to_string(checked) + " typed pin(s).");
auto nets = compute_all_nets(sys.get());
int bridged = 0, inconsistent = 0;
for (const auto &n : nets) {
if (n.members.size() < 2) continue;
++bridged;
SignalType dom;
if (net_type_consistent(n, dom)) continue;
++inconsistent;
std::string line = " net mixes Power and GndShield:";
for (const auto &mp : n.members) {
line += " " + mp.first->name + "/" + mp.second->name
+ "(" + signal_type_name(mp.second->type) + ")";
}
Print(line);
}
Print("verify: " + std::to_string(inconsistent) + " inconsistent net(s) over "
+ std::to_string(bridged) + " bridged net(s) ("
+ std::to_string(nets.size()) + " total).");
}, true,
"check that each pin's connected signal matches its connector_type's expected role" };
"check pin roles locally and signal-type consistency across bridged nets" };
commands["net"] = {
{{"module", Completion::None},
{"signal name", Completion::None}},
[this](const std::vector<std::string> &args) {
if (!sys) { Print("no system: run 'new' first."); return; }
Module *mod;
try { mod = sys->modules()->get(args[0]); }
catch (const std::exception &) {
Print("unknown module: " + args[0]); return;
}
Signal *sig;
try { sig = mod->signals->get(args[1]); }
catch (const std::exception &) {
Print("unknown signal: " + mod->name + "/" + args[1]); return;
}
Net n = find_net(sys.get(), mod, sig);
SignalType dom;
bool ok = net_type_consistent(n, dom);
Print("net containing " + mod->name + "/" + sig->name
+ "" + std::to_string(n.members.size()) + " signal(s)"
+ (ok ? "" : " [INCONSISTENT]")
+ ", dominant: " + signal_type_name(dom));
for (const auto &mp : n.members) {
Print(" " + mp.first->name + "/" + mp.second->name
+ " (" + signal_type_name(mp.second->type) + ")");
}
},
/*prompt_for_missing=*/ true,
"show all signals reachable from <module>/<signal> through connections",
};
commands["set-signal-type"] = {
{{"module", Completion::None},
@@ -267,10 +353,14 @@ void Tui::RegisterCommands() {
return;
}
prt->connector_type = args[2];
int filled = FillPartFromLayout(prt, args[2]);
for (auto &kv : *prt)
kv.second->expected_signal_type = pin_role(args[2], kv.first);
Print(mod->name + "/" + prt->name + ": connector_type = "
+ (args[2].empty() ? "(none)" : args[2]));
if (filled > 0)
Print("set-type: materialised " + std::to_string(filled)
+ " NC pin(s) from connector layout");
},
/*prompt_for_missing=*/ false,
"tag a part's connector type for transform lookup",
@@ -376,11 +466,19 @@ void Tui::RegisterCommands() {
+ "'. Set matching types via 'set-type' first.");
return;
}
std::string err = CheckIdentityCompatible(p1, p2);
std::string info;
std::string err = CheckIdentityCompatible(p1, p2, &info);
if (!err.empty()) {
Print("connect refused: " + err);
return;
}
if (!info.empty()) {
int added = FillIdentityNCs(p1, p2);
Print("connect: " + info);
if (added > 0)
Print("connect: materialised " + std::to_string(added)
+ " NC pin(s) so both sides match");
}
}
auto pin_map = t->apply(p1, p2);
@@ -478,4 +576,58 @@ void Tui::RegisterCommands() {
/*prompt_for_missing=*/ false,
"list parts/signals matching a pattern (interactive screen if no args)",
};
commands["duplicate"] = {
{{"source module", Completion::None},
{"new module name", Completion::None}},
[this](const std::vector<std::string> &args) {
if (!sys) { Print("no system: run 'new' first."); return; }
Module *src;
try { src = sys->modules()->get(args[0]); }
catch (const std::exception &) {
Print("unknown module: " + args[0]); return;
}
if (sys->modules()->exists(args[1])) {
Print("duplicate refused: module '" + args[1] + "' already exists.");
return;
}
Module *dst = new Module(args[1]);
// 1. Copy signals (preserve type overrides).
for (auto &skv : *src->signals) {
Signal *ss = skv.second;
Signal *ds = new Signal(ss->name);
ds->type = ss->type;
dst->signals->add(ds);
}
// 2. Copy parts, pins, and re-wire pin→signal.
for (auto &pkv : *src) {
Part *sp = pkv.second;
Part *dp = new Part(sp->name);
dp->connector_type = sp->connector_type;
for (auto &nkv : *sp) {
Pin *sn = nkv.second;
Pin *dn = new Pin(sn->name);
dn->expected_signal_type = sn->expected_signal_type;
dp->add(dn);
if (sn->signal()) {
Signal *ds = dst->signals->get(sn->signal()->name);
ds->add(dn);
dn->connect(ds);
}
}
dst->add(dp);
}
sys->modules()->add(dst);
Print("duplicate: '" + args[0] + "' → '" + args[1] + "'"
+ " (" + std::to_string(dst->size()) + " part(s), "
+ std::to_string(dst->signals->size()) + " signal(s))");
},
/*prompt_for_missing=*/ true,
"clone a module under a new name (parts, pins, signals; no connections)",
};
}

View File

@@ -22,10 +22,21 @@ Component Tui::BuildMainScreen(ScreenInteractive &screen) {
return Renderer(input_component, [this, &screen, input_component] {
if (quit) screen.Exit();
// Clamp scroll offset to a meaningful range and pick the line to focus.
int n = (int)output.size();
if (scroll_offset < 0) scroll_offset = 0;
if (scroll_offset > n - 1) scroll_offset = std::max(0, n - 1);
int focus_idx = std::max(0, n - 1 - scroll_offset);
Elements lines;
for (const auto &l : output) lines.push_back(text(l));
lines.reserve(output.size());
for (int i = 0; i < n; ++i) {
auto el = text(output[i]);
if (i == focus_idx) el = el | focus;
lines.push_back(el);
}
auto view = vbox(std::move(lines))
| focusPositionRelative(0, 1)
| vscroll_indicator
| yframe
| flex;
@@ -33,10 +44,29 @@ Component Tui::BuildMainScreen(ScreenInteractive &screen) {
? "> "
: pending.front().question + "? ";
return vbox({
std::string status = scroll_offset > 0
? " [scroll: -" + std::to_string(scroll_offset)
+ " / PgUp PgDn Home End to navigate]"
: "";
auto base = vbox({
view,
separator(),
hbox({text(label), input_component->Render()}),
hbox({text(label), input_component->Render(), filler(), text(status) | dim}),
}) | border;
if (loading) {
int total = (int)loading_lines.size();
std::string progress = std::to_string(loading_executed) + " / "
+ std::to_string(total) + " lines";
auto modal = vbox({
text(" Computing… ") | bold | center,
separator(),
text(loading_filename) | center,
text(progress) | center,
}) | borderDouble | size(WIDTH, GREATER_THAN, 40);
return dbox({base, modal | center});
}
return base;
});
}

View File

@@ -2,14 +2,17 @@
#include "tui/tui_helpers.hpp"
#include <cctype>
#include <chrono>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <set>
#include <system_error>
#include <thread>
void Tui::Print(const std::string &line) {
output.push_back(line);
scroll_offset = 0; // any new line snaps the view back to the tail
}
void Tui::HistoryUp() {
@@ -109,6 +112,8 @@ void Tui::Dispatch(const std::string &raw) {
void Tui::Finalize(const std::string &name,
const CommandSpec &spec,
const std::vector<std::string> &args) {
// Build the canonical form from the *raw* args (pre-expansion) so that
// history and script-save preserve `$var` references.
std::string canonical = name;
for (const auto &a : args) {
if (a.find_first_of(" \t\"") != std::string::npos)
@@ -120,7 +125,12 @@ void Tui::Finalize(const std::string &name,
history.push_back(canonical);
AppendHistory(canonical);
}
spec.action(args);
// Expand variables only for the action call so commands see resolved values.
std::vector<std::string> exec_args;
exec_args.reserve(args.size());
for (const auto &a : args) exec_args.push_back(ExpandVars(a));
spec.action(exec_args);
static const std::set<std::string> no_record = {
"clear", "help", "quit", "exit", "source", "script-save",
@@ -128,6 +138,34 @@ void Tui::Finalize(const std::string &name,
if (spec.scriptable && !no_record.count(name)) recorded.push_back(canonical);
}
std::string Tui::ExpandVars(const std::string &s) const {
std::string out;
out.reserve(s.size());
size_t i = 0;
while (i < s.size()) {
if (s[i] != '$') { out.push_back(s[i++]); continue; }
size_t j = i + 1;
bool braces = (j < s.size() && s[j] == '{');
if (braces) ++j;
size_t start = j;
while (j < s.size() && (std::isalnum((unsigned char)s[j]) || s[j] == '_')) ++j;
std::string name = s.substr(start, j - start);
if (braces) {
if (j >= s.size() || s[j] != '}') {
// Unmatched brace — emit literally and resume after the '$'.
out.push_back('$'); ++i; continue;
}
++j;
}
if (name.empty()) { out.push_back('$'); ++i; continue; }
auto it = vars.find(name);
if (it != vars.end()) out += it->second;
else out += s.substr(i, j - i); // keep unknown as-is
i = j;
}
return out;
}
namespace {
std::filesystem::path HistoryPath() {
@@ -168,19 +206,59 @@ void Tui::Source(const std::string &filename) {
std::ifstream f(expanded);
if (!f) { Print("source failed: cannot open " + filename); return; }
bool prev = in_source;
in_source = true;
int executed = 0;
int lineno = 0;
bool aborted = false;
// Slurp the whole file so we can drive line-by-line processing from the
// event loop (one line per posted task). This lets the screen redraw
// between lines and surface the "Computing…" modal.
loading_lines.clear();
std::string line;
while (std::getline(f, line)) {
++lineno;
size_t start = line.find_first_not_of(" \t");
while (std::getline(f, line)) loading_lines.push_back(line);
loading_filename = filename;
loading_idx = 0;
loading_executed = 0;
loading_lineno = 0;
loading_prev_in_source = in_source;
in_source = true;
loading = true;
if (!screen_ptr) {
// Headless fallback (e.g. tests): drain synchronously.
while (loading.load()) ProcessNextSourceLine();
return;
}
// Pacing thread: post one tick at a time and wait for the main thread
// to ack it (by clearing tick_in_flight from ProcessNextSourceLine)
// before sleeping & posting the next. Without this, a long-running line
// (e.g. a Mentor parse) lets the ticker queue many ticks; FTXUI then
// drains them in a batch without redrawing between, so the modal
// counter freezes.
tick_in_flight.store(false);
std::thread([this]() {
using namespace std::chrono_literals;
while (loading.load()) {
// Wait until main thread is ready for a new tick.
while (loading.load() && tick_in_flight.load())
std::this_thread::sleep_for(5ms);
if (!loading.load()) break;
std::this_thread::sleep_for(30ms);
if (!loading.load()) break;
tick_in_flight.store(true);
if (screen_ptr)
screen_ptr->PostEvent(ftxui::Event::Special("\x02tick"));
}
}).detach();
}
void Tui::ProcessNextSourceLine() {
if (!loading.load()) return;
while (loading_idx < loading_lines.size()) {
const std::string &raw = loading_lines[loading_idx++];
++loading_lineno;
size_t start = raw.find_first_not_of(" \t");
if (start == std::string::npos) continue;
if (line[start] == '#') continue;
std::string trimmed = line.substr(start);
if (raw[start] == '#') continue;
std::string trimmed = raw.substr(start);
while (!trimmed.empty() && std::isspace((unsigned char)trimmed.back()))
trimmed.pop_back();
if (trimmed.empty()) continue;
@@ -188,21 +266,27 @@ void Tui::Source(const std::string &filename) {
input = trimmed;
cursor_pos = (int)input.size();
Submit();
++executed;
++loading_executed;
if (screen_idx != 0) {
Print("source: line " + std::to_string(lineno)
Print("source: line " + std::to_string(loading_lineno)
+ " is interactive (would open a screen) — aborting.");
screen_idx = 0;
aborted = true;
break;
loading.store(false);
tick_in_flight.store(false);
in_source = loading_prev_in_source;
return;
}
// One effective line per tick — ack so the ticker can pace the next.
tick_in_flight.store(false);
return;
}
in_source = prev;
if (!aborted)
Print("source: " + filename + " (" + std::to_string(executed) + " line(s))");
Print("source: " + loading_filename
+ " (" + std::to_string(loading_executed) + " line(s))");
loading.store(false);
tick_in_flight.store(false);
in_source = loading_prev_in_source;
}
void Tui::AppendHistory(const std::string &cmd) {

View File

@@ -9,7 +9,10 @@
using namespace ftxui;
Tui::Tui()
: cursor_pos(0), history_idx(-1), quit(false), in_source(false),
: cursor_pos(0), history_idx(-1), scroll_offset(0), quit(false), in_source(false),
loading(false), tick_in_flight(false),
loading_idx(0), loading_executed(0), loading_lineno(0),
loading_prev_in_source(false), screen_ptr(nullptr),
screen_idx(0),
search_types{"parts", "signals"},
search_module_idx(0), search_type_idx(0), search_focus_idx(0),
@@ -31,6 +34,7 @@ Tui::~Tui() = default;
void Tui::Run() {
auto screen = ScreenInteractive::Fullscreen();
screen_ptr = &screen;
auto main_screen = BuildMainScreen(screen);
auto search_screen = BuildSearchScreen();
@@ -69,7 +73,12 @@ void Tui::Run() {
return false;
default: // main
if (e == Event::Special("\x02tick")) { ProcessNextSourceLine(); return true; }
if (e == Event::Escape && !pending.empty()) { CancelPending(); return true; }
if (e == Event::PageUp) { scroll_offset += 10; return true; }
if (e == Event::PageDown) { scroll_offset = std::max(0, scroll_offset - 10); return true; }
if (e == Event::Home) { scroll_offset = (int)output.size(); return true; }
if (e == Event::End) { scroll_offset = 0; return true; }
if (e == Event::ArrowUp || e == Event::ArrowDown) {
if (pending.empty()) {
if (e == Event::ArrowUp) HistoryUp();

View File

@@ -1,6 +1,7 @@
#ifndef _TUI_HPP_
#define _TUI_HPP_
#include <atomic>
#include <deque>
#include <functional>
#include <map>
@@ -41,12 +42,14 @@ class Tui {
std::string input;
int cursor_pos;
int history_idx;
int scroll_offset; ///< Lines scrolled up from the tail; 0 = follow newest output.
bool quit;
bool in_source;
std::unique_ptr<System> sys;
std::deque<Prompt> pending;
std::map<std::string, CommandSpec> commands;
std::map<std::string, std::string> vars; ///< $var-style substitution table.
// ---- Screen orchestration ----
int screen_idx;
@@ -85,6 +88,17 @@ class Tui {
std::string explore_header;
int explore_focus_idx;
// ---- Source-file loading (event-driven, one line per tick) ----
std::atomic<bool> loading; ///< true while a script is being processed; read by tick thread.
std::atomic<bool> tick_in_flight; ///< main thread acks each tick by clearing this; ticker waits.
std::string loading_filename;
std::vector<std::string> loading_lines;
size_t loading_idx;
int loading_executed;
int loading_lineno;
bool loading_prev_in_source;
ftxui::ScreenInteractive *screen_ptr; ///< set in Run() so Source() can post events.
// ---- Set-type screen state ----
std::vector<std::string> settype_modules;
int settype_m_idx;
@@ -117,6 +131,8 @@ private:
void LoadHistory();
void AppendHistory(const std::string &cmd);
void Source(const std::string &filename);
void ProcessNextSourceLine();
std::string ExpandVars(const std::string &s) const;
// Completion (completion.cpp)
void CompleteCommand(size_t start = 0);

86
test/system.essim Normal file
View File

@@ -0,0 +1,86 @@
# essim system bring-up script.
new
# ---------------------------------------------------------------- variables
set test_dir /home/francois/Projets/essim_test
set bpb_nets $test_dir/BPB-2177-10222_NETLIST_3_1.qcv
set bkp_nets $test_dir/MERCVPX3UBPA_20221122.NET
set cb3p_nets $test_dir/CB3P-6359-10232_NETLIST_3_0.qcv
set vdn_nets $test_dir/VDN-2910.qcv
set cob_nets $test_dir/COB-2277_NETLIST_10211_2_0.qcv
set ssu_nets $test_dir/SSU-2134_PCB873.qcv
# ---------------------------------------------------------------- modules
load vdn1 $vdn_nets mentor
duplicate vdn1 vdn2
duplicate vdn1 vdn3
load bpb $bpb_nets mentor
load bkp $bkp_nets altium
load cb3p $cb3p_nets mentor
load cob $cob_nets mentor
load ssu $ssu_nets mentor
# ---------------------------------------------------------------- VPX tags
# Backplane payload-side connectors on BKP, one slot per (Jx0,Jx1,Jx2):
# J2x → VDN1, J3x → VDN2, J4x → VDN3, J5x → CB3P.
set-type bkp J20 vpx-3u-bkp-p0
set-type bkp J21 vpx-3u-bkp-p1
set-type bkp J22 vpx-3u-bkp-p2
set-type bkp J30 vpx-3u-bkp-p0
set-type bkp J31 vpx-3u-bkp-p1
set-type bkp J32 vpx-3u-bkp-p2
set-type bkp J40 vpx-3u-bkp-p0
set-type bkp J41 vpx-3u-bkp-p1
set-type bkp J42 vpx-3u-bkp-p2
set-type bkp J50 vpx-3u-bkp-p0
set-type bkp J51 vpx-3u-bkp-p1
set-type bkp J52 vpx-3u-bkp-p2
# Payload connectors on each plug-in card.
set-type vdn1 P0 vpx-3u-payload-p0
set-type vdn1 P1 vpx-3u-payload-p1
set-type vdn1 P2 vpx-3u-payload-p2
set-type vdn2 P0 vpx-3u-payload-p0
set-type vdn2 P1 vpx-3u-payload-p1
set-type vdn2 P2 vpx-3u-payload-p2
set-type vdn3 P0 vpx-3u-payload-p0
set-type vdn3 P1 vpx-3u-payload-p1
set-type vdn3 P2 vpx-3u-payload-p2
set-type cb3p P0 vpx-3u-payload-p0
set-type cb3p P1 vpx-3u-payload-p1
set-type cb3p P2 vpx-3u-payload-p2
# ---------------------------------------------------------------- VPX wiring
# Each connect dispatches via the registered vpx-3u transform.
connect bkp J20 vdn1 P0
connect bkp J21 vdn1 P1
connect bkp J22 vdn1 P2
connect bkp J30 vdn2 P0
connect bkp J31 vdn2 P1
connect bkp J32 vdn2 P2
connect bkp J40 vdn3 P0
connect bkp J41 vdn3 P1
connect bkp J42 vdn3 P2
connect bkp J50 cb3p P0
connect bkp J51 cb3p P1
connect bkp J52 cb3p P2
# ---------------------------------------------------------------- non-VPX
# Both ends untagged → IdentityTransform (matches by canonical pin name,
# so e.g. A1 ↔ A001 is paired thanks to canonical_pin_name).
connect cob P3 ssu P6
connect bkp J1 ssu P1
# BPB ↔ BKP
connect bkp P100 bpb J100
connect bkp P101 bpb J101
connect bkp P102 bpb J102
# BPB ↔ COB
connect bpb J0 cob P0
connect bpb J1 cob P1
connect bpb J2 cob P2

View File

@@ -0,0 +1,70 @@
#include <doctest/doctest.h>
#include "system/component_kind.hpp"
#include "system/parts.hpp"
#include <memory>
TEST_CASE("infer_component_kind: passives") {
CHECK(infer_component_kind("R12") == ComponentKind::Passive);
CHECK(infer_component_kind("C1") == ComponentKind::Passive);
CHECK(infer_component_kind("L4") == ComponentKind::Passive);
CHECK(infer_component_kind("FB1") == ComponentKind::Passive);
CHECK(infer_component_kind("RN2") == ComponentKind::Passive);
CHECK(infer_component_kind("F3") == ComponentKind::Passive); // fuse
}
TEST_CASE("infer_component_kind: semiconductors") {
CHECK(infer_component_kind("D1") == ComponentKind::Semiconductor);
CHECK(infer_component_kind("Q4") == ComponentKind::Semiconductor);
CHECK(infer_component_kind("LED2") == ComponentKind::Semiconductor);
}
TEST_CASE("infer_component_kind: ICs") {
CHECK(infer_component_kind("U1") == ComponentKind::IntegratedCircuit);
CHECK(infer_component_kind("U100") == ComponentKind::IntegratedCircuit);
}
TEST_CASE("infer_component_kind: connectors") {
CHECK(infer_component_kind("J1") == ComponentKind::Connector);
CHECK(infer_component_kind("P0") == ComponentKind::Connector);
CHECK(infer_component_kind("J100") == ComponentKind::Connector);
}
TEST_CASE("infer_component_kind: misc") {
CHECK(infer_component_kind("TP1") == ComponentKind::TestPoint);
CHECK(infer_component_kind("SW1") == ComponentKind::Switch);
CHECK(infer_component_kind("Y1") == ComponentKind::Crystal);
CHECK(infer_component_kind("X1") == ComponentKind::Crystal);
CHECK(infer_component_kind("MK1") == ComponentKind::Mechanical);
CHECK(infer_component_kind("HS2") == ComponentKind::Mechanical);
}
TEST_CASE("infer_component_kind: fallback") {
CHECK(infer_component_kind("") == ComponentKind::Other);
CHECK(infer_component_kind("123") == ComponentKind::Other); // no letter prefix
CHECK(infer_component_kind("ZZZ1") == ComponentKind::Other); // unknown prefix
}
TEST_CASE("infer_component_kind: case insensitive") {
CHECK(infer_component_kind("r1") == ComponentKind::Passive);
CHECK(infer_component_kind("u1") == ComponentKind::IntegratedCircuit);
}
TEST_CASE("Part ctor populates kind from name") {
auto r = std::make_unique<Part>("R12");
auto u = std::make_unique<Part>("U7");
auto j = std::make_unique<Part>("J1");
CHECK(r->kind == ComponentKind::Passive);
CHECK(u->kind == ComponentKind::IntegratedCircuit);
CHECK(j->kind == ComponentKind::Connector);
}
TEST_CASE("component_kind_from_name accepts canonical and aliases") {
ComponentKind k;
CHECK(component_kind_from_name("passive", k)); CHECK(k == ComponentKind::Passive);
CHECK(component_kind_from_name("ic", k)); CHECK(k == ComponentKind::IntegratedCircuit);
CHECK(component_kind_from_name("conn", k)); CHECK(k == ComponentKind::Connector);
CHECK(component_kind_from_name("Connector", k)); CHECK(k == ComponentKind::Connector);
CHECK(!component_kind_from_name("nonsense", k));
}

102
tests/test_pin_name.cpp Normal file
View File

@@ -0,0 +1,102 @@
#include <doctest/doctest.h>
#include "system/parts.hpp"
#include "system/pin_name.hpp"
#include "system/pins.hpp"
#include "system/transform.hpp"
#include <memory>
TEST_CASE("canonical_pin_name: zero-pads pure digit suffix to 3") {
CHECK(canonical_pin_name("A1") == "A001");
CHECK(canonical_pin_name("A001") == "A001");
CHECK(canonical_pin_name("AB12") == "AB012");
CHECK(canonical_pin_name("12") == "012");
CHECK(canonical_pin_name("J1") == "J001");
CHECK(canonical_pin_name("J999") == "J999");
CHECK(canonical_pin_name("J1000") == "J1000");
}
TEST_CASE("canonical_pin_name: leaves names without digit suffix unchanged") {
CHECK(canonical_pin_name("") == "");
CHECK(canonical_pin_name("VCC") == "VCC");
CHECK(canonical_pin_name("GND") == "GND");
}
TEST_CASE("canonical_pin_name: leaves mixed suffixes unchanged") {
CHECK(canonical_pin_name("A1B") == "A1B");
CHECK(canonical_pin_name("P3.3V") == "P3.3V");
CHECK(canonical_pin_name("D+") == "D+");
}
TEST_CASE("IdentityTransform pairs A1 ↔ A001 and reports compatible") {
auto a = std::make_unique<Part>("U_a");
auto b = std::make_unique<Part>("U_b");
Pin *a1 = new Pin("A1");
Pin *a2 = new Pin("A2");
Pin *b1 = new Pin("A001");
Pin *b2 = new Pin("A002");
a->add(a1); a->add(a2);
b->add(b1); b->add(b2);
CHECK(CheckIdentityCompatible(a.get(), b.get()) == "");
IdentityTransform t;
auto wires = t.apply(a.get(), b.get());
CHECK(wires.size() == 2);
// Build a quick set for order-independent verification.
bool saw_a1 = false, saw_a2 = false;
for (auto &w : wires) {
if (w.first == a1) { CHECK(w.second == b1); saw_a1 = true; }
if (w.first == a2) { CHECK(w.second == b2); saw_a2 = true; }
}
CHECK(saw_a1);
CHECK(saw_a2);
}
TEST_CASE("CheckIdentityCompatible accepts subset, surfaces orphans as info") {
auto a = std::make_unique<Part>("U_a");
auto b = std::make_unique<Part>("U_b");
a->add(new Pin("A1"));
a->add(new Pin("A2"));
b->add(new Pin("A001")); // canonical match for A1
// b is a strict subset of a — accept, info mentions A2 (orphan on a).
std::string info;
std::string err = CheckIdentityCompatible(a.get(), b.get(), &info);
CHECK(err.empty());
CHECK(!info.empty());
CHECK(info.find("A2") != std::string::npos);
CHECK(info.find("U_a") != std::string::npos);
}
TEST_CASE("FillIdentityNCs materialises the missing side") {
auto a = std::make_unique<Part>("U_a");
auto b = std::make_unique<Part>("U_b");
a->add(new Pin("A1"));
a->add(new Pin("A2"));
a->add(new Pin("A3"));
b->add(new Pin("A001"));
// b is a strict subset; A2/A3 missing → 2 created on b.
int added = FillIdentityNCs(a.get(), b.get());
CHECK(added == 2);
CHECK(b->size() == 3);
CHECK(b->exists("A2"));
CHECK(b->exists("A3"));
// Idempotent.
CHECK(FillIdentityNCs(a.get(), b.get()) == 0);
// Materialised pins are NC.
CHECK(b->get("A2")->signal() == nullptr);
}
TEST_CASE("CheckIdentityCompatible refuses bidirectional mismatch") {
auto a = std::make_unique<Part>("U_a");
auto b = std::make_unique<Part>("U_b");
a->add(new Pin("A1"));
a->add(new Pin("X9")); // only on a
b->add(new Pin("A001")); // canonical match
b->add(new Pin("Y7")); // only on b
std::string err = CheckIdentityCompatible(a.get(), b.get());
CHECK(!err.empty());
CHECK(err.find("X9") != std::string::npos);
CHECK(err.find("Y7") != std::string::npos);
}