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)
set(FTXUI_BUILD_DOCS OFF CACHE INTERNAL "")
set(FTXUI_BUILD_EXAMPLES OFF CACHE INTERNAL "")
set(FTXUI_BUILD_TESTS OFF CACHE INTERNAL "")
set(FTXUI_ENABLE_INSTALL OFF CACHE INTERNAL "")
FetchContent_Declare(ftxui
GIT_REPOSITORY https://github.com/ArthurSonzogni/FTXUI.git
GIT_TAG v6.1.9
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(ftxui)
find_package(libzip REQUIRED)
find_package(pugixml REQUIRED)
# libbsdl — standalone BSDL parser (LGPL-2.1), dynamically linked from essim
# (EUPL-1.2, which the LGPL permits). Path overridable via -DBSDL_DIR=...;
# its CLI and tests are not needed inside essim's build.
# ----------------------------------------------------------------- core deps
# libbsdl — standalone BSDL parser (LGPL-2.1), dynamically linked (EUPL-1.2,
# which the LGPL permits). Override its path with -DBSDL_DIR=...
set(BSDL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../libbsdl" CACHE PATH "libbsdl source tree")
set(BSDL_BUILD_CLI OFF CACHE BOOL "" FORCE)
set(BSDL_BUILD_TESTS OFF CACHE BOOL "" FORCE)
add_subdirectory(${BSDL_DIR} ${CMAKE_BINARY_DIR}/libbsdl)
# Library target = everything except main.cpp; reused by `essim` and `essim_tests`.
file(GLOB_RECURSE LIB_SOURCES "src/*.cpp")
list(REMOVE_ITEM LIB_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp")
find_package(libzip REQUIRED)
find_package(pugixml REQUIRED)
add_library(essim_lib STATIC ${LIB_SOURCES})
target_include_directories(essim_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_link_libraries(essim_lib
# =============================================================== essim_core
# All business logic — domain model, importers, application operations
# (src/core/{domain,imports,app}). Frontend-agnostic: it links NO GUI/TUI
# toolkit, so every frontend and the test suite share the exact same core.
file(GLOB_RECURSE CORE_SOURCES "src/core/*.cpp")
add_library(essim_core STATIC ${CORE_SOURCES})
target_include_directories(essim_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_link_libraries(essim_core
PUBLIC
ftxui::screen
ftxui::dom
ftxui::component
libzip::zip
pugixml::pugixml
bsdl::bsdl
)
add_executable(essim src/main.cpp)
target_link_libraries(essim PRIVATE essim_lib)
# =============================================================== frontend(s)
# Pick the GUI/TUI frontend to build the `essim` binary against. Each frontend
# is a self-contained src/frontends/<name>/ (own CMakeLists, GUI toolkit, and
# main.cpp) that links essim_core. "none" builds the core + tests only — no GUI
# toolkit is fetched. To add a frontend (e.g. a Qt GUI), create
# src/frontends/gui/ and configure with -DESSIM_FRONTEND=gui.
set(ESSIM_FRONTEND "tui" CACHE STRING
"Frontend to build: a directory name under src/frontends/, or 'none'")
set_property(CACHE ESSIM_FRONTEND PROPERTY STRINGS tui none)
# Tests
if(ESSIM_FRONTEND STREQUAL "none")
message(STATUS "essim: ESSIM_FRONTEND=none — core + tests only (no frontend, no GUI toolkit)")
elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/frontends/${ESSIM_FRONTEND}/CMakeLists.txt")
message(STATUS "essim: building frontend '${ESSIM_FRONTEND}'")
# Shared, GUI-toolkit-free frontend support: the abstract Frontend interface
# (header-only) and the frontend-agnostic launcher frontend_main(). Every
# frontend's main() links this and forwards argv to it.
add_library(essim_frontend STATIC
"${CMAKE_CURRENT_SOURCE_DIR}/src/frontends/frontend_main.cpp")
target_include_directories(essim_frontend PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src)
add_subdirectory(src/frontends/${ESSIM_FRONTEND})
else()
message(FATAL_ERROR
"Unknown ESSIM_FRONTEND '${ESSIM_FRONTEND}' — expected "
"src/frontends/${ESSIM_FRONTEND}/CMakeLists.txt, or 'none'.")
endif()
# =============================================================== tests (core)
# The suite exercises essim_core only — no frontend, no GUI toolkit.
include(CTest)
if(BUILD_TESTING)
set(CMAKE_POLICY_VERSION_MINIMUM 3.5)
@@ -65,20 +76,35 @@ if(BUILD_TESTING)
FetchContent_MakeAvailable(doctest)
unset(CMAKE_POLICY_VERSION_MINIMUM)
# Core tests — exercise essim_core only (tests/*.cpp, non-recursive, so the
# per-frontend tests under tests/<frontend>/ are not pulled in here).
file(GLOB TEST_SOURCES "tests/*.cpp")
if(TEST_SOURCES)
add_executable(essim_tests ${TEST_SOURCES})
target_link_libraries(essim_tests PRIVATE essim_lib doctest::doctest)
target_link_libraries(essim_tests PRIVATE essim_core doctest::doctest)
add_test(NAME essim_tests COMMAND essim_tests)
endif()
# Per-frontend tests — tests/<frontend>/*.cpp, built and linked against that
# frontend's library only when the frontend itself is built.
if(TARGET essim_tui)
file(GLOB TUI_TEST_SOURCES "tests/tui/*.cpp")
if(TUI_TEST_SOURCES)
add_executable(essim_tui_tests
"${CMAKE_CURRENT_SOURCE_DIR}/tests/doctest_main.cpp" ${TUI_TEST_SOURCES})
target_link_libraries(essim_tui_tests PRIVATE essim_tui doctest::doctest)
add_test(NAME essim_tui_tests COMMAND essim_tui_tests)
endif()
endif()
endif()
# Documentation: Doxygen → XML → custom Python script → doc/api/ (Markdown rendered by gitea).
# Optional — `doc` target is only created if Doxygen and Python 3 are present.
# =============================================================== documentation
# Doxygen → XML → gen_api_md.py → doc/api/, plus `essim --commands-md`. Needs the
# `essim` binary, so it's only wired when a frontend that provides one is built.
find_package(Doxygen COMPONENTS doxygen)
find_package(Python3 COMPONENTS Interpreter)
if(DOXYGEN_FOUND AND Python3_Interpreter_FOUND)
if(TARGET essim AND DOXYGEN_FOUND AND Python3_Interpreter_FOUND)
set(DOXYGEN_OUTPUT_DIR "${CMAKE_BINARY_DIR}/doc")
file(MAKE_DIRECTORY "${DOXYGEN_OUTPUT_DIR}")
configure_file(
@@ -103,11 +129,10 @@ if(DOXYGEN_FOUND AND Python3_Interpreter_FOUND)
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
COMMENT "Generating documentation (doxygen → gen_api_md.py → doc/api/, essim --commands-md → doc/user/commands.md)"
VERBATIM)
elseif(NOT DOXYGEN_FOUND AND NOT Python3_Interpreter_FOUND)
message(STATUS "doc: Doxygen and Python 3 not found — `doc` target disabled.")
elseif(NOT TARGET essim)
message(STATUS "doc: no `essim` binary (ESSIM_FRONTEND=none) — `doc` target disabled.")
elseif(NOT DOXYGEN_FOUND)
message(STATUS "doc: Doxygen not found — `doc` target disabled "
"(install via `pacman -S doxygen`).")
message(STATUS "doc: Doxygen not found — `doc` target disabled.")
else()
message(STATUS "doc: Python 3 interpreter not found — `doc` target disabled.")
endif()

183
DESIGN.md
View File

@@ -10,72 +10,111 @@ cmake --build build -j
./build/essim
```
- CMake **3.14+** required (uses `FetchContent_MakeAvailable`).
- FTXUI is fetched at configure time from GitHub (`v6.1.9`, shallow clone). First configure pays ~20 s for the clone; subsequent ones are cached in `build/_deps/`.
- **System dependencies** (resolved via `find_package`): `libzip` (target `libzip::zip`) and `pugixml` (target `pugixml::pugixml`). Used by the ODS importer. Arch: `pacman -S libzip pugixml`; Debian/Ubuntu: `libzip-dev libpugixml-dev`.
- **`libbsdl`** (standalone BSDL parser, LGPL-2.1) is the sibling repo at `../libbsdl`, pulled in via `add_subdirectory` (path overridable with `-DBSDL_DIR=...`) and linked dynamically (`bsdl::bsdl`; an LGPL `.so` is fine from EUPL essim). Powers the BSDL ingest behind `attach-bsdl`.
- Sources are collected with `file(GLOB_RECURSE ALL_SOURCES "src/*.cpp")`. **After adding a new `.cpp`, re-run `cmake -S . -B build`** — CMake does not re-glob automatically and link will fail with "undefined reference".
- CMake **3.14+** (uses `FetchContent_MakeAvailable`).
- **Layered build** (see *Architecture* below). `essim_core` is the
frontend-agnostic business library; a frontend under `src/frontends/<name>/`
links it and produces the `essim` binary. Choose it with
`-DESSIM_FRONTEND=<name>` (default `tui`). **`-DESSIM_FRONTEND=none` builds the
core + tests only — no GUI toolkit is fetched.**
- **Core system dependencies** (via `find_package`): `libzip` (`libzip::zip`)
and `pugixml` (`pugixml::pugixml`) for the ODS importer. Arch:
`pacman -S libzip pugixml`; Debian/Ubuntu: `libzip-dev libpugixml-dev`.
- **`libbsdl`** (standalone BSDL parser, LGPL-2.1) — sibling repo at
`../libbsdl`, `add_subdirectory` (override `-DBSDL_DIR=...`), linked
dynamically **into the core** (`bsdl::bsdl`).
- **FTXUI** is fetched by the **tui frontend only**
(`src/frontends/tui/CMakeLists.txt`), never by the core.
- Sources are globbed per layer: `src/core/*.cpp``essim_core`,
`src/frontends/<fe>/*.cpp` → that frontend's lib + the `essim` binary.
**After adding a `.cpp`, re-run `cmake -S . -B build`** — CMake doesn't re-glob.
- **Tests** are split: `essim_tests` links `essim_core` (no FTXUI) from
`tests/*.cpp`; per-frontend tests like `essim_tui_tests` link `essim_tui` from
`tests/<frontend>/*.cpp`.
- **Headless / batch**: `essim --batch --source FILE` runs a script and prints its console output to stdout, then exits without the TUI (good for CI / capturing `verify`). Also `--restore FILE` and `--commands-md [FILE]`. `BootDispatch` runs `--restore`/`--source` synchronously before the event loop (`Source` takes its headless drain branch when no screen is attached), so the console buffer is complete by the time `--batch` dumps it (`Tui::DumpOutput`).
## Layout
```
src/
main.cpp -- launches Tui
system/ -- domain model
syselmts.hpp SystemElement + SystemElementContainer<T> (templated, get/merge/iterate)
modules.{hpp,cpp} Module, Modules
parts.{hpp,cpp} Part (carries `kind` + `connector_type`), Parts
pins.{hpp,cpp} Pin (carries PinSpec `spec`), Pins
signals.{hpp,cpp} Signal, Signals
signal_type.hpp SignalType + helpers
pin_spec.hpp PinSpec (function/direction/pad/source) + SignalType mapping
component_kind.{hpp,cpp} ComponentKind enum + infer_component_kind(name)
pin_name.{hpp,cpp} canonical_pin_name(s) — zero-pad digit suffix to 3
connect.{hpp,cpp} Connection, Connections
transform.{hpp,cpp} Transform / IdentityTransform / TransformRegistry +
CheckIdentityCompatible + FillIdentityNCs
pin_role.{hpp,cpp} pin_role(kind, name) → PinSpec, pin_layout(kind),
FillPartFromLayout(part, kind)
pin_model.{hpp,cpp} PinModel + apply_model(Part*, model) + ConnectorModel
bsdl_model.{hpp,cpp} BsdlModel (libbsdl wrapper) + BsdlPinModel + apply_bsdl
bsdl_check.{hpp,cpp} check_pin_specs / check_jtag_chain → vector<Anomaly>
nets.{hpp,cpp} find_net / compute_all_nets / net_type_consistent
analysis.{hpp,cpp} analyze_system → AnalysisReport (diff pairs, buses, anomalies)
persist.{hpp,cpp} save / restore (tab-delimited)
system.{hpp,cpp} System: owns Modules + Connections, exposes Load()
imports/ -- adapters that populate or emit the domain
import_base.hpp ImportBase interface
import_mentor.{hpp,cpp} Mentor Graphics netlist parser
import_altium.{hpp,cpp} Altium netlist parser (`[ ]` parts, `( )` signals)
import_ods.{hpp,cpp} ODS spreadsheet pinout (libzip + pugixml)
ods_writer.{hpp,cpp} Minimal .ods writer (multi-sheet, string cells)
tui/ -- FTXUI shell, split by responsibility
tui.{hpp,cpp} Class Tui (state + Run() orchestrator + screen-mode event dispatcher)
tui_helpers.{hpp,cpp} Free helpers: ToLower, NaturalLess, LongestCommonPrefix
shell.cpp Print, Submit, Dispatch, Finalize, Tokenize, history persistence
completion.cpp CompleteCommand, CompletePath, CompleteInline
commands.cpp RegisterCommands (orchestrator + lifecycle / shell / topology commands)
commands_export.cpp RegisterExportCommands (export → CSV / ODS, file-dialog hook)
screen_main.cpp BuildMainScreen (visualisation area + bottom input)
screen_connect.cpp BuildConnectScreen + shared RefreshFilteredPartList helper
screen_settype.cpp BuildSettypeScreen
screen_explore.cpp BuildExploreScreen (browse modules → parts/signals/connections → details; not scriptable)
screen_dashboard.cpp BuildDashboardScreen (read-only system overview)
screen_analyze.cpp BuildAnalyzeScreen (anomalies / groups / power decisions)
screen_palette.cpp BuildPaletteModal (global Ctrl-P fuzzy launcher)
screen_filedialog.cpp BuildFileDialog + OpenFileDialog (reusable file picker)
screen_error.cpp BuildErrorModal + ShowError (centred error popup)
screen_help.cpp BuildHelpScreen (topic-driven feature reference)
screen_sigtype_modal.cpp BuildSignalTypeModal (popup attached to explore via Modal())
doc/classes.puml -- PlantUML class diagram
core/ -- business logic; NO GUI toolkit (builds libessim_core)
domain/ -- the model + read-only analyses
syselmts.hpp SystemElement + SystemElementContainer<T> (get/merge/iterate)
modules.{hpp,cpp} Module, Modules
parts.{hpp,cpp} Part (kind, connector_type, bsdl_path; PinSpec per pin)
pins.{hpp,cpp} Pin (carries PinSpec `spec`), Pins
signals.{hpp,cpp} Signal, Signals
signal_type.hpp SignalType + helpers
pin_spec.hpp PinSpec (function/direction/pad/source), mappings, spec_source_rank
component_kind.{hpp,cpp} ComponentKind + infer_component_kind(name)
pin_name.{hpp,cpp} canonical_pin_name (zero-pad digit suffix to 3)
connect.{hpp,cpp} Connection, Connections
transform*.{hpp,cpp} Transform / IdentityTransform / TransformRegistry, VPX transform
pin_role.{hpp,cpp} pin_role(kind,name) -> PinSpec, pin_layout, FillPartFromLayout
pin_model.{hpp,cpp} PinModel + apply_model(Part*, model) + ConnectorModel
bsdl_model.{hpp,cpp} BsdlModel (libbsdl wrapper) + BsdlPinModel + apply_bsdl
bsdl_check.{hpp,cpp} check_pin_specs / _jtag_chain / _source_conflicts / _bsdl_completeness
nets.{hpp,cpp} find_net / compute_all_nets / net_type_consistent
analysis.{hpp,cpp} analyze_system -> AnalysisReport (diff pairs, buses, anomalies)
persist.{hpp,cpp} save / restore (tab-delimited; `B` tag = bsdl_path)
system.{hpp,cpp} System: owns Modules + Connections, Load()
imports/ -- netlist / pinout adapters
import_base.hpp / import_{mentor,altium,ods}.{hpp,cpp} / ods_writer.{hpp,cpp}
app/ -- application operations (UI-independent use cases)
export.{hpp,cpp} export_connections(System*, path, format) -> ExportResult
verify.{hpp,cpp} verify(System*) -> VerifyReport (the 7 verify passes)
connect.{hpp,cpp} connect_parts(System*, m1,p1, m2,p2) -> ConnectResult
load.{hpp,cpp} load_module(System*, name, path, ImportType) -> LoadResult
frontends/ -- one directory per GUI/TUI engine; each links essim_core
frontend.hpp -- abstract Frontend interface (BootDispatch/Dump*/Run)
frontend_main.{hpp,cpp} -- frontend_main(argc,argv,Frontend&): argv + boot/batch/run
tui/ -- FTXUI shell (builds libessim_tui + the `essim` binary)
CMakeLists.txt fetches FTXUI; builds essim_tui + essim
main.cpp entry point: construct Tui, call frontend_main
tui.{hpp,cpp} class Tui (state + Run() + screen-mode event dispatch)
tui_helpers.{hpp,cpp} ToLower, NaturalLess, RenderHelpPanel
shell.cpp Print, Submit, Dispatch, Source / ProcessNextSourceLine
completion.cpp CompleteCommand / CompletePath / CompleteInline
commands.cpp RegisterCommands (thin: resolve args -> call core -> render)
commands_export.cpp thin wrapper over app::export_connections
screen_*.cpp dashboard, connect, settype, explore, analyze, help, main
(console), palette, file dialog, error/confirm, sigtype modal
tests/ -- core tests (link essim_core)
tui/ -- frontend tests (link essim_tui)
doc/ , test/ -- docs; sample netlists + system.essim bring-up script
```
`include/` and `lib/` are kept empty by design — FTXUI used to live there as precompiled `.a` + headers, now it comes through FetchContent.
## Architecture — core vs frontends
The hard rule: **`src/core/` never depends on a frontend** — no `#include
"frontends/…"`, no GUI toolkit. Frontends depend on the core, never the reverse
(`essim_core` links libzip / pugixml / bsdl only).
- **Domain** (`core/domain/`) — the model and the read-only analyses
(`analyze_system`, the `check_*` passes, `compute_all_nets`).
- **Application** (`core/app/`) — use-case operations a frontend invokes, e.g.
`export_connections(System*, path, format) -> ExportResult`. An operation
builds its artefact and returns data/stats; it **never** prints or opens a
dialog. (Anti-pattern being removed: the export command used to build the file
inside its lambda. The TUI command is now a thin wrapper — resolve args/dialog
→ call the core op → render the result.)
- **Frontends** (`frontends/<name>/`) — thin: map UI events to core calls and
render results. Each implements the **`Frontend`** interface
(`frontends/frontend.hpp`: `BootDispatch`, `DumpCommandsMd`, `DumpOutput`,
`Run`). The process entry is shared and frontend-agnostic:
`frontend_main(argc, argv, Frontend&)` (`frontends/frontend_main.cpp`, built
into the toolkit-free `essim_frontend` lib) parses the CLI flags and drives the
boot → batch/run flow through the interface; a frontend's `main()` is just
*construct the concrete Frontend, call `frontend_main`*. Add one by creating
`src/frontends/<name>/CMakeLists.txt` (build `essim_<name>` linking `essim_core`,
produce the `essim` binary linking `essim_frontend`) and configuring
`-DESSIM_FRONTEND=<name>`.
Because the core links no toolkit, the suite links `essim_core` directly and
`-DESSIM_FRONTEND=none` builds + tests the whole core with FTXUI never fetched.
## Domain conventions
- Everything in `system/` is **pointer-based** (commit `d8122d1`: "everything is pointer"). Containers store `T*`, ownership lives with the container.
- Everything in `core/domain/` is **pointer-based** (commit `d8122d1`: "everything is pointer"). Containers store `T*`, ownership lives with the container.
- `SystemElementContainer<T>::merge(name)` is the get-or-create primitive — call it instead of `add` when you don't know whether the element already exists. `add` throws on duplicate names or empty names.
- `using namespace std;` is present in `syselmts.hpp` — pre-existing, don't add more `using namespace` in headers.
- Include guards `_NAME_HPP_` *and* `#pragma once` are both used. Match the existing style.
@@ -110,11 +149,11 @@ Built-in commands: `new`, `set`, `load`, `duplicate`, `save`, `restore`, `source
Pending prompts (from incomplete inline commands) are NOT considered interactive and are filled by subsequent script lines, the way you'd expect. Lines starting with `#` and blank lines are skipped; leading/trailing whitespace is trimmed; `~/` is expanded.
`save` / `restore` (`src/system/persist.{hpp,cpp}`): tab-delimited line format, no extra deps. Tags: `M` (module), `P` (part with `connector_type`), `B` (part's attached BSDL `.bsd` path — re-parsed and re-applied on restore; the path is persisted, **not** the derived pin specs), `N` (pin → signal name; empty = NC; optional 4th field carries `nc_origin_tag()`: `U` = ImportedUnconnected, `D` = DroppedSingleton — omitted when the pin has a signal or when origin is `None`), `S` (signal → type override; only emitted for non-default), `C` (connection header with endpoints + `transform_name`), `W` (wire pair within the current connection). The 4th N field is backward-compatible: pre-existing snapshots without it restore with `nc_origin = None`.
`save` / `restore` (`src/core/domain/persist.{hpp,cpp}`): tab-delimited line format, no extra deps. Tags: `M` (module), `P` (part with `connector_type`), `B` (part's attached BSDL `.bsd` path — re-parsed and re-applied on restore; the path is persisted, **not** the derived pin specs), `N` (pin → signal name; empty = NC; optional 4th field carries `nc_origin_tag()`: `U` = ImportedUnconnected, `D` = DroppedSingleton — omitted when the pin has a signal or when origin is `None`), `S` (signal → type override; only emitted for non-default), `C` (connection header with endpoints + `transform_name`), `W` (wire pair within the current connection). The 4th N field is backward-compatible: pre-existing snapshots without it restore with `nc_origin = None`.
**Signals** carry a `type` (`SignalType::Power | GndShield | Other`). The `Signal` constructor **defaults to `Other`** — auto-inference no longer happens at construction. Types are set in three ways, in priority order:
1. **`infer_signal_types(System*)`** (`src/system/analysis.{hpp,cpp}`) runs at the end of every `load` (after `drop_singleton_signals`). It assigns:
1. **`infer_signal_types(System*)`** (`src/core/domain/analysis.{hpp,cpp}`) runs at the end of every `load` (after `drop_singleton_signals`). It assigns:
- `GndShield` when the **name alone** is unambiguous (`GND`, `SHIELD`, `CHASSIS`, `EARTH`, …) — false-positive rate is essentially zero on these.
- `Power` requires (a) the name heuristic (`infer_signal_type` says Power), (b) a **hard fan-out floor**: signals with fewer than `POWER_FANOUT_HARD_FLOOR = 3` pins are *always* refused, regardless of name or voltage pattern (a real rail physically cannot land on just 1-2 pads), and (c) at least one positive structural signal — fan-out ≥ `POWER_FANOUT_CONFIRM_MIN = 4` **or** a voltage pattern in the name (`3V3`, `5V`, `12V`, …; detector: a `V` adjacent to a digit). This catches `VSEL_*`, `PWR_OK`, `_VDD_SENSE` etc. which look like Power by name but aren't real rails. Both thresholds are exposed in `analysis.hpp` so the analyze screen can render the same reasoning without duplicating constants.
- `Other` otherwise. The "name-said-Power-but-refuted-by-structure" count is reported by `load`.
@@ -123,17 +162,17 @@ Pending prompts (from incomplete inline commands) are NOT considered interactive
The explore screen shows the type in the signal detail header.
**Pin spec (expected attributes)**: every Pin carries a `PinSpec spec` (`src/system/pin_spec.hpp`) — the *expected* half of verification, set from a model: `function` (Power/Ground/Signal/Clock/NoConnect/Jtag*), `direction` (In/Out/Bidir/Passive/Power), `pad` (physical package terminal, e.g. a BSDL ball), and `source` (which model wrote it). `set-connector-type` populates it via `pin_role(connector_type, pin_name) → PinSpec`. `Pin::expected_signal_type()` is now a **derived accessor**`to_signal_type(spec.function)` (Power→Power, Ground→GndShield, else Other) — not a stored field; the *observed* half stays `Pin::signal()` + the net + inference, and `verify` diffs the two. The VPX 3U connector lookup (`vpx_3u_role`) is still a stub returning Other, so connector-typed pins resolve to function Unknown until that table is filled. **`direction`/`function`/`pad` are populated from BSDL** via `attach-bsdl` (see below) and consumed by the model-driven checks (`check_pin_specs`: contention / undriven / NC-wired) and the JTAG chain check (`check_jtag_chain`), both run by `verify`.
**Pin spec (expected attributes)**: every Pin carries a `PinSpec spec` (`src/core/domain/pin_spec.hpp`) — the *expected* half of verification, set from a model: `function` (Power/Ground/Signal/Clock/NoConnect/Jtag*), `direction` (In/Out/Bidir/Passive/Power), `pad` (physical package terminal, e.g. a BSDL ball), and `source` (which model wrote it). `set-connector-type` populates it via `pin_role(connector_type, pin_name) → PinSpec`. `Pin::expected_signal_type()` is now a **derived accessor**`to_signal_type(spec.function)` (Power→Power, Ground→GndShield, else Other) — not a stored field; the *observed* half stays `Pin::signal()` + the net + inference, and `verify` diffs the two. The VPX 3U connector lookup (`vpx_3u_role`) is still a stub returning Other, so connector-typed pins resolve to function Unknown until that table is filled. **`direction`/`function`/`pad` are populated from BSDL** via `attach-bsdl` (see below) and consumed by the model-driven checks (`check_pin_specs`: contention / undriven / NC-wired) and the JTAG chain check (`check_jtag_chain`), both run by `verify`.
**Connector pin layout (preparation)**: `pin_layout(connector_type)` returns the canonical full pin-name list for a known connector kind, and `FillPartFromLayout(part, kind)` materialises NC pins for any layout position absent from the imported netlist. `set-connector-type` calls it after setting `connector_type` (no-op today since `pin_layout` is a stub returning `{}` for everything — populate alongside `vpx_3u_role`). End-to-end chain in place: `set-connector-type → FillPartFromLayout → pin_role`.
**BSDL models (`attach-bsdl`)**: `attach-bsdl <module> <part> <file.bsd>` parses a BSDL device through `libbsdl` (wrapped by `BsdlModel`, `src/system/bsdl_model.{hpp,cpp}`), then `apply_bsdl(part, model)` binds each port to a Pin **by port name first, then by physical pad** — so a netlist that names IC pins either by signal or by package ball both bind. Each bound pin gets its `spec` set: `direction` (BSDL in/out/inout/linkage), `function` (TAP role → Jtag\*, `linkage` → Power/Ground/NoConnect by name, else Signal), `pad` (PIN_MAP ball), `source = Bsdl`. The `.bsd` path is stored on `Part::bsdl_path`, persisted via the `B` tag and re-applied on `restore`. Real-world check: an `xcku15p` FPGA in a VPX system binds 1517/1517 ports.
**BSDL models (`attach-bsdl`)**: `attach-bsdl <module> <part> <file.bsd>` parses a BSDL device through `libbsdl` (wrapped by `BsdlModel`, `src/core/domain/bsdl_model.{hpp,cpp}`), then `apply_bsdl(part, model)` binds each port to a Pin **by port name first, then by physical pad** — so a netlist that names IC pins either by signal or by package ball both bind. Each bound pin gets its `spec` set: `direction` (BSDL in/out/inout/linkage), `function` (TAP role → Jtag\*, `linkage` → Power/Ground/NoConnect by name, else Signal), `pad` (PIN_MAP ball), `source = Bsdl`. The `.bsd` path is stored on `Part::bsdl_path`, persisted via the `B` tag and re-applied on `restore`. Real-world check: an `xcku15p` FPGA in a VPX system binds 1517/1517 ports.
**Unified application (`apply_model`)**: connector layout and BSDL are two implementations of one `PinModel` interface (`src/system/pin_model.{hpp,cpp}`: `spec_for(pin_name)`, `layout()`, `source()`). `ConnectorModel` wraps `pin_role`/`pin_layout`; `BsdlPinModel` wraps a parsed `BsdlModel`, indexed by both port name and physical pad. A single `apply_model(Part*, const PinModel&)` materialises the layout pins missing from the netlist, then sets each pin's `spec` **only where the model speaks** (`spec.source != None`). Sources are ranked (`spec_source_rank` in `pin_spec.hpp`: UserOverride > Bsdl > ConnectorModel > Inferred > Imported) and apply_model refuses to overwrite a spec owned by a higher-rank source — so one source never clobbers a more authoritative one, which is also the basis for `check_source_conflicts`. `set-connector-type` and `attach-bsdl` both funnel through it (the latter via the thin `apply_bsdl` adapter); `verify` stays agnostic of where a spec came from. A future SPICE/Modelica source would be a third `PinModel`.
**Unified application (`apply_model`)**: connector layout and BSDL are two implementations of one `PinModel` interface (`src/core/domain/pin_model.{hpp,cpp}`: `spec_for(pin_name)`, `layout()`, `source()`). `ConnectorModel` wraps `pin_role`/`pin_layout`; `BsdlPinModel` wraps a parsed `BsdlModel`, indexed by both port name and physical pad. A single `apply_model(Part*, const PinModel&)` materialises the layout pins missing from the netlist, then sets each pin's `spec` **only where the model speaks** (`spec.source != None`). Sources are ranked (`spec_source_rank` in `pin_spec.hpp`: UserOverride > Bsdl > ConnectorModel > Inferred > Imported) and apply_model refuses to overwrite a spec owned by a higher-rank source — so one source never clobbers a more authoritative one, which is also the basis for `check_source_conflicts`. `set-connector-type` and `attach-bsdl` both funnel through it (the latter via the thin `apply_bsdl` adapter); `verify` stays agnostic of where a spec came from. A future SPICE/Modelica source would be a third `PinModel`.
**`verify` (seven passes)**: (1) typed pins — local mismatch between each pin's `expected_signal_type()` (derived from its `PinSpec`) and the actual signal type; (2) bridged nets — Power↔GndShield inconsistencies; (3) orphan summary `N orphan pin(s) at import (X imported NC, Y dropped singleton)` (filters out pins bridged via any `Connection::pin_map` — typically `FillIdentityNCs`-materialised); (4) **model-driven pin checks** (`check_pin_specs`): `DriveContention` (≥2 push-pull `Out` on a net), `UndrivenNet` (a **fully-modelled** net with input(s) but no driver — nets with any Unknown-direction pin are skipped, so un-modelled drivers don't cause false positives), `NcWired` (a no-connect pin on a multi-pin net); (5) **JTAG chain** (`check_jtag_chain`): collects TAP pins by `spec.function`, maps each to its net, emits `JtagTapIncomplete` / `JtagBusUnbridged` (TMS or TCK not common to all TAP devices) / `JtagChainBreak` (dangling TDO/TDI, chain fan-out, or not a single head→tail daisy chain); (6) **source conflicts** (`check_source_conflicts`): a pin the BSDL declares power/ground (a must-connect rail) that the netlist leaves unconnected — a rail floated in the schematic (`SourceConflict`; the reverse, a BSDL no-connect that *is* wired, is the `NcWired` check); (7) **BSDL completeness** (`check_bsdl_completeness`): device power/ground ports (from the attached `.bsd`, re-parsed) with no matching pin on the netlist part — a rail the schematic symbol is missing (`BsdlPinMissing`, one aggregated finding per part). The BFS-reached `(module, signal)` set for any signal is shown live in `explore`'s detail pane when a signal entry is selected.
**`analyze` (post-processing pass)**: `analyze_system(System*) → AnalysisReport` (`src/system/analysis.{hpp,cpp}`) is a stateless read-only pass that detects structural signal groups and anomalies. Per-module (signals are module-scoped):
**`analyze` (post-processing pass)**: `analyze_system(System*) → AnalysisReport` (`src/core/domain/analysis.{hpp,cpp}`) is a stateless read-only pass that detects structural signal groups and anomalies. Per-module (signals are module-scoped):
- **Diff pairs**: signal names ending `_P` / `_N` (case-insensitive) grouped by stem. Both halves present → candidate matched pair.
- **Diff buses**: ≥ 2 matched diff pairs whose pair-stems share a common outer-stem after stripping a trailing integer (`MDI0` / `MDI1` / `MDI2` → outer `MDI` + indices). The strict `_` rule from plain buses does NOT apply to this trailing-index split: `_P`/`_N` was already stripped, so we know remaining digits are an index. Two index variants accepted: contiguous (`MDI0`) and underscore-separated (`PCIE_TX_0`). Emitted as `SignalGroup{kind=DiffBus, lo, hi}` with label `OUTER[lo..hi]_P/N`. Members include all 2·N constituent signals. A "bus" of size 1 falls back to `DiffPair` (single index does not a bus make).
@@ -145,13 +184,13 @@ Exposed as the `analyze` shell command which prints groups (sorted by module + l
**Component classification**: every `Part` carries a `ComponentKind kind` (`Passive | Semiconductor | IntegratedCircuit | Connector | TestPoint | Switch | Crystal | Mechanical | Other`) inferred at construction by `infer_component_kind(name)` from the leading reference-designator letter(s) (longest-match: `LED/TP/SW/FB/MK/MP/MH/HS/RA/RN/RP/RV` first, then single-letter R/C/L/F/D/Q/U/J/P/Y/X/S). Recomputed on `restore` (no persistence tag). Not yet exposed in TUI commands — branchpoints will be `set-connector-type` guard, `explore` filter, and `explore` header.
`SignalType` lives in its own header `src/system/signal_type.hpp` (extracted from signals to avoid a pins↔signals include cycle).
`SignalType` lives in its own header `src/core/domain/signal_type.hpp` (extracted from signals to avoid a pins↔signals include cycle).
**Pins** are either NC (`signal() == nullptr`) or connected to exactly one signal. The ODS importer creates a Pin for every row that has a non-empty pin name, even when the signal column is empty or `"NC"` — the pin stays in the Part as NC. `restore` replaces `Tui::sys` entirely (`unique_ptr::reset`). Names are stored as-is — must not contain TAB or newline (true for the EE netlists we ingest). Format is versioned by the `# essim system snapshot v1` header for future compatibility.
**NC origin tag**: each `Pin` carries `NcOrigin nc_origin` (`None | ImportedUnconnected | DroppedSingleton`, default `None`). Set in three places: (a) Mentor importer when the signal field starts with `unconnected``ImportedUnconnected`; (b) `drop_singleton_signals(Signals*)` called at the end of `load``DroppedSingleton` on each detached pin (signals with exactly one pin are NC by definition — see commits motivating this); (c) `duplicate` propagates the tag. Pins materialised by `FillIdentityNCs` keep `None` — they have no local signal but are bridged via `pin_map` and shouldn't be counted as orphans. The tag is persisted (see `N` record), reported as a total in `verify`, and tested in `tests/test_nc_origin.cpp`.
**Connector types & transforms**: every `Part` carries a `connector_type` string (default `""`, set via the `set-connector-type` command — inline `set-connector-type m p kind` or bare which opens a TUI screen with module menu, part filter+menu, type input, list of types already in use, and an Apply button). When `connect` validates a pair, it consults `TransformRegistry::lookup(p1->connector_type, p2->connector_type)` (defined in `src/system/transform.{hpp,cpp}`) — both directions of the pair are tried. If neither is registered, an `IdentityTransform` fallback wires each pin of A to the canonical-equivalent pin of B (when present). The resulting `(Pin*, Pin*)` list and the transform's name are stored on the `Connection` (`pin_map`, `transform_name`). To register a real transform: define a `Transform` subclass in `transform.cpp` and call `TransformRegistry::get().add("kindA", "kindB", new MyTransform())` at init — there's no startup hook for this yet, so a small `RegisterBuiltinTransforms()` helper is the natural place to add when more types appear.
**Connector types & transforms**: every `Part` carries a `connector_type` string (default `""`, set via the `set-connector-type` command — inline `set-connector-type m p kind` or bare which opens a TUI screen with module menu, part filter+menu, type input, list of types already in use, and an Apply button). When `connect` validates a pair, it consults `TransformRegistry::lookup(p1->connector_type, p2->connector_type)` (defined in `src/core/domain/transform.{hpp,cpp}`) — both directions of the pair are tried. If neither is registered, an `IdentityTransform` fallback wires each pin of A to the canonical-equivalent pin of B (when present). The resulting `(Pin*, Pin*)` list and the transform's name are stored on the `Connection` (`pin_map`, `transform_name`). To register a real transform: define a `Transform` subclass in `transform.cpp` and call `TransformRegistry::get().add("kindA", "kindB", new MyTransform())` at init — there's no startup hook for this yet, so a small `RegisterBuiltinTransforms()` helper is the natural place to add when more types appear.
**Identity wiring uses canonical names**: `IdentityTransform::apply` builds `unordered_map<canonical, Pin*>` for side B and looks up each side-A pin by its canonical form. So `A1` (one card) auto-pairs with `A001` (the other) thanks to `canonical_pin_name` (`pre + zero-padded(3) digit suffix`; mixed/non-numeric returns the original). Same canonicalisation in `CheckIdentityCompatible`. **`pin_role` doesn't need canonicalisation** because `parse_pin` extracts `(col, row)` via `stoi` which already strips leading zeros.
@@ -188,7 +227,7 @@ Today the only caller is `export` (`{"CSV", ".csv"}, {"ODS", ".ods"}` filters, k
**Per-key path persistence** (`SaveLastUsed(key, dir, filename)` / `LoadLastUsed(key, &dir, &filename)` in `shell.cpp`): each key writes a tiny two-line file (`dir\nfilename\n`) under `UserDataDir() / <key>.last`. `UserDataDir()` is the cross-platform `XDG_DATA_HOME` / `LOCALAPPDATA` etc. helper also used by the command history file. Free functions, not Tui members, so any module (the file dialog today; could be the script-save buffer or the save command tomorrow) can use them with the same minimal API.
**ODS writer** (`src/imports/ods_writer.{hpp,cpp}`): minimal OpenDocument Spreadsheet writer. Backed by libzip + pugixml (both already pulled in by the ODS importer). Class hierarchy is two structs:
**ODS writer** (`src/core/imports/ods_writer.{hpp,cpp}`): minimal OpenDocument Spreadsheet writer. Backed by libzip + pugixml (both already pulled in by the ODS importer). Class hierarchy is two structs:
- `OdsSheet` — sparse row-major grid of string cells (`set(row, col, value)`).
- `OdsWriter` — owns the sheets, emits a valid `.ods` archive with `mimetype` (stored uncompressed, magic header), `META-INF/manifest.xml`, and `content.xml`.
@@ -248,9 +287,9 @@ Everything in this section is a precise description of how signals, pins, parts,
Default: `Signal::type = SignalType::Other` (constructor does no inference).
Type is set by `infer_signal_types(System*)` (`src/system/analysis.cpp`), called at the end of every `load` and after `duplicate`. The decision per signal:
Type is set by `infer_signal_types(System*)` (`src/core/domain/analysis.cpp`), called at the end of every `load` and after `duplicate`. The decision per signal:
1. Compute `named = infer_signal_type(name)` (`src/system/signals.cpp`):
1. Compute `named = infer_signal_type(name)` (`src/core/domain/signals.cpp`):
- `GndShield` if name matches **case-insensitively**: `GND`, `GROUND`, `EARTH`, `SHIELD`, `CHASSIS`, or starts with `GND_` / `GROUND_` / `EARTH_` / `SHIELD_` / `CHASSIS_`.
- `Power` if the name contains any of `PWR`, `POWER`, `VCC`, `VDD`, `VEE`, `VSS`, `VBAT`, or starts with `VS_`, `VS3_`, `+`, `-` followed by a digit.
- Else `Other`.
@@ -264,15 +303,15 @@ Type is set by `infer_signal_types(System*)` (`src/system/analysis.cpp`), called
### NC pin origin
`Pin::nc_origin` (`src/system/pins.hpp`). Default `NcOrigin::None`. Set by:
`Pin::nc_origin` (`src/core/domain/pins.hpp`). Default `NcOrigin::None`. Set by:
- **`NcOrigin::ImportedUnconnected`** — Mentor importer (`src/imports/import_mentor.cpp`): the signal field of an `Explicit Pin:` row starts case-insensitively with `unconnected` (e.g. `'unconnected'`, `'unconnected (by TERM)'`). The pin is kept on the part with no signal.
- **`NcOrigin::DroppedSingleton`** — `drop_singleton_signals(Signals*)` (`src/system/signals.cpp`) called at the end of `load`: any signal whose pin set has size exactly 1 is unconnected by definition. The pin is detached (`sig = nullptr`) and tagged; the `Signal` object is deleted.
- **`NcOrigin::ImportedUnconnected`** — Mentor importer (`src/core/imports/import_mentor.cpp`): the signal field of an `Explicit Pin:` row starts case-insensitively with `unconnected` (e.g. `'unconnected'`, `'unconnected (by TERM)'`). The pin is kept on the part with no signal.
- **`NcOrigin::DroppedSingleton`** — `drop_singleton_signals(Signals*)` (`src/core/domain/signals.cpp`) called at the end of `load`: any signal whose pin set has size exactly 1 is unconnected by definition. The pin is detached (`sig = nullptr`) and tagged; the `Signal` object is deleted.
- **`NcOrigin::None` (no tag)** — pins materialised by `FillIdentityNCs` at `connect` time. These are unconnected locally but bridged to a real signal on the peer module via `Connection::pin_map`; they are explicitly excluded from the "orphan" count in `verify` and the analyze screen.
### Signal groups
`analyze_system(System*)` (`src/system/analysis.cpp`) emits `SignalGroup`s **per module** (signals are module-scoped). Internal names (starting with `$` — Mentor's `$Nxxxx` convention) are skipped wholesale at every step.
`analyze_system(System*)` (`src/core/domain/analysis.cpp`) emits `SignalGroup`s **per module** (signals are module-scoped). Internal names (starting with `$` — Mentor's `$Nxxxx` convention) are skipped wholesale at every step.
**DiffPair** (`GroupKind::DiffPair`):
- Signal name ends `_P` or `_N` (case-insensitive). The character before the suffix must be `_`.
@@ -303,7 +342,7 @@ The `verify` command (not the analyze screen, yet) also emits the **model-driven
### Component kind
`Part::kind` is inferred at construction (`src/system/component_kind.cpp`) from the leading reference-designator letter(s) of the part name. **Longest-match wins**:
`Part::kind` is inferred at construction (`src/core/domain/component_kind.cpp`) from the leading reference-designator letter(s) of the part name. **Longest-match wins**:
- Two-letter prefixes (checked first, case-insensitive): `LED → Semiconductor`, `TP → TestPoint`, `SW → Switch`, `FB → Passive`, `MK / MP / MH → Mechanical`, `HS → Mechanical`, `RA / RN / RP / RV → Passive`.
- Single-letter fallback: `R / C / L / F → Passive`, `D / Q → Semiconductor`, `U → IntegratedCircuit`, `J / P → Connector`, `Y / X → Crystal`, `S → Switch`.
@@ -315,7 +354,7 @@ Recomputed on `restore` (no persistence tag). Currently not used by any decision
`connect` looks up a registered transform for `(p1->connector_type, p2->connector_type)` via `TransformRegistry::lookup`, tried in both directions. Fall-through is `IdentityTransform`:
- Compares pin sets by **canonical name** (`canonical_pin_name(s)`, `src/system/pin_name.cpp`): split into prefix + digit suffix; if the suffix is pure digits, zero-pad to width 3 (`A1``A001`, `A001``A001`, `A1B``A1B`, `VCC``VCC`).
- Compares pin sets by **canonical name** (`canonical_pin_name(s)`, `src/core/domain/pin_name.cpp`): split into prefix + digit suffix; if the suffix is pure digits, zero-pad to width 3 (`A1``A001`, `A001``A001`, `A1B``A1B`, `VCC``VCC`).
- `CheckIdentityCompatible(a, b)` accepts the **subset case** (one side's canonical set is a subset of the other's — typical because Altium drops NC, Mentor doesn't). Bidirectional mismatch (both sides have orphans) is refused.
- After acceptance, `FillIdentityNCs(p1, p2)` **materialises** the missing canonical positions on the smaller side as new NC pins (`new Pin(other_side_name)`, no signal attached, `nc_origin = None`). Idempotent.

View File

@@ -15,6 +15,13 @@ cmake --build build -j
./build/essim
```
The build is **layered**: `essim_core` is the frontend-agnostic business
library (domain + importers + operations); the `essim` binary comes from a
**frontend** under `src/frontends/<name>/` that links it. Select one with
`-DESSIM_FRONTEND=<name>` (default `tui`); `-DESSIM_FRONTEND=none` builds the
core + tests only, with no GUI toolkit fetched. Architecture in
[`DESIGN.md`](DESIGN.md).
Inside the shell, type `help` for the live command list — or read the
auto-generated reference at [`doc/user/commands.md`](doc/user/commands.md).
A worked bring-up script is at [`test/system.essim`](test/system.essim);
@@ -41,10 +48,14 @@ Step-by-step walkthroughs for both the batch and TUI workflows are in
`../libbsdl`, pulled in via `add_subdirectory` and linked dynamically.
Override its location with `-DBSDL_DIR=/path/to/libbsdl`. Powers the
`attach-bsdl` command and the pin/JTAG checks.
- Fetched automatically at configure time via `FetchContent` (nothing to
install): **FTXUI** v6.1.9 and **doctest** v2.4.11.
- Fetched automatically via `FetchContent` (nothing to install): **FTXUI**
v6.1.9 — only when building the **tui** frontend — and **doctest** v2.4.11
for the tests.
- Optional, only for the `doc` target: **doxygen** and **python3**.
libzip, pugixml and libbsdl are the **core** dependencies; FTXUI belongs to the
tui frontend, so a `-DESSIM_FRONTEND=none` build needs none of it.
## Tests
```sh
@@ -53,6 +64,9 @@ Step-by-step walkthroughs for both the batch and TUI workflows are in
ctest --test-dir build
```
`ctest` runs `essim_tests` (core — links `essim_core`, no GUI toolkit) and
`essim_tui_tests` (the FTXUI frontend's tests, under `tests/tui/`).
Skip building tests entirely:
```sh
@@ -76,12 +90,17 @@ cmake --build build --target doc # needs doxygen + python3
## Project layout
```
src/system/ domain model (Module/Part/Pin/Signal, Connection, Transform, …)
src/imports/ Mentor / Altium / ODS netlist importers
src/tui/ FTXUI shell (commands, screens, completion, history)
tests/ doctest suite
doc/ api/ + user/ Markdown trees, Doxyfile.in, gen_api_md.py
test/ sample netlists + system.essim bring-up script
src/
core/ business logic, NO GUI toolkit (→ libessim_core)
domain/ model (Module/Part/Pin/Signal, Connection, Transform…) + analyses
imports/ Mentor / Altium / ODS netlist importers + ODS writer
app/ use-case operations (export → CSV/ODS, …)
frontends/ one dir per GUI/TUI engine, each links essim_core
tui/ FTXUI shell + main.cpp (→ libessim_tui + the `essim` binary)
tests/ core tests (link essim_core)
tui/ frontend tests (link essim_tui)
doc/ api/ + user/ Markdown, Doxyfile.in, gen_api_md.py
test/ sample netlists + system.essim bring-up script
```
Full layout & rationale in [`DESIGN.md`](DESIGN.md).

67
src/core/app/connect.cpp Normal file
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 <string>

View File

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

View File

@@ -1,4 +1,4 @@
#include "system/pin_name.hpp"
#include "core/domain/pin_name.hpp"
#include <cstdio>
#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_
#define _SYSTEM_HPP_
#include "imports/import_base.hpp"
#include "core/imports/import_base.hpp"
#pragma once
class Modules; ///< Forward declaration of the Modules class.

View File

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

View File

@@ -4,8 +4,8 @@
#include <string>
#include <fstream>
#include "system/parts.hpp"
#include "system/signals.hpp"
#include "core/domain/parts.hpp"
#include "core/domain/signals.hpp"
/**
* @brief Base class for importing data from a file.
@@ -27,11 +27,22 @@ public:
*
* @param file_name Name of the file to be imported.
*/
ImportBase(std::string file_name) : file_lines(std::fstream(file_name))
ImportBase(std::string file_name) : file_lines(file_name, std::ios::in)
{
prts = new Parts();
};
/**
* @brief Whether the input file was opened successfully.
*
* Opened read-only, so this is false only when the file is genuinely
* missing or unreadable (a read-only but present file still opens).
* System::Load checks it to fail fast instead of producing an empty module.
*
* @return true if the file stream is open.
*/
bool is_open() const { return file_lines.is_open(); }
/**
* @brief Pure virtual method for parsing the file.
*
@@ -53,9 +64,13 @@ public:
/**
* @brief Virtual destructor for ImportBase.
*
* Ensures proper cleanup of derived classes.
* Frees the Parts container object. Only the container is deleted, not the
* Part objects it holds: by the time the importer is destroyed those have
* been transferred to a Module (SystemElementContainer::add copies the
* pointers), which owns and deletes them. The default ~Parts frees the map
* without touching the elements, so there is no double free.
*/
virtual ~ImportBase() = default;
virtual ~ImportBase() { delete prts; }
};
#endif // _IMPORT_BASE_HPP_

View File

@@ -1,6 +1,6 @@
#include "import_mentor.hpp"
#include "system/pins.hpp"
#include "system/parts.hpp"
#include "core/domain/pins.hpp"
#include "core/domain/parts.hpp"
#include <cctype>
#include <vector>
@@ -43,16 +43,6 @@ enum class State
*/
ImportMentor::ImportMentor(string filename) : ImportBase(filename) {}
/**
* @brief Destructor for ImportMentor.
*
* Ensures proper cleanup by calling the base class destructor.
*/
ImportMentor::~ImportMentor()
{
ImportBase::~ImportBase();
}
/**
* @brief Parses the file to extract parts, pins, and signals.
*

View File

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

View File

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

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

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 "tui/tui_helpers.hpp"
#include "frontends/tui/tui.hpp"
#include "frontends/tui/tui_helpers.hpp"
#include "system/analysis.hpp"
#include "system/connect.hpp"
#include "system/modules.hpp"
#include "system/nets.hpp"
#include "system/parts.hpp"
#include "system/persist.hpp"
#include "system/pin_role.hpp"
#include "system/pin_model.hpp"
#include "system/bsdl_model.hpp"
#include "system/bsdl_check.hpp"
#include "system/pins.hpp"
#include "system/signals.hpp"
#include "system/system.hpp"
#include "system/transform.hpp"
#include "system/transform_vpx.hpp"
#include "core/domain/analysis.hpp"
#include "core/domain/connect.hpp"
#include "core/domain/modules.hpp"
#include "core/domain/parts.hpp"
#include "core/domain/persist.hpp"
#include "core/domain/pin_role.hpp"
#include "core/domain/pin_model.hpp"
#include "core/domain/bsdl_model.hpp"
#include "core/domain/pins.hpp"
#include "core/domain/signals.hpp"
#include "core/domain/system.hpp"
#include "core/app/connect.hpp"
#include "core/app/load.hpp"
#include "core/app/verify.hpp"
#include "core/domain/transform_vpx.hpp" // ValidatePartForKind
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <exception>
#include <fstream>
#include <unordered_set>
#include <utility>
void Tui::RegisterCommands() {
@@ -138,29 +138,23 @@ void Tui::RegisterCommands() {
{"import type [mentor|altium|ods]", Completion::None}},
[this](const std::vector<std::string> &args) {
if (!sys) { Print("no system: run 'new' first."); return; }
std::string ls = ToLower(args[2]);
ImportType t;
if (ls == "mentor") t = ImportType::IMPORT_MENTOR;
else if (ls == "altium") t = ImportType::IMPORT_ALTIUM;
else if (ls == "ods") t = ImportType::IMPORT_ODS;
else { Print("unknown import type: " + args[2]); return; }
try {
sys->Load(args[0], args[1], t);
Module *mod = sys->modules()->get(args[0]);
int dropped = drop_singleton_signals(mod->signals);
auto inf = infer_signal_types(sys.get());
Print("loaded '" + args[0] + "' from " + args[1]);
Print(" parts: " + std::to_string(mod->size()));
Print(" signals: " + std::to_string(mod->signals->size())
+ (dropped ? " (dropped " + std::to_string(dropped)
+ " singleton/NC signal(s))" : ""));
Print(" types: " + std::to_string(inf.power) + " power, "
+ std::to_string(inf.gnd) + " gnd, "
+ std::to_string(inf.kept_other)
+ " suspect Power (name only — kept as Other)");
} catch (const std::exception &e) {
Print(std::string("load failed: ") + e.what());
if (!app::import_type_from_name(args[2], t)) {
Print("unknown import type: " + args[2]); return;
}
// Import + drop-singletons + infer-types is one core op; the command
// only parses the type and renders the counts.
app::LoadResult r = app::load_module(sys.get(), args[0], args[1], t);
if (!r.ok) { Print(std::string("load failed: ") + r.error); return; }
Print("loaded '" + args[0] + "' from " + args[1]);
Print(" parts: " + std::to_string(r.parts));
Print(" signals: " + std::to_string(r.signals)
+ (r.dropped ? " (dropped " + std::to_string(r.dropped)
+ " singleton/NC signal(s))" : ""));
Print(" types: " + std::to_string(r.power) + " power, "
+ std::to_string(r.gnd) + " gnd, "
+ std::to_string(r.kept_other)
+ " suspect Power (name only — kept as Other)");
},
/*prompt_for_missing=*/ true,
"load a module from a netlist / pinout file (mentor, altium, ods)",
@@ -228,104 +222,43 @@ void Tui::RegisterCommands() {
commands["verify"] = { {}, [this](auto &) {
if (!sys) { Print("no system: run 'new' first."); return; }
int checked = 0;
int mismatches = 0;
for (auto &mkv : *sys->modules()) {
Module *mod = mkv.second;
for (auto &pkv : *mod) {
Part *prt = pkv.second;
if (prt->connector_type.empty()) continue;
for (auto &nkv : *prt) {
Pin *pin = nkv.second;
++checked;
SignalType expected = pin->expected_signal_type();
if (expected == SignalType::Other) continue;
Signal *s = pin->signal();
SignalType actual = s ? s->type : SignalType::Other;
if (actual == expected) continue;
++mismatches;
std::string sig_label = s ? s->name : std::string("(NC)");
Print(" " + mod->name + "/" + prt->name + "/" + pin->name
+ ": expected " + signal_type_name(expected)
+ ", got " + signal_type_name(actual)
+ " (signal: " + sig_label + ")");
}
}
}
Print("verify: " + std::to_string(mismatches) + " local mismatch(es) over "
+ std::to_string(checked) + " typed pin(s).");
app::VerifyReport r = app::verify(sys.get());
auto nets = compute_all_nets(sys.get());
int bridged = 0, inconsistent = 0;
for (const auto &n : nets) {
if (n.members.size() < 2) continue;
++bridged;
SignalType dom;
if (net_type_consistent(n, dom)) continue;
++inconsistent;
for (const auto &m : r.role_mismatches)
Print(" " + m.module + "/" + m.part + "/" + m.pin
+ ": expected " + signal_type_name(m.expected)
+ ", got " + signal_type_name(m.actual)
+ " (signal: " + m.signal + ")");
Print("verify: " + std::to_string(r.role_mismatches.size())
+ " local mismatch(es) over " + std::to_string(r.typed_pins)
+ " typed pin(s).");
for (const auto &ni : r.net_inconsistencies) {
std::string line = " net mixes Power and GndShield:";
for (const auto &mp : n.members) {
line += " " + mp.first->name + "/" + mp.second->name
+ "(" + signal_type_name(mp.second->type) + ")";
}
for (const auto &mem : ni.members)
line += " " + mem.module + "/" + mem.signal
+ "(" + signal_type_name(mem.type) + ")";
Print(line);
}
Print("verify: " + std::to_string(inconsistent) + " inconsistent net(s) over "
+ std::to_string(bridged) + " bridged net(s) ("
+ std::to_string(nets.size()) + " total).");
Print("verify: " + std::to_string(r.net_inconsistencies.size())
+ " inconsistent net(s) over " + std::to_string(r.bridged_nets)
+ " bridged net(s) (" + std::to_string(r.total_nets) + " total).");
// Orphan pin report. A pin is "orphan" if it came out of import (or
// post-import drop) with no signal, and is still not bridged to a
// real signal via any Connection::pin_map. Use `nc-export` for the
// per-pin list.
std::unordered_set<Pin *> bridged_pins;
for (auto &ckv : *sys->connections())
for (auto &wp : ckv.second->pin_map) {
if (wp.first) bridged_pins.insert(wp.first);
if (wp.second) bridged_pins.insert(wp.second);
}
int orph_imported = 0, orph_dropped = 0;
for (auto &mkv : *sys->modules())
for (auto &pkv : *mkv.second)
for (auto &nkv : *pkv.second) {
Pin *pin = nkv.second;
if (pin->signal() || bridged_pins.count(pin)) continue;
if (pin->nc_origin == NcOrigin::ImportedUnconnected) ++orph_imported;
else if (pin->nc_origin == NcOrigin::DroppedSingleton) ++orph_dropped;
}
Print("verify: " + std::to_string(orph_imported + orph_dropped)
+ " orphan pin(s) at import ("
+ std::to_string(orph_imported) + " imported NC, "
+ std::to_string(orph_dropped) + " dropped singleton).");
Print("verify: " + std::to_string(r.orphan_total())
+ " orphan pin(s) at import (" + std::to_string(r.orphan_imported)
+ " imported NC, " + std::to_string(r.orphan_dropped)
+ " dropped singleton).");
// Model-driven pin checks (drive contention / undriven net / NC-wired)
// from the PinSpec direction/function populated by connector/BSDL models.
auto pin_anoms = check_pin_specs(sys.get(), &nets);
for (const auto &a : pin_anoms)
Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message);
Print("verify: " + std::to_string(pin_anoms.size())
+ " model-driven pin anomaly(ies).");
// JTAG boundary-scan chain integrity (TAP pins → nets).
auto jtag_anoms = check_jtag_chain(sys.get(), &nets);
for (const auto &a : jtag_anoms)
Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message);
Print("verify: " + std::to_string(jtag_anoms.size())
+ " JTAG chain anomaly(ies).");
// Model-vs-netlist conflicts (e.g. a BSDL power pin left unconnected).
auto conflict_anoms = check_source_conflicts(sys.get());
for (const auto &a : conflict_anoms)
Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message);
Print("verify: " + std::to_string(conflict_anoms.size())
+ " source-conflict(s).");
// BSDL completeness: device power/ground pins missing from the netlist.
auto missing_anoms = check_bsdl_completeness(sys.get());
for (const auto &a : missing_anoms)
Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message);
Print("verify: " + std::to_string(missing_anoms.size())
+ " BSDL completeness issue(s).");
// Each model-driven group: per-finding lines + a one-line summary.
auto render = [this](const std::vector<Anomaly> &v, const char *tail) {
for (const auto &a : v)
Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message);
Print("verify: " + std::to_string(v.size()) + tail);
};
render(r.pin_anomalies, " model-driven pin anomaly(ies).");
render(r.jtag_anomalies, " JTAG chain anomaly(ies).");
render(r.conflict_anomalies, " source-conflict(s).");
render(r.completeness_anomalies, " BSDL completeness issue(s).");
}, true,
"check pin roles, power/gnd net consistency, and (with BSDL) pin and JTAG checks" };
@@ -622,47 +555,23 @@ void Tui::RegisterCommands() {
auto [p2, p2_alts] = resolve_part(m2, args[3]);
if (!p2) { report_ambiguous("part in " + m2->name, args[3], p2_alts); return; }
auto &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) {
Print("connect refused: no transform for types '"
+ (p1->connector_type.empty() ? "(none)" : p1->connector_type)
+ "' ↔ '"
+ (p2->connector_type.empty() ? "(none)" : p2->connector_type)
+ "'. Set matching types via 'set-connector-type' first.");
return;
}
std::string info;
std::string err = CheckIdentityCompatible(p1, p2, &info);
if (!err.empty()) {
Print("connect refused: " + err);
return;
}
if (!info.empty()) {
int added = FillIdentityNCs(p1, p2);
Print("connect: " + info);
if (added > 0)
Print("connect: added " + std::to_string(added)
+ " NC pin(s) so both sides match");
}
}
auto pin_map = t->apply(p1, p2);
std::string conn_name = m1->name + "/" + p1->name
+ " <-> " + m2->name + "/" + p2->name;
try {
Connection *c = new Connection(conn_name, m1, p1, m2, p2);
c->transform_name = t->name;
c->pin_map = std::move(pin_map);
sys->connections()->add(c);
Print("connected: " + conn_name
+ " via " + t->name
+ " (" + std::to_string(c->pin_map.size()) + " wires)");
} catch (const std::exception &e) {
Print(std::string("connect failed: ") + e.what());
// Resolution above is arg-parsing (user text → objects); the wiring
// itself — transform lookup, identity NC fill, Connection creation —
// is app::connect_parts.
app::ConnectResult cr = app::connect_parts(sys.get(), m1, p1, m2, p2);
if (cr.refused) { Print("connect refused: " + cr.error); return; }
if (!cr.identity_info.empty()) {
Print("connect: " + cr.identity_info);
if (cr.nc_added > 0)
Print("connect: added " + std::to_string(cr.nc_added)
+ " NC pin(s) so both sides match");
}
if (cr.ok)
Print("connected: " + cr.connection_name
+ " via " + cr.transform_name
+ " (" + std::to_string(cr.wires) + " wires)");
else
Print(std::string("connect failed: ") + cr.error);
},
/*prompt_for_missing=*/ false,
"connect a part across two modules (interactive screen if no args)",

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

View File

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

View File

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

View File

@@ -1,22 +1,19 @@
#include "tui/tui.hpp"
#include "tui/tui_helpers.hpp"
#include "frontends/tui/tui.hpp"
#include "frontends/tui/tui_helpers.hpp"
#include "system/analysis.hpp"
#include "system/bsdl_check.hpp"
#include "system/connect.hpp"
#include "system/modules.hpp"
#include "system/nets.hpp"
#include "system/parts.hpp"
#include "system/pins.hpp"
#include "system/signals.hpp"
#include "system/system.hpp"
#include "core/app/verify.hpp"
#include "core/domain/analysis.hpp"
#include "core/domain/connect.hpp"
#include "core/domain/modules.hpp"
#include "core/domain/parts.hpp"
#include "core/domain/signals.hpp"
#include "core/domain/system.hpp"
#include <ftxui/component/component.hpp>
#include <ftxui/dom/elements.hpp>
#include <algorithm>
#include <map>
#include <unordered_set>
#include <vector>
using namespace ftxui;
@@ -77,58 +74,23 @@ Component Tui::BuildDashboardScreen() {
}
int n_conn = (int)sys->connections()->size();
// ---- verify-style health (recomputed; cheap on realistic sizes) ----
int n_role_mismatches = 0, n_typed_pins = 0;
for (auto &mkv : *sys->modules())
for (auto &pkv : *mkv.second) {
Part *prt = pkv.second;
if (prt->connector_type.empty()) continue;
for (auto &nkv : *prt) {
Pin *pin = nkv.second;
++n_typed_pins;
SignalType expected = pin->expected_signal_type();
if (expected == SignalType::Other) continue;
Signal *s = pin->signal();
SignalType actual = s ? s->type : SignalType::Other;
if (actual != expected) ++n_role_mismatches;
}
}
// ---- verify-style health (shared core op; cheap on realistic sizes) ----
app::VerifyReport vr = app::verify(sys.get());
int n_role_mismatches = (int)vr.role_mismatches.size();
int n_typed_pins = vr.typed_pins;
int n_inconsistent = (int)vr.net_inconsistencies.size();
int n_bridged = vr.bridged_nets;
int orph_imported = vr.orphan_imported;
int orph_dropped = vr.orphan_dropped;
auto nets = compute_all_nets(sys.get());
int n_bridged = 0, n_inconsistent = 0;
for (const auto &n : nets) {
if (n.members.size() < 2) continue;
++n_bridged;
SignalType dom;
if (!net_type_consistent(n, dom)) ++n_inconsistent;
}
// ---- NC orphan summary (matches verify pass 3) ----
std::unordered_set<Pin *> bridged_pins;
for (auto &ckv : *sys->connections())
for (auto &wp : ckv.second->pin_map) {
if (wp.first) bridged_pins.insert(wp.first);
if (wp.second) bridged_pins.insert(wp.second);
}
int orph_imported = 0, orph_dropped = 0;
// Per-module list of dropped-singleton pins, for the detail rows below
// the NC health line. The signal name is gone (the Signal object was
// deleted by `drop_singleton_signals`), but the pin's full path is
// enough to locate it in `explore`.
std::map<std::string, std::vector<std::string>> dropped_by_module;
for (auto &mkv : *sys->modules())
for (auto &pkv : *mkv.second)
for (auto &nkv : *pkv.second) {
Pin *pin = nkv.second;
if (pin->signal() || bridged_pins.count(pin)) continue;
if (pin->nc_origin == NcOrigin::ImportedUnconnected) {
++orph_imported;
} else if (pin->nc_origin == NcOrigin::DroppedSingleton) {
++orph_dropped;
dropped_by_module[mkv.first].push_back(
pkv.first + "/" + nkv.first);
}
}
for (const auto &o : vr.orphans)
if (o.dropped)
dropped_by_module[o.module].push_back(o.part + "/" + o.pin);
auto health_line = [](bool ok, const std::string &s) {
return hbox({
@@ -144,7 +106,7 @@ Component Tui::BuildDashboardScreen() {
+ " typed pin(s)"));
health_rows.push_back(health_line(n_inconsistent == 0,
"nets: " + std::to_string(n_inconsistent) + " inconsistent over "
+ std::to_string(n_bridged) + " bridged (" + std::to_string(nets.size())
+ std::to_string(n_bridged) + " bridged (" + std::to_string(vr.total_nets)
+ " total)"));
int orph_total = orph_imported + orph_dropped;
health_rows.push_back(health_line(orph_total == 0,
@@ -172,12 +134,9 @@ Component Tui::BuildDashboardScreen() {
}
}
// Model-driven checks (BSDL pin specs, JTAG chain, source conflicts),
// reusing the nets computed above.
int n_model = (int)(check_pin_specs(sys.get(), &nets).size()
+ check_jtag_chain(sys.get(), &nets).size()
+ check_source_conflicts(sys.get()).size()
+ check_bsdl_completeness(sys.get()).size());
// Model-driven checks (BSDL pin specs, JTAG chain, source conflicts,
// completeness) — from the shared verify report.
int n_model = vr.model_total();
health_rows.push_back(health_line(n_model == 0,
"model: " + std::to_string(n_model) + " BSDL/JTAG anomaly(ies)"));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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/event.hpp>

View File

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

View File

@@ -1,4 +1,4 @@
#include "tui/tui_helpers.hpp"
#include "frontends/tui/tui_helpers.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 "system/analysis.hpp"
#include "system/modules.hpp"
#include "system/parts.hpp"
#include "system/pins.hpp"
#include "system/signals.hpp"
#include "system/system.hpp"
#include "core/domain/analysis.hpp"
#include "core/domain/modules.hpp"
#include "core/domain/parts.hpp"
#include "core/domain/pins.hpp"
#include "core/domain/signals.hpp"
#include "core/domain/system.hpp"
#include <memory>
#include <string>

View File

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

View File

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

View File

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

79
tests/test_connect.cpp Normal file
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 "system/modules.hpp"
#include "system/parts.hpp"
#include "system/persist.hpp"
#include "system/pins.hpp"
#include "system/signals.hpp"
#include "system/system.hpp"
#include "core/domain/modules.hpp"
#include "core/domain/parts.hpp"
#include "core/domain/persist.hpp"
#include "core/domain/pins.hpp"
#include "core/domain/signals.hpp"
#include "core/domain/system.hpp"
#include <filesystem>
#include <memory>

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
#include <doctest/doctest.h>
#include "system/signal_type.hpp"
#include "core/domain/signal_type.hpp"
TEST_CASE("signal_type_name round-trips with from_name") {
SignalType t;

95
tests/test_verify.cpp Normal file
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 "system/modules.hpp"
#include "system/parts.hpp"
#include "system/pins.hpp"
#include "system/signals.hpp"
#include "system/system.hpp"
#include "system/transform.hpp"
#include "system/transform_vpx.hpp"
#include "core/domain/modules.hpp"
#include "core/domain/parts.hpp"
#include "core/domain/pins.hpp"
#include "core/domain/signals.hpp"
#include "core/domain/system.hpp"
#include "core/domain/transform.hpp"
#include "core/domain/transform_vpx.hpp"
#include <map>
#include <memory>

View File

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