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:
43
CLAUDE.md
43
CLAUDE.md
@@ -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.
|
||||
|
||||
93
src/imports/import_altium.cpp
Normal file
93
src/imports/import_altium.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/imports/import_altium.hpp
Normal file
14
src/imports/import_altium.hpp
Normal 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_
|
||||
76
src/system/component_kind.cpp
Normal file
76
src/system/component_kind.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
26
src/system/component_kind.hpp
Normal file
26
src/system/component_kind.hpp
Normal 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
117
src/system/nets.cpp
Normal 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
34
src/system/nets.hpp
Normal 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_
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
26
src/system/pin_name.cpp
Normal 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
25
src/system/pin_name.hpp
Normal 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_
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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_
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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)",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
86
test/system.essim
Normal 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
|
||||
70
tests/test_component_kind.cpp
Normal file
70
tests/test_component_kind.cpp
Normal 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
102
tests/test_pin_name.cpp
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user