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:
2026-06-03 20:37:35 +02:00
99 changed files with 1649 additions and 909 deletions

View File

@@ -11,49 +11,60 @@ project(essim
include(FetchContent) include(FetchContent)
set(FTXUI_BUILD_DOCS OFF CACHE INTERNAL "") # ----------------------------------------------------------------- core deps
set(FTXUI_BUILD_EXAMPLES OFF CACHE INTERNAL "") # libbsdl — standalone BSDL parser (LGPL-2.1), dynamically linked (EUPL-1.2,
set(FTXUI_BUILD_TESTS OFF CACHE INTERNAL "") # which the LGPL permits). Override its path with -DBSDL_DIR=...
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.
set(BSDL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../libbsdl" CACHE PATH "libbsdl source tree") set(BSDL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../libbsdl" CACHE PATH "libbsdl source tree")
set(BSDL_BUILD_CLI OFF CACHE BOOL "" FORCE) set(BSDL_BUILD_CLI OFF CACHE BOOL "" FORCE)
set(BSDL_BUILD_TESTS OFF CACHE BOOL "" FORCE) set(BSDL_BUILD_TESTS OFF CACHE BOOL "" FORCE)
add_subdirectory(${BSDL_DIR} ${CMAKE_BINARY_DIR}/libbsdl) add_subdirectory(${BSDL_DIR} ${CMAKE_BINARY_DIR}/libbsdl)
# Library target = everything except main.cpp; reused by `essim` and `essim_tests`. find_package(libzip REQUIRED)
file(GLOB_RECURSE LIB_SOURCES "src/*.cpp") find_package(pugixml REQUIRED)
list(REMOVE_ITEM LIB_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp")
add_library(essim_lib STATIC ${LIB_SOURCES}) # =============================================================== essim_core
target_include_directories(essim_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src) # All business logic — domain model, importers, application operations
target_link_libraries(essim_lib # (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 PUBLIC
ftxui::screen
ftxui::dom
ftxui::component
libzip::zip libzip::zip
pugixml::pugixml pugixml::pugixml
bsdl::bsdl bsdl::bsdl
) )
add_executable(essim src/main.cpp) # =============================================================== frontend(s)
target_link_libraries(essim PRIVATE essim_lib) # 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) include(CTest)
if(BUILD_TESTING) if(BUILD_TESTING)
set(CMAKE_POLICY_VERSION_MINIMUM 3.5) set(CMAKE_POLICY_VERSION_MINIMUM 3.5)
@@ -65,20 +76,35 @@ if(BUILD_TESTING)
FetchContent_MakeAvailable(doctest) FetchContent_MakeAvailable(doctest)
unset(CMAKE_POLICY_VERSION_MINIMUM) 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") file(GLOB TEST_SOURCES "tests/*.cpp")
if(TEST_SOURCES) if(TEST_SOURCES)
add_executable(essim_tests ${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) add_test(NAME essim_tests COMMAND essim_tests)
endif() 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() endif()
# Documentation: Doxygen → XML → custom Python script → doc/api/ (Markdown rendered by gitea). # =============================================================== documentation
# Optional — `doc` target is only created if Doxygen and Python 3 are present. # 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(Doxygen COMPONENTS doxygen)
find_package(Python3 COMPONENTS Interpreter) 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") set(DOXYGEN_OUTPUT_DIR "${CMAKE_BINARY_DIR}/doc")
file(MAKE_DIRECTORY "${DOXYGEN_OUTPUT_DIR}") file(MAKE_DIRECTORY "${DOXYGEN_OUTPUT_DIR}")
configure_file( configure_file(
@@ -103,11 +129,10 @@ if(DOXYGEN_FOUND AND Python3_Interpreter_FOUND)
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
COMMENT "Generating documentation (doxygen → gen_api_md.py → doc/api/, essim --commands-md → doc/user/commands.md)" COMMENT "Generating documentation (doxygen → gen_api_md.py → doc/api/, essim --commands-md → doc/user/commands.md)"
VERBATIM) VERBATIM)
elseif(NOT DOXYGEN_FOUND AND NOT Python3_Interpreter_FOUND) elseif(NOT TARGET essim)
message(STATUS "doc: Doxygen and Python 3 not found — `doc` target disabled.") message(STATUS "doc: no `essim` binary (ESSIM_FRONTEND=none) — `doc` target disabled.")
elseif(NOT DOXYGEN_FOUND) elseif(NOT DOXYGEN_FOUND)
message(STATUS "doc: Doxygen not found — `doc` target disabled " message(STATUS "doc: Doxygen not found — `doc` target disabled.")
"(install via `pacman -S doxygen`).")
else() else()
message(STATUS "doc: Python 3 interpreter not found — `doc` target disabled.") message(STATUS "doc: Python 3 interpreter not found — `doc` target disabled.")
endif() endif()

183
DESIGN.md
View File

@@ -10,72 +10,111 @@ cmake --build build -j
./build/essim ./build/essim
``` ```
- CMake **3.14+** required (uses `FetchContent_MakeAvailable`). - CMake **3.14+** (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/`. - **Layered build** (see *Architecture* below). `essim_core` is the
- **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`. frontend-agnostic business library; a frontend under `src/frontends/<name>/`
- **`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`. links it and produces the `essim` binary. Choose it with
- 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". `-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`). - **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 ## Layout
``` ```
src/ src/
main.cpp -- launches Tui core/ -- business logic; NO GUI toolkit (builds libessim_core)
system/ -- domain model domain/ -- the model + read-only analyses
syselmts.hpp SystemElement + SystemElementContainer<T> (templated, get/merge/iterate) syselmts.hpp SystemElement + SystemElementContainer<T> (get/merge/iterate)
modules.{hpp,cpp} Module, Modules modules.{hpp,cpp} Module, Modules
parts.{hpp,cpp} Part (carries `kind` + `connector_type`), Parts parts.{hpp,cpp} Part (kind, connector_type, bsdl_path; PinSpec per pin)
pins.{hpp,cpp} Pin (carries PinSpec `spec`), Pins pins.{hpp,cpp} Pin (carries PinSpec `spec`), Pins
signals.{hpp,cpp} Signal, Signals signals.{hpp,cpp} Signal, Signals
signal_type.hpp SignalType + helpers signal_type.hpp SignalType + helpers
pin_spec.hpp PinSpec (function/direction/pad/source) + SignalType mapping pin_spec.hpp PinSpec (function/direction/pad/source), mappings, spec_source_rank
component_kind.{hpp,cpp} ComponentKind enum + infer_component_kind(name) component_kind.{hpp,cpp} ComponentKind + infer_component_kind(name)
pin_name.{hpp,cpp} canonical_pin_name(s) — zero-pad digit suffix to 3 pin_name.{hpp,cpp} canonical_pin_name (zero-pad digit suffix to 3)
connect.{hpp,cpp} Connection, Connections connect.{hpp,cpp} Connection, Connections
transform.{hpp,cpp} Transform / IdentityTransform / TransformRegistry + transform*.{hpp,cpp} Transform / IdentityTransform / TransformRegistry, VPX transform
CheckIdentityCompatible + FillIdentityNCs pin_role.{hpp,cpp} pin_role(kind,name) -> PinSpec, pin_layout, FillPartFromLayout
pin_role.{hpp,cpp} pin_role(kind, name) → PinSpec, pin_layout(kind), pin_model.{hpp,cpp} PinModel + apply_model(Part*, model) + ConnectorModel
FillPartFromLayout(part, kind) bsdl_model.{hpp,cpp} BsdlModel (libbsdl wrapper) + BsdlPinModel + apply_bsdl
pin_model.{hpp,cpp} PinModel + apply_model(Part*, model) + ConnectorModel bsdl_check.{hpp,cpp} check_pin_specs / _jtag_chain / _source_conflicts / _bsdl_completeness
bsdl_model.{hpp,cpp} BsdlModel (libbsdl wrapper) + BsdlPinModel + apply_bsdl nets.{hpp,cpp} find_net / compute_all_nets / net_type_consistent
bsdl_check.{hpp,cpp} check_pin_specs / check_jtag_chain → vector<Anomaly> analysis.{hpp,cpp} analyze_system -> AnalysisReport (diff pairs, buses, anomalies)
nets.{hpp,cpp} find_net / compute_all_nets / net_type_consistent persist.{hpp,cpp} save / restore (tab-delimited; `B` tag = bsdl_path)
analysis.{hpp,cpp} analyze_system → AnalysisReport (diff pairs, buses, anomalies) system.{hpp,cpp} System: owns Modules + Connections, Load()
persist.{hpp,cpp} save / restore (tab-delimited) imports/ -- netlist / pinout adapters
system.{hpp,cpp} System: owns Modules + Connections, exposes Load() import_base.hpp / import_{mentor,altium,ods}.{hpp,cpp} / ods_writer.{hpp,cpp}
imports/ -- adapters that populate or emit the domain app/ -- application operations (UI-independent use cases)
import_base.hpp ImportBase interface export.{hpp,cpp} export_connections(System*, path, format) -> ExportResult
import_mentor.{hpp,cpp} Mentor Graphics netlist parser verify.{hpp,cpp} verify(System*) -> VerifyReport (the 7 verify passes)
import_altium.{hpp,cpp} Altium netlist parser (`[ ]` parts, `( )` signals) connect.{hpp,cpp} connect_parts(System*, m1,p1, m2,p2) -> ConnectResult
import_ods.{hpp,cpp} ODS spreadsheet pinout (libzip + pugixml) load.{hpp,cpp} load_module(System*, name, path, ImportType) -> LoadResult
ods_writer.{hpp,cpp} Minimal .ods writer (multi-sheet, string cells) frontends/ -- one directory per GUI/TUI engine; each links essim_core
tui/ -- FTXUI shell, split by responsibility frontend.hpp -- abstract Frontend interface (BootDispatch/Dump*/Run)
tui.{hpp,cpp} Class Tui (state + Run() orchestrator + screen-mode event dispatcher) frontend_main.{hpp,cpp} -- frontend_main(argc,argv,Frontend&): argv + boot/batch/run
tui_helpers.{hpp,cpp} Free helpers: ToLower, NaturalLess, LongestCommonPrefix tui/ -- FTXUI shell (builds libessim_tui + the `essim` binary)
shell.cpp Print, Submit, Dispatch, Finalize, Tokenize, history persistence CMakeLists.txt fetches FTXUI; builds essim_tui + essim
completion.cpp CompleteCommand, CompletePath, CompleteInline main.cpp entry point: construct Tui, call frontend_main
commands.cpp RegisterCommands (orchestrator + lifecycle / shell / topology commands) tui.{hpp,cpp} class Tui (state + Run() + screen-mode event dispatch)
commands_export.cpp RegisterExportCommands (export → CSV / ODS, file-dialog hook) tui_helpers.{hpp,cpp} ToLower, NaturalLess, RenderHelpPanel
screen_main.cpp BuildMainScreen (visualisation area + bottom input) shell.cpp Print, Submit, Dispatch, Source / ProcessNextSourceLine
screen_connect.cpp BuildConnectScreen + shared RefreshFilteredPartList helper completion.cpp CompleteCommand / CompletePath / CompleteInline
screen_settype.cpp BuildSettypeScreen commands.cpp RegisterCommands (thin: resolve args -> call core -> render)
screen_explore.cpp BuildExploreScreen (browse modules → parts/signals/connections → details; not scriptable) commands_export.cpp thin wrapper over app::export_connections
screen_dashboard.cpp BuildDashboardScreen (read-only system overview) screen_*.cpp dashboard, connect, settype, explore, analyze, help, main
screen_analyze.cpp BuildAnalyzeScreen (anomalies / groups / power decisions) (console), palette, file dialog, error/confirm, sigtype modal
screen_palette.cpp BuildPaletteModal (global Ctrl-P fuzzy launcher) tests/ -- core tests (link essim_core)
screen_filedialog.cpp BuildFileDialog + OpenFileDialog (reusable file picker) tui/ -- frontend tests (link essim_tui)
screen_error.cpp BuildErrorModal + ShowError (centred error popup) doc/ , test/ -- docs; sample netlists + system.essim bring-up script
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
``` ```
`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 ## 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. - `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. - `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. - 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. 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: **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. - `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. - `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`. - `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. 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`. **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. **`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 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). - **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. **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. **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`. **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. **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. **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)`). - `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`. - `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). 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_`. - `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. - `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`. - Else `Other`.
@@ -264,15 +303,15 @@ Type is set by `infer_signal_types(System*)` (`src/system/analysis.cpp`), called
### NC pin origin ### 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::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/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::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. - **`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 ### 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`): **DiffPair** (`GroupKind::DiffPair`):
- Signal name ends `_P` or `_N` (case-insensitive). The character before the suffix must be `_`. - 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 ### 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`. - 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`. - 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`: `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. - `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. - 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.

View File

@@ -15,6 +15,13 @@ cmake --build build -j
./build/essim ./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 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). 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); 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. `../libbsdl`, pulled in via `add_subdirectory` and linked dynamically.
Override its location with `-DBSDL_DIR=/path/to/libbsdl`. Powers the Override its location with `-DBSDL_DIR=/path/to/libbsdl`. Powers the
`attach-bsdl` command and the pin/JTAG checks. `attach-bsdl` command and the pin/JTAG checks.
- Fetched automatically at configure time via `FetchContent` (nothing to - Fetched automatically via `FetchContent` (nothing to install): **FTXUI**
install): **FTXUI** v6.1.9 and **doctest** v2.4.11. 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**. - 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 ## Tests
```sh ```sh
@@ -53,6 +64,9 @@ Step-by-step walkthroughs for both the batch and TUI workflows are in
ctest --test-dir build 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: Skip building tests entirely:
```sh ```sh
@@ -76,12 +90,17 @@ cmake --build build --target doc # needs doxygen + python3
## Project layout ## Project layout
``` ```
src/system/ domain model (Module/Part/Pin/Signal, Connection, Transform, …) src/
src/imports/ Mentor / Altium / ODS netlist importers core/ business logic, NO GUI toolkit (→ libessim_core)
src/tui/ FTXUI shell (commands, screens, completion, history) domain/ model (Module/Part/Pin/Signal, Connection, Transform…) + analyses
tests/ doctest suite imports/ Mentor / Altium / ODS netlist importers + ODS writer
doc/ api/ + user/ Markdown trees, Doxyfile.in, gen_api_md.py app/ use-case operations (export → CSV/ODS, …)
test/ sample netlists + system.essim bring-up script 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). Full layout & rationale in [`DESIGN.md`](DESIGN.md).

67
src/core/app/connect.cpp Normal file
View 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 &reg = 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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_

View File

@@ -1,4 +1,4 @@
#include "system/component_kind.hpp" #include "core/domain/component_kind.hpp"
#include <cctype> #include <cctype>
#include <string> #include <string>

View File

@@ -1,11 +1,11 @@
#include "system/nets.hpp" #include "core/domain/nets.hpp"
#include "system/connect.hpp" #include "core/domain/connect.hpp"
#include "system/modules.hpp" #include "core/domain/modules.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include "system/signals.hpp" #include "core/domain/signals.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include <queue> #include <queue>
#include <unordered_map> #include <unordered_map>

View File

@@ -1,4 +1,4 @@
#include "system/pin_name.hpp" #include "core/domain/pin_name.hpp"
#include <cstdio> #include <cstdio>
#include <stdexcept> #include <stdexcept>

View 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;
}

View File

@@ -1,7 +1,7 @@
#ifndef _SYSTEM_HPP_ #ifndef _SYSTEM_HPP_
#define _SYSTEM_HPP_ #define _SYSTEM_HPP_
#include "imports/import_base.hpp" #include "core/imports/import_base.hpp"
#pragma once #pragma once
class Modules; ///< Forward declaration of the Modules class. class Modules; ///< Forward declaration of the Modules class.

View File

@@ -1,8 +1,8 @@
#include "import_altium.hpp" #include "import_altium.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include "system/signals.hpp" #include "core/domain/signals.hpp"
#include <cctype> #include <cctype>
#include <string> #include <string>

View File

@@ -4,8 +4,8 @@
#include <string> #include <string>
#include <fstream> #include <fstream>
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/signals.hpp" #include "core/domain/signals.hpp"
/** /**
* @brief Base class for importing data from a file. * @brief Base class for importing data from a file.
@@ -27,11 +27,22 @@ public:
* *
* @param file_name Name of the file to be imported. * @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(); 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. * @brief Pure virtual method for parsing the file.
* *
@@ -53,9 +64,13 @@ public:
/** /**
* @brief Virtual destructor for ImportBase. * @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_ #endif // _IMPORT_BASE_HPP_

View File

@@ -1,6 +1,6 @@
#include "import_mentor.hpp" #include "import_mentor.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include <cctype> #include <cctype>
#include <vector> #include <vector>
@@ -43,16 +43,6 @@ enum class State
*/ */
ImportMentor::ImportMentor(string filename) : ImportBase(filename) {} 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. * @brief Parses the file to extract parts, pins, and signals.
* *

View File

@@ -10,7 +10,6 @@ class ImportMentor : public ImportBase
public: public:
ImportMentor(std::string filename); ImportMentor(std::string filename);
void parse(Signals *signals) override; void parse(Signals *signals) override;
~ImportMentor();
}; };
#endif // _IMPORT_MENTOR_HPP_ #endif // _IMPORT_MENTOR_HPP_

View File

@@ -1,8 +1,8 @@
#include "import_ods.hpp" #include "import_ods.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include "system/signals.hpp" #include "core/domain/signals.hpp"
#include <pugixml.hpp> #include <pugixml.hpp>
#include <zip.h> #include <zip.h>

View 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_

View File

@@ -1,4 +1,6 @@
#include "tui/tui.hpp" #include "frontends/frontend_main.hpp"
#include "frontends/frontend.hpp"
#include <fstream> #include <fstream>
#include <iostream> #include <iostream>
@@ -11,16 +13,17 @@ void print_usage(const char *prog) {
"usage: " << prog << " [--batch] [--source FILE] [--restore FILE]\n" "usage: " << prog << " [--batch] [--source FILE] [--restore FILE]\n"
" " << prog << " --commands-md [FILE]\n" " " << prog << " --commands-md [FILE]\n"
" " << prog << " --help\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" " --source FILE after boot, run FILE as an essim script\n"
" (one command per line; same as the `source`\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" " --restore FILE after boot, restore the system snapshot in\n"
" FILE (same as the `restore` command).\n" " FILE (same as the `restore` command).\n"
" Combine with --source to layer a script on\n" " Combine with --source to layer a script on\n"
" top of a restored snapshot.\n" " top of a restored snapshot.\n"
" --batch run --restore/--source, print the console\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" " --commands-md [FILE] dump the command registry as Markdown.\n"
" With FILE: write there. Without: stdout.\n" " With FILE: write there. Without: stdout.\n"
" (Used by `cmake --build build --target doc`.)\n" " (Used by `cmake --build build --target doc`.)\n"
@@ -29,7 +32,7 @@ void print_usage(const char *prog) {
} // namespace } // namespace
int main(int argc, char **argv) { int frontend_main(int argc, char **argv, Frontend &fe) {
std::string boot_restore; std::string boot_restore;
std::string boot_source; std::string boot_source;
bool batch = false; bool batch = false;
@@ -37,16 +40,15 @@ int main(int argc, char **argv) {
for (int i = 1; i < argc; ++i) { for (int i = 1; i < argc; ++i) {
std::string a = argv[i]; std::string a = argv[i];
if (a == "--commands-md") { if (a == "--commands-md") {
Tui tui;
if (i + 1 < argc) { if (i + 1 < argc) {
std::ofstream f(argv[++i]); std::ofstream f(argv[++i]);
if (!f) { if (!f) {
std::cerr << "essim: cannot open " << argv[i] << " for writing\n"; std::cerr << "essim: cannot open " << argv[i] << " for writing\n";
return 1; return 1;
} }
tui.DumpCommandsMd(f); fe.DumpCommandsMd(f);
} else { } else {
tui.DumpCommandsMd(std::cout); fe.DumpCommandsMd(std::cout);
} }
return 0; return 0;
} }
@@ -79,20 +81,19 @@ int main(int argc, char **argv) {
return 2; return 2;
} }
Tui tui;
// Order matters: a `--restore` brings up a snapshot, then `--source` // Order matters: a `--restore` brings up a snapshot, then `--source`
// can layer additional commands on top of it (useful e.g. for "load // 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"). // snapshot, then re-run a small script that adds a new card").
if (!boot_restore.empty()) tui.BootDispatch("restore " + boot_restore); if (!boot_restore.empty()) fe.BootDispatch("restore " + boot_restore);
if (!boot_source.empty()) tui.BootDispatch("source " + boot_source); if (!boot_source.empty()) fe.BootDispatch("source " + boot_source);
// Batch mode: the boot dispatch already ran synchronously (no screen yet), // Batch mode: the boot dispatch already ran synchronously (no event loop
// so the console output is complete. Print it and exit without the TUI. // yet), so the console output is complete. Print it and exit.
if (batch) { if (batch) {
tui.DumpOutput(std::cout); fe.DumpOutput(std::cout);
return 0; return 0;
} }
tui.Run(); fe.Run();
return 0; return 0;
} }

View 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_

View 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}")

View File

@@ -1,28 +1,28 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include "tui/tui_helpers.hpp" #include "frontends/tui/tui_helpers.hpp"
#include "system/analysis.hpp" #include "core/domain/analysis.hpp"
#include "system/connect.hpp" #include "core/domain/connect.hpp"
#include "system/modules.hpp" #include "core/domain/modules.hpp"
#include "system/nets.hpp" #include "core/domain/parts.hpp"
#include "system/parts.hpp" #include "core/domain/persist.hpp"
#include "system/persist.hpp" #include "core/domain/pin_role.hpp"
#include "system/pin_role.hpp" #include "core/domain/pin_model.hpp"
#include "system/pin_model.hpp" #include "core/domain/bsdl_model.hpp"
#include "system/bsdl_model.hpp" #include "core/domain/pins.hpp"
#include "system/bsdl_check.hpp" #include "core/domain/signals.hpp"
#include "system/pins.hpp" #include "core/domain/system.hpp"
#include "system/signals.hpp"
#include "system/system.hpp" #include "core/app/connect.hpp"
#include "system/transform.hpp" #include "core/app/load.hpp"
#include "system/transform_vpx.hpp" #include "core/app/verify.hpp"
#include "core/domain/transform_vpx.hpp" // ValidatePartForKind
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
#include <cstdlib> #include <cstdlib>
#include <exception> #include <exception>
#include <fstream> #include <fstream>
#include <unordered_set>
#include <utility> #include <utility>
void Tui::RegisterCommands() { void Tui::RegisterCommands() {
@@ -138,29 +138,23 @@ void Tui::RegisterCommands() {
{"import type [mentor|altium|ods]", Completion::None}}, {"import type [mentor|altium|ods]", Completion::None}},
[this](const std::vector<std::string> &args) { [this](const std::vector<std::string> &args) {
if (!sys) { Print("no system: run 'new' first."); return; } if (!sys) { Print("no system: run 'new' first."); return; }
std::string ls = ToLower(args[2]);
ImportType t; ImportType t;
if (ls == "mentor") t = ImportType::IMPORT_MENTOR; if (!app::import_type_from_name(args[2], t)) {
else if (ls == "altium") t = ImportType::IMPORT_ALTIUM; Print("unknown import type: " + args[2]); return;
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());
} }
// 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, /*prompt_for_missing=*/ true,
"load a module from a netlist / pinout file (mentor, altium, ods)", "load a module from a netlist / pinout file (mentor, altium, ods)",
@@ -228,104 +222,43 @@ void Tui::RegisterCommands() {
commands["verify"] = { {}, [this](auto &) { commands["verify"] = { {}, [this](auto &) {
if (!sys) { Print("no system: run 'new' first."); return; } if (!sys) { Print("no system: run 'new' first."); return; }
int checked = 0; app::VerifyReport r = app::verify(sys.get());
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).");
auto nets = compute_all_nets(sys.get()); for (const auto &m : r.role_mismatches)
int bridged = 0, inconsistent = 0; Print(" " + m.module + "/" + m.part + "/" + m.pin
for (const auto &n : nets) { + ": expected " + signal_type_name(m.expected)
if (n.members.size() < 2) continue; + ", got " + signal_type_name(m.actual)
++bridged; + " (signal: " + m.signal + ")");
SignalType dom; Print("verify: " + std::to_string(r.role_mismatches.size())
if (net_type_consistent(n, dom)) continue; + " local mismatch(es) over " + std::to_string(r.typed_pins)
++inconsistent; + " typed pin(s).");
for (const auto &ni : r.net_inconsistencies) {
std::string line = " net mixes Power and GndShield:"; std::string line = " net mixes Power and GndShield:";
for (const auto &mp : n.members) { for (const auto &mem : ni.members)
line += " " + mp.first->name + "/" + mp.second->name line += " " + mem.module + "/" + mem.signal
+ "(" + signal_type_name(mp.second->type) + ")"; + "(" + signal_type_name(mem.type) + ")";
}
Print(line); Print(line);
} }
Print("verify: " + std::to_string(inconsistent) + " inconsistent net(s) over " Print("verify: " + std::to_string(r.net_inconsistencies.size())
+ std::to_string(bridged) + " bridged net(s) (" + " inconsistent net(s) over " + std::to_string(r.bridged_nets)
+ std::to_string(nets.size()) + " total)."); + " bridged net(s) (" + std::to_string(r.total_nets) + " total).");
// Orphan pin report. A pin is "orphan" if it came out of import (or Print("verify: " + std::to_string(r.orphan_total())
// post-import drop) with no signal, and is still not bridged to a + " orphan pin(s) at import (" + std::to_string(r.orphan_imported)
// real signal via any Connection::pin_map. Use `nc-export` for the + " imported NC, " + std::to_string(r.orphan_dropped)
// per-pin list. + " dropped singleton).");
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).");
// Model-driven pin checks (drive contention / undriven net / NC-wired) // Each model-driven group: per-finding lines + a one-line summary.
// from the PinSpec direction/function populated by connector/BSDL models. auto render = [this](const std::vector<Anomaly> &v, const char *tail) {
auto pin_anoms = check_pin_specs(sys.get(), &nets); for (const auto &a : v)
for (const auto &a : pin_anoms) Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message);
Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message); Print("verify: " + std::to_string(v.size()) + tail);
Print("verify: " + std::to_string(pin_anoms.size()) };
+ " model-driven pin anomaly(ies)."); render(r.pin_anomalies, " model-driven pin anomaly(ies).");
render(r.jtag_anomalies, " JTAG chain anomaly(ies).");
// JTAG boundary-scan chain integrity (TAP pins → nets). render(r.conflict_anomalies, " source-conflict(s).");
auto jtag_anoms = check_jtag_chain(sys.get(), &nets); render(r.completeness_anomalies, " BSDL completeness issue(s).");
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).");
}, true, }, true,
"check pin roles, power/gnd net consistency, and (with BSDL) pin and JTAG checks" }; "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]); auto [p2, p2_alts] = resolve_part(m2, args[3]);
if (!p2) { report_ambiguous("part in " + m2->name, args[3], p2_alts); return; } if (!p2) { report_ambiguous("part in " + m2->name, args[3], p2_alts); return; }
auto &reg = TransformRegistry::get(); // Resolution above is arg-parsing (user text → objects); the wiring
Transform *t = reg.lookup(p1->connector_type, p2->connector_type); // itself — transform lookup, identity NC fill, Connection creation —
bool both_empty = p1->connector_type.empty() && p2->connector_type.empty(); // is app::connect_parts.
if (t == reg.identity()) { app::ConnectResult cr = app::connect_parts(sys.get(), m1, p1, m2, p2);
if (!both_empty) { if (cr.refused) { Print("connect refused: " + cr.error); return; }
Print("connect refused: no transform for types '" if (!cr.identity_info.empty()) {
+ (p1->connector_type.empty() ? "(none)" : p1->connector_type) Print("connect: " + cr.identity_info);
+ "' ↔ '" if (cr.nc_added > 0)
+ (p2->connector_type.empty() ? "(none)" : p2->connector_type) Print("connect: added " + std::to_string(cr.nc_added)
+ "'. Set matching types via 'set-connector-type' first."); + " NC pin(s) so both sides match");
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());
} }
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, /*prompt_for_missing=*/ false,
"connect a part across two modules (interactive screen if no args)", "connect a part across two modules (interactive screen if no args)",

View 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,
};
}

View File

@@ -1,5 +1,5 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include "tui/tui_helpers.hpp" #include "frontends/tui/tui_helpers.hpp"
#include <cctype> #include <cctype>
#include <cstdlib> #include <cstdlib>

View 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);
}

View File

@@ -1,15 +1,11 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include "tui/tui_helpers.hpp" #include "frontends/tui/tui_helpers.hpp"
#include "system/analysis.hpp" #include "core/app/verify.hpp"
#include "system/bsdl_check.hpp" #include "core/domain/analysis.hpp"
#include "system/connect.hpp" #include "core/domain/modules.hpp"
#include "system/modules.hpp" #include "core/domain/signals.hpp"
#include "system/nets.hpp" #include "core/domain/system.hpp"
#include "system/parts.hpp"
#include "system/pins.hpp"
#include "system/signals.hpp"
#include "system/system.hpp"
#include <ftxui/component/component.hpp> #include <ftxui/component/component.hpp>
#include <ftxui/component/component_options.hpp> #include <ftxui/component/component_options.hpp>
@@ -17,7 +13,6 @@
#include <algorithm> #include <algorithm>
#include <array> #include <array>
#include <unordered_set>
using namespace ftxui; using namespace ftxui;
@@ -57,41 +52,23 @@ Component Tui::BuildAnalyzeScreen() {
// connection), then structural anomalies from the analysis pass. // connection), then structural anomalies from the analysis pass.
analyze_issues.clear(); analyze_issues.clear();
int n_role_mismatches = 0, n_typed_pins = 0; // verify + structural anomalies. The verify passes (pin-role, net-mix,
for (auto &mkv : *sys->modules()) // orphans, model checks) come from the shared core op; the structural
for (auto &pkv : *mkv.second) { // anomalies (diff-pair/bus) come from analyze_system above.
Part *prt = pkv.second; app::VerifyReport vr = app::verify(sys.get());
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 + ")");
}
}
auto nets = compute_all_nets(sys.get()); for (const auto &m : vr.role_mismatches)
int n_bridged = 0, n_inconsistent = 0; analyze_issues.push_back(
for (const auto &n : nets) { "[pin-role] " + m.module + "/" + m.part + "/" + m.pin
if (n.members.size() < 2) continue; + ": expected " + signal_type_name(m.expected)
++n_bridged; + ", got " + signal_type_name(m.actual)
SignalType dom; + " (signal: " + m.signal + ")");
if (net_type_consistent(n, dom)) continue;
++n_inconsistent; for (const auto &ni : vr.net_inconsistencies) {
std::string line = "[net-mix] mixes Power and Gnd:"; std::string line = "[net-mix] mixes Power and Gnd:";
for (const auto &mp : n.members) for (const auto &mem : ni.members)
line += " " + mp.first->name + "/" + mp.second->name line += " " + mem.module + "/" + mem.signal
+ "(" + signal_type_name(mp.second->type) + ")"; + "(" + signal_type_name(mem.type) + ")";
analyze_issues.push_back(std::move(line)); analyze_issues.push_back(std::move(line));
} }
@@ -100,28 +77,25 @@ Component Tui::BuildAnalyzeScreen() {
+ anomaly_kind_name(a.kind) + "] " + anomaly_kind_name(a.kind) + "] "
+ a.message); + a.message);
// Model-driven checks (same as `verify`), reusing the nets above. // Model-driven checks (pin / JTAG / source-conflict / completeness).
std::vector<Anomaly> model_anoms; auto push_anoms = [this](const std::vector<Anomaly> &v) {
{ for (const auto &a : v)
auto a1 = check_pin_specs(sys.get(), &nets); analyze_issues.push_back(std::string("[")
auto a2 = check_jtag_chain(sys.get(), &nets); + anomaly_kind_name(a.kind) + "] "
auto a3 = check_source_conflicts(sys.get()); + a.message);
auto a4 = check_bsdl_completeness(sys.get()); };
model_anoms.insert(model_anoms.end(), a1.begin(), a1.end()); push_anoms(vr.pin_anomalies);
model_anoms.insert(model_anoms.end(), a2.begin(), a2.end()); push_anoms(vr.jtag_anomalies);
model_anoms.insert(model_anoms.end(), a3.begin(), a3.end()); push_anoms(vr.conflict_anomalies);
model_anoms.insert(model_anoms.end(), a4.begin(), a4.end()); push_anoms(vr.completeness_anomalies);
} int n_model = vr.model_total();
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();
if (analyze_issues.empty()) analyze_issues.push_back("(no issue found)"); if (analyze_issues.empty()) analyze_issues.push_back("(no issue found)");
if (analyze_issue_idx >= (int)analyze_issues.size()) if (analyze_issue_idx >= (int)analyze_issues.size())
analyze_issue_idx = (int)analyze_issues.size() - 1; 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::string issues_header = "Issues ("
+ std::to_string(n_role_mismatches + n_inconsistent + std::to_string(n_role_mismatches + n_inconsistent
+ (int)rep.anomalies.size() + n_model) + (int)rep.anomalies.size() + n_model)
@@ -215,26 +189,11 @@ Component Tui::BuildAnalyzeScreen() {
std::string(tag) + r.mod + "/" + r.sig + "" + reason); std::string(tag) + r.mod + "/" + r.sig + "" + reason);
} }
// NC orphan rollup — same filter as the verify pass. // NC orphan rollup — from the shared verify report.
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;
}
analyze_types.push_back( analyze_types.push_back(
"[NC] orphan pin(s): " + std::to_string(orph_imported + orph_dropped) "[NC] orphan pin(s): " + std::to_string(vr.orphan_total())
+ " (" + std::to_string(orph_imported) + " imported, " + " (" + std::to_string(vr.orphan_imported) + " imported, "
+ std::to_string(orph_dropped) + " dropped)"); + std::to_string(vr.orphan_dropped) + " dropped)");
if (analyze_type_idx >= (int)analyze_types.size()) if (analyze_type_idx >= (int)analyze_types.size())
analyze_type_idx = (int)analyze_types.size() - 1; analyze_type_idx = (int)analyze_types.size() - 1;

View File

@@ -1,4 +1,4 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include <ftxui/component/component.hpp> #include <ftxui/component/component.hpp>
#include <ftxui/component/event.hpp> #include <ftxui/component/event.hpp>

View File

@@ -1,11 +1,10 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include "tui/tui_helpers.hpp" #include "frontends/tui/tui_helpers.hpp"
#include "system/connect.hpp" #include "core/app/connect.hpp"
#include "system/modules.hpp" #include "core/domain/modules.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include "system/transform.hpp"
#include <ftxui/component/component.hpp> #include <ftxui/component/component.hpp>
#include <ftxui/component/component_options.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 *p1 = m1->get(connect_p1_list[connect_p1_idx]);
Part *p2 = m2->get(connect_p2_list[connect_p2_idx]); Part *p2 = m2->get(connect_p2_list[connect_p2_idx]);
auto &reg = TransformRegistry::get(); // Same wiring op as the `connect` command — see app::connect_parts.
Transform *t = reg.lookup(p1->connector_type, p2->connector_type); app::ConnectResult cr = app::connect_parts(sys.get(), m1, p1, m2, p2);
bool both_empty = p1->connector_type.empty() && p2->connector_type.empty(); if (cr.refused) {
if (t == reg.identity()) { Print("connect refused: " + cr.error);
if (!both_empty) { } else {
Print("connect refused: no transform for types '" if (!cr.identity_info.empty()) {
+ (p1->connector_type.empty() ? "(none)" : p1->connector_type) Print("connect: " + cr.identity_info);
+ "' ↔ '" if (cr.nc_added > 0)
+ (p2->connector_type.empty() ? "(none)" : p2->connector_type) Print("connect: added " + std::to_string(cr.nc_added)
+ "'. Set matching types via 'set-connector-type' first."); + " NC pin(s) so both sides match");
screen_idx = 0;
return;
}
std::string err = CheckIdentityCompatible(p1, p2);
if (!err.empty()) {
Print("connect refused: " + err);
screen_idx = 0;
return;
} }
if (cr.ok)
Print("connected: " + cr.connection_name
+ " via " + cr.transform_name
+ " (" + std::to_string(cr.wires) + " wires)");
else
Print(std::string("connect failed: ") + cr.error);
} }
auto pin_map = t->apply(p1, p2);
std::string conn_name = m1->name + "/" + p1->name
+ " <-> " + m2->name + "/" + p2->name;
Connection *c = new Connection(conn_name, m1, p1, m2, p2);
c->transform_name = t->name;
c->pin_map = std::move(pin_map);
sys->connections()->add(c);
Print("connected: " + conn_name
+ " via " + t->name
+ " (" + std::to_string(c->pin_map.size()) + " wires)");
} catch (const std::exception &e) { } catch (const std::exception &e) {
Print(std::string("connect failed: ") + e.what()); Print(std::string("connect failed: ") + e.what());
} }

View File

@@ -1,22 +1,19 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include "tui/tui_helpers.hpp" #include "frontends/tui/tui_helpers.hpp"
#include "system/analysis.hpp" #include "core/app/verify.hpp"
#include "system/bsdl_check.hpp" #include "core/domain/analysis.hpp"
#include "system/connect.hpp" #include "core/domain/connect.hpp"
#include "system/modules.hpp" #include "core/domain/modules.hpp"
#include "system/nets.hpp" #include "core/domain/parts.hpp"
#include "system/parts.hpp" #include "core/domain/signals.hpp"
#include "system/pins.hpp" #include "core/domain/system.hpp"
#include "system/signals.hpp"
#include "system/system.hpp"
#include <ftxui/component/component.hpp> #include <ftxui/component/component.hpp>
#include <ftxui/dom/elements.hpp> #include <ftxui/dom/elements.hpp>
#include <algorithm> #include <algorithm>
#include <map> #include <map>
#include <unordered_set>
#include <vector> #include <vector>
using namespace ftxui; using namespace ftxui;
@@ -77,58 +74,23 @@ Component Tui::BuildDashboardScreen() {
} }
int n_conn = (int)sys->connections()->size(); int n_conn = (int)sys->connections()->size();
// ---- verify-style health (recomputed; cheap on realistic sizes) ---- // ---- verify-style health (shared core op; cheap on realistic sizes) ----
int n_role_mismatches = 0, n_typed_pins = 0; app::VerifyReport vr = app::verify(sys.get());
for (auto &mkv : *sys->modules()) int n_role_mismatches = (int)vr.role_mismatches.size();
for (auto &pkv : *mkv.second) { int n_typed_pins = vr.typed_pins;
Part *prt = pkv.second; int n_inconsistent = (int)vr.net_inconsistencies.size();
if (prt->connector_type.empty()) continue; int n_bridged = vr.bridged_nets;
for (auto &nkv : *prt) { int orph_imported = vr.orphan_imported;
Pin *pin = nkv.second; int orph_dropped = vr.orphan_dropped;
++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;
}
}
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 // 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 // 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 // deleted by `drop_singleton_signals`), but the pin's full path is
// enough to locate it in `explore`. // enough to locate it in `explore`.
std::map<std::string, std::vector<std::string>> dropped_by_module; std::map<std::string, std::vector<std::string>> dropped_by_module;
for (auto &mkv : *sys->modules()) for (const auto &o : vr.orphans)
for (auto &pkv : *mkv.second) if (o.dropped)
for (auto &nkv : *pkv.second) { dropped_by_module[o.module].push_back(o.part + "/" + o.pin);
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);
}
}
auto health_line = [](bool ok, const std::string &s) { auto health_line = [](bool ok, const std::string &s) {
return hbox({ return hbox({
@@ -144,7 +106,7 @@ Component Tui::BuildDashboardScreen() {
+ " typed pin(s)")); + " typed pin(s)"));
health_rows.push_back(health_line(n_inconsistent == 0, health_rows.push_back(health_line(n_inconsistent == 0,
"nets: " + std::to_string(n_inconsistent) + " inconsistent over " "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)")); + " total)"));
int orph_total = orph_imported + orph_dropped; int orph_total = orph_imported + orph_dropped;
health_rows.push_back(health_line(orph_total == 0, 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), // Model-driven checks (BSDL pin specs, JTAG chain, source conflicts,
// reusing the nets computed above. // completeness) — from the shared verify report.
int n_model = (int)(check_pin_specs(sys.get(), &nets).size() int n_model = vr.model_total();
+ check_jtag_chain(sys.get(), &nets).size()
+ check_source_conflicts(sys.get()).size()
+ check_bsdl_completeness(sys.get()).size());
health_rows.push_back(health_line(n_model == 0, health_rows.push_back(health_line(n_model == 0,
"model: " + std::to_string(n_model) + " BSDL/JTAG anomaly(ies)")); "model: " + std::to_string(n_model) + " BSDL/JTAG anomaly(ies)"));

View File

@@ -1,4 +1,4 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include <ftxui/component/component.hpp> #include <ftxui/component/component.hpp>
#include <ftxui/component/event.hpp> #include <ftxui/component/event.hpp>

View File

@@ -1,13 +1,13 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include "tui/tui_helpers.hpp" #include "frontends/tui/tui_helpers.hpp"
#include "system/connect.hpp" #include "core/domain/connect.hpp"
#include "system/modules.hpp" #include "core/domain/modules.hpp"
#include "system/nets.hpp" #include "core/domain/nets.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include "system/signals.hpp" #include "core/domain/signals.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include <ftxui/component/component.hpp> #include <ftxui/component/component.hpp>
#include <ftxui/component/component_options.hpp> #include <ftxui/component/component_options.hpp>

View File

@@ -1,5 +1,5 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include "tui/tui_helpers.hpp" #include "frontends/tui/tui_helpers.hpp"
#include <ftxui/component/component.hpp> #include <ftxui/component/component.hpp>
#include <ftxui/component/component_options.hpp> #include <ftxui/component/component_options.hpp>

View File

@@ -1,5 +1,5 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include "tui/tui_helpers.hpp" #include "frontends/tui/tui_helpers.hpp"
#include <ftxui/component/component.hpp> #include <ftxui/component/component.hpp>
#include <ftxui/component/component_options.hpp> #include <ftxui/component/component_options.hpp>

View File

@@ -1,5 +1,5 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include "tui/tui_helpers.hpp" #include "frontends/tui/tui_helpers.hpp"
#include <ftxui/component/component.hpp> #include <ftxui/component/component.hpp>
#include <ftxui/component/component_options.hpp> #include <ftxui/component/component_options.hpp>

View File

@@ -1,9 +1,9 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include "tui/tui_helpers.hpp" #include "frontends/tui/tui_helpers.hpp"
#include "system/modules.hpp" #include "core/domain/modules.hpp"
#include "system/signals.hpp" #include "core/domain/signals.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include <ftxui/component/component.hpp> #include <ftxui/component/component.hpp>
#include <ftxui/component/component_options.hpp> #include <ftxui/component/component_options.hpp>

View File

@@ -1,13 +1,13 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include "tui/tui_helpers.hpp" #include "frontends/tui/tui_helpers.hpp"
#include "system/modules.hpp" #include "core/domain/modules.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/pin_model.hpp" #include "core/domain/pin_model.hpp"
#include "system/pin_role.hpp" #include "core/domain/pin_role.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include "system/transform_vpx.hpp" #include "core/domain/transform_vpx.hpp"
#include <ftxui/component/component.hpp> #include <ftxui/component/component.hpp>
#include <ftxui/component/component_options.hpp> #include <ftxui/component/component_options.hpp>

View File

@@ -1,8 +1,8 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include "system/modules.hpp" #include "core/domain/modules.hpp"
#include "system/signals.hpp" #include "core/domain/signals.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include <ftxui/component/component.hpp> #include <ftxui/component/component.hpp>
#include <ftxui/component/component_options.hpp> #include <ftxui/component/component_options.hpp>

View File

@@ -1,9 +1,9 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include "tui/tui_helpers.hpp" #include "frontends/tui/tui_helpers.hpp"
#include "system/modules.hpp" #include "core/domain/modules.hpp"
#include "system/signals.hpp" #include "core/domain/signals.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include <cctype> #include <cctype>
#include <chrono> #include <chrono>

View File

@@ -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/component.hpp>
#include <ftxui/component/event.hpp> #include <ftxui/component/event.hpp>

View File

@@ -13,9 +13,11 @@
#include <ftxui/component/component.hpp> #include <ftxui/component/component.hpp>
#include <ftxui/component/screen_interactive.hpp> #include <ftxui/component/screen_interactive.hpp>
#include "frontends/frontend.hpp"
class System; class System;
class Tui { class Tui : public Frontend {
enum class Completion { None, Path, Command }; enum class Completion { None, Path, Command };
struct Prompt { struct Prompt {
@@ -198,16 +200,16 @@ private:
public: public:
Tui(); Tui();
~Tui(); ~Tui();
void Run(); void Run() override;
void DumpCommandsMd(std::ostream &out) const; void DumpCommandsMd(std::ostream &out) const override;
// Write the accumulated console output to `out`. Used by batch mode to // Write the accumulated console output to `out`. Used by batch mode to
// surface a script's output without starting the TUI. // 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 // Boot-time hook: dispatch a single command exactly as if the user
// typed it (e.g. `restore foo.essim` or `source bring-up.essim`). // typed it (e.g. `restore foo.essim` or `source bring-up.essim`).
// Call before `Run()` to seed the system before the event loop starts. // 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: private:
// Lifecycle (commands.cpp) // Lifecycle (commands.cpp)

View File

@@ -1,4 +1,4 @@
#include "tui/tui_helpers.hpp" #include "frontends/tui/tui_helpers.hpp"
#include <ftxui/dom/elements.hpp> #include <ftxui/dom/elements.hpp>

View File

@@ -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);
}

View File

@@ -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,
};
}

View File

@@ -1,11 +1,11 @@
#include <doctest/doctest.h> #include <doctest/doctest.h>
#include "system/analysis.hpp" #include "core/domain/analysis.hpp"
#include "system/modules.hpp" #include "core/domain/modules.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include "system/signals.hpp" #include "core/domain/signals.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include <memory> #include <memory>
#include <string> #include <string>

View File

@@ -1,14 +1,14 @@
#include <doctest/doctest.h> #include <doctest/doctest.h>
#include "system/analysis.hpp" #include "core/domain/analysis.hpp"
#include "system/bsdl_check.hpp" #include "core/domain/bsdl_check.hpp"
#include "system/bsdl_model.hpp" #include "core/domain/bsdl_model.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include "system/pin_spec.hpp" #include "core/domain/pin_spec.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include "system/modules.hpp" #include "core/domain/modules.hpp"
#include "system/persist.hpp" #include "core/domain/persist.hpp"
#include <cstdio> #include <cstdio>
#include <fstream> #include <fstream>

View File

@@ -1,13 +1,13 @@
#include <doctest/doctest.h> #include <doctest/doctest.h>
#include "system/analysis.hpp" #include "core/domain/analysis.hpp"
#include "system/bsdl_check.hpp" #include "core/domain/bsdl_check.hpp"
#include "system/modules.hpp" #include "core/domain/modules.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/pin_spec.hpp" #include "core/domain/pin_spec.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include "system/signals.hpp" #include "core/domain/signals.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include <algorithm> #include <algorithm>
#include <vector> #include <vector>

View File

@@ -1,7 +1,7 @@
#include <doctest/doctest.h> #include <doctest/doctest.h>
#include "system/component_kind.hpp" #include "core/domain/component_kind.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include <memory> #include <memory>

79
tests/test_connect.cpp Normal file
View 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
View 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
View 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);
}

View File

@@ -1,11 +1,11 @@
#include <doctest/doctest.h> #include <doctest/doctest.h>
#include "system/modules.hpp" #include "core/domain/modules.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/persist.hpp" #include "core/domain/persist.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include "system/signals.hpp" #include "core/domain/signals.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include <filesystem> #include <filesystem>
#include <memory> #include <memory>

View File

@@ -1,12 +1,12 @@
#include <doctest/doctest.h> #include <doctest/doctest.h>
#include "system/connect.hpp" #include "core/domain/connect.hpp"
#include "system/modules.hpp" #include "core/domain/modules.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/persist.hpp" #include "core/domain/persist.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include "system/signals.hpp" #include "core/domain/signals.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include <filesystem> #include <filesystem>
#include <memory> #include <memory>

View File

@@ -1,9 +1,9 @@
#include <doctest/doctest.h> #include <doctest/doctest.h>
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/pin_model.hpp" #include "core/domain/pin_model.hpp"
#include "system/pin_spec.hpp" #include "core/domain/pin_spec.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include <string> #include <string>
#include <vector> #include <vector>

View File

@@ -1,9 +1,9 @@
#include <doctest/doctest.h> #include <doctest/doctest.h>
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/pin_name.hpp" #include "core/domain/pin_name.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include "system/transform.hpp" #include "core/domain/transform.hpp"
#include <memory> #include <memory>

View File

@@ -1,6 +1,6 @@
#include <doctest/doctest.h> #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") { TEST_CASE("signal_type_name round-trips with from_name") {
SignalType t; SignalType t;

95
tests/test_verify.cpp Normal file
View 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);
}

View File

@@ -1,12 +1,12 @@
#include <doctest/doctest.h> #include <doctest/doctest.h>
#include "system/modules.hpp" #include "core/domain/modules.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include "system/signals.hpp" #include "core/domain/signals.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include "system/transform.hpp" #include "core/domain/transform.hpp"
#include "system/transform_vpx.hpp" #include "core/domain/transform_vpx.hpp"
#include <map> #include <map>
#include <memory> #include <memory>

View File

@@ -1,6 +1,6 @@
#include <doctest/doctest.h> #include <doctest/doctest.h>
#include "tui/tui_helpers.hpp" #include "frontends/tui/tui_helpers.hpp"
#include <algorithm> #include <algorithm>
#include <vector> #include <vector>