Compare commits
29 Commits
separate-c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| dceb61237d | |||
| 0b10e1c1b7 | |||
| 9cf43696a2 | |||
| e914c84c18 | |||
| 1943f1f88a | |||
| c2b1f4c4ae | |||
| 794430e86c | |||
| a9039a8eea | |||
| b0e260a2ec | |||
| fc71cce647 | |||
| 184b0d306f | |||
| d4eac9557b | |||
| 19dbec9672 | |||
| fc3ef333fa | |||
| b999446151 | |||
| 7e88f82446 | |||
| 76807b0307 | |||
| 4803d7d01c | |||
| e561c0f960 | |||
| 091ef6fe4b | |||
| 3b6e626c8f | |||
| af36f7c150 | |||
| 0517a82a5c | |||
| 4ef110ab70 | |||
| b36af3167a | |||
| a040cc1957 | |||
| 25939998ab | |||
| e3350b8d95 | |||
| cccc5f131d |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1 +1 @@
|
||||
build/
|
||||
build*/
|
||||
|
||||
@@ -11,6 +11,10 @@ project(essim
|
||||
|
||||
include(FetchContent)
|
||||
|
||||
# Shared CMake helpers (essim_add_frontend — per-frontend target boilerplate).
|
||||
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
|
||||
include(EssimFrontend)
|
||||
|
||||
# ----------------------------------------------------------------- core deps
|
||||
# libbsdl — standalone BSDL parser (LGPL-2.1), dynamically linked (EUPL-1.2,
|
||||
# which the LGPL permits). Override its path with -DBSDL_DIR=...
|
||||
@@ -44,12 +48,18 @@ target_link_libraries(essim_core
|
||||
# src/frontends/gui/ and configure with -DESSIM_FRONTEND=gui.
|
||||
set(ESSIM_FRONTEND "tui" CACHE STRING
|
||||
"Frontend to build: a directory name under src/frontends/, or 'none'")
|
||||
set_property(CACHE ESSIM_FRONTEND PROPERTY STRINGS tui none)
|
||||
set_property(CACHE ESSIM_FRONTEND PROPERTY STRINGS tui wx none)
|
||||
|
||||
if(ESSIM_FRONTEND STREQUAL "none")
|
||||
message(STATUS "essim: ESSIM_FRONTEND=none — core + tests only (no frontend, no GUI toolkit)")
|
||||
elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/frontends/${ESSIM_FRONTEND}/CMakeLists.txt")
|
||||
message(STATUS "essim: building frontend '${ESSIM_FRONTEND}'")
|
||||
# Shared, GUI-toolkit-free frontend support: the abstract Frontend interface
|
||||
# (header-only) and the frontend-agnostic launcher frontend_main(). Every
|
||||
# frontend's main() links this and forwards argv to it.
|
||||
add_library(essim_frontend STATIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/frontends/frontend_main.cpp")
|
||||
target_include_directories(essim_frontend PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src)
|
||||
add_subdirectory(src/frontends/${ESSIM_FRONTEND})
|
||||
else()
|
||||
message(FATAL_ERROR
|
||||
|
||||
176
DESIGN.md
176
DESIGN.md
@@ -10,72 +10,120 @@ cmake --build build -j
|
||||
./build/essim
|
||||
```
|
||||
|
||||
- CMake **3.14+** required (uses `FetchContent_MakeAvailable`).
|
||||
- FTXUI is fetched at configure time from GitHub (`v6.1.9`, shallow clone). First configure pays ~20 s for the clone; subsequent ones are cached in `build/_deps/`.
|
||||
- **System dependencies** (resolved via `find_package`): `libzip` (target `libzip::zip`) and `pugixml` (target `pugixml::pugixml`). Used by the ODS importer. Arch: `pacman -S libzip pugixml`; Debian/Ubuntu: `libzip-dev libpugixml-dev`.
|
||||
- **`libbsdl`** (standalone BSDL parser, LGPL-2.1) is the sibling repo at `../libbsdl`, pulled in via `add_subdirectory` (path overridable with `-DBSDL_DIR=...`) and linked dynamically (`bsdl::bsdl`; an LGPL `.so` is fine from EUPL essim). Powers the BSDL ingest behind `attach-bsdl`.
|
||||
- Sources are collected with `file(GLOB_RECURSE ALL_SOURCES "src/*.cpp")`. **After adding a new `.cpp`, re-run `cmake -S . -B build`** — CMake does not re-glob automatically and link will fail with "undefined reference".
|
||||
- CMake **3.14+** (uses `FetchContent_MakeAvailable`).
|
||||
- **Layered build** (see *Architecture* below). `essim_core` is the
|
||||
frontend-agnostic business library; a frontend under `src/frontends/<name>/`
|
||||
links it and produces the `essim` binary. Choose it with
|
||||
`-DESSIM_FRONTEND=<name>` (default `tui`). **`-DESSIM_FRONTEND=none` builds the
|
||||
core + tests only — no GUI toolkit is fetched.**
|
||||
- **Core system dependencies** (via `find_package`): `libzip` (`libzip::zip`)
|
||||
and `pugixml` (`pugixml::pugixml`) for the ODS importer. Arch:
|
||||
`pacman -S libzip pugixml`; Debian/Ubuntu: `libzip-dev libpugixml-dev`.
|
||||
- **`libbsdl`** (standalone BSDL parser, LGPL-2.1) — sibling repo at
|
||||
`../libbsdl`, `add_subdirectory` (override `-DBSDL_DIR=...`), linked
|
||||
dynamically **into the core** (`bsdl::bsdl`).
|
||||
- **FTXUI** is fetched by the **tui frontend only**
|
||||
(`src/frontends/tui/CMakeLists.txt`), never by the core.
|
||||
- Sources are globbed per layer: `src/core/*.cpp` → `essim_core`,
|
||||
`src/frontends/<fe>/*.cpp` → that frontend's lib + the `essim` binary.
|
||||
**After adding a `.cpp`, re-run `cmake -S . -B build`** — CMake doesn't re-glob.
|
||||
- **Tests** are split: `essim_tests` links `essim_core` (no FTXUI) from
|
||||
`tests/*.cpp`; per-frontend tests like `essim_tui_tests` link `essim_tui` from
|
||||
`tests/<frontend>/*.cpp`.
|
||||
- **Headless / batch**: `essim --batch --source FILE` runs a script and prints its console output to stdout, then exits without the TUI (good for CI / capturing `verify`). Also `--restore FILE` and `--commands-md [FILE]`. `BootDispatch` runs `--restore`/`--source` synchronously before the event loop (`Source` takes its headless drain branch when no screen is attached), so the console buffer is complete by the time `--batch` dumps it (`Tui::DumpOutput`).
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
src/
|
||||
main.cpp -- launches Tui
|
||||
system/ -- domain model
|
||||
syselmts.hpp SystemElement + SystemElementContainer<T> (templated, get/merge/iterate)
|
||||
core/ -- business logic; NO GUI toolkit (builds libessim_core)
|
||||
domain/ -- the model + read-only analyses
|
||||
syselmts.hpp SystemElement + SystemElementContainer<T> (get/merge/iterate)
|
||||
modules.{hpp,cpp} Module, Modules
|
||||
parts.{hpp,cpp} Part (carries `kind` + `connector_type`), Parts
|
||||
parts.{hpp,cpp} Part (kind, connector_type, bsdl_path; PinSpec per pin)
|
||||
pins.{hpp,cpp} Pin (carries PinSpec `spec`), Pins
|
||||
signals.{hpp,cpp} Signal, Signals
|
||||
signal_type.hpp SignalType + helpers
|
||||
pin_spec.hpp PinSpec (function/direction/pad/source) + SignalType mapping
|
||||
component_kind.{hpp,cpp} ComponentKind enum + infer_component_kind(name)
|
||||
pin_name.{hpp,cpp} canonical_pin_name(s) — zero-pad digit suffix to 3
|
||||
pin_spec.hpp PinSpec (function/direction/pad/source), mappings, spec_source_rank
|
||||
component_kind.{hpp,cpp} ComponentKind + infer_component_kind(name)
|
||||
pin_name.{hpp,cpp} canonical_pin_name (zero-pad digit suffix to 3)
|
||||
connect.{hpp,cpp} Connection, Connections
|
||||
transform.{hpp,cpp} Transform / IdentityTransform / TransformRegistry +
|
||||
CheckIdentityCompatible + FillIdentityNCs
|
||||
pin_role.{hpp,cpp} pin_role(kind, name) → PinSpec, pin_layout(kind),
|
||||
FillPartFromLayout(part, kind)
|
||||
transform*.{hpp,cpp} Transform / IdentityTransform / TransformRegistry, VPX transform
|
||||
pin_role.{hpp,cpp} pin_role(kind,name) -> PinSpec, pin_layout, FillPartFromLayout
|
||||
pin_model.{hpp,cpp} PinModel + apply_model(Part*, model) + ConnectorModel
|
||||
bsdl_model.{hpp,cpp} BsdlModel (libbsdl wrapper) + BsdlPinModel + apply_bsdl
|
||||
bsdl_check.{hpp,cpp} check_pin_specs / check_jtag_chain → vector<Anomaly>
|
||||
bsdl_check.{hpp,cpp} check_pin_specs / _jtag_chain / _source_conflicts / _bsdl_completeness
|
||||
nets.{hpp,cpp} find_net / compute_all_nets / net_type_consistent
|
||||
analysis.{hpp,cpp} analyze_system → AnalysisReport (diff pairs, buses, anomalies)
|
||||
persist.{hpp,cpp} save / restore (tab-delimited)
|
||||
system.{hpp,cpp} System: owns Modules + Connections, exposes Load()
|
||||
imports/ -- adapters that populate or emit the domain
|
||||
import_base.hpp ImportBase interface
|
||||
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)
|
||||
ods_writer.{hpp,cpp} Minimal .ods writer (multi-sheet, string cells)
|
||||
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
|
||||
shell.cpp Print, Submit, Dispatch, Finalize, Tokenize, history persistence
|
||||
completion.cpp CompleteCommand, CompletePath, CompleteInline
|
||||
commands.cpp RegisterCommands (orchestrator + lifecycle / shell / topology commands)
|
||||
commands_export.cpp RegisterExportCommands (export → CSV / ODS, file-dialog hook)
|
||||
screen_main.cpp BuildMainScreen (visualisation area + bottom input)
|
||||
screen_connect.cpp BuildConnectScreen + shared RefreshFilteredPartList helper
|
||||
screen_settype.cpp BuildSettypeScreen
|
||||
screen_explore.cpp BuildExploreScreen (browse modules → parts/signals/connections → details; not scriptable)
|
||||
screen_dashboard.cpp BuildDashboardScreen (read-only system overview)
|
||||
screen_analyze.cpp BuildAnalyzeScreen (anomalies / groups / power decisions)
|
||||
screen_palette.cpp BuildPaletteModal (global Ctrl-P fuzzy launcher)
|
||||
screen_filedialog.cpp BuildFileDialog + OpenFileDialog (reusable file picker)
|
||||
screen_error.cpp BuildErrorModal + ShowError (centred error popup)
|
||||
screen_help.cpp BuildHelpScreen (topic-driven feature reference)
|
||||
screen_sigtype_modal.cpp BuildSignalTypeModal (popup attached to explore via Modal())
|
||||
doc/classes.puml -- PlantUML class diagram
|
||||
analysis.{hpp,cpp} analyze_system -> AnalysisReport (diff pairs, buses, anomalies)
|
||||
persist.{hpp,cpp} save / restore (tab-delimited; `B` tag = bsdl_path)
|
||||
system.{hpp,cpp} System: owns Modules + Connections, Load()
|
||||
imports/ -- netlist / pinout adapters
|
||||
import_base.hpp / import_{mentor,altium,ods}.{hpp,cpp} / ods_writer.{hpp,cpp}
|
||||
app/ -- application operations (UI-independent use cases)
|
||||
export.{hpp,cpp} export_connections(System*, path, format) -> ExportResult
|
||||
verify.{hpp,cpp} verify(System*) -> VerifyReport (the 7 verify passes)
|
||||
connect.{hpp,cpp} connect_parts(System*, m1,p1, m2,p2) -> ConnectResult
|
||||
load.{hpp,cpp} load_module(System*, name, path, ImportType) -> LoadResult
|
||||
frontends/ -- one directory per GUI/TUI engine; each links essim_core
|
||||
frontend.hpp -- abstract Frontend interface (BootDispatch/Dump*/Run)
|
||||
frontend_main.{hpp,cpp} -- frontend_main(argc,argv,Frontend&): argv + boot/batch/run
|
||||
tui/ -- FTXUI shell (builds libessim_tui + the `essim` binary)
|
||||
CMakeLists.txt fetches FTXUI; builds essim_tui + essim
|
||||
main.cpp entry point: construct Tui, call frontend_main
|
||||
tui.{hpp,cpp} class Tui (state + Run() + screen-mode event dispatch)
|
||||
tui_helpers.{hpp,cpp} ToLower, NaturalLess, RenderHelpPanel
|
||||
shell.cpp Print, Submit, Dispatch, Source / ProcessNextSourceLine
|
||||
completion.cpp CompleteCommand / CompletePath / CompleteInline
|
||||
commands.cpp RegisterCommands (thin: resolve args -> call core -> render)
|
||||
commands_export.cpp thin wrapper over app::export_connections
|
||||
screen_*.cpp dashboard, connect, settype, explore, analyze, help, main
|
||||
(console), palette, file dialog, error/confirm, sigtype modal
|
||||
wx/ -- wxWidgets GUI (builds libessim_wx + the `essim` binary)
|
||||
CMakeLists.txt find_package(wxWidgets) + essim_add_frontend(wx ...)
|
||||
main.cpp entry point: construct WxFrontend, call frontend_main
|
||||
wx_frontend.{hpp,cpp} WxFrontend : Frontend (owns System; boots wx in Run())
|
||||
wx_frame.{hpp,cpp} EssimFrame: menu/tree/overview/log over core + app::*
|
||||
cmake/EssimFrontend.cmake -- essim_add_frontend(name LIBS ...) per-frontend wiring
|
||||
tests/ -- core tests (link essim_core)
|
||||
tui/ -- frontend tests (link essim_tui)
|
||||
doc/ , test/ -- docs; sample netlists + system.essim bring-up script
|
||||
```
|
||||
|
||||
`include/` and `lib/` are kept empty by design — FTXUI used to live there as precompiled `.a` + headers, now it comes through FetchContent.
|
||||
## Architecture — core vs frontends
|
||||
|
||||
The hard rule: **`src/core/` never depends on a frontend** — no `#include
|
||||
"frontends/…"`, no GUI toolkit. Frontends depend on the core, never the reverse
|
||||
(`essim_core` links libzip / pugixml / bsdl only).
|
||||
|
||||
- **Domain** (`core/domain/`) — the model and the read-only analyses
|
||||
(`analyze_system`, the `check_*` passes, `compute_all_nets`).
|
||||
- **Application** (`core/app/`) — use-case operations a frontend invokes, e.g.
|
||||
`export_connections(System*, path, format) -> ExportResult`. An operation
|
||||
builds its artefact and returns data/stats; it **never** prints or opens a
|
||||
dialog. (Anti-pattern being removed: the export command used to build the file
|
||||
inside its lambda. The TUI command is now a thin wrapper — resolve args/dialog
|
||||
→ call the core op → render the result.)
|
||||
- **Frontends** (`frontends/<name>/`) — thin: map UI events to core calls and
|
||||
render results. Each implements the **`Frontend`** interface
|
||||
(`frontends/frontend.hpp`: `BootDispatch`, `DumpCommandsMd`, `DumpOutput`,
|
||||
`Run`). The process entry is shared and frontend-agnostic:
|
||||
`frontend_main(argc, argv, Frontend&)` (`frontends/frontend_main.cpp`, built
|
||||
into the toolkit-free `essim_frontend` lib) parses the CLI flags and drives the
|
||||
boot → batch/run flow through the interface; a frontend's `main()` is just
|
||||
*construct the concrete Frontend, call `frontend_main`*. Two frontends ship
|
||||
today: **tui** (FTXUI, default) and **wx** (a wxWidgets GUI, menu-driven over
|
||||
`app::*`). Add another by creating `src/frontends/<name>/CMakeLists.txt` — its
|
||||
toolkit setup (FetchContent / find_package) plus one call to
|
||||
`essim_add_frontend(<name> LIBS …)` (the shared helper in
|
||||
`cmake/EssimFrontend.cmake` that builds `essim_<name>` + the `essim` binary) —
|
||||
and configuring `-DESSIM_FRONTEND=<name>`.
|
||||
|
||||
Because the core links no toolkit, the suite links `essim_core` directly and
|
||||
`-DESSIM_FRONTEND=none` builds + tests the whole core with FTXUI never fetched.
|
||||
|
||||
## Domain conventions
|
||||
|
||||
- Everything in `system/` is **pointer-based** (commit `d8122d1`: "everything is pointer"). Containers store `T*`, ownership lives with the container.
|
||||
- Everything in `core/domain/` is **pointer-based** (commit `d8122d1`: "everything is pointer"). Containers store `T*`, ownership lives with the container.
|
||||
- `SystemElementContainer<T>::merge(name)` is the get-or-create primitive — call it instead of `add` when you don't know whether the element already exists. `add` throws on duplicate names or empty names.
|
||||
- `using namespace std;` is present in `syselmts.hpp` — pre-existing, don't add more `using namespace` in headers.
|
||||
- Include guards `_NAME_HPP_` *and* `#pragma once` are both used. Match the existing style.
|
||||
@@ -110,11 +158,11 @@ Built-in commands: `new`, `set`, `load`, `duplicate`, `save`, `restore`, `source
|
||||
|
||||
Pending prompts (from incomplete inline commands) are NOT considered interactive and are filled by subsequent script lines, the way you'd expect. Lines starting with `#` and blank lines are skipped; leading/trailing whitespace is trimmed; `~/` is expanded.
|
||||
|
||||
`save` / `restore` (`src/system/persist.{hpp,cpp}`): tab-delimited line format, no extra deps. Tags: `M` (module), `P` (part with `connector_type`), `B` (part's attached BSDL `.bsd` path — re-parsed and re-applied on restore; the path is persisted, **not** the derived pin specs), `N` (pin → signal name; empty = NC; optional 4th field carries `nc_origin_tag()`: `U` = ImportedUnconnected, `D` = DroppedSingleton — omitted when the pin has a signal or when origin is `None`), `S` (signal → type override; only emitted for non-default), `C` (connection header with endpoints + `transform_name`), `W` (wire pair within the current connection). The 4th N field is backward-compatible: pre-existing snapshots without it restore with `nc_origin = None`.
|
||||
`save` / `restore` (`src/core/domain/persist.{hpp,cpp}`): tab-delimited line format, no extra deps. Tags: `M` (module), `P` (part with `connector_type`), `B` (part's attached BSDL `.bsd` path — re-parsed and re-applied on restore; the path is persisted, **not** the derived pin specs), `N` (pin → signal name; empty = NC; optional 4th field carries `nc_origin_tag()`: `U` = ImportedUnconnected, `D` = DroppedSingleton — omitted when the pin has a signal or when origin is `None`), `S` (signal → type override; only emitted for non-default), `C` (connection header with endpoints + `transform_name`), `W` (wire pair within the current connection). The 4th N field is backward-compatible: pre-existing snapshots without it restore with `nc_origin = None`.
|
||||
|
||||
**Signals** carry a `type` (`SignalType::Power | GndShield | Other`). The `Signal` constructor **defaults to `Other`** — auto-inference no longer happens at construction. Types are set in three ways, in priority order:
|
||||
|
||||
1. **`infer_signal_types(System*)`** (`src/system/analysis.{hpp,cpp}`) runs at the end of every `load` (after `drop_singleton_signals`). It assigns:
|
||||
1. **`infer_signal_types(System*)`** (`src/core/domain/analysis.{hpp,cpp}`) runs at the end of every `load` (after `drop_singleton_signals`). It assigns:
|
||||
- `GndShield` when the **name alone** is unambiguous (`GND`, `SHIELD`, `CHASSIS`, `EARTH`, …) — false-positive rate is essentially zero on these.
|
||||
- `Power` requires (a) the name heuristic (`infer_signal_type` says Power), (b) a **hard fan-out floor**: signals with fewer than `POWER_FANOUT_HARD_FLOOR = 3` pins are *always* refused, regardless of name or voltage pattern (a real rail physically cannot land on just 1-2 pads), and (c) at least one positive structural signal — fan-out ≥ `POWER_FANOUT_CONFIRM_MIN = 4` **or** a voltage pattern in the name (`3V3`, `5V`, `12V`, …; detector: a `V` adjacent to a digit). This catches `VSEL_*`, `PWR_OK`, `_VDD_SENSE` etc. which look like Power by name but aren't real rails. Both thresholds are exposed in `analysis.hpp` so the analyze screen can render the same reasoning without duplicating constants.
|
||||
- `Other` otherwise. The "name-said-Power-but-refuted-by-structure" count is reported by `load`.
|
||||
@@ -123,17 +171,17 @@ Pending prompts (from incomplete inline commands) are NOT considered interactive
|
||||
|
||||
The explore screen shows the type in the signal detail header.
|
||||
|
||||
**Pin spec (expected attributes)**: every Pin carries a `PinSpec spec` (`src/system/pin_spec.hpp`) — the *expected* half of verification, set from a model: `function` (Power/Ground/Signal/Clock/NoConnect/Jtag*), `direction` (In/Out/Bidir/Passive/Power), `pad` (physical package terminal, e.g. a BSDL ball), and `source` (which model wrote it). `set-connector-type` populates it via `pin_role(connector_type, pin_name) → PinSpec`. `Pin::expected_signal_type()` is now a **derived accessor** — `to_signal_type(spec.function)` (Power→Power, Ground→GndShield, else Other) — not a stored field; the *observed* half stays `Pin::signal()` + the net + inference, and `verify` diffs the two. The VPX 3U connector lookup (`vpx_3u_role`) is still a stub returning Other, so connector-typed pins resolve to function Unknown until that table is filled. **`direction`/`function`/`pad` are populated from BSDL** via `attach-bsdl` (see below) and consumed by the model-driven checks (`check_pin_specs`: contention / undriven / NC-wired) and the JTAG chain check (`check_jtag_chain`), both run by `verify`.
|
||||
**Pin spec (expected attributes)**: every Pin carries a `PinSpec spec` (`src/core/domain/pin_spec.hpp`) — the *expected* half of verification, set from a model: `function` (Power/Ground/Signal/Clock/NoConnect/Jtag*), `direction` (In/Out/Bidir/Passive/Power), `pad` (physical package terminal, e.g. a BSDL ball), and `source` (which model wrote it). `set-connector-type` populates it via `pin_role(connector_type, pin_name) → PinSpec`. `Pin::expected_signal_type()` is now a **derived accessor** — `to_signal_type(spec.function)` (Power→Power, Ground→GndShield, else Other) — not a stored field; the *observed* half stays `Pin::signal()` + the net + inference, and `verify` diffs the two. The VPX 3U connector lookup (`vpx_3u_role`) is still a stub returning Other, so connector-typed pins resolve to function Unknown until that table is filled. **`direction`/`function`/`pad` are populated from BSDL** via `attach-bsdl` (see below) and consumed by the model-driven checks (`check_pin_specs`: contention / undriven / NC-wired) and the JTAG chain check (`check_jtag_chain`), both run by `verify`.
|
||||
|
||||
**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-connector-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-connector-type → FillPartFromLayout → pin_role`.
|
||||
|
||||
**BSDL models (`attach-bsdl`)**: `attach-bsdl <module> <part> <file.bsd>` parses a BSDL device through `libbsdl` (wrapped by `BsdlModel`, `src/system/bsdl_model.{hpp,cpp}`), then `apply_bsdl(part, model)` binds each port to a Pin **by port name first, then by physical pad** — so a netlist that names IC pins either by signal or by package ball both bind. Each bound pin gets its `spec` set: `direction` (BSDL in/out/inout/linkage), `function` (TAP role → Jtag\*, `linkage` → Power/Ground/NoConnect by name, else Signal), `pad` (PIN_MAP ball), `source = Bsdl`. The `.bsd` path is stored on `Part::bsdl_path`, persisted via the `B` tag and re-applied on `restore`. Real-world check: an `xcku15p` FPGA in a VPX system binds 1517/1517 ports.
|
||||
**BSDL models (`attach-bsdl`)**: `attach-bsdl <module> <part> <file.bsd>` parses a BSDL device through `libbsdl` (wrapped by `BsdlModel`, `src/core/domain/bsdl_model.{hpp,cpp}`), then `apply_bsdl(part, model)` binds each port to a Pin **by port name first, then by physical pad** — so a netlist that names IC pins either by signal or by package ball both bind. Each bound pin gets its `spec` set: `direction` (BSDL in/out/inout/linkage), `function` (TAP role → Jtag\*, `linkage` → Power/Ground/NoConnect by name, else Signal), `pad` (PIN_MAP ball), `source = Bsdl`. The `.bsd` path is stored on `Part::bsdl_path`, persisted via the `B` tag and re-applied on `restore`. Real-world check: an `xcku15p` FPGA in a VPX system binds 1517/1517 ports.
|
||||
|
||||
**Unified application (`apply_model`)**: connector layout and BSDL are two implementations of one `PinModel` interface (`src/system/pin_model.{hpp,cpp}`: `spec_for(pin_name)`, `layout()`, `source()`). `ConnectorModel` wraps `pin_role`/`pin_layout`; `BsdlPinModel` wraps a parsed `BsdlModel`, indexed by both port name and physical pad. A single `apply_model(Part*, const PinModel&)` materialises the layout pins missing from the netlist, then sets each pin's `spec` **only where the model speaks** (`spec.source != None`). Sources are ranked (`spec_source_rank` in `pin_spec.hpp`: UserOverride > Bsdl > ConnectorModel > Inferred > Imported) and apply_model refuses to overwrite a spec owned by a higher-rank source — so one source never clobbers a more authoritative one, which is also the basis for `check_source_conflicts`. `set-connector-type` and `attach-bsdl` both funnel through it (the latter via the thin `apply_bsdl` adapter); `verify` stays agnostic of where a spec came from. A future SPICE/Modelica source would be a third `PinModel`.
|
||||
**Unified application (`apply_model`)**: connector layout and BSDL are two implementations of one `PinModel` interface (`src/core/domain/pin_model.{hpp,cpp}`: `spec_for(pin_name)`, `layout()`, `source()`). `ConnectorModel` wraps `pin_role`/`pin_layout`; `BsdlPinModel` wraps a parsed `BsdlModel`, indexed by both port name and physical pad. A single `apply_model(Part*, const PinModel&)` materialises the layout pins missing from the netlist, then sets each pin's `spec` **only where the model speaks** (`spec.source != None`). Sources are ranked (`spec_source_rank` in `pin_spec.hpp`: UserOverride > Bsdl > ConnectorModel > Inferred > Imported) and apply_model refuses to overwrite a spec owned by a higher-rank source — so one source never clobbers a more authoritative one, which is also the basis for `check_source_conflicts`. `set-connector-type` and `attach-bsdl` both funnel through it (the latter via the thin `apply_bsdl` adapter); `verify` stays agnostic of where a spec came from. A future SPICE/Modelica source would be a third `PinModel`.
|
||||
|
||||
**`verify` (seven passes)**: (1) typed pins — local mismatch between each pin's `expected_signal_type()` (derived from its `PinSpec`) and the actual signal type; (2) bridged nets — Power↔GndShield inconsistencies; (3) orphan summary `N orphan pin(s) at import (X imported NC, Y dropped singleton)` (filters out pins bridged via any `Connection::pin_map` — typically `FillIdentityNCs`-materialised); (4) **model-driven pin checks** (`check_pin_specs`): `DriveContention` (≥2 push-pull `Out` on a net), `UndrivenNet` (a **fully-modelled** net with input(s) but no driver — nets with any Unknown-direction pin are skipped, so un-modelled drivers don't cause false positives), `NcWired` (a no-connect pin on a multi-pin net); (5) **JTAG chain** (`check_jtag_chain`): collects TAP pins by `spec.function`, maps each to its net, emits `JtagTapIncomplete` / `JtagBusUnbridged` (TMS or TCK not common to all TAP devices) / `JtagChainBreak` (dangling TDO/TDI, chain fan-out, or not a single head→tail daisy chain); (6) **source conflicts** (`check_source_conflicts`): a pin the BSDL declares power/ground (a must-connect rail) that the netlist leaves unconnected — a rail floated in the schematic (`SourceConflict`; the reverse, a BSDL no-connect that *is* wired, is the `NcWired` check); (7) **BSDL completeness** (`check_bsdl_completeness`): device power/ground ports (from the attached `.bsd`, re-parsed) with no matching pin on the netlist part — a rail the schematic symbol is missing (`BsdlPinMissing`, one aggregated finding per part). The BFS-reached `(module, signal)` set for any signal is shown live in `explore`'s detail pane when a signal entry is selected.
|
||||
|
||||
**`analyze` (post-processing pass)**: `analyze_system(System*) → AnalysisReport` (`src/system/analysis.{hpp,cpp}`) is a stateless read-only pass that detects structural signal groups and anomalies. Per-module (signals are module-scoped):
|
||||
**`analyze` (post-processing pass)**: `analyze_system(System*) → AnalysisReport` (`src/core/domain/analysis.{hpp,cpp}`) is a stateless read-only pass that detects structural signal groups and anomalies. Per-module (signals are module-scoped):
|
||||
|
||||
- **Diff pairs**: signal names ending `_P` / `_N` (case-insensitive) grouped by stem. Both halves present → candidate matched pair.
|
||||
- **Diff buses**: ≥ 2 matched diff pairs whose pair-stems share a common outer-stem after stripping a trailing integer (`MDI0` / `MDI1` / `MDI2` → outer `MDI` + indices). The strict `_` rule from plain buses does NOT apply to this trailing-index split: `_P`/`_N` was already stripped, so we know remaining digits are an index. Two index variants accepted: contiguous (`MDI0`) and underscore-separated (`PCIE_TX_0`). Emitted as `SignalGroup{kind=DiffBus, lo, hi}` with label `OUTER[lo..hi]_P/N`. Members include all 2·N constituent signals. A "bus" of size 1 falls back to `DiffPair` (single index does not a bus make).
|
||||
@@ -145,13 +193,13 @@ Exposed as the `analyze` shell command which prints groups (sorted by module + l
|
||||
|
||||
**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 `set-connector-type` guard, `explore` filter, and `explore` header.
|
||||
|
||||
`SignalType` lives in its own header `src/system/signal_type.hpp` (extracted from signals to avoid a pins↔signals include cycle).
|
||||
`SignalType` lives in its own header `src/core/domain/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.
|
||||
|
||||
**NC origin tag**: each `Pin` carries `NcOrigin nc_origin` (`None | ImportedUnconnected | DroppedSingleton`, default `None`). Set in three places: (a) Mentor importer when the signal field starts with `unconnected` → `ImportedUnconnected`; (b) `drop_singleton_signals(Signals*)` called at the end of `load` → `DroppedSingleton` on each detached pin (signals with exactly one pin are NC by definition — see commits motivating this); (c) `duplicate` propagates the tag. Pins materialised by `FillIdentityNCs` keep `None` — they have no local signal but are bridged via `pin_map` and shouldn't be counted as orphans. The tag is persisted (see `N` record), reported as a total in `verify`, and tested in `tests/test_nc_origin.cpp`.
|
||||
|
||||
**Connector types & transforms**: every `Part` carries a `connector_type` string (default `""`, set via the `set-connector-type` command — inline `set-connector-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.
|
||||
**Connector types & transforms**: every `Part` carries a `connector_type` string (default `""`, set via the `set-connector-type` command — inline `set-connector-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/core/domain/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.
|
||||
|
||||
**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.
|
||||
|
||||
@@ -188,7 +236,7 @@ Today the only caller is `export` (`{"CSV", ".csv"}, {"ODS", ".ods"}` filters, k
|
||||
|
||||
**Per-key path persistence** (`SaveLastUsed(key, dir, filename)` / `LoadLastUsed(key, &dir, &filename)` in `shell.cpp`): each key writes a tiny two-line file (`dir\nfilename\n`) under `UserDataDir() / <key>.last`. `UserDataDir()` is the cross-platform `XDG_DATA_HOME` / `LOCALAPPDATA` etc. helper also used by the command history file. Free functions, not Tui members, so any module (the file dialog today; could be the script-save buffer or the save command tomorrow) can use them with the same minimal API.
|
||||
|
||||
**ODS writer** (`src/imports/ods_writer.{hpp,cpp}`): minimal OpenDocument Spreadsheet writer. Backed by libzip + pugixml (both already pulled in by the ODS importer). Class hierarchy is two structs:
|
||||
**ODS writer** (`src/core/imports/ods_writer.{hpp,cpp}`): minimal OpenDocument Spreadsheet writer. Backed by libzip + pugixml (both already pulled in by the ODS importer). Class hierarchy is two structs:
|
||||
|
||||
- `OdsSheet` — sparse row-major grid of string cells (`set(row, col, value)`).
|
||||
- `OdsWriter` — owns the sheets, emits a valid `.ods` archive with `mimetype` (stored uncompressed, magic header), `META-INF/manifest.xml`, and `content.xml`.
|
||||
@@ -248,9 +296,9 @@ Everything in this section is a precise description of how signals, pins, parts,
|
||||
|
||||
Default: `Signal::type = SignalType::Other` (constructor does no inference).
|
||||
|
||||
Type is set by `infer_signal_types(System*)` (`src/system/analysis.cpp`), called at the end of every `load` and after `duplicate`. The decision per signal:
|
||||
Type is set by `infer_signal_types(System*)` (`src/core/domain/analysis.cpp`), called at the end of every `load` and after `duplicate`. The decision per signal:
|
||||
|
||||
1. Compute `named = infer_signal_type(name)` (`src/system/signals.cpp`):
|
||||
1. Compute `named = infer_signal_type(name)` (`src/core/domain/signals.cpp`):
|
||||
- `GndShield` if name matches **case-insensitively**: `GND`, `GROUND`, `EARTH`, `SHIELD`, `CHASSIS`, or starts with `GND_` / `GROUND_` / `EARTH_` / `SHIELD_` / `CHASSIS_`.
|
||||
- `Power` if the name contains any of `PWR`, `POWER`, `VCC`, `VDD`, `VEE`, `VSS`, `VBAT`, or starts with `VS_`, `VS3_`, `+`, `-` followed by a digit.
|
||||
- Else `Other`.
|
||||
@@ -264,15 +312,15 @@ Type is set by `infer_signal_types(System*)` (`src/system/analysis.cpp`), called
|
||||
|
||||
### NC pin origin
|
||||
|
||||
`Pin::nc_origin` (`src/system/pins.hpp`). Default `NcOrigin::None`. Set by:
|
||||
`Pin::nc_origin` (`src/core/domain/pins.hpp`). Default `NcOrigin::None`. Set by:
|
||||
|
||||
- **`NcOrigin::ImportedUnconnected`** — Mentor importer (`src/imports/import_mentor.cpp`): the signal field of an `Explicit Pin:` row starts case-insensitively with `unconnected` (e.g. `'unconnected'`, `'unconnected (by TERM)'`). The pin is kept on the part with no signal.
|
||||
- **`NcOrigin::DroppedSingleton`** — `drop_singleton_signals(Signals*)` (`src/system/signals.cpp`) called at the end of `load`: any signal whose pin set has size exactly 1 is unconnected by definition. The pin is detached (`sig = nullptr`) and tagged; the `Signal` object is deleted.
|
||||
- **`NcOrigin::ImportedUnconnected`** — Mentor importer (`src/core/imports/import_mentor.cpp`): the signal field of an `Explicit Pin:` row starts case-insensitively with `unconnected` (e.g. `'unconnected'`, `'unconnected (by TERM)'`). The pin is kept on the part with no signal.
|
||||
- **`NcOrigin::DroppedSingleton`** — `drop_singleton_signals(Signals*)` (`src/core/domain/signals.cpp`) called at the end of `load`: any signal whose pin set has size exactly 1 is unconnected by definition. The pin is detached (`sig = nullptr`) and tagged; the `Signal` object is deleted.
|
||||
- **`NcOrigin::None` (no tag)** — pins materialised by `FillIdentityNCs` at `connect` time. These are unconnected locally but bridged to a real signal on the peer module via `Connection::pin_map`; they are explicitly excluded from the "orphan" count in `verify` and the analyze screen.
|
||||
|
||||
### Signal groups
|
||||
|
||||
`analyze_system(System*)` (`src/system/analysis.cpp`) emits `SignalGroup`s **per module** (signals are module-scoped). Internal names (starting with `$` — Mentor's `$Nxxxx` convention) are skipped wholesale at every step.
|
||||
`analyze_system(System*)` (`src/core/domain/analysis.cpp`) emits `SignalGroup`s **per module** (signals are module-scoped). Internal names (starting with `$` — Mentor's `$Nxxxx` convention) are skipped wholesale at every step.
|
||||
|
||||
**DiffPair** (`GroupKind::DiffPair`):
|
||||
- Signal name ends `_P` or `_N` (case-insensitive). The character before the suffix must be `_`.
|
||||
@@ -303,7 +351,7 @@ The `verify` command (not the analyze screen, yet) also emits the **model-driven
|
||||
|
||||
### Component kind
|
||||
|
||||
`Part::kind` is inferred at construction (`src/system/component_kind.cpp`) from the leading reference-designator letter(s) of the part name. **Longest-match wins**:
|
||||
`Part::kind` is inferred at construction (`src/core/domain/component_kind.cpp`) from the leading reference-designator letter(s) of the part name. **Longest-match wins**:
|
||||
|
||||
- Two-letter prefixes (checked first, case-insensitive): `LED → Semiconductor`, `TP → TestPoint`, `SW → Switch`, `FB → Passive`, `MK / MP / MH → Mechanical`, `HS → Mechanical`, `RA / RN / RP / RV → Passive`.
|
||||
- Single-letter fallback: `R / C / L / F → Passive`, `D / Q → Semiconductor`, `U → IntegratedCircuit`, `J / P → Connector`, `Y / X → Crystal`, `S → Switch`.
|
||||
@@ -315,7 +363,7 @@ Recomputed on `restore` (no persistence tag). Currently not used by any decision
|
||||
|
||||
`connect` looks up a registered transform for `(p1->connector_type, p2->connector_type)` via `TransformRegistry::lookup`, tried in both directions. Fall-through is `IdentityTransform`:
|
||||
|
||||
- Compares pin sets by **canonical name** (`canonical_pin_name(s)`, `src/system/pin_name.cpp`): split into prefix + digit suffix; if the suffix is pure digits, zero-pad to width 3 (`A1` → `A001`, `A001` → `A001`, `A1B` → `A1B`, `VCC` → `VCC`).
|
||||
- Compares pin sets by **canonical name** (`canonical_pin_name(s)`, `src/core/domain/pin_name.cpp`): split into prefix + digit suffix; if the suffix is pure digits, zero-pad to width 3 (`A1` → `A001`, `A001` → `A001`, `A1B` → `A1B`, `VCC` → `VCC`).
|
||||
- `CheckIdentityCompatible(a, b)` accepts the **subset case** (one side's canonical set is a subset of the other's — typical because Altium drops NC, Mentor doesn't). Bidirectional mismatch (both sides have orphans) is refused.
|
||||
- After acceptance, `FillIdentityNCs(p1, p2)` **materialises** the missing canonical positions on the smaller side as new NC pins (`new Pin(other_side_name)`, no signal attached, `nc_origin = None`). Idempotent.
|
||||
|
||||
|
||||
40
README.md
40
README.md
@@ -15,6 +15,13 @@ cmake --build build -j
|
||||
./build/essim
|
||||
```
|
||||
|
||||
The build is **layered**: `essim_core` is the frontend-agnostic business
|
||||
library (domain + importers + operations); the `essim` binary comes from a
|
||||
**frontend** under `src/frontends/<name>/` that links it. Select one with
|
||||
`-DESSIM_FRONTEND=<name>` (default `tui`); `-DESSIM_FRONTEND=none` builds the
|
||||
core + tests only, with no GUI toolkit fetched. Architecture in
|
||||
[`DESIGN.md`](DESIGN.md).
|
||||
|
||||
Inside the shell, type `help` for the live command list — or read the
|
||||
auto-generated reference at [`doc/user/commands.md`](doc/user/commands.md).
|
||||
A worked bring-up script is at [`test/system.essim`](test/system.essim);
|
||||
@@ -41,10 +48,21 @@ Step-by-step walkthroughs for both the batch and TUI workflows are in
|
||||
`../libbsdl`, pulled in via `add_subdirectory` and linked dynamically.
|
||||
Override its location with `-DBSDL_DIR=/path/to/libbsdl`. Powers the
|
||||
`attach-bsdl` command and the pin/JTAG checks.
|
||||
- Fetched automatically at configure time via `FetchContent` (nothing to
|
||||
install): **FTXUI** v6.1.9 and **doctest** v2.4.11.
|
||||
- Fetched automatically via `FetchContent` (nothing to install): **FTXUI**
|
||||
v6.1.9 — only when building the **tui** frontend — and **doctest** v2.4.11
|
||||
for the tests.
|
||||
- **wxWidgets** (≥ 3.2) — only for the **wx** GUI frontend
|
||||
(`-DESSIM_FRONTEND=wx`). Install the development package:
|
||||
- Debian/Ubuntu — `sudo apt install libwxgtk3.2-dev`
|
||||
- Arch — `sudo pacman -S wxwidgets-gtk3`
|
||||
- Fedora — `sudo dnf install wxGTK-devel`
|
||||
- Optional, only for the `doc` target: **doxygen** and **python3**.
|
||||
|
||||
libzip, pugixml and libbsdl are the **core** dependencies; each frontend pulls
|
||||
its own toolkit (FTXUI for tui, wxWidgets for wx), so a `-DESSIM_FRONTEND=none`
|
||||
build needs neither. Pick a GUI/TUI with `-DESSIM_FRONTEND=tui|wx` (default
|
||||
`tui`).
|
||||
|
||||
## Tests
|
||||
|
||||
```sh
|
||||
@@ -53,6 +71,9 @@ Step-by-step walkthroughs for both the batch and TUI workflows are in
|
||||
ctest --test-dir build
|
||||
```
|
||||
|
||||
`ctest` runs `essim_tests` (core — links `essim_core`, no GUI toolkit) and
|
||||
`essim_tui_tests` (the FTXUI frontend's tests, under `tests/tui/`).
|
||||
|
||||
Skip building tests entirely:
|
||||
|
||||
```sh
|
||||
@@ -76,11 +97,16 @@ cmake --build build --target doc # needs doxygen + python3
|
||||
## Project layout
|
||||
|
||||
```
|
||||
src/system/ domain model (Module/Part/Pin/Signal, Connection, Transform, …)
|
||||
src/imports/ Mentor / Altium / ODS netlist importers
|
||||
src/tui/ FTXUI shell (commands, screens, completion, history)
|
||||
tests/ doctest suite
|
||||
doc/ api/ + user/ Markdown trees, Doxyfile.in, gen_api_md.py
|
||||
src/
|
||||
core/ business logic, NO GUI toolkit (→ libessim_core)
|
||||
domain/ model (Module/Part/Pin/Signal, Connection, Transform…) + analyses
|
||||
imports/ Mentor / Altium / ODS netlist importers + ODS writer
|
||||
app/ use-case operations (export → CSV/ODS, …)
|
||||
frontends/ one dir per GUI/TUI engine, each links essim_core
|
||||
tui/ FTXUI shell + main.cpp (→ libessim_tui + the `essim` binary)
|
||||
tests/ core tests (link essim_core)
|
||||
tui/ frontend tests (link essim_tui)
|
||||
doc/ api/ + user/ Markdown, Doxyfile.in, gen_api_md.py
|
||||
test/ sample netlists + system.essim bring-up script
|
||||
```
|
||||
|
||||
|
||||
30
cmake/EssimFrontend.cmake
Normal file
30
cmake/EssimFrontend.cmake
Normal file
@@ -0,0 +1,30 @@
|
||||
# essim_add_frontend(<name> [LIBS <toolkit link targets>...])
|
||||
#
|
||||
# Builds the boilerplate shared by every frontend under src/frontends/<name>/:
|
||||
# * a static library essim_<name> from every .cpp in the current directory
|
||||
# except main.cpp, linking essim_core plus the frontend's own GUI/TUI
|
||||
# toolkit (LIBS);
|
||||
# * the `essim` executable from main.cpp, linking essim_<name> and the shared,
|
||||
# toolkit-free launcher essim_frontend, emitted at the top of the build tree
|
||||
# (./build/essim) whichever frontend produced it.
|
||||
#
|
||||
# A per-frontend CMakeLists only sets up its toolkit (FetchContent /
|
||||
# find_package, and any directory-scoped include dirs / definitions) and then
|
||||
# calls this with the toolkit's link targets — no target wiring repeated.
|
||||
function(essim_add_frontend name)
|
||||
cmake_parse_arguments(FE "" "" "LIBS" ${ARGN})
|
||||
|
||||
set(dir "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
|
||||
# Frontend library = every .cpp here except the entry point.
|
||||
file(GLOB FE_SOURCES "${dir}/*.cpp")
|
||||
list(REMOVE_ITEM FE_SOURCES "${dir}/main.cpp")
|
||||
|
||||
add_library(essim_${name} STATIC ${FE_SOURCES})
|
||||
target_link_libraries(essim_${name} PUBLIC essim_core ${FE_LIBS})
|
||||
|
||||
add_executable(essim "${dir}/main.cpp")
|
||||
target_link_libraries(essim PRIVATE essim_${name} essim_frontend)
|
||||
set_target_properties(essim PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}")
|
||||
endfunction()
|
||||
67
src/core/app/connect.cpp
Normal file
67
src/core/app/connect.cpp
Normal file
@@ -0,0 +1,67 @@
|
||||
#include "core/app/connect.hpp"
|
||||
|
||||
#include "core/domain/connect.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
#include "core/domain/transform.hpp"
|
||||
|
||||
#include <exception>
|
||||
#include <utility>
|
||||
|
||||
namespace app {
|
||||
|
||||
ConnectResult connect_parts(System *sys, Module *m1, Part *p1,
|
||||
Module *m2, Part *p2)
|
||||
{
|
||||
ConnectResult r;
|
||||
|
||||
auto ® = TransformRegistry::get();
|
||||
Transform *t = reg.lookup(p1->connector_type, p2->connector_type);
|
||||
bool both_empty = p1->connector_type.empty() && p2->connector_type.empty();
|
||||
|
||||
if (t == reg.identity()) {
|
||||
if (!both_empty) {
|
||||
r.refused = true;
|
||||
r.error = "no transform for types '"
|
||||
+ (p1->connector_type.empty() ? std::string("(none)")
|
||||
: p1->connector_type)
|
||||
+ "' ↔ '"
|
||||
+ (p2->connector_type.empty() ? std::string("(none)")
|
||||
: p2->connector_type)
|
||||
+ "'. Set matching types via 'set-connector-type' first.";
|
||||
return r;
|
||||
}
|
||||
std::string info;
|
||||
std::string err = CheckIdentityCompatible(p1, p2, &info);
|
||||
if (!err.empty()) {
|
||||
r.refused = true;
|
||||
r.error = err;
|
||||
return r;
|
||||
}
|
||||
if (!info.empty()) {
|
||||
r.identity_info = info;
|
||||
r.nc_added = FillIdentityNCs(p1, p2);
|
||||
}
|
||||
}
|
||||
|
||||
auto pin_map = t->apply(p1, p2);
|
||||
|
||||
r.connection_name = m1->name + "/" + p1->name
|
||||
+ " <-> " + m2->name + "/" + p2->name;
|
||||
r.transform_name = t->name;
|
||||
try {
|
||||
Connection *c = new Connection(r.connection_name, m1, p1, m2, p2);
|
||||
c->transform_name = t->name;
|
||||
c->pin_map = std::move(pin_map);
|
||||
sys->connections()->add(c);
|
||||
r.wires = (int)c->pin_map.size();
|
||||
r.ok = true;
|
||||
} catch (const std::exception &e) {
|
||||
r.error = e.what();
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
42
src/core/app/connect.hpp
Normal file
42
src/core/app/connect.hpp
Normal file
@@ -0,0 +1,42 @@
|
||||
#ifndef _APP_CONNECT_HPP_
|
||||
#define _APP_CONNECT_HPP_
|
||||
|
||||
#include <string>
|
||||
|
||||
class System;
|
||||
class Module;
|
||||
class Part;
|
||||
|
||||
// Application layer: UI-independent operations that any frontend (TUI, GUI, …)
|
||||
// can call. No console, no dialogs, no FTXUI — just System in, result out.
|
||||
namespace app {
|
||||
|
||||
// Outcome of connecting two parts. The side effects (filling identity NC pins,
|
||||
// creating the Connection and adding it to the system) all happen in core; the
|
||||
// caller only renders the fields.
|
||||
struct ConnectResult {
|
||||
bool ok = false; ///< a Connection was created and added
|
||||
bool refused = false; ///< a business rule rejected it (vs. an exception)
|
||||
std::string error; ///< why refused/failed; empty when ok
|
||||
|
||||
std::string connection_name;
|
||||
std::string transform_name;
|
||||
int wires = 0; ///< pin_map size of the created connection
|
||||
|
||||
// Identity-transform path only: the compatibility info line and how many NC
|
||||
// pins were materialised so both sides match. Empty / 0 otherwise.
|
||||
std::string identity_info;
|
||||
int nc_added = 0;
|
||||
};
|
||||
|
||||
// Wire part `p1` (in module `m1`) to part `p2` (in module `m2`): look up the
|
||||
// transform for their connector types, refuse on an unknown pairing or an
|
||||
// identity-incompatible layout, fill identity NC pins when needed, apply the
|
||||
// transform and create the Connection. Pure core — no resolution of names or
|
||||
// patterns (the frontend turns user input into the Module*/Part* it passes).
|
||||
ConnectResult connect_parts(System *sys, Module *m1, Part *p1,
|
||||
Module *m2, Part *p2);
|
||||
|
||||
} // namespace app
|
||||
|
||||
#endif // _APP_CONNECT_HPP_
|
||||
137
src/core/app/edit.cpp
Normal file
137
src/core/app/edit.cpp
Normal file
@@ -0,0 +1,137 @@
|
||||
#include "core/app/edit.hpp"
|
||||
|
||||
#include "core/domain/bsdl_model.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/pin_model.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
#include "core/domain/transform_vpx.hpp" // ValidatePartForKind
|
||||
|
||||
#include <exception>
|
||||
|
||||
namespace app {
|
||||
|
||||
SetConnectorTypeResult set_connector_type(Part *part, const std::string &kind)
|
||||
{
|
||||
SetConnectorTypeResult r;
|
||||
if (!part) {
|
||||
r.error = "no part";
|
||||
return r;
|
||||
}
|
||||
|
||||
std::string err = ValidatePartForKind(part, kind);
|
||||
if (!err.empty()) {
|
||||
r.error = err;
|
||||
return r;
|
||||
}
|
||||
|
||||
part->connector_type = kind;
|
||||
ConnectorModel model(kind);
|
||||
ApplyReport rep = apply_model(part, model);
|
||||
r.materialised = rep.materialised;
|
||||
r.ok = true;
|
||||
return r;
|
||||
}
|
||||
|
||||
AttachBsdlResult attach_bsdl(Part *part, const std::string &path)
|
||||
{
|
||||
AttachBsdlResult r;
|
||||
if (!part) {
|
||||
r.error = "no part";
|
||||
return r;
|
||||
}
|
||||
|
||||
BsdlModel model = BsdlModel::from_file(path);
|
||||
if (!model.valid()) {
|
||||
r.error = "cannot parse " + path
|
||||
+ (model.error().empty() ? "" : (": " + model.error()));
|
||||
return r;
|
||||
}
|
||||
|
||||
BsdlApplyReport rep = apply_bsdl(part, model);
|
||||
part->bsdl_path = path;
|
||||
r.entity = model.entity();
|
||||
r.bound = rep.bound;
|
||||
r.unbound = rep.unbound;
|
||||
r.ports_total = (int)model.ports().size();
|
||||
r.ok = true;
|
||||
return r;
|
||||
}
|
||||
|
||||
SetSignalTypeResult set_signal_type(Signal *sig, const std::string &type_name)
|
||||
{
|
||||
SetSignalTypeResult r;
|
||||
if (!sig) {
|
||||
r.error = "no signal";
|
||||
return r;
|
||||
}
|
||||
|
||||
SignalType t;
|
||||
if (!signal_type_from_name(type_name, t)) {
|
||||
r.error = "type must be one of: power, gnd, other (got: " + type_name + ")";
|
||||
return r;
|
||||
}
|
||||
|
||||
sig->type = t;
|
||||
r.type = t;
|
||||
r.ok = true;
|
||||
return r;
|
||||
}
|
||||
|
||||
DuplicateResult duplicate_module(System *sys, const std::string &src_name,
|
||||
const std::string &dst_name)
|
||||
{
|
||||
DuplicateResult r;
|
||||
if (!sys) { r.error = "no system"; return r; }
|
||||
|
||||
Module *src;
|
||||
try { src = sys->modules()->get(src_name); }
|
||||
catch (const std::exception &) {
|
||||
r.error = "unknown module: " + src_name;
|
||||
return r;
|
||||
}
|
||||
if (sys->modules()->exists(dst_name)) {
|
||||
r.error = "duplicate refused: module '" + dst_name + "' already exists.";
|
||||
return r;
|
||||
}
|
||||
|
||||
Module *dst = new Module(dst_name);
|
||||
|
||||
// Signals first (preserve type overrides), so pins can re-wire to them.
|
||||
for (auto &skv : *src->signals) {
|
||||
Signal *ss = skv.second;
|
||||
Signal *ds = new Signal(ss->name);
|
||||
ds->type = ss->type;
|
||||
dst->signals->add(ds);
|
||||
}
|
||||
|
||||
// Parts, pins (spec + nc_origin), and the pin→signal wiring.
|
||||
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->spec = sn->spec;
|
||||
dn->nc_origin = sn->nc_origin;
|
||||
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);
|
||||
r.parts = (int)dst->size();
|
||||
r.signals = (int)dst->signals->size();
|
||||
r.ok = true;
|
||||
return r;
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
73
src/core/app/edit.hpp
Normal file
73
src/core/app/edit.hpp
Normal file
@@ -0,0 +1,73 @@
|
||||
#ifndef _APP_EDIT_HPP_
|
||||
#define _APP_EDIT_HPP_
|
||||
|
||||
#include "core/domain/signal_type.hpp" // SignalType
|
||||
|
||||
#include <string>
|
||||
|
||||
class Part;
|
||||
class Signal;
|
||||
class System;
|
||||
|
||||
// Application layer: UI-independent part-editing operations any frontend can
|
||||
// call. No console, no dialogs, no FTXUI — Part in, result struct out.
|
||||
namespace app {
|
||||
|
||||
// Outcome of tagging a part's connector type. The op validates the kind, sets
|
||||
// the type and applies the connector model (which may materialise the layout's
|
||||
// missing NC pins); the caller renders the result.
|
||||
struct SetConnectorTypeResult {
|
||||
bool ok = false;
|
||||
std::string error; ///< set when refused (kind invalid for the part)
|
||||
int materialised = 0; ///< NC pins created from the connector layout
|
||||
};
|
||||
|
||||
// Tag `part`'s connector type and apply the matching connector model. Refuses
|
||||
// (ok=false, error set, no mutation) when the kind is invalid for the part.
|
||||
SetConnectorTypeResult set_connector_type(Part *part, const std::string &kind);
|
||||
|
||||
// Outcome of attaching a BSDL model to a part. On success the part's pin specs
|
||||
// are filled from the model and its bsdl_path is recorded.
|
||||
struct AttachBsdlResult {
|
||||
bool ok = false;
|
||||
std::string error; ///< set when the .bsd cannot be parsed
|
||||
std::string entity; ///< the BSDL entity name
|
||||
int bound = 0; ///< ports matched to a pin
|
||||
int unbound = 0; ///< ports with no matching pin
|
||||
int ports_total = 0; ///< ports declared in the model
|
||||
};
|
||||
|
||||
// Parse the BSDL file at `path` and apply it to `part` (fills each pin's role
|
||||
// and direction; records bsdl_path). Fails (ok=false, error set, no mutation)
|
||||
// when the file cannot be parsed.
|
||||
AttachBsdlResult attach_bsdl(Part *part, const std::string &path);
|
||||
|
||||
// Outcome of overriding a signal's type from a user-supplied name.
|
||||
struct SetSignalTypeResult {
|
||||
bool ok = false;
|
||||
std::string error; ///< set when the name isn't power/gnd/other
|
||||
SignalType type = SignalType::Other; ///< the resolved type (for rendering)
|
||||
};
|
||||
|
||||
// Set `sig`'s type from `type_name` (power | gnd | other, case-insensitive).
|
||||
// Fails (ok=false, error set, no mutation) on an unrecognised name.
|
||||
SetSignalTypeResult set_signal_type(Signal *sig, const std::string &type_name);
|
||||
|
||||
// Outcome of cloning a module under a new name.
|
||||
struct DuplicateResult {
|
||||
bool ok = false;
|
||||
std::string error; ///< unknown source, or destination name already taken
|
||||
int parts = 0;
|
||||
int signals = 0;
|
||||
};
|
||||
|
||||
// Deep-clone module `src_name` as `dst_name`: parts, pins (spec + nc_origin),
|
||||
// signals (with type overrides) and the pin→signal wiring — but not the
|
||||
// system's connections. Fails (ok=false, error set, no change) when the source
|
||||
// is unknown or the destination name already exists.
|
||||
DuplicateResult duplicate_module(System *sys, const std::string &src_name,
|
||||
const std::string &dst_name);
|
||||
|
||||
} // namespace app
|
||||
|
||||
#endif // _APP_EDIT_HPP_
|
||||
47
src/core/app/load.cpp
Normal file
47
src/core/app/load.cpp
Normal file
@@ -0,0 +1,47 @@
|
||||
#include "core/app/load.hpp"
|
||||
|
||||
#include "core/domain/analysis.hpp" // infer_signal_types, SignalTypeInferenceStats
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/signals.hpp" // drop_singleton_signals
|
||||
#include "core/domain/system.hpp"
|
||||
|
||||
#include <cctype>
|
||||
#include <exception>
|
||||
|
||||
namespace app {
|
||||
|
||||
bool import_type_from_name(const std::string &name, ImportType &out)
|
||||
{
|
||||
std::string ls;
|
||||
ls.reserve(name.size());
|
||||
for (char c : name) ls += (char)std::tolower((unsigned char)c);
|
||||
if (ls == "mentor") { out = ImportType::IMPORT_MENTOR; return true; }
|
||||
if (ls == "altium") { out = ImportType::IMPORT_ALTIUM; return true; }
|
||||
if (ls == "ods") { out = ImportType::IMPORT_ODS; return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
LoadResult load_module(System *sys, const std::string &module_name,
|
||||
const std::string &path, ImportType type)
|
||||
{
|
||||
LoadResult r;
|
||||
if (!sys) { r.error = "no system"; return r; }
|
||||
try {
|
||||
sys->Load(module_name, path, type);
|
||||
Module *mod = sys->modules()->get(module_name);
|
||||
r.dropped = drop_singleton_signals(mod->signals);
|
||||
SignalTypeInferenceStats inf = infer_signal_types(sys);
|
||||
r.parts = (int)mod->size();
|
||||
r.signals = (int)mod->signals->size();
|
||||
r.power = inf.power;
|
||||
r.gnd = inf.gnd;
|
||||
r.kept_other = inf.kept_other;
|
||||
r.mgmt = inf.mgmt;
|
||||
r.ok = true;
|
||||
} catch (const std::exception &e) {
|
||||
r.error = e.what();
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
37
src/core/app/load.hpp
Normal file
37
src/core/app/load.hpp
Normal file
@@ -0,0 +1,37 @@
|
||||
#ifndef _APP_LOAD_HPP_
|
||||
#define _APP_LOAD_HPP_
|
||||
|
||||
#include "core/domain/system.hpp" // ImportType
|
||||
|
||||
#include <string>
|
||||
|
||||
// Application layer: UI-independent operations that any frontend (TUI, GUI, …)
|
||||
// can call. No console, no dialogs, no FTXUI — just System in, result out.
|
||||
namespace app {
|
||||
|
||||
// Map an import-type name (mentor / altium / ods, case-insensitive) to an
|
||||
// ImportType. Returns false if the name is none of those.
|
||||
bool import_type_from_name(const std::string &name, ImportType &out);
|
||||
|
||||
// Outcome of loading a module: the post-import counts the caller renders.
|
||||
struct LoadResult {
|
||||
bool ok = false;
|
||||
std::string error; ///< human-readable, set when !ok
|
||||
int parts = 0;
|
||||
int signals = 0;
|
||||
int dropped = 0; ///< singleton/NC signals removed after import
|
||||
int power = 0; ///< signals inferred Power (name + structure)
|
||||
int gnd = 0; ///< signals inferred GndShield (name)
|
||||
int kept_other = 0; ///< name said Power but evidence too weak → kept Other
|
||||
int mgmt = 0; ///< power-management signal (rail + SENSE/EN/PG/… token) → Other, not suspect
|
||||
};
|
||||
|
||||
// Import a module from a netlist/pinout file into `sys`, drop singleton signals,
|
||||
// then infer signal types. Returns the counts or an error. Pure core — safe to
|
||||
// call from any frontend.
|
||||
LoadResult load_module(System *sys, const std::string &module_name,
|
||||
const std::string &path, ImportType type);
|
||||
|
||||
} // namespace app
|
||||
|
||||
#endif // _APP_LOAD_HPP_
|
||||
332
src/core/app/script.cpp
Normal file
332
src/core/app/script.cpp
Normal file
@@ -0,0 +1,332 @@
|
||||
#include "core/app/script.hpp"
|
||||
|
||||
#include "core/app/connect.hpp"
|
||||
#include "core/app/edit.hpp"
|
||||
#include "core/app/export.hpp"
|
||||
#include "core/app/load.hpp"
|
||||
#include "core/app/verify.hpp"
|
||||
|
||||
#include "core/domain/analysis.hpp"
|
||||
#include "core/domain/connect.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/persist.hpp"
|
||||
#include "core/domain/signal_type.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
|
||||
#include <cctype>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace app {
|
||||
|
||||
namespace {
|
||||
|
||||
// Whitespace split with "double quotes" grouping — same rules as the TUI shell.
|
||||
std::vector<std::string> tokenize(const std::string &s) {
|
||||
std::vector<std::string> out;
|
||||
std::string cur;
|
||||
bool in_q = false;
|
||||
bool has = false;
|
||||
for (char c : s) {
|
||||
if (c == '"') { in_q = !in_q; has = true; continue; }
|
||||
if (!in_q && std::isspace((unsigned char)c)) {
|
||||
if (has) { out.push_back(std::move(cur)); cur.clear(); has = false; }
|
||||
} else {
|
||||
cur.push_back(c);
|
||||
has = true;
|
||||
}
|
||||
}
|
||||
if (has) out.push_back(std::move(cur));
|
||||
return out;
|
||||
}
|
||||
|
||||
// One script execution: holds the variable table, the System reference (so new/
|
||||
// restore can replace it) and the output stream.
|
||||
class Runner {
|
||||
public:
|
||||
Runner(std::unique_ptr<System> &sys, std::ostream &out) : sys_(sys), out_(out) {}
|
||||
|
||||
// Run a file; `opened` reports whether it could be opened. Returns the count
|
||||
// of effective lines; accumulates command errors into `r.errors`.
|
||||
int run_file(const std::string &path, int depth, ScriptResult &r, bool &opened) {
|
||||
opened = false;
|
||||
if (depth > 32) {
|
||||
emit("source: nesting too deep, skipping " + path);
|
||||
return 0;
|
||||
}
|
||||
std::ifstream f(path);
|
||||
if (!f) return 0;
|
||||
opened = true;
|
||||
|
||||
int count = 0;
|
||||
std::string line;
|
||||
while (std::getline(f, line)) {
|
||||
std::size_t s = line.find_first_not_of(" \t");
|
||||
if (s == std::string::npos) continue;
|
||||
if (line[s] == '#') continue;
|
||||
std::string t = line.substr(s);
|
||||
while (!t.empty() && std::isspace((unsigned char)t.back())) t.pop_back();
|
||||
if (t.empty()) continue;
|
||||
++count;
|
||||
if (!exec(t, depth, r)) ++r.errors;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private:
|
||||
void emit(const std::string &line) { out_ << line << '\n'; }
|
||||
|
||||
// $name / ${name} → variable value; unknown names kept literally.
|
||||
std::string expand(const std::string &s) const {
|
||||
std::string out;
|
||||
std::size_t i = 0;
|
||||
while (i < s.size()) {
|
||||
if (s[i] != '$') { out.push_back(s[i++]); continue; }
|
||||
std::size_t j = i + 1;
|
||||
bool braces = (j < s.size() && s[j] == '{');
|
||||
if (braces) ++j;
|
||||
std::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] != '}') { 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);
|
||||
i = j;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Module *resolve_module(const std::string &name) {
|
||||
try { return sys_->modules()->get(name); }
|
||||
catch (const std::exception &) { emit("unknown module: " + name); return nullptr; }
|
||||
}
|
||||
Part *resolve_part(Module *m, const std::string &name) {
|
||||
try { return m->get(name); }
|
||||
catch (const std::exception &) {
|
||||
emit("part in " + m->name + " not found: " + name);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void render_verify() {
|
||||
VerifyReport r = verify(sys_.get());
|
||||
for (const auto &m : r.role_mismatches)
|
||||
emit(" " + m.module + "/" + m.part + "/" + m.pin + ": expected "
|
||||
+ signal_type_name(m.expected) + ", got " + signal_type_name(m.actual)
|
||||
+ " (signal: " + m.signal + ")");
|
||||
emit("verify: " + std::to_string(r.role_mismatches.size())
|
||||
+ " local mismatch(es) over " + std::to_string(r.typed_pins)
|
||||
+ " typed pin(s).");
|
||||
for (const auto &ni : r.net_inconsistencies) {
|
||||
std::string line = " net mixes Power and GndShield:";
|
||||
for (const auto &mem : ni.members)
|
||||
line += " " + mem.module + "/" + mem.signal
|
||||
+ "(" + signal_type_name(mem.type) + ")";
|
||||
emit(line);
|
||||
}
|
||||
emit("verify: " + std::to_string(r.net_inconsistencies.size())
|
||||
+ " inconsistent net(s) over " + std::to_string(r.bridged_nets)
|
||||
+ " bridged net(s) (" + std::to_string(r.total_nets) + " total).");
|
||||
emit("verify: " + std::to_string(r.orphan_total())
|
||||
+ " orphan pin(s) at import (" + std::to_string(r.orphan_imported)
|
||||
+ " imported NC, " + std::to_string(r.orphan_dropped)
|
||||
+ " dropped singleton).");
|
||||
auto grp = [&](const std::vector<Anomaly> &v, const char *tail) {
|
||||
for (const auto &a : v)
|
||||
emit(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message);
|
||||
emit("verify: " + std::to_string(v.size()) + tail);
|
||||
};
|
||||
grp(r.pin_anomalies, " model-driven pin anomaly(ies).");
|
||||
grp(r.jtag_anomalies, " JTAG chain anomaly(ies).");
|
||||
grp(r.conflict_anomalies, " source-conflict(s).");
|
||||
grp(r.completeness_anomalies, " BSDL completeness issue(s).");
|
||||
grp(r.diff_anomalies, " diff-pair crossing anomaly(ies).");
|
||||
}
|
||||
|
||||
// Execute one already-trimmed line. Returns false on a hard error.
|
||||
bool exec(const std::string &raw, int depth, ScriptResult &top) {
|
||||
std::vector<std::string> tok = tokenize(raw);
|
||||
if (tok.empty()) return true;
|
||||
const std::string cmd = tok[0];
|
||||
std::vector<std::string> a;
|
||||
for (std::size_t i = 1; i < tok.size(); ++i) a.push_back(expand(tok[i]));
|
||||
|
||||
auto need = [&](std::size_t n) {
|
||||
if (a.size() == n) return true;
|
||||
emit(cmd + ": expected " + std::to_string(n) + " argument(s)");
|
||||
return false;
|
||||
};
|
||||
|
||||
if (cmd == "set") {
|
||||
if (a.size() != 2) { emit("set: usage: set <name> <value>"); return false; }
|
||||
vars_[a[0]] = a[1];
|
||||
return true;
|
||||
}
|
||||
if (cmd == "new") {
|
||||
sys_ = std::make_unique<System>();
|
||||
emit("system created.");
|
||||
return true;
|
||||
}
|
||||
if (cmd == "load") {
|
||||
if (!need(3)) return false;
|
||||
ImportType t;
|
||||
if (!import_type_from_name(a[2], t)) { emit("unknown import type: " + a[2]); return false; }
|
||||
LoadResult r = load_module(sys_.get(), a[0], a[1], t);
|
||||
if (!r.ok) { emit("load failed: " + r.error); return false; }
|
||||
emit("loaded '" + a[0] + "' from " + a[1]);
|
||||
emit(" parts: " + std::to_string(r.parts));
|
||||
emit(" signals: " + std::to_string(r.signals)
|
||||
+ (r.dropped ? " (dropped " + std::to_string(r.dropped)
|
||||
+ " singleton/NC signal(s))" : ""));
|
||||
emit(" types: " + std::to_string(r.power) + " power, "
|
||||
+ std::to_string(r.gnd) + " gnd, " + std::to_string(r.kept_other)
|
||||
+ " suspect Power (name only — kept as Other), "
|
||||
+ std::to_string(r.mgmt)
|
||||
+ " power-management (control/measure — kept as Other)");
|
||||
return true;
|
||||
}
|
||||
if (cmd == "connect" || cmd == "plug") {
|
||||
if (!need(4)) return false;
|
||||
Module *m1 = resolve_module(a[0]); if (!m1) return false;
|
||||
Part *p1 = resolve_part(m1, a[1]); if (!p1) return false;
|
||||
Module *m2 = resolve_module(a[2]); if (!m2) return false;
|
||||
Part *p2 = resolve_part(m2, a[3]); if (!p2) return false;
|
||||
ConnectResult r = connect_parts(sys_.get(), m1, p1, m2, p2);
|
||||
if (r.refused) { emit("connect refused: " + r.error); return false; }
|
||||
if (!r.identity_info.empty()) {
|
||||
emit("connect: " + r.identity_info);
|
||||
if (r.nc_added > 0)
|
||||
emit("connect: added " + std::to_string(r.nc_added)
|
||||
+ " NC pin(s) so both sides match");
|
||||
}
|
||||
if (!r.ok) { emit("connect failed: " + r.error); return false; }
|
||||
emit("connected: " + r.connection_name + " via " + r.transform_name
|
||||
+ " (" + std::to_string(r.wires) + " wires)");
|
||||
return true;
|
||||
}
|
||||
if (cmd == "set-connector-type") {
|
||||
if (!need(3)) return false;
|
||||
Module *m = resolve_module(a[0]); if (!m) return false;
|
||||
Part *p = resolve_part(m, a[1]); if (!p) return false;
|
||||
SetConnectorTypeResult r = set_connector_type(p, a[2]);
|
||||
if (!r.ok) { emit("set-connector-type refused: " + r.error); return false; }
|
||||
emit(m->name + "/" + p->name + ": connector_type = "
|
||||
+ (a[2].empty() ? "(none)" : a[2]));
|
||||
if (r.materialised > 0)
|
||||
emit("set-connector-type: added " + std::to_string(r.materialised)
|
||||
+ " NC pin(s) from the connector layout");
|
||||
return true;
|
||||
}
|
||||
if (cmd == "set-signal-type") {
|
||||
if (!need(3)) return false;
|
||||
Module *m = resolve_module(a[0]); if (!m) return false;
|
||||
Signal *sig;
|
||||
try { sig = m->signals->get(a[1]); }
|
||||
catch (const std::exception &) {
|
||||
emit("unknown signal: " + m->name + "/" + a[1]); return false;
|
||||
}
|
||||
SetSignalTypeResult r = set_signal_type(sig, a[2]);
|
||||
if (!r.ok) { emit(r.error); return false; }
|
||||
emit(m->name + "/" + sig->name + ": signal type = " + signal_type_name(r.type));
|
||||
return true;
|
||||
}
|
||||
if (cmd == "attach-bsdl") {
|
||||
if (!need(3)) return false;
|
||||
Module *m = resolve_module(a[0]); if (!m) return false;
|
||||
Part *p = resolve_part(m, a[1]); if (!p) return false;
|
||||
AttachBsdlResult r = attach_bsdl(p, a[2]);
|
||||
if (!r.ok) { emit("attach-bsdl: " + r.error); return false; }
|
||||
emit(m->name + "/" + p->name + ": attached BSDL '" + r.entity + "' — "
|
||||
+ std::to_string(r.bound) + "/" + std::to_string(r.ports_total)
|
||||
+ " ports bound" + (r.unbound ? (", " + std::to_string(r.unbound) + " unbound") : ""));
|
||||
return true;
|
||||
}
|
||||
if (cmd == "duplicate") {
|
||||
if (!need(2)) return false;
|
||||
DuplicateResult r = duplicate_module(sys_.get(), a[0], a[1]);
|
||||
if (!r.ok) { emit(r.error); return false; }
|
||||
emit("duplicate: '" + a[0] + "' → '" + a[1] + "' ("
|
||||
+ std::to_string(r.parts) + " part(s), "
|
||||
+ std::to_string(r.signals) + " signal(s))");
|
||||
return true;
|
||||
}
|
||||
if (cmd == "verify") {
|
||||
render_verify();
|
||||
return true;
|
||||
}
|
||||
if (cmd == "export") {
|
||||
if (!need(1)) return false;
|
||||
ExportFormat fmt;
|
||||
if (!export_format_from_path(a[0], fmt)) {
|
||||
emit("export: unknown extension (use .csv or .ods): " + a[0]); return false;
|
||||
}
|
||||
ExportResult r = export_connections(sys_.get(), a[0], fmt);
|
||||
if (!r.ok) { emit("export failed: " + r.error); return false; }
|
||||
emit("exported " + std::to_string(r.rows) + " row(s) to " + a[0]);
|
||||
return true;
|
||||
}
|
||||
if (cmd == "save") {
|
||||
if (!need(1)) return false;
|
||||
std::string err;
|
||||
if (!save_system(sys_.get(), a[0], err)) { emit("save failed: " + err); return false; }
|
||||
emit("saved to " + a[0]);
|
||||
return true;
|
||||
}
|
||||
if (cmd == "restore") {
|
||||
if (!need(1)) return false;
|
||||
std::string err;
|
||||
System *fresh = restore_system(a[0], err);
|
||||
if (!fresh) { emit("restore failed: " + err); return false; }
|
||||
sys_.reset(fresh);
|
||||
emit("restored from " + a[0] + " ("
|
||||
+ std::to_string(sys_->modules()->size()) + " module(s), "
|
||||
+ std::to_string(sys_->connections()->size()) + " connection(s))");
|
||||
return true;
|
||||
}
|
||||
if (cmd == "source") {
|
||||
if (!need(1)) return false;
|
||||
bool opened;
|
||||
int n = run_file(a[0], depth + 1, top, opened);
|
||||
if (!opened) { emit("source: cannot open " + a[0]); return false; }
|
||||
emit("source: " + a[0] + " (" + std::to_string(n) + " line(s))");
|
||||
return true;
|
||||
}
|
||||
|
||||
emit("script: unsupported command '" + cmd + "'");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::unique_ptr<System> &sys_;
|
||||
std::ostream &out_;
|
||||
std::map<std::string, std::string> vars_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
ScriptResult run_script(std::unique_ptr<System> &sys, const std::string &path,
|
||||
std::ostream &out)
|
||||
{
|
||||
ScriptResult r;
|
||||
Runner runner(sys, out);
|
||||
bool opened = false;
|
||||
int n = runner.run_file(path, 0, r, opened);
|
||||
if (!opened) {
|
||||
r.error = "cannot open " + path;
|
||||
return r;
|
||||
}
|
||||
r.ok = true;
|
||||
r.lines = n;
|
||||
return r;
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
35
src/core/app/script.hpp
Normal file
35
src/core/app/script.hpp
Normal file
@@ -0,0 +1,35 @@
|
||||
#ifndef _APP_SCRIPT_HPP_
|
||||
#define _APP_SCRIPT_HPP_
|
||||
|
||||
#include <iosfwd>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
class System;
|
||||
|
||||
// Application layer: a frontend-agnostic runner for essim command scripts.
|
||||
// Dispatches the scriptable, system-building commands to the app::* operations
|
||||
// and writes their output to a stream — no console, no dialogs, no FTXUI. Any
|
||||
// frontend (and batch mode) can drive a script through it.
|
||||
namespace app {
|
||||
|
||||
// Outcome of running a script file.
|
||||
struct ScriptResult {
|
||||
bool ok = false; ///< the top-level file opened and ran
|
||||
std::string error; ///< set when the top-level file can't be opened
|
||||
int lines = 0; ///< effective (non-comment, non-blank) lines run
|
||||
int errors = 0; ///< commands that failed / were unsupported
|
||||
};
|
||||
|
||||
// Run the script at `path` against `sys`, writing per-command output to `out`.
|
||||
// `sys` is taken by reference because `new` / `restore` replace the System.
|
||||
// Supported: # comments, blank lines, set + $var/${var} expansion, new, load,
|
||||
// connect, set-connector-type, set-signal-type, attach-bsdl, verify, export,
|
||||
// save, restore, source (nested). Unsupported commands are reported and counted
|
||||
// in `errors`, and execution continues.
|
||||
ScriptResult run_script(std::unique_ptr<System> &sys, const std::string &path,
|
||||
std::ostream &out);
|
||||
|
||||
} // namespace app
|
||||
|
||||
#endif // _APP_SCRIPT_HPP_
|
||||
107
src/core/app/verify.cpp
Normal file
107
src/core/app/verify.cpp
Normal file
@@ -0,0 +1,107 @@
|
||||
#include "core/app/verify.hpp"
|
||||
|
||||
#include "core/domain/bsdl_check.hpp"
|
||||
#include "core/domain/connect.hpp"
|
||||
#include "core/domain/diff_check.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/nets.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace app {
|
||||
|
||||
VerifyReport verify(System *sys)
|
||||
{
|
||||
VerifyReport r;
|
||||
if (!sys)
|
||||
return r;
|
||||
|
||||
// Pass 1 — typed pins: expected (model) vs actual (net) signal type.
|
||||
for (auto &mkv : *sys->modules()) {
|
||||
Module *mod = mkv.second;
|
||||
for (auto &pkv : *mod) {
|
||||
Part *prt = pkv.second;
|
||||
if (prt->connector_type.empty())
|
||||
continue;
|
||||
for (auto &nkv : *prt) {
|
||||
Pin *pin = nkv.second;
|
||||
++r.typed_pins;
|
||||
SignalType expected = pin->expected_signal_type();
|
||||
if (expected == SignalType::Other)
|
||||
continue;
|
||||
Signal *s = pin->signal();
|
||||
SignalType actual = s ? s->type : SignalType::Other;
|
||||
if (actual == expected)
|
||||
continue;
|
||||
RoleMismatch m;
|
||||
m.module = mod->name;
|
||||
m.part = prt->name;
|
||||
m.pin = pin->name;
|
||||
m.signal = s ? s->name : std::string("(NC)");
|
||||
m.expected = expected;
|
||||
m.actual = actual;
|
||||
r.role_mismatches.push_back(std::move(m));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2 — bridged nets: flag Power/GndShield mixing. Compute the nets once
|
||||
// here and reuse them for the model checks below.
|
||||
std::vector<Net> nets = compute_all_nets(sys);
|
||||
r.total_nets = (int)nets.size();
|
||||
for (const Net &n : nets) {
|
||||
if (n.members.size() < 2)
|
||||
continue;
|
||||
++r.bridged_nets;
|
||||
SignalType dom;
|
||||
if (net_type_consistent(n, dom))
|
||||
continue;
|
||||
NetInconsistency ni;
|
||||
for (const auto &mp : n.members)
|
||||
ni.members.push_back({mp.first->name, mp.second->name, mp.second->type});
|
||||
r.net_inconsistencies.push_back(std::move(ni));
|
||||
}
|
||||
|
||||
// Pass 3 — orphans: pins with no signal and not bridged via a connection.
|
||||
std::unordered_set<Pin *> bridged_pins;
|
||||
for (auto &ckv : *sys->connections())
|
||||
for (auto &wp : ckv.second->pin_map) {
|
||||
if (wp.first) bridged_pins.insert(wp.first);
|
||||
if (wp.second) bridged_pins.insert(wp.second);
|
||||
}
|
||||
for (auto &mkv : *sys->modules())
|
||||
for (auto &pkv : *mkv.second)
|
||||
for (auto &nkv : *pkv.second) {
|
||||
Pin *pin = nkv.second;
|
||||
if (pin->signal() || bridged_pins.count(pin))
|
||||
continue;
|
||||
bool dropped;
|
||||
if (pin->nc_origin == NcOrigin::ImportedUnconnected) {
|
||||
++r.orphan_imported;
|
||||
dropped = false;
|
||||
} else if (pin->nc_origin == NcOrigin::DroppedSingleton) {
|
||||
++r.orphan_dropped;
|
||||
dropped = true;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
r.orphans.push_back({mkv.first, pkv.first, nkv.first, dropped});
|
||||
}
|
||||
|
||||
// Passes 4-8 — model-driven checks (reuse the nets from pass 2).
|
||||
r.pin_anomalies = check_pin_specs(sys, &nets);
|
||||
r.jtag_anomalies = check_jtag_chain(sys, &nets);
|
||||
r.conflict_anomalies = check_source_conflicts(sys);
|
||||
r.completeness_anomalies = check_bsdl_completeness(sys);
|
||||
r.diff_anomalies = check_diff_crossings(sys, &nets);
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
71
src/core/app/verify.hpp
Normal file
71
src/core/app/verify.hpp
Normal file
@@ -0,0 +1,71 @@
|
||||
#ifndef _APP_VERIFY_HPP_
|
||||
#define _APP_VERIFY_HPP_
|
||||
|
||||
#include "core/domain/analysis.hpp" // Anomaly
|
||||
#include "core/domain/signal_type.hpp" // SignalType
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class System;
|
||||
|
||||
namespace app {
|
||||
|
||||
// One typed-pin role mismatch: the connector/BSDL expectation disagrees with
|
||||
// the actual net type.
|
||||
struct RoleMismatch {
|
||||
std::string module, part, pin;
|
||||
std::string signal; ///< signal name, or "(NC)"
|
||||
SignalType expected = SignalType::Other;
|
||||
SignalType actual = SignalType::Other;
|
||||
};
|
||||
|
||||
// One bridged net mixing Power and GndShield, with its members for display.
|
||||
struct NetInconsistency {
|
||||
struct Member { std::string module, signal; SignalType type; };
|
||||
std::vector<Member> members;
|
||||
};
|
||||
|
||||
// One orphan pin: no signal and not bridged via a connection. `dropped` is true
|
||||
// for a dropped singleton (essim detached it), false for an import-time NC.
|
||||
struct OrphanPin {
|
||||
std::string module, part, pin;
|
||||
bool dropped = false;
|
||||
};
|
||||
|
||||
// The full result of `verify`: structured data only — no strings beyond the
|
||||
// names, no formatting. Frontends (the verify command, the analyze screen, the
|
||||
// dashboard) render it however they like.
|
||||
struct VerifyReport {
|
||||
int typed_pins = 0; ///< pins with a non-Other expectation considered
|
||||
std::vector<RoleMismatch> role_mismatches;
|
||||
|
||||
int total_nets = 0;
|
||||
int bridged_nets = 0;
|
||||
std::vector<NetInconsistency> net_inconsistencies;
|
||||
|
||||
int orphan_imported = 0;
|
||||
int orphan_dropped = 0;
|
||||
std::vector<OrphanPin> orphans; ///< per-pin detail (both origins)
|
||||
|
||||
std::vector<Anomaly> pin_anomalies; ///< check_pin_specs
|
||||
std::vector<Anomaly> jtag_anomalies; ///< check_jtag_chain
|
||||
std::vector<Anomaly> conflict_anomalies; ///< check_source_conflicts
|
||||
std::vector<Anomaly> completeness_anomalies; ///< check_bsdl_completeness
|
||||
std::vector<Anomaly> diff_anomalies; ///< check_diff_crossings
|
||||
|
||||
int orphan_total() const { return orphan_imported + orphan_dropped; }
|
||||
int model_total() const {
|
||||
return (int)(pin_anomalies.size() + jtag_anomalies.size()
|
||||
+ conflict_anomalies.size() + completeness_anomalies.size()
|
||||
+ diff_anomalies.size());
|
||||
}
|
||||
};
|
||||
|
||||
// Run every verify pass over the system and return the findings. Pure core —
|
||||
// computes the nets once and feeds them to the net-based checks.
|
||||
VerifyReport verify(System *sys);
|
||||
|
||||
} // namespace app
|
||||
|
||||
#endif // _APP_VERIFY_HPP_
|
||||
@@ -31,15 +31,15 @@ const char *anomaly_kind_name(AnomalyKind k) {
|
||||
case AnomalyKind::JtagBusUnbridged: return "jtag-bus-unbridged";
|
||||
case AnomalyKind::SourceConflict: return "source-conflict";
|
||||
case AnomalyKind::BsdlPinMissing: return "bsdl-pin-missing";
|
||||
case AnomalyKind::DiffPolaritySwap: return "diff-polarity-swap";
|
||||
case AnomalyKind::DiffCrossIncomplete: return "diff-cross-incomplete";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Diff-pair suffix detection. Returns true and fills <stem, polarity> if
|
||||
// `name` ends with one of {_P, _N, _p, _n} preceded by a non-suffix char.
|
||||
// 'P' / 'N' result is normalised to uppercase.
|
||||
// 'P' / 'N' result is normalised to uppercase. Shared with diff_check.cpp.
|
||||
bool diff_suffix(const std::string &name, std::string &stem, char &pol) {
|
||||
if (name.size() < 3) return false;
|
||||
char last = name.back();
|
||||
@@ -52,6 +52,29 @@ bool diff_suffix(const std::string &name, std::string &stem, char &pol) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Tool-internal net names we never want to surface to the user (Mentor's
|
||||
// `$Nxxxx` convention, ODS placeholders, etc.). Cheap prefix check.
|
||||
bool is_internal_name(const std::string &n) {
|
||||
return !n.empty() && n[0] == '$';
|
||||
}
|
||||
|
||||
// Trailing-integer split: "MDI0" → ("MDI", 0); "PCIE_TX_3" → ("PCIE_TX_", 3);
|
||||
// "USB" → false (no trailing digits). Used for diff-bus aggregation only —
|
||||
// the strict `_` rule from `numeric_suffix` does NOT apply here because the
|
||||
// caller has already stripped a `_P` / `_N` polarity suffix, so we know the
|
||||
// remaining digits are an index rather than part of a longer name.
|
||||
bool split_trailing_index(const std::string &s, std::string &outer, int &idx) {
|
||||
if (s.empty()) return false;
|
||||
size_t i = s.size();
|
||||
while (i > 0 && std::isdigit((unsigned char)s[i - 1])) --i;
|
||||
if (i == s.size() || i == 0) return false;
|
||||
idx = std::atoi(s.c_str() + i);
|
||||
outer = s.substr(0, i);
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Bus suffix detection. Two accepted forms:
|
||||
// - bracketed: NAME[12] → stem "NAME", idx 12
|
||||
// - underscore: NAME_12 → stem "NAME_", idx 12 (underscore is REQUIRED
|
||||
@@ -81,27 +104,6 @@ bool numeric_suffix(const std::string &name, std::string &stem, int &idx,
|
||||
return true;
|
||||
}
|
||||
|
||||
// Tool-internal net names we never want to surface to the user (Mentor's
|
||||
// `$Nxxxx` convention, ODS placeholders, etc.). Cheap prefix check.
|
||||
bool is_internal_name(const std::string &n) {
|
||||
return !n.empty() && n[0] == '$';
|
||||
}
|
||||
|
||||
// Trailing-integer split: "MDI0" → ("MDI", 0); "PCIE_TX_3" → ("PCIE_TX_", 3);
|
||||
// "USB" → false (no trailing digits). Used for diff-bus aggregation only —
|
||||
// the strict `_` rule from `numeric_suffix` does NOT apply here because the
|
||||
// caller has already stripped a `_P` / `_N` polarity suffix, so we know the
|
||||
// remaining digits are an index rather than part of a longer name.
|
||||
bool split_trailing_index(const std::string &s, std::string &outer, int &idx) {
|
||||
if (s.empty()) return false;
|
||||
size_t i = s.size();
|
||||
while (i > 0 && std::isdigit((unsigned char)s[i - 1])) --i;
|
||||
if (i == s.size() || i == 0) return false;
|
||||
idx = std::atoi(s.c_str() + i);
|
||||
outer = s.substr(0, i);
|
||||
return true;
|
||||
}
|
||||
|
||||
void analyse_module(Module *mod, AnalysisReport &out) {
|
||||
// ---- Pass 1: diff pairs ----
|
||||
std::unordered_map<std::string, std::pair<Signal *, Signal *>> dp; // stem -> {P, N}
|
||||
@@ -276,13 +278,21 @@ SignalTypeInferenceStats infer_signal_types(System *sys) {
|
||||
Module *mod = mkv.second;
|
||||
for (auto &skv : *mod->signals) {
|
||||
Signal *s = skv.second;
|
||||
SignalType named = infer_signal_type(s->name);
|
||||
if (named == SignalType::GndShield) {
|
||||
NameClassification ncl = classify_signal_name(s->name);
|
||||
if (ncl.verdict == NameVerdict::GndShield) {
|
||||
s->type = SignalType::GndShield;
|
||||
++st.gnd;
|
||||
continue;
|
||||
}
|
||||
if (named == SignalType::Power) {
|
||||
if (ncl.verdict == NameVerdict::PowerMgmt) {
|
||||
// A rail token next to a control token (SENSE, EN, PG, …):
|
||||
// a signal about a rail, confidently NOT the rail — never
|
||||
// suspect, whatever the fan-out.
|
||||
s->type = SignalType::Other;
|
||||
++st.mgmt;
|
||||
continue;
|
||||
}
|
||||
if (ncl.verdict == NameVerdict::Rail) {
|
||||
int fanout = (int)s->size();
|
||||
// Hard rule: a "power" net that touches fewer than three
|
||||
// pins cannot physically be a rail (a real rail goes to
|
||||
|
||||
@@ -39,6 +39,8 @@ enum class AnomalyKind {
|
||||
JtagBusUnbridged, ///< TMS or TCK is not common to all TAP devices.
|
||||
SourceConflict, ///< A model contradicts the netlist (e.g. BSDL power pin left NC).
|
||||
BsdlPinMissing, ///< A BSDL power/ground port has no pin on the netlist part.
|
||||
DiffPolaritySwap, ///< A diff pair crosses a connection with P and N swapped.
|
||||
DiffCrossIncomplete, ///< A diff pair/bus only partially crosses a connection.
|
||||
};
|
||||
|
||||
struct Anomaly {
|
||||
@@ -62,6 +64,8 @@ struct SignalTypeInferenceStats {
|
||||
int power = 0; ///< Signals promoted to Power (name + structural).
|
||||
int gnd = 0; ///< Signals promoted to GndShield (name only).
|
||||
int kept_other = 0; ///< Name said Power but structural evidence too weak.
|
||||
int mgmt = 0; ///< Power-management signal (rail + SENSE/EN/PG/…
|
||||
///< token) → confidently Other, never suspect.
|
||||
};
|
||||
|
||||
// Thresholds used by `infer_signal_types` (re-exposed so the analyze screen
|
||||
@@ -71,6 +75,15 @@ inline constexpr int POWER_FANOUT_CONFIRM_MIN = 4; ///< ≥ this confirms Powe
|
||||
|
||||
bool has_voltage_pattern(const std::string &name);
|
||||
|
||||
// Name-parsing helpers shared with the diff-crossing check (diff_check.cpp).
|
||||
// diff_suffix: true if `name` ends with _P/_N (case-insensitive); fills the
|
||||
// stem and the polarity normalised to uppercase 'P'/'N'.
|
||||
// split_trailing_index: "MDI0" → ("MDI", 0); false without trailing digits.
|
||||
// is_internal_name: tool-internal net names never surfaced ($Nxxxx …).
|
||||
bool diff_suffix(const std::string &name, std::string &stem, char &pol);
|
||||
bool split_trailing_index(const std::string &s, std::string &outer, int &idx);
|
||||
bool is_internal_name(const std::string &n);
|
||||
|
||||
// Best-effort signal-type inference. Sets `Signal::type`:
|
||||
// - GndShield when the name unambiguously matches GND/SHIELD/CHASSIS/EARTH.
|
||||
// - Power when the name suggests Power AND there is structural evidence
|
||||
|
||||
194
src/core/domain/diff_check.cpp
Normal file
194
src/core/domain/diff_check.cpp
Normal file
@@ -0,0 +1,194 @@
|
||||
#include "diff_check.hpp"
|
||||
|
||||
#include "modules.hpp"
|
||||
#include "signals.hpp"
|
||||
#include "system.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
|
||||
namespace {
|
||||
|
||||
// One complete local diff pair with the net ids of its two legs.
|
||||
struct LocalPair {
|
||||
Module *mod = nullptr;
|
||||
std::string stem;
|
||||
Signal *p = nullptr, *n = nullptr;
|
||||
int np = -1, nn = -1;
|
||||
};
|
||||
|
||||
std::string pair_label(const LocalPair &lp) {
|
||||
return lp.mod->name + "/" + lp.stem + "_P/N";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<Anomaly> check_diff_crossings(System *sys,
|
||||
const std::vector<Net> *nets)
|
||||
{
|
||||
std::vector<Anomaly> out;
|
||||
if (!sys || !nets) return out;
|
||||
|
||||
// Signal → net id (compute_all_nets covers every signal, singletons too).
|
||||
std::unordered_map<Signal *, int> net_of;
|
||||
for (size_t i = 0; i < nets->size(); ++i)
|
||||
for (const auto &mp : (*nets)[i].members)
|
||||
net_of[mp.second] = (int)i;
|
||||
|
||||
// Complete local pairs, module by module. Orphan halves (X_P without
|
||||
// X_N) are analysis's DiffPairOrphan business — skipped here.
|
||||
std::vector<LocalPair> pairs;
|
||||
std::unordered_map<Signal *, int> pair_of; // leg signal → index in `pairs`
|
||||
for (auto &mkv : *sys->modules()) {
|
||||
Module *mod = mkv.second;
|
||||
std::map<std::string, LocalPair> by_stem;
|
||||
for (auto &skv : *mod->signals) {
|
||||
if (is_internal_name(skv.first)) continue;
|
||||
std::string stem; char pol;
|
||||
if (!diff_suffix(skv.first, stem, pol)) continue;
|
||||
LocalPair &lp = by_stem[stem];
|
||||
lp.mod = mod; lp.stem = stem;
|
||||
if (pol == 'P') lp.p = skv.second;
|
||||
else lp.n = skv.second;
|
||||
}
|
||||
for (auto &kv : by_stem) {
|
||||
LocalPair lp = kv.second;
|
||||
if (!lp.p || !lp.n) continue;
|
||||
auto ip = net_of.find(lp.p), in = net_of.find(lp.n);
|
||||
if (ip == net_of.end() || in == net_of.end()) continue;
|
||||
lp.np = ip->second;
|
||||
lp.nn = in->second;
|
||||
int idx = (int)pairs.size();
|
||||
pairs.push_back(lp);
|
||||
pair_of[lp.p] = idx;
|
||||
pair_of[lp.n] = idx;
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 1 — pair against pair. Each unordered couple of pairs is judged
|
||||
// once (dedup set), so A↔B is never also reported as B↔A.
|
||||
std::set<std::pair<int, int>> seen;
|
||||
for (int i = 0; i < (int)pairs.size(); ++i) {
|
||||
const LocalPair &a = pairs[i];
|
||||
if (a.np == a.nn) {
|
||||
// Degenerate: both legs land on one net — only the connections
|
||||
// can do that (two module-local signals are distinct by nature).
|
||||
Anomaly an;
|
||||
an.kind = AnomalyKind::DiffPolaritySwap;
|
||||
an.module = a.mod;
|
||||
an.message = a.mod->name + ": " + a.stem + "_P and " + a.stem
|
||||
+ "_N join the same net (through the connections)";
|
||||
an.involved = {a.p, a.n};
|
||||
out.push_back(std::move(an));
|
||||
continue;
|
||||
}
|
||||
// Candidate peers: pairs of OTHER modules with a leg on net np or nn.
|
||||
std::set<int> cands;
|
||||
for (int net : {a.np, a.nn})
|
||||
for (const auto &mp : (*nets)[net].members) {
|
||||
auto it = pair_of.find(mp.second);
|
||||
if (it == pair_of.end()) continue;
|
||||
const LocalPair &b = pairs[it->second];
|
||||
if (b.mod == a.mod) continue; // intra-module: nothing to say
|
||||
if (b.np == b.nn) continue; // degenerate: own anomaly above
|
||||
cands.insert(it->second);
|
||||
}
|
||||
for (int j : cands) {
|
||||
std::pair<int, int> key = std::minmax(i, j);
|
||||
if (!seen.insert(key).second) continue;
|
||||
const LocalPair &b = pairs[j];
|
||||
if (a.np == b.np && a.nn == b.nn) continue; // straight: all good
|
||||
Anomaly an;
|
||||
an.module = a.mod;
|
||||
an.involved = {a.p, a.n, b.p, b.n};
|
||||
if (a.np == b.nn && a.nn == b.np) {
|
||||
an.kind = AnomalyKind::DiffPolaritySwap;
|
||||
an.message = pair_label(a) + " <-> " + pair_label(b)
|
||||
+ ": polarity swapped (P legs meet N legs)";
|
||||
} else {
|
||||
an.kind = AnomalyKind::DiffCrossIncomplete;
|
||||
std::string how;
|
||||
if (a.np == b.np) how = "only the P legs are bridged";
|
||||
else if (a.nn == b.nn) how = "only the N legs are bridged";
|
||||
else if (a.np == b.nn) how = "P leg bridged to N leg; "
|
||||
"the other legs are not";
|
||||
else how = "N leg bridged to P leg; "
|
||||
"the other legs are not";
|
||||
an.message = pair_label(a) + " <-> " + pair_label(b)
|
||||
+ ": " + how;
|
||||
}
|
||||
out.push_back(std::move(an));
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2 — diff buses at a crossing. Lanes grouped by outer stem. A lane
|
||||
// is "dangling" when it crosses to NO module at all while sibling lanes
|
||||
// do cross — distributed buses (lanes fanned out to different peers, a
|
||||
// backplane classic) are legitimate and stay silent. Lanes crossing
|
||||
// partially are already reported above, so they don't count as dangling.
|
||||
// One aggregated anomaly per bus, per side (each side names its lanes).
|
||||
std::map<std::pair<Module *, std::string>, std::map<int, int>> groups;
|
||||
for (int i = 0; i < (int)pairs.size(); ++i) {
|
||||
std::string outer; int idx;
|
||||
if (!split_trailing_index(pairs[i].stem, outer, idx)) continue;
|
||||
groups[{pairs[i].mod, outer}][idx] = i;
|
||||
}
|
||||
for (auto &gkv : groups) {
|
||||
auto &lanes = gkv.second; // lane index → pair index
|
||||
if (lanes.size() < 2) continue;
|
||||
std::set<int> touching_any; // lanes sharing ≥1 net with a peer
|
||||
std::set<int> complete_any; // lanes fully crossing somewhere
|
||||
std::set<Module *> reached;
|
||||
for (auto &lkv : lanes) {
|
||||
const LocalPair &a = pairs[lkv.second];
|
||||
if (a.np == a.nn) continue;
|
||||
for (int net : {a.np, a.nn})
|
||||
for (const auto &mp : (*nets)[net].members) {
|
||||
auto it = pair_of.find(mp.second);
|
||||
if (it == pair_of.end()) continue;
|
||||
const LocalPair &b = pairs[it->second];
|
||||
if (b.mod == a.mod) continue;
|
||||
touching_any.insert(lkv.first);
|
||||
bool straight = (a.np == b.np && a.nn == b.nn);
|
||||
bool swapped = (a.np == b.nn && a.nn == b.np);
|
||||
if (straight || swapped) {
|
||||
complete_any.insert(lkv.first);
|
||||
reached.insert(b.mod);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (complete_any.empty()) continue; // fully local bus: fine
|
||||
std::vector<int> dangling;
|
||||
for (auto &lkv : lanes)
|
||||
if (!touching_any.count(lkv.first))
|
||||
dangling.push_back(lkv.first);
|
||||
if (dangling.empty()) continue;
|
||||
int lo = lanes.begin()->first;
|
||||
int hi = lanes.rbegin()->first;
|
||||
Anomaly an;
|
||||
an.kind = AnomalyKind::DiffCrossIncomplete;
|
||||
an.module = gkv.first.first;
|
||||
std::string m = gkv.first.first->name + ": " + gkv.first.second
|
||||
+ "[" + std::to_string(lo) + ".."
|
||||
+ std::to_string(hi) + "]_P/N: lane(s)";
|
||||
for (int ix : dangling) m += " " + std::to_string(ix);
|
||||
m += " do not cross (others reach";
|
||||
std::vector<std::string> names;
|
||||
for (Module *mod : reached) names.push_back(mod->name);
|
||||
std::sort(names.begin(), names.end());
|
||||
for (const std::string &nm : names) m += " " + nm;
|
||||
m += ")";
|
||||
an.message = std::move(m);
|
||||
for (auto &lkv : lanes) {
|
||||
an.involved.push_back(pairs[lkv.second].p);
|
||||
an.involved.push_back(pairs[lkv.second].n);
|
||||
}
|
||||
out.push_back(std::move(an));
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
27
src/core/domain/diff_check.hpp
Normal file
27
src/core/domain/diff_check.hpp
Normal file
@@ -0,0 +1,27 @@
|
||||
#ifndef _DIFF_CHECK_HPP_
|
||||
#define _DIFF_CHECK_HPP_
|
||||
|
||||
#include "analysis.hpp" // Anomaly, diff_suffix, split_trailing_index
|
||||
#include "nets.hpp" // Net
|
||||
|
||||
#include <vector>
|
||||
|
||||
class System;
|
||||
|
||||
// Differential-pair crossing checks. Every complete local diff pair
|
||||
// (X_P / X_N, name-based) resolves its two legs to two bridged nets; any
|
||||
// other module whose own pair sits on those nets must match them leg for
|
||||
// leg. Findings:
|
||||
// - DiffPolaritySwap: the peer pair is wired P→N / N→P, or a pair's two
|
||||
// legs end up joined onto one single net through the connections.
|
||||
// - DiffCrossIncomplete: the two pairs share only one leg (the other does
|
||||
// not cross), or some lanes of a diff bus do not reach a module the
|
||||
// other lanes reach.
|
||||
// Name-based on BOTH sides: a peer whose signals carry no _P/_N suffix is
|
||||
// not judged (silent). Polarity swaps are sometimes intentional (routing
|
||||
// compensation, SerDes with configurable polarity) — these are findings to
|
||||
// review, not hard errors. `nets` must come from compute_all_nets(sys).
|
||||
std::vector<Anomaly> check_diff_crossings(System *sys,
|
||||
const std::vector<Net> *nets);
|
||||
|
||||
#endif // _DIFF_CHECK_HPP_
|
||||
@@ -5,8 +5,25 @@
|
||||
|
||||
enum class SignalType { Power, GndShield, Other };
|
||||
|
||||
// Name-level verdict, richer than SignalType. `PowerMgmt` is the key
|
||||
// addition: a name holding BOTH a rail token (VCC/VDD/PWR/…) and a control
|
||||
// token (SENSE/EN/PG/FB/…) is a power-management signal — measurement,
|
||||
// enable, power-good — not the rail itself. Its non-Power classification is
|
||||
// therefore confident, where a bare rail name without structural evidence
|
||||
// stays suspect.
|
||||
enum class NameVerdict { Rail, GndShield, PowerMgmt, Other };
|
||||
|
||||
struct NameClassification {
|
||||
NameVerdict verdict = NameVerdict::Other;
|
||||
std::string token; ///< PowerMgmt only: the control token that decided it.
|
||||
};
|
||||
|
||||
NameClassification classify_signal_name(const std::string &signal_name);
|
||||
|
||||
const char *signal_type_name(SignalType t);
|
||||
bool signal_type_from_name(const std::string &s, SignalType &out);
|
||||
// Thin wrapper over classify_signal_name: Rail → Power, GndShield → GndShield,
|
||||
// PowerMgmt/Other → Other.
|
||||
SignalType infer_signal_type(const std::string &signal_name);
|
||||
SignalType next_signal_type(SignalType t); // Power → GndShield → Other → Power
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
const char *signal_type_name(SignalType t) {
|
||||
@@ -36,11 +37,74 @@ bool signal_type_from_name(const std::string &s, SignalType &out) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Heuristic. Names like GND, GNDA, SHIELD, CHASSIS → GndShield.
|
||||
// Names containing PWR/POWER/VCC/VDD/VEE/VSS, or matching V±N or +N.NV
|
||||
// patterns, or starting with VS_/VS3_ → Power. Else Other.
|
||||
SignalType infer_signal_type(const std::string &name) {
|
||||
if (name.empty()) return SignalType::Other;
|
||||
namespace {
|
||||
|
||||
// Control/monitoring vocabulary: a name holding both a rail token and one of
|
||||
// these is a signal ABOUT a rail (feedback, enable, power-good, fault, …) —
|
||||
// not the rail itself. Matched against whole separator-delimited tokens
|
||||
// (uppercase, trailing digits stripped so EN1/PG0 still hit). Entries of
|
||||
// length ≥ 4 also match as a token *suffix*, catching fused (VSENSE, PWRGOOD)
|
||||
// and active-low (NFAULT) forms; short entries match exactly, so GREEN or
|
||||
// SENSOR never trip on EN / SENSE.
|
||||
const char *const kPowerControlTokens[] = {
|
||||
"SENSE", "SNS", "KELVIN", // remote / Kelvin sense
|
||||
"FB", "FBK", "FDB", "FDBK", "VFB", // regulator feedback
|
||||
"FEEDBACK",
|
||||
"EN", "ENA", "ENABLE", "INH", "INHIBIT", // enable / inhibit
|
||||
"PG", "PGOOD", "PWRGD", "PWROK", // power-good
|
||||
"GOOD", "OK", "FAIL", "FAULT", "FLT", // status / fault
|
||||
"ALERT", "ALRT", "WARN",
|
||||
"MON", "IMON", "VMON", "PMON", // monitoring
|
||||
"DET", "DETECT", "PRSNT", "PRESENT", // presence detection
|
||||
"OC", "OCP", "OV", "OVP", "UV", "UVP", // protection trips
|
||||
"TRIP", "SHDN", "SHUTDOWN",
|
||||
"ADJ", "ADJUST", "VADJ", "TRIM", // regulator adjust / trim
|
||||
"MARG", "MARGIN", // voltage margining
|
||||
"SET", "VSET", "ISET", // set-point pins
|
||||
"SEQ", "CTRL", "CTL", "CMD", // sequencing / control / command
|
||||
"STAT", "STATUS",
|
||||
"ON", "OFF", "BTN", // on/off request
|
||||
"REF", "VREF", // voltage reference
|
||||
"LED", // indicator drive
|
||||
"CS", "IRQ",
|
||||
};
|
||||
|
||||
bool is_power_control_token(std::string tok) {
|
||||
while (!tok.empty() && std::isdigit((unsigned char)tok.back()))
|
||||
tok.pop_back(); // EN1, PG0, FB2 …
|
||||
if (tok.empty()) return false;
|
||||
for (const char *lex : kPowerControlTokens) {
|
||||
size_t n = std::strlen(lex);
|
||||
if (tok == lex) return true;
|
||||
if (n >= 4 && tok.size() > n
|
||||
&& tok.compare(tok.size() - n, n, lex) == 0)
|
||||
return true; // VSENSE, PWRGOOD, NFAULT …
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Split on every non-alphanumeric character. `u` is already uppercase.
|
||||
std::vector<std::string> alnum_tokens(const std::string &u) {
|
||||
std::vector<std::string> out;
|
||||
std::string cur;
|
||||
for (char c : u) {
|
||||
if (std::isalnum((unsigned char)c)) { cur += c; continue; }
|
||||
if (!cur.empty()) { out.push_back(std::move(cur)); cur.clear(); }
|
||||
}
|
||||
if (!cur.empty()) out.push_back(std::move(cur));
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Heuristic. Names like GND, GNDA, SHIELD, CHASSIS → GndShield (name alone is
|
||||
// reliable there — left out of the control-token logic on purpose). Names
|
||||
// containing PWR/POWER/VCC/VDD/VEE/VSS, or starting with VS_/VBAT/+/− → rail
|
||||
// candidates; a rail candidate whose tokens include a control word (SENSE,
|
||||
// EN, PG, …) is downgraded to PowerMgmt. Else Other.
|
||||
NameClassification classify_signal_name(const std::string &name) {
|
||||
NameClassification out;
|
||||
if (name.empty()) return out;
|
||||
std::string u = name;
|
||||
std::transform(u.begin(), u.end(), u.begin(),
|
||||
[](unsigned char c) { return std::toupper(c); });
|
||||
@@ -52,14 +116,14 @@ SignalType infer_signal_type(const std::string &name) {
|
||||
return u.rfind(needle, 0) == 0;
|
||||
};
|
||||
|
||||
if (u == "GND" || u == "GROUND") return SignalType::GndShield;
|
||||
if (starts_with("GND_")
|
||||
if (u == "GND" || u == "GROUND"
|
||||
|| starts_with("GND_")
|
||||
|| (starts_with("GND") && u.size() >= 4
|
||||
&& std::isalpha((unsigned char)u[3]))) {
|
||||
return SignalType::GndShield;
|
||||
&& std::isalpha((unsigned char)u[3]))
|
||||
|| contains("SHIELD") || contains("CHASSIS") || contains("EARTH")) {
|
||||
out.verdict = NameVerdict::GndShield;
|
||||
return out;
|
||||
}
|
||||
if (contains("SHIELD") || contains("CHASSIS") || contains("EARTH"))
|
||||
return SignalType::GndShield;
|
||||
|
||||
if (contains("PWR") || contains("POWER")
|
||||
|| contains("VCC") || contains("VDD") || contains("VEE") || contains("VSS")
|
||||
@@ -67,7 +131,25 @@ SignalType infer_signal_type(const std::string &name) {
|
||||
|| starts_with("VS3_") || starts_with("VS4_")
|
||||
|| starts_with("VBAT") || starts_with("VBUS")
|
||||
|| starts_with("+") || starts_with("-")) {
|
||||
return SignalType::Power;
|
||||
for (const std::string &tok : alnum_tokens(u)) {
|
||||
if (is_power_control_token(tok)) {
|
||||
out.verdict = NameVerdict::PowerMgmt;
|
||||
out.token = tok;
|
||||
return out;
|
||||
}
|
||||
}
|
||||
out.verdict = NameVerdict::Rail;
|
||||
return out;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
SignalType infer_signal_type(const std::string &name) {
|
||||
switch (classify_signal_name(name).verdict) {
|
||||
case NameVerdict::Rail: return SignalType::Power;
|
||||
case NameVerdict::GndShield: return SignalType::GndShield;
|
||||
case NameVerdict::PowerMgmt:
|
||||
case NameVerdict::Other: break;
|
||||
}
|
||||
return SignalType::Other;
|
||||
}
|
||||
|
||||
@@ -21,14 +21,8 @@ System::~System()
|
||||
|
||||
void System::Load(std::string module_name, std::string file_name, ImportType type)
|
||||
{
|
||||
// Build the importer first, based on the import type.
|
||||
ImportBase *imp;
|
||||
Module *mod = nullptr;
|
||||
Parts *prts = nullptr;
|
||||
|
||||
// Creation or retrieval of the module.
|
||||
mod = mods->merge(module_name);
|
||||
|
||||
// Parsing of the file based on the import type.
|
||||
if (type == ImportType::IMPORT_MENTOR)
|
||||
{
|
||||
imp = new ImportMentor(file_name);
|
||||
@@ -43,7 +37,20 @@ void System::Load(std::string module_name, std::string file_name, ImportType typ
|
||||
{
|
||||
throw std::runtime_error("Unknown import type");
|
||||
}
|
||||
imp->parse(mod->signals);
|
||||
prts = imp->parts();
|
||||
mod->add(prts);
|
||||
|
||||
// Fail fast on a missing/unreadable file, before touching the module table,
|
||||
// so a failed load never leaves behind an empty module.
|
||||
if (!imp->is_open())
|
||||
{
|
||||
delete imp;
|
||||
throw std::runtime_error("cannot open file: " + file_name);
|
||||
}
|
||||
|
||||
// Creation or retrieval of the module, then parse into it. add() copies the
|
||||
// Part pointers into the module, which takes ownership; deleting the
|
||||
// importer then frees the (now drained) Parts container, not the parts.
|
||||
Module *mod = mods->merge(module_name);
|
||||
imp->parse(mod->signals);
|
||||
mod->add(imp->parts());
|
||||
delete imp;
|
||||
}
|
||||
@@ -27,11 +27,22 @@ public:
|
||||
*
|
||||
* @param file_name Name of the file to be imported.
|
||||
*/
|
||||
ImportBase(std::string file_name) : file_lines(std::fstream(file_name))
|
||||
ImportBase(std::string file_name) : file_lines(file_name, std::ios::in)
|
||||
{
|
||||
prts = new Parts();
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Whether the input file was opened successfully.
|
||||
*
|
||||
* Opened read-only, so this is false only when the file is genuinely
|
||||
* missing or unreadable (a read-only but present file still opens).
|
||||
* System::Load checks it to fail fast instead of producing an empty module.
|
||||
*
|
||||
* @return true if the file stream is open.
|
||||
*/
|
||||
bool is_open() const { return file_lines.is_open(); }
|
||||
|
||||
/**
|
||||
* @brief Pure virtual method for parsing the file.
|
||||
*
|
||||
@@ -53,9 +64,13 @@ public:
|
||||
/**
|
||||
* @brief Virtual destructor for ImportBase.
|
||||
*
|
||||
* Ensures proper cleanup of derived classes.
|
||||
* Frees the Parts container object. Only the container is deleted, not the
|
||||
* Part objects it holds: by the time the importer is destroyed those have
|
||||
* been transferred to a Module (SystemElementContainer::add copies the
|
||||
* pointers), which owns and deletes them. The default ~Parts frees the map
|
||||
* without touching the elements, so there is no double free.
|
||||
*/
|
||||
virtual ~ImportBase() = default;
|
||||
virtual ~ImportBase() { delete prts; }
|
||||
};
|
||||
|
||||
#endif // _IMPORT_BASE_HPP_
|
||||
@@ -43,16 +43,6 @@ enum class State
|
||||
*/
|
||||
ImportMentor::ImportMentor(string filename) : ImportBase(filename) {}
|
||||
|
||||
/**
|
||||
* @brief Destructor for ImportMentor.
|
||||
*
|
||||
* Ensures proper cleanup by calling the base class destructor.
|
||||
*/
|
||||
ImportMentor::~ImportMentor()
|
||||
{
|
||||
ImportBase::~ImportBase();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parses the file to extract parts, pins, and signals.
|
||||
*
|
||||
|
||||
@@ -10,7 +10,6 @@ class ImportMentor : public ImportBase
|
||||
public:
|
||||
ImportMentor(std::string filename);
|
||||
void parse(Signals *signals) override;
|
||||
~ImportMentor();
|
||||
};
|
||||
|
||||
#endif // _IMPORT_MENTOR_HPP_
|
||||
30
src/frontends/frontend.hpp
Normal file
30
src/frontends/frontend.hpp
Normal file
@@ -0,0 +1,30 @@
|
||||
#ifndef _FRONTEND_HPP_
|
||||
#define _FRONTEND_HPP_
|
||||
|
||||
#include <iosfwd>
|
||||
#include <string>
|
||||
|
||||
// Abstract entry-point interface every frontend (TUI, GUI, …) implements, so
|
||||
// one shared launcher (frontend_main) can drive any of them: parse argv, run
|
||||
// boot commands, optionally dump output (batch / docs) and enter the event
|
||||
// loop. Lives in the frontends layer — essim_core never depends on it.
|
||||
class Frontend {
|
||||
public:
|
||||
virtual ~Frontend() = default;
|
||||
|
||||
// Dispatch one command synchronously, exactly as if the user typed it
|
||||
// (e.g. "restore foo.essim" or "source bring-up.essim"), before the event
|
||||
// loop starts — used to seed the system at boot.
|
||||
virtual void BootDispatch(const std::string &raw) = 0;
|
||||
|
||||
// Write the command registry as Markdown (used for doc generation).
|
||||
virtual void DumpCommandsMd(std::ostream &out) const = 0;
|
||||
|
||||
// Write the accumulated console output (batch mode: no event loop).
|
||||
virtual void DumpOutput(std::ostream &out) const = 0;
|
||||
|
||||
// Enter the interactive event loop.
|
||||
virtual void Run() = 0;
|
||||
};
|
||||
|
||||
#endif // _FRONTEND_HPP_
|
||||
99
src/frontends/frontend_main.cpp
Normal file
99
src/frontends/frontend_main.cpp
Normal file
@@ -0,0 +1,99 @@
|
||||
#include "frontends/frontend_main.hpp"
|
||||
|
||||
#include "frontends/frontend.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
void print_usage(const char *prog) {
|
||||
std::cerr <<
|
||||
"usage: " << prog << " [--batch] [--source FILE] [--restore FILE]\n"
|
||||
" " << prog << " --commands-md [FILE]\n"
|
||||
" " << prog << " --help\n"
|
||||
" (no args) launch the interface on an empty system.\n"
|
||||
" --source FILE after boot, run FILE as an essim script\n"
|
||||
" (one command per line; same as the `source`\n"
|
||||
" command). Output goes to the console.\n"
|
||||
" --restore FILE after boot, restore the system snapshot in\n"
|
||||
" FILE (same as the `restore` command).\n"
|
||||
" Combine with --source to layer a script on\n"
|
||||
" top of a restored snapshot.\n"
|
||||
" --batch run --restore/--source, print the console\n"
|
||||
" output to stdout, and exit without launching\n"
|
||||
" the interface.\n"
|
||||
" --commands-md [FILE] dump the command registry as Markdown.\n"
|
||||
" With FILE: write there. Without: stdout.\n"
|
||||
" (Used by `cmake --build build --target doc`.)\n"
|
||||
" --help, -h show this help.\n";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int frontend_main(int argc, char **argv, Frontend &fe) {
|
||||
std::string boot_restore;
|
||||
std::string boot_source;
|
||||
bool batch = false;
|
||||
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
std::string a = argv[i];
|
||||
if (a == "--commands-md") {
|
||||
if (i + 1 < argc) {
|
||||
std::ofstream f(argv[++i]);
|
||||
if (!f) {
|
||||
std::cerr << "essim: cannot open " << argv[i] << " for writing\n";
|
||||
return 1;
|
||||
}
|
||||
fe.DumpCommandsMd(f);
|
||||
} else {
|
||||
fe.DumpCommandsMd(std::cout);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (a == "--source") {
|
||||
if (i + 1 >= argc) {
|
||||
std::cerr << "essim: --source needs a filename\n";
|
||||
return 2;
|
||||
}
|
||||
boot_source = argv[++i];
|
||||
continue;
|
||||
}
|
||||
if (a == "--restore") {
|
||||
if (i + 1 >= argc) {
|
||||
std::cerr << "essim: --restore needs a filename\n";
|
||||
return 2;
|
||||
}
|
||||
boot_restore = argv[++i];
|
||||
continue;
|
||||
}
|
||||
if (a == "--batch") {
|
||||
batch = true;
|
||||
continue;
|
||||
}
|
||||
if (a == "--help" || a == "-h") {
|
||||
print_usage(argv[0]);
|
||||
return 0;
|
||||
}
|
||||
std::cerr << "essim: unknown option: " << a << "\n";
|
||||
print_usage(argv[0]);
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Order matters: a `--restore` brings up a snapshot, then `--source`
|
||||
// can layer additional commands on top of it (useful e.g. for "load
|
||||
// snapshot, then re-run a small script that adds a new card").
|
||||
if (!boot_restore.empty()) fe.BootDispatch("restore " + boot_restore);
|
||||
if (!boot_source.empty()) fe.BootDispatch("source " + boot_source);
|
||||
|
||||
// Batch mode: the boot dispatch already ran synchronously (no event loop
|
||||
// yet), so the console output is complete. Print it and exit.
|
||||
if (batch) {
|
||||
fe.DumpOutput(std::cout);
|
||||
return 0;
|
||||
}
|
||||
|
||||
fe.Run();
|
||||
return 0;
|
||||
}
|
||||
13
src/frontends/frontend_main.hpp
Normal file
13
src/frontends/frontend_main.hpp
Normal file
@@ -0,0 +1,13 @@
|
||||
#ifndef _FRONTEND_MAIN_HPP_
|
||||
#define _FRONTEND_MAIN_HPP_
|
||||
|
||||
class Frontend;
|
||||
|
||||
// Shared process entry point, frontend-agnostic. Parses argv
|
||||
// (--source / --restore / --batch / --commands-md / --help), drives `fe`
|
||||
// through the boot commands and then either dumps output (batch) or enters its
|
||||
// event loop, and returns the process exit code. Each frontend's main() just
|
||||
// constructs its concrete Frontend and forwards to this.
|
||||
int frontend_main(int argc, char **argv, Frontend &fe);
|
||||
|
||||
#endif // _FRONTEND_MAIN_HPP_
|
||||
@@ -18,18 +18,5 @@ FetchContent_Declare(ftxui
|
||||
)
|
||||
FetchContent_MakeAvailable(ftxui)
|
||||
|
||||
# Frontend library = every .cpp here except the entry point.
|
||||
file(GLOB TUI_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp")
|
||||
list(REMOVE_ITEM TUI_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/main.cpp")
|
||||
|
||||
add_library(essim_tui STATIC ${TUI_SOURCES})
|
||||
target_link_libraries(essim_tui
|
||||
PUBLIC
|
||||
essim_core
|
||||
ftxui::screen
|
||||
ftxui::dom
|
||||
ftxui::component
|
||||
)
|
||||
|
||||
add_executable(essim "${CMAKE_CURRENT_SOURCE_DIR}/main.cpp")
|
||||
target_link_libraries(essim PRIVATE essim_tui)
|
||||
# Library essim_tui (sources here minus main.cpp) + the `essim` binary.
|
||||
essim_add_frontend(tui LIBS ftxui::screen ftxui::dom ftxui::component)
|
||||
|
||||
@@ -4,25 +4,23 @@
|
||||
#include "core/domain/analysis.hpp"
|
||||
#include "core/domain/connect.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/nets.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/persist.hpp"
|
||||
#include "core/domain/pin_role.hpp"
|
||||
#include "core/domain/pin_model.hpp"
|
||||
#include "core/domain/bsdl_model.hpp"
|
||||
#include "core/domain/bsdl_check.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
#include "core/domain/transform.hpp"
|
||||
#include "core/domain/transform_vpx.hpp"
|
||||
|
||||
#include "core/app/connect.hpp"
|
||||
#include "core/app/edit.hpp"
|
||||
#include "core/app/load.hpp"
|
||||
#include "core/app/verify.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <exception>
|
||||
#include <fstream>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
void Tui::RegisterCommands() {
|
||||
@@ -138,29 +136,25 @@ void Tui::RegisterCommands() {
|
||||
{"import type [mentor|altium|ods]", Completion::None}},
|
||||
[this](const std::vector<std::string> &args) {
|
||||
if (!sys) { Print("no system: run 'new' first."); return; }
|
||||
std::string ls = ToLower(args[2]);
|
||||
ImportType t;
|
||||
if (ls == "mentor") t = ImportType::IMPORT_MENTOR;
|
||||
else if (ls == "altium") t = ImportType::IMPORT_ALTIUM;
|
||||
else if (ls == "ods") t = ImportType::IMPORT_ODS;
|
||||
else { Print("unknown import type: " + args[2]); return; }
|
||||
try {
|
||||
sys->Load(args[0], args[1], t);
|
||||
Module *mod = sys->modules()->get(args[0]);
|
||||
int dropped = drop_singleton_signals(mod->signals);
|
||||
auto inf = infer_signal_types(sys.get());
|
||||
Print("loaded '" + args[0] + "' from " + args[1]);
|
||||
Print(" parts: " + std::to_string(mod->size()));
|
||||
Print(" signals: " + std::to_string(mod->signals->size())
|
||||
+ (dropped ? " (dropped " + std::to_string(dropped)
|
||||
+ " singleton/NC signal(s))" : ""));
|
||||
Print(" types: " + std::to_string(inf.power) + " power, "
|
||||
+ std::to_string(inf.gnd) + " gnd, "
|
||||
+ std::to_string(inf.kept_other)
|
||||
+ " suspect Power (name only — kept as Other)");
|
||||
} catch (const std::exception &e) {
|
||||
Print(std::string("load failed: ") + e.what());
|
||||
if (!app::import_type_from_name(args[2], t)) {
|
||||
Print("unknown import type: " + args[2]); return;
|
||||
}
|
||||
// Import + drop-singletons + infer-types is one core op; the command
|
||||
// only parses the type and renders the counts.
|
||||
app::LoadResult r = app::load_module(sys.get(), args[0], args[1], t);
|
||||
if (!r.ok) { Print(std::string("load failed: ") + r.error); return; }
|
||||
Print("loaded '" + args[0] + "' from " + args[1]);
|
||||
Print(" parts: " + std::to_string(r.parts));
|
||||
Print(" signals: " + std::to_string(r.signals)
|
||||
+ (r.dropped ? " (dropped " + std::to_string(r.dropped)
|
||||
+ " singleton/NC signal(s))" : ""));
|
||||
Print(" types: " + std::to_string(r.power) + " power, "
|
||||
+ std::to_string(r.gnd) + " gnd, "
|
||||
+ std::to_string(r.kept_other)
|
||||
+ " suspect Power (name only — kept as Other), "
|
||||
+ std::to_string(r.mgmt)
|
||||
+ " power-management (control/measure — kept as Other)");
|
||||
},
|
||||
/*prompt_for_missing=*/ true,
|
||||
"load a module from a netlist / pinout file (mentor, altium, ods)",
|
||||
@@ -228,104 +222,44 @@ void Tui::RegisterCommands() {
|
||||
|
||||
commands["verify"] = { {}, [this](auto &) {
|
||||
if (!sys) { Print("no system: run 'new' first."); return; }
|
||||
int checked = 0;
|
||||
int mismatches = 0;
|
||||
for (auto &mkv : *sys->modules()) {
|
||||
Module *mod = mkv.second;
|
||||
for (auto &pkv : *mod) {
|
||||
Part *prt = pkv.second;
|
||||
if (prt->connector_type.empty()) continue;
|
||||
for (auto &nkv : *prt) {
|
||||
Pin *pin = nkv.second;
|
||||
++checked;
|
||||
SignalType expected = pin->expected_signal_type();
|
||||
if (expected == SignalType::Other) continue;
|
||||
Signal *s = pin->signal();
|
||||
SignalType actual = s ? s->type : SignalType::Other;
|
||||
if (actual == expected) continue;
|
||||
++mismatches;
|
||||
std::string sig_label = s ? s->name : std::string("(NC)");
|
||||
Print(" " + mod->name + "/" + prt->name + "/" + pin->name
|
||||
+ ": expected " + signal_type_name(expected)
|
||||
+ ", got " + signal_type_name(actual)
|
||||
+ " (signal: " + sig_label + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
Print("verify: " + std::to_string(mismatches) + " local mismatch(es) over "
|
||||
+ std::to_string(checked) + " typed pin(s).");
|
||||
app::VerifyReport r = app::verify(sys.get());
|
||||
|
||||
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;
|
||||
for (const auto &m : r.role_mismatches)
|
||||
Print(" " + m.module + "/" + m.part + "/" + m.pin
|
||||
+ ": expected " + signal_type_name(m.expected)
|
||||
+ ", got " + signal_type_name(m.actual)
|
||||
+ " (signal: " + m.signal + ")");
|
||||
Print("verify: " + std::to_string(r.role_mismatches.size())
|
||||
+ " local mismatch(es) over " + std::to_string(r.typed_pins)
|
||||
+ " typed pin(s).");
|
||||
|
||||
for (const auto &ni : r.net_inconsistencies) {
|
||||
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) + ")";
|
||||
}
|
||||
for (const auto &mem : ni.members)
|
||||
line += " " + mem.module + "/" + mem.signal
|
||||
+ "(" + signal_type_name(mem.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).");
|
||||
Print("verify: " + std::to_string(r.net_inconsistencies.size())
|
||||
+ " inconsistent net(s) over " + std::to_string(r.bridged_nets)
|
||||
+ " bridged net(s) (" + std::to_string(r.total_nets) + " total).");
|
||||
|
||||
// Orphan pin report. A pin is "orphan" if it came out of import (or
|
||||
// post-import drop) with no signal, and is still not bridged to a
|
||||
// real signal via any Connection::pin_map. Use `nc-export` for the
|
||||
// per-pin list.
|
||||
std::unordered_set<Pin *> bridged_pins;
|
||||
for (auto &ckv : *sys->connections())
|
||||
for (auto &wp : ckv.second->pin_map) {
|
||||
if (wp.first) bridged_pins.insert(wp.first);
|
||||
if (wp.second) bridged_pins.insert(wp.second);
|
||||
}
|
||||
int orph_imported = 0, orph_dropped = 0;
|
||||
for (auto &mkv : *sys->modules())
|
||||
for (auto &pkv : *mkv.second)
|
||||
for (auto &nkv : *pkv.second) {
|
||||
Pin *pin = nkv.second;
|
||||
if (pin->signal() || bridged_pins.count(pin)) continue;
|
||||
if (pin->nc_origin == NcOrigin::ImportedUnconnected) ++orph_imported;
|
||||
else if (pin->nc_origin == NcOrigin::DroppedSingleton) ++orph_dropped;
|
||||
}
|
||||
Print("verify: " + std::to_string(orph_imported + orph_dropped)
|
||||
+ " orphan pin(s) at import ("
|
||||
+ std::to_string(orph_imported) + " imported NC, "
|
||||
+ std::to_string(orph_dropped) + " dropped singleton).");
|
||||
Print("verify: " + std::to_string(r.orphan_total())
|
||||
+ " orphan pin(s) at import (" + std::to_string(r.orphan_imported)
|
||||
+ " imported NC, " + std::to_string(r.orphan_dropped)
|
||||
+ " dropped singleton).");
|
||||
|
||||
// Model-driven pin checks (drive contention / undriven net / NC-wired)
|
||||
// from the PinSpec direction/function populated by connector/BSDL models.
|
||||
auto pin_anoms = check_pin_specs(sys.get(), &nets);
|
||||
for (const auto &a : pin_anoms)
|
||||
// Each model-driven group: per-finding lines + a one-line summary.
|
||||
auto render = [this](const std::vector<Anomaly> &v, const char *tail) {
|
||||
for (const auto &a : v)
|
||||
Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message);
|
||||
Print("verify: " + std::to_string(pin_anoms.size())
|
||||
+ " model-driven pin anomaly(ies).");
|
||||
|
||||
// JTAG boundary-scan chain integrity (TAP pins → nets).
|
||||
auto jtag_anoms = check_jtag_chain(sys.get(), &nets);
|
||||
for (const auto &a : jtag_anoms)
|
||||
Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message);
|
||||
Print("verify: " + std::to_string(jtag_anoms.size())
|
||||
+ " JTAG chain anomaly(ies).");
|
||||
|
||||
// Model-vs-netlist conflicts (e.g. a BSDL power pin left unconnected).
|
||||
auto conflict_anoms = check_source_conflicts(sys.get());
|
||||
for (const auto &a : conflict_anoms)
|
||||
Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message);
|
||||
Print("verify: " + std::to_string(conflict_anoms.size())
|
||||
+ " source-conflict(s).");
|
||||
|
||||
// BSDL completeness: device power/ground pins missing from the netlist.
|
||||
auto missing_anoms = check_bsdl_completeness(sys.get());
|
||||
for (const auto &a : missing_anoms)
|
||||
Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message);
|
||||
Print("verify: " + std::to_string(missing_anoms.size())
|
||||
+ " BSDL completeness issue(s).");
|
||||
Print("verify: " + std::to_string(v.size()) + tail);
|
||||
};
|
||||
render(r.pin_anomalies, " model-driven pin anomaly(ies).");
|
||||
render(r.jtag_anomalies, " JTAG chain anomaly(ies).");
|
||||
render(r.conflict_anomalies, " source-conflict(s).");
|
||||
render(r.completeness_anomalies, " BSDL completeness issue(s).");
|
||||
render(r.diff_anomalies, " diff-pair crossing anomaly(ies).");
|
||||
}, true,
|
||||
"check pin roles, power/gnd net consistency, and (with BSDL) pin and JTAG checks" };
|
||||
|
||||
@@ -403,14 +337,10 @@ void Tui::RegisterCommands() {
|
||||
catch (const std::exception &) {
|
||||
Print("unknown signal: " + mod->name + "/" + args[1]); return;
|
||||
}
|
||||
SignalType t;
|
||||
if (!signal_type_from_name(args[2], t)) {
|
||||
Print("type must be one of: power, gnd, other (got: " + args[2] + ")");
|
||||
return;
|
||||
}
|
||||
sig->type = t;
|
||||
app::SetSignalTypeResult r = app::set_signal_type(sig, args[2]);
|
||||
if (!r.ok) { Print(r.error); return; }
|
||||
Print(mod->name + "/" + sig->name + ": signal type = "
|
||||
+ signal_type_name(t));
|
||||
+ signal_type_name(r.type));
|
||||
},
|
||||
/*prompt_for_missing=*/ true,
|
||||
"override the auto-detected signal type (power | gnd | other)",
|
||||
@@ -464,18 +394,15 @@ void Tui::RegisterCommands() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
std::string err = ValidatePartForKind(prt, args[2]);
|
||||
if (!err.empty()) {
|
||||
Print("set-connector-type refused: " + err);
|
||||
app::SetConnectorTypeResult r = app::set_connector_type(prt, args[2]);
|
||||
if (!r.ok) {
|
||||
Print("set-connector-type refused: " + r.error);
|
||||
return;
|
||||
}
|
||||
prt->connector_type = args[2];
|
||||
ConnectorModel model(args[2]);
|
||||
ApplyReport rep = apply_model(prt, model);
|
||||
Print(mod->name + "/" + prt->name + ": connector_type = "
|
||||
+ (args[2].empty() ? "(none)" : args[2]));
|
||||
if (rep.materialised > 0)
|
||||
Print("set-connector-type: added " + std::to_string(rep.materialised)
|
||||
if (r.materialised > 0)
|
||||
Print("set-connector-type: added " + std::to_string(r.materialised)
|
||||
+ " NC pin(s) from the connector layout");
|
||||
},
|
||||
/*prompt_for_missing=*/ false,
|
||||
@@ -516,17 +443,11 @@ void Tui::RegisterCommands() {
|
||||
}
|
||||
}
|
||||
|
||||
BsdlModel model = BsdlModel::from_file(args[2]);
|
||||
if (!model.valid()) {
|
||||
Print("attach-bsdl: cannot parse " + args[2]
|
||||
+ (model.error().empty() ? "" : (": " + model.error())));
|
||||
return;
|
||||
}
|
||||
BsdlApplyReport r = apply_bsdl(prt, model);
|
||||
prt->bsdl_path = args[2];
|
||||
Print(mod->name + "/" + prt->name + ": attached BSDL '" + model.entity()
|
||||
app::AttachBsdlResult r = app::attach_bsdl(prt, args[2]);
|
||||
if (!r.ok) { Print("attach-bsdl: " + r.error); return; }
|
||||
Print(mod->name + "/" + prt->name + ": attached BSDL '" + r.entity
|
||||
+ "' — " + std::to_string(r.bound) + "/"
|
||||
+ std::to_string((int)model.ports().size()) + " ports bound"
|
||||
+ std::to_string(r.ports_total) + " ports bound"
|
||||
+ (r.unbound ? (", " + std::to_string(r.unbound) + " unbound") : ""));
|
||||
},
|
||||
/*prompt_for_missing=*/ false,
|
||||
@@ -622,47 +543,23 @@ void Tui::RegisterCommands() {
|
||||
auto [p2, p2_alts] = resolve_part(m2, args[3]);
|
||||
if (!p2) { report_ambiguous("part in " + m2->name, args[3], p2_alts); return; }
|
||||
|
||||
auto ® = TransformRegistry::get();
|
||||
Transform *t = reg.lookup(p1->connector_type, p2->connector_type);
|
||||
bool both_empty = p1->connector_type.empty() && p2->connector_type.empty();
|
||||
if (t == reg.identity()) {
|
||||
if (!both_empty) {
|
||||
Print("connect refused: no transform for types '"
|
||||
+ (p1->connector_type.empty() ? "(none)" : p1->connector_type)
|
||||
+ "' ↔ '"
|
||||
+ (p2->connector_type.empty() ? "(none)" : p2->connector_type)
|
||||
+ "'. Set matching types via 'set-connector-type' first.");
|
||||
return;
|
||||
}
|
||||
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: added " + std::to_string(added)
|
||||
// Resolution above is arg-parsing (user text → objects); the wiring
|
||||
// itself — transform lookup, identity NC fill, Connection creation —
|
||||
// is app::connect_parts.
|
||||
app::ConnectResult cr = app::connect_parts(sys.get(), m1, p1, m2, p2);
|
||||
if (cr.refused) { Print("connect refused: " + cr.error); return; }
|
||||
if (!cr.identity_info.empty()) {
|
||||
Print("connect: " + cr.identity_info);
|
||||
if (cr.nc_added > 0)
|
||||
Print("connect: added " + std::to_string(cr.nc_added)
|
||||
+ " NC pin(s) so both sides match");
|
||||
}
|
||||
}
|
||||
auto pin_map = t->apply(p1, p2);
|
||||
|
||||
std::string conn_name = m1->name + "/" + p1->name
|
||||
+ " <-> " + m2->name + "/" + p2->name;
|
||||
try {
|
||||
Connection *c = new Connection(conn_name, m1, p1, m2, p2);
|
||||
c->transform_name = t->name;
|
||||
c->pin_map = std::move(pin_map);
|
||||
sys->connections()->add(c);
|
||||
Print("connected: " + conn_name
|
||||
+ " via " + t->name
|
||||
+ " (" + std::to_string(c->pin_map.size()) + " wires)");
|
||||
} catch (const std::exception &e) {
|
||||
Print(std::string("connect failed: ") + e.what());
|
||||
}
|
||||
if (cr.ok)
|
||||
Print("connected: " + cr.connection_name
|
||||
+ " via " + cr.transform_name
|
||||
+ " (" + std::to_string(cr.wires) + " wires)");
|
||||
else
|
||||
Print(std::string("connect failed: ") + cr.error);
|
||||
},
|
||||
/*prompt_for_missing=*/ false,
|
||||
"connect a part across two modules (interactive screen if no args)",
|
||||
@@ -701,51 +598,12 @@ void Tui::RegisterCommands() {
|
||||
{"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->spec = sn->spec;
|
||||
dn->nc_origin = sn->nc_origin;
|
||||
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);
|
||||
app::DuplicateResult r =
|
||||
app::duplicate_module(sys.get(), args[0], args[1]);
|
||||
if (!r.ok) { Print(r.error); return; }
|
||||
Print("duplicate: '" + args[0] + "' → '" + args[1] + "'"
|
||||
+ " (" + std::to_string(dst->size()) + " part(s), "
|
||||
+ std::to_string(dst->signals->size()) + " signal(s))");
|
||||
+ " (" + std::to_string(r.parts) + " part(s), "
|
||||
+ std::to_string(r.signals) + " signal(s))");
|
||||
},
|
||||
/*prompt_for_missing=*/ true,
|
||||
"clone a module under a new name (parts, pins, signals; no connections)",
|
||||
|
||||
@@ -1,98 +1,11 @@
|
||||
#include "frontends/frontend_main.hpp"
|
||||
#include "frontends/tui/tui.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
void print_usage(const char *prog) {
|
||||
std::cerr <<
|
||||
"usage: " << prog << " [--batch] [--source FILE] [--restore FILE]\n"
|
||||
" " << prog << " --commands-md [FILE]\n"
|
||||
" " << prog << " --help\n"
|
||||
" (no args) launch the TUI on an empty system.\n"
|
||||
" --source FILE after boot, run FILE as an essim script\n"
|
||||
" (one command per line; same as the `source`\n"
|
||||
" command). Output is in the console screen.\n"
|
||||
" --restore FILE after boot, restore the system snapshot in\n"
|
||||
" FILE (same as the `restore` command).\n"
|
||||
" Combine with --source to layer a script on\n"
|
||||
" top of a restored snapshot.\n"
|
||||
" --batch run --restore/--source, print the console\n"
|
||||
" output to stdout, and exit without the TUI.\n"
|
||||
" --commands-md [FILE] dump the command registry as Markdown.\n"
|
||||
" With FILE: write there. Without: stdout.\n"
|
||||
" (Used by `cmake --build build --target doc`.)\n"
|
||||
" --help, -h show this help.\n";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// The TUI frontend's entry point: construct the concrete Frontend (Tui) and
|
||||
// hand off to the shared, frontend-agnostic launcher. All argv parsing and the
|
||||
// boot/batch/run flow live in frontend_main(); a second frontend's main() looks
|
||||
// exactly like this with its own Frontend type.
|
||||
int main(int argc, char **argv) {
|
||||
std::string boot_restore;
|
||||
std::string boot_source;
|
||||
bool batch = false;
|
||||
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
std::string a = argv[i];
|
||||
if (a == "--commands-md") {
|
||||
Tui tui;
|
||||
if (i + 1 < argc) {
|
||||
std::ofstream f(argv[++i]);
|
||||
if (!f) {
|
||||
std::cerr << "essim: cannot open " << argv[i] << " for writing\n";
|
||||
return 1;
|
||||
}
|
||||
tui.DumpCommandsMd(f);
|
||||
} else {
|
||||
tui.DumpCommandsMd(std::cout);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (a == "--source") {
|
||||
if (i + 1 >= argc) {
|
||||
std::cerr << "essim: --source needs a filename\n";
|
||||
return 2;
|
||||
}
|
||||
boot_source = argv[++i];
|
||||
continue;
|
||||
}
|
||||
if (a == "--restore") {
|
||||
if (i + 1 >= argc) {
|
||||
std::cerr << "essim: --restore needs a filename\n";
|
||||
return 2;
|
||||
}
|
||||
boot_restore = argv[++i];
|
||||
continue;
|
||||
}
|
||||
if (a == "--batch") {
|
||||
batch = true;
|
||||
continue;
|
||||
}
|
||||
if (a == "--help" || a == "-h") {
|
||||
print_usage(argv[0]);
|
||||
return 0;
|
||||
}
|
||||
std::cerr << "essim: unknown option: " << a << "\n";
|
||||
print_usage(argv[0]);
|
||||
return 2;
|
||||
}
|
||||
|
||||
Tui tui;
|
||||
// Order matters: a `--restore` brings up a snapshot, then `--source`
|
||||
// can layer additional commands on top of it (useful e.g. for "load
|
||||
// snapshot, then re-run a small script that adds a new card").
|
||||
if (!boot_restore.empty()) tui.BootDispatch("restore " + boot_restore);
|
||||
if (!boot_source.empty()) tui.BootDispatch("source " + boot_source);
|
||||
|
||||
// Batch mode: the boot dispatch already ran synchronously (no screen yet),
|
||||
// so the console output is complete. Print it and exit without the TUI.
|
||||
if (batch) {
|
||||
tui.DumpOutput(std::cout);
|
||||
return 0;
|
||||
}
|
||||
|
||||
tui.Run();
|
||||
return 0;
|
||||
return frontend_main(argc, argv, tui);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
#include "frontends/tui/tui.hpp"
|
||||
#include "frontends/tui/tui_helpers.hpp"
|
||||
|
||||
#include "core/app/verify.hpp"
|
||||
#include "core/domain/analysis.hpp"
|
||||
#include "core/domain/bsdl_check.hpp"
|
||||
#include "core/domain/connect.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/nets.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
|
||||
@@ -17,7 +13,6 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <unordered_set>
|
||||
|
||||
using namespace ftxui;
|
||||
|
||||
@@ -57,41 +52,23 @@ Component Tui::BuildAnalyzeScreen() {
|
||||
// connection), then structural anomalies from the analysis pass.
|
||||
analyze_issues.clear();
|
||||
|
||||
int n_role_mismatches = 0, n_typed_pins = 0;
|
||||
for (auto &mkv : *sys->modules())
|
||||
for (auto &pkv : *mkv.second) {
|
||||
Part *prt = pkv.second;
|
||||
if (prt->connector_type.empty()) continue;
|
||||
for (auto &nkv : *prt) {
|
||||
Pin *pin = nkv.second;
|
||||
++n_typed_pins;
|
||||
SignalType expected = pin->expected_signal_type();
|
||||
if (expected == SignalType::Other) continue;
|
||||
Signal *s = pin->signal();
|
||||
SignalType actual = s ? s->type : SignalType::Other;
|
||||
if (actual == expected) continue;
|
||||
++n_role_mismatches;
|
||||
std::string sig_label = s ? s->name : std::string("(NC)");
|
||||
analyze_issues.push_back(
|
||||
"[pin-role] " + mkv.first + "/" + prt->name + "/"
|
||||
+ pin->name + ": expected " + signal_type_name(expected)
|
||||
+ ", got " + signal_type_name(actual)
|
||||
+ " (signal: " + sig_label + ")");
|
||||
}
|
||||
}
|
||||
// verify + structural anomalies. The verify passes (pin-role, net-mix,
|
||||
// orphans, model checks) come from the shared core op; the structural
|
||||
// anomalies (diff-pair/bus) come from analyze_system above.
|
||||
app::VerifyReport vr = app::verify(sys.get());
|
||||
|
||||
auto nets = compute_all_nets(sys.get());
|
||||
int n_bridged = 0, n_inconsistent = 0;
|
||||
for (const auto &n : nets) {
|
||||
if (n.members.size() < 2) continue;
|
||||
++n_bridged;
|
||||
SignalType dom;
|
||||
if (net_type_consistent(n, dom)) continue;
|
||||
++n_inconsistent;
|
||||
for (const auto &m : vr.role_mismatches)
|
||||
analyze_issues.push_back(
|
||||
"[pin-role] " + m.module + "/" + m.part + "/" + m.pin
|
||||
+ ": expected " + signal_type_name(m.expected)
|
||||
+ ", got " + signal_type_name(m.actual)
|
||||
+ " (signal: " + m.signal + ")");
|
||||
|
||||
for (const auto &ni : vr.net_inconsistencies) {
|
||||
std::string line = "[net-mix] mixes Power and Gnd:";
|
||||
for (const auto &mp : n.members)
|
||||
line += " " + mp.first->name + "/" + mp.second->name
|
||||
+ "(" + signal_type_name(mp.second->type) + ")";
|
||||
for (const auto &mem : ni.members)
|
||||
line += " " + mem.module + "/" + mem.signal
|
||||
+ "(" + signal_type_name(mem.type) + ")";
|
||||
analyze_issues.push_back(std::move(line));
|
||||
}
|
||||
|
||||
@@ -100,28 +77,26 @@ Component Tui::BuildAnalyzeScreen() {
|
||||
+ anomaly_kind_name(a.kind) + "] "
|
||||
+ a.message);
|
||||
|
||||
// Model-driven checks (same as `verify`), reusing the nets above.
|
||||
std::vector<Anomaly> model_anoms;
|
||||
{
|
||||
auto a1 = check_pin_specs(sys.get(), &nets);
|
||||
auto a2 = check_jtag_chain(sys.get(), &nets);
|
||||
auto a3 = check_source_conflicts(sys.get());
|
||||
auto a4 = check_bsdl_completeness(sys.get());
|
||||
model_anoms.insert(model_anoms.end(), a1.begin(), a1.end());
|
||||
model_anoms.insert(model_anoms.end(), a2.begin(), a2.end());
|
||||
model_anoms.insert(model_anoms.end(), a3.begin(), a3.end());
|
||||
model_anoms.insert(model_anoms.end(), a4.begin(), a4.end());
|
||||
}
|
||||
for (const auto &a : model_anoms)
|
||||
// Model-driven checks (pin / JTAG / source-conflict / completeness).
|
||||
auto push_anoms = [this](const std::vector<Anomaly> &v) {
|
||||
for (const auto &a : v)
|
||||
analyze_issues.push_back(std::string("[")
|
||||
+ anomaly_kind_name(a.kind) + "] "
|
||||
+ a.message);
|
||||
int n_model = (int)model_anoms.size();
|
||||
};
|
||||
push_anoms(vr.pin_anomalies);
|
||||
push_anoms(vr.jtag_anomalies);
|
||||
push_anoms(vr.conflict_anomalies);
|
||||
push_anoms(vr.completeness_anomalies);
|
||||
push_anoms(vr.diff_anomalies);
|
||||
int n_model = vr.model_total();
|
||||
|
||||
if (analyze_issues.empty()) analyze_issues.push_back("(no issue found)");
|
||||
if (analyze_issue_idx >= (int)analyze_issues.size())
|
||||
analyze_issue_idx = (int)analyze_issues.size() - 1;
|
||||
|
||||
int n_role_mismatches = (int)vr.role_mismatches.size();
|
||||
int n_inconsistent = (int)vr.net_inconsistencies.size();
|
||||
std::string issues_header = "Issues ("
|
||||
+ std::to_string(n_role_mismatches + n_inconsistent
|
||||
+ (int)rep.anomalies.size() + n_model)
|
||||
@@ -157,29 +132,39 @@ Component Tui::BuildAnalyzeScreen() {
|
||||
// ============================================================= Types
|
||||
// Power decisions (confirmed / refuted) and NC orphan breakdown.
|
||||
analyze_types.clear();
|
||||
int conf_pwr = 0, ref_pwr = 0, gnd = 0;
|
||||
struct Row { char kind; std::string mod, sig; int fanout; bool voltage; };
|
||||
int conf_pwr = 0, ref_pwr = 0, mgmt = 0, gnd = 0;
|
||||
struct Row { char kind; std::string mod, sig; int fanout; bool voltage;
|
||||
std::string token; };
|
||||
std::vector<Row> rows;
|
||||
for (auto &mkv : *sys->modules()) {
|
||||
Module *mod = mkv.second;
|
||||
for (auto &skv : *mod->signals) {
|
||||
Signal *s = skv.second;
|
||||
SignalType named = infer_signal_type(s->name);
|
||||
NameClassification ncl = classify_signal_name(s->name);
|
||||
char kind = 0;
|
||||
if (named == SignalType::GndShield && s->type == SignalType::GndShield) {
|
||||
std::string token;
|
||||
if (ncl.verdict == NameVerdict::GndShield && s->type == SignalType::GndShield) {
|
||||
kind = 'G'; ++gnd;
|
||||
} else if (named == SignalType::Power && s->type == SignalType::Power) {
|
||||
} else if (ncl.verdict == NameVerdict::Rail && s->type == SignalType::Power) {
|
||||
kind = 'P'; ++conf_pwr;
|
||||
} else if (named == SignalType::Power && s->type == SignalType::Other) {
|
||||
} else if (ncl.verdict == NameVerdict::Rail && s->type == SignalType::Other) {
|
||||
kind = 'R'; ++ref_pwr;
|
||||
} else if (ncl.verdict == NameVerdict::PowerMgmt) {
|
||||
kind = 'M'; ++mgmt; token = ncl.token;
|
||||
} else continue;
|
||||
rows.push_back({kind, mod->name, s->name,
|
||||
(int)s->size(), has_voltage_pattern(s->name)});
|
||||
(int)s->size(), has_voltage_pattern(s->name),
|
||||
token});
|
||||
}
|
||||
}
|
||||
// Deliberate display order: confirmed rails, then the suspects (the
|
||||
// actionable residue), then the power-management signals, gnd last.
|
||||
auto rank = [](char k) {
|
||||
return k == 'P' ? 0 : k == 'R' ? 1 : k == 'M' ? 2 : 3;
|
||||
};
|
||||
std::sort(rows.begin(), rows.end(),
|
||||
[](const Row &a, const Row &b) {
|
||||
if (a.kind != b.kind) return a.kind < b.kind;
|
||||
[&](const Row &a, const Row &b) {
|
||||
if (a.kind != b.kind) return rank(a.kind) < rank(b.kind);
|
||||
if (a.mod != b.mod) return a.mod < b.mod;
|
||||
return a.sig < b.sig;
|
||||
});
|
||||
@@ -207,6 +192,10 @@ Component Tui::BuildAnalyzeScreen() {
|
||||
else reason = "name only — fan-out "
|
||||
+ std::to_string(r.fanout)
|
||||
+ ", no voltage";
|
||||
} else if (r.kind == 'M') {
|
||||
tag = "[Power mgmt] ";
|
||||
reason = "control token '" + r.token
|
||||
+ "' in name — kept as Other";
|
||||
} else {
|
||||
tag = "[Gnd] ";
|
||||
reason = "name match (fan-out " + std::to_string(r.fanout) + ")";
|
||||
@@ -215,33 +204,19 @@ Component Tui::BuildAnalyzeScreen() {
|
||||
std::string(tag) + r.mod + "/" + r.sig + " — " + reason);
|
||||
}
|
||||
|
||||
// NC orphan rollup — same filter as the verify pass.
|
||||
std::unordered_set<Pin *> bridged_pins;
|
||||
for (auto &ckv : *sys->connections())
|
||||
for (auto &wp : ckv.second->pin_map) {
|
||||
if (wp.first) bridged_pins.insert(wp.first);
|
||||
if (wp.second) bridged_pins.insert(wp.second);
|
||||
}
|
||||
int orph_imported = 0, orph_dropped = 0;
|
||||
for (auto &mkv : *sys->modules())
|
||||
for (auto &pkv : *mkv.second)
|
||||
for (auto &nkv : *pkv.second) {
|
||||
Pin *pin = nkv.second;
|
||||
if (pin->signal() || bridged_pins.count(pin)) continue;
|
||||
if (pin->nc_origin == NcOrigin::ImportedUnconnected) ++orph_imported;
|
||||
else if (pin->nc_origin == NcOrigin::DroppedSingleton) ++orph_dropped;
|
||||
}
|
||||
// NC orphan rollup — from the shared verify report.
|
||||
analyze_types.push_back(
|
||||
"[NC] orphan pin(s): " + std::to_string(orph_imported + orph_dropped)
|
||||
+ " (" + std::to_string(orph_imported) + " imported, "
|
||||
+ std::to_string(orph_dropped) + " dropped)");
|
||||
"[NC] orphan pin(s): " + std::to_string(vr.orphan_total())
|
||||
+ " (" + std::to_string(vr.orphan_imported) + " imported, "
|
||||
+ std::to_string(vr.orphan_dropped) + " dropped)");
|
||||
|
||||
if (analyze_type_idx >= (int)analyze_types.size())
|
||||
analyze_type_idx = (int)analyze_types.size() - 1;
|
||||
|
||||
std::string types_header = "Types: " + std::to_string(conf_pwr)
|
||||
+ " Power, " + std::to_string(ref_pwr)
|
||||
+ " Suspect, " + std::to_string(gnd)
|
||||
+ " Suspect, " + std::to_string(mgmt)
|
||||
+ " Pwr-mgmt, " + std::to_string(gnd)
|
||||
+ " Gnd";
|
||||
|
||||
// Tab bar — horizontal headers, active one inverted.
|
||||
@@ -281,8 +256,14 @@ Component Tui::BuildAnalyzeScreen() {
|
||||
"Name suggests Power AND structure agrees: fan-out ≥ 4 pins, "
|
||||
"or a voltage pattern in the name (e.g. 3V3, 5V, 12V)."),
|
||||
term("Suspect Power",
|
||||
"Name suggests Power but the structural check failed — "
|
||||
"fan-out too low and no voltage in the name."),
|
||||
"Name suggests Power, no control token explains it, but the "
|
||||
"structural check failed — fan-out too low and no voltage "
|
||||
"in the name."),
|
||||
term("Power mgmt",
|
||||
"Name holds a rail token AND a control token (SENSE, EN, PG, "
|
||||
"FB, …): a power-management signal — measurement or control "
|
||||
"of a rail — not the rail itself. Confidently Other, never "
|
||||
"suspect."),
|
||||
term("Hard floor",
|
||||
"Fan-out below 3 pins forces Other regardless of the name. "
|
||||
"A real rail physically cannot live on 1-2 pads."),
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
#include "frontends/tui/tui.hpp"
|
||||
#include "frontends/tui/tui_helpers.hpp"
|
||||
|
||||
#include "core/domain/connect.hpp"
|
||||
#include "core/app/connect.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
#include "core/domain/transform.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/component_options.hpp>
|
||||
@@ -67,37 +66,24 @@ Component Tui::BuildConnectScreen() {
|
||||
Part *p1 = m1->get(connect_p1_list[connect_p1_idx]);
|
||||
Part *p2 = m2->get(connect_p2_list[connect_p2_idx]);
|
||||
|
||||
auto ® = TransformRegistry::get();
|
||||
Transform *t = reg.lookup(p1->connector_type, p2->connector_type);
|
||||
bool both_empty = p1->connector_type.empty() && p2->connector_type.empty();
|
||||
if (t == reg.identity()) {
|
||||
if (!both_empty) {
|
||||
Print("connect refused: no transform for types '"
|
||||
+ (p1->connector_type.empty() ? "(none)" : p1->connector_type)
|
||||
+ "' ↔ '"
|
||||
+ (p2->connector_type.empty() ? "(none)" : p2->connector_type)
|
||||
+ "'. Set matching types via 'set-connector-type' first.");
|
||||
screen_idx = 0;
|
||||
return;
|
||||
// Same wiring op as the `connect` command — see app::connect_parts.
|
||||
app::ConnectResult cr = app::connect_parts(sys.get(), m1, p1, m2, p2);
|
||||
if (cr.refused) {
|
||||
Print("connect refused: " + cr.error);
|
||||
} else {
|
||||
if (!cr.identity_info.empty()) {
|
||||
Print("connect: " + cr.identity_info);
|
||||
if (cr.nc_added > 0)
|
||||
Print("connect: added " + std::to_string(cr.nc_added)
|
||||
+ " NC pin(s) so both sides match");
|
||||
}
|
||||
std::string err = CheckIdentityCompatible(p1, p2);
|
||||
if (!err.empty()) {
|
||||
Print("connect refused: " + err);
|
||||
screen_idx = 0;
|
||||
return;
|
||||
if (cr.ok)
|
||||
Print("connected: " + cr.connection_name
|
||||
+ " via " + cr.transform_name
|
||||
+ " (" + std::to_string(cr.wires) + " wires)");
|
||||
else
|
||||
Print(std::string("connect failed: ") + cr.error);
|
||||
}
|
||||
}
|
||||
auto pin_map = t->apply(p1, p2);
|
||||
|
||||
std::string conn_name = m1->name + "/" + p1->name
|
||||
+ " <-> " + m2->name + "/" + p2->name;
|
||||
Connection *c = new Connection(conn_name, m1, p1, m2, p2);
|
||||
c->transform_name = t->name;
|
||||
c->pin_map = std::move(pin_map);
|
||||
sys->connections()->add(c);
|
||||
Print("connected: " + conn_name
|
||||
+ " via " + t->name
|
||||
+ " (" + std::to_string(c->pin_map.size()) + " wires)");
|
||||
} catch (const std::exception &e) {
|
||||
Print(std::string("connect failed: ") + e.what());
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
#include "frontends/tui/tui.hpp"
|
||||
#include "frontends/tui/tui_helpers.hpp"
|
||||
|
||||
#include "core/app/verify.hpp"
|
||||
#include "core/domain/analysis.hpp"
|
||||
#include "core/domain/bsdl_check.hpp"
|
||||
#include "core/domain/connect.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/nets.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
|
||||
@@ -16,7 +14,6 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
using namespace ftxui;
|
||||
@@ -77,58 +74,23 @@ Component Tui::BuildDashboardScreen() {
|
||||
}
|
||||
int n_conn = (int)sys->connections()->size();
|
||||
|
||||
// ---- verify-style health (recomputed; cheap on realistic sizes) ----
|
||||
int n_role_mismatches = 0, n_typed_pins = 0;
|
||||
for (auto &mkv : *sys->modules())
|
||||
for (auto &pkv : *mkv.second) {
|
||||
Part *prt = pkv.second;
|
||||
if (prt->connector_type.empty()) continue;
|
||||
for (auto &nkv : *prt) {
|
||||
Pin *pin = nkv.second;
|
||||
++n_typed_pins;
|
||||
SignalType expected = pin->expected_signal_type();
|
||||
if (expected == SignalType::Other) continue;
|
||||
Signal *s = pin->signal();
|
||||
SignalType actual = s ? s->type : SignalType::Other;
|
||||
if (actual != expected) ++n_role_mismatches;
|
||||
}
|
||||
}
|
||||
// ---- verify-style health (shared core op; cheap on realistic sizes) ----
|
||||
app::VerifyReport vr = app::verify(sys.get());
|
||||
int n_role_mismatches = (int)vr.role_mismatches.size();
|
||||
int n_typed_pins = vr.typed_pins;
|
||||
int n_inconsistent = (int)vr.net_inconsistencies.size();
|
||||
int n_bridged = vr.bridged_nets;
|
||||
int orph_imported = vr.orphan_imported;
|
||||
int orph_dropped = vr.orphan_dropped;
|
||||
|
||||
auto nets = compute_all_nets(sys.get());
|
||||
int n_bridged = 0, n_inconsistent = 0;
|
||||
for (const auto &n : nets) {
|
||||
if (n.members.size() < 2) continue;
|
||||
++n_bridged;
|
||||
SignalType dom;
|
||||
if (!net_type_consistent(n, dom)) ++n_inconsistent;
|
||||
}
|
||||
|
||||
// ---- NC orphan summary (matches verify pass 3) ----
|
||||
std::unordered_set<Pin *> bridged_pins;
|
||||
for (auto &ckv : *sys->connections())
|
||||
for (auto &wp : ckv.second->pin_map) {
|
||||
if (wp.first) bridged_pins.insert(wp.first);
|
||||
if (wp.second) bridged_pins.insert(wp.second);
|
||||
}
|
||||
int orph_imported = 0, orph_dropped = 0;
|
||||
// Per-module list of dropped-singleton pins, for the detail rows below
|
||||
// the NC health line. The signal name is gone (the Signal object was
|
||||
// deleted by `drop_singleton_signals`), but the pin's full path is
|
||||
// enough to locate it in `explore`.
|
||||
std::map<std::string, std::vector<std::string>> dropped_by_module;
|
||||
for (auto &mkv : *sys->modules())
|
||||
for (auto &pkv : *mkv.second)
|
||||
for (auto &nkv : *pkv.second) {
|
||||
Pin *pin = nkv.second;
|
||||
if (pin->signal() || bridged_pins.count(pin)) continue;
|
||||
if (pin->nc_origin == NcOrigin::ImportedUnconnected) {
|
||||
++orph_imported;
|
||||
} else if (pin->nc_origin == NcOrigin::DroppedSingleton) {
|
||||
++orph_dropped;
|
||||
dropped_by_module[mkv.first].push_back(
|
||||
pkv.first + "/" + nkv.first);
|
||||
}
|
||||
}
|
||||
for (const auto &o : vr.orphans)
|
||||
if (o.dropped)
|
||||
dropped_by_module[o.module].push_back(o.part + "/" + o.pin);
|
||||
|
||||
auto health_line = [](bool ok, const std::string &s) {
|
||||
return hbox({
|
||||
@@ -144,7 +106,7 @@ Component Tui::BuildDashboardScreen() {
|
||||
+ " typed pin(s)"));
|
||||
health_rows.push_back(health_line(n_inconsistent == 0,
|
||||
"nets: " + std::to_string(n_inconsistent) + " inconsistent over "
|
||||
+ std::to_string(n_bridged) + " bridged (" + std::to_string(nets.size())
|
||||
+ std::to_string(n_bridged) + " bridged (" + std::to_string(vr.total_nets)
|
||||
+ " total)"));
|
||||
int orph_total = orph_imported + orph_dropped;
|
||||
health_rows.push_back(health_line(orph_total == 0,
|
||||
@@ -172,12 +134,9 @@ Component Tui::BuildDashboardScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
// Model-driven checks (BSDL pin specs, JTAG chain, source conflicts),
|
||||
// reusing the nets computed above.
|
||||
int n_model = (int)(check_pin_specs(sys.get(), &nets).size()
|
||||
+ check_jtag_chain(sys.get(), &nets).size()
|
||||
+ check_source_conflicts(sys.get()).size()
|
||||
+ check_bsdl_completeness(sys.get()).size());
|
||||
// Model-driven checks (BSDL pin specs, JTAG chain, source conflicts,
|
||||
// completeness) — from the shared verify report.
|
||||
int n_model = vr.model_total();
|
||||
health_rows.push_back(health_line(n_model == 0,
|
||||
"model: " + std::to_string(n_model) + " BSDL/JTAG anomaly(ies)"));
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
#include "frontends/tui/tui.hpp"
|
||||
#include "frontends/tui/tui_helpers.hpp"
|
||||
|
||||
#include "core/app/edit.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/pin_model.hpp"
|
||||
#include "core/domain/pin_role.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
#include "core/domain/transform_vpx.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/component_options.hpp>
|
||||
@@ -63,14 +62,11 @@ Component Tui::BuildSettypeScreen() {
|
||||
try {
|
||||
Module *mod = sys->modules()->get(settype_modules[settype_m_idx]);
|
||||
Part *prt = mod->get(settype_p_list[settype_p_idx]);
|
||||
std::string err = ValidatePartForKind(prt, settype_type);
|
||||
if (!err.empty()) {
|
||||
settype_status = "refused: " + err;
|
||||
app::SetConnectorTypeResult r = app::set_connector_type(prt, settype_type);
|
||||
if (!r.ok) {
|
||||
settype_status = "refused: " + r.error;
|
||||
return;
|
||||
}
|
||||
prt->connector_type = settype_type;
|
||||
ConnectorModel model(settype_type);
|
||||
apply_model(prt, model);
|
||||
std::string msg = mod->name + "/" + prt->name + " = "
|
||||
+ (settype_type.empty() ? "(none)" : settype_type);
|
||||
settype_status = "applied: " + msg;
|
||||
|
||||
@@ -281,20 +281,29 @@ void Tui::Source(const std::string &filename) {
|
||||
if (const char *home = std::getenv("HOME"))
|
||||
expanded = std::string(home) + expanded.substr(1);
|
||||
}
|
||||
if (source_stack.size() >= 32) { // same depth guard as the core engine
|
||||
Print("source: nesting too deep, skipping " + filename);
|
||||
return;
|
||||
}
|
||||
std::ifstream f(expanded);
|
||||
if (!f) { Print("source failed: cannot open " + filename); return; }
|
||||
|
||||
// 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();
|
||||
SourceFrame fr;
|
||||
fr.filename = filename;
|
||||
std::string line;
|
||||
while (std::getline(f, line)) loading_lines.push_back(line);
|
||||
while (std::getline(f, line)) fr.lines.push_back(std::move(line));
|
||||
|
||||
// Nested source (a sourced line is itself `source …`): just stack the
|
||||
// frame — the driver already running (ticker thread or headless drain)
|
||||
// picks it up on the next ProcessNextSourceLine, and the caller's frame
|
||||
// resumes when it finishes.
|
||||
bool nested = !source_stack.empty();
|
||||
source_stack.push_back(std::move(fr));
|
||||
if (nested) return;
|
||||
|
||||
loading_filename = filename;
|
||||
loading_idx = 0;
|
||||
loading_executed = 0;
|
||||
loading_lineno = 0;
|
||||
loading_prev_in_source = in_source;
|
||||
source_origin_screen = screen_idx; // a sourced line that leaves this screen aborts
|
||||
in_source = true;
|
||||
@@ -332,9 +341,18 @@ void Tui::Source(const std::string &filename) {
|
||||
|
||||
void Tui::ProcessNextSourceLine() {
|
||||
if (!loading.load()) return;
|
||||
while (loading_idx < loading_lines.size()) {
|
||||
const std::string &raw = loading_lines[loading_idx++];
|
||||
++loading_lineno;
|
||||
while (!source_stack.empty()) {
|
||||
if (source_stack.back().idx >= source_stack.back().lines.size()) {
|
||||
// Frame done: summarise it and resume the caller's frame.
|
||||
const SourceFrame &done = source_stack.back();
|
||||
Print("source: " + done.filename
|
||||
+ " (" + std::to_string(done.executed) + " line(s))");
|
||||
source_stack.pop_back();
|
||||
continue;
|
||||
}
|
||||
SourceFrame &fr = source_stack.back();
|
||||
const std::string raw = fr.lines[fr.idx++];
|
||||
++fr.lineno;
|
||||
size_t start = raw.find_first_not_of(" \t");
|
||||
if (start == std::string::npos) continue;
|
||||
if (raw[start] == '#') continue;
|
||||
@@ -343,28 +361,26 @@ void Tui::ProcessNextSourceLine() {
|
||||
trimmed.pop_back();
|
||||
if (trimmed.empty()) continue;
|
||||
|
||||
++fr.executed;
|
||||
int lineno = fr.lineno; // copies: Submit can push a nested frame,
|
||||
// which may reallocate and invalidate `fr`.
|
||||
input = trimmed;
|
||||
cursor_pos = (int)input.size();
|
||||
Submit();
|
||||
++loading_executed;
|
||||
|
||||
if (screen_idx != source_origin_screen) {
|
||||
Print("source: line " + std::to_string(loading_lineno)
|
||||
Print("source: line " + std::to_string(lineno)
|
||||
+ " is interactive (would open a screen) — aborting.");
|
||||
screen_idx = source_origin_screen;
|
||||
loading.store(false);
|
||||
computing_open = false;
|
||||
tick_in_flight.store(false);
|
||||
in_source = loading_prev_in_source;
|
||||
return;
|
||||
source_stack.clear(); // an abort cancels the whole chain
|
||||
break;
|
||||
}
|
||||
// One effective line per tick — ack so the ticker can pace the next.
|
||||
tick_in_flight.store(false);
|
||||
return;
|
||||
}
|
||||
|
||||
Print("source: " + loading_filename
|
||||
+ " (" + std::to_string(loading_executed) + " line(s))");
|
||||
// Stack drained (or aborted): close up.
|
||||
loading.store(false);
|
||||
computing_open = false;
|
||||
tick_in_flight.store(false);
|
||||
|
||||
@@ -12,7 +12,6 @@ using namespace ftxui;
|
||||
Tui::Tui()
|
||||
: 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(4), // boot to the dashboard; console (screen 0) is now a sub-screen
|
||||
connect_m1_idx(0), connect_m2_idx(0),
|
||||
@@ -64,12 +63,17 @@ void Tui::Run() {
|
||||
// script is opened from the dashboard. The Renderer re-reads the live
|
||||
// progress every frame.
|
||||
auto computing_modal = Renderer([this] {
|
||||
std::string progress = std::to_string(loading_executed) + " / "
|
||||
+ std::to_string((int)loading_lines.size()) + " lines";
|
||||
std::string fname, progress;
|
||||
if (!source_stack.empty()) { // top frame = the file currently running
|
||||
const SourceFrame &fr = source_stack.back();
|
||||
fname = fr.filename;
|
||||
progress = std::to_string(fr.executed) + " / "
|
||||
+ std::to_string((int)fr.lines.size()) + " lines";
|
||||
}
|
||||
return vbox({
|
||||
text(" Computing… ") | bold | center,
|
||||
separator(),
|
||||
text(loading_filename) | center,
|
||||
text(fname) | center,
|
||||
text(progress) | center,
|
||||
}) | borderDouble | size(WIDTH, GREATER_THAN, 40);
|
||||
});
|
||||
|
||||
@@ -13,9 +13,11 @@
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/screen_interactive.hpp>
|
||||
|
||||
#include "frontends/frontend.hpp"
|
||||
|
||||
class System;
|
||||
|
||||
class Tui {
|
||||
class Tui : public Frontend {
|
||||
enum class Completion { None, Path, Command };
|
||||
|
||||
struct Prompt {
|
||||
@@ -97,11 +99,16 @@ class Tui {
|
||||
// ---- 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;
|
||||
// One script being processed. Nested `source` pushes a frame so the
|
||||
// caller resumes where it left off — the stack IS the call chain.
|
||||
struct SourceFrame {
|
||||
std::string filename;
|
||||
std::vector<std::string> lines;
|
||||
size_t idx = 0; ///< next line to process
|
||||
int executed = 0; ///< effective (non-blank, non-comment) lines run
|
||||
int lineno = 0; ///< current 1-based line, for messages
|
||||
};
|
||||
std::vector<SourceFrame> source_stack;
|
||||
bool loading_prev_in_source;
|
||||
int source_origin_screen = 0; ///< screen a `source` started from; a sourced line that navigates away (opens an interactive screen) aborts it.
|
||||
bool computing_open = false; ///< drives the global "Computing…" progress modal while a script loads.
|
||||
@@ -198,16 +205,16 @@ private:
|
||||
public:
|
||||
Tui();
|
||||
~Tui();
|
||||
void Run();
|
||||
void DumpCommandsMd(std::ostream &out) const;
|
||||
void Run() override;
|
||||
void DumpCommandsMd(std::ostream &out) const override;
|
||||
// Write the accumulated console output to `out`. Used by batch mode to
|
||||
// surface a script's output without starting the TUI.
|
||||
void DumpOutput(std::ostream &out) const;
|
||||
void DumpOutput(std::ostream &out) const override;
|
||||
|
||||
// Boot-time hook: dispatch a single command exactly as if the user
|
||||
// typed it (e.g. `restore foo.essim` or `source bring-up.essim`).
|
||||
// Call before `Run()` to seed the system before the event loop starts.
|
||||
void BootDispatch(const std::string &raw);
|
||||
void BootDispatch(const std::string &raw) override;
|
||||
|
||||
private:
|
||||
// Lifecycle (commands.cpp)
|
||||
|
||||
16
src/frontends/wx/CMakeLists.txt
Normal file
16
src/frontends/wx/CMakeLists.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
# wxWidgets GUI frontend. Builds the `essim` executable against essim_core.
|
||||
#
|
||||
# Self-contained like every frontend: it pulls its own GUI toolkit (here a
|
||||
# system wxWidgets via find_package), then defers the target wiring to the
|
||||
# shared essim_add_frontend() helper. Select it with -DESSIM_FRONTEND=wx.
|
||||
#
|
||||
# Needs the wxWidgets development package, e.g. on Debian/Ubuntu:
|
||||
# sudo apt install libwxgtk3.2-dev
|
||||
|
||||
find_package(wxWidgets REQUIRED COMPONENTS core base)
|
||||
|
||||
# UsewxWidgets sets the include dirs and compile definitions for targets defined
|
||||
# afterwards in this directory — so it must come before essim_add_frontend().
|
||||
include(${wxWidgets_USE_FILE})
|
||||
|
||||
essim_add_frontend(wx LIBS ${wxWidgets_LIBRARIES})
|
||||
10
src/frontends/wx/main.cpp
Normal file
10
src/frontends/wx/main.cpp
Normal file
@@ -0,0 +1,10 @@
|
||||
#include "frontends/frontend_main.hpp"
|
||||
#include "frontends/wx/wx_frontend.hpp"
|
||||
|
||||
// The wx frontend's entry point: construct the concrete Frontend (WxFrontend)
|
||||
// and hand off to the shared, frontend-agnostic launcher. Identical in shape to
|
||||
// the tui frontend's main — only the Frontend type differs.
|
||||
int main(int argc, char **argv) {
|
||||
WxFrontend fe;
|
||||
return frontend_main(argc, argv, fe);
|
||||
}
|
||||
680
src/frontends/wx/wx_frame.cpp
Normal file
680
src/frontends/wx/wx_frame.cpp
Normal file
@@ -0,0 +1,680 @@
|
||||
#include "frontends/wx/wx_frame.hpp"
|
||||
|
||||
#include "frontends/wx/wx_frontend.hpp"
|
||||
|
||||
#include "core/app/connect.hpp"
|
||||
#include "core/app/edit.hpp"
|
||||
#include "core/app/export.hpp"
|
||||
#include "core/app/load.hpp"
|
||||
#include "core/app/script.hpp"
|
||||
#include "core/app/verify.hpp"
|
||||
#include "core/domain/analysis.hpp"
|
||||
#include "core/domain/connect.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/persist.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
#include "core/domain/signal_type.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
|
||||
#include <wx/wx.h>
|
||||
#include <wx/choicdlg.h>
|
||||
#include <wx/filedlg.h>
|
||||
#include <wx/filename.h>
|
||||
#include <wx/textdlg.h>
|
||||
#include <wx/treectrl.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
enum {
|
||||
ID_LOAD = wxID_HIGHEST + 1,
|
||||
ID_RESTORE,
|
||||
ID_RUN_SCRIPT,
|
||||
ID_SAVE,
|
||||
ID_EXPORT,
|
||||
ID_SET_CONNECTOR_TYPE,
|
||||
ID_ATTACH_BSDL,
|
||||
ID_SET_SIGNAL_TYPE,
|
||||
ID_CONNECT,
|
||||
ID_DUPLICATE,
|
||||
ID_VERIFY,
|
||||
ID_QUIT,
|
||||
ID_ABOUT,
|
||||
};
|
||||
|
||||
// Core (UTF-8 std::string) -> wxString, and back for paths.
|
||||
inline wxString wx(const std::string &s) { return wxString::FromUTF8(s.c_str()); }
|
||||
|
||||
// Natural order ("2" < "10", "A2" < "A10") so pin/part lists read intuitively.
|
||||
bool natural_less(const std::string &a, const std::string &b) {
|
||||
size_t i = 0, j = 0;
|
||||
while (i < a.size() && j < b.size()) {
|
||||
unsigned char ca = a[i], cb = b[j];
|
||||
if (std::isdigit(ca) && std::isdigit(cb)) {
|
||||
size_t i0 = i, j0 = j;
|
||||
while (i < a.size() && std::isdigit((unsigned char)a[i])) ++i;
|
||||
while (j < b.size() && std::isdigit((unsigned char)b[j])) ++j;
|
||||
std::string na = a.substr(i0, i - i0), nb = b.substr(j0, j - j0);
|
||||
na.erase(0, na.find_first_not_of('0')); // ignore leading zeros
|
||||
nb.erase(0, nb.find_first_not_of('0'));
|
||||
if (na.size() != nb.size()) return na.size() < nb.size();
|
||||
if (na != nb) return na < nb;
|
||||
} else {
|
||||
if (ca != cb) return ca < cb;
|
||||
++i; ++j;
|
||||
}
|
||||
}
|
||||
return a.size() < b.size();
|
||||
}
|
||||
|
||||
// " (Power)" / " (Gnd)" — only for the meaningful types; "" for Other.
|
||||
wxString type_suffix(SignalType t) {
|
||||
return t == SignalType::Other ? wxString()
|
||||
: " (" + wxString(signal_type_name(t)) + ")";
|
||||
}
|
||||
|
||||
// What a tree node stands for, attached to the item so a selection or a
|
||||
// right-click can drive the edit operations on the right domain object.
|
||||
struct NodeData : public wxTreeItemData {
|
||||
enum class Kind { Other, Module, Part, Pin, Signal };
|
||||
Kind kind;
|
||||
Module *module = nullptr;
|
||||
Part *part = nullptr;
|
||||
Signal *signal = nullptr;
|
||||
explicit NodeData(Kind k) : kind(k) {}
|
||||
};
|
||||
|
||||
NodeData *node_of(wxTreeCtrl *tree, const wxTreeItemId &id) {
|
||||
return id.IsOk() ? static_cast<NodeData *>(tree->GetItemData(id)) : nullptr;
|
||||
}
|
||||
|
||||
// The part of the current selection — a Part node, or the Pin's owning part.
|
||||
Part *selected_part(wxTreeCtrl *tree) {
|
||||
NodeData *d = node_of(tree, tree->GetSelection());
|
||||
if (d && (d->kind == NodeData::Kind::Part || d->kind == NodeData::Kind::Pin))
|
||||
return d->part;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// The signal of the current selection (and, via `mod`, its module).
|
||||
Signal *selected_signal(wxTreeCtrl *tree, Module **mod) {
|
||||
NodeData *d = node_of(tree, tree->GetSelection());
|
||||
if (d && d->kind == NodeData::Kind::Signal) {
|
||||
if (mod) *mod = d->module;
|
||||
return d->signal;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
EssimFrame::EssimFrame(WxFrontend &fe)
|
||||
: wxFrame(nullptr, wxID_ANY, "essim — system digital twin",
|
||||
wxDefaultPosition, wxSize(960, 640)),
|
||||
fe_(fe) {
|
||||
auto *file = new wxMenu;
|
||||
file->Append(ID_LOAD, "&Load module…\tCtrl-L");
|
||||
file->Append(ID_RESTORE, "&Restore snapshot…\tCtrl-R");
|
||||
file->Append(ID_RUN_SCRIPT, "&Run script…\tCtrl-U");
|
||||
file->Append(ID_SAVE, "&Save snapshot…\tCtrl-S");
|
||||
file->AppendSeparator();
|
||||
file->Append(ID_EXPORT, "&Export connections…\tCtrl-E");
|
||||
file->AppendSeparator();
|
||||
file->Append(ID_QUIT, "&Quit\tCtrl-Q");
|
||||
|
||||
auto *edit = new wxMenu;
|
||||
edit->Append(ID_SET_CONNECTOR_TYPE, "Set &connector type…\tCtrl-T");
|
||||
edit->Append(ID_ATTACH_BSDL, "Attach &BSDL…\tCtrl-B");
|
||||
edit->Append(ID_SET_SIGNAL_TYPE, "Set &signal type…\tCtrl-G");
|
||||
edit->AppendSeparator();
|
||||
edit->Append(ID_CONNECT, "C&onnect parts…\tCtrl-O");
|
||||
edit->AppendSeparator();
|
||||
edit->Append(ID_DUPLICATE, "&Duplicate module…\tCtrl-D");
|
||||
|
||||
auto *sysm = new wxMenu;
|
||||
sysm->Append(ID_VERIFY, "&Verify\tCtrl-K");
|
||||
|
||||
auto *help = new wxMenu;
|
||||
help->Append(ID_ABOUT, "&About");
|
||||
|
||||
auto *bar = new wxMenuBar;
|
||||
bar->Append(file, "&File");
|
||||
bar->Append(edit, "&Edit");
|
||||
bar->Append(sysm, "&System");
|
||||
bar->Append(help, "&Help");
|
||||
SetMenuBar(bar);
|
||||
|
||||
CreateStatusBar();
|
||||
SetStatusText("essim — wx frontend");
|
||||
|
||||
auto *panel = new wxPanel(this);
|
||||
tree_ = new wxTreeCtrl(panel, wxID_ANY);
|
||||
overview_ = new wxTextCtrl(panel, wxID_ANY, "", wxDefaultPosition,
|
||||
wxDefaultSize, wxTE_MULTILINE | wxTE_READONLY);
|
||||
log_ = new wxTextCtrl(panel, wxID_ANY, "", wxDefaultPosition,
|
||||
wxDefaultSize, wxTE_MULTILINE | wxTE_READONLY);
|
||||
|
||||
wxFont mono(wxFontInfo().Family(wxFONTFAMILY_TELETYPE));
|
||||
overview_->SetFont(mono);
|
||||
log_->SetFont(mono);
|
||||
|
||||
// Cap each control's minimum so its *content* can't inflate the layout's
|
||||
// minimum size: on GTK a full tree/text reports a large natural size, which
|
||||
// would otherwise eat all the vertical space and freeze the log at its
|
||||
// minimum (it stopped resizing once a script populated the tree). With a
|
||||
// modest min, the sizer proportions govern and content scrolls inside.
|
||||
tree_->SetMinSize(wxSize(260, 120));
|
||||
overview_->SetMinSize(wxSize(260, 120));
|
||||
log_->SetMinSize(wxSize(420, 90));
|
||||
|
||||
auto *top = new wxBoxSizer(wxHORIZONTAL);
|
||||
top->Add(tree_, 1, wxEXPAND | wxALL, 4);
|
||||
top->Add(overview_, 1, wxEXPAND | wxALL, 4);
|
||||
|
||||
auto *root = new wxBoxSizer(wxVERTICAL);
|
||||
root->Add(top, 2, wxEXPAND);
|
||||
root->Add(new wxStaticText(panel, wxID_ANY, " Log"), 0, wxLEFT | wxTOP, 6);
|
||||
root->Add(log_, 1, wxEXPAND | wxALL, 4);
|
||||
panel->SetSizer(root);
|
||||
|
||||
// Drive the panel from a frame sizer so it fills the client area and
|
||||
// re-lays-out on every resize (the implicit single-child fill is not
|
||||
// reliable here — without this the log keeps its size when the window grows).
|
||||
auto *frame_sizer = new wxBoxSizer(wxVERTICAL);
|
||||
frame_sizer->Add(panel, 1, wxEXPAND);
|
||||
SetSizer(frame_sizer);
|
||||
|
||||
Bind(wxEVT_MENU, &EssimFrame::OnLoad, this, ID_LOAD);
|
||||
Bind(wxEVT_MENU, &EssimFrame::OnRestore, this, ID_RESTORE);
|
||||
Bind(wxEVT_MENU, &EssimFrame::OnSave, this, ID_SAVE);
|
||||
Bind(wxEVT_MENU, &EssimFrame::OnRunScript, this, ID_RUN_SCRIPT);
|
||||
Bind(wxEVT_MENU, &EssimFrame::OnExport, this, ID_EXPORT);
|
||||
Bind(wxEVT_MENU, &EssimFrame::OnSetConnectorType, this, ID_SET_CONNECTOR_TYPE);
|
||||
Bind(wxEVT_MENU, &EssimFrame::OnAttachBsdl, this, ID_ATTACH_BSDL);
|
||||
Bind(wxEVT_MENU, &EssimFrame::OnSetSignalType, this, ID_SET_SIGNAL_TYPE);
|
||||
Bind(wxEVT_MENU, &EssimFrame::OnConnect, this, ID_CONNECT);
|
||||
Bind(wxEVT_MENU, &EssimFrame::OnDuplicateModule, this, ID_DUPLICATE);
|
||||
Bind(wxEVT_MENU, &EssimFrame::OnVerify, this, ID_VERIFY);
|
||||
Bind(wxEVT_MENU, &EssimFrame::OnQuit, this, ID_QUIT);
|
||||
Bind(wxEVT_MENU, &EssimFrame::OnAbout, this, ID_ABOUT);
|
||||
|
||||
tree_->Bind(wxEVT_TREE_ITEM_MENU, &EssimFrame::OnTreeContextMenu, this);
|
||||
|
||||
RebuildModelView();
|
||||
}
|
||||
|
||||
void EssimFrame::Log(const wxString &line) {
|
||||
log_->AppendText(line + "\n");
|
||||
}
|
||||
|
||||
void EssimFrame::RebuildModelView() {
|
||||
System *sys = fe_.system();
|
||||
|
||||
tree_->DeleteAllItems();
|
||||
wxTreeItemId root = tree_->AddRoot("System");
|
||||
|
||||
int n_mods = 0, n_parts = 0, n_sigs = 0;
|
||||
if (sys) {
|
||||
std::vector<std::string> mods;
|
||||
for (auto &mkv : *sys->modules()) mods.push_back(mkv.first);
|
||||
std::sort(mods.begin(), mods.end());
|
||||
n_mods = (int)mods.size();
|
||||
for (const auto &mname : mods) {
|
||||
Module *m = sys->modules()->get(mname);
|
||||
int mp = (int)m->size();
|
||||
int ms = (int)m->signals->size();
|
||||
n_parts += mp;
|
||||
n_sigs += ms;
|
||||
wxTreeItemId mid = tree_->AppendItem(
|
||||
root, wx(mname) + wxString::Format(" — %d part(s), %d signal(s)",
|
||||
mp, ms));
|
||||
{
|
||||
auto *d = new NodeData(NodeData::Kind::Module);
|
||||
d->module = m;
|
||||
tree_->SetItemData(mid, d);
|
||||
}
|
||||
|
||||
// Parts → pins (each pin shows the signal it is wired to, or NC).
|
||||
std::vector<std::string> parts;
|
||||
for (auto &pkv : *m) parts.push_back(pkv.first);
|
||||
std::sort(parts.begin(), parts.end(), natural_less);
|
||||
for (const auto &pname : parts) {
|
||||
Part *p = m->get(pname);
|
||||
wxString label = wx(pname)
|
||||
+ wxString::Format(" (%d pin(s))", (int)p->size());
|
||||
if (!p->connector_type.empty())
|
||||
label += " [" + wx(p->connector_type) + "]";
|
||||
wxTreeItemId pid = tree_->AppendItem(mid, label);
|
||||
{
|
||||
auto *d = new NodeData(NodeData::Kind::Part);
|
||||
d->module = m;
|
||||
d->part = p;
|
||||
tree_->SetItemData(pid, d);
|
||||
}
|
||||
|
||||
std::vector<std::string> pins;
|
||||
for (auto &nkv : *p) pins.push_back(nkv.first);
|
||||
std::sort(pins.begin(), pins.end(), natural_less);
|
||||
for (const auto &pinname : pins) {
|
||||
Pin *pin = p->get(pinname);
|
||||
wxString pl = wx(pinname) + " -> ";
|
||||
if (Signal *s = pin->signal()) {
|
||||
pl += wx(s->name) + type_suffix(s->type);
|
||||
} else {
|
||||
pl += "(NC";
|
||||
if (pin->nc_origin == NcOrigin::ImportedUnconnected)
|
||||
pl += ", imported";
|
||||
else if (pin->nc_origin == NcOrigin::DroppedSingleton)
|
||||
pl += ", dropped";
|
||||
pl += ")";
|
||||
}
|
||||
wxTreeItemId nid = tree_->AppendItem(pid, pl);
|
||||
auto *d = new NodeData(NodeData::Kind::Pin);
|
||||
d->module = m;
|
||||
d->part = p;
|
||||
tree_->SetItemData(nid, d);
|
||||
}
|
||||
}
|
||||
|
||||
// Signals branch (the per-module net view: type + fan-out).
|
||||
if (ms > 0) {
|
||||
wxTreeItemId sid =
|
||||
tree_->AppendItem(mid, wxString::Format("Signals (%d)", ms));
|
||||
std::vector<std::string> sigs;
|
||||
for (auto &skv : *m->signals) sigs.push_back(skv.first);
|
||||
std::sort(sigs.begin(), sigs.end(), natural_less);
|
||||
for (const auto &sname : sigs) {
|
||||
Signal *s = m->signals->get(sname);
|
||||
wxTreeItemId nid = tree_->AppendItem(
|
||||
sid, wx(sname) + type_suffix(s->type)
|
||||
+ wxString::Format(" — %d pin(s)", (int)s->size()));
|
||||
auto *d = new NodeData(NodeData::Kind::Signal);
|
||||
d->module = m;
|
||||
d->signal = s;
|
||||
tree_->SetItemData(nid, d);
|
||||
}
|
||||
}
|
||||
|
||||
tree_->Expand(mid); // parts + Signals visible; pins/nets on demand
|
||||
}
|
||||
}
|
||||
tree_->Expand(root);
|
||||
|
||||
int n_conn = sys ? (int)sys->connections()->size() : 0;
|
||||
|
||||
wxString ov;
|
||||
ov << "Modules: " << n_mods << "\n"
|
||||
<< "Parts: " << n_parts << "\n"
|
||||
<< "Signals: " << n_sigs << "\n"
|
||||
<< "Connections: " << n_conn << "\n";
|
||||
if (sys) {
|
||||
app::VerifyReport r = app::verify(sys);
|
||||
ov << "\nHealth (verify):\n"
|
||||
<< wxString::Format(" pin-role mismatches: %d / %d typed pin(s)\n",
|
||||
(int)r.role_mismatches.size(), r.typed_pins)
|
||||
<< wxString::Format(" net inconsistencies: %d / %d bridged net(s)\n",
|
||||
(int)r.net_inconsistencies.size(), r.bridged_nets)
|
||||
<< wxString::Format(" orphan pins: %d (%d imported, %d dropped)\n",
|
||||
r.orphan_total(), r.orphan_imported, r.orphan_dropped)
|
||||
<< wxString::Format(" model anomalies: %d\n", r.model_total());
|
||||
}
|
||||
overview_->SetValue(ov);
|
||||
}
|
||||
|
||||
void EssimFrame::OnLoad(wxCommandEvent &) {
|
||||
wxFileDialog dlg(this, "Load a netlist / pinout file", "", "",
|
||||
"All files (*.*)|*.*", wxFD_OPEN | wxFD_FILE_MUST_EXIST);
|
||||
if (dlg.ShowModal() != wxID_OK) return;
|
||||
const wxString path = dlg.GetPath();
|
||||
|
||||
wxString modname = wxGetTextFromUser("Module name:", "Load module",
|
||||
wxFileName(path).GetName(), this);
|
||||
if (modname.empty()) return;
|
||||
|
||||
static const wxString kinds[] = {"mentor", "altium", "ods"};
|
||||
int ki = wxGetSingleChoiceIndex("Import type:", "Load module",
|
||||
WXSIZEOF(kinds), kinds, this);
|
||||
if (ki < 0) return;
|
||||
|
||||
ImportType type;
|
||||
app::import_type_from_name(kinds[ki].ToStdString(), type); // choice is valid
|
||||
app::LoadResult r = app::load_module(
|
||||
fe_.system(), modname.utf8_string(), path.utf8_string(), type);
|
||||
if (!r.ok) {
|
||||
Log("load failed: " + wx(r.error));
|
||||
wxMessageBox(wx(r.error), "Load failed", wxOK | wxICON_ERROR, this);
|
||||
return;
|
||||
}
|
||||
Log(wxString::Format(
|
||||
"loaded '%s' from %s — %d part(s), %d signal(s)"
|
||||
" (dropped %d; types: %d power / %d gnd / %d suspect / %d pwr-mgmt)",
|
||||
modname, path, r.parts, r.signals, r.dropped, r.power, r.gnd,
|
||||
r.kept_other, r.mgmt));
|
||||
RebuildModelView();
|
||||
}
|
||||
|
||||
void EssimFrame::OnRestore(wxCommandEvent &) {
|
||||
wxFileDialog dlg(this, "Restore a system snapshot", "", "",
|
||||
"essim snapshots (*.essim)|*.essim|All files (*.*)|*.*",
|
||||
wxFD_OPEN | wxFD_FILE_MUST_EXIST);
|
||||
if (dlg.ShowModal() != wxID_OK) return;
|
||||
std::string err;
|
||||
System *fresh = restore_system(dlg.GetPath().utf8_string(), err);
|
||||
if (!fresh) {
|
||||
Log("restore failed: " + wx(err));
|
||||
wxMessageBox(wx(err), "Restore failed", wxOK | wxICON_ERROR, this);
|
||||
return;
|
||||
}
|
||||
fe_.set_system(fresh);
|
||||
Log("restored from " + dlg.GetPath());
|
||||
RebuildModelView();
|
||||
}
|
||||
|
||||
void EssimFrame::OnRunScript(wxCommandEvent &) {
|
||||
wxFileDialog dlg(this, "Run an essim script", "", "",
|
||||
"essim scripts (*.essim)|*.essim|All files (*.*)|*.*",
|
||||
wxFD_OPEN | wxFD_FILE_MUST_EXIST);
|
||||
if (dlg.ShowModal() != wxID_OK) return;
|
||||
|
||||
fe_.ensure_system();
|
||||
std::ostringstream out;
|
||||
app::ScriptResult r =
|
||||
app::run_script(fe_.system_ptr(), dlg.GetPath().utf8_string(), out);
|
||||
|
||||
if (!r.ok) {
|
||||
Log("run script: " + wx(r.error));
|
||||
wxMessageBox(wx(r.error), "Run script", wxOK | wxICON_ERROR, this);
|
||||
return;
|
||||
}
|
||||
|
||||
// Echo each line of the script's output into the log pane.
|
||||
std::istringstream lines(out.str());
|
||||
std::string line;
|
||||
while (std::getline(lines, line)) Log(wx(line));
|
||||
Log(wxString::Format("source: %s (%d line(s), %d error(s))",
|
||||
dlg.GetPath(), r.lines, r.errors));
|
||||
RebuildModelView();
|
||||
}
|
||||
|
||||
void EssimFrame::OnSave(wxCommandEvent &) {
|
||||
wxFileDialog dlg(this, "Save system snapshot", "", "system.essim",
|
||||
"essim snapshots (*.essim)|*.essim|All files (*.*)|*.*",
|
||||
wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
|
||||
if (dlg.ShowModal() != wxID_OK) return;
|
||||
std::string err;
|
||||
if (save_system(fe_.system(), dlg.GetPath().utf8_string(), err)) {
|
||||
Log("saved to " + dlg.GetPath());
|
||||
} else {
|
||||
Log("save failed: " + wx(err));
|
||||
wxMessageBox(wx(err), "Save failed", wxOK | wxICON_ERROR, this);
|
||||
}
|
||||
}
|
||||
|
||||
void EssimFrame::OnExport(wxCommandEvent &) {
|
||||
wxFileDialog dlg(this, "Export connections", "", "connections.csv",
|
||||
"CSV (*.csv)|*.csv|OpenDocument sheet (*.ods)|*.ods",
|
||||
wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
|
||||
if (dlg.ShowModal() != wxID_OK) return;
|
||||
const std::string path = dlg.GetPath().utf8_string();
|
||||
|
||||
app::ExportFormat fmt;
|
||||
if (!app::export_format_from_path(path, fmt)) {
|
||||
wxMessageBox("Unknown export extension (use .csv or .ods).",
|
||||
"Export failed", wxOK | wxICON_ERROR, this);
|
||||
return;
|
||||
}
|
||||
app::ExportResult r = app::export_connections(fe_.system(), path, fmt);
|
||||
if (r.ok) {
|
||||
Log(wxString::Format("exported %d row(s) to %s", r.rows, dlg.GetPath()));
|
||||
} else {
|
||||
Log("export failed: " + wx(r.error));
|
||||
wxMessageBox(wx(r.error), "Export failed", wxOK | wxICON_ERROR, this);
|
||||
}
|
||||
}
|
||||
|
||||
Module *EssimFrame::PickModule(const wxString &caption) {
|
||||
System *sys = fe_.system();
|
||||
if (!sys || sys->modules()->size() == 0) {
|
||||
wxMessageBox("No modules loaded.", caption,
|
||||
wxOK | wxICON_INFORMATION, this);
|
||||
return nullptr;
|
||||
}
|
||||
std::vector<std::string> mods;
|
||||
for (auto &mkv : *sys->modules()) mods.push_back(mkv.first);
|
||||
std::sort(mods.begin(), mods.end());
|
||||
wxArrayString choices;
|
||||
for (const auto &m : mods) choices.Add(wx(m));
|
||||
int mi = wxGetSingleChoiceIndex("Module:", caption, choices, this);
|
||||
if (mi < 0) return nullptr;
|
||||
return sys->modules()->get(mods[mi]);
|
||||
}
|
||||
|
||||
Part *EssimFrame::PickPart(const wxString &caption) {
|
||||
Module *m = PickModule(caption);
|
||||
if (!m) return nullptr;
|
||||
if (m->size() == 0) {
|
||||
wxMessageBox("That module has no parts.", caption,
|
||||
wxOK | wxICON_INFORMATION, this);
|
||||
return nullptr;
|
||||
}
|
||||
std::vector<std::string> parts;
|
||||
for (auto &pkv : *m) parts.push_back(pkv.first);
|
||||
std::sort(parts.begin(), parts.end());
|
||||
wxArrayString choices;
|
||||
for (const auto &p : parts) choices.Add(wx(p));
|
||||
int pi = wxGetSingleChoiceIndex("Part:", caption, choices, this);
|
||||
if (pi < 0) return nullptr;
|
||||
return m->get(parts[pi]);
|
||||
}
|
||||
|
||||
|
||||
void EssimFrame::OnSetConnectorType(wxCommandEvent &) {
|
||||
Part *p = selected_part(tree_);
|
||||
if (!p) p = PickPart();
|
||||
if (!p) return;
|
||||
|
||||
wxTextEntryDialog dlg(this, "Connector type (empty = none):",
|
||||
"Set connector type", wx(p->connector_type));
|
||||
if (dlg.ShowModal() != wxID_OK) return;
|
||||
const std::string kind = dlg.GetValue().utf8_string();
|
||||
|
||||
app::SetConnectorTypeResult r = app::set_connector_type(p, kind);
|
||||
if (!r.ok) {
|
||||
Log("set-connector-type refused: " + wx(r.error));
|
||||
wxMessageBox(wx(r.error), "Refused", wxOK | wxICON_ERROR, this);
|
||||
return;
|
||||
}
|
||||
|
||||
wxString who = (p->prnt ? wx(p->prnt->name) + "/" : wxString()) + wx(p->name);
|
||||
Log(who + ": connector_type = " + (kind.empty() ? wxString("(none)") : wx(kind)));
|
||||
if (r.materialised > 0)
|
||||
Log(wxString::Format(" added %d NC pin(s) from the connector layout",
|
||||
r.materialised));
|
||||
RebuildModelView();
|
||||
}
|
||||
|
||||
void EssimFrame::OnAttachBsdl(wxCommandEvent &) {
|
||||
Part *p = selected_part(tree_);
|
||||
if (!p) p = PickPart();
|
||||
if (!p) return;
|
||||
|
||||
wxFileDialog dlg(this, "Attach a BSDL model", "", "",
|
||||
"BSDL files (*.bsd)|*.bsd|All files (*.*)|*.*",
|
||||
wxFD_OPEN | wxFD_FILE_MUST_EXIST);
|
||||
if (dlg.ShowModal() != wxID_OK) return;
|
||||
|
||||
app::AttachBsdlResult r = app::attach_bsdl(p, dlg.GetPath().utf8_string());
|
||||
if (!r.ok) {
|
||||
Log("attach-bsdl: " + wx(r.error));
|
||||
wxMessageBox(wx(r.error), "Attach BSDL failed", wxOK | wxICON_ERROR, this);
|
||||
return;
|
||||
}
|
||||
|
||||
wxString who = (p->prnt ? wx(p->prnt->name) + "/" : wxString()) + wx(p->name);
|
||||
wxString tail = r.unbound ? wxString::Format(", %d unbound", r.unbound)
|
||||
: wxString();
|
||||
Log(wxString::Format("%s: attached BSDL '%s' — %d/%d ports bound%s",
|
||||
who, wx(r.entity), r.bound, r.ports_total, tail));
|
||||
RebuildModelView();
|
||||
}
|
||||
|
||||
void EssimFrame::OnConnect(wxCommandEvent &) {
|
||||
Part *p1 = selected_part(tree_);
|
||||
if (!p1) p1 = PickPart("Connect — first part");
|
||||
if (!p1) return;
|
||||
Part *p2 = PickPart("Connect — second part");
|
||||
if (!p2) return;
|
||||
if (p1 == p2) {
|
||||
wxMessageBox("Pick two different parts.", "Connect",
|
||||
wxOK | wxICON_INFORMATION, this);
|
||||
return;
|
||||
}
|
||||
|
||||
// m1/m2 are the parts' parent modules — connect_parts needs them for the
|
||||
// Connection name and ownership.
|
||||
app::ConnectResult r =
|
||||
app::connect_parts(fe_.system(), p1->prnt, p1, p2->prnt, p2);
|
||||
|
||||
if (r.refused) {
|
||||
Log("connect refused: " + wx(r.error));
|
||||
wxMessageBox(wx(r.error), "Connect refused", wxOK | wxICON_ERROR, this);
|
||||
return;
|
||||
}
|
||||
if (!r.identity_info.empty()) {
|
||||
Log("connect: " + wx(r.identity_info));
|
||||
if (r.nc_added > 0)
|
||||
Log(wxString::Format("connect: added %d NC pin(s) so both sides match",
|
||||
r.nc_added));
|
||||
}
|
||||
if (r.ok) {
|
||||
Log(wxString::Format("connected: %s via %s (%d wires)",
|
||||
wx(r.connection_name), wx(r.transform_name), r.wires));
|
||||
} else {
|
||||
Log("connect failed: " + wx(r.error));
|
||||
wxMessageBox(wx(r.error), "Connect failed", wxOK | wxICON_ERROR, this);
|
||||
}
|
||||
RebuildModelView();
|
||||
}
|
||||
|
||||
void EssimFrame::OnSetSignalType(wxCommandEvent &) {
|
||||
Module *m = nullptr;
|
||||
Signal *sig = selected_signal(tree_, &m);
|
||||
if (!sig) {
|
||||
m = PickModule("Set signal type");
|
||||
if (!m) return;
|
||||
if (m->signals->size() == 0) {
|
||||
wxMessageBox("That module has no signals.", "Set signal type",
|
||||
wxOK | wxICON_INFORMATION, this);
|
||||
return;
|
||||
}
|
||||
std::vector<std::string> sigs;
|
||||
for (auto &skv : *m->signals) sigs.push_back(skv.first);
|
||||
std::sort(sigs.begin(), sigs.end(), natural_less);
|
||||
wxArrayString schoices;
|
||||
for (const auto &s : sigs) schoices.Add(wx(s));
|
||||
int si = wxGetSingleChoiceIndex("Signal:", "Set signal type", schoices, this);
|
||||
if (si < 0) return;
|
||||
sig = m->signals->get(sigs[si]);
|
||||
}
|
||||
|
||||
static const wxString types[] = {"power", "gnd", "other"};
|
||||
int ti = wxGetSingleChoiceIndex("Type:", "Set signal type",
|
||||
WXSIZEOF(types), types, this);
|
||||
if (ti < 0) return;
|
||||
|
||||
app::SetSignalTypeResult r = app::set_signal_type(sig, types[ti].ToStdString());
|
||||
if (!r.ok) {
|
||||
Log(wx(r.error));
|
||||
wxMessageBox(wx(r.error), "Set signal type", wxOK | wxICON_ERROR, this);
|
||||
return;
|
||||
}
|
||||
Log(wxString::Format("%s/%s: signal type = %s", wx(m->name), wx(sig->name),
|
||||
wx(signal_type_name(r.type))));
|
||||
RebuildModelView();
|
||||
}
|
||||
|
||||
void EssimFrame::OnDuplicateModule(wxCommandEvent &) {
|
||||
Module *m = PickModule("Duplicate module");
|
||||
if (!m) return;
|
||||
const std::string src = m->name; // m may move in the table after the add
|
||||
|
||||
wxTextEntryDialog dlg(this, "New module name:", "Duplicate module",
|
||||
wx(src) + "_copy");
|
||||
if (dlg.ShowModal() != wxID_OK) return;
|
||||
const std::string dst = dlg.GetValue().utf8_string();
|
||||
if (dst.empty()) return;
|
||||
|
||||
app::DuplicateResult r = app::duplicate_module(fe_.system(), src, dst);
|
||||
if (!r.ok) {
|
||||
Log(wx(r.error));
|
||||
wxMessageBox(wx(r.error), "Duplicate module", wxOK | wxICON_ERROR, this);
|
||||
return;
|
||||
}
|
||||
Log(wx("duplicate: '" + src + "' → '" + dst + "' ("
|
||||
+ std::to_string(r.parts) + " part(s), "
|
||||
+ std::to_string(r.signals) + " signal(s))"));
|
||||
RebuildModelView();
|
||||
}
|
||||
|
||||
void EssimFrame::OnVerify(wxCommandEvent &) {
|
||||
app::VerifyReport r = app::verify(fe_.system());
|
||||
|
||||
Log("verify:");
|
||||
Log(wxString::Format(" %d pin-role mismatch(es) over %d typed pin(s)",
|
||||
(int)r.role_mismatches.size(), r.typed_pins));
|
||||
for (const auto &m : r.role_mismatches)
|
||||
Log(wx(" " + m.module + "/" + m.part + "/" + m.pin + ": expected "
|
||||
+ signal_type_name(m.expected) + ", got "
|
||||
+ signal_type_name(m.actual)));
|
||||
|
||||
Log(wxString::Format(" %d inconsistent net(s) over %d bridged net(s)",
|
||||
(int)r.net_inconsistencies.size(), r.bridged_nets));
|
||||
Log(wxString::Format(" %d orphan pin(s) (%d imported, %d dropped)",
|
||||
r.orphan_total(), r.orphan_imported, r.orphan_dropped));
|
||||
|
||||
auto log_anoms = [this](const std::vector<Anomaly> &v, const char *tail) {
|
||||
Log(wxString::Format(" %d %s", (int)v.size(), tail));
|
||||
for (const auto &a : v)
|
||||
Log(wx(std::string(" [") + anomaly_kind_name(a.kind) + "] "
|
||||
+ a.message));
|
||||
};
|
||||
log_anoms(r.pin_anomalies, "model-driven pin anomaly(ies)");
|
||||
log_anoms(r.jtag_anomalies, "JTAG chain anomaly(ies)");
|
||||
log_anoms(r.conflict_anomalies, "source-conflict(s)");
|
||||
log_anoms(r.completeness_anomalies, "BSDL completeness issue(s)");
|
||||
log_anoms(r.diff_anomalies, "diff-pair crossing anomaly(ies)");
|
||||
|
||||
RebuildModelView();
|
||||
}
|
||||
|
||||
void EssimFrame::OnQuit(wxCommandEvent &) { Close(true); }
|
||||
|
||||
void EssimFrame::OnAbout(wxCommandEvent &) {
|
||||
wxMessageBox("essim — system digital twin\n\n"
|
||||
"wxWidgets frontend over essim_core.",
|
||||
"About essim", wxOK | wxICON_INFORMATION, this);
|
||||
}
|
||||
|
||||
void EssimFrame::OnTreeContextMenu(wxTreeEvent &ev) {
|
||||
wxTreeItemId id = ev.GetItem();
|
||||
if (id.IsOk()) tree_->SelectItem(id); // the edit handlers read the selection
|
||||
NodeData *d = node_of(tree_, id);
|
||||
if (!d) return;
|
||||
|
||||
// Reuse the menu IDs so these route to the same handlers, which now act on
|
||||
// the (just-selected) tree item.
|
||||
wxMenu menu;
|
||||
if (d->kind == NodeData::Kind::Part || d->kind == NodeData::Kind::Pin) {
|
||||
menu.Append(ID_SET_CONNECTOR_TYPE, "Set connector type…");
|
||||
menu.Append(ID_ATTACH_BSDL, "Attach BSDL…");
|
||||
menu.Append(ID_CONNECT, "Connect to…");
|
||||
} else if (d->kind == NodeData::Kind::Signal) {
|
||||
menu.Append(ID_SET_SIGNAL_TYPE, "Set signal type…");
|
||||
}
|
||||
if (menu.GetMenuItemCount() > 0) PopupMenu(&menu);
|
||||
}
|
||||
56
src/frontends/wx/wx_frame.hpp
Normal file
56
src/frontends/wx/wx_frame.hpp
Normal file
@@ -0,0 +1,56 @@
|
||||
#ifndef _WX_FRAME_HPP_
|
||||
#define _WX_FRAME_HPP_
|
||||
|
||||
#include <wx/frame.h>
|
||||
|
||||
class WxFrontend;
|
||||
class wxTreeCtrl;
|
||||
class wxTextCtrl;
|
||||
class wxCommandEvent;
|
||||
class wxTreeEvent;
|
||||
|
||||
// The essim main window. Holds no domain state of its own: it reads and mutates
|
||||
// the System owned by the WxFrontend, calling the core/app operations directly
|
||||
// (load, verify, export, save, restore) and rendering their results into a
|
||||
// model tree, an overview panel and a log.
|
||||
class EssimFrame : public wxFrame {
|
||||
public:
|
||||
explicit EssimFrame(WxFrontend &fe);
|
||||
|
||||
private:
|
||||
// Menu handlers — each is a thin wrapper over a core/app operation.
|
||||
void OnLoad(wxCommandEvent &);
|
||||
void OnRestore(wxCommandEvent &);
|
||||
void OnRunScript(wxCommandEvent &);
|
||||
void OnSave(wxCommandEvent &);
|
||||
void OnExport(wxCommandEvent &);
|
||||
void OnSetConnectorType(wxCommandEvent &);
|
||||
void OnAttachBsdl(wxCommandEvent &);
|
||||
void OnConnect(wxCommandEvent &);
|
||||
void OnSetSignalType(wxCommandEvent &);
|
||||
void OnDuplicateModule(wxCommandEvent &);
|
||||
void OnVerify(wxCommandEvent &);
|
||||
void OnQuit(wxCommandEvent &);
|
||||
void OnAbout(wxCommandEvent &);
|
||||
|
||||
// Right-click on a tree item → context menu of the edit actions valid for
|
||||
// that node (part / signal). The actions reuse the menu IDs, so they run
|
||||
// the same handlers — which read the tree selection.
|
||||
void OnTreeContextMenu(wxTreeEvent &);
|
||||
|
||||
// Modal pickers over the current System. `caption` titles the dialogs (e.g.
|
||||
// to distinguish two picks). Each returns nullptr if there is nothing to
|
||||
// pick or the user cancels.
|
||||
class Module *PickModule(const wxString &caption);
|
||||
class Part *PickPart(const wxString &caption = "Select part");
|
||||
|
||||
void RebuildModelView(); ///< refresh tree + overview from the System
|
||||
void Log(const wxString &line); ///< append a line to the log pane
|
||||
|
||||
WxFrontend &fe_;
|
||||
wxTreeCtrl *tree_ = nullptr;
|
||||
wxTextCtrl *overview_ = nullptr;
|
||||
wxTextCtrl *log_ = nullptr;
|
||||
};
|
||||
|
||||
#endif // _WX_FRAME_HPP_
|
||||
113
src/frontends/wx/wx_frontend.cpp
Normal file
113
src/frontends/wx/wx_frontend.cpp
Normal file
@@ -0,0 +1,113 @@
|
||||
#include "frontends/wx/wx_frontend.hpp"
|
||||
|
||||
#include "frontends/wx/wx_frame.hpp"
|
||||
|
||||
#include "core/app/script.hpp"
|
||||
#include "core/domain/connect.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/persist.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
|
||||
#include <wx/app.h>
|
||||
#include <wx/init.h>
|
||||
|
||||
#include <clocale>
|
||||
#include <ostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
// Minimal wxApp: on init it shows the main window bound to the frontend.
|
||||
class EssimApp : public wxApp {
|
||||
public:
|
||||
explicit EssimApp(WxFrontend &fe) : fe_(fe) {}
|
||||
bool OnInit() override {
|
||||
// Decode the UTF-8 in our narrow string literals (em dash, ellipsis…)
|
||||
// correctly: wxString converts const char* via the C locale, which is
|
||||
// "C" (ASCII) at startup. Set only LC_CTYPE — leave LC_NUMERIC as "C"
|
||||
// so number formatting stays dot-decimal.
|
||||
std::setlocale(LC_CTYPE, "");
|
||||
(new EssimFrame(fe_))->Show(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
WxFrontend &fe_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
WxFrontend::WxFrontend() = default;
|
||||
WxFrontend::~WxFrontend() = default;
|
||||
|
||||
void WxFrontend::ensure_system() {
|
||||
if (!sys_) sys_.reset(new System());
|
||||
}
|
||||
|
||||
void WxFrontend::set_system(System *fresh) {
|
||||
sys_.reset(fresh);
|
||||
}
|
||||
|
||||
void WxFrontend::BootDispatch(const std::string &raw) {
|
||||
// The GUI has no command shell. Honour the boot commands that make sense
|
||||
// headlessly: `restore <file>` seeds a snapshot; anything else is noted.
|
||||
std::istringstream iss(raw);
|
||||
std::string cmd;
|
||||
iss >> cmd;
|
||||
std::string arg;
|
||||
std::getline(iss, arg);
|
||||
if (std::size_t b = arg.find_first_not_of(" \t"); b != std::string::npos)
|
||||
arg = arg.substr(b);
|
||||
else
|
||||
arg.clear();
|
||||
|
||||
if (cmd == "restore") {
|
||||
std::string err;
|
||||
System *fresh = restore_system(arg, err);
|
||||
if (!fresh) {
|
||||
output_ += "restore failed: " + err + "\n";
|
||||
return;
|
||||
}
|
||||
sys_.reset(fresh);
|
||||
output_ += "restored from " + arg + " ("
|
||||
+ std::to_string(sys_->modules()->size()) + " module(s), "
|
||||
+ std::to_string(sys_->connections()->size())
|
||||
+ " connection(s))\n";
|
||||
} else if (cmd == "source") {
|
||||
ensure_system();
|
||||
std::ostringstream out;
|
||||
app::ScriptResult r = app::run_script(sys_, arg, out);
|
||||
output_ += out.str();
|
||||
if (!r.ok)
|
||||
output_ += "source: " + r.error + "\n";
|
||||
else
|
||||
output_ += "source: " + arg + " (" + std::to_string(r.lines)
|
||||
+ " line(s), " + std::to_string(r.errors) + " error(s))\n";
|
||||
} else if (!cmd.empty()) {
|
||||
output_ += "boot: ignored '" + raw + "'.\n";
|
||||
}
|
||||
}
|
||||
|
||||
void WxFrontend::DumpCommandsMd(std::ostream &out) const {
|
||||
out << "# essim — wx frontend\n\n"
|
||||
<< "The wx frontend is menu-driven and exposes no textual command "
|
||||
<< "registry. Generate the command reference from the tui frontend "
|
||||
<< "(`-DESSIM_FRONTEND=tui`, then `essim --commands-md`).\n";
|
||||
}
|
||||
|
||||
void WxFrontend::DumpOutput(std::ostream &out) const {
|
||||
out << output_;
|
||||
}
|
||||
|
||||
void WxFrontend::Run() {
|
||||
ensure_system();
|
||||
|
||||
wxApp::SetInstance(new EssimApp(*this));
|
||||
int argc = 0;
|
||||
wxEntryStart(argc, static_cast<char **>(nullptr));
|
||||
if (wxTheApp->CallOnInit())
|
||||
wxTheApp->OnRun();
|
||||
wxTheApp->OnExit();
|
||||
wxEntryCleanup();
|
||||
}
|
||||
38
src/frontends/wx/wx_frontend.hpp
Normal file
38
src/frontends/wx/wx_frontend.hpp
Normal file
@@ -0,0 +1,38 @@
|
||||
#ifndef _WX_FRONTEND_HPP_
|
||||
#define _WX_FRONTEND_HPP_
|
||||
|
||||
#include "frontends/frontend.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
class System;
|
||||
|
||||
// wxWidgets GUI frontend. Implements the shared Frontend interface so the same
|
||||
// launcher (frontend_main) drives it: it owns the System and a console buffer,
|
||||
// handles boot commands headlessly (for --restore/--batch), and Run() opens the
|
||||
// wxWidgets window. The window itself (EssimFrame) drives essim_core / app::*
|
||||
// operations directly — no command shell, no TUI reuse.
|
||||
class WxFrontend : public Frontend {
|
||||
public:
|
||||
WxFrontend();
|
||||
~WxFrontend() override;
|
||||
|
||||
// --- Frontend interface ---
|
||||
void BootDispatch(const std::string &raw) override;
|
||||
void DumpCommandsMd(std::ostream &out) const override;
|
||||
void DumpOutput(std::ostream &out) const override;
|
||||
void Run() override;
|
||||
|
||||
// --- used by the window (EssimFrame) ---
|
||||
System *system() const { return sys_.get(); }
|
||||
std::unique_ptr<System> &system_ptr() { return sys_; } ///< for run_script (new/restore replace it)
|
||||
void set_system(System *fresh); ///< take ownership (used by Restore)
|
||||
void ensure_system(); ///< create an empty System if none yet
|
||||
|
||||
private:
|
||||
std::unique_ptr<System> sys_;
|
||||
std::string output_; ///< console buffer surfaced by DumpOutput (batch)
|
||||
};
|
||||
|
||||
#endif // _WX_FRONTEND_HPP_
|
||||
@@ -235,7 +235,7 @@ TEST_CASE("infer_signal_types: Power requires name+structural agreement") {
|
||||
|
||||
Signal *p_3v3 = m->signals->merge("PWR_3V3"); fan_out(p_3v3, 3); // voltage + ≥ floor → Power
|
||||
Signal *vcc = m->signals->merge("VCC"); fan_out(vcc, 5); // fan-out ≥ 4 → Power
|
||||
Signal *pwr_ok = m->signals->merge("PWR_OK"); fan_out(pwr_ok, 1); // < 3 → hard floor → Other
|
||||
Signal *pwr_ok = m->signals->merge("PWR_OK"); fan_out(pwr_ok, 1); // control token → pwr-mgmt
|
||||
Signal *pwr_2 = m->signals->merge("PWR_2"); fan_out(pwr_2, 2); // < 3 → hard floor → Other
|
||||
Signal *gnd = m->signals->merge("GND"); fan_out(gnd, 1); // gnd: name alone
|
||||
Signal *clk = m->signals->merge("CLK_50MHZ"); fan_out(clk, 3); // not power-ish → Other
|
||||
@@ -243,7 +243,8 @@ TEST_CASE("infer_signal_types: Power requires name+structural agreement") {
|
||||
auto st = infer_signal_types(sys.get());
|
||||
CHECK(st.power == 2); // PWR_3V3, VCC
|
||||
CHECK(st.gnd == 1); // GND (name alone)
|
||||
CHECK(st.kept_other == 2); // PWR_OK, PWR_2 below the hard floor
|
||||
CHECK(st.kept_other == 1); // PWR_2 below the hard floor
|
||||
CHECK(st.mgmt == 1); // PWR_OK: power-good control, not suspect
|
||||
|
||||
CHECK(p_3v3->type == SignalType::Power);
|
||||
CHECK(vcc->type == SignalType::Power);
|
||||
@@ -253,6 +254,27 @@ TEST_CASE("infer_signal_types: Power requires name+structural agreement") {
|
||||
CHECK(clk->type == SignalType::Other);
|
||||
}
|
||||
|
||||
TEST_CASE("infer_signal_types: power-management beats fan-out — a big sense net is still Other") {
|
||||
auto sys = std::make_unique<System>();
|
||||
Module *m = sys->modules()->merge("M");
|
||||
Part *p = new Part("U1"); m->add(p);
|
||||
|
||||
// VDD_CORE_SENSE with fan-out 5: structure alone would confirm Power,
|
||||
// but the control token settles it as a measurement net → Other,
|
||||
// counted mgmt (not suspect, not power).
|
||||
Signal *s = m->signals->merge("VDD_CORE_SENSE");
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
Pin *pin = new Pin("p" + std::to_string(i));
|
||||
p->add(pin); s->add(pin); pin->connect(s);
|
||||
}
|
||||
|
||||
auto st = infer_signal_types(sys.get());
|
||||
CHECK(st.power == 0);
|
||||
CHECK(st.kept_other == 0);
|
||||
CHECK(st.mgmt == 1);
|
||||
CHECK(s->type == SignalType::Other);
|
||||
}
|
||||
|
||||
TEST_CASE("infer_signal_types: fan-out hard floor overrides voltage in name") {
|
||||
auto sys = std::make_unique<System>();
|
||||
Module *m = sys->modules()->merge("M");
|
||||
|
||||
79
tests/test_connect.cpp
Normal file
79
tests/test_connect.cpp
Normal file
@@ -0,0 +1,79 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "core/app/connect.hpp"
|
||||
#include "core/domain/connect.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
|
||||
// app::connect_parts is pure core: given two already-resolved parts it looks up
|
||||
// the transform, fills identity NC pins, creates the Connection and returns a
|
||||
// ConnectResult. No Print/dialog/FTXUI. These tests drive it directly.
|
||||
|
||||
namespace {
|
||||
|
||||
// A part with the given pin names, attached to a fresh module.
|
||||
Part *make_part(System &sys, const std::string &mod, const std::string &part,
|
||||
std::initializer_list<const char *> pins,
|
||||
const std::string &type = "")
|
||||
{
|
||||
Module *m = sys.modules()->merge(mod);
|
||||
Part *p = new Part(part);
|
||||
p->connector_type = type;
|
||||
m->add(p);
|
||||
for (const char *pn : pins) p->add(new Pin(pn));
|
||||
return p;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("connect_parts wires an identity-compatible pair") {
|
||||
System sys;
|
||||
Module *a = sys.modules()->merge("A");
|
||||
Module *b = sys.modules()->merge("B");
|
||||
Part *p1 = make_part(sys, "A", "J1", {"1", "2"});
|
||||
Part *p2 = make_part(sys, "B", "P1", {"1", "2"});
|
||||
|
||||
app::ConnectResult r = app::connect_parts(&sys, a, p1, b, p2);
|
||||
|
||||
CHECK(r.ok);
|
||||
CHECK_FALSE(r.refused);
|
||||
CHECK(r.transform_name == "identity");
|
||||
CHECK(r.wires == 2);
|
||||
CHECK(r.identity_info.empty()); // identical sets → no NC fill, no warning
|
||||
CHECK(r.nc_added == 0);
|
||||
CHECK(r.connection_name == "A/J1 <-> B/P1");
|
||||
CHECK(sys.connections()->size() == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("connect_parts refuses an unknown connector-type pairing") {
|
||||
System sys;
|
||||
Module *a = sys.modules()->merge("A");
|
||||
Module *b = sys.modules()->merge("B");
|
||||
Part *p1 = make_part(sys, "A", "J1", {"1"}, "foo");
|
||||
Part *p2 = make_part(sys, "B", "P1", {"1"}, "bar");
|
||||
|
||||
app::ConnectResult r = app::connect_parts(&sys, a, p1, b, p2);
|
||||
|
||||
CHECK_FALSE(r.ok);
|
||||
CHECK(r.refused);
|
||||
CHECK(r.error.find("no transform") != std::string::npos);
|
||||
CHECK(sys.connections()->size() == 0); // nothing created
|
||||
}
|
||||
|
||||
TEST_CASE("connect_parts fills NC pins on the subset side and reports it") {
|
||||
System sys;
|
||||
Module *a = sys.modules()->merge("A");
|
||||
Module *b = sys.modules()->merge("B");
|
||||
Part *p1 = make_part(sys, "A", "J1", {"1", "2", "3"}); // larger side
|
||||
Part *p2 = make_part(sys, "B", "P1", {"1", "2"}); // missing "3"
|
||||
|
||||
app::ConnectResult r = app::connect_parts(&sys, a, p1, b, p2);
|
||||
|
||||
CHECK(r.ok);
|
||||
CHECK_FALSE(r.identity_info.empty()); // subset path surfaces a warning
|
||||
CHECK(r.nc_added == 1); // pin "3" materialised on B
|
||||
CHECK(r.wires == 3); // all three now wired
|
||||
CHECK(p2->size() == 3); // the NC pin really got added
|
||||
}
|
||||
184
tests/test_diff_check.cpp
Normal file
184
tests/test_diff_check.cpp
Normal file
@@ -0,0 +1,184 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "core/app/verify.hpp"
|
||||
#include "core/domain/analysis.hpp"
|
||||
#include "core/domain/connect.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
|
||||
// check_diff_crossings: a complete local diff pair (X_P / X_N) must cross a
|
||||
// connection leg for leg. Both sides are judged by name only.
|
||||
|
||||
namespace {
|
||||
|
||||
// New pin on `part`, wired to (or creating) module signal `sig_name`.
|
||||
Pin *wire(Module *m, Part *p, const std::string &pin_name,
|
||||
const std::string &sig_name)
|
||||
{
|
||||
Pin *pin = new Pin(pin_name);
|
||||
p->add(pin);
|
||||
Signal *s = m->signals->merge(sig_name);
|
||||
s->add(pin);
|
||||
pin->connect(s);
|
||||
return pin;
|
||||
}
|
||||
|
||||
struct Rig {
|
||||
System sys;
|
||||
Module *a, *b;
|
||||
Part *ja, *jb;
|
||||
Rig() {
|
||||
a = sys.modules()->merge("A");
|
||||
b = sys.modules()->merge("B");
|
||||
ja = new Part("J1"); a->add(ja);
|
||||
jb = new Part("P1"); b->add(jb);
|
||||
}
|
||||
void bridge(std::initializer_list<std::pair<Pin *, Pin *>> wires) {
|
||||
Connection *c = new Connection("A/J1 <-> B/P1", a, ja, b, jb);
|
||||
c->transform_name = "identity";
|
||||
for (const auto &w : wires) c->pin_map.push_back(w);
|
||||
sys.connections()->add(c);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("diff crossing: straight P↔P / N↔N is silent") {
|
||||
Rig r;
|
||||
Pin *ap = wire(r.a, r.ja, "1", "TX_P");
|
||||
Pin *an = wire(r.a, r.ja, "2", "TX_N");
|
||||
Pin *bp = wire(r.b, r.jb, "1", "RX_P");
|
||||
Pin *bn = wire(r.b, r.jb, "2", "RX_N");
|
||||
r.bridge({{ap, bp}, {an, bn}});
|
||||
|
||||
app::VerifyReport vr = app::verify(&r.sys);
|
||||
CHECK(vr.diff_anomalies.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("diff crossing: swapped legs report ONE polarity-swap anomaly") {
|
||||
Rig r;
|
||||
Pin *ap = wire(r.a, r.ja, "1", "TX_P");
|
||||
Pin *an = wire(r.a, r.ja, "2", "TX_N");
|
||||
Pin *bp = wire(r.b, r.jb, "1", "RX_P");
|
||||
Pin *bn = wire(r.b, r.jb, "2", "RX_N");
|
||||
r.bridge({{ap, bn}, {an, bp}}); // crossed on purpose
|
||||
|
||||
app::VerifyReport vr = app::verify(&r.sys);
|
||||
REQUIRE(vr.diff_anomalies.size() == 1); // deduped: not once per side
|
||||
CHECK(vr.diff_anomalies[0].kind == AnomalyKind::DiffPolaritySwap);
|
||||
CHECK(vr.diff_anomalies[0].message.find("polarity swapped")
|
||||
!= std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("diff crossing: a single bridged leg reports incomplete") {
|
||||
Rig r;
|
||||
Pin *ap = wire(r.a, r.ja, "1", "TX_P");
|
||||
wire(r.a, r.ja, "2", "TX_N"); // N leg stays local
|
||||
Pin *bp = wire(r.b, r.jb, "1", "RX_P");
|
||||
wire(r.b, r.jb, "2", "RX_N"); // peer N leg exists, unbridged
|
||||
r.bridge({{ap, bp}});
|
||||
|
||||
app::VerifyReport vr = app::verify(&r.sys);
|
||||
REQUIRE(vr.diff_anomalies.size() == 1);
|
||||
CHECK(vr.diff_anomalies[0].kind == AnomalyKind::DiffCrossIncomplete);
|
||||
CHECK(vr.diff_anomalies[0].message.find("only the P legs")
|
||||
!= std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("diff crossing: an unsuffixed peer is not judged") {
|
||||
Rig r;
|
||||
Pin *ap = wire(r.a, r.ja, "1", "TX_P");
|
||||
Pin *an = wire(r.a, r.ja, "2", "TX_N");
|
||||
Pin *b1 = wire(r.b, r.jb, "1", "RXP"); // no _P/_N suffix
|
||||
Pin *b2 = wire(r.b, r.jb, "2", "RXN");
|
||||
r.bridge({{ap, b2}, {an, b1}}); // even crossed: silent
|
||||
|
||||
app::VerifyReport vr = app::verify(&r.sys);
|
||||
CHECK(vr.diff_anomalies.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("diff crossing: P and N joined onto one net is flagged") {
|
||||
Rig r;
|
||||
Pin *ap = wire(r.a, r.ja, "1", "TX_P");
|
||||
Pin *an = wire(r.a, r.ja, "2", "TX_N");
|
||||
Pin *b1 = wire(r.b, r.jb, "1", "X");
|
||||
Pin *b2 = wire(r.b, r.jb, "2", "X"); // same peer signal
|
||||
r.bridge({{ap, b1}, {an, b2}});
|
||||
|
||||
app::VerifyReport vr = app::verify(&r.sys);
|
||||
REQUIRE(vr.diff_anomalies.size() == 1);
|
||||
CHECK(vr.diff_anomalies[0].kind == AnomalyKind::DiffPolaritySwap);
|
||||
CHECK(vr.diff_anomalies[0].message.find("join the same net")
|
||||
!= std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("diff bus crossing: a dangling lane is listed, once per side") {
|
||||
Rig r;
|
||||
// Two lanes on each side; only lane 0 is bridged — lane 1 crosses nowhere.
|
||||
Pin *a0p = wire(r.a, r.ja, "1", "TX0_P");
|
||||
Pin *a0n = wire(r.a, r.ja, "2", "TX0_N");
|
||||
wire(r.a, r.ja, "3", "TX1_P");
|
||||
wire(r.a, r.ja, "4", "TX1_N");
|
||||
Pin *b0p = wire(r.b, r.jb, "1", "RX0_P");
|
||||
Pin *b0n = wire(r.b, r.jb, "2", "RX0_N");
|
||||
wire(r.b, r.jb, "3", "RX1_P");
|
||||
wire(r.b, r.jb, "4", "RX1_N");
|
||||
r.bridge({{a0p, b0p}, {a0n, b0n}});
|
||||
|
||||
app::VerifyReport vr = app::verify(&r.sys);
|
||||
// One aggregated anomaly per side (each names its own lane signals).
|
||||
REQUIRE(vr.diff_anomalies.size() == 2);
|
||||
for (const auto &an : vr.diff_anomalies) {
|
||||
CHECK(an.kind == AnomalyKind::DiffCrossIncomplete);
|
||||
CHECK(an.message.find("lane(s) 1 do not cross") != std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("diff bus crossing: a distributed bus (lanes fanned out) is silent") {
|
||||
// A's two lanes go to two DIFFERENT modules — legitimate backplane fan-out.
|
||||
System sys;
|
||||
Module *a = sys.modules()->merge("A");
|
||||
Module *b = sys.modules()->merge("B");
|
||||
Module *c = sys.modules()->merge("C");
|
||||
Part *ja = new Part("J1"); a->add(ja);
|
||||
Part *jb = new Part("P1"); b->add(jb);
|
||||
Part *jc = new Part("P1"); c->add(jc);
|
||||
Pin *a0p = wire(a, ja, "1", "TX0_P");
|
||||
Pin *a0n = wire(a, ja, "2", "TX0_N");
|
||||
Pin *a1p = wire(a, ja, "3", "TX1_P");
|
||||
Pin *a1n = wire(a, ja, "4", "TX1_N");
|
||||
Pin *b0p = wire(b, jb, "1", "RX0_P");
|
||||
Pin *b0n = wire(b, jb, "2", "RX0_N");
|
||||
Pin *c0p = wire(c, jc, "1", "RX0_P");
|
||||
Pin *c0n = wire(c, jc, "2", "RX0_N");
|
||||
|
||||
Connection *cb = new Connection("A/J1 <-> B/P1", a, ja, b, jb);
|
||||
cb->transform_name = "identity";
|
||||
cb->pin_map.emplace_back(a0p, b0p);
|
||||
cb->pin_map.emplace_back(a0n, b0n);
|
||||
sys.connections()->add(cb);
|
||||
Connection *cc = new Connection("A/J1 <-> C/P1", a, ja, c, jc);
|
||||
cc->transform_name = "identity";
|
||||
cc->pin_map.emplace_back(a1p, c0p);
|
||||
cc->pin_map.emplace_back(a1n, c0n);
|
||||
sys.connections()->add(cc);
|
||||
|
||||
app::VerifyReport vr = app::verify(&sys);
|
||||
CHECK(vr.diff_anomalies.empty()); // every lane crosses somewhere
|
||||
}
|
||||
|
||||
TEST_CASE("diff crossing: empty / unconnected systems are silent") {
|
||||
System sys;
|
||||
app::VerifyReport vr = app::verify(&sys);
|
||||
CHECK(vr.diff_anomalies.empty());
|
||||
|
||||
// A pair that never crosses anything: silent (local pairs are fine).
|
||||
Rig r;
|
||||
wire(r.a, r.ja, "1", "TX_P");
|
||||
wire(r.a, r.ja, "2", "TX_N");
|
||||
vr = app::verify(&r.sys);
|
||||
CHECK(vr.diff_anomalies.empty());
|
||||
}
|
||||
113
tests/test_edit.cpp
Normal file
113
tests/test_edit.cpp
Normal file
@@ -0,0 +1,113 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "core/app/edit.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
#include "core/domain/signal_type.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
|
||||
// app::set_connector_type is pure core: validate the kind, tag the part and
|
||||
// apply the connector model. No Print/dialog/FTXUI.
|
||||
|
||||
TEST_CASE("set_connector_type tags a part with a free-form kind") {
|
||||
Part p("J1");
|
||||
p.add(new Pin("1"));
|
||||
p.add(new Pin("2"));
|
||||
|
||||
app::SetConnectorTypeResult r = app::set_connector_type(&p, "myconn");
|
||||
|
||||
CHECK(r.ok);
|
||||
CHECK(r.error.empty());
|
||||
CHECK(p.connector_type == "myconn");
|
||||
}
|
||||
|
||||
TEST_CASE("set_connector_type refuses a kind the part doesn't fit — no mutation") {
|
||||
Part p("J1");
|
||||
p.add(new Pin("1"));
|
||||
p.add(new Pin("2"));
|
||||
p.add(new Pin("3")); // numeric pins don't fit the VPX single-letter columns
|
||||
|
||||
app::SetConnectorTypeResult r = app::set_connector_type(&p, "vpx-3u-bkp-p0");
|
||||
|
||||
CHECK_FALSE(r.ok);
|
||||
CHECK_FALSE(r.error.empty());
|
||||
CHECK(p.connector_type.empty()); // refused before any change
|
||||
}
|
||||
|
||||
TEST_CASE("set_connector_type on a null part fails cleanly") {
|
||||
app::SetConnectorTypeResult r = app::set_connector_type(nullptr, "x");
|
||||
CHECK_FALSE(r.ok);
|
||||
CHECK_FALSE(r.error.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("attach_bsdl reports a parse failure without mutating the part") {
|
||||
Part p("J1");
|
||||
p.add(new Pin("1"));
|
||||
|
||||
app::AttachBsdlResult r = app::attach_bsdl(&p, "/nonexistent-xyz/none.bsd");
|
||||
|
||||
CHECK_FALSE(r.ok);
|
||||
CHECK(r.error.find("cannot parse") != std::string::npos);
|
||||
CHECK(p.bsdl_path.empty()); // failure leaves the part untouched
|
||||
}
|
||||
|
||||
TEST_CASE("attach_bsdl on a null part fails cleanly") {
|
||||
app::AttachBsdlResult r = app::attach_bsdl(nullptr, "x.bsd");
|
||||
CHECK_FALSE(r.ok);
|
||||
CHECK_FALSE(r.error.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("set_signal_type parses the name and sets the type") {
|
||||
Signal s("NET");
|
||||
app::SetSignalTypeResult r = app::set_signal_type(&s, "power");
|
||||
CHECK(r.ok);
|
||||
CHECK(r.type == SignalType::Power);
|
||||
CHECK(s.type == SignalType::Power);
|
||||
}
|
||||
|
||||
TEST_CASE("set_signal_type rejects an unknown name without mutating") {
|
||||
Signal s("NET");
|
||||
s.type = SignalType::Other;
|
||||
app::SetSignalTypeResult r = app::set_signal_type(&s, "bogus");
|
||||
CHECK_FALSE(r.ok);
|
||||
CHECK(r.error.find("power, gnd, other") != std::string::npos);
|
||||
CHECK(s.type == SignalType::Other); // unchanged
|
||||
}
|
||||
|
||||
TEST_CASE("duplicate_module deep-clones parts, pins and signals") {
|
||||
System sys;
|
||||
Module *a = sys.modules()->merge("A");
|
||||
Part *p = new Part("J1"); a->add(p);
|
||||
Pin *pin = new Pin("1"); p->add(pin);
|
||||
Signal *s = a->signals->merge("NET");
|
||||
s->type = SignalType::Power;
|
||||
s->add(pin); pin->connect(s);
|
||||
|
||||
app::DuplicateResult r = app::duplicate_module(&sys, "A", "B");
|
||||
|
||||
CHECK(r.ok);
|
||||
CHECK(r.parts == 1);
|
||||
CHECK(r.signals == 1);
|
||||
REQUIRE(sys.modules()->exists("B"));
|
||||
Module *b = sys.modules()->get("B");
|
||||
CHECK(b->signals->get("NET")->type == SignalType::Power); // type preserved
|
||||
Pin *bpin = b->get("J1")->get("1");
|
||||
REQUIRE(bpin->signal() != nullptr);
|
||||
CHECK(bpin->signal() == b->signals->get("NET")); // wired to the clone's own signal
|
||||
CHECK(bpin->signal() != s); // not aliasing the source
|
||||
}
|
||||
|
||||
TEST_CASE("duplicate_module refuses an unknown source or an existing destination") {
|
||||
System sys;
|
||||
sys.modules()->merge("A");
|
||||
|
||||
app::DuplicateResult dst = app::duplicate_module(&sys, "A", "A");
|
||||
CHECK_FALSE(dst.ok);
|
||||
CHECK(dst.error.find("already exists") != std::string::npos);
|
||||
|
||||
app::DuplicateResult unk = app::duplicate_module(&sys, "NOPE", "X");
|
||||
CHECK_FALSE(unk.ok);
|
||||
CHECK(unk.error.find("unknown module") != std::string::npos);
|
||||
}
|
||||
63
tests/test_load.cpp
Normal file
63
tests/test_load.cpp
Normal file
@@ -0,0 +1,63 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "core/app/load.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
// app::load_module is pure core: import a module, drop singleton signals, infer
|
||||
// signal types, return counts or an error — no Print/dialog/FTXUI. The parse
|
||||
// helper import_type_from_name is likewise UI-free.
|
||||
|
||||
TEST_CASE("import_type_from_name maps names case-insensitively") {
|
||||
ImportType t;
|
||||
CHECK(app::import_type_from_name("mentor", t));
|
||||
CHECK(t == ImportType::IMPORT_MENTOR);
|
||||
CHECK(app::import_type_from_name("ALTIUM", t));
|
||||
CHECK(t == ImportType::IMPORT_ALTIUM);
|
||||
CHECK(app::import_type_from_name("Ods", t));
|
||||
CHECK(t == ImportType::IMPORT_ODS);
|
||||
CHECK_FALSE(app::import_type_from_name("kicad", t));
|
||||
CHECK_FALSE(app::import_type_from_name("", t));
|
||||
}
|
||||
|
||||
TEST_CASE("load_module imports, drops singletons and reports counts") {
|
||||
// Minimal Mentor netlist: two parts; NETA/NETB span both parts (2 pins
|
||||
// each, kept), LONELY sits on one pin only (dropped as a singleton).
|
||||
const char *path = "test_load_in.net";
|
||||
{
|
||||
std::ofstream f(path);
|
||||
f << "COMP: 'C1' 'J1'\n"
|
||||
" Explicit Pin: '1' 'x' 'NETA'\n"
|
||||
" Explicit Pin: '2' 'x' 'NETB'\n"
|
||||
" Explicit Pin: '3' 'x' 'LONELY'\n"
|
||||
"COMP: 'C2' 'J2'\n"
|
||||
" Explicit Pin: '1' 'x' 'NETA'\n"
|
||||
" Explicit Pin: '2' 'x' 'NETB'\n";
|
||||
}
|
||||
|
||||
System sys;
|
||||
app::LoadResult r = app::load_module(&sys, "M", path, ImportType::IMPORT_MENTOR);
|
||||
|
||||
CHECK(r.ok);
|
||||
CHECK(r.error.empty());
|
||||
CHECK(r.parts == 2);
|
||||
CHECK(r.signals == 2); // NETA, NETB — LONELY dropped
|
||||
CHECK(r.dropped == 1); // LONELY
|
||||
|
||||
std::remove(path);
|
||||
}
|
||||
|
||||
TEST_CASE("load_module fails cleanly on a missing file") {
|
||||
// ImportBase opens read-only and System::Load checks is_open(), so a missing
|
||||
// file is a clean error — and no empty module is left in the system.
|
||||
System sys;
|
||||
app::LoadResult r = app::load_module(
|
||||
&sys, "M", "/nonexistent-dir-xyz/nope.net", ImportType::IMPORT_MENTOR);
|
||||
CHECK_FALSE(r.ok);
|
||||
CHECK(r.error.find("cannot open") != std::string::npos);
|
||||
CHECK(sys.modules()->size() == 0);
|
||||
}
|
||||
82
tests/test_script.cpp
Normal file
82
tests/test_script.cpp
Normal file
@@ -0,0 +1,82 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "core/app/script.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
// app::run_script is pure core: it drives the app::* operations from a command
|
||||
// script and writes their output to a stream. No UI.
|
||||
|
||||
TEST_CASE("run_script builds a system from a command script") {
|
||||
const char *net = "test_script_in.net";
|
||||
{
|
||||
std::ofstream f(net);
|
||||
f << "COMP: 'C1' 'J1'\n"
|
||||
" Explicit Pin: '1' 'x' 'NETA'\n"
|
||||
" Explicit Pin: '2' 'x' 'NETB'\n"
|
||||
"COMP: 'C2' 'J2'\n"
|
||||
" Explicit Pin: '1' 'x' 'NETA'\n"
|
||||
" Explicit Pin: '2' 'x' 'NETB'\n";
|
||||
}
|
||||
const char *scr = "test_script_in.essim";
|
||||
{
|
||||
std::ofstream f(scr);
|
||||
f << "# a comment\n"
|
||||
"\n"
|
||||
"set NET " << net << "\n"
|
||||
"new\n"
|
||||
"load M $NET mentor\n"
|
||||
"set-signal-type M NETA power\n"
|
||||
"verify\n";
|
||||
}
|
||||
|
||||
std::unique_ptr<System> sys;
|
||||
std::ostringstream out;
|
||||
app::ScriptResult r = app::run_script(sys, scr, out);
|
||||
|
||||
CHECK(r.ok);
|
||||
CHECK(r.errors == 0);
|
||||
REQUIRE(sys != nullptr);
|
||||
CHECK(sys->modules()->size() == 1);
|
||||
|
||||
const std::string log = out.str();
|
||||
CHECK(log.find("loaded 'M'") != std::string::npos); // load ran
|
||||
CHECK(log.find("signal type = power") != std::string::npos); // $NET expanded + set
|
||||
CHECK(log.find("verify:") != std::string::npos); // verify ran
|
||||
|
||||
std::remove(net);
|
||||
std::remove(scr);
|
||||
}
|
||||
|
||||
TEST_CASE("run_script reports a missing top-level file") {
|
||||
std::unique_ptr<System> sys;
|
||||
std::ostringstream out;
|
||||
app::ScriptResult r =
|
||||
app::run_script(sys, "/nonexistent-xyz/none.essim", out);
|
||||
CHECK_FALSE(r.ok);
|
||||
CHECK(r.error.find("cannot open") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("run_script flags an unsupported command and keeps going") {
|
||||
const char *scr = "test_script_unsup.essim";
|
||||
{
|
||||
std::ofstream f(scr);
|
||||
f << "new\nfrobnicate widgets\nnew\n";
|
||||
}
|
||||
std::unique_ptr<System> sys;
|
||||
std::ostringstream out;
|
||||
app::ScriptResult r = app::run_script(sys, scr, out);
|
||||
|
||||
CHECK(r.ok);
|
||||
CHECK(r.errors == 1);
|
||||
CHECK(out.str().find("unsupported command 'frobnicate'") != std::string::npos);
|
||||
REQUIRE(sys != nullptr); // the two `new` lines still ran
|
||||
|
||||
std::remove(scr);
|
||||
}
|
||||
@@ -51,7 +51,52 @@ TEST_CASE("infer_signal_type: power family") {
|
||||
CHECK(infer_signal_type("VS3_5V0") == SignalType::Power);
|
||||
CHECK(infer_signal_type("+5V") == SignalType::Power);
|
||||
CHECK(infer_signal_type("-12V") == SignalType::Power);
|
||||
CHECK(infer_signal_type("VBAT_SENSE") == SignalType::Power);
|
||||
// Rail token + control token → power-management, mapped to Other by the
|
||||
// wrapper (it is a sense line ABOUT VBAT, not the rail).
|
||||
CHECK(infer_signal_type("VBAT_SENSE") == SignalType::Other);
|
||||
}
|
||||
|
||||
TEST_CASE("classify_signal_name: rail vs power-management signals") {
|
||||
CHECK(classify_signal_name("VCC").verdict == NameVerdict::Rail);
|
||||
CHECK(classify_signal_name("VDD_3V3").verdict == NameVerdict::Rail);
|
||||
CHECK(classify_signal_name("+5V").verdict == NameVerdict::Rail);
|
||||
|
||||
NameClassification c = classify_signal_name("VDD_CORE_SENSE");
|
||||
CHECK(c.verdict == NameVerdict::PowerMgmt);
|
||||
CHECK(c.token == "SENSE");
|
||||
|
||||
CHECK(classify_signal_name("VBAT_SENSE").verdict == NameVerdict::PowerMgmt);
|
||||
CHECK(classify_signal_name("VCC_EN").verdict == NameVerdict::PowerMgmt);
|
||||
CHECK(classify_signal_name("VCC_EN1").verdict == NameVerdict::PowerMgmt); // trailing digit
|
||||
CHECK(classify_signal_name("VDD_FB").verdict == NameVerdict::PowerMgmt);
|
||||
CHECK(classify_signal_name("PWR_GOOD").verdict == NameVerdict::PowerMgmt);
|
||||
CHECK(classify_signal_name("PWR_OK").verdict == NameVerdict::PowerMgmt);
|
||||
CHECK(classify_signal_name("VBUS_DET").verdict == NameVerdict::PowerMgmt);
|
||||
CHECK(classify_signal_name("POWER_FAIL").verdict == NameVerdict::PowerMgmt);
|
||||
CHECK(classify_signal_name("VDD_VSENSE").verdict == NameVerdict::PowerMgmt); // fused suffix
|
||||
CHECK(classify_signal_name("PWR_NFAULT").verdict == NameVerdict::PowerMgmt); // active-low
|
||||
CHECK(classify_signal_name("VDD_ADJ").verdict == NameVerdict::PowerMgmt); // regulator adjust
|
||||
CHECK(classify_signal_name("VCC_TRIM").verdict == NameVerdict::PowerMgmt);
|
||||
CHECK(classify_signal_name("VDD_VTRIM").verdict == NameVerdict::PowerMgmt); // fused suffix
|
||||
CHECK(classify_signal_name("VCC_VSET").verdict == NameVerdict::PowerMgmt);
|
||||
CHECK(classify_signal_name("VCC_FBK").verdict == NameVerdict::PowerMgmt); // feedback variants
|
||||
CHECK(classify_signal_name("VDD_FDB").verdict == NameVerdict::PowerMgmt);
|
||||
CHECK(classify_signal_name("PWR_CMD").verdict == NameVerdict::PowerMgmt); // command
|
||||
CHECK(classify_signal_name("PWR_LED").verdict == NameVerdict::PowerMgmt); // indicator
|
||||
CHECK(classify_signal_name("VDD_REF").verdict == NameVerdict::PowerMgmt); // reference
|
||||
CHECK(classify_signal_name("VCC_VREF").verdict == NameVerdict::PowerMgmt);
|
||||
|
||||
// Whole-token matching: SENSOR is not SENSE, GREEN is not EN —
|
||||
// these stay genuine rails.
|
||||
CHECK(classify_signal_name("VDD_SENSOR").verdict == NameVerdict::Rail);
|
||||
CHECK(classify_signal_name("VCC_GREEN").verdict == NameVerdict::Rail);
|
||||
|
||||
// No rail token at all → Other, even with a control word.
|
||||
CHECK(classify_signal_name("SPI_CS").verdict == NameVerdict::Other);
|
||||
CHECK(classify_signal_name("FAN_SENSE").verdict == NameVerdict::Other);
|
||||
|
||||
// GND family is deliberately left out of the control-token logic.
|
||||
CHECK(classify_signal_name("GND_RET").verdict == NameVerdict::GndShield);
|
||||
}
|
||||
|
||||
TEST_CASE("infer_signal_type: other (data signals)") {
|
||||
|
||||
95
tests/test_verify.cpp
Normal file
95
tests/test_verify.cpp
Normal file
@@ -0,0 +1,95 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "core/app/verify.hpp"
|
||||
#include "core/domain/connect.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
|
||||
// app::verify is pure core: it takes a System* and returns a VerifyReport of
|
||||
// structured findings, with no Print/dialog/FTXUI. These tests build small
|
||||
// systems by hand and assert the report — no UI involved.
|
||||
|
||||
TEST_CASE("verify on a null or empty system reports nothing") {
|
||||
app::VerifyReport none = app::verify(nullptr);
|
||||
CHECK(none.typed_pins == 0);
|
||||
CHECK(none.total_nets == 0);
|
||||
CHECK(none.role_mismatches.empty());
|
||||
|
||||
System sys;
|
||||
app::VerifyReport r = app::verify(&sys);
|
||||
CHECK(r.typed_pins == 0);
|
||||
CHECK(r.total_nets == 0);
|
||||
CHECK(r.bridged_nets == 0);
|
||||
CHECK(r.net_inconsistencies.empty());
|
||||
CHECK(r.orphan_total() == 0);
|
||||
CHECK(r.model_total() == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("verify flags a bridged net that mixes Power and GndShield") {
|
||||
// Two cards, one wired pin pair: A.NETA (Power) <-> B.NETB (GndShield).
|
||||
System sys;
|
||||
Module *a = sys.modules()->merge("A");
|
||||
Module *b = sys.modules()->merge("B");
|
||||
Part *ja = new Part("J1"); a->add(ja);
|
||||
Part *jb = new Part("P1"); b->add(jb);
|
||||
Pin *pa = new Pin("1"); ja->add(pa);
|
||||
Pin *pb = new Pin("1"); jb->add(pb);
|
||||
Signal *sa = a->signals->merge("NETA"); sa->type = SignalType::Power;
|
||||
Signal *sb = b->signals->merge("NETB"); sb->type = SignalType::GndShield;
|
||||
sa->add(pa); pa->connect(sa);
|
||||
sb->add(pb); pb->connect(sb);
|
||||
|
||||
Connection *c = new Connection("A.J1<->B.P1", a, ja, b, jb);
|
||||
c->transform_name = "identity";
|
||||
c->pin_map.emplace_back(pa, pb);
|
||||
sys.connections()->add(c);
|
||||
|
||||
app::VerifyReport r = app::verify(&sys);
|
||||
|
||||
CHECK(r.total_nets == 1);
|
||||
CHECK(r.bridged_nets == 1);
|
||||
REQUIRE(r.net_inconsistencies.size() == 1);
|
||||
CHECK(r.net_inconsistencies[0].members.size() == 2);
|
||||
// Both endpoints are present with their declared types.
|
||||
bool seen_power = false, seen_gnd = false;
|
||||
for (const auto &m : r.net_inconsistencies[0].members) {
|
||||
if (m.type == SignalType::Power) seen_power = true;
|
||||
if (m.type == SignalType::GndShield) seen_gnd = true;
|
||||
}
|
||||
CHECK(seen_power);
|
||||
CHECK(seen_gnd);
|
||||
}
|
||||
|
||||
TEST_CASE("verify counts orphan pins by their import origin") {
|
||||
System sys;
|
||||
Module *m = sys.modules()->merge("M");
|
||||
Part *p = new Part("J1"); m->add(p);
|
||||
Pin *imp = new Pin("1"); imp->nc_origin = NcOrigin::ImportedUnconnected; p->add(imp);
|
||||
Pin *drp = new Pin("2"); drp->nc_origin = NcOrigin::DroppedSingleton; p->add(drp);
|
||||
Pin *wired = new Pin("3"); p->add(wired);
|
||||
Signal *s = m->signals->merge("NET"); s->add(wired); wired->connect(s);
|
||||
|
||||
app::VerifyReport r = app::verify(&sys);
|
||||
|
||||
CHECK(r.orphan_imported == 1);
|
||||
CHECK(r.orphan_dropped == 1);
|
||||
CHECK(r.orphan_total() == 2);
|
||||
|
||||
// Per-pin detail carries the path and origin (the dashboard lists the
|
||||
// dropped ones under the NC health row).
|
||||
REQUIRE(r.orphans.size() == 2);
|
||||
int n_dropped = 0;
|
||||
bool dropped_path_ok = false;
|
||||
for (const auto &o : r.orphans) {
|
||||
if (o.dropped) {
|
||||
++n_dropped;
|
||||
if (o.module == "M" && o.part == "J1" && o.pin == "2")
|
||||
dropped_path_ok = true;
|
||||
}
|
||||
}
|
||||
CHECK(n_dropped == 1);
|
||||
CHECK(dropped_path_ok);
|
||||
}
|
||||
71
tests/tui/test_source.cpp
Normal file
71
tests/tui/test_source.cpp
Normal file
@@ -0,0 +1,71 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "frontends/tui/tui.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
// Tui::Source nesting — regression for the bug where a nested `source`
|
||||
// overwrote the single loading state, so the CALLING script's remaining
|
||||
// lines never ran. Headless path (no screen): BootDispatch drains
|
||||
// synchronously, exactly like `essim --batch --source`.
|
||||
|
||||
namespace {
|
||||
|
||||
std::string run_boot(const std::string &cmd) {
|
||||
Tui t;
|
||||
t.BootDispatch(cmd);
|
||||
std::ostringstream oss;
|
||||
t.DumpOutput(oss);
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("source: lines after a nested source still run") {
|
||||
const char *inner = "test_src_inner.essim";
|
||||
const char *outer = "test_src_outer.essim";
|
||||
{
|
||||
std::ofstream f(inner);
|
||||
f << "new\n";
|
||||
}
|
||||
{
|
||||
std::ofstream f(outer);
|
||||
f << "source " << inner << "\n"
|
||||
"verify\n";
|
||||
}
|
||||
|
||||
std::string out = run_boot(std::string("source ") + outer);
|
||||
|
||||
// The inner script ran and was summarised…
|
||||
CHECK(out.find("system created.") != std::string::npos);
|
||||
CHECK(out.find(std::string("source: ") + inner) != std::string::npos);
|
||||
// …and the OUTER script kept going after it: verify executed…
|
||||
CHECK(out.find("verify: 0 local mismatch(es)") != std::string::npos);
|
||||
// …and the outer summary counts its 2 effective lines.
|
||||
CHECK(out.find(std::string("source: ") + outer + " (2 line(s))")
|
||||
!= std::string::npos);
|
||||
|
||||
std::remove(inner);
|
||||
std::remove(outer);
|
||||
}
|
||||
|
||||
TEST_CASE("source: self-recursion stops at the depth guard") {
|
||||
const char *loop = "test_src_loop.essim";
|
||||
{
|
||||
std::ofstream f(loop);
|
||||
f << "source " << loop << "\n";
|
||||
}
|
||||
|
||||
std::string out = run_boot(std::string("source ") + loop);
|
||||
|
||||
CHECK(out.find("source: nesting too deep, skipping")
|
||||
!= std::string::npos);
|
||||
// Every frame still closes with its own summary.
|
||||
CHECK(out.find(std::string("source: ") + loop + " (1 line(s))")
|
||||
!= std::string::npos);
|
||||
|
||||
std::remove(loop);
|
||||
}
|
||||
Reference in New Issue
Block a user