From c3bb00cb4d418f09a30fba5394d76eb5b6d7a74f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Sat, 9 May 2026 20:28:21 +0200 Subject: [PATCH] Altium import, nets, canonical pins, component kinds, set/$var, scrollback, source modal. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 ` 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 ` 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 ` 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 --- CLAUDE.md | 43 +++++++-- src/imports/import_altium.cpp | 93 +++++++++++++++++++ src/imports/import_altium.hpp | 14 +++ src/system/component_kind.cpp | 76 ++++++++++++++++ src/system/component_kind.hpp | 26 ++++++ src/system/nets.cpp | 117 ++++++++++++++++++++++++ src/system/nets.hpp | 34 +++++++ src/system/parts.cpp | 6 +- src/system/parts.hpp | 2 + src/system/pin_name.cpp | 26 ++++++ src/system/pin_name.hpp | 25 ++++++ src/system/pin_role.cpp | 33 +++++++ src/system/pin_role.hpp | 18 ++++ src/system/system.cpp | 3 +- src/system/transform.cpp | 75 ++++++++++++---- src/system/transform.hpp | 24 +++-- src/tui/commands.cpp | 162 ++++++++++++++++++++++++++++++++-- src/tui/screen_main.cpp | 38 +++++++- src/tui/shell.cpp | 124 +++++++++++++++++++++----- src/tui/tui.cpp | 11 ++- src/tui/tui.hpp | 16 ++++ test/system.essim | 86 ++++++++++++++++++ tests/test_component_kind.cpp | 70 +++++++++++++++ tests/test_pin_name.cpp | 102 +++++++++++++++++++++ 24 files changed, 1163 insertions(+), 61 deletions(-) create mode 100644 src/imports/import_altium.cpp create mode 100644 src/imports/import_altium.hpp create mode 100644 src/system/component_kind.cpp create mode 100644 src/system/component_kind.hpp create mode 100644 src/system/nets.cpp create mode 100644 src/system/nets.hpp create mode 100644 src/system/pin_name.cpp create mode 100644 src/system/pin_name.hpp create mode 100644 test/system.essim create mode 100644 tests/test_component_kind.cpp create mode 100644 tests/test_pin_name.cpp diff --git a/CLAUDE.md b/CLAUDE.md index 11dbb3e..1f8e88b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,15 +23,25 @@ src/ system/ -- domain model syselmts.hpp SystemElement + SystemElementContainer (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` 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 ` 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 ` 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 ` 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 `. 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 ` 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` 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 ''" 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` are available but `begin()`/`end()` are non-const-safe in some places. diff --git a/src/imports/import_altium.cpp b/src/imports/import_altium.cpp new file mode 100644 index 0000000..f8f7521 --- /dev/null +++ b/src/imports/import_altium.cpp @@ -0,0 +1,93 @@ +#include "import_altium.hpp" + +#include "system/parts.hpp" +#include "system/pins.hpp" +#include "system/signals.hpp" + +#include +#include +#include + +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 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); + } + } +} diff --git a/src/imports/import_altium.hpp b/src/imports/import_altium.hpp new file mode 100644 index 0000000..f25b700 --- /dev/null +++ b/src/imports/import_altium.hpp @@ -0,0 +1,14 @@ +#ifndef _IMPORT_ALTIUM_HPP_ +#define _IMPORT_ALTIUM_HPP_ + +#include + +#include "import_base.hpp" + +class ImportAltium : public ImportBase { +public: + ImportAltium(std::string filename); + void parse(Signals *signals) override; +}; + +#endif // _IMPORT_ALTIUM_HPP_ diff --git a/src/system/component_kind.cpp b/src/system/component_kind.cpp new file mode 100644 index 0000000..e35da3c --- /dev/null +++ b/src/system/component_kind.cpp @@ -0,0 +1,76 @@ +#include "system/component_kind.hpp" + +#include +#include + +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; + } +} diff --git a/src/system/component_kind.hpp b/src/system/component_kind.hpp new file mode 100644 index 0000000..2afc3a9 --- /dev/null +++ b/src/system/component_kind.hpp @@ -0,0 +1,26 @@ +#ifndef _COMPONENT_KIND_HPP_ +#define _COMPONENT_KIND_HPP_ + +#include + +// 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_ diff --git a/src/system/nets.cpp b/src/system/nets.cpp new file mode 100644 index 0000000..f43d1e9 --- /dev/null +++ b/src/system/nets.cpp @@ -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 +#include +#include + +namespace { + +using SigKey = std::pair; + +struct SigKeyHash { + size_t operator()(const SigKey &k) const noexcept { + return std::hash()(k.first) ^ (std::hash()(k.second) << 1); + } +}; + +std::unordered_map> +build_bridges(System *sys) { + std::unordered_map> 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> &bridges, + Module *start_m, Signal *start_s, + std::unordered_set &visited, + Net &out) { + if (!start_m || !start_s) return; + SigKey start{start_m, start_s}; + if (!visited.insert(start).second) return; + std::queue 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 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 compute_all_nets(System *sys) { + std::vector nets; + if (!sys || !sys->modules()) return nets; + auto bridges = build_bridges(sys); + std::unordered_set 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; +} diff --git a/src/system/nets.hpp b/src/system/nets.hpp new file mode 100644 index 0000000..8bf4677 --- /dev/null +++ b/src/system/nets.hpp @@ -0,0 +1,34 @@ +#ifndef _NETS_HPP_ +#define _NETS_HPP_ + +#include "signal_type.hpp" + +#include +#include + +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> 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 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_ diff --git a/src/system/parts.cpp b/src/system/parts.cpp index dad7c51..4524c09 100644 --- a/src/system/parts.cpp +++ b/src/system/parts.cpp @@ -1,6 +1,10 @@ #include "parts.hpp" -Part::Part(std::string name) : SystemElementContainer(name), prnt(nullptr), connector_type() {}; +Part::Part(std::string name) + : SystemElementContainer(name), + prnt(nullptr), + connector_type(), + kind(infer_component_kind(name)) {}; void Part::add(Pin *pin) { diff --git a/src/system/parts.hpp b/src/system/parts.hpp index 5ece419..4f301e2 100644 --- a/src/system/parts.hpp +++ b/src/system/parts.hpp @@ -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; }; diff --git a/src/system/pin_name.cpp b/src/system/pin_name.cpp new file mode 100644 index 0000000..d50da1b --- /dev/null +++ b/src/system/pin_name.cpp @@ -0,0 +1,26 @@ +#include "system/pin_name.hpp" + +#include +#include +#include + +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; + } +} diff --git a/src/system/pin_name.hpp b/src/system/pin_name.hpp new file mode 100644 index 0000000..bed9290 --- /dev/null +++ b/src/system/pin_name.hpp @@ -0,0 +1,25 @@ +#ifndef _PIN_NAME_HPP_ +#define _PIN_NAME_HPP_ + +#include + +// Canonical form of a pin name for cross-card matching: +// + . 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_ diff --git a/src/system/pin_role.cpp b/src/system/pin_role.cpp index 9491484..576aa81 100644 --- a/src/system/pin_role.cpp +++ b/src/system/pin_role.cpp @@ -1,8 +1,14 @@ #include "pin_role.hpp" +#include "parts.hpp" +#include "pin_name.hpp" +#include "pins.hpp" + #include #include #include +#include +#include // 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 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 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; +} diff --git a/src/system/pin_role.hpp b/src/system/pin_role.hpp index aa9017b..ce0e0e4 100644 --- a/src/system/pin_role.hpp +++ b/src/system/pin_role.hpp @@ -4,6 +4,9 @@ #include "signal_type.hpp" #include +#include + +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 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_ diff --git a/src/system/system.cpp b/src/system/system.cpp index 4183ae1..923f01f 100644 --- a/src/system/system.cpp +++ b/src/system/system.cpp @@ -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); diff --git a/src/system/transform.cpp b/src/system/transform.cpp index e48840a..552b0a5 100644 --- a/src/system/transform.cpp +++ b/src/system/transform.cpp @@ -1,10 +1,12 @@ #include "transform.hpp" #include "parts.hpp" +#include "pin_name.hpp" #include "pins.hpp" #include "transform_vpx.hpp" #include +#include #include #include @@ -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 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 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 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 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 &src, + const std::unordered_map &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> IdentityTransform::apply(Part *a, Part *b) const { + // Match pins on canonical name so A1 (one card) wires to A001 (the other). std::vector> out; + std::unordered_map 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; } diff --git a/src/system/transform.hpp b/src/system/transform.hpp index a9a9a28..c848c86 100644 --- a/src/system/transform.hpp +++ b/src/system/transform.hpp @@ -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 { diff --git a/src/tui/commands.cpp b/src/tui/commands.cpp index 9cfead6..856b298 100644 --- a/src/tui/commands.cpp +++ b/src/tui/commands.cpp @@ -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(); 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 &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 (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 &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 / 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 &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)", + }; } diff --git a/src/tui/screen_main.cpp b/src/tui/screen_main.cpp index 9788122..2007954 100644 --- a/src/tui/screen_main.cpp +++ b/src/tui/screen_main.cpp @@ -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; }); } diff --git a/src/tui/shell.cpp b/src/tui/shell.cpp index adebc32..8dcc9b9 100644 --- a/src/tui/shell.cpp +++ b/src/tui/shell.cpp @@ -2,14 +2,17 @@ #include "tui/tui_helpers.hpp" #include +#include #include #include #include #include #include +#include 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 &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 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 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) { diff --git a/src/tui/tui.cpp b/src/tui/tui.cpp index 4e5f453..b6f5a90 100644 --- a/src/tui/tui.cpp +++ b/src/tui/tui.cpp @@ -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(); diff --git a/src/tui/tui.hpp b/src/tui/tui.hpp index 71eee48..43bfaae 100644 --- a/src/tui/tui.hpp +++ b/src/tui/tui.hpp @@ -1,6 +1,7 @@ #ifndef _TUI_HPP_ #define _TUI_HPP_ +#include #include #include #include @@ -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 sys; std::deque pending; std::map commands; + std::map 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 loading; ///< true while a script is being processed; read by tick thread. + std::atomic tick_in_flight; ///< main thread acks each tick by clearing this; ticker waits. + std::string loading_filename; + std::vector 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 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); diff --git a/test/system.essim b/test/system.essim new file mode 100644 index 0000000..810e17c --- /dev/null +++ b/test/system.essim @@ -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 diff --git a/tests/test_component_kind.cpp b/tests/test_component_kind.cpp new file mode 100644 index 0000000..70779aa --- /dev/null +++ b/tests/test_component_kind.cpp @@ -0,0 +1,70 @@ +#include + +#include "system/component_kind.hpp" +#include "system/parts.hpp" + +#include + +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("R12"); + auto u = std::make_unique("U7"); + auto j = std::make_unique("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)); +} diff --git a/tests/test_pin_name.cpp b/tests/test_pin_name.cpp new file mode 100644 index 0000000..9be5099 --- /dev/null +++ b/tests/test_pin_name.cpp @@ -0,0 +1,102 @@ +#include + +#include "system/parts.hpp" +#include "system/pin_name.hpp" +#include "system/pins.hpp" +#include "system/transform.hpp" + +#include + +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("U_a"); + auto b = std::make_unique("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("U_a"); + auto b = std::make_unique("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("U_a"); + auto b = std::make_unique("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("U_a"); + auto b = std::make_unique("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); +}