Merge separate-core-ui: split business logic from the T/GUI layer.
Reorganises essim around a hard core/frontends boundary so multiple GUI/TUI
engines can be built on the same business logic:
- src/core/{domain,imports,app} — frontend-agnostic essim_core (no GUI
toolkit); src/frontends/<name>/ — per-frontend UI. CMake selects one with
-DESSIM_FRONTEND=<name> (default tui; "none" = core+tests, no FTXUI).
- Application layer (core/app/): export / verify / connect / load extracted
as System-in, result-struct-out operations; TUI commands and screens are
thin wrappers. The analyze screen and dashboard now share app::verify
instead of recomputing the passes (de-dup).
- Frontend interface (frontends/frontend.hpp) + shared frontend_main: main.cpp
is frontend-agnostic; a new frontend is a 4-line main plus a Frontend impl.
- ImportBase hardened (read-only open, fail-fast on a missing file) and made
leak-free (valgrind: no leaks, 0 errors).
Tests: 78 core cases / 368 assertions + the TUI suite, all green. Binary stays
./build/essim; --batch/--commands-md/--help behaviour unchanged.
This commit is contained in:
101
CMakeLists.txt
101
CMakeLists.txt
@@ -11,49 +11,60 @@ project(essim
|
||||
|
||||
include(FetchContent)
|
||||
|
||||
set(FTXUI_BUILD_DOCS OFF CACHE INTERNAL "")
|
||||
set(FTXUI_BUILD_EXAMPLES OFF CACHE INTERNAL "")
|
||||
set(FTXUI_BUILD_TESTS OFF CACHE INTERNAL "")
|
||||
set(FTXUI_ENABLE_INSTALL OFF CACHE INTERNAL "")
|
||||
|
||||
FetchContent_Declare(ftxui
|
||||
GIT_REPOSITORY https://github.com/ArthurSonzogni/FTXUI.git
|
||||
GIT_TAG v6.1.9
|
||||
GIT_SHALLOW TRUE
|
||||
)
|
||||
FetchContent_MakeAvailable(ftxui)
|
||||
|
||||
find_package(libzip REQUIRED)
|
||||
find_package(pugixml REQUIRED)
|
||||
|
||||
# libbsdl — standalone BSDL parser (LGPL-2.1), dynamically linked from essim
|
||||
# (EUPL-1.2, which the LGPL permits). Path overridable via -DBSDL_DIR=...;
|
||||
# its CLI and tests are not needed inside essim's build.
|
||||
# ----------------------------------------------------------------- core deps
|
||||
# libbsdl — standalone BSDL parser (LGPL-2.1), dynamically linked (EUPL-1.2,
|
||||
# which the LGPL permits). Override its path with -DBSDL_DIR=...
|
||||
set(BSDL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../libbsdl" CACHE PATH "libbsdl source tree")
|
||||
set(BSDL_BUILD_CLI OFF CACHE BOOL "" FORCE)
|
||||
set(BSDL_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
add_subdirectory(${BSDL_DIR} ${CMAKE_BINARY_DIR}/libbsdl)
|
||||
|
||||
# Library target = everything except main.cpp; reused by `essim` and `essim_tests`.
|
||||
file(GLOB_RECURSE LIB_SOURCES "src/*.cpp")
|
||||
list(REMOVE_ITEM LIB_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp")
|
||||
find_package(libzip REQUIRED)
|
||||
find_package(pugixml REQUIRED)
|
||||
|
||||
add_library(essim_lib STATIC ${LIB_SOURCES})
|
||||
target_include_directories(essim_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src)
|
||||
target_link_libraries(essim_lib
|
||||
# =============================================================== essim_core
|
||||
# All business logic — domain model, importers, application operations
|
||||
# (src/core/{domain,imports,app}). Frontend-agnostic: it links NO GUI/TUI
|
||||
# toolkit, so every frontend and the test suite share the exact same core.
|
||||
file(GLOB_RECURSE CORE_SOURCES "src/core/*.cpp")
|
||||
add_library(essim_core STATIC ${CORE_SOURCES})
|
||||
target_include_directories(essim_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src)
|
||||
target_link_libraries(essim_core
|
||||
PUBLIC
|
||||
ftxui::screen
|
||||
ftxui::dom
|
||||
ftxui::component
|
||||
libzip::zip
|
||||
pugixml::pugixml
|
||||
bsdl::bsdl
|
||||
)
|
||||
|
||||
add_executable(essim src/main.cpp)
|
||||
target_link_libraries(essim PRIVATE essim_lib)
|
||||
# =============================================================== frontend(s)
|
||||
# Pick the GUI/TUI frontend to build the `essim` binary against. Each frontend
|
||||
# is a self-contained src/frontends/<name>/ (own CMakeLists, GUI toolkit, and
|
||||
# main.cpp) that links essim_core. "none" builds the core + tests only — no GUI
|
||||
# toolkit is fetched. To add a frontend (e.g. a Qt GUI), create
|
||||
# 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)
|
||||
|
||||
# Tests
|
||||
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
|
||||
"Unknown ESSIM_FRONTEND '${ESSIM_FRONTEND}' — expected "
|
||||
"src/frontends/${ESSIM_FRONTEND}/CMakeLists.txt, or 'none'.")
|
||||
endif()
|
||||
|
||||
# =============================================================== tests (core)
|
||||
# The suite exercises essim_core only — no frontend, no GUI toolkit.
|
||||
include(CTest)
|
||||
if(BUILD_TESTING)
|
||||
set(CMAKE_POLICY_VERSION_MINIMUM 3.5)
|
||||
@@ -65,20 +76,35 @@ if(BUILD_TESTING)
|
||||
FetchContent_MakeAvailable(doctest)
|
||||
unset(CMAKE_POLICY_VERSION_MINIMUM)
|
||||
|
||||
# Core tests — exercise essim_core only (tests/*.cpp, non-recursive, so the
|
||||
# per-frontend tests under tests/<frontend>/ are not pulled in here).
|
||||
file(GLOB TEST_SOURCES "tests/*.cpp")
|
||||
if(TEST_SOURCES)
|
||||
add_executable(essim_tests ${TEST_SOURCES})
|
||||
target_link_libraries(essim_tests PRIVATE essim_lib doctest::doctest)
|
||||
target_link_libraries(essim_tests PRIVATE essim_core doctest::doctest)
|
||||
add_test(NAME essim_tests COMMAND essim_tests)
|
||||
endif()
|
||||
|
||||
# Per-frontend tests — tests/<frontend>/*.cpp, built and linked against that
|
||||
# frontend's library only when the frontend itself is built.
|
||||
if(TARGET essim_tui)
|
||||
file(GLOB TUI_TEST_SOURCES "tests/tui/*.cpp")
|
||||
if(TUI_TEST_SOURCES)
|
||||
add_executable(essim_tui_tests
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/doctest_main.cpp" ${TUI_TEST_SOURCES})
|
||||
target_link_libraries(essim_tui_tests PRIVATE essim_tui doctest::doctest)
|
||||
add_test(NAME essim_tui_tests COMMAND essim_tui_tests)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Documentation: Doxygen → XML → custom Python script → doc/api/ (Markdown rendered by gitea).
|
||||
# Optional — `doc` target is only created if Doxygen and Python 3 are present.
|
||||
# =============================================================== documentation
|
||||
# Doxygen → XML → gen_api_md.py → doc/api/, plus `essim --commands-md`. Needs the
|
||||
# `essim` binary, so it's only wired when a frontend that provides one is built.
|
||||
find_package(Doxygen COMPONENTS doxygen)
|
||||
find_package(Python3 COMPONENTS Interpreter)
|
||||
|
||||
if(DOXYGEN_FOUND AND Python3_Interpreter_FOUND)
|
||||
if(TARGET essim AND DOXYGEN_FOUND AND Python3_Interpreter_FOUND)
|
||||
set(DOXYGEN_OUTPUT_DIR "${CMAKE_BINARY_DIR}/doc")
|
||||
file(MAKE_DIRECTORY "${DOXYGEN_OUTPUT_DIR}")
|
||||
configure_file(
|
||||
@@ -103,11 +129,10 @@ if(DOXYGEN_FOUND AND Python3_Interpreter_FOUND)
|
||||
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
|
||||
COMMENT "Generating documentation (doxygen → gen_api_md.py → doc/api/, essim --commands-md → doc/user/commands.md)"
|
||||
VERBATIM)
|
||||
elseif(NOT DOXYGEN_FOUND AND NOT Python3_Interpreter_FOUND)
|
||||
message(STATUS "doc: Doxygen and Python 3 not found — `doc` target disabled.")
|
||||
elseif(NOT TARGET essim)
|
||||
message(STATUS "doc: no `essim` binary (ESSIM_FRONTEND=none) — `doc` target disabled.")
|
||||
elseif(NOT DOXYGEN_FOUND)
|
||||
message(STATUS "doc: Doxygen not found — `doc` target disabled "
|
||||
"(install via `pacman -S doxygen`).")
|
||||
message(STATUS "doc: Doxygen not found — `doc` target disabled.")
|
||||
else()
|
||||
message(STATUS "doc: Python 3 interpreter not found — `doc` target disabled.")
|
||||
endif()
|
||||
|
||||
183
DESIGN.md
183
DESIGN.md
@@ -10,72 +10,111 @@ 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)
|
||||
modules.{hpp,cpp} Module, Modules
|
||||
parts.{hpp,cpp} Part (carries `kind` + `connector_type`), Parts
|
||||
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
|
||||
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)
|
||||
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>
|
||||
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
|
||||
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 (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), 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, 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 / _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; `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
|
||||
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`*. Add one by creating
|
||||
`src/frontends/<name>/CMakeLists.txt` (build `essim_<name>` linking `essim_core`,
|
||||
produce the `essim` binary linking `essim_frontend`) 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 +149,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 +162,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 +184,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 +227,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 +287,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 +303,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 +342,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 +354,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.
|
||||
|
||||
|
||||
35
README.md
35
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,14 @@ 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.
|
||||
- Optional, only for the `doc` target: **doxygen** and **python3**.
|
||||
|
||||
libzip, pugixml and libbsdl are the **core** dependencies; FTXUI belongs to the
|
||||
tui frontend, so a `-DESSIM_FRONTEND=none` build needs none of it.
|
||||
|
||||
## Tests
|
||||
|
||||
```sh
|
||||
@@ -53,6 +64,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,12 +90,17 @@ 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
|
||||
test/ sample netlists + system.essim bring-up script
|
||||
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
|
||||
```
|
||||
|
||||
Full layout & rationale in [`DESIGN.md`](DESIGN.md).
|
||||
|
||||
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_
|
||||
190
src/core/app/export.cpp
Normal file
190
src/core/app/export.cpp
Normal file
@@ -0,0 +1,190 @@
|
||||
#include "core/app/export.hpp"
|
||||
|
||||
#include "core/imports/ods_writer.hpp"
|
||||
#include "core/domain/connect.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"
|
||||
|
||||
#include <cctype>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
namespace app {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string to_lower(std::string s)
|
||||
{
|
||||
for (char &c : s) c = (char)std::tolower((unsigned char)c);
|
||||
return s;
|
||||
}
|
||||
|
||||
// Minimal CSV quoter — wraps in "…" and doubles internal quotes when the field
|
||||
// contains a comma, quote, or newline.
|
||||
std::string csv_quote(const std::string &s)
|
||||
{
|
||||
if (s.find_first_of(",\"\n") == std::string::npos)
|
||||
return s;
|
||||
std::string out = "\"";
|
||||
for (char c : s) { if (c == '"') out += '"'; out += c; }
|
||||
out += '"';
|
||||
return out;
|
||||
}
|
||||
|
||||
// Flatten one pin into the string slots an export row uses.
|
||||
void pin_side(Pin *p, std::string &mod, std::string &part, std::string &pin,
|
||||
std::string &sig, std::string &type, std::string &suspect)
|
||||
{
|
||||
if (!p) { mod = part = pin = sig = type = suspect = ""; return; }
|
||||
mod = (p->prnt && p->prnt->prnt) ? p->prnt->prnt->name : "";
|
||||
part = p->prnt ? p->prnt->name : "";
|
||||
pin = p->name;
|
||||
Signal *s = p->signal();
|
||||
if (!s) {
|
||||
sig = ""; type = "(NC)"; suspect = "";
|
||||
} else {
|
||||
sig = s->name;
|
||||
type = signal_type_name(s->type);
|
||||
suspect = (infer_signal_type(s->name) == SignalType::Power
|
||||
&& s->type == SignalType::Other) ? "yes" : "no";
|
||||
}
|
||||
}
|
||||
|
||||
// Excel rejects /\?*:[] in sheet names; ODS forbids < > & in raw table names.
|
||||
// Sanitise to underscores and clip to Excel's 31-char hard limit.
|
||||
std::string sanitise_sheet_name(std::string name)
|
||||
{
|
||||
for (char &ch : name)
|
||||
if (ch == '/' || ch == '\\' || ch == '?' || ch == '*' || ch == ':'
|
||||
|| ch == '[' || ch == ']' || ch == '<' || ch == '>' || ch == '&')
|
||||
ch = '_';
|
||||
if (name.size() > 31) name = name.substr(0, 31);
|
||||
return name;
|
||||
}
|
||||
|
||||
ExportResult write_ods(const System *sys, const std::string &path)
|
||||
{
|
||||
ExportResult r;
|
||||
OdsWriter w;
|
||||
for (auto &ckv : *sys->connections()) {
|
||||
Connection *c = ckv.second;
|
||||
OdsSheet *s = w.add_sheet(sanitise_sheet_name(c->name));
|
||||
|
||||
std::string lmod = c->m1 ? c->m1->name : "";
|
||||
std::string lprt = c->p1 ? c->p1->name : "";
|
||||
std::string rmod = c->m2 ? c->m2->name : "";
|
||||
std::string rprt = c->p2 ? c->p2->name : "";
|
||||
|
||||
// Meta header: label/value rows, a blank row, then the column headers.
|
||||
auto meta = [&](int row, const std::string &k, const std::string &v) {
|
||||
s->set(row, 0, k);
|
||||
s->set(row, 1, v);
|
||||
};
|
||||
meta(0, "Connection", c->name);
|
||||
meta(1, "Transform", c->transform_name);
|
||||
meta(2, "Left", lmod + " / " + lprt);
|
||||
meta(3, "Right", rmod + " / " + rprt);
|
||||
// Row 4 left blank by design.
|
||||
|
||||
const int HDR = 5;
|
||||
s->set_header_row(HDR);
|
||||
const char *hdr[] = {
|
||||
"left_pin", "left_signal", "left_type", "left_suspect",
|
||||
"right_pin", "right_signal", "right_type", "right_suspect",
|
||||
"type_mismatch"};
|
||||
for (int i = 0; i < 9; ++i) s->set(HDR, i, hdr[i]);
|
||||
|
||||
int row = HDR + 1;
|
||||
for (auto &wp : c->pin_map) {
|
||||
std::string lm, lp, ln, ls, lt, lsus;
|
||||
std::string rm, rp, rn, rs, rt, rsus;
|
||||
pin_side(wp.first, lm, lp, ln, ls, lt, lsus);
|
||||
pin_side(wp.second, rm, rp, rn, rs, rt, rsus);
|
||||
std::string tm = (lt != "(NC)" && rt != "(NC)" && lt != rt) ? "yes" : "no";
|
||||
s->set(row, 0, ln); s->set(row, 1, ls);
|
||||
s->set(row, 2, lt); s->set(row, 3, lsus);
|
||||
s->set(row, 4, rn); s->set(row, 5, rs);
|
||||
s->set(row, 6, rt); s->set(row, 7, rsus);
|
||||
s->set(row, 8, tm);
|
||||
++row; ++r.rows;
|
||||
}
|
||||
}
|
||||
|
||||
std::string err;
|
||||
if (!w.save(path, err)) {
|
||||
r.error = err;
|
||||
return r;
|
||||
}
|
||||
r.ok = true;
|
||||
r.sheets = (int)sys->connections()->size();
|
||||
return r;
|
||||
}
|
||||
|
||||
ExportResult write_csv(const System *sys, const std::string &path)
|
||||
{
|
||||
ExportResult r;
|
||||
std::ofstream f(path);
|
||||
if (!f) {
|
||||
r.error = "cannot open '" + path + "' for writing";
|
||||
return r;
|
||||
}
|
||||
|
||||
// One rectangular table: each row repeats the per-connection constants so
|
||||
// the file stays parser-friendly (pandas / awk / spreadsheet).
|
||||
f << "connection,transform,"
|
||||
"left_module,left_part,"
|
||||
"left_pin,left_signal,left_type,left_suspect,"
|
||||
"right_module,right_part,"
|
||||
"right_pin,right_signal,right_type,right_suspect,"
|
||||
"type_mismatch\n";
|
||||
|
||||
for (auto &ckv : *sys->connections()) {
|
||||
Connection *c = ckv.second;
|
||||
for (auto &wp : c->pin_map) {
|
||||
std::string lm, lp, ln, ls, lt, lsus;
|
||||
std::string rm, rp, rn, rs, rt, rsus;
|
||||
pin_side(wp.first, lm, lp, ln, ls, lt, lsus);
|
||||
pin_side(wp.second, rm, rp, rn, rs, rt, rsus);
|
||||
std::string tm = (lt != "(NC)" && rt != "(NC)" && lt != rt) ? "yes" : "no";
|
||||
f << csv_quote(c->name) << ','
|
||||
<< csv_quote(c->transform_name) << ','
|
||||
<< csv_quote(lm) << ',' << csv_quote(lp) << ','
|
||||
<< csv_quote(ln) << ',' << csv_quote(ls) << ','
|
||||
<< csv_quote(lt) << ',' << csv_quote(lsus) << ','
|
||||
<< csv_quote(rm) << ',' << csv_quote(rp) << ','
|
||||
<< csv_quote(rn) << ',' << csv_quote(rs) << ','
|
||||
<< csv_quote(rt) << ',' << csv_quote(rsus) << ','
|
||||
<< tm << '\n';
|
||||
++r.rows;
|
||||
}
|
||||
}
|
||||
r.ok = f.good();
|
||||
if (!r.ok) r.error = "write error on '" + path + "'";
|
||||
return r;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool export_format_from_path(const std::string &path, ExportFormat &out)
|
||||
{
|
||||
size_t dot = path.rfind('.');
|
||||
std::string ext = (dot == std::string::npos) ? "" : to_lower(path.substr(dot));
|
||||
if (ext == ".csv") { out = ExportFormat::Csv; return true; }
|
||||
if (ext == ".ods") { out = ExportFormat::Ods; return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
ExportResult export_connections(const System *sys, const std::string &path,
|
||||
ExportFormat format)
|
||||
{
|
||||
ExportResult r;
|
||||
if (!sys) { r.error = "no system"; return r; }
|
||||
return (format == ExportFormat::Ods) ? write_ods(sys, path)
|
||||
: write_csv(sys, path);
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
34
src/core/app/export.hpp
Normal file
34
src/core/app/export.hpp
Normal file
@@ -0,0 +1,34 @@
|
||||
#ifndef _APP_EXPORT_HPP_
|
||||
#define _APP_EXPORT_HPP_
|
||||
|
||||
#include <string>
|
||||
|
||||
class System;
|
||||
|
||||
// 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 {
|
||||
|
||||
enum class ExportFormat { Csv, Ods };
|
||||
|
||||
// Outcome of an export. The only side effect is writing the target file; the
|
||||
// caller renders `error` / the stats however it likes.
|
||||
struct ExportResult {
|
||||
bool ok = false;
|
||||
std::string error; ///< human-readable, set when !ok
|
||||
int sheets = 0; ///< ODS: number of sheets (one per connection); 0 for CSV
|
||||
int rows = 0; ///< wires written
|
||||
};
|
||||
|
||||
// Map a filename extension (.csv / .ods, case-insensitive) to a format.
|
||||
// Returns false if the extension is neither.
|
||||
bool export_format_from_path(const std::string &path, ExportFormat &out);
|
||||
|
||||
// Export the system's connections to `path` in `format`. Builds the file and
|
||||
// returns stats or an error. Pure core — safe to call from any frontend.
|
||||
ExportResult export_connections(const System *sys, const std::string &path,
|
||||
ExportFormat format);
|
||||
|
||||
} // namespace app
|
||||
|
||||
#endif // _APP_EXPORT_HPP_
|
||||
46
src/core/app/load.cpp
Normal file
46
src/core/app/load.cpp
Normal file
@@ -0,0 +1,46 @@
|
||||
#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.ok = true;
|
||||
} catch (const std::exception &e) {
|
||||
r.error = e.what();
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
36
src/core/app/load.hpp
Normal file
36
src/core/app/load.hpp
Normal file
@@ -0,0 +1,36 @@
|
||||
#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
|
||||
};
|
||||
|
||||
// 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_
|
||||
105
src/core/app/verify.cpp
Normal file
105
src/core/app/verify.cpp
Normal file
@@ -0,0 +1,105 @@
|
||||
#include "core/app/verify.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"
|
||||
|
||||
#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-7 — 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);
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
69
src/core/app/verify.hpp
Normal file
69
src/core/app/verify.hpp
Normal file
@@ -0,0 +1,69 @@
|
||||
#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
|
||||
|
||||
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());
|
||||
}
|
||||
};
|
||||
|
||||
// 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_
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "system/component_kind.hpp"
|
||||
#include "core/domain/component_kind.hpp"
|
||||
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
@@ -1,11 +1,11 @@
|
||||
#include "system/nets.hpp"
|
||||
#include "core/domain/nets.hpp"
|
||||
|
||||
#include "system/connect.hpp"
|
||||
#include "system/modules.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.hpp"
|
||||
#include "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"
|
||||
|
||||
#include <queue>
|
||||
#include <unordered_map>
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "system/pin_name.hpp"
|
||||
#include "core/domain/pin_name.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <stdexcept>
|
||||
56
src/core/domain/system.cpp
Normal file
56
src/core/domain/system.cpp
Normal file
@@ -0,0 +1,56 @@
|
||||
|
||||
#include "system.hpp"
|
||||
|
||||
#include "connect.hpp"
|
||||
#include "modules.hpp"
|
||||
#include "core/imports/import_altium.hpp"
|
||||
#include "core/imports/import_mentor.hpp"
|
||||
#include "core/imports/import_ods.hpp"
|
||||
|
||||
System::System() : mods(nullptr), conns(nullptr)
|
||||
{
|
||||
mods = new Modules();
|
||||
conns = new Connections();
|
||||
}
|
||||
|
||||
System::~System()
|
||||
{
|
||||
delete mods;
|
||||
delete conns;
|
||||
}
|
||||
|
||||
void System::Load(std::string module_name, std::string file_name, ImportType type)
|
||||
{
|
||||
// Build the importer first, based on the import type.
|
||||
ImportBase *imp;
|
||||
if (type == ImportType::IMPORT_MENTOR)
|
||||
{
|
||||
imp = new ImportMentor(file_name);
|
||||
} else if (type == ImportType::IMPORT_ALTIUM)
|
||||
{
|
||||
imp = new ImportAltium(file_name);
|
||||
} else if (type == ImportType::IMPORT_ODS)
|
||||
{
|
||||
imp = new ImportOds(file_name);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error("Unknown import type");
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
#ifndef _SYSTEM_HPP_
|
||||
#define _SYSTEM_HPP_
|
||||
|
||||
#include "imports/import_base.hpp"
|
||||
#include "core/imports/import_base.hpp"
|
||||
|
||||
#pragma once
|
||||
class Modules; ///< Forward declaration of the Modules class.
|
||||
@@ -1,8 +1,8 @@
|
||||
#include "import_altium.hpp"
|
||||
|
||||
#include "system/parts.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
@@ -4,8 +4,8 @@
|
||||
#include <string>
|
||||
#include <fstream>
|
||||
|
||||
#include "system/parts.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
|
||||
/**
|
||||
* @brief Base class for importing data from a file.
|
||||
@@ -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_
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "import_mentor.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
|
||||
#include <cctype>
|
||||
#include <vector>
|
||||
@@ -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_
|
||||
@@ -1,8 +1,8 @@
|
||||
#include "import_ods.hpp"
|
||||
|
||||
#include "system/parts.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
|
||||
#include <pugixml.hpp>
|
||||
#include <zip.h>
|
||||
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_
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "frontends/frontend_main.hpp"
|
||||
|
||||
#include "frontends/frontend.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
@@ -11,16 +13,17 @@ void print_usage(const char *prog) {
|
||||
"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"
|
||||
" (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 is in the console screen.\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 the TUI.\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"
|
||||
@@ -29,7 +32,7 @@ void print_usage(const char *prog) {
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int frontend_main(int argc, char **argv, Frontend &fe) {
|
||||
std::string boot_restore;
|
||||
std::string boot_source;
|
||||
bool batch = false;
|
||||
@@ -37,16 +40,15 @@ int main(int argc, char **argv) {
|
||||
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);
|
||||
fe.DumpCommandsMd(f);
|
||||
} else {
|
||||
tui.DumpCommandsMd(std::cout);
|
||||
fe.DumpCommandsMd(std::cout);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -79,20 +81,19 @@ int main(int argc, char **argv) {
|
||||
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);
|
||||
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 screen yet),
|
||||
// so the console output is complete. Print it and exit without the TUI.
|
||||
// Batch mode: the boot dispatch already ran synchronously (no event loop
|
||||
// yet), so the console output is complete. Print it and exit.
|
||||
if (batch) {
|
||||
tui.DumpOutput(std::cout);
|
||||
fe.DumpOutput(std::cout);
|
||||
return 0;
|
||||
}
|
||||
|
||||
tui.Run();
|
||||
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_
|
||||
39
src/frontends/tui/CMakeLists.txt
Normal file
39
src/frontends/tui/CMakeLists.txt
Normal file
@@ -0,0 +1,39 @@
|
||||
# TUI frontend (FTXUI). Builds the `essim` executable against essim_core.
|
||||
#
|
||||
# A frontend is self-contained here: it pulls its own GUI toolkit, compiles its
|
||||
# sources into a library that links essim_core, and produces the `essim` binary
|
||||
# from its own entry point (main.cpp). To add another frontend, create a sibling
|
||||
# src/frontends/<name>/ with the same shape and select it with
|
||||
# -DESSIM_FRONTEND=<name>.
|
||||
|
||||
set(FTXUI_BUILD_DOCS OFF CACHE INTERNAL "")
|
||||
set(FTXUI_BUILD_EXAMPLES OFF CACHE INTERNAL "")
|
||||
set(FTXUI_BUILD_TESTS OFF CACHE INTERNAL "")
|
||||
set(FTXUI_ENABLE_INSTALL OFF CACHE INTERNAL "")
|
||||
|
||||
FetchContent_Declare(ftxui
|
||||
GIT_REPOSITORY https://github.com/ArthurSonzogni/FTXUI.git
|
||||
GIT_TAG v6.1.9
|
||||
GIT_SHALLOW TRUE
|
||||
)
|
||||
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 essim_frontend)
|
||||
|
||||
# Keep the binary at the top of the build tree (./build/essim), regardless of
|
||||
# which frontend subdir produced it.
|
||||
set_target_properties(essim PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}")
|
||||
@@ -1,28 +1,28 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
#include "frontends/tui/tui.hpp"
|
||||
#include "frontends/tui/tui_helpers.hpp"
|
||||
|
||||
#include "system/analysis.hpp"
|
||||
#include "system/connect.hpp"
|
||||
#include "system/modules.hpp"
|
||||
#include "system/nets.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/persist.hpp"
|
||||
#include "system/pin_role.hpp"
|
||||
#include "system/pin_model.hpp"
|
||||
#include "system/bsdl_model.hpp"
|
||||
#include "system/bsdl_check.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.hpp"
|
||||
#include "system/transform.hpp"
|
||||
#include "system/transform_vpx.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/pin_role.hpp"
|
||||
#include "core/domain/pin_model.hpp"
|
||||
#include "core/domain/bsdl_model.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
|
||||
#include "core/app/connect.hpp"
|
||||
#include "core/app/load.hpp"
|
||||
#include "core/app/verify.hpp"
|
||||
#include "core/domain/transform_vpx.hpp" // ValidatePartForKind
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <exception>
|
||||
#include <fstream>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
void Tui::RegisterCommands() {
|
||||
@@ -138,29 +138,23 @@ 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)");
|
||||
},
|
||||
/*prompt_for_missing=*/ true,
|
||||
"load a module from a netlist / pinout file (mentor, altium, ods)",
|
||||
@@ -228,104 +222,43 @@ 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)
|
||||
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).");
|
||||
// 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(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).");
|
||||
}, true,
|
||||
"check pin roles, power/gnd net consistency, and (with BSDL) pin and JTAG checks" };
|
||||
|
||||
@@ -622,47 +555,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)
|
||||
+ " 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());
|
||||
// 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");
|
||||
}
|
||||
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)",
|
||||
70
src/frontends/tui/commands_export.cpp
Normal file
70
src/frontends/tui/commands_export.cpp
Normal file
@@ -0,0 +1,70 @@
|
||||
#include "frontends/tui/tui.hpp"
|
||||
|
||||
#include "core/app/export.hpp"
|
||||
#include "core/domain/connect.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Thin UI wrapper around app::export_connections — this file only resolves
|
||||
// arguments / the file dialog and renders the result. All the actual export
|
||||
// (CSV / ODS building, file writing) lives in src/app/export.cpp.
|
||||
void Tui::RegisterExportCommands() {
|
||||
commands["export"] = {
|
||||
{{"kind [connections]", Completion::None},
|
||||
{"filename (.csv)", Completion::Path}},
|
||||
[this](const std::vector<std::string> &args) {
|
||||
if (!sys) { Print("no system: run 'new' first."); return; }
|
||||
|
||||
if (args.empty()) {
|
||||
// Bare → the generic file dialog. The CSV/ODS filter rewrites
|
||||
// the extension; the action below dispatches on it.
|
||||
OpenFileDialog(
|
||||
"Export — connections",
|
||||
"export.connections",
|
||||
"connections.csv",
|
||||
{{"CSV", ".csv"}, {"ODS", ".ods"}},
|
||||
[this](const std::string &path) {
|
||||
Dispatch("export connections " + path);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (args.size() != 2) {
|
||||
Print("usage: export <kind> <file> (or no args for the dialog)");
|
||||
return;
|
||||
}
|
||||
const std::string &kind = args[0];
|
||||
const std::string &path = args[1];
|
||||
|
||||
if (kind != "connections") {
|
||||
ShowError("export: unknown kind '" + kind + "'\n"
|
||||
"Known kinds: connections");
|
||||
return;
|
||||
}
|
||||
|
||||
app::ExportFormat fmt;
|
||||
if (!app::export_format_from_path(path, fmt)) {
|
||||
ShowError("export: unknown extension — accepted: .csv, .ods");
|
||||
return;
|
||||
}
|
||||
|
||||
app::ExportResult r = app::export_connections(sys.get(), path, fmt);
|
||||
if (!r.ok) {
|
||||
ShowError("export failed:\n" + r.error);
|
||||
return;
|
||||
}
|
||||
|
||||
const bool ods = (fmt == app::ExportFormat::Ods);
|
||||
std::string msg = ods ? "export connections (.ods): "
|
||||
: "export connections (.csv): ";
|
||||
if (ods) msg += std::to_string(r.sheets) + " sheet(s), ";
|
||||
Print(msg + std::to_string(r.rows) + " wire(s) → " + path);
|
||||
},
|
||||
/*prompt_for_missing=*/ false,
|
||||
"export structured data to CSV / ODS (kinds: connections; "
|
||||
"bare form opens the file-picker dialog)",
|
||||
/*scriptable=*/ true,
|
||||
/*interactive=*/ true,
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
#include "frontends/tui/tui.hpp"
|
||||
#include "frontends/tui/tui_helpers.hpp"
|
||||
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
11
src/frontends/tui/main.cpp
Normal file
11
src/frontends/tui/main.cpp
Normal file
@@ -0,0 +1,11 @@
|
||||
#include "frontends/frontend_main.hpp"
|
||||
#include "frontends/tui/tui.hpp"
|
||||
|
||||
// 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) {
|
||||
Tui tui;
|
||||
return frontend_main(argc, argv, tui);
|
||||
}
|
||||
@@ -1,15 +1,11 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
#include "frontends/tui/tui.hpp"
|
||||
#include "frontends/tui/tui_helpers.hpp"
|
||||
|
||||
#include "system/analysis.hpp"
|
||||
#include "system/bsdl_check.hpp"
|
||||
#include "system/connect.hpp"
|
||||
#include "system/modules.hpp"
|
||||
#include "system/nets.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.hpp"
|
||||
#include "core/app/verify.hpp"
|
||||
#include "core/domain/analysis.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/component_options.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,25 @@ 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)
|
||||
analyze_issues.push_back(std::string("[")
|
||||
+ anomaly_kind_name(a.kind) + "] "
|
||||
+ a.message);
|
||||
int n_model = (int)model_anoms.size();
|
||||
// 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);
|
||||
};
|
||||
push_anoms(vr.pin_anomalies);
|
||||
push_anoms(vr.jtag_anomalies);
|
||||
push_anoms(vr.conflict_anomalies);
|
||||
push_anoms(vr.completeness_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)
|
||||
@@ -215,26 +189,11 @@ 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;
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "frontends/tui/tui.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/event.hpp>
|
||||
@@ -1,11 +1,10 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
#include "frontends/tui/tui.hpp"
|
||||
#include "frontends/tui/tui_helpers.hpp"
|
||||
|
||||
#include "system/connect.hpp"
|
||||
#include "system/modules.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/system.hpp"
|
||||
#include "system/transform.hpp"
|
||||
#include "core/app/connect.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/system.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;
|
||||
}
|
||||
std::string err = CheckIdentityCompatible(p1, p2);
|
||||
if (!err.empty()) {
|
||||
Print("connect refused: " + err);
|
||||
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");
|
||||
}
|
||||
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,22 +1,19 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
#include "frontends/tui/tui.hpp"
|
||||
#include "frontends/tui/tui_helpers.hpp"
|
||||
|
||||
#include "system/analysis.hpp"
|
||||
#include "system/bsdl_check.hpp"
|
||||
#include "system/connect.hpp"
|
||||
#include "system/modules.hpp"
|
||||
#include "system/nets.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.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/signals.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/dom/elements.hpp>
|
||||
|
||||
#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,4 +1,4 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "frontends/tui/tui.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/event.hpp>
|
||||
@@ -1,13 +1,13 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
#include "frontends/tui/tui.hpp"
|
||||
#include "frontends/tui/tui_helpers.hpp"
|
||||
|
||||
#include "system/connect.hpp"
|
||||
#include "system/modules.hpp"
|
||||
#include "system/nets.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.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"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/component_options.hpp>
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
#include "frontends/tui/tui.hpp"
|
||||
#include "frontends/tui/tui_helpers.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/component_options.hpp>
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
#include "frontends/tui/tui.hpp"
|
||||
#include "frontends/tui/tui_helpers.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/component_options.hpp>
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
#include "frontends/tui/tui.hpp"
|
||||
#include "frontends/tui/tui_helpers.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/component_options.hpp>
|
||||
@@ -1,9 +1,9 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
#include "frontends/tui/tui.hpp"
|
||||
#include "frontends/tui/tui_helpers.hpp"
|
||||
|
||||
#include "system/modules.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/component_options.hpp>
|
||||
@@ -1,13 +1,13 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
#include "frontends/tui/tui.hpp"
|
||||
#include "frontends/tui/tui_helpers.hpp"
|
||||
|
||||
#include "system/modules.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/pin_model.hpp"
|
||||
#include "system/pin_role.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/system.hpp"
|
||||
#include "system/transform_vpx.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>
|
||||
@@ -1,8 +1,8 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "frontends/tui/tui.hpp"
|
||||
|
||||
#include "system/modules.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/component_options.hpp>
|
||||
@@ -1,9 +1,9 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
#include "frontends/tui/tui.hpp"
|
||||
#include "frontends/tui/tui_helpers.hpp"
|
||||
|
||||
#include "system/modules.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "frontends/tui/tui.hpp"
|
||||
|
||||
#include "system/system.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/event.hpp>
|
||||
@@ -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 {
|
||||
@@ -198,16 +200,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)
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "tui/tui_helpers.hpp"
|
||||
#include "frontends/tui/tui_helpers.hpp"
|
||||
|
||||
#include <ftxui/dom/elements.hpp>
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
|
||||
#include "system.hpp"
|
||||
|
||||
#include "connect.hpp"
|
||||
#include "modules.hpp"
|
||||
#include "imports/import_altium.hpp"
|
||||
#include "imports/import_mentor.hpp"
|
||||
#include "imports/import_ods.hpp"
|
||||
|
||||
System::System() : mods(nullptr), conns(nullptr)
|
||||
{
|
||||
mods = new Modules();
|
||||
conns = new Connections();
|
||||
}
|
||||
|
||||
System::~System()
|
||||
{
|
||||
delete mods;
|
||||
delete conns;
|
||||
}
|
||||
|
||||
void System::Load(std::string module_name, std::string file_name, ImportType 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);
|
||||
} else if (type == ImportType::IMPORT_ALTIUM)
|
||||
{
|
||||
imp = new ImportAltium(file_name);
|
||||
} else if (type == ImportType::IMPORT_ODS)
|
||||
{
|
||||
imp = new ImportOds(file_name);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error("Unknown import type");
|
||||
}
|
||||
imp->parse(mod->signals);
|
||||
prts = imp->parts();
|
||||
mod->add(prts);
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
#include "tui/tui.hpp"
|
||||
#include "tui/tui_helpers.hpp"
|
||||
|
||||
#include "imports/ods_writer.hpp"
|
||||
#include "system/connect.hpp"
|
||||
#include "system/modules.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/signal_type.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
// Minimal CSV quoter — wraps in `"…"` and doubles internal quotes when
|
||||
// the field contains a comma, quote, or newline. Local to this file.
|
||||
std::string csv_quote(const std::string &s) {
|
||||
bool needs = s.find_first_of(",\"\n") != std::string::npos;
|
||||
if (!needs) return s;
|
||||
std::string out = "\"";
|
||||
for (char c : s) { if (c == '"') out += '"'; out += c; }
|
||||
out += '"';
|
||||
return out;
|
||||
}
|
||||
|
||||
// Flatten one pin into the 6 string slots the export row uses.
|
||||
void pin_side(Pin *p, std::string &mod, std::string &part,
|
||||
std::string &pin, std::string &sig,
|
||||
std::string &type, std::string &suspect) {
|
||||
if (!p) { mod = part = pin = sig = type = suspect = ""; return; }
|
||||
mod = (p->prnt && p->prnt->prnt) ? p->prnt->prnt->name : "";
|
||||
part = p->prnt ? p->prnt->name : "";
|
||||
pin = p->name;
|
||||
Signal *s = p->signal();
|
||||
if (!s) {
|
||||
sig = ""; type = "(NC)"; suspect = "";
|
||||
} else {
|
||||
sig = s->name;
|
||||
type = signal_type_name(s->type);
|
||||
suspect = (infer_signal_type(s->name) == SignalType::Power
|
||||
&& s->type == SignalType::Other) ? "yes" : "no";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Tui::RegisterExportCommands() {
|
||||
commands["export"] = {
|
||||
{{"kind [connections]", Completion::None},
|
||||
{"filename (.csv)", Completion::Path}},
|
||||
[this](const std::vector<std::string> &args) {
|
||||
if (!sys) { Print("no system: run 'new' first."); return; }
|
||||
if (args.empty()) {
|
||||
// Bare → reuse the generic file dialog. Filters give a
|
||||
// one-keystroke CSV/ODS toggle; picking either rewrites
|
||||
// the filename's extension, and the action below
|
||||
// dispatches on that extension.
|
||||
OpenFileDialog(
|
||||
"Export — connections",
|
||||
"export.connections",
|
||||
"connections.csv",
|
||||
{{"CSV", ".csv"}, {"ODS", ".ods"}},
|
||||
[this](const std::string &path) {
|
||||
Dispatch("export connections " + path);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (args.size() != 2) {
|
||||
Print("usage: export <kind> <file> (or no args for the dialog)");
|
||||
return;
|
||||
}
|
||||
const std::string &kind = args[0];
|
||||
const std::string &path = args[1];
|
||||
|
||||
if (kind == "connections") {
|
||||
// Accepted extensions: `.csv` (flat file) and `.ods`
|
||||
// (one sheet per connection). Anything else is an error.
|
||||
std::string ext;
|
||||
{
|
||||
size_t dot = path.rfind('.');
|
||||
if (dot != std::string::npos) ext = ToLower(path.substr(dot));
|
||||
}
|
||||
bool ods = (ext == ".ods");
|
||||
bool csv = (ext == ".csv");
|
||||
if (!ods && !csv) {
|
||||
ShowError("export: unknown extension '"
|
||||
+ (ext.empty() ? std::string("(none)") : ext)
|
||||
+ "'\nAccepted: .csv, .ods");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ods) {
|
||||
OdsWriter w;
|
||||
int total = 0;
|
||||
for (auto &ckv : *sys->connections()) {
|
||||
Connection *c = ckv.second;
|
||||
// Sheet names: Excel rejects /\?*:[] characters,
|
||||
// ODS forbids < > & in raw cell/table names.
|
||||
// Sanitise to underscores; clip to 31 chars
|
||||
// (Excel's hard limit).
|
||||
std::string sname = c->name;
|
||||
for (char &ch : sname)
|
||||
if (ch == '/' || ch == '\\' || ch == '?' || ch == '*'
|
||||
|| ch == ':' || ch == '[' || ch == ']'
|
||||
|| ch == '<' || ch == '>' || ch == '&') ch = '_';
|
||||
if (sname.size() > 31) sname = sname.substr(0, 31);
|
||||
OdsSheet *s = w.add_sheet(sname);
|
||||
|
||||
// Pull the constants for this connection once.
|
||||
// `transform`, the left module/part, and the
|
||||
// right module/part don't vary across the wires
|
||||
// of a single connection — putting them in
|
||||
// every row was repetitive.
|
||||
std::string lmod, lprt;
|
||||
std::string rmod, rprt;
|
||||
if (c->m1) lmod = c->m1->name;
|
||||
if (c->p1) lprt = c->p1->name;
|
||||
if (c->m2) rmod = c->m2->name;
|
||||
if (c->p2) rprt = c->p2->name;
|
||||
|
||||
// Meta header above the table: 5 rows of label /
|
||||
// value, then a blank, then the column headers
|
||||
// on row 6 (index 5).
|
||||
auto meta = [&](int r, const std::string &k,
|
||||
const std::string &v) {
|
||||
s->set(r, 0, k);
|
||||
s->set(r, 1, v);
|
||||
};
|
||||
meta(0, "Connection", c->name);
|
||||
meta(1, "Transform", c->transform_name);
|
||||
meta(2, "Left", lmod + " / " + lprt);
|
||||
meta(3, "Right", rmod + " / " + rprt);
|
||||
// Row 4 left blank by design.
|
||||
|
||||
const int HDR = 5;
|
||||
s->set_header_row(HDR);
|
||||
const char *hdr[] = {
|
||||
"left_pin", "left_signal", "left_type", "left_suspect",
|
||||
"right_pin", "right_signal", "right_type", "right_suspect",
|
||||
"type_mismatch"};
|
||||
for (int i = 0; i < 9; ++i) s->set(HDR, i, hdr[i]);
|
||||
|
||||
int row = HDR + 1;
|
||||
for (auto &wp : c->pin_map) {
|
||||
std::string lm, lp, ln, ls, lt, lsus;
|
||||
std::string rm, rp, rn, rs, rt, rsus;
|
||||
pin_side(wp.first, lm, lp, ln, ls, lt, lsus);
|
||||
pin_side(wp.second, rm, rp, rn, rs, rt, rsus);
|
||||
// `type_mismatch = yes` when both sides have a
|
||||
// real signal AND their types disagree (e.g.
|
||||
// Power ↔ Gnd, or Power ↔ Other).
|
||||
std::string tm = (lt != "(NC)" && rt != "(NC)" && lt != rt)
|
||||
? "yes" : "no";
|
||||
s->set(row, 0, ln); s->set(row, 1, ls);
|
||||
s->set(row, 2, lt); s->set(row, 3, lsus);
|
||||
s->set(row, 4, rn); s->set(row, 5, rs);
|
||||
s->set(row, 6, rt); s->set(row, 7, rsus);
|
||||
s->set(row, 8, tm);
|
||||
++row; ++total;
|
||||
}
|
||||
}
|
||||
std::string err;
|
||||
if (!w.save(path, err)) {
|
||||
ShowError("export (.ods) failed:\n" + err);
|
||||
return;
|
||||
}
|
||||
Print("export connections (.ods): "
|
||||
+ std::to_string(sys->connections()->size())
|
||||
+ " sheet(s), " + std::to_string(total)
|
||||
+ " wire(s) → " + path);
|
||||
return;
|
||||
}
|
||||
|
||||
// Classic flat CSV: a single rectangular table — one
|
||||
// header line, N data rows, every row carries the per-
|
||||
// connection constants too. The 9 right-most column
|
||||
// names match the ODS sheet headers exactly; the 5
|
||||
// leading ones (connection, transform, left_module,
|
||||
// left_part, right_module, right_part) correspond to
|
||||
// the ODS meta block (Connection, Transform, Left,
|
||||
// Right). Repeating the constants per row keeps the
|
||||
// file parser-friendly (pandas / awk / spreadsheet).
|
||||
std::ofstream f(path);
|
||||
if (!f) {
|
||||
ShowError("export: cannot open '" + path + "' for writing");
|
||||
return;
|
||||
}
|
||||
f << "connection,transform,"
|
||||
"left_module,left_part,"
|
||||
"left_pin,left_signal,left_type,left_suspect,"
|
||||
"right_module,right_part,"
|
||||
"right_pin,right_signal,right_type,right_suspect,"
|
||||
"type_mismatch\n";
|
||||
|
||||
int rows = 0;
|
||||
for (auto &ckv : *sys->connections()) {
|
||||
Connection *c = ckv.second;
|
||||
for (auto &wp : c->pin_map) {
|
||||
std::string lm, lp, ln, ls, lt, lsus;
|
||||
std::string rm, rp, rn, rs, rt, rsus;
|
||||
pin_side(wp.first, lm, lp, ln, ls, lt, lsus);
|
||||
pin_side(wp.second, rm, rp, rn, rs, rt, rsus);
|
||||
std::string tm = "no";
|
||||
if (lt != "(NC)" && rt != "(NC)" && lt != rt) tm = "yes";
|
||||
f << csv_quote(c->name) << ','
|
||||
<< csv_quote(c->transform_name) << ','
|
||||
<< csv_quote(lm) << ',' << csv_quote(lp) << ','
|
||||
<< csv_quote(ln) << ',' << csv_quote(ls) << ','
|
||||
<< csv_quote(lt) << ',' << csv_quote(lsus) << ','
|
||||
<< csv_quote(rm) << ',' << csv_quote(rp) << ','
|
||||
<< csv_quote(rn) << ',' << csv_quote(rs) << ','
|
||||
<< csv_quote(rt) << ',' << csv_quote(rsus) << ','
|
||||
<< tm << '\n';
|
||||
++rows;
|
||||
}
|
||||
}
|
||||
Print("export connections (.csv): " + std::to_string(rows)
|
||||
+ " wire(s) → " + path);
|
||||
return;
|
||||
}
|
||||
|
||||
ShowError("export: unknown kind '" + kind + "'\n"
|
||||
"Known kinds: connections");
|
||||
},
|
||||
/*prompt_for_missing=*/ false,
|
||||
"export structured data to CSV / ODS (kinds: connections; "
|
||||
"bare form opens the file-picker dialog)",
|
||||
/*scriptable=*/ true,
|
||||
/*interactive=*/ true,
|
||||
};
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "system/analysis.hpp"
|
||||
#include "system/modules.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.hpp"
|
||||
#include "core/domain/analysis.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"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "system/analysis.hpp"
|
||||
#include "system/bsdl_check.hpp"
|
||||
#include "system/bsdl_model.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/pin_spec.hpp"
|
||||
#include "system/system.hpp"
|
||||
#include "system/modules.hpp"
|
||||
#include "system/persist.hpp"
|
||||
#include "core/domain/analysis.hpp"
|
||||
#include "core/domain/bsdl_check.hpp"
|
||||
#include "core/domain/bsdl_model.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
#include "core/domain/pin_spec.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/persist.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "system/analysis.hpp"
|
||||
#include "system/bsdl_check.hpp"
|
||||
#include "system/modules.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/pin_spec.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.hpp"
|
||||
#include "core/domain/analysis.hpp"
|
||||
#include "core/domain/bsdl_check.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/pin_spec.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "system/component_kind.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "core/domain/component_kind.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
76
tests/test_export.cpp
Normal file
76
tests/test_export.cpp
Normal file
@@ -0,0 +1,76 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "core/app/export.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"
|
||||
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
std::string slurp(const std::string &path)
|
||||
{
|
||||
std::ifstream f(path);
|
||||
std::stringstream ss;
|
||||
ss << f.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("export_format_from_path maps extensions") {
|
||||
app::ExportFormat f;
|
||||
CHECK(app::export_format_from_path("a.csv", f));
|
||||
CHECK(f == app::ExportFormat::Csv);
|
||||
CHECK(app::export_format_from_path("a.ODS", f));
|
||||
CHECK(f == app::ExportFormat::Ods);
|
||||
CHECK_FALSE(app::export_format_from_path("a.txt", f));
|
||||
CHECK_FALSE(app::export_format_from_path("noext", f));
|
||||
}
|
||||
|
||||
TEST_CASE("export_connections writes a flat CSV (no UI needed)") {
|
||||
// Two cards, one wired pin pair via a connection.
|
||||
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->add(pa); pa->connect(sa);
|
||||
Signal *sb = b->signals->merge("NETB"); 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);
|
||||
|
||||
const char *path = "test_export_out.csv";
|
||||
app::ExportResult r = app::export_connections(&sys, path, app::ExportFormat::Csv);
|
||||
CHECK(r.ok);
|
||||
CHECK(r.rows == 1);
|
||||
|
||||
std::string out = slurp(path);
|
||||
CHECK(out.find("connection,transform,") == 0); // header present
|
||||
CHECK(out.find("A.J1<->B.P1") != std::string::npos); // connection name
|
||||
CHECK(out.find("identity") != std::string::npos); // transform
|
||||
CHECK(out.find("NETA") != std::string::npos); // left signal
|
||||
CHECK(out.find("NETB") != std::string::npos); // right signal
|
||||
|
||||
std::remove(path);
|
||||
}
|
||||
|
||||
TEST_CASE("export_connections reports a bad path instead of crashing") {
|
||||
System sys;
|
||||
app::ExportResult r = app::export_connections(
|
||||
&sys, "/nonexistent-dir-xyz/out.csv", app::ExportFormat::Csv);
|
||||
CHECK_FALSE(r.ok);
|
||||
CHECK_FALSE(r.error.empty());
|
||||
}
|
||||
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);
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "system/modules.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/persist.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.hpp"
|
||||
#include "core/domain/modules.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/persist.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
#include "core/domain/signals.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "system/connect.hpp"
|
||||
#include "system/modules.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/persist.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.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/signals.hpp"
|
||||
#include "core/domain/system.hpp"
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "system/parts.hpp"
|
||||
#include "system/pin_model.hpp"
|
||||
#include "system/pin_spec.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/pin_model.hpp"
|
||||
#include "core/domain/pin_spec.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "system/parts.hpp"
|
||||
#include "system/pin_name.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/transform.hpp"
|
||||
#include "core/domain/parts.hpp"
|
||||
#include "core/domain/pin_name.hpp"
|
||||
#include "core/domain/pins.hpp"
|
||||
#include "core/domain/transform.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "system/signal_type.hpp"
|
||||
#include "core/domain/signal_type.hpp"
|
||||
|
||||
TEST_CASE("signal_type_name round-trips with from_name") {
|
||||
SignalType t;
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "system/modules.hpp"
|
||||
#include "system/parts.hpp"
|
||||
#include "system/pins.hpp"
|
||||
#include "system/signals.hpp"
|
||||
#include "system/system.hpp"
|
||||
#include "system/transform.hpp"
|
||||
#include "system/transform_vpx.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"
|
||||
#include "core/domain/transform.hpp"
|
||||
#include "core/domain/transform_vpx.hpp"
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include "tui/tui_helpers.hpp"
|
||||
#include "frontends/tui/tui_helpers.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
Reference in New Issue
Block a user