Merge separate-core-ui: split business logic from the T/GUI layer.
Reorganises essim around a hard core/frontends boundary so multiple GUI/TUI
engines can be built on the same business logic:
- src/core/{domain,imports,app} — frontend-agnostic essim_core (no GUI
toolkit); src/frontends/<name>/ — per-frontend UI. CMake selects one with
-DESSIM_FRONTEND=<name> (default tui; "none" = core+tests, no FTXUI).
- Application layer (core/app/): export / verify / connect / load extracted
as System-in, result-struct-out operations; TUI commands and screens are
thin wrappers. The analyze screen and dashboard now share app::verify
instead of recomputing the passes (de-dup).
- Frontend interface (frontends/frontend.hpp) + shared frontend_main: main.cpp
is frontend-agnostic; a new frontend is a 4-line main plus a Frontend impl.
- ImportBase hardened (read-only open, fail-fast on a missing file) and made
leak-free (valgrind: no leaks, 0 errors).
Tests: 78 core cases / 368 assertions + the TUI suite, all green. Binary stays
./build/essim; --batch/--commands-md/--help behaviour unchanged.
This commit is contained in:
101
CMakeLists.txt
101
CMakeLists.txt
@@ -11,49 +11,60 @@ project(essim
|
|||||||
|
|
||||||
include(FetchContent)
|
include(FetchContent)
|
||||||
|
|
||||||
set(FTXUI_BUILD_DOCS OFF CACHE INTERNAL "")
|
# ----------------------------------------------------------------- core deps
|
||||||
set(FTXUI_BUILD_EXAMPLES OFF CACHE INTERNAL "")
|
# libbsdl — standalone BSDL parser (LGPL-2.1), dynamically linked (EUPL-1.2,
|
||||||
set(FTXUI_BUILD_TESTS OFF CACHE INTERNAL "")
|
# which the LGPL permits). Override its path with -DBSDL_DIR=...
|
||||||
set(FTXUI_ENABLE_INSTALL OFF CACHE INTERNAL "")
|
|
||||||
|
|
||||||
FetchContent_Declare(ftxui
|
|
||||||
GIT_REPOSITORY https://github.com/ArthurSonzogni/FTXUI.git
|
|
||||||
GIT_TAG v6.1.9
|
|
||||||
GIT_SHALLOW TRUE
|
|
||||||
)
|
|
||||||
FetchContent_MakeAvailable(ftxui)
|
|
||||||
|
|
||||||
find_package(libzip REQUIRED)
|
|
||||||
find_package(pugixml REQUIRED)
|
|
||||||
|
|
||||||
# libbsdl — standalone BSDL parser (LGPL-2.1), dynamically linked from essim
|
|
||||||
# (EUPL-1.2, which the LGPL permits). Path overridable via -DBSDL_DIR=...;
|
|
||||||
# its CLI and tests are not needed inside essim's build.
|
|
||||||
set(BSDL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../libbsdl" CACHE PATH "libbsdl source tree")
|
set(BSDL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../libbsdl" CACHE PATH "libbsdl source tree")
|
||||||
set(BSDL_BUILD_CLI OFF CACHE BOOL "" FORCE)
|
set(BSDL_BUILD_CLI OFF CACHE BOOL "" FORCE)
|
||||||
set(BSDL_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
set(BSDL_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||||
add_subdirectory(${BSDL_DIR} ${CMAKE_BINARY_DIR}/libbsdl)
|
add_subdirectory(${BSDL_DIR} ${CMAKE_BINARY_DIR}/libbsdl)
|
||||||
|
|
||||||
# Library target = everything except main.cpp; reused by `essim` and `essim_tests`.
|
find_package(libzip REQUIRED)
|
||||||
file(GLOB_RECURSE LIB_SOURCES "src/*.cpp")
|
find_package(pugixml REQUIRED)
|
||||||
list(REMOVE_ITEM LIB_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp")
|
|
||||||
|
|
||||||
add_library(essim_lib STATIC ${LIB_SOURCES})
|
# =============================================================== essim_core
|
||||||
target_include_directories(essim_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src)
|
# All business logic — domain model, importers, application operations
|
||||||
target_link_libraries(essim_lib
|
# (src/core/{domain,imports,app}). Frontend-agnostic: it links NO GUI/TUI
|
||||||
|
# toolkit, so every frontend and the test suite share the exact same core.
|
||||||
|
file(GLOB_RECURSE CORE_SOURCES "src/core/*.cpp")
|
||||||
|
add_library(essim_core STATIC ${CORE_SOURCES})
|
||||||
|
target_include_directories(essim_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src)
|
||||||
|
target_link_libraries(essim_core
|
||||||
PUBLIC
|
PUBLIC
|
||||||
ftxui::screen
|
|
||||||
ftxui::dom
|
|
||||||
ftxui::component
|
|
||||||
libzip::zip
|
libzip::zip
|
||||||
pugixml::pugixml
|
pugixml::pugixml
|
||||||
bsdl::bsdl
|
bsdl::bsdl
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(essim src/main.cpp)
|
# =============================================================== frontend(s)
|
||||||
target_link_libraries(essim PRIVATE essim_lib)
|
# Pick the GUI/TUI frontend to build the `essim` binary against. Each frontend
|
||||||
|
# is a self-contained src/frontends/<name>/ (own CMakeLists, GUI toolkit, and
|
||||||
|
# main.cpp) that links essim_core. "none" builds the core + tests only — no GUI
|
||||||
|
# toolkit is fetched. To add a frontend (e.g. a Qt GUI), create
|
||||||
|
# src/frontends/gui/ and configure with -DESSIM_FRONTEND=gui.
|
||||||
|
set(ESSIM_FRONTEND "tui" CACHE STRING
|
||||||
|
"Frontend to build: a directory name under src/frontends/, or 'none'")
|
||||||
|
set_property(CACHE ESSIM_FRONTEND PROPERTY STRINGS tui none)
|
||||||
|
|
||||||
# Tests
|
if(ESSIM_FRONTEND STREQUAL "none")
|
||||||
|
message(STATUS "essim: ESSIM_FRONTEND=none — core + tests only (no frontend, no GUI toolkit)")
|
||||||
|
elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/frontends/${ESSIM_FRONTEND}/CMakeLists.txt")
|
||||||
|
message(STATUS "essim: building frontend '${ESSIM_FRONTEND}'")
|
||||||
|
# Shared, GUI-toolkit-free frontend support: the abstract Frontend interface
|
||||||
|
# (header-only) and the frontend-agnostic launcher frontend_main(). Every
|
||||||
|
# frontend's main() links this and forwards argv to it.
|
||||||
|
add_library(essim_frontend STATIC
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/src/frontends/frontend_main.cpp")
|
||||||
|
target_include_directories(essim_frontend PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src)
|
||||||
|
add_subdirectory(src/frontends/${ESSIM_FRONTEND})
|
||||||
|
else()
|
||||||
|
message(FATAL_ERROR
|
||||||
|
"Unknown ESSIM_FRONTEND '${ESSIM_FRONTEND}' — expected "
|
||||||
|
"src/frontends/${ESSIM_FRONTEND}/CMakeLists.txt, or 'none'.")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# =============================================================== tests (core)
|
||||||
|
# The suite exercises essim_core only — no frontend, no GUI toolkit.
|
||||||
include(CTest)
|
include(CTest)
|
||||||
if(BUILD_TESTING)
|
if(BUILD_TESTING)
|
||||||
set(CMAKE_POLICY_VERSION_MINIMUM 3.5)
|
set(CMAKE_POLICY_VERSION_MINIMUM 3.5)
|
||||||
@@ -65,20 +76,35 @@ if(BUILD_TESTING)
|
|||||||
FetchContent_MakeAvailable(doctest)
|
FetchContent_MakeAvailable(doctest)
|
||||||
unset(CMAKE_POLICY_VERSION_MINIMUM)
|
unset(CMAKE_POLICY_VERSION_MINIMUM)
|
||||||
|
|
||||||
|
# Core tests — exercise essim_core only (tests/*.cpp, non-recursive, so the
|
||||||
|
# per-frontend tests under tests/<frontend>/ are not pulled in here).
|
||||||
file(GLOB TEST_SOURCES "tests/*.cpp")
|
file(GLOB TEST_SOURCES "tests/*.cpp")
|
||||||
if(TEST_SOURCES)
|
if(TEST_SOURCES)
|
||||||
add_executable(essim_tests ${TEST_SOURCES})
|
add_executable(essim_tests ${TEST_SOURCES})
|
||||||
target_link_libraries(essim_tests PRIVATE essim_lib doctest::doctest)
|
target_link_libraries(essim_tests PRIVATE essim_core doctest::doctest)
|
||||||
add_test(NAME essim_tests COMMAND essim_tests)
|
add_test(NAME essim_tests COMMAND essim_tests)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# Per-frontend tests — tests/<frontend>/*.cpp, built and linked against that
|
||||||
|
# frontend's library only when the frontend itself is built.
|
||||||
|
if(TARGET essim_tui)
|
||||||
|
file(GLOB TUI_TEST_SOURCES "tests/tui/*.cpp")
|
||||||
|
if(TUI_TEST_SOURCES)
|
||||||
|
add_executable(essim_tui_tests
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/doctest_main.cpp" ${TUI_TEST_SOURCES})
|
||||||
|
target_link_libraries(essim_tui_tests PRIVATE essim_tui doctest::doctest)
|
||||||
|
add_test(NAME essim_tui_tests COMMAND essim_tui_tests)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Documentation: Doxygen → XML → custom Python script → doc/api/ (Markdown rendered by gitea).
|
# =============================================================== documentation
|
||||||
# Optional — `doc` target is only created if Doxygen and Python 3 are present.
|
# Doxygen → XML → gen_api_md.py → doc/api/, plus `essim --commands-md`. Needs the
|
||||||
|
# `essim` binary, so it's only wired when a frontend that provides one is built.
|
||||||
find_package(Doxygen COMPONENTS doxygen)
|
find_package(Doxygen COMPONENTS doxygen)
|
||||||
find_package(Python3 COMPONENTS Interpreter)
|
find_package(Python3 COMPONENTS Interpreter)
|
||||||
|
|
||||||
if(DOXYGEN_FOUND AND Python3_Interpreter_FOUND)
|
if(TARGET essim AND DOXYGEN_FOUND AND Python3_Interpreter_FOUND)
|
||||||
set(DOXYGEN_OUTPUT_DIR "${CMAKE_BINARY_DIR}/doc")
|
set(DOXYGEN_OUTPUT_DIR "${CMAKE_BINARY_DIR}/doc")
|
||||||
file(MAKE_DIRECTORY "${DOXYGEN_OUTPUT_DIR}")
|
file(MAKE_DIRECTORY "${DOXYGEN_OUTPUT_DIR}")
|
||||||
configure_file(
|
configure_file(
|
||||||
@@ -103,11 +129,10 @@ if(DOXYGEN_FOUND AND Python3_Interpreter_FOUND)
|
|||||||
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
|
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
|
||||||
COMMENT "Generating documentation (doxygen → gen_api_md.py → doc/api/, essim --commands-md → doc/user/commands.md)"
|
COMMENT "Generating documentation (doxygen → gen_api_md.py → doc/api/, essim --commands-md → doc/user/commands.md)"
|
||||||
VERBATIM)
|
VERBATIM)
|
||||||
elseif(NOT DOXYGEN_FOUND AND NOT Python3_Interpreter_FOUND)
|
elseif(NOT TARGET essim)
|
||||||
message(STATUS "doc: Doxygen and Python 3 not found — `doc` target disabled.")
|
message(STATUS "doc: no `essim` binary (ESSIM_FRONTEND=none) — `doc` target disabled.")
|
||||||
elseif(NOT DOXYGEN_FOUND)
|
elseif(NOT DOXYGEN_FOUND)
|
||||||
message(STATUS "doc: Doxygen not found — `doc` target disabled "
|
message(STATUS "doc: Doxygen not found — `doc` target disabled.")
|
||||||
"(install via `pacman -S doxygen`).")
|
|
||||||
else()
|
else()
|
||||||
message(STATUS "doc: Python 3 interpreter not found — `doc` target disabled.")
|
message(STATUS "doc: Python 3 interpreter not found — `doc` target disabled.")
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
183
DESIGN.md
183
DESIGN.md
@@ -10,72 +10,111 @@ cmake --build build -j
|
|||||||
./build/essim
|
./build/essim
|
||||||
```
|
```
|
||||||
|
|
||||||
- CMake **3.14+** required (uses `FetchContent_MakeAvailable`).
|
- CMake **3.14+** (uses `FetchContent_MakeAvailable`).
|
||||||
- FTXUI is fetched at configure time from GitHub (`v6.1.9`, shallow clone). First configure pays ~20 s for the clone; subsequent ones are cached in `build/_deps/`.
|
- **Layered build** (see *Architecture* below). `essim_core` is the
|
||||||
- **System dependencies** (resolved via `find_package`): `libzip` (target `libzip::zip`) and `pugixml` (target `pugixml::pugixml`). Used by the ODS importer. Arch: `pacman -S libzip pugixml`; Debian/Ubuntu: `libzip-dev libpugixml-dev`.
|
frontend-agnostic business library; a frontend under `src/frontends/<name>/`
|
||||||
- **`libbsdl`** (standalone BSDL parser, LGPL-2.1) is the sibling repo at `../libbsdl`, pulled in via `add_subdirectory` (path overridable with `-DBSDL_DIR=...`) and linked dynamically (`bsdl::bsdl`; an LGPL `.so` is fine from EUPL essim). Powers the BSDL ingest behind `attach-bsdl`.
|
links it and produces the `essim` binary. Choose it with
|
||||||
- Sources are collected with `file(GLOB_RECURSE ALL_SOURCES "src/*.cpp")`. **After adding a new `.cpp`, re-run `cmake -S . -B build`** — CMake does not re-glob automatically and link will fail with "undefined reference".
|
`-DESSIM_FRONTEND=<name>` (default `tui`). **`-DESSIM_FRONTEND=none` builds the
|
||||||
|
core + tests only — no GUI toolkit is fetched.**
|
||||||
|
- **Core system dependencies** (via `find_package`): `libzip` (`libzip::zip`)
|
||||||
|
and `pugixml` (`pugixml::pugixml`) for the ODS importer. Arch:
|
||||||
|
`pacman -S libzip pugixml`; Debian/Ubuntu: `libzip-dev libpugixml-dev`.
|
||||||
|
- **`libbsdl`** (standalone BSDL parser, LGPL-2.1) — sibling repo at
|
||||||
|
`../libbsdl`, `add_subdirectory` (override `-DBSDL_DIR=...`), linked
|
||||||
|
dynamically **into the core** (`bsdl::bsdl`).
|
||||||
|
- **FTXUI** is fetched by the **tui frontend only**
|
||||||
|
(`src/frontends/tui/CMakeLists.txt`), never by the core.
|
||||||
|
- Sources are globbed per layer: `src/core/*.cpp` → `essim_core`,
|
||||||
|
`src/frontends/<fe>/*.cpp` → that frontend's lib + the `essim` binary.
|
||||||
|
**After adding a `.cpp`, re-run `cmake -S . -B build`** — CMake doesn't re-glob.
|
||||||
|
- **Tests** are split: `essim_tests` links `essim_core` (no FTXUI) from
|
||||||
|
`tests/*.cpp`; per-frontend tests like `essim_tui_tests` link `essim_tui` from
|
||||||
|
`tests/<frontend>/*.cpp`.
|
||||||
- **Headless / batch**: `essim --batch --source FILE` runs a script and prints its console output to stdout, then exits without the TUI (good for CI / capturing `verify`). Also `--restore FILE` and `--commands-md [FILE]`. `BootDispatch` runs `--restore`/`--source` synchronously before the event loop (`Source` takes its headless drain branch when no screen is attached), so the console buffer is complete by the time `--batch` dumps it (`Tui::DumpOutput`).
|
- **Headless / batch**: `essim --batch --source FILE` runs a script and prints its console output to stdout, then exits without the TUI (good for CI / capturing `verify`). Also `--restore FILE` and `--commands-md [FILE]`. `BootDispatch` runs `--restore`/`--source` synchronously before the event loop (`Source` takes its headless drain branch when no screen is attached), so the console buffer is complete by the time `--batch` dumps it (`Tui::DumpOutput`).
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
main.cpp -- launches Tui
|
core/ -- business logic; NO GUI toolkit (builds libessim_core)
|
||||||
system/ -- domain model
|
domain/ -- the model + read-only analyses
|
||||||
syselmts.hpp SystemElement + SystemElementContainer<T> (templated, get/merge/iterate)
|
syselmts.hpp SystemElement + SystemElementContainer<T> (get/merge/iterate)
|
||||||
modules.{hpp,cpp} Module, Modules
|
modules.{hpp,cpp} Module, Modules
|
||||||
parts.{hpp,cpp} Part (carries `kind` + `connector_type`), Parts
|
parts.{hpp,cpp} Part (kind, connector_type, bsdl_path; PinSpec per pin)
|
||||||
pins.{hpp,cpp} Pin (carries PinSpec `spec`), Pins
|
pins.{hpp,cpp} Pin (carries PinSpec `spec`), Pins
|
||||||
signals.{hpp,cpp} Signal, Signals
|
signals.{hpp,cpp} Signal, Signals
|
||||||
signal_type.hpp SignalType + helpers
|
signal_type.hpp SignalType + helpers
|
||||||
pin_spec.hpp PinSpec (function/direction/pad/source) + SignalType mapping
|
pin_spec.hpp PinSpec (function/direction/pad/source), mappings, spec_source_rank
|
||||||
component_kind.{hpp,cpp} ComponentKind enum + infer_component_kind(name)
|
component_kind.{hpp,cpp} ComponentKind + infer_component_kind(name)
|
||||||
pin_name.{hpp,cpp} canonical_pin_name(s) — zero-pad digit suffix to 3
|
pin_name.{hpp,cpp} canonical_pin_name (zero-pad digit suffix to 3)
|
||||||
connect.{hpp,cpp} Connection, Connections
|
connect.{hpp,cpp} Connection, Connections
|
||||||
transform.{hpp,cpp} Transform / IdentityTransform / TransformRegistry +
|
transform*.{hpp,cpp} Transform / IdentityTransform / TransformRegistry, VPX transform
|
||||||
CheckIdentityCompatible + FillIdentityNCs
|
pin_role.{hpp,cpp} pin_role(kind,name) -> PinSpec, pin_layout, FillPartFromLayout
|
||||||
pin_role.{hpp,cpp} pin_role(kind, name) → PinSpec, pin_layout(kind),
|
pin_model.{hpp,cpp} PinModel + apply_model(Part*, model) + ConnectorModel
|
||||||
FillPartFromLayout(part, kind)
|
bsdl_model.{hpp,cpp} BsdlModel (libbsdl wrapper) + BsdlPinModel + apply_bsdl
|
||||||
pin_model.{hpp,cpp} PinModel + apply_model(Part*, model) + ConnectorModel
|
bsdl_check.{hpp,cpp} check_pin_specs / _jtag_chain / _source_conflicts / _bsdl_completeness
|
||||||
bsdl_model.{hpp,cpp} BsdlModel (libbsdl wrapper) + BsdlPinModel + apply_bsdl
|
nets.{hpp,cpp} find_net / compute_all_nets / net_type_consistent
|
||||||
bsdl_check.{hpp,cpp} check_pin_specs / check_jtag_chain → vector<Anomaly>
|
analysis.{hpp,cpp} analyze_system -> AnalysisReport (diff pairs, buses, anomalies)
|
||||||
nets.{hpp,cpp} find_net / compute_all_nets / net_type_consistent
|
persist.{hpp,cpp} save / restore (tab-delimited; `B` tag = bsdl_path)
|
||||||
analysis.{hpp,cpp} analyze_system → AnalysisReport (diff pairs, buses, anomalies)
|
system.{hpp,cpp} System: owns Modules + Connections, Load()
|
||||||
persist.{hpp,cpp} save / restore (tab-delimited)
|
imports/ -- netlist / pinout adapters
|
||||||
system.{hpp,cpp} System: owns Modules + Connections, exposes Load()
|
import_base.hpp / import_{mentor,altium,ods}.{hpp,cpp} / ods_writer.{hpp,cpp}
|
||||||
imports/ -- adapters that populate or emit the domain
|
app/ -- application operations (UI-independent use cases)
|
||||||
import_base.hpp ImportBase interface
|
export.{hpp,cpp} export_connections(System*, path, format) -> ExportResult
|
||||||
import_mentor.{hpp,cpp} Mentor Graphics netlist parser
|
verify.{hpp,cpp} verify(System*) -> VerifyReport (the 7 verify passes)
|
||||||
import_altium.{hpp,cpp} Altium netlist parser (`[ ]` parts, `( )` signals)
|
connect.{hpp,cpp} connect_parts(System*, m1,p1, m2,p2) -> ConnectResult
|
||||||
import_ods.{hpp,cpp} ODS spreadsheet pinout (libzip + pugixml)
|
load.{hpp,cpp} load_module(System*, name, path, ImportType) -> LoadResult
|
||||||
ods_writer.{hpp,cpp} Minimal .ods writer (multi-sheet, string cells)
|
frontends/ -- one directory per GUI/TUI engine; each links essim_core
|
||||||
tui/ -- FTXUI shell, split by responsibility
|
frontend.hpp -- abstract Frontend interface (BootDispatch/Dump*/Run)
|
||||||
tui.{hpp,cpp} Class Tui (state + Run() orchestrator + screen-mode event dispatcher)
|
frontend_main.{hpp,cpp} -- frontend_main(argc,argv,Frontend&): argv + boot/batch/run
|
||||||
tui_helpers.{hpp,cpp} Free helpers: ToLower, NaturalLess, LongestCommonPrefix
|
tui/ -- FTXUI shell (builds libessim_tui + the `essim` binary)
|
||||||
shell.cpp Print, Submit, Dispatch, Finalize, Tokenize, history persistence
|
CMakeLists.txt fetches FTXUI; builds essim_tui + essim
|
||||||
completion.cpp CompleteCommand, CompletePath, CompleteInline
|
main.cpp entry point: construct Tui, call frontend_main
|
||||||
commands.cpp RegisterCommands (orchestrator + lifecycle / shell / topology commands)
|
tui.{hpp,cpp} class Tui (state + Run() + screen-mode event dispatch)
|
||||||
commands_export.cpp RegisterExportCommands (export → CSV / ODS, file-dialog hook)
|
tui_helpers.{hpp,cpp} ToLower, NaturalLess, RenderHelpPanel
|
||||||
screen_main.cpp BuildMainScreen (visualisation area + bottom input)
|
shell.cpp Print, Submit, Dispatch, Source / ProcessNextSourceLine
|
||||||
screen_connect.cpp BuildConnectScreen + shared RefreshFilteredPartList helper
|
completion.cpp CompleteCommand / CompletePath / CompleteInline
|
||||||
screen_settype.cpp BuildSettypeScreen
|
commands.cpp RegisterCommands (thin: resolve args -> call core -> render)
|
||||||
screen_explore.cpp BuildExploreScreen (browse modules → parts/signals/connections → details; not scriptable)
|
commands_export.cpp thin wrapper over app::export_connections
|
||||||
screen_dashboard.cpp BuildDashboardScreen (read-only system overview)
|
screen_*.cpp dashboard, connect, settype, explore, analyze, help, main
|
||||||
screen_analyze.cpp BuildAnalyzeScreen (anomalies / groups / power decisions)
|
(console), palette, file dialog, error/confirm, sigtype modal
|
||||||
screen_palette.cpp BuildPaletteModal (global Ctrl-P fuzzy launcher)
|
tests/ -- core tests (link essim_core)
|
||||||
screen_filedialog.cpp BuildFileDialog + OpenFileDialog (reusable file picker)
|
tui/ -- frontend tests (link essim_tui)
|
||||||
screen_error.cpp BuildErrorModal + ShowError (centred error popup)
|
doc/ , test/ -- docs; sample netlists + system.essim bring-up script
|
||||||
screen_help.cpp BuildHelpScreen (topic-driven feature reference)
|
|
||||||
screen_sigtype_modal.cpp BuildSignalTypeModal (popup attached to explore via Modal())
|
|
||||||
doc/classes.puml -- PlantUML class diagram
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`include/` and `lib/` are kept empty by design — FTXUI used to live there as precompiled `.a` + headers, now it comes through FetchContent.
|
## Architecture — core vs frontends
|
||||||
|
|
||||||
|
The hard rule: **`src/core/` never depends on a frontend** — no `#include
|
||||||
|
"frontends/…"`, no GUI toolkit. Frontends depend on the core, never the reverse
|
||||||
|
(`essim_core` links libzip / pugixml / bsdl only).
|
||||||
|
|
||||||
|
- **Domain** (`core/domain/`) — the model and the read-only analyses
|
||||||
|
(`analyze_system`, the `check_*` passes, `compute_all_nets`).
|
||||||
|
- **Application** (`core/app/`) — use-case operations a frontend invokes, e.g.
|
||||||
|
`export_connections(System*, path, format) -> ExportResult`. An operation
|
||||||
|
builds its artefact and returns data/stats; it **never** prints or opens a
|
||||||
|
dialog. (Anti-pattern being removed: the export command used to build the file
|
||||||
|
inside its lambda. The TUI command is now a thin wrapper — resolve args/dialog
|
||||||
|
→ call the core op → render the result.)
|
||||||
|
- **Frontends** (`frontends/<name>/`) — thin: map UI events to core calls and
|
||||||
|
render results. Each implements the **`Frontend`** interface
|
||||||
|
(`frontends/frontend.hpp`: `BootDispatch`, `DumpCommandsMd`, `DumpOutput`,
|
||||||
|
`Run`). The process entry is shared and frontend-agnostic:
|
||||||
|
`frontend_main(argc, argv, Frontend&)` (`frontends/frontend_main.cpp`, built
|
||||||
|
into the toolkit-free `essim_frontend` lib) parses the CLI flags and drives the
|
||||||
|
boot → batch/run flow through the interface; a frontend's `main()` is just
|
||||||
|
*construct the concrete Frontend, call `frontend_main`*. Add one by creating
|
||||||
|
`src/frontends/<name>/CMakeLists.txt` (build `essim_<name>` linking `essim_core`,
|
||||||
|
produce the `essim` binary linking `essim_frontend`) and configuring
|
||||||
|
`-DESSIM_FRONTEND=<name>`.
|
||||||
|
|
||||||
|
Because the core links no toolkit, the suite links `essim_core` directly and
|
||||||
|
`-DESSIM_FRONTEND=none` builds + tests the whole core with FTXUI never fetched.
|
||||||
|
|
||||||
## Domain conventions
|
## Domain conventions
|
||||||
|
|
||||||
- Everything in `system/` is **pointer-based** (commit `d8122d1`: "everything is pointer"). Containers store `T*`, ownership lives with the container.
|
- Everything in `core/domain/` is **pointer-based** (commit `d8122d1`: "everything is pointer"). Containers store `T*`, ownership lives with the container.
|
||||||
- `SystemElementContainer<T>::merge(name)` is the get-or-create primitive — call it instead of `add` when you don't know whether the element already exists. `add` throws on duplicate names or empty names.
|
- `SystemElementContainer<T>::merge(name)` is the get-or-create primitive — call it instead of `add` when you don't know whether the element already exists. `add` throws on duplicate names or empty names.
|
||||||
- `using namespace std;` is present in `syselmts.hpp` — pre-existing, don't add more `using namespace` in headers.
|
- `using namespace std;` is present in `syselmts.hpp` — pre-existing, don't add more `using namespace` in headers.
|
||||||
- Include guards `_NAME_HPP_` *and* `#pragma once` are both used. Match the existing style.
|
- Include guards `_NAME_HPP_` *and* `#pragma once` are both used. Match the existing style.
|
||||||
@@ -110,11 +149,11 @@ Built-in commands: `new`, `set`, `load`, `duplicate`, `save`, `restore`, `source
|
|||||||
|
|
||||||
Pending prompts (from incomplete inline commands) are NOT considered interactive and are filled by subsequent script lines, the way you'd expect. Lines starting with `#` and blank lines are skipped; leading/trailing whitespace is trimmed; `~/` is expanded.
|
Pending prompts (from incomplete inline commands) are NOT considered interactive and are filled by subsequent script lines, the way you'd expect. Lines starting with `#` and blank lines are skipped; leading/trailing whitespace is trimmed; `~/` is expanded.
|
||||||
|
|
||||||
`save` / `restore` (`src/system/persist.{hpp,cpp}`): tab-delimited line format, no extra deps. Tags: `M` (module), `P` (part with `connector_type`), `B` (part's attached BSDL `.bsd` path — re-parsed and re-applied on restore; the path is persisted, **not** the derived pin specs), `N` (pin → signal name; empty = NC; optional 4th field carries `nc_origin_tag()`: `U` = ImportedUnconnected, `D` = DroppedSingleton — omitted when the pin has a signal or when origin is `None`), `S` (signal → type override; only emitted for non-default), `C` (connection header with endpoints + `transform_name`), `W` (wire pair within the current connection). The 4th N field is backward-compatible: pre-existing snapshots without it restore with `nc_origin = None`.
|
`save` / `restore` (`src/core/domain/persist.{hpp,cpp}`): tab-delimited line format, no extra deps. Tags: `M` (module), `P` (part with `connector_type`), `B` (part's attached BSDL `.bsd` path — re-parsed and re-applied on restore; the path is persisted, **not** the derived pin specs), `N` (pin → signal name; empty = NC; optional 4th field carries `nc_origin_tag()`: `U` = ImportedUnconnected, `D` = DroppedSingleton — omitted when the pin has a signal or when origin is `None`), `S` (signal → type override; only emitted for non-default), `C` (connection header with endpoints + `transform_name`), `W` (wire pair within the current connection). The 4th N field is backward-compatible: pre-existing snapshots without it restore with `nc_origin = None`.
|
||||||
|
|
||||||
**Signals** carry a `type` (`SignalType::Power | GndShield | Other`). The `Signal` constructor **defaults to `Other`** — auto-inference no longer happens at construction. Types are set in three ways, in priority order:
|
**Signals** carry a `type` (`SignalType::Power | GndShield | Other`). The `Signal` constructor **defaults to `Other`** — auto-inference no longer happens at construction. Types are set in three ways, in priority order:
|
||||||
|
|
||||||
1. **`infer_signal_types(System*)`** (`src/system/analysis.{hpp,cpp}`) runs at the end of every `load` (after `drop_singleton_signals`). It assigns:
|
1. **`infer_signal_types(System*)`** (`src/core/domain/analysis.{hpp,cpp}`) runs at the end of every `load` (after `drop_singleton_signals`). It assigns:
|
||||||
- `GndShield` when the **name alone** is unambiguous (`GND`, `SHIELD`, `CHASSIS`, `EARTH`, …) — false-positive rate is essentially zero on these.
|
- `GndShield` when the **name alone** is unambiguous (`GND`, `SHIELD`, `CHASSIS`, `EARTH`, …) — false-positive rate is essentially zero on these.
|
||||||
- `Power` requires (a) the name heuristic (`infer_signal_type` says Power), (b) a **hard fan-out floor**: signals with fewer than `POWER_FANOUT_HARD_FLOOR = 3` pins are *always* refused, regardless of name or voltage pattern (a real rail physically cannot land on just 1-2 pads), and (c) at least one positive structural signal — fan-out ≥ `POWER_FANOUT_CONFIRM_MIN = 4` **or** a voltage pattern in the name (`3V3`, `5V`, `12V`, …; detector: a `V` adjacent to a digit). This catches `VSEL_*`, `PWR_OK`, `_VDD_SENSE` etc. which look like Power by name but aren't real rails. Both thresholds are exposed in `analysis.hpp` so the analyze screen can render the same reasoning without duplicating constants.
|
- `Power` requires (a) the name heuristic (`infer_signal_type` says Power), (b) a **hard fan-out floor**: signals with fewer than `POWER_FANOUT_HARD_FLOOR = 3` pins are *always* refused, regardless of name or voltage pattern (a real rail physically cannot land on just 1-2 pads), and (c) at least one positive structural signal — fan-out ≥ `POWER_FANOUT_CONFIRM_MIN = 4` **or** a voltage pattern in the name (`3V3`, `5V`, `12V`, …; detector: a `V` adjacent to a digit). This catches `VSEL_*`, `PWR_OK`, `_VDD_SENSE` etc. which look like Power by name but aren't real rails. Both thresholds are exposed in `analysis.hpp` so the analyze screen can render the same reasoning without duplicating constants.
|
||||||
- `Other` otherwise. The "name-said-Power-but-refuted-by-structure" count is reported by `load`.
|
- `Other` otherwise. The "name-said-Power-but-refuted-by-structure" count is reported by `load`.
|
||||||
@@ -123,17 +162,17 @@ Pending prompts (from incomplete inline commands) are NOT considered interactive
|
|||||||
|
|
||||||
The explore screen shows the type in the signal detail header.
|
The explore screen shows the type in the signal detail header.
|
||||||
|
|
||||||
**Pin spec (expected attributes)**: every Pin carries a `PinSpec spec` (`src/system/pin_spec.hpp`) — the *expected* half of verification, set from a model: `function` (Power/Ground/Signal/Clock/NoConnect/Jtag*), `direction` (In/Out/Bidir/Passive/Power), `pad` (physical package terminal, e.g. a BSDL ball), and `source` (which model wrote it). `set-connector-type` populates it via `pin_role(connector_type, pin_name) → PinSpec`. `Pin::expected_signal_type()` is now a **derived accessor** — `to_signal_type(spec.function)` (Power→Power, Ground→GndShield, else Other) — not a stored field; the *observed* half stays `Pin::signal()` + the net + inference, and `verify` diffs the two. The VPX 3U connector lookup (`vpx_3u_role`) is still a stub returning Other, so connector-typed pins resolve to function Unknown until that table is filled. **`direction`/`function`/`pad` are populated from BSDL** via `attach-bsdl` (see below) and consumed by the model-driven checks (`check_pin_specs`: contention / undriven / NC-wired) and the JTAG chain check (`check_jtag_chain`), both run by `verify`.
|
**Pin spec (expected attributes)**: every Pin carries a `PinSpec spec` (`src/core/domain/pin_spec.hpp`) — the *expected* half of verification, set from a model: `function` (Power/Ground/Signal/Clock/NoConnect/Jtag*), `direction` (In/Out/Bidir/Passive/Power), `pad` (physical package terminal, e.g. a BSDL ball), and `source` (which model wrote it). `set-connector-type` populates it via `pin_role(connector_type, pin_name) → PinSpec`. `Pin::expected_signal_type()` is now a **derived accessor** — `to_signal_type(spec.function)` (Power→Power, Ground→GndShield, else Other) — not a stored field; the *observed* half stays `Pin::signal()` + the net + inference, and `verify` diffs the two. The VPX 3U connector lookup (`vpx_3u_role`) is still a stub returning Other, so connector-typed pins resolve to function Unknown until that table is filled. **`direction`/`function`/`pad` are populated from BSDL** via `attach-bsdl` (see below) and consumed by the model-driven checks (`check_pin_specs`: contention / undriven / NC-wired) and the JTAG chain check (`check_jtag_chain`), both run by `verify`.
|
||||||
|
|
||||||
**Connector pin layout (preparation)**: `pin_layout(connector_type)` returns the canonical full pin-name list for a known connector kind, and `FillPartFromLayout(part, kind)` materialises NC pins for any layout position absent from the imported netlist. `set-connector-type` calls it after setting `connector_type` (no-op today since `pin_layout` is a stub returning `{}` for everything — populate alongside `vpx_3u_role`). End-to-end chain in place: `set-connector-type → FillPartFromLayout → pin_role`.
|
**Connector pin layout (preparation)**: `pin_layout(connector_type)` returns the canonical full pin-name list for a known connector kind, and `FillPartFromLayout(part, kind)` materialises NC pins for any layout position absent from the imported netlist. `set-connector-type` calls it after setting `connector_type` (no-op today since `pin_layout` is a stub returning `{}` for everything — populate alongside `vpx_3u_role`). End-to-end chain in place: `set-connector-type → FillPartFromLayout → pin_role`.
|
||||||
|
|
||||||
**BSDL models (`attach-bsdl`)**: `attach-bsdl <module> <part> <file.bsd>` parses a BSDL device through `libbsdl` (wrapped by `BsdlModel`, `src/system/bsdl_model.{hpp,cpp}`), then `apply_bsdl(part, model)` binds each port to a Pin **by port name first, then by physical pad** — so a netlist that names IC pins either by signal or by package ball both bind. Each bound pin gets its `spec` set: `direction` (BSDL in/out/inout/linkage), `function` (TAP role → Jtag\*, `linkage` → Power/Ground/NoConnect by name, else Signal), `pad` (PIN_MAP ball), `source = Bsdl`. The `.bsd` path is stored on `Part::bsdl_path`, persisted via the `B` tag and re-applied on `restore`. Real-world check: an `xcku15p` FPGA in a VPX system binds 1517/1517 ports.
|
**BSDL models (`attach-bsdl`)**: `attach-bsdl <module> <part> <file.bsd>` parses a BSDL device through `libbsdl` (wrapped by `BsdlModel`, `src/core/domain/bsdl_model.{hpp,cpp}`), then `apply_bsdl(part, model)` binds each port to a Pin **by port name first, then by physical pad** — so a netlist that names IC pins either by signal or by package ball both bind. Each bound pin gets its `spec` set: `direction` (BSDL in/out/inout/linkage), `function` (TAP role → Jtag\*, `linkage` → Power/Ground/NoConnect by name, else Signal), `pad` (PIN_MAP ball), `source = Bsdl`. The `.bsd` path is stored on `Part::bsdl_path`, persisted via the `B` tag and re-applied on `restore`. Real-world check: an `xcku15p` FPGA in a VPX system binds 1517/1517 ports.
|
||||||
|
|
||||||
**Unified application (`apply_model`)**: connector layout and BSDL are two implementations of one `PinModel` interface (`src/system/pin_model.{hpp,cpp}`: `spec_for(pin_name)`, `layout()`, `source()`). `ConnectorModel` wraps `pin_role`/`pin_layout`; `BsdlPinModel` wraps a parsed `BsdlModel`, indexed by both port name and physical pad. A single `apply_model(Part*, const PinModel&)` materialises the layout pins missing from the netlist, then sets each pin's `spec` **only where the model speaks** (`spec.source != None`). Sources are ranked (`spec_source_rank` in `pin_spec.hpp`: UserOverride > Bsdl > ConnectorModel > Inferred > Imported) and apply_model refuses to overwrite a spec owned by a higher-rank source — so one source never clobbers a more authoritative one, which is also the basis for `check_source_conflicts`. `set-connector-type` and `attach-bsdl` both funnel through it (the latter via the thin `apply_bsdl` adapter); `verify` stays agnostic of where a spec came from. A future SPICE/Modelica source would be a third `PinModel`.
|
**Unified application (`apply_model`)**: connector layout and BSDL are two implementations of one `PinModel` interface (`src/core/domain/pin_model.{hpp,cpp}`: `spec_for(pin_name)`, `layout()`, `source()`). `ConnectorModel` wraps `pin_role`/`pin_layout`; `BsdlPinModel` wraps a parsed `BsdlModel`, indexed by both port name and physical pad. A single `apply_model(Part*, const PinModel&)` materialises the layout pins missing from the netlist, then sets each pin's `spec` **only where the model speaks** (`spec.source != None`). Sources are ranked (`spec_source_rank` in `pin_spec.hpp`: UserOverride > Bsdl > ConnectorModel > Inferred > Imported) and apply_model refuses to overwrite a spec owned by a higher-rank source — so one source never clobbers a more authoritative one, which is also the basis for `check_source_conflicts`. `set-connector-type` and `attach-bsdl` both funnel through it (the latter via the thin `apply_bsdl` adapter); `verify` stays agnostic of where a spec came from. A future SPICE/Modelica source would be a third `PinModel`.
|
||||||
|
|
||||||
**`verify` (seven passes)**: (1) typed pins — local mismatch between each pin's `expected_signal_type()` (derived from its `PinSpec`) and the actual signal type; (2) bridged nets — Power↔GndShield inconsistencies; (3) orphan summary `N orphan pin(s) at import (X imported NC, Y dropped singleton)` (filters out pins bridged via any `Connection::pin_map` — typically `FillIdentityNCs`-materialised); (4) **model-driven pin checks** (`check_pin_specs`): `DriveContention` (≥2 push-pull `Out` on a net), `UndrivenNet` (a **fully-modelled** net with input(s) but no driver — nets with any Unknown-direction pin are skipped, so un-modelled drivers don't cause false positives), `NcWired` (a no-connect pin on a multi-pin net); (5) **JTAG chain** (`check_jtag_chain`): collects TAP pins by `spec.function`, maps each to its net, emits `JtagTapIncomplete` / `JtagBusUnbridged` (TMS or TCK not common to all TAP devices) / `JtagChainBreak` (dangling TDO/TDI, chain fan-out, or not a single head→tail daisy chain); (6) **source conflicts** (`check_source_conflicts`): a pin the BSDL declares power/ground (a must-connect rail) that the netlist leaves unconnected — a rail floated in the schematic (`SourceConflict`; the reverse, a BSDL no-connect that *is* wired, is the `NcWired` check); (7) **BSDL completeness** (`check_bsdl_completeness`): device power/ground ports (from the attached `.bsd`, re-parsed) with no matching pin on the netlist part — a rail the schematic symbol is missing (`BsdlPinMissing`, one aggregated finding per part). The BFS-reached `(module, signal)` set for any signal is shown live in `explore`'s detail pane when a signal entry is selected.
|
**`verify` (seven passes)**: (1) typed pins — local mismatch between each pin's `expected_signal_type()` (derived from its `PinSpec`) and the actual signal type; (2) bridged nets — Power↔GndShield inconsistencies; (3) orphan summary `N orphan pin(s) at import (X imported NC, Y dropped singleton)` (filters out pins bridged via any `Connection::pin_map` — typically `FillIdentityNCs`-materialised); (4) **model-driven pin checks** (`check_pin_specs`): `DriveContention` (≥2 push-pull `Out` on a net), `UndrivenNet` (a **fully-modelled** net with input(s) but no driver — nets with any Unknown-direction pin are skipped, so un-modelled drivers don't cause false positives), `NcWired` (a no-connect pin on a multi-pin net); (5) **JTAG chain** (`check_jtag_chain`): collects TAP pins by `spec.function`, maps each to its net, emits `JtagTapIncomplete` / `JtagBusUnbridged` (TMS or TCK not common to all TAP devices) / `JtagChainBreak` (dangling TDO/TDI, chain fan-out, or not a single head→tail daisy chain); (6) **source conflicts** (`check_source_conflicts`): a pin the BSDL declares power/ground (a must-connect rail) that the netlist leaves unconnected — a rail floated in the schematic (`SourceConflict`; the reverse, a BSDL no-connect that *is* wired, is the `NcWired` check); (7) **BSDL completeness** (`check_bsdl_completeness`): device power/ground ports (from the attached `.bsd`, re-parsed) with no matching pin on the netlist part — a rail the schematic symbol is missing (`BsdlPinMissing`, one aggregated finding per part). The BFS-reached `(module, signal)` set for any signal is shown live in `explore`'s detail pane when a signal entry is selected.
|
||||||
|
|
||||||
**`analyze` (post-processing pass)**: `analyze_system(System*) → AnalysisReport` (`src/system/analysis.{hpp,cpp}`) is a stateless read-only pass that detects structural signal groups and anomalies. Per-module (signals are module-scoped):
|
**`analyze` (post-processing pass)**: `analyze_system(System*) → AnalysisReport` (`src/core/domain/analysis.{hpp,cpp}`) is a stateless read-only pass that detects structural signal groups and anomalies. Per-module (signals are module-scoped):
|
||||||
|
|
||||||
- **Diff pairs**: signal names ending `_P` / `_N` (case-insensitive) grouped by stem. Both halves present → candidate matched pair.
|
- **Diff pairs**: signal names ending `_P` / `_N` (case-insensitive) grouped by stem. Both halves present → candidate matched pair.
|
||||||
- **Diff buses**: ≥ 2 matched diff pairs whose pair-stems share a common outer-stem after stripping a trailing integer (`MDI0` / `MDI1` / `MDI2` → outer `MDI` + indices). The strict `_` rule from plain buses does NOT apply to this trailing-index split: `_P`/`_N` was already stripped, so we know remaining digits are an index. Two index variants accepted: contiguous (`MDI0`) and underscore-separated (`PCIE_TX_0`). Emitted as `SignalGroup{kind=DiffBus, lo, hi}` with label `OUTER[lo..hi]_P/N`. Members include all 2·N constituent signals. A "bus" of size 1 falls back to `DiffPair` (single index does not a bus make).
|
- **Diff buses**: ≥ 2 matched diff pairs whose pair-stems share a common outer-stem after stripping a trailing integer (`MDI0` / `MDI1` / `MDI2` → outer `MDI` + indices). The strict `_` rule from plain buses does NOT apply to this trailing-index split: `_P`/`_N` was already stripped, so we know remaining digits are an index. Two index variants accepted: contiguous (`MDI0`) and underscore-separated (`PCIE_TX_0`). Emitted as `SignalGroup{kind=DiffBus, lo, hi}` with label `OUTER[lo..hi]_P/N`. Members include all 2·N constituent signals. A "bus" of size 1 falls back to `DiffPair` (single index does not a bus make).
|
||||||
@@ -145,13 +184,13 @@ Exposed as the `analyze` shell command which prints groups (sorted by module + l
|
|||||||
|
|
||||||
**Component classification**: every `Part` carries a `ComponentKind kind` (`Passive | Semiconductor | IntegratedCircuit | Connector | TestPoint | Switch | Crystal | Mechanical | Other`) inferred at construction by `infer_component_kind(name)` from the leading reference-designator letter(s) (longest-match: `LED/TP/SW/FB/MK/MP/MH/HS/RA/RN/RP/RV` first, then single-letter R/C/L/F/D/Q/U/J/P/Y/X/S). Recomputed on `restore` (no persistence tag). Not yet exposed in TUI commands — branchpoints will be `set-connector-type` guard, `explore` filter, and `explore` header.
|
**Component classification**: every `Part` carries a `ComponentKind kind` (`Passive | Semiconductor | IntegratedCircuit | Connector | TestPoint | Switch | Crystal | Mechanical | Other`) inferred at construction by `infer_component_kind(name)` from the leading reference-designator letter(s) (longest-match: `LED/TP/SW/FB/MK/MP/MH/HS/RA/RN/RP/RV` first, then single-letter R/C/L/F/D/Q/U/J/P/Y/X/S). Recomputed on `restore` (no persistence tag). Not yet exposed in TUI commands — branchpoints will be `set-connector-type` guard, `explore` filter, and `explore` header.
|
||||||
|
|
||||||
`SignalType` lives in its own header `src/system/signal_type.hpp` (extracted from signals to avoid a pins↔signals include cycle).
|
`SignalType` lives in its own header `src/core/domain/signal_type.hpp` (extracted from signals to avoid a pins↔signals include cycle).
|
||||||
|
|
||||||
**Pins** are either NC (`signal() == nullptr`) or connected to exactly one signal. The ODS importer creates a Pin for every row that has a non-empty pin name, even when the signal column is empty or `"NC"` — the pin stays in the Part as NC. `restore` replaces `Tui::sys` entirely (`unique_ptr::reset`). Names are stored as-is — must not contain TAB or newline (true for the EE netlists we ingest). Format is versioned by the `# essim system snapshot v1` header for future compatibility.
|
**Pins** are either NC (`signal() == nullptr`) or connected to exactly one signal. The ODS importer creates a Pin for every row that has a non-empty pin name, even when the signal column is empty or `"NC"` — the pin stays in the Part as NC. `restore` replaces `Tui::sys` entirely (`unique_ptr::reset`). Names are stored as-is — must not contain TAB or newline (true for the EE netlists we ingest). Format is versioned by the `# essim system snapshot v1` header for future compatibility.
|
||||||
|
|
||||||
**NC origin tag**: each `Pin` carries `NcOrigin nc_origin` (`None | ImportedUnconnected | DroppedSingleton`, default `None`). Set in three places: (a) Mentor importer when the signal field starts with `unconnected` → `ImportedUnconnected`; (b) `drop_singleton_signals(Signals*)` called at the end of `load` → `DroppedSingleton` on each detached pin (signals with exactly one pin are NC by definition — see commits motivating this); (c) `duplicate` propagates the tag. Pins materialised by `FillIdentityNCs` keep `None` — they have no local signal but are bridged via `pin_map` and shouldn't be counted as orphans. The tag is persisted (see `N` record), reported as a total in `verify`, and tested in `tests/test_nc_origin.cpp`.
|
**NC origin tag**: each `Pin` carries `NcOrigin nc_origin` (`None | ImportedUnconnected | DroppedSingleton`, default `None`). Set in three places: (a) Mentor importer when the signal field starts with `unconnected` → `ImportedUnconnected`; (b) `drop_singleton_signals(Signals*)` called at the end of `load` → `DroppedSingleton` on each detached pin (signals with exactly one pin are NC by definition — see commits motivating this); (c) `duplicate` propagates the tag. Pins materialised by `FillIdentityNCs` keep `None` — they have no local signal but are bridged via `pin_map` and shouldn't be counted as orphans. The tag is persisted (see `N` record), reported as a total in `verify`, and tested in `tests/test_nc_origin.cpp`.
|
||||||
|
|
||||||
**Connector types & transforms**: every `Part` carries a `connector_type` string (default `""`, set via the `set-connector-type` command — inline `set-connector-type m p kind` or bare which opens a TUI screen with module menu, part filter+menu, type input, list of types already in use, and an Apply button). When `connect` validates a pair, it consults `TransformRegistry::lookup(p1->connector_type, p2->connector_type)` (defined in `src/system/transform.{hpp,cpp}`) — both directions of the pair are tried. If neither is registered, an `IdentityTransform` fallback wires each pin of A to the canonical-equivalent pin of B (when present). The resulting `(Pin*, Pin*)` list and the transform's name are stored on the `Connection` (`pin_map`, `transform_name`). To register a real transform: define a `Transform` subclass in `transform.cpp` and call `TransformRegistry::get().add("kindA", "kindB", new MyTransform())` at init — there's no startup hook for this yet, so a small `RegisterBuiltinTransforms()` helper is the natural place to add when more types appear.
|
**Connector types & transforms**: every `Part` carries a `connector_type` string (default `""`, set via the `set-connector-type` command — inline `set-connector-type m p kind` or bare which opens a TUI screen with module menu, part filter+menu, type input, list of types already in use, and an Apply button). When `connect` validates a pair, it consults `TransformRegistry::lookup(p1->connector_type, p2->connector_type)` (defined in `src/core/domain/transform.{hpp,cpp}`) — both directions of the pair are tried. If neither is registered, an `IdentityTransform` fallback wires each pin of A to the canonical-equivalent pin of B (when present). The resulting `(Pin*, Pin*)` list and the transform's name are stored on the `Connection` (`pin_map`, `transform_name`). To register a real transform: define a `Transform` subclass in `transform.cpp` and call `TransformRegistry::get().add("kindA", "kindB", new MyTransform())` at init — there's no startup hook for this yet, so a small `RegisterBuiltinTransforms()` helper is the natural place to add when more types appear.
|
||||||
|
|
||||||
**Identity wiring uses canonical names**: `IdentityTransform::apply` builds `unordered_map<canonical, Pin*>` for side B and looks up each side-A pin by its canonical form. So `A1` (one card) auto-pairs with `A001` (the other) thanks to `canonical_pin_name` (`pre + zero-padded(3) digit suffix`; mixed/non-numeric returns the original). Same canonicalisation in `CheckIdentityCompatible`. **`pin_role` doesn't need canonicalisation** because `parse_pin` extracts `(col, row)` via `stoi` which already strips leading zeros.
|
**Identity wiring uses canonical names**: `IdentityTransform::apply` builds `unordered_map<canonical, Pin*>` for side B and looks up each side-A pin by its canonical form. So `A1` (one card) auto-pairs with `A001` (the other) thanks to `canonical_pin_name` (`pre + zero-padded(3) digit suffix`; mixed/non-numeric returns the original). Same canonicalisation in `CheckIdentityCompatible`. **`pin_role` doesn't need canonicalisation** because `parse_pin` extracts `(col, row)` via `stoi` which already strips leading zeros.
|
||||||
|
|
||||||
@@ -188,7 +227,7 @@ Today the only caller is `export` (`{"CSV", ".csv"}, {"ODS", ".ods"}` filters, k
|
|||||||
|
|
||||||
**Per-key path persistence** (`SaveLastUsed(key, dir, filename)` / `LoadLastUsed(key, &dir, &filename)` in `shell.cpp`): each key writes a tiny two-line file (`dir\nfilename\n`) under `UserDataDir() / <key>.last`. `UserDataDir()` is the cross-platform `XDG_DATA_HOME` / `LOCALAPPDATA` etc. helper also used by the command history file. Free functions, not Tui members, so any module (the file dialog today; could be the script-save buffer or the save command tomorrow) can use them with the same minimal API.
|
**Per-key path persistence** (`SaveLastUsed(key, dir, filename)` / `LoadLastUsed(key, &dir, &filename)` in `shell.cpp`): each key writes a tiny two-line file (`dir\nfilename\n`) under `UserDataDir() / <key>.last`. `UserDataDir()` is the cross-platform `XDG_DATA_HOME` / `LOCALAPPDATA` etc. helper also used by the command history file. Free functions, not Tui members, so any module (the file dialog today; could be the script-save buffer or the save command tomorrow) can use them with the same minimal API.
|
||||||
|
|
||||||
**ODS writer** (`src/imports/ods_writer.{hpp,cpp}`): minimal OpenDocument Spreadsheet writer. Backed by libzip + pugixml (both already pulled in by the ODS importer). Class hierarchy is two structs:
|
**ODS writer** (`src/core/imports/ods_writer.{hpp,cpp}`): minimal OpenDocument Spreadsheet writer. Backed by libzip + pugixml (both already pulled in by the ODS importer). Class hierarchy is two structs:
|
||||||
|
|
||||||
- `OdsSheet` — sparse row-major grid of string cells (`set(row, col, value)`).
|
- `OdsSheet` — sparse row-major grid of string cells (`set(row, col, value)`).
|
||||||
- `OdsWriter` — owns the sheets, emits a valid `.ods` archive with `mimetype` (stored uncompressed, magic header), `META-INF/manifest.xml`, and `content.xml`.
|
- `OdsWriter` — owns the sheets, emits a valid `.ods` archive with `mimetype` (stored uncompressed, magic header), `META-INF/manifest.xml`, and `content.xml`.
|
||||||
@@ -248,9 +287,9 @@ Everything in this section is a precise description of how signals, pins, parts,
|
|||||||
|
|
||||||
Default: `Signal::type = SignalType::Other` (constructor does no inference).
|
Default: `Signal::type = SignalType::Other` (constructor does no inference).
|
||||||
|
|
||||||
Type is set by `infer_signal_types(System*)` (`src/system/analysis.cpp`), called at the end of every `load` and after `duplicate`. The decision per signal:
|
Type is set by `infer_signal_types(System*)` (`src/core/domain/analysis.cpp`), called at the end of every `load` and after `duplicate`. The decision per signal:
|
||||||
|
|
||||||
1. Compute `named = infer_signal_type(name)` (`src/system/signals.cpp`):
|
1. Compute `named = infer_signal_type(name)` (`src/core/domain/signals.cpp`):
|
||||||
- `GndShield` if name matches **case-insensitively**: `GND`, `GROUND`, `EARTH`, `SHIELD`, `CHASSIS`, or starts with `GND_` / `GROUND_` / `EARTH_` / `SHIELD_` / `CHASSIS_`.
|
- `GndShield` if name matches **case-insensitively**: `GND`, `GROUND`, `EARTH`, `SHIELD`, `CHASSIS`, or starts with `GND_` / `GROUND_` / `EARTH_` / `SHIELD_` / `CHASSIS_`.
|
||||||
- `Power` if the name contains any of `PWR`, `POWER`, `VCC`, `VDD`, `VEE`, `VSS`, `VBAT`, or starts with `VS_`, `VS3_`, `+`, `-` followed by a digit.
|
- `Power` if the name contains any of `PWR`, `POWER`, `VCC`, `VDD`, `VEE`, `VSS`, `VBAT`, or starts with `VS_`, `VS3_`, `+`, `-` followed by a digit.
|
||||||
- Else `Other`.
|
- Else `Other`.
|
||||||
@@ -264,15 +303,15 @@ Type is set by `infer_signal_types(System*)` (`src/system/analysis.cpp`), called
|
|||||||
|
|
||||||
### NC pin origin
|
### NC pin origin
|
||||||
|
|
||||||
`Pin::nc_origin` (`src/system/pins.hpp`). Default `NcOrigin::None`. Set by:
|
`Pin::nc_origin` (`src/core/domain/pins.hpp`). Default `NcOrigin::None`. Set by:
|
||||||
|
|
||||||
- **`NcOrigin::ImportedUnconnected`** — Mentor importer (`src/imports/import_mentor.cpp`): the signal field of an `Explicit Pin:` row starts case-insensitively with `unconnected` (e.g. `'unconnected'`, `'unconnected (by TERM)'`). The pin is kept on the part with no signal.
|
- **`NcOrigin::ImportedUnconnected`** — Mentor importer (`src/core/imports/import_mentor.cpp`): the signal field of an `Explicit Pin:` row starts case-insensitively with `unconnected` (e.g. `'unconnected'`, `'unconnected (by TERM)'`). The pin is kept on the part with no signal.
|
||||||
- **`NcOrigin::DroppedSingleton`** — `drop_singleton_signals(Signals*)` (`src/system/signals.cpp`) called at the end of `load`: any signal whose pin set has size exactly 1 is unconnected by definition. The pin is detached (`sig = nullptr`) and tagged; the `Signal` object is deleted.
|
- **`NcOrigin::DroppedSingleton`** — `drop_singleton_signals(Signals*)` (`src/core/domain/signals.cpp`) called at the end of `load`: any signal whose pin set has size exactly 1 is unconnected by definition. The pin is detached (`sig = nullptr`) and tagged; the `Signal` object is deleted.
|
||||||
- **`NcOrigin::None` (no tag)** — pins materialised by `FillIdentityNCs` at `connect` time. These are unconnected locally but bridged to a real signal on the peer module via `Connection::pin_map`; they are explicitly excluded from the "orphan" count in `verify` and the analyze screen.
|
- **`NcOrigin::None` (no tag)** — pins materialised by `FillIdentityNCs` at `connect` time. These are unconnected locally but bridged to a real signal on the peer module via `Connection::pin_map`; they are explicitly excluded from the "orphan" count in `verify` and the analyze screen.
|
||||||
|
|
||||||
### Signal groups
|
### Signal groups
|
||||||
|
|
||||||
`analyze_system(System*)` (`src/system/analysis.cpp`) emits `SignalGroup`s **per module** (signals are module-scoped). Internal names (starting with `$` — Mentor's `$Nxxxx` convention) are skipped wholesale at every step.
|
`analyze_system(System*)` (`src/core/domain/analysis.cpp`) emits `SignalGroup`s **per module** (signals are module-scoped). Internal names (starting with `$` — Mentor's `$Nxxxx` convention) are skipped wholesale at every step.
|
||||||
|
|
||||||
**DiffPair** (`GroupKind::DiffPair`):
|
**DiffPair** (`GroupKind::DiffPair`):
|
||||||
- Signal name ends `_P` or `_N` (case-insensitive). The character before the suffix must be `_`.
|
- Signal name ends `_P` or `_N` (case-insensitive). The character before the suffix must be `_`.
|
||||||
@@ -303,7 +342,7 @@ The `verify` command (not the analyze screen, yet) also emits the **model-driven
|
|||||||
|
|
||||||
### Component kind
|
### Component kind
|
||||||
|
|
||||||
`Part::kind` is inferred at construction (`src/system/component_kind.cpp`) from the leading reference-designator letter(s) of the part name. **Longest-match wins**:
|
`Part::kind` is inferred at construction (`src/core/domain/component_kind.cpp`) from the leading reference-designator letter(s) of the part name. **Longest-match wins**:
|
||||||
|
|
||||||
- Two-letter prefixes (checked first, case-insensitive): `LED → Semiconductor`, `TP → TestPoint`, `SW → Switch`, `FB → Passive`, `MK / MP / MH → Mechanical`, `HS → Mechanical`, `RA / RN / RP / RV → Passive`.
|
- Two-letter prefixes (checked first, case-insensitive): `LED → Semiconductor`, `TP → TestPoint`, `SW → Switch`, `FB → Passive`, `MK / MP / MH → Mechanical`, `HS → Mechanical`, `RA / RN / RP / RV → Passive`.
|
||||||
- Single-letter fallback: `R / C / L / F → Passive`, `D / Q → Semiconductor`, `U → IntegratedCircuit`, `J / P → Connector`, `Y / X → Crystal`, `S → Switch`.
|
- Single-letter fallback: `R / C / L / F → Passive`, `D / Q → Semiconductor`, `U → IntegratedCircuit`, `J / P → Connector`, `Y / X → Crystal`, `S → Switch`.
|
||||||
@@ -315,7 +354,7 @@ Recomputed on `restore` (no persistence tag). Currently not used by any decision
|
|||||||
|
|
||||||
`connect` looks up a registered transform for `(p1->connector_type, p2->connector_type)` via `TransformRegistry::lookup`, tried in both directions. Fall-through is `IdentityTransform`:
|
`connect` looks up a registered transform for `(p1->connector_type, p2->connector_type)` via `TransformRegistry::lookup`, tried in both directions. Fall-through is `IdentityTransform`:
|
||||||
|
|
||||||
- Compares pin sets by **canonical name** (`canonical_pin_name(s)`, `src/system/pin_name.cpp`): split into prefix + digit suffix; if the suffix is pure digits, zero-pad to width 3 (`A1` → `A001`, `A001` → `A001`, `A1B` → `A1B`, `VCC` → `VCC`).
|
- Compares pin sets by **canonical name** (`canonical_pin_name(s)`, `src/core/domain/pin_name.cpp`): split into prefix + digit suffix; if the suffix is pure digits, zero-pad to width 3 (`A1` → `A001`, `A001` → `A001`, `A1B` → `A1B`, `VCC` → `VCC`).
|
||||||
- `CheckIdentityCompatible(a, b)` accepts the **subset case** (one side's canonical set is a subset of the other's — typical because Altium drops NC, Mentor doesn't). Bidirectional mismatch (both sides have orphans) is refused.
|
- `CheckIdentityCompatible(a, b)` accepts the **subset case** (one side's canonical set is a subset of the other's — typical because Altium drops NC, Mentor doesn't). Bidirectional mismatch (both sides have orphans) is refused.
|
||||||
- After acceptance, `FillIdentityNCs(p1, p2)` **materialises** the missing canonical positions on the smaller side as new NC pins (`new Pin(other_side_name)`, no signal attached, `nc_origin = None`). Idempotent.
|
- After acceptance, `FillIdentityNCs(p1, p2)` **materialises** the missing canonical positions on the smaller side as new NC pins (`new Pin(other_side_name)`, no signal attached, `nc_origin = None`). Idempotent.
|
||||||
|
|
||||||
|
|||||||
35
README.md
35
README.md
@@ -15,6 +15,13 @@ cmake --build build -j
|
|||||||
./build/essim
|
./build/essim
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The build is **layered**: `essim_core` is the frontend-agnostic business
|
||||||
|
library (domain + importers + operations); the `essim` binary comes from a
|
||||||
|
**frontend** under `src/frontends/<name>/` that links it. Select one with
|
||||||
|
`-DESSIM_FRONTEND=<name>` (default `tui`); `-DESSIM_FRONTEND=none` builds the
|
||||||
|
core + tests only, with no GUI toolkit fetched. Architecture in
|
||||||
|
[`DESIGN.md`](DESIGN.md).
|
||||||
|
|
||||||
Inside the shell, type `help` for the live command list — or read the
|
Inside the shell, type `help` for the live command list — or read the
|
||||||
auto-generated reference at [`doc/user/commands.md`](doc/user/commands.md).
|
auto-generated reference at [`doc/user/commands.md`](doc/user/commands.md).
|
||||||
A worked bring-up script is at [`test/system.essim`](test/system.essim);
|
A worked bring-up script is at [`test/system.essim`](test/system.essim);
|
||||||
@@ -41,10 +48,14 @@ Step-by-step walkthroughs for both the batch and TUI workflows are in
|
|||||||
`../libbsdl`, pulled in via `add_subdirectory` and linked dynamically.
|
`../libbsdl`, pulled in via `add_subdirectory` and linked dynamically.
|
||||||
Override its location with `-DBSDL_DIR=/path/to/libbsdl`. Powers the
|
Override its location with `-DBSDL_DIR=/path/to/libbsdl`. Powers the
|
||||||
`attach-bsdl` command and the pin/JTAG checks.
|
`attach-bsdl` command and the pin/JTAG checks.
|
||||||
- Fetched automatically at configure time via `FetchContent` (nothing to
|
- Fetched automatically via `FetchContent` (nothing to install): **FTXUI**
|
||||||
install): **FTXUI** v6.1.9 and **doctest** v2.4.11.
|
v6.1.9 — only when building the **tui** frontend — and **doctest** v2.4.11
|
||||||
|
for the tests.
|
||||||
- Optional, only for the `doc` target: **doxygen** and **python3**.
|
- Optional, only for the `doc` target: **doxygen** and **python3**.
|
||||||
|
|
||||||
|
libzip, pugixml and libbsdl are the **core** dependencies; FTXUI belongs to the
|
||||||
|
tui frontend, so a `-DESSIM_FRONTEND=none` build needs none of it.
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -53,6 +64,9 @@ Step-by-step walkthroughs for both the batch and TUI workflows are in
|
|||||||
ctest --test-dir build
|
ctest --test-dir build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`ctest` runs `essim_tests` (core — links `essim_core`, no GUI toolkit) and
|
||||||
|
`essim_tui_tests` (the FTXUI frontend's tests, under `tests/tui/`).
|
||||||
|
|
||||||
Skip building tests entirely:
|
Skip building tests entirely:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -76,12 +90,17 @@ cmake --build build --target doc # needs doxygen + python3
|
|||||||
## Project layout
|
## Project layout
|
||||||
|
|
||||||
```
|
```
|
||||||
src/system/ domain model (Module/Part/Pin/Signal, Connection, Transform, …)
|
src/
|
||||||
src/imports/ Mentor / Altium / ODS netlist importers
|
core/ business logic, NO GUI toolkit (→ libessim_core)
|
||||||
src/tui/ FTXUI shell (commands, screens, completion, history)
|
domain/ model (Module/Part/Pin/Signal, Connection, Transform…) + analyses
|
||||||
tests/ doctest suite
|
imports/ Mentor / Altium / ODS netlist importers + ODS writer
|
||||||
doc/ api/ + user/ Markdown trees, Doxyfile.in, gen_api_md.py
|
app/ use-case operations (export → CSV/ODS, …)
|
||||||
test/ sample netlists + system.essim bring-up script
|
frontends/ one dir per GUI/TUI engine, each links essim_core
|
||||||
|
tui/ FTXUI shell + main.cpp (→ libessim_tui + the `essim` binary)
|
||||||
|
tests/ core tests (link essim_core)
|
||||||
|
tui/ frontend tests (link essim_tui)
|
||||||
|
doc/ api/ + user/ Markdown, Doxyfile.in, gen_api_md.py
|
||||||
|
test/ sample netlists + system.essim bring-up script
|
||||||
```
|
```
|
||||||
|
|
||||||
Full layout & rationale in [`DESIGN.md`](DESIGN.md).
|
Full layout & rationale in [`DESIGN.md`](DESIGN.md).
|
||||||
|
|||||||
67
src/core/app/connect.cpp
Normal file
67
src/core/app/connect.cpp
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
#include "core/app/connect.hpp"
|
||||||
|
|
||||||
|
#include "core/domain/connect.hpp"
|
||||||
|
#include "core/domain/modules.hpp"
|
||||||
|
#include "core/domain/parts.hpp"
|
||||||
|
#include "core/domain/system.hpp"
|
||||||
|
#include "core/domain/transform.hpp"
|
||||||
|
|
||||||
|
#include <exception>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace app {
|
||||||
|
|
||||||
|
ConnectResult connect_parts(System *sys, Module *m1, Part *p1,
|
||||||
|
Module *m2, Part *p2)
|
||||||
|
{
|
||||||
|
ConnectResult r;
|
||||||
|
|
||||||
|
auto ® = TransformRegistry::get();
|
||||||
|
Transform *t = reg.lookup(p1->connector_type, p2->connector_type);
|
||||||
|
bool both_empty = p1->connector_type.empty() && p2->connector_type.empty();
|
||||||
|
|
||||||
|
if (t == reg.identity()) {
|
||||||
|
if (!both_empty) {
|
||||||
|
r.refused = true;
|
||||||
|
r.error = "no transform for types '"
|
||||||
|
+ (p1->connector_type.empty() ? std::string("(none)")
|
||||||
|
: p1->connector_type)
|
||||||
|
+ "' ↔ '"
|
||||||
|
+ (p2->connector_type.empty() ? std::string("(none)")
|
||||||
|
: p2->connector_type)
|
||||||
|
+ "'. Set matching types via 'set-connector-type' first.";
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
std::string info;
|
||||||
|
std::string err = CheckIdentityCompatible(p1, p2, &info);
|
||||||
|
if (!err.empty()) {
|
||||||
|
r.refused = true;
|
||||||
|
r.error = err;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
if (!info.empty()) {
|
||||||
|
r.identity_info = info;
|
||||||
|
r.nc_added = FillIdentityNCs(p1, p2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto pin_map = t->apply(p1, p2);
|
||||||
|
|
||||||
|
r.connection_name = m1->name + "/" + p1->name
|
||||||
|
+ " <-> " + m2->name + "/" + p2->name;
|
||||||
|
r.transform_name = t->name;
|
||||||
|
try {
|
||||||
|
Connection *c = new Connection(r.connection_name, m1, p1, m2, p2);
|
||||||
|
c->transform_name = t->name;
|
||||||
|
c->pin_map = std::move(pin_map);
|
||||||
|
sys->connections()->add(c);
|
||||||
|
r.wires = (int)c->pin_map.size();
|
||||||
|
r.ok = true;
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
r.error = e.what();
|
||||||
|
}
|
||||||
|
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace app
|
||||||
42
src/core/app/connect.hpp
Normal file
42
src/core/app/connect.hpp
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#ifndef _APP_CONNECT_HPP_
|
||||||
|
#define _APP_CONNECT_HPP_
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class System;
|
||||||
|
class Module;
|
||||||
|
class Part;
|
||||||
|
|
||||||
|
// Application layer: UI-independent operations that any frontend (TUI, GUI, …)
|
||||||
|
// can call. No console, no dialogs, no FTXUI — just System in, result out.
|
||||||
|
namespace app {
|
||||||
|
|
||||||
|
// Outcome of connecting two parts. The side effects (filling identity NC pins,
|
||||||
|
// creating the Connection and adding it to the system) all happen in core; the
|
||||||
|
// caller only renders the fields.
|
||||||
|
struct ConnectResult {
|
||||||
|
bool ok = false; ///< a Connection was created and added
|
||||||
|
bool refused = false; ///< a business rule rejected it (vs. an exception)
|
||||||
|
std::string error; ///< why refused/failed; empty when ok
|
||||||
|
|
||||||
|
std::string connection_name;
|
||||||
|
std::string transform_name;
|
||||||
|
int wires = 0; ///< pin_map size of the created connection
|
||||||
|
|
||||||
|
// Identity-transform path only: the compatibility info line and how many NC
|
||||||
|
// pins were materialised so both sides match. Empty / 0 otherwise.
|
||||||
|
std::string identity_info;
|
||||||
|
int nc_added = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Wire part `p1` (in module `m1`) to part `p2` (in module `m2`): look up the
|
||||||
|
// transform for their connector types, refuse on an unknown pairing or an
|
||||||
|
// identity-incompatible layout, fill identity NC pins when needed, apply the
|
||||||
|
// transform and create the Connection. Pure core — no resolution of names or
|
||||||
|
// patterns (the frontend turns user input into the Module*/Part* it passes).
|
||||||
|
ConnectResult connect_parts(System *sys, Module *m1, Part *p1,
|
||||||
|
Module *m2, Part *p2);
|
||||||
|
|
||||||
|
} // namespace app
|
||||||
|
|
||||||
|
#endif // _APP_CONNECT_HPP_
|
||||||
190
src/core/app/export.cpp
Normal file
190
src/core/app/export.cpp
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
#include "core/app/export.hpp"
|
||||||
|
|
||||||
|
#include "core/imports/ods_writer.hpp"
|
||||||
|
#include "core/domain/connect.hpp"
|
||||||
|
#include "core/domain/modules.hpp"
|
||||||
|
#include "core/domain/parts.hpp"
|
||||||
|
#include "core/domain/pins.hpp"
|
||||||
|
#include "core/domain/signal_type.hpp"
|
||||||
|
#include "core/domain/signals.hpp"
|
||||||
|
#include "core/domain/system.hpp"
|
||||||
|
|
||||||
|
#include <cctype>
|
||||||
|
#include <fstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace app {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::string to_lower(std::string s)
|
||||||
|
{
|
||||||
|
for (char &c : s) c = (char)std::tolower((unsigned char)c);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minimal CSV quoter — wraps in "…" and doubles internal quotes when the field
|
||||||
|
// contains a comma, quote, or newline.
|
||||||
|
std::string csv_quote(const std::string &s)
|
||||||
|
{
|
||||||
|
if (s.find_first_of(",\"\n") == std::string::npos)
|
||||||
|
return s;
|
||||||
|
std::string out = "\"";
|
||||||
|
for (char c : s) { if (c == '"') out += '"'; out += c; }
|
||||||
|
out += '"';
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flatten one pin into the string slots an export row uses.
|
||||||
|
void pin_side(Pin *p, std::string &mod, std::string &part, std::string &pin,
|
||||||
|
std::string &sig, std::string &type, std::string &suspect)
|
||||||
|
{
|
||||||
|
if (!p) { mod = part = pin = sig = type = suspect = ""; return; }
|
||||||
|
mod = (p->prnt && p->prnt->prnt) ? p->prnt->prnt->name : "";
|
||||||
|
part = p->prnt ? p->prnt->name : "";
|
||||||
|
pin = p->name;
|
||||||
|
Signal *s = p->signal();
|
||||||
|
if (!s) {
|
||||||
|
sig = ""; type = "(NC)"; suspect = "";
|
||||||
|
} else {
|
||||||
|
sig = s->name;
|
||||||
|
type = signal_type_name(s->type);
|
||||||
|
suspect = (infer_signal_type(s->name) == SignalType::Power
|
||||||
|
&& s->type == SignalType::Other) ? "yes" : "no";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Excel rejects /\?*:[] in sheet names; ODS forbids < > & in raw table names.
|
||||||
|
// Sanitise to underscores and clip to Excel's 31-char hard limit.
|
||||||
|
std::string sanitise_sheet_name(std::string name)
|
||||||
|
{
|
||||||
|
for (char &ch : name)
|
||||||
|
if (ch == '/' || ch == '\\' || ch == '?' || ch == '*' || ch == ':'
|
||||||
|
|| ch == '[' || ch == ']' || ch == '<' || ch == '>' || ch == '&')
|
||||||
|
ch = '_';
|
||||||
|
if (name.size() > 31) name = name.substr(0, 31);
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExportResult write_ods(const System *sys, const std::string &path)
|
||||||
|
{
|
||||||
|
ExportResult r;
|
||||||
|
OdsWriter w;
|
||||||
|
for (auto &ckv : *sys->connections()) {
|
||||||
|
Connection *c = ckv.second;
|
||||||
|
OdsSheet *s = w.add_sheet(sanitise_sheet_name(c->name));
|
||||||
|
|
||||||
|
std::string lmod = c->m1 ? c->m1->name : "";
|
||||||
|
std::string lprt = c->p1 ? c->p1->name : "";
|
||||||
|
std::string rmod = c->m2 ? c->m2->name : "";
|
||||||
|
std::string rprt = c->p2 ? c->p2->name : "";
|
||||||
|
|
||||||
|
// Meta header: label/value rows, a blank row, then the column headers.
|
||||||
|
auto meta = [&](int row, const std::string &k, const std::string &v) {
|
||||||
|
s->set(row, 0, k);
|
||||||
|
s->set(row, 1, v);
|
||||||
|
};
|
||||||
|
meta(0, "Connection", c->name);
|
||||||
|
meta(1, "Transform", c->transform_name);
|
||||||
|
meta(2, "Left", lmod + " / " + lprt);
|
||||||
|
meta(3, "Right", rmod + " / " + rprt);
|
||||||
|
// Row 4 left blank by design.
|
||||||
|
|
||||||
|
const int HDR = 5;
|
||||||
|
s->set_header_row(HDR);
|
||||||
|
const char *hdr[] = {
|
||||||
|
"left_pin", "left_signal", "left_type", "left_suspect",
|
||||||
|
"right_pin", "right_signal", "right_type", "right_suspect",
|
||||||
|
"type_mismatch"};
|
||||||
|
for (int i = 0; i < 9; ++i) s->set(HDR, i, hdr[i]);
|
||||||
|
|
||||||
|
int row = HDR + 1;
|
||||||
|
for (auto &wp : c->pin_map) {
|
||||||
|
std::string lm, lp, ln, ls, lt, lsus;
|
||||||
|
std::string rm, rp, rn, rs, rt, rsus;
|
||||||
|
pin_side(wp.first, lm, lp, ln, ls, lt, lsus);
|
||||||
|
pin_side(wp.second, rm, rp, rn, rs, rt, rsus);
|
||||||
|
std::string tm = (lt != "(NC)" && rt != "(NC)" && lt != rt) ? "yes" : "no";
|
||||||
|
s->set(row, 0, ln); s->set(row, 1, ls);
|
||||||
|
s->set(row, 2, lt); s->set(row, 3, lsus);
|
||||||
|
s->set(row, 4, rn); s->set(row, 5, rs);
|
||||||
|
s->set(row, 6, rt); s->set(row, 7, rsus);
|
||||||
|
s->set(row, 8, tm);
|
||||||
|
++row; ++r.rows;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string err;
|
||||||
|
if (!w.save(path, err)) {
|
||||||
|
r.error = err;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
r.ok = true;
|
||||||
|
r.sheets = (int)sys->connections()->size();
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExportResult write_csv(const System *sys, const std::string &path)
|
||||||
|
{
|
||||||
|
ExportResult r;
|
||||||
|
std::ofstream f(path);
|
||||||
|
if (!f) {
|
||||||
|
r.error = "cannot open '" + path + "' for writing";
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One rectangular table: each row repeats the per-connection constants so
|
||||||
|
// the file stays parser-friendly (pandas / awk / spreadsheet).
|
||||||
|
f << "connection,transform,"
|
||||||
|
"left_module,left_part,"
|
||||||
|
"left_pin,left_signal,left_type,left_suspect,"
|
||||||
|
"right_module,right_part,"
|
||||||
|
"right_pin,right_signal,right_type,right_suspect,"
|
||||||
|
"type_mismatch\n";
|
||||||
|
|
||||||
|
for (auto &ckv : *sys->connections()) {
|
||||||
|
Connection *c = ckv.second;
|
||||||
|
for (auto &wp : c->pin_map) {
|
||||||
|
std::string lm, lp, ln, ls, lt, lsus;
|
||||||
|
std::string rm, rp, rn, rs, rt, rsus;
|
||||||
|
pin_side(wp.first, lm, lp, ln, ls, lt, lsus);
|
||||||
|
pin_side(wp.second, rm, rp, rn, rs, rt, rsus);
|
||||||
|
std::string tm = (lt != "(NC)" && rt != "(NC)" && lt != rt) ? "yes" : "no";
|
||||||
|
f << csv_quote(c->name) << ','
|
||||||
|
<< csv_quote(c->transform_name) << ','
|
||||||
|
<< csv_quote(lm) << ',' << csv_quote(lp) << ','
|
||||||
|
<< csv_quote(ln) << ',' << csv_quote(ls) << ','
|
||||||
|
<< csv_quote(lt) << ',' << csv_quote(lsus) << ','
|
||||||
|
<< csv_quote(rm) << ',' << csv_quote(rp) << ','
|
||||||
|
<< csv_quote(rn) << ',' << csv_quote(rs) << ','
|
||||||
|
<< csv_quote(rt) << ',' << csv_quote(rsus) << ','
|
||||||
|
<< tm << '\n';
|
||||||
|
++r.rows;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.ok = f.good();
|
||||||
|
if (!r.ok) r.error = "write error on '" + path + "'";
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool export_format_from_path(const std::string &path, ExportFormat &out)
|
||||||
|
{
|
||||||
|
size_t dot = path.rfind('.');
|
||||||
|
std::string ext = (dot == std::string::npos) ? "" : to_lower(path.substr(dot));
|
||||||
|
if (ext == ".csv") { out = ExportFormat::Csv; return true; }
|
||||||
|
if (ext == ".ods") { out = ExportFormat::Ods; return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExportResult export_connections(const System *sys, const std::string &path,
|
||||||
|
ExportFormat format)
|
||||||
|
{
|
||||||
|
ExportResult r;
|
||||||
|
if (!sys) { r.error = "no system"; return r; }
|
||||||
|
return (format == ExportFormat::Ods) ? write_ods(sys, path)
|
||||||
|
: write_csv(sys, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace app
|
||||||
34
src/core/app/export.hpp
Normal file
34
src/core/app/export.hpp
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#ifndef _APP_EXPORT_HPP_
|
||||||
|
#define _APP_EXPORT_HPP_
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class System;
|
||||||
|
|
||||||
|
// Application layer: UI-independent operations that any frontend (TUI, GUI, …)
|
||||||
|
// can call. No console, no dialogs, no FTXUI — just System in, result out.
|
||||||
|
namespace app {
|
||||||
|
|
||||||
|
enum class ExportFormat { Csv, Ods };
|
||||||
|
|
||||||
|
// Outcome of an export. The only side effect is writing the target file; the
|
||||||
|
// caller renders `error` / the stats however it likes.
|
||||||
|
struct ExportResult {
|
||||||
|
bool ok = false;
|
||||||
|
std::string error; ///< human-readable, set when !ok
|
||||||
|
int sheets = 0; ///< ODS: number of sheets (one per connection); 0 for CSV
|
||||||
|
int rows = 0; ///< wires written
|
||||||
|
};
|
||||||
|
|
||||||
|
// Map a filename extension (.csv / .ods, case-insensitive) to a format.
|
||||||
|
// Returns false if the extension is neither.
|
||||||
|
bool export_format_from_path(const std::string &path, ExportFormat &out);
|
||||||
|
|
||||||
|
// Export the system's connections to `path` in `format`. Builds the file and
|
||||||
|
// returns stats or an error. Pure core — safe to call from any frontend.
|
||||||
|
ExportResult export_connections(const System *sys, const std::string &path,
|
||||||
|
ExportFormat format);
|
||||||
|
|
||||||
|
} // namespace app
|
||||||
|
|
||||||
|
#endif // _APP_EXPORT_HPP_
|
||||||
46
src/core/app/load.cpp
Normal file
46
src/core/app/load.cpp
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
#include "core/app/load.hpp"
|
||||||
|
|
||||||
|
#include "core/domain/analysis.hpp" // infer_signal_types, SignalTypeInferenceStats
|
||||||
|
#include "core/domain/modules.hpp"
|
||||||
|
#include "core/domain/signals.hpp" // drop_singleton_signals
|
||||||
|
#include "core/domain/system.hpp"
|
||||||
|
|
||||||
|
#include <cctype>
|
||||||
|
#include <exception>
|
||||||
|
|
||||||
|
namespace app {
|
||||||
|
|
||||||
|
bool import_type_from_name(const std::string &name, ImportType &out)
|
||||||
|
{
|
||||||
|
std::string ls;
|
||||||
|
ls.reserve(name.size());
|
||||||
|
for (char c : name) ls += (char)std::tolower((unsigned char)c);
|
||||||
|
if (ls == "mentor") { out = ImportType::IMPORT_MENTOR; return true; }
|
||||||
|
if (ls == "altium") { out = ImportType::IMPORT_ALTIUM; return true; }
|
||||||
|
if (ls == "ods") { out = ImportType::IMPORT_ODS; return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadResult load_module(System *sys, const std::string &module_name,
|
||||||
|
const std::string &path, ImportType type)
|
||||||
|
{
|
||||||
|
LoadResult r;
|
||||||
|
if (!sys) { r.error = "no system"; return r; }
|
||||||
|
try {
|
||||||
|
sys->Load(module_name, path, type);
|
||||||
|
Module *mod = sys->modules()->get(module_name);
|
||||||
|
r.dropped = drop_singleton_signals(mod->signals);
|
||||||
|
SignalTypeInferenceStats inf = infer_signal_types(sys);
|
||||||
|
r.parts = (int)mod->size();
|
||||||
|
r.signals = (int)mod->signals->size();
|
||||||
|
r.power = inf.power;
|
||||||
|
r.gnd = inf.gnd;
|
||||||
|
r.kept_other = inf.kept_other;
|
||||||
|
r.ok = true;
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
r.error = e.what();
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace app
|
||||||
36
src/core/app/load.hpp
Normal file
36
src/core/app/load.hpp
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
#ifndef _APP_LOAD_HPP_
|
||||||
|
#define _APP_LOAD_HPP_
|
||||||
|
|
||||||
|
#include "core/domain/system.hpp" // ImportType
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// Application layer: UI-independent operations that any frontend (TUI, GUI, …)
|
||||||
|
// can call. No console, no dialogs, no FTXUI — just System in, result out.
|
||||||
|
namespace app {
|
||||||
|
|
||||||
|
// Map an import-type name (mentor / altium / ods, case-insensitive) to an
|
||||||
|
// ImportType. Returns false if the name is none of those.
|
||||||
|
bool import_type_from_name(const std::string &name, ImportType &out);
|
||||||
|
|
||||||
|
// Outcome of loading a module: the post-import counts the caller renders.
|
||||||
|
struct LoadResult {
|
||||||
|
bool ok = false;
|
||||||
|
std::string error; ///< human-readable, set when !ok
|
||||||
|
int parts = 0;
|
||||||
|
int signals = 0;
|
||||||
|
int dropped = 0; ///< singleton/NC signals removed after import
|
||||||
|
int power = 0; ///< signals inferred Power (name + structure)
|
||||||
|
int gnd = 0; ///< signals inferred GndShield (name)
|
||||||
|
int kept_other = 0; ///< name said Power but evidence too weak → kept Other
|
||||||
|
};
|
||||||
|
|
||||||
|
// Import a module from a netlist/pinout file into `sys`, drop singleton signals,
|
||||||
|
// then infer signal types. Returns the counts or an error. Pure core — safe to
|
||||||
|
// call from any frontend.
|
||||||
|
LoadResult load_module(System *sys, const std::string &module_name,
|
||||||
|
const std::string &path, ImportType type);
|
||||||
|
|
||||||
|
} // namespace app
|
||||||
|
|
||||||
|
#endif // _APP_LOAD_HPP_
|
||||||
105
src/core/app/verify.cpp
Normal file
105
src/core/app/verify.cpp
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
#include "core/app/verify.hpp"
|
||||||
|
|
||||||
|
#include "core/domain/bsdl_check.hpp"
|
||||||
|
#include "core/domain/connect.hpp"
|
||||||
|
#include "core/domain/modules.hpp"
|
||||||
|
#include "core/domain/nets.hpp"
|
||||||
|
#include "core/domain/parts.hpp"
|
||||||
|
#include "core/domain/pins.hpp"
|
||||||
|
#include "core/domain/signals.hpp"
|
||||||
|
#include "core/domain/system.hpp"
|
||||||
|
|
||||||
|
#include <unordered_set>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace app {
|
||||||
|
|
||||||
|
VerifyReport verify(System *sys)
|
||||||
|
{
|
||||||
|
VerifyReport r;
|
||||||
|
if (!sys)
|
||||||
|
return r;
|
||||||
|
|
||||||
|
// Pass 1 — typed pins: expected (model) vs actual (net) signal type.
|
||||||
|
for (auto &mkv : *sys->modules()) {
|
||||||
|
Module *mod = mkv.second;
|
||||||
|
for (auto &pkv : *mod) {
|
||||||
|
Part *prt = pkv.second;
|
||||||
|
if (prt->connector_type.empty())
|
||||||
|
continue;
|
||||||
|
for (auto &nkv : *prt) {
|
||||||
|
Pin *pin = nkv.second;
|
||||||
|
++r.typed_pins;
|
||||||
|
SignalType expected = pin->expected_signal_type();
|
||||||
|
if (expected == SignalType::Other)
|
||||||
|
continue;
|
||||||
|
Signal *s = pin->signal();
|
||||||
|
SignalType actual = s ? s->type : SignalType::Other;
|
||||||
|
if (actual == expected)
|
||||||
|
continue;
|
||||||
|
RoleMismatch m;
|
||||||
|
m.module = mod->name;
|
||||||
|
m.part = prt->name;
|
||||||
|
m.pin = pin->name;
|
||||||
|
m.signal = s ? s->name : std::string("(NC)");
|
||||||
|
m.expected = expected;
|
||||||
|
m.actual = actual;
|
||||||
|
r.role_mismatches.push_back(std::move(m));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass 2 — bridged nets: flag Power/GndShield mixing. Compute the nets once
|
||||||
|
// here and reuse them for the model checks below.
|
||||||
|
std::vector<Net> nets = compute_all_nets(sys);
|
||||||
|
r.total_nets = (int)nets.size();
|
||||||
|
for (const Net &n : nets) {
|
||||||
|
if (n.members.size() < 2)
|
||||||
|
continue;
|
||||||
|
++r.bridged_nets;
|
||||||
|
SignalType dom;
|
||||||
|
if (net_type_consistent(n, dom))
|
||||||
|
continue;
|
||||||
|
NetInconsistency ni;
|
||||||
|
for (const auto &mp : n.members)
|
||||||
|
ni.members.push_back({mp.first->name, mp.second->name, mp.second->type});
|
||||||
|
r.net_inconsistencies.push_back(std::move(ni));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass 3 — orphans: pins with no signal and not bridged via a connection.
|
||||||
|
std::unordered_set<Pin *> bridged_pins;
|
||||||
|
for (auto &ckv : *sys->connections())
|
||||||
|
for (auto &wp : ckv.second->pin_map) {
|
||||||
|
if (wp.first) bridged_pins.insert(wp.first);
|
||||||
|
if (wp.second) bridged_pins.insert(wp.second);
|
||||||
|
}
|
||||||
|
for (auto &mkv : *sys->modules())
|
||||||
|
for (auto &pkv : *mkv.second)
|
||||||
|
for (auto &nkv : *pkv.second) {
|
||||||
|
Pin *pin = nkv.second;
|
||||||
|
if (pin->signal() || bridged_pins.count(pin))
|
||||||
|
continue;
|
||||||
|
bool dropped;
|
||||||
|
if (pin->nc_origin == NcOrigin::ImportedUnconnected) {
|
||||||
|
++r.orphan_imported;
|
||||||
|
dropped = false;
|
||||||
|
} else if (pin->nc_origin == NcOrigin::DroppedSingleton) {
|
||||||
|
++r.orphan_dropped;
|
||||||
|
dropped = true;
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
r.orphans.push_back({mkv.first, pkv.first, nkv.first, dropped});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Passes 4-7 — model-driven checks (reuse the nets from pass 2).
|
||||||
|
r.pin_anomalies = check_pin_specs(sys, &nets);
|
||||||
|
r.jtag_anomalies = check_jtag_chain(sys, &nets);
|
||||||
|
r.conflict_anomalies = check_source_conflicts(sys);
|
||||||
|
r.completeness_anomalies = check_bsdl_completeness(sys);
|
||||||
|
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace app
|
||||||
69
src/core/app/verify.hpp
Normal file
69
src/core/app/verify.hpp
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
#ifndef _APP_VERIFY_HPP_
|
||||||
|
#define _APP_VERIFY_HPP_
|
||||||
|
|
||||||
|
#include "core/domain/analysis.hpp" // Anomaly
|
||||||
|
#include "core/domain/signal_type.hpp" // SignalType
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class System;
|
||||||
|
|
||||||
|
namespace app {
|
||||||
|
|
||||||
|
// One typed-pin role mismatch: the connector/BSDL expectation disagrees with
|
||||||
|
// the actual net type.
|
||||||
|
struct RoleMismatch {
|
||||||
|
std::string module, part, pin;
|
||||||
|
std::string signal; ///< signal name, or "(NC)"
|
||||||
|
SignalType expected = SignalType::Other;
|
||||||
|
SignalType actual = SignalType::Other;
|
||||||
|
};
|
||||||
|
|
||||||
|
// One bridged net mixing Power and GndShield, with its members for display.
|
||||||
|
struct NetInconsistency {
|
||||||
|
struct Member { std::string module, signal; SignalType type; };
|
||||||
|
std::vector<Member> members;
|
||||||
|
};
|
||||||
|
|
||||||
|
// One orphan pin: no signal and not bridged via a connection. `dropped` is true
|
||||||
|
// for a dropped singleton (essim detached it), false for an import-time NC.
|
||||||
|
struct OrphanPin {
|
||||||
|
std::string module, part, pin;
|
||||||
|
bool dropped = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The full result of `verify`: structured data only — no strings beyond the
|
||||||
|
// names, no formatting. Frontends (the verify command, the analyze screen, the
|
||||||
|
// dashboard) render it however they like.
|
||||||
|
struct VerifyReport {
|
||||||
|
int typed_pins = 0; ///< pins with a non-Other expectation considered
|
||||||
|
std::vector<RoleMismatch> role_mismatches;
|
||||||
|
|
||||||
|
int total_nets = 0;
|
||||||
|
int bridged_nets = 0;
|
||||||
|
std::vector<NetInconsistency> net_inconsistencies;
|
||||||
|
|
||||||
|
int orphan_imported = 0;
|
||||||
|
int orphan_dropped = 0;
|
||||||
|
std::vector<OrphanPin> orphans; ///< per-pin detail (both origins)
|
||||||
|
|
||||||
|
std::vector<Anomaly> pin_anomalies; ///< check_pin_specs
|
||||||
|
std::vector<Anomaly> jtag_anomalies; ///< check_jtag_chain
|
||||||
|
std::vector<Anomaly> conflict_anomalies; ///< check_source_conflicts
|
||||||
|
std::vector<Anomaly> completeness_anomalies; ///< check_bsdl_completeness
|
||||||
|
|
||||||
|
int orphan_total() const { return orphan_imported + orphan_dropped; }
|
||||||
|
int model_total() const {
|
||||||
|
return (int)(pin_anomalies.size() + jtag_anomalies.size()
|
||||||
|
+ conflict_anomalies.size() + completeness_anomalies.size());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run every verify pass over the system and return the findings. Pure core —
|
||||||
|
// computes the nets once and feeds them to the net-based checks.
|
||||||
|
VerifyReport verify(System *sys);
|
||||||
|
|
||||||
|
} // namespace app
|
||||||
|
|
||||||
|
#endif // _APP_VERIFY_HPP_
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#include "system/component_kind.hpp"
|
#include "core/domain/component_kind.hpp"
|
||||||
|
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
#include "system/nets.hpp"
|
#include "core/domain/nets.hpp"
|
||||||
|
|
||||||
#include "system/connect.hpp"
|
#include "core/domain/connect.hpp"
|
||||||
#include "system/modules.hpp"
|
#include "core/domain/modules.hpp"
|
||||||
#include "system/parts.hpp"
|
#include "core/domain/parts.hpp"
|
||||||
#include "system/pins.hpp"
|
#include "core/domain/pins.hpp"
|
||||||
#include "system/signals.hpp"
|
#include "core/domain/signals.hpp"
|
||||||
#include "system/system.hpp"
|
#include "core/domain/system.hpp"
|
||||||
|
|
||||||
#include <queue>
|
#include <queue>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#include "system/pin_name.hpp"
|
#include "core/domain/pin_name.hpp"
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
56
src/core/domain/system.cpp
Normal file
56
src/core/domain/system.cpp
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
|
||||||
|
#include "system.hpp"
|
||||||
|
|
||||||
|
#include "connect.hpp"
|
||||||
|
#include "modules.hpp"
|
||||||
|
#include "core/imports/import_altium.hpp"
|
||||||
|
#include "core/imports/import_mentor.hpp"
|
||||||
|
#include "core/imports/import_ods.hpp"
|
||||||
|
|
||||||
|
System::System() : mods(nullptr), conns(nullptr)
|
||||||
|
{
|
||||||
|
mods = new Modules();
|
||||||
|
conns = new Connections();
|
||||||
|
}
|
||||||
|
|
||||||
|
System::~System()
|
||||||
|
{
|
||||||
|
delete mods;
|
||||||
|
delete conns;
|
||||||
|
}
|
||||||
|
|
||||||
|
void System::Load(std::string module_name, std::string file_name, ImportType type)
|
||||||
|
{
|
||||||
|
// Build the importer first, based on the import type.
|
||||||
|
ImportBase *imp;
|
||||||
|
if (type == ImportType::IMPORT_MENTOR)
|
||||||
|
{
|
||||||
|
imp = new ImportMentor(file_name);
|
||||||
|
} else if (type == ImportType::IMPORT_ALTIUM)
|
||||||
|
{
|
||||||
|
imp = new ImportAltium(file_name);
|
||||||
|
} else if (type == ImportType::IMPORT_ODS)
|
||||||
|
{
|
||||||
|
imp = new ImportOds(file_name);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw std::runtime_error("Unknown import type");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fail fast on a missing/unreadable file, before touching the module table,
|
||||||
|
// so a failed load never leaves behind an empty module.
|
||||||
|
if (!imp->is_open())
|
||||||
|
{
|
||||||
|
delete imp;
|
||||||
|
throw std::runtime_error("cannot open file: " + file_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creation or retrieval of the module, then parse into it. add() copies the
|
||||||
|
// Part pointers into the module, which takes ownership; deleting the
|
||||||
|
// importer then frees the (now drained) Parts container, not the parts.
|
||||||
|
Module *mod = mods->merge(module_name);
|
||||||
|
imp->parse(mod->signals);
|
||||||
|
mod->add(imp->parts());
|
||||||
|
delete imp;
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#ifndef _SYSTEM_HPP_
|
#ifndef _SYSTEM_HPP_
|
||||||
#define _SYSTEM_HPP_
|
#define _SYSTEM_HPP_
|
||||||
|
|
||||||
#include "imports/import_base.hpp"
|
#include "core/imports/import_base.hpp"
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
class Modules; ///< Forward declaration of the Modules class.
|
class Modules; ///< Forward declaration of the Modules class.
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
#include "import_altium.hpp"
|
#include "import_altium.hpp"
|
||||||
|
|
||||||
#include "system/parts.hpp"
|
#include "core/domain/parts.hpp"
|
||||||
#include "system/pins.hpp"
|
#include "core/domain/pins.hpp"
|
||||||
#include "system/signals.hpp"
|
#include "core/domain/signals.hpp"
|
||||||
|
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
|
||||||
#include "system/parts.hpp"
|
#include "core/domain/parts.hpp"
|
||||||
#include "system/signals.hpp"
|
#include "core/domain/signals.hpp"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Base class for importing data from a file.
|
* @brief Base class for importing data from a file.
|
||||||
@@ -27,11 +27,22 @@ public:
|
|||||||
*
|
*
|
||||||
* @param file_name Name of the file to be imported.
|
* @param file_name Name of the file to be imported.
|
||||||
*/
|
*/
|
||||||
ImportBase(std::string file_name) : file_lines(std::fstream(file_name))
|
ImportBase(std::string file_name) : file_lines(file_name, std::ios::in)
|
||||||
{
|
{
|
||||||
prts = new Parts();
|
prts = new Parts();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the input file was opened successfully.
|
||||||
|
*
|
||||||
|
* Opened read-only, so this is false only when the file is genuinely
|
||||||
|
* missing or unreadable (a read-only but present file still opens).
|
||||||
|
* System::Load checks it to fail fast instead of producing an empty module.
|
||||||
|
*
|
||||||
|
* @return true if the file stream is open.
|
||||||
|
*/
|
||||||
|
bool is_open() const { return file_lines.is_open(); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Pure virtual method for parsing the file.
|
* @brief Pure virtual method for parsing the file.
|
||||||
*
|
*
|
||||||
@@ -53,9 +64,13 @@ public:
|
|||||||
/**
|
/**
|
||||||
* @brief Virtual destructor for ImportBase.
|
* @brief Virtual destructor for ImportBase.
|
||||||
*
|
*
|
||||||
* Ensures proper cleanup of derived classes.
|
* Frees the Parts container object. Only the container is deleted, not the
|
||||||
|
* Part objects it holds: by the time the importer is destroyed those have
|
||||||
|
* been transferred to a Module (SystemElementContainer::add copies the
|
||||||
|
* pointers), which owns and deletes them. The default ~Parts frees the map
|
||||||
|
* without touching the elements, so there is no double free.
|
||||||
*/
|
*/
|
||||||
virtual ~ImportBase() = default;
|
virtual ~ImportBase() { delete prts; }
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // _IMPORT_BASE_HPP_
|
#endif // _IMPORT_BASE_HPP_
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#include "import_mentor.hpp"
|
#include "import_mentor.hpp"
|
||||||
#include "system/pins.hpp"
|
#include "core/domain/pins.hpp"
|
||||||
#include "system/parts.hpp"
|
#include "core/domain/parts.hpp"
|
||||||
|
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -43,16 +43,6 @@ enum class State
|
|||||||
*/
|
*/
|
||||||
ImportMentor::ImportMentor(string filename) : ImportBase(filename) {}
|
ImportMentor::ImportMentor(string filename) : ImportBase(filename) {}
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Destructor for ImportMentor.
|
|
||||||
*
|
|
||||||
* Ensures proper cleanup by calling the base class destructor.
|
|
||||||
*/
|
|
||||||
ImportMentor::~ImportMentor()
|
|
||||||
{
|
|
||||||
ImportBase::~ImportBase();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Parses the file to extract parts, pins, and signals.
|
* @brief Parses the file to extract parts, pins, and signals.
|
||||||
*
|
*
|
||||||
@@ -10,7 +10,6 @@ class ImportMentor : public ImportBase
|
|||||||
public:
|
public:
|
||||||
ImportMentor(std::string filename);
|
ImportMentor(std::string filename);
|
||||||
void parse(Signals *signals) override;
|
void parse(Signals *signals) override;
|
||||||
~ImportMentor();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // _IMPORT_MENTOR_HPP_
|
#endif // _IMPORT_MENTOR_HPP_
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
#include "import_ods.hpp"
|
#include "import_ods.hpp"
|
||||||
|
|
||||||
#include "system/parts.hpp"
|
#include "core/domain/parts.hpp"
|
||||||
#include "system/pins.hpp"
|
#include "core/domain/pins.hpp"
|
||||||
#include "system/signals.hpp"
|
#include "core/domain/signals.hpp"
|
||||||
|
|
||||||
#include <pugixml.hpp>
|
#include <pugixml.hpp>
|
||||||
#include <zip.h>
|
#include <zip.h>
|
||||||
30
src/frontends/frontend.hpp
Normal file
30
src/frontends/frontend.hpp
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#ifndef _FRONTEND_HPP_
|
||||||
|
#define _FRONTEND_HPP_
|
||||||
|
|
||||||
|
#include <iosfwd>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// Abstract entry-point interface every frontend (TUI, GUI, …) implements, so
|
||||||
|
// one shared launcher (frontend_main) can drive any of them: parse argv, run
|
||||||
|
// boot commands, optionally dump output (batch / docs) and enter the event
|
||||||
|
// loop. Lives in the frontends layer — essim_core never depends on it.
|
||||||
|
class Frontend {
|
||||||
|
public:
|
||||||
|
virtual ~Frontend() = default;
|
||||||
|
|
||||||
|
// Dispatch one command synchronously, exactly as if the user typed it
|
||||||
|
// (e.g. "restore foo.essim" or "source bring-up.essim"), before the event
|
||||||
|
// loop starts — used to seed the system at boot.
|
||||||
|
virtual void BootDispatch(const std::string &raw) = 0;
|
||||||
|
|
||||||
|
// Write the command registry as Markdown (used for doc generation).
|
||||||
|
virtual void DumpCommandsMd(std::ostream &out) const = 0;
|
||||||
|
|
||||||
|
// Write the accumulated console output (batch mode: no event loop).
|
||||||
|
virtual void DumpOutput(std::ostream &out) const = 0;
|
||||||
|
|
||||||
|
// Enter the interactive event loop.
|
||||||
|
virtual void Run() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // _FRONTEND_HPP_
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
#include "tui/tui.hpp"
|
#include "frontends/frontend_main.hpp"
|
||||||
|
|
||||||
|
#include "frontends/frontend.hpp"
|
||||||
|
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
@@ -11,16 +13,17 @@ void print_usage(const char *prog) {
|
|||||||
"usage: " << prog << " [--batch] [--source FILE] [--restore FILE]\n"
|
"usage: " << prog << " [--batch] [--source FILE] [--restore FILE]\n"
|
||||||
" " << prog << " --commands-md [FILE]\n"
|
" " << prog << " --commands-md [FILE]\n"
|
||||||
" " << prog << " --help\n"
|
" " << prog << " --help\n"
|
||||||
" (no args) launch the TUI on an empty system.\n"
|
" (no args) launch the interface on an empty system.\n"
|
||||||
" --source FILE after boot, run FILE as an essim script\n"
|
" --source FILE after boot, run FILE as an essim script\n"
|
||||||
" (one command per line; same as the `source`\n"
|
" (one command per line; same as the `source`\n"
|
||||||
" command). Output is in the console screen.\n"
|
" command). Output goes to the console.\n"
|
||||||
" --restore FILE after boot, restore the system snapshot in\n"
|
" --restore FILE after boot, restore the system snapshot in\n"
|
||||||
" FILE (same as the `restore` command).\n"
|
" FILE (same as the `restore` command).\n"
|
||||||
" Combine with --source to layer a script on\n"
|
" Combine with --source to layer a script on\n"
|
||||||
" top of a restored snapshot.\n"
|
" top of a restored snapshot.\n"
|
||||||
" --batch run --restore/--source, print the console\n"
|
" --batch run --restore/--source, print the console\n"
|
||||||
" output to stdout, and exit without the TUI.\n"
|
" output to stdout, and exit without launching\n"
|
||||||
|
" the interface.\n"
|
||||||
" --commands-md [FILE] dump the command registry as Markdown.\n"
|
" --commands-md [FILE] dump the command registry as Markdown.\n"
|
||||||
" With FILE: write there. Without: stdout.\n"
|
" With FILE: write there. Without: stdout.\n"
|
||||||
" (Used by `cmake --build build --target doc`.)\n"
|
" (Used by `cmake --build build --target doc`.)\n"
|
||||||
@@ -29,7 +32,7 @@ void print_usage(const char *prog) {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
int main(int argc, char **argv) {
|
int frontend_main(int argc, char **argv, Frontend &fe) {
|
||||||
std::string boot_restore;
|
std::string boot_restore;
|
||||||
std::string boot_source;
|
std::string boot_source;
|
||||||
bool batch = false;
|
bool batch = false;
|
||||||
@@ -37,16 +40,15 @@ int main(int argc, char **argv) {
|
|||||||
for (int i = 1; i < argc; ++i) {
|
for (int i = 1; i < argc; ++i) {
|
||||||
std::string a = argv[i];
|
std::string a = argv[i];
|
||||||
if (a == "--commands-md") {
|
if (a == "--commands-md") {
|
||||||
Tui tui;
|
|
||||||
if (i + 1 < argc) {
|
if (i + 1 < argc) {
|
||||||
std::ofstream f(argv[++i]);
|
std::ofstream f(argv[++i]);
|
||||||
if (!f) {
|
if (!f) {
|
||||||
std::cerr << "essim: cannot open " << argv[i] << " for writing\n";
|
std::cerr << "essim: cannot open " << argv[i] << " for writing\n";
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
tui.DumpCommandsMd(f);
|
fe.DumpCommandsMd(f);
|
||||||
} else {
|
} else {
|
||||||
tui.DumpCommandsMd(std::cout);
|
fe.DumpCommandsMd(std::cout);
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -79,20 +81,19 @@ int main(int argc, char **argv) {
|
|||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
Tui tui;
|
|
||||||
// Order matters: a `--restore` brings up a snapshot, then `--source`
|
// Order matters: a `--restore` brings up a snapshot, then `--source`
|
||||||
// can layer additional commands on top of it (useful e.g. for "load
|
// can layer additional commands on top of it (useful e.g. for "load
|
||||||
// snapshot, then re-run a small script that adds a new card").
|
// snapshot, then re-run a small script that adds a new card").
|
||||||
if (!boot_restore.empty()) tui.BootDispatch("restore " + boot_restore);
|
if (!boot_restore.empty()) fe.BootDispatch("restore " + boot_restore);
|
||||||
if (!boot_source.empty()) tui.BootDispatch("source " + boot_source);
|
if (!boot_source.empty()) fe.BootDispatch("source " + boot_source);
|
||||||
|
|
||||||
// Batch mode: the boot dispatch already ran synchronously (no screen yet),
|
// Batch mode: the boot dispatch already ran synchronously (no event loop
|
||||||
// so the console output is complete. Print it and exit without the TUI.
|
// yet), so the console output is complete. Print it and exit.
|
||||||
if (batch) {
|
if (batch) {
|
||||||
tui.DumpOutput(std::cout);
|
fe.DumpOutput(std::cout);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
tui.Run();
|
fe.Run();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
13
src/frontends/frontend_main.hpp
Normal file
13
src/frontends/frontend_main.hpp
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#ifndef _FRONTEND_MAIN_HPP_
|
||||||
|
#define _FRONTEND_MAIN_HPP_
|
||||||
|
|
||||||
|
class Frontend;
|
||||||
|
|
||||||
|
// Shared process entry point, frontend-agnostic. Parses argv
|
||||||
|
// (--source / --restore / --batch / --commands-md / --help), drives `fe`
|
||||||
|
// through the boot commands and then either dumps output (batch) or enters its
|
||||||
|
// event loop, and returns the process exit code. Each frontend's main() just
|
||||||
|
// constructs its concrete Frontend and forwards to this.
|
||||||
|
int frontend_main(int argc, char **argv, Frontend &fe);
|
||||||
|
|
||||||
|
#endif // _FRONTEND_MAIN_HPP_
|
||||||
39
src/frontends/tui/CMakeLists.txt
Normal file
39
src/frontends/tui/CMakeLists.txt
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# TUI frontend (FTXUI). Builds the `essim` executable against essim_core.
|
||||||
|
#
|
||||||
|
# A frontend is self-contained here: it pulls its own GUI toolkit, compiles its
|
||||||
|
# sources into a library that links essim_core, and produces the `essim` binary
|
||||||
|
# from its own entry point (main.cpp). To add another frontend, create a sibling
|
||||||
|
# src/frontends/<name>/ with the same shape and select it with
|
||||||
|
# -DESSIM_FRONTEND=<name>.
|
||||||
|
|
||||||
|
set(FTXUI_BUILD_DOCS OFF CACHE INTERNAL "")
|
||||||
|
set(FTXUI_BUILD_EXAMPLES OFF CACHE INTERNAL "")
|
||||||
|
set(FTXUI_BUILD_TESTS OFF CACHE INTERNAL "")
|
||||||
|
set(FTXUI_ENABLE_INSTALL OFF CACHE INTERNAL "")
|
||||||
|
|
||||||
|
FetchContent_Declare(ftxui
|
||||||
|
GIT_REPOSITORY https://github.com/ArthurSonzogni/FTXUI.git
|
||||||
|
GIT_TAG v6.1.9
|
||||||
|
GIT_SHALLOW TRUE
|
||||||
|
)
|
||||||
|
FetchContent_MakeAvailable(ftxui)
|
||||||
|
|
||||||
|
# Frontend library = every .cpp here except the entry point.
|
||||||
|
file(GLOB TUI_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp")
|
||||||
|
list(REMOVE_ITEM TUI_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/main.cpp")
|
||||||
|
|
||||||
|
add_library(essim_tui STATIC ${TUI_SOURCES})
|
||||||
|
target_link_libraries(essim_tui
|
||||||
|
PUBLIC
|
||||||
|
essim_core
|
||||||
|
ftxui::screen
|
||||||
|
ftxui::dom
|
||||||
|
ftxui::component
|
||||||
|
)
|
||||||
|
|
||||||
|
add_executable(essim "${CMAKE_CURRENT_SOURCE_DIR}/main.cpp")
|
||||||
|
target_link_libraries(essim PRIVATE essim_tui essim_frontend)
|
||||||
|
|
||||||
|
# Keep the binary at the top of the build tree (./build/essim), regardless of
|
||||||
|
# which frontend subdir produced it.
|
||||||
|
set_target_properties(essim PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}")
|
||||||
@@ -1,28 +1,28 @@
|
|||||||
#include "tui/tui.hpp"
|
#include "frontends/tui/tui.hpp"
|
||||||
#include "tui/tui_helpers.hpp"
|
#include "frontends/tui/tui_helpers.hpp"
|
||||||
|
|
||||||
#include "system/analysis.hpp"
|
#include "core/domain/analysis.hpp"
|
||||||
#include "system/connect.hpp"
|
#include "core/domain/connect.hpp"
|
||||||
#include "system/modules.hpp"
|
#include "core/domain/modules.hpp"
|
||||||
#include "system/nets.hpp"
|
#include "core/domain/parts.hpp"
|
||||||
#include "system/parts.hpp"
|
#include "core/domain/persist.hpp"
|
||||||
#include "system/persist.hpp"
|
#include "core/domain/pin_role.hpp"
|
||||||
#include "system/pin_role.hpp"
|
#include "core/domain/pin_model.hpp"
|
||||||
#include "system/pin_model.hpp"
|
#include "core/domain/bsdl_model.hpp"
|
||||||
#include "system/bsdl_model.hpp"
|
#include "core/domain/pins.hpp"
|
||||||
#include "system/bsdl_check.hpp"
|
#include "core/domain/signals.hpp"
|
||||||
#include "system/pins.hpp"
|
#include "core/domain/system.hpp"
|
||||||
#include "system/signals.hpp"
|
|
||||||
#include "system/system.hpp"
|
#include "core/app/connect.hpp"
|
||||||
#include "system/transform.hpp"
|
#include "core/app/load.hpp"
|
||||||
#include "system/transform_vpx.hpp"
|
#include "core/app/verify.hpp"
|
||||||
|
#include "core/domain/transform_vpx.hpp" // ValidatePartForKind
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <exception>
|
#include <exception>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <unordered_set>
|
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
void Tui::RegisterCommands() {
|
void Tui::RegisterCommands() {
|
||||||
@@ -138,29 +138,23 @@ void Tui::RegisterCommands() {
|
|||||||
{"import type [mentor|altium|ods]", Completion::None}},
|
{"import type [mentor|altium|ods]", Completion::None}},
|
||||||
[this](const std::vector<std::string> &args) {
|
[this](const std::vector<std::string> &args) {
|
||||||
if (!sys) { Print("no system: run 'new' first."); return; }
|
if (!sys) { Print("no system: run 'new' first."); return; }
|
||||||
std::string ls = ToLower(args[2]);
|
|
||||||
ImportType t;
|
ImportType t;
|
||||||
if (ls == "mentor") t = ImportType::IMPORT_MENTOR;
|
if (!app::import_type_from_name(args[2], t)) {
|
||||||
else if (ls == "altium") t = ImportType::IMPORT_ALTIUM;
|
Print("unknown import type: " + args[2]); return;
|
||||||
else if (ls == "ods") t = ImportType::IMPORT_ODS;
|
|
||||||
else { Print("unknown import type: " + args[2]); return; }
|
|
||||||
try {
|
|
||||||
sys->Load(args[0], args[1], t);
|
|
||||||
Module *mod = sys->modules()->get(args[0]);
|
|
||||||
int dropped = drop_singleton_signals(mod->signals);
|
|
||||||
auto inf = infer_signal_types(sys.get());
|
|
||||||
Print("loaded '" + args[0] + "' from " + args[1]);
|
|
||||||
Print(" parts: " + std::to_string(mod->size()));
|
|
||||||
Print(" signals: " + std::to_string(mod->signals->size())
|
|
||||||
+ (dropped ? " (dropped " + std::to_string(dropped)
|
|
||||||
+ " singleton/NC signal(s))" : ""));
|
|
||||||
Print(" types: " + std::to_string(inf.power) + " power, "
|
|
||||||
+ std::to_string(inf.gnd) + " gnd, "
|
|
||||||
+ std::to_string(inf.kept_other)
|
|
||||||
+ " suspect Power (name only — kept as Other)");
|
|
||||||
} catch (const std::exception &e) {
|
|
||||||
Print(std::string("load failed: ") + e.what());
|
|
||||||
}
|
}
|
||||||
|
// Import + drop-singletons + infer-types is one core op; the command
|
||||||
|
// only parses the type and renders the counts.
|
||||||
|
app::LoadResult r = app::load_module(sys.get(), args[0], args[1], t);
|
||||||
|
if (!r.ok) { Print(std::string("load failed: ") + r.error); return; }
|
||||||
|
Print("loaded '" + args[0] + "' from " + args[1]);
|
||||||
|
Print(" parts: " + std::to_string(r.parts));
|
||||||
|
Print(" signals: " + std::to_string(r.signals)
|
||||||
|
+ (r.dropped ? " (dropped " + std::to_string(r.dropped)
|
||||||
|
+ " singleton/NC signal(s))" : ""));
|
||||||
|
Print(" types: " + std::to_string(r.power) + " power, "
|
||||||
|
+ std::to_string(r.gnd) + " gnd, "
|
||||||
|
+ std::to_string(r.kept_other)
|
||||||
|
+ " suspect Power (name only — kept as Other)");
|
||||||
},
|
},
|
||||||
/*prompt_for_missing=*/ true,
|
/*prompt_for_missing=*/ true,
|
||||||
"load a module from a netlist / pinout file (mentor, altium, ods)",
|
"load a module from a netlist / pinout file (mentor, altium, ods)",
|
||||||
@@ -228,104 +222,43 @@ void Tui::RegisterCommands() {
|
|||||||
|
|
||||||
commands["verify"] = { {}, [this](auto &) {
|
commands["verify"] = { {}, [this](auto &) {
|
||||||
if (!sys) { Print("no system: run 'new' first."); return; }
|
if (!sys) { Print("no system: run 'new' first."); return; }
|
||||||
int checked = 0;
|
app::VerifyReport r = app::verify(sys.get());
|
||||||
int mismatches = 0;
|
|
||||||
for (auto &mkv : *sys->modules()) {
|
|
||||||
Module *mod = mkv.second;
|
|
||||||
for (auto &pkv : *mod) {
|
|
||||||
Part *prt = pkv.second;
|
|
||||||
if (prt->connector_type.empty()) continue;
|
|
||||||
for (auto &nkv : *prt) {
|
|
||||||
Pin *pin = nkv.second;
|
|
||||||
++checked;
|
|
||||||
SignalType expected = pin->expected_signal_type();
|
|
||||||
if (expected == SignalType::Other) continue;
|
|
||||||
Signal *s = pin->signal();
|
|
||||||
SignalType actual = s ? s->type : SignalType::Other;
|
|
||||||
if (actual == expected) continue;
|
|
||||||
++mismatches;
|
|
||||||
std::string sig_label = s ? s->name : std::string("(NC)");
|
|
||||||
Print(" " + mod->name + "/" + prt->name + "/" + pin->name
|
|
||||||
+ ": expected " + signal_type_name(expected)
|
|
||||||
+ ", got " + signal_type_name(actual)
|
|
||||||
+ " (signal: " + sig_label + ")");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Print("verify: " + std::to_string(mismatches) + " local mismatch(es) over "
|
|
||||||
+ std::to_string(checked) + " typed pin(s).");
|
|
||||||
|
|
||||||
auto nets = compute_all_nets(sys.get());
|
for (const auto &m : r.role_mismatches)
|
||||||
int bridged = 0, inconsistent = 0;
|
Print(" " + m.module + "/" + m.part + "/" + m.pin
|
||||||
for (const auto &n : nets) {
|
+ ": expected " + signal_type_name(m.expected)
|
||||||
if (n.members.size() < 2) continue;
|
+ ", got " + signal_type_name(m.actual)
|
||||||
++bridged;
|
+ " (signal: " + m.signal + ")");
|
||||||
SignalType dom;
|
Print("verify: " + std::to_string(r.role_mismatches.size())
|
||||||
if (net_type_consistent(n, dom)) continue;
|
+ " local mismatch(es) over " + std::to_string(r.typed_pins)
|
||||||
++inconsistent;
|
+ " typed pin(s).");
|
||||||
|
|
||||||
|
for (const auto &ni : r.net_inconsistencies) {
|
||||||
std::string line = " net mixes Power and GndShield:";
|
std::string line = " net mixes Power and GndShield:";
|
||||||
for (const auto &mp : n.members) {
|
for (const auto &mem : ni.members)
|
||||||
line += " " + mp.first->name + "/" + mp.second->name
|
line += " " + mem.module + "/" + mem.signal
|
||||||
+ "(" + signal_type_name(mp.second->type) + ")";
|
+ "(" + signal_type_name(mem.type) + ")";
|
||||||
}
|
|
||||||
Print(line);
|
Print(line);
|
||||||
}
|
}
|
||||||
Print("verify: " + std::to_string(inconsistent) + " inconsistent net(s) over "
|
Print("verify: " + std::to_string(r.net_inconsistencies.size())
|
||||||
+ std::to_string(bridged) + " bridged net(s) ("
|
+ " inconsistent net(s) over " + std::to_string(r.bridged_nets)
|
||||||
+ std::to_string(nets.size()) + " total).");
|
+ " bridged net(s) (" + std::to_string(r.total_nets) + " total).");
|
||||||
|
|
||||||
// Orphan pin report. A pin is "orphan" if it came out of import (or
|
Print("verify: " + std::to_string(r.orphan_total())
|
||||||
// post-import drop) with no signal, and is still not bridged to a
|
+ " orphan pin(s) at import (" + std::to_string(r.orphan_imported)
|
||||||
// real signal via any Connection::pin_map. Use `nc-export` for the
|
+ " imported NC, " + std::to_string(r.orphan_dropped)
|
||||||
// per-pin list.
|
+ " dropped singleton).");
|
||||||
std::unordered_set<Pin *> bridged_pins;
|
|
||||||
for (auto &ckv : *sys->connections())
|
|
||||||
for (auto &wp : ckv.second->pin_map) {
|
|
||||||
if (wp.first) bridged_pins.insert(wp.first);
|
|
||||||
if (wp.second) bridged_pins.insert(wp.second);
|
|
||||||
}
|
|
||||||
int orph_imported = 0, orph_dropped = 0;
|
|
||||||
for (auto &mkv : *sys->modules())
|
|
||||||
for (auto &pkv : *mkv.second)
|
|
||||||
for (auto &nkv : *pkv.second) {
|
|
||||||
Pin *pin = nkv.second;
|
|
||||||
if (pin->signal() || bridged_pins.count(pin)) continue;
|
|
||||||
if (pin->nc_origin == NcOrigin::ImportedUnconnected) ++orph_imported;
|
|
||||||
else if (pin->nc_origin == NcOrigin::DroppedSingleton) ++orph_dropped;
|
|
||||||
}
|
|
||||||
Print("verify: " + std::to_string(orph_imported + orph_dropped)
|
|
||||||
+ " orphan pin(s) at import ("
|
|
||||||
+ std::to_string(orph_imported) + " imported NC, "
|
|
||||||
+ std::to_string(orph_dropped) + " dropped singleton).");
|
|
||||||
|
|
||||||
// Model-driven pin checks (drive contention / undriven net / NC-wired)
|
// Each model-driven group: per-finding lines + a one-line summary.
|
||||||
// from the PinSpec direction/function populated by connector/BSDL models.
|
auto render = [this](const std::vector<Anomaly> &v, const char *tail) {
|
||||||
auto pin_anoms = check_pin_specs(sys.get(), &nets);
|
for (const auto &a : v)
|
||||||
for (const auto &a : pin_anoms)
|
Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message);
|
||||||
Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message);
|
Print("verify: " + std::to_string(v.size()) + tail);
|
||||||
Print("verify: " + std::to_string(pin_anoms.size())
|
};
|
||||||
+ " model-driven pin anomaly(ies).");
|
render(r.pin_anomalies, " model-driven pin anomaly(ies).");
|
||||||
|
render(r.jtag_anomalies, " JTAG chain anomaly(ies).");
|
||||||
// JTAG boundary-scan chain integrity (TAP pins → nets).
|
render(r.conflict_anomalies, " source-conflict(s).");
|
||||||
auto jtag_anoms = check_jtag_chain(sys.get(), &nets);
|
render(r.completeness_anomalies, " BSDL completeness issue(s).");
|
||||||
for (const auto &a : jtag_anoms)
|
|
||||||
Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message);
|
|
||||||
Print("verify: " + std::to_string(jtag_anoms.size())
|
|
||||||
+ " JTAG chain anomaly(ies).");
|
|
||||||
|
|
||||||
// Model-vs-netlist conflicts (e.g. a BSDL power pin left unconnected).
|
|
||||||
auto conflict_anoms = check_source_conflicts(sys.get());
|
|
||||||
for (const auto &a : conflict_anoms)
|
|
||||||
Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message);
|
|
||||||
Print("verify: " + std::to_string(conflict_anoms.size())
|
|
||||||
+ " source-conflict(s).");
|
|
||||||
|
|
||||||
// BSDL completeness: device power/ground pins missing from the netlist.
|
|
||||||
auto missing_anoms = check_bsdl_completeness(sys.get());
|
|
||||||
for (const auto &a : missing_anoms)
|
|
||||||
Print(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message);
|
|
||||||
Print("verify: " + std::to_string(missing_anoms.size())
|
|
||||||
+ " BSDL completeness issue(s).");
|
|
||||||
}, true,
|
}, true,
|
||||||
"check pin roles, power/gnd net consistency, and (with BSDL) pin and JTAG checks" };
|
"check pin roles, power/gnd net consistency, and (with BSDL) pin and JTAG checks" };
|
||||||
|
|
||||||
@@ -622,47 +555,23 @@ void Tui::RegisterCommands() {
|
|||||||
auto [p2, p2_alts] = resolve_part(m2, args[3]);
|
auto [p2, p2_alts] = resolve_part(m2, args[3]);
|
||||||
if (!p2) { report_ambiguous("part in " + m2->name, args[3], p2_alts); return; }
|
if (!p2) { report_ambiguous("part in " + m2->name, args[3], p2_alts); return; }
|
||||||
|
|
||||||
auto ® = TransformRegistry::get();
|
// Resolution above is arg-parsing (user text → objects); the wiring
|
||||||
Transform *t = reg.lookup(p1->connector_type, p2->connector_type);
|
// itself — transform lookup, identity NC fill, Connection creation —
|
||||||
bool both_empty = p1->connector_type.empty() && p2->connector_type.empty();
|
// is app::connect_parts.
|
||||||
if (t == reg.identity()) {
|
app::ConnectResult cr = app::connect_parts(sys.get(), m1, p1, m2, p2);
|
||||||
if (!both_empty) {
|
if (cr.refused) { Print("connect refused: " + cr.error); return; }
|
||||||
Print("connect refused: no transform for types '"
|
if (!cr.identity_info.empty()) {
|
||||||
+ (p1->connector_type.empty() ? "(none)" : p1->connector_type)
|
Print("connect: " + cr.identity_info);
|
||||||
+ "' ↔ '"
|
if (cr.nc_added > 0)
|
||||||
+ (p2->connector_type.empty() ? "(none)" : p2->connector_type)
|
Print("connect: added " + std::to_string(cr.nc_added)
|
||||||
+ "'. Set matching types via 'set-connector-type' first.");
|
+ " NC pin(s) so both sides match");
|
||||||
return;
|
|
||||||
}
|
|
||||||
std::string info;
|
|
||||||
std::string err = CheckIdentityCompatible(p1, p2, &info);
|
|
||||||
if (!err.empty()) {
|
|
||||||
Print("connect refused: " + err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!info.empty()) {
|
|
||||||
int added = FillIdentityNCs(p1, p2);
|
|
||||||
Print("connect: " + info);
|
|
||||||
if (added > 0)
|
|
||||||
Print("connect: added " + std::to_string(added)
|
|
||||||
+ " NC pin(s) so both sides match");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
auto pin_map = t->apply(p1, p2);
|
|
||||||
|
|
||||||
std::string conn_name = m1->name + "/" + p1->name
|
|
||||||
+ " <-> " + m2->name + "/" + p2->name;
|
|
||||||
try {
|
|
||||||
Connection *c = new Connection(conn_name, m1, p1, m2, p2);
|
|
||||||
c->transform_name = t->name;
|
|
||||||
c->pin_map = std::move(pin_map);
|
|
||||||
sys->connections()->add(c);
|
|
||||||
Print("connected: " + conn_name
|
|
||||||
+ " via " + t->name
|
|
||||||
+ " (" + std::to_string(c->pin_map.size()) + " wires)");
|
|
||||||
} catch (const std::exception &e) {
|
|
||||||
Print(std::string("connect failed: ") + e.what());
|
|
||||||
}
|
}
|
||||||
|
if (cr.ok)
|
||||||
|
Print("connected: " + cr.connection_name
|
||||||
|
+ " via " + cr.transform_name
|
||||||
|
+ " (" + std::to_string(cr.wires) + " wires)");
|
||||||
|
else
|
||||||
|
Print(std::string("connect failed: ") + cr.error);
|
||||||
},
|
},
|
||||||
/*prompt_for_missing=*/ false,
|
/*prompt_for_missing=*/ false,
|
||||||
"connect a part across two modules (interactive screen if no args)",
|
"connect a part across two modules (interactive screen if no args)",
|
||||||
70
src/frontends/tui/commands_export.cpp
Normal file
70
src/frontends/tui/commands_export.cpp
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
#include "frontends/tui/tui.hpp"
|
||||||
|
|
||||||
|
#include "core/app/export.hpp"
|
||||||
|
#include "core/domain/connect.hpp"
|
||||||
|
#include "core/domain/system.hpp"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// Thin UI wrapper around app::export_connections — this file only resolves
|
||||||
|
// arguments / the file dialog and renders the result. All the actual export
|
||||||
|
// (CSV / ODS building, file writing) lives in src/app/export.cpp.
|
||||||
|
void Tui::RegisterExportCommands() {
|
||||||
|
commands["export"] = {
|
||||||
|
{{"kind [connections]", Completion::None},
|
||||||
|
{"filename (.csv)", Completion::Path}},
|
||||||
|
[this](const std::vector<std::string> &args) {
|
||||||
|
if (!sys) { Print("no system: run 'new' first."); return; }
|
||||||
|
|
||||||
|
if (args.empty()) {
|
||||||
|
// Bare → the generic file dialog. The CSV/ODS filter rewrites
|
||||||
|
// the extension; the action below dispatches on it.
|
||||||
|
OpenFileDialog(
|
||||||
|
"Export — connections",
|
||||||
|
"export.connections",
|
||||||
|
"connections.csv",
|
||||||
|
{{"CSV", ".csv"}, {"ODS", ".ods"}},
|
||||||
|
[this](const std::string &path) {
|
||||||
|
Dispatch("export connections " + path);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (args.size() != 2) {
|
||||||
|
Print("usage: export <kind> <file> (or no args for the dialog)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const std::string &kind = args[0];
|
||||||
|
const std::string &path = args[1];
|
||||||
|
|
||||||
|
if (kind != "connections") {
|
||||||
|
ShowError("export: unknown kind '" + kind + "'\n"
|
||||||
|
"Known kinds: connections");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
app::ExportFormat fmt;
|
||||||
|
if (!app::export_format_from_path(path, fmt)) {
|
||||||
|
ShowError("export: unknown extension — accepted: .csv, .ods");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
app::ExportResult r = app::export_connections(sys.get(), path, fmt);
|
||||||
|
if (!r.ok) {
|
||||||
|
ShowError("export failed:\n" + r.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool ods = (fmt == app::ExportFormat::Ods);
|
||||||
|
std::string msg = ods ? "export connections (.ods): "
|
||||||
|
: "export connections (.csv): ";
|
||||||
|
if (ods) msg += std::to_string(r.sheets) + " sheet(s), ";
|
||||||
|
Print(msg + std::to_string(r.rows) + " wire(s) → " + path);
|
||||||
|
},
|
||||||
|
/*prompt_for_missing=*/ false,
|
||||||
|
"export structured data to CSV / ODS (kinds: connections; "
|
||||||
|
"bare form opens the file-picker dialog)",
|
||||||
|
/*scriptable=*/ true,
|
||||||
|
/*interactive=*/ true,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#include "tui/tui.hpp"
|
#include "frontends/tui/tui.hpp"
|
||||||
#include "tui/tui_helpers.hpp"
|
#include "frontends/tui/tui_helpers.hpp"
|
||||||
|
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
11
src/frontends/tui/main.cpp
Normal file
11
src/frontends/tui/main.cpp
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#include "frontends/frontend_main.hpp"
|
||||||
|
#include "frontends/tui/tui.hpp"
|
||||||
|
|
||||||
|
// The TUI frontend's entry point: construct the concrete Frontend (Tui) and
|
||||||
|
// hand off to the shared, frontend-agnostic launcher. All argv parsing and the
|
||||||
|
// boot/batch/run flow live in frontend_main(); a second frontend's main() looks
|
||||||
|
// exactly like this with its own Frontend type.
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
Tui tui;
|
||||||
|
return frontend_main(argc, argv, tui);
|
||||||
|
}
|
||||||
@@ -1,15 +1,11 @@
|
|||||||
#include "tui/tui.hpp"
|
#include "frontends/tui/tui.hpp"
|
||||||
#include "tui/tui_helpers.hpp"
|
#include "frontends/tui/tui_helpers.hpp"
|
||||||
|
|
||||||
#include "system/analysis.hpp"
|
#include "core/app/verify.hpp"
|
||||||
#include "system/bsdl_check.hpp"
|
#include "core/domain/analysis.hpp"
|
||||||
#include "system/connect.hpp"
|
#include "core/domain/modules.hpp"
|
||||||
#include "system/modules.hpp"
|
#include "core/domain/signals.hpp"
|
||||||
#include "system/nets.hpp"
|
#include "core/domain/system.hpp"
|
||||||
#include "system/parts.hpp"
|
|
||||||
#include "system/pins.hpp"
|
|
||||||
#include "system/signals.hpp"
|
|
||||||
#include "system/system.hpp"
|
|
||||||
|
|
||||||
#include <ftxui/component/component.hpp>
|
#include <ftxui/component/component.hpp>
|
||||||
#include <ftxui/component/component_options.hpp>
|
#include <ftxui/component/component_options.hpp>
|
||||||
@@ -17,7 +13,6 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <unordered_set>
|
|
||||||
|
|
||||||
using namespace ftxui;
|
using namespace ftxui;
|
||||||
|
|
||||||
@@ -57,41 +52,23 @@ Component Tui::BuildAnalyzeScreen() {
|
|||||||
// connection), then structural anomalies from the analysis pass.
|
// connection), then structural anomalies from the analysis pass.
|
||||||
analyze_issues.clear();
|
analyze_issues.clear();
|
||||||
|
|
||||||
int n_role_mismatches = 0, n_typed_pins = 0;
|
// verify + structural anomalies. The verify passes (pin-role, net-mix,
|
||||||
for (auto &mkv : *sys->modules())
|
// orphans, model checks) come from the shared core op; the structural
|
||||||
for (auto &pkv : *mkv.second) {
|
// anomalies (diff-pair/bus) come from analyze_system above.
|
||||||
Part *prt = pkv.second;
|
app::VerifyReport vr = app::verify(sys.get());
|
||||||
if (prt->connector_type.empty()) continue;
|
|
||||||
for (auto &nkv : *prt) {
|
|
||||||
Pin *pin = nkv.second;
|
|
||||||
++n_typed_pins;
|
|
||||||
SignalType expected = pin->expected_signal_type();
|
|
||||||
if (expected == SignalType::Other) continue;
|
|
||||||
Signal *s = pin->signal();
|
|
||||||
SignalType actual = s ? s->type : SignalType::Other;
|
|
||||||
if (actual == expected) continue;
|
|
||||||
++n_role_mismatches;
|
|
||||||
std::string sig_label = s ? s->name : std::string("(NC)");
|
|
||||||
analyze_issues.push_back(
|
|
||||||
"[pin-role] " + mkv.first + "/" + prt->name + "/"
|
|
||||||
+ pin->name + ": expected " + signal_type_name(expected)
|
|
||||||
+ ", got " + signal_type_name(actual)
|
|
||||||
+ " (signal: " + sig_label + ")");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto nets = compute_all_nets(sys.get());
|
for (const auto &m : vr.role_mismatches)
|
||||||
int n_bridged = 0, n_inconsistent = 0;
|
analyze_issues.push_back(
|
||||||
for (const auto &n : nets) {
|
"[pin-role] " + m.module + "/" + m.part + "/" + m.pin
|
||||||
if (n.members.size() < 2) continue;
|
+ ": expected " + signal_type_name(m.expected)
|
||||||
++n_bridged;
|
+ ", got " + signal_type_name(m.actual)
|
||||||
SignalType dom;
|
+ " (signal: " + m.signal + ")");
|
||||||
if (net_type_consistent(n, dom)) continue;
|
|
||||||
++n_inconsistent;
|
for (const auto &ni : vr.net_inconsistencies) {
|
||||||
std::string line = "[net-mix] mixes Power and Gnd:";
|
std::string line = "[net-mix] mixes Power and Gnd:";
|
||||||
for (const auto &mp : n.members)
|
for (const auto &mem : ni.members)
|
||||||
line += " " + mp.first->name + "/" + mp.second->name
|
line += " " + mem.module + "/" + mem.signal
|
||||||
+ "(" + signal_type_name(mp.second->type) + ")";
|
+ "(" + signal_type_name(mem.type) + ")";
|
||||||
analyze_issues.push_back(std::move(line));
|
analyze_issues.push_back(std::move(line));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,28 +77,25 @@ Component Tui::BuildAnalyzeScreen() {
|
|||||||
+ anomaly_kind_name(a.kind) + "] "
|
+ anomaly_kind_name(a.kind) + "] "
|
||||||
+ a.message);
|
+ a.message);
|
||||||
|
|
||||||
// Model-driven checks (same as `verify`), reusing the nets above.
|
// Model-driven checks (pin / JTAG / source-conflict / completeness).
|
||||||
std::vector<Anomaly> model_anoms;
|
auto push_anoms = [this](const std::vector<Anomaly> &v) {
|
||||||
{
|
for (const auto &a : v)
|
||||||
auto a1 = check_pin_specs(sys.get(), &nets);
|
analyze_issues.push_back(std::string("[")
|
||||||
auto a2 = check_jtag_chain(sys.get(), &nets);
|
+ anomaly_kind_name(a.kind) + "] "
|
||||||
auto a3 = check_source_conflicts(sys.get());
|
+ a.message);
|
||||||
auto a4 = check_bsdl_completeness(sys.get());
|
};
|
||||||
model_anoms.insert(model_anoms.end(), a1.begin(), a1.end());
|
push_anoms(vr.pin_anomalies);
|
||||||
model_anoms.insert(model_anoms.end(), a2.begin(), a2.end());
|
push_anoms(vr.jtag_anomalies);
|
||||||
model_anoms.insert(model_anoms.end(), a3.begin(), a3.end());
|
push_anoms(vr.conflict_anomalies);
|
||||||
model_anoms.insert(model_anoms.end(), a4.begin(), a4.end());
|
push_anoms(vr.completeness_anomalies);
|
||||||
}
|
int n_model = vr.model_total();
|
||||||
for (const auto &a : model_anoms)
|
|
||||||
analyze_issues.push_back(std::string("[")
|
|
||||||
+ anomaly_kind_name(a.kind) + "] "
|
|
||||||
+ a.message);
|
|
||||||
int n_model = (int)model_anoms.size();
|
|
||||||
|
|
||||||
if (analyze_issues.empty()) analyze_issues.push_back("(no issue found)");
|
if (analyze_issues.empty()) analyze_issues.push_back("(no issue found)");
|
||||||
if (analyze_issue_idx >= (int)analyze_issues.size())
|
if (analyze_issue_idx >= (int)analyze_issues.size())
|
||||||
analyze_issue_idx = (int)analyze_issues.size() - 1;
|
analyze_issue_idx = (int)analyze_issues.size() - 1;
|
||||||
|
|
||||||
|
int n_role_mismatches = (int)vr.role_mismatches.size();
|
||||||
|
int n_inconsistent = (int)vr.net_inconsistencies.size();
|
||||||
std::string issues_header = "Issues ("
|
std::string issues_header = "Issues ("
|
||||||
+ std::to_string(n_role_mismatches + n_inconsistent
|
+ std::to_string(n_role_mismatches + n_inconsistent
|
||||||
+ (int)rep.anomalies.size() + n_model)
|
+ (int)rep.anomalies.size() + n_model)
|
||||||
@@ -215,26 +189,11 @@ Component Tui::BuildAnalyzeScreen() {
|
|||||||
std::string(tag) + r.mod + "/" + r.sig + " — " + reason);
|
std::string(tag) + r.mod + "/" + r.sig + " — " + reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
// NC orphan rollup — same filter as the verify pass.
|
// NC orphan rollup — from the shared verify report.
|
||||||
std::unordered_set<Pin *> bridged_pins;
|
|
||||||
for (auto &ckv : *sys->connections())
|
|
||||||
for (auto &wp : ckv.second->pin_map) {
|
|
||||||
if (wp.first) bridged_pins.insert(wp.first);
|
|
||||||
if (wp.second) bridged_pins.insert(wp.second);
|
|
||||||
}
|
|
||||||
int orph_imported = 0, orph_dropped = 0;
|
|
||||||
for (auto &mkv : *sys->modules())
|
|
||||||
for (auto &pkv : *mkv.second)
|
|
||||||
for (auto &nkv : *pkv.second) {
|
|
||||||
Pin *pin = nkv.second;
|
|
||||||
if (pin->signal() || bridged_pins.count(pin)) continue;
|
|
||||||
if (pin->nc_origin == NcOrigin::ImportedUnconnected) ++orph_imported;
|
|
||||||
else if (pin->nc_origin == NcOrigin::DroppedSingleton) ++orph_dropped;
|
|
||||||
}
|
|
||||||
analyze_types.push_back(
|
analyze_types.push_back(
|
||||||
"[NC] orphan pin(s): " + std::to_string(orph_imported + orph_dropped)
|
"[NC] orphan pin(s): " + std::to_string(vr.orphan_total())
|
||||||
+ " (" + std::to_string(orph_imported) + " imported, "
|
+ " (" + std::to_string(vr.orphan_imported) + " imported, "
|
||||||
+ std::to_string(orph_dropped) + " dropped)");
|
+ std::to_string(vr.orphan_dropped) + " dropped)");
|
||||||
|
|
||||||
if (analyze_type_idx >= (int)analyze_types.size())
|
if (analyze_type_idx >= (int)analyze_types.size())
|
||||||
analyze_type_idx = (int)analyze_types.size() - 1;
|
analyze_type_idx = (int)analyze_types.size() - 1;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#include "tui/tui.hpp"
|
#include "frontends/tui/tui.hpp"
|
||||||
|
|
||||||
#include <ftxui/component/component.hpp>
|
#include <ftxui/component/component.hpp>
|
||||||
#include <ftxui/component/event.hpp>
|
#include <ftxui/component/event.hpp>
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
#include "tui/tui.hpp"
|
#include "frontends/tui/tui.hpp"
|
||||||
#include "tui/tui_helpers.hpp"
|
#include "frontends/tui/tui_helpers.hpp"
|
||||||
|
|
||||||
#include "system/connect.hpp"
|
#include "core/app/connect.hpp"
|
||||||
#include "system/modules.hpp"
|
#include "core/domain/modules.hpp"
|
||||||
#include "system/parts.hpp"
|
#include "core/domain/parts.hpp"
|
||||||
#include "system/system.hpp"
|
#include "core/domain/system.hpp"
|
||||||
#include "system/transform.hpp"
|
|
||||||
|
|
||||||
#include <ftxui/component/component.hpp>
|
#include <ftxui/component/component.hpp>
|
||||||
#include <ftxui/component/component_options.hpp>
|
#include <ftxui/component/component_options.hpp>
|
||||||
@@ -67,37 +66,24 @@ Component Tui::BuildConnectScreen() {
|
|||||||
Part *p1 = m1->get(connect_p1_list[connect_p1_idx]);
|
Part *p1 = m1->get(connect_p1_list[connect_p1_idx]);
|
||||||
Part *p2 = m2->get(connect_p2_list[connect_p2_idx]);
|
Part *p2 = m2->get(connect_p2_list[connect_p2_idx]);
|
||||||
|
|
||||||
auto ® = TransformRegistry::get();
|
// Same wiring op as the `connect` command — see app::connect_parts.
|
||||||
Transform *t = reg.lookup(p1->connector_type, p2->connector_type);
|
app::ConnectResult cr = app::connect_parts(sys.get(), m1, p1, m2, p2);
|
||||||
bool both_empty = p1->connector_type.empty() && p2->connector_type.empty();
|
if (cr.refused) {
|
||||||
if (t == reg.identity()) {
|
Print("connect refused: " + cr.error);
|
||||||
if (!both_empty) {
|
} else {
|
||||||
Print("connect refused: no transform for types '"
|
if (!cr.identity_info.empty()) {
|
||||||
+ (p1->connector_type.empty() ? "(none)" : p1->connector_type)
|
Print("connect: " + cr.identity_info);
|
||||||
+ "' ↔ '"
|
if (cr.nc_added > 0)
|
||||||
+ (p2->connector_type.empty() ? "(none)" : p2->connector_type)
|
Print("connect: added " + std::to_string(cr.nc_added)
|
||||||
+ "'. Set matching types via 'set-connector-type' first.");
|
+ " NC pin(s) so both sides match");
|
||||||
screen_idx = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
std::string err = CheckIdentityCompatible(p1, p2);
|
|
||||||
if (!err.empty()) {
|
|
||||||
Print("connect refused: " + err);
|
|
||||||
screen_idx = 0;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
if (cr.ok)
|
||||||
|
Print("connected: " + cr.connection_name
|
||||||
|
+ " via " + cr.transform_name
|
||||||
|
+ " (" + std::to_string(cr.wires) + " wires)");
|
||||||
|
else
|
||||||
|
Print(std::string("connect failed: ") + cr.error);
|
||||||
}
|
}
|
||||||
auto pin_map = t->apply(p1, p2);
|
|
||||||
|
|
||||||
std::string conn_name = m1->name + "/" + p1->name
|
|
||||||
+ " <-> " + m2->name + "/" + p2->name;
|
|
||||||
Connection *c = new Connection(conn_name, m1, p1, m2, p2);
|
|
||||||
c->transform_name = t->name;
|
|
||||||
c->pin_map = std::move(pin_map);
|
|
||||||
sys->connections()->add(c);
|
|
||||||
Print("connected: " + conn_name
|
|
||||||
+ " via " + t->name
|
|
||||||
+ " (" + std::to_string(c->pin_map.size()) + " wires)");
|
|
||||||
} catch (const std::exception &e) {
|
} catch (const std::exception &e) {
|
||||||
Print(std::string("connect failed: ") + e.what());
|
Print(std::string("connect failed: ") + e.what());
|
||||||
}
|
}
|
||||||
@@ -1,22 +1,19 @@
|
|||||||
#include "tui/tui.hpp"
|
#include "frontends/tui/tui.hpp"
|
||||||
#include "tui/tui_helpers.hpp"
|
#include "frontends/tui/tui_helpers.hpp"
|
||||||
|
|
||||||
#include "system/analysis.hpp"
|
#include "core/app/verify.hpp"
|
||||||
#include "system/bsdl_check.hpp"
|
#include "core/domain/analysis.hpp"
|
||||||
#include "system/connect.hpp"
|
#include "core/domain/connect.hpp"
|
||||||
#include "system/modules.hpp"
|
#include "core/domain/modules.hpp"
|
||||||
#include "system/nets.hpp"
|
#include "core/domain/parts.hpp"
|
||||||
#include "system/parts.hpp"
|
#include "core/domain/signals.hpp"
|
||||||
#include "system/pins.hpp"
|
#include "core/domain/system.hpp"
|
||||||
#include "system/signals.hpp"
|
|
||||||
#include "system/system.hpp"
|
|
||||||
|
|
||||||
#include <ftxui/component/component.hpp>
|
#include <ftxui/component/component.hpp>
|
||||||
#include <ftxui/dom/elements.hpp>
|
#include <ftxui/dom/elements.hpp>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <unordered_set>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
using namespace ftxui;
|
using namespace ftxui;
|
||||||
@@ -77,58 +74,23 @@ Component Tui::BuildDashboardScreen() {
|
|||||||
}
|
}
|
||||||
int n_conn = (int)sys->connections()->size();
|
int n_conn = (int)sys->connections()->size();
|
||||||
|
|
||||||
// ---- verify-style health (recomputed; cheap on realistic sizes) ----
|
// ---- verify-style health (shared core op; cheap on realistic sizes) ----
|
||||||
int n_role_mismatches = 0, n_typed_pins = 0;
|
app::VerifyReport vr = app::verify(sys.get());
|
||||||
for (auto &mkv : *sys->modules())
|
int n_role_mismatches = (int)vr.role_mismatches.size();
|
||||||
for (auto &pkv : *mkv.second) {
|
int n_typed_pins = vr.typed_pins;
|
||||||
Part *prt = pkv.second;
|
int n_inconsistent = (int)vr.net_inconsistencies.size();
|
||||||
if (prt->connector_type.empty()) continue;
|
int n_bridged = vr.bridged_nets;
|
||||||
for (auto &nkv : *prt) {
|
int orph_imported = vr.orphan_imported;
|
||||||
Pin *pin = nkv.second;
|
int orph_dropped = vr.orphan_dropped;
|
||||||
++n_typed_pins;
|
|
||||||
SignalType expected = pin->expected_signal_type();
|
|
||||||
if (expected == SignalType::Other) continue;
|
|
||||||
Signal *s = pin->signal();
|
|
||||||
SignalType actual = s ? s->type : SignalType::Other;
|
|
||||||
if (actual != expected) ++n_role_mismatches;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto nets = compute_all_nets(sys.get());
|
|
||||||
int n_bridged = 0, n_inconsistent = 0;
|
|
||||||
for (const auto &n : nets) {
|
|
||||||
if (n.members.size() < 2) continue;
|
|
||||||
++n_bridged;
|
|
||||||
SignalType dom;
|
|
||||||
if (!net_type_consistent(n, dom)) ++n_inconsistent;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- NC orphan summary (matches verify pass 3) ----
|
|
||||||
std::unordered_set<Pin *> bridged_pins;
|
|
||||||
for (auto &ckv : *sys->connections())
|
|
||||||
for (auto &wp : ckv.second->pin_map) {
|
|
||||||
if (wp.first) bridged_pins.insert(wp.first);
|
|
||||||
if (wp.second) bridged_pins.insert(wp.second);
|
|
||||||
}
|
|
||||||
int orph_imported = 0, orph_dropped = 0;
|
|
||||||
// Per-module list of dropped-singleton pins, for the detail rows below
|
// Per-module list of dropped-singleton pins, for the detail rows below
|
||||||
// the NC health line. The signal name is gone (the Signal object was
|
// the NC health line. The signal name is gone (the Signal object was
|
||||||
// deleted by `drop_singleton_signals`), but the pin's full path is
|
// deleted by `drop_singleton_signals`), but the pin's full path is
|
||||||
// enough to locate it in `explore`.
|
// enough to locate it in `explore`.
|
||||||
std::map<std::string, std::vector<std::string>> dropped_by_module;
|
std::map<std::string, std::vector<std::string>> dropped_by_module;
|
||||||
for (auto &mkv : *sys->modules())
|
for (const auto &o : vr.orphans)
|
||||||
for (auto &pkv : *mkv.second)
|
if (o.dropped)
|
||||||
for (auto &nkv : *pkv.second) {
|
dropped_by_module[o.module].push_back(o.part + "/" + o.pin);
|
||||||
Pin *pin = nkv.second;
|
|
||||||
if (pin->signal() || bridged_pins.count(pin)) continue;
|
|
||||||
if (pin->nc_origin == NcOrigin::ImportedUnconnected) {
|
|
||||||
++orph_imported;
|
|
||||||
} else if (pin->nc_origin == NcOrigin::DroppedSingleton) {
|
|
||||||
++orph_dropped;
|
|
||||||
dropped_by_module[mkv.first].push_back(
|
|
||||||
pkv.first + "/" + nkv.first);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto health_line = [](bool ok, const std::string &s) {
|
auto health_line = [](bool ok, const std::string &s) {
|
||||||
return hbox({
|
return hbox({
|
||||||
@@ -144,7 +106,7 @@ Component Tui::BuildDashboardScreen() {
|
|||||||
+ " typed pin(s)"));
|
+ " typed pin(s)"));
|
||||||
health_rows.push_back(health_line(n_inconsistent == 0,
|
health_rows.push_back(health_line(n_inconsistent == 0,
|
||||||
"nets: " + std::to_string(n_inconsistent) + " inconsistent over "
|
"nets: " + std::to_string(n_inconsistent) + " inconsistent over "
|
||||||
+ std::to_string(n_bridged) + " bridged (" + std::to_string(nets.size())
|
+ std::to_string(n_bridged) + " bridged (" + std::to_string(vr.total_nets)
|
||||||
+ " total)"));
|
+ " total)"));
|
||||||
int orph_total = orph_imported + orph_dropped;
|
int orph_total = orph_imported + orph_dropped;
|
||||||
health_rows.push_back(health_line(orph_total == 0,
|
health_rows.push_back(health_line(orph_total == 0,
|
||||||
@@ -172,12 +134,9 @@ Component Tui::BuildDashboardScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Model-driven checks (BSDL pin specs, JTAG chain, source conflicts),
|
// Model-driven checks (BSDL pin specs, JTAG chain, source conflicts,
|
||||||
// reusing the nets computed above.
|
// completeness) — from the shared verify report.
|
||||||
int n_model = (int)(check_pin_specs(sys.get(), &nets).size()
|
int n_model = vr.model_total();
|
||||||
+ check_jtag_chain(sys.get(), &nets).size()
|
|
||||||
+ check_source_conflicts(sys.get()).size()
|
|
||||||
+ check_bsdl_completeness(sys.get()).size());
|
|
||||||
health_rows.push_back(health_line(n_model == 0,
|
health_rows.push_back(health_line(n_model == 0,
|
||||||
"model: " + std::to_string(n_model) + " BSDL/JTAG anomaly(ies)"));
|
"model: " + std::to_string(n_model) + " BSDL/JTAG anomaly(ies)"));
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#include "tui/tui.hpp"
|
#include "frontends/tui/tui.hpp"
|
||||||
|
|
||||||
#include <ftxui/component/component.hpp>
|
#include <ftxui/component/component.hpp>
|
||||||
#include <ftxui/component/event.hpp>
|
#include <ftxui/component/event.hpp>
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
#include "tui/tui.hpp"
|
#include "frontends/tui/tui.hpp"
|
||||||
#include "tui/tui_helpers.hpp"
|
#include "frontends/tui/tui_helpers.hpp"
|
||||||
|
|
||||||
#include "system/connect.hpp"
|
#include "core/domain/connect.hpp"
|
||||||
#include "system/modules.hpp"
|
#include "core/domain/modules.hpp"
|
||||||
#include "system/nets.hpp"
|
#include "core/domain/nets.hpp"
|
||||||
#include "system/parts.hpp"
|
#include "core/domain/parts.hpp"
|
||||||
#include "system/pins.hpp"
|
#include "core/domain/pins.hpp"
|
||||||
#include "system/signals.hpp"
|
#include "core/domain/signals.hpp"
|
||||||
#include "system/system.hpp"
|
#include "core/domain/system.hpp"
|
||||||
|
|
||||||
#include <ftxui/component/component.hpp>
|
#include <ftxui/component/component.hpp>
|
||||||
#include <ftxui/component/component_options.hpp>
|
#include <ftxui/component/component_options.hpp>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#include "tui/tui.hpp"
|
#include "frontends/tui/tui.hpp"
|
||||||
#include "tui/tui_helpers.hpp"
|
#include "frontends/tui/tui_helpers.hpp"
|
||||||
|
|
||||||
#include <ftxui/component/component.hpp>
|
#include <ftxui/component/component.hpp>
|
||||||
#include <ftxui/component/component_options.hpp>
|
#include <ftxui/component/component_options.hpp>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#include "tui/tui.hpp"
|
#include "frontends/tui/tui.hpp"
|
||||||
#include "tui/tui_helpers.hpp"
|
#include "frontends/tui/tui_helpers.hpp"
|
||||||
|
|
||||||
#include <ftxui/component/component.hpp>
|
#include <ftxui/component/component.hpp>
|
||||||
#include <ftxui/component/component_options.hpp>
|
#include <ftxui/component/component_options.hpp>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#include "tui/tui.hpp"
|
#include "frontends/tui/tui.hpp"
|
||||||
#include "tui/tui_helpers.hpp"
|
#include "frontends/tui/tui_helpers.hpp"
|
||||||
|
|
||||||
#include <ftxui/component/component.hpp>
|
#include <ftxui/component/component.hpp>
|
||||||
#include <ftxui/component/component_options.hpp>
|
#include <ftxui/component/component_options.hpp>
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
#include "tui/tui.hpp"
|
#include "frontends/tui/tui.hpp"
|
||||||
#include "tui/tui_helpers.hpp"
|
#include "frontends/tui/tui_helpers.hpp"
|
||||||
|
|
||||||
#include "system/modules.hpp"
|
#include "core/domain/modules.hpp"
|
||||||
#include "system/signals.hpp"
|
#include "core/domain/signals.hpp"
|
||||||
#include "system/system.hpp"
|
#include "core/domain/system.hpp"
|
||||||
|
|
||||||
#include <ftxui/component/component.hpp>
|
#include <ftxui/component/component.hpp>
|
||||||
#include <ftxui/component/component_options.hpp>
|
#include <ftxui/component/component_options.hpp>
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
#include "tui/tui.hpp"
|
#include "frontends/tui/tui.hpp"
|
||||||
#include "tui/tui_helpers.hpp"
|
#include "frontends/tui/tui_helpers.hpp"
|
||||||
|
|
||||||
#include "system/modules.hpp"
|
#include "core/domain/modules.hpp"
|
||||||
#include "system/parts.hpp"
|
#include "core/domain/parts.hpp"
|
||||||
#include "system/pin_model.hpp"
|
#include "core/domain/pin_model.hpp"
|
||||||
#include "system/pin_role.hpp"
|
#include "core/domain/pin_role.hpp"
|
||||||
#include "system/pins.hpp"
|
#include "core/domain/pins.hpp"
|
||||||
#include "system/system.hpp"
|
#include "core/domain/system.hpp"
|
||||||
#include "system/transform_vpx.hpp"
|
#include "core/domain/transform_vpx.hpp"
|
||||||
|
|
||||||
#include <ftxui/component/component.hpp>
|
#include <ftxui/component/component.hpp>
|
||||||
#include <ftxui/component/component_options.hpp>
|
#include <ftxui/component/component_options.hpp>
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
#include "tui/tui.hpp"
|
#include "frontends/tui/tui.hpp"
|
||||||
|
|
||||||
#include "system/modules.hpp"
|
#include "core/domain/modules.hpp"
|
||||||
#include "system/signals.hpp"
|
#include "core/domain/signals.hpp"
|
||||||
#include "system/system.hpp"
|
#include "core/domain/system.hpp"
|
||||||
|
|
||||||
#include <ftxui/component/component.hpp>
|
#include <ftxui/component/component.hpp>
|
||||||
#include <ftxui/component/component_options.hpp>
|
#include <ftxui/component/component_options.hpp>
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
#include "tui/tui.hpp"
|
#include "frontends/tui/tui.hpp"
|
||||||
#include "tui/tui_helpers.hpp"
|
#include "frontends/tui/tui_helpers.hpp"
|
||||||
|
|
||||||
#include "system/modules.hpp"
|
#include "core/domain/modules.hpp"
|
||||||
#include "system/signals.hpp"
|
#include "core/domain/signals.hpp"
|
||||||
#include "system/system.hpp"
|
#include "core/domain/system.hpp"
|
||||||
|
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#include "tui/tui.hpp"
|
#include "frontends/tui/tui.hpp"
|
||||||
|
|
||||||
#include "system/system.hpp"
|
#include "core/domain/system.hpp"
|
||||||
|
|
||||||
#include <ftxui/component/component.hpp>
|
#include <ftxui/component/component.hpp>
|
||||||
#include <ftxui/component/event.hpp>
|
#include <ftxui/component/event.hpp>
|
||||||
@@ -13,9 +13,11 @@
|
|||||||
#include <ftxui/component/component.hpp>
|
#include <ftxui/component/component.hpp>
|
||||||
#include <ftxui/component/screen_interactive.hpp>
|
#include <ftxui/component/screen_interactive.hpp>
|
||||||
|
|
||||||
|
#include "frontends/frontend.hpp"
|
||||||
|
|
||||||
class System;
|
class System;
|
||||||
|
|
||||||
class Tui {
|
class Tui : public Frontend {
|
||||||
enum class Completion { None, Path, Command };
|
enum class Completion { None, Path, Command };
|
||||||
|
|
||||||
struct Prompt {
|
struct Prompt {
|
||||||
@@ -198,16 +200,16 @@ private:
|
|||||||
public:
|
public:
|
||||||
Tui();
|
Tui();
|
||||||
~Tui();
|
~Tui();
|
||||||
void Run();
|
void Run() override;
|
||||||
void DumpCommandsMd(std::ostream &out) const;
|
void DumpCommandsMd(std::ostream &out) const override;
|
||||||
// Write the accumulated console output to `out`. Used by batch mode to
|
// Write the accumulated console output to `out`. Used by batch mode to
|
||||||
// surface a script's output without starting the TUI.
|
// surface a script's output without starting the TUI.
|
||||||
void DumpOutput(std::ostream &out) const;
|
void DumpOutput(std::ostream &out) const override;
|
||||||
|
|
||||||
// Boot-time hook: dispatch a single command exactly as if the user
|
// Boot-time hook: dispatch a single command exactly as if the user
|
||||||
// typed it (e.g. `restore foo.essim` or `source bring-up.essim`).
|
// typed it (e.g. `restore foo.essim` or `source bring-up.essim`).
|
||||||
// Call before `Run()` to seed the system before the event loop starts.
|
// Call before `Run()` to seed the system before the event loop starts.
|
||||||
void BootDispatch(const std::string &raw);
|
void BootDispatch(const std::string &raw) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Lifecycle (commands.cpp)
|
// Lifecycle (commands.cpp)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#include "tui/tui_helpers.hpp"
|
#include "frontends/tui/tui_helpers.hpp"
|
||||||
|
|
||||||
#include <ftxui/dom/elements.hpp>
|
#include <ftxui/dom/elements.hpp>
|
||||||
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
|
|
||||||
#include "system.hpp"
|
|
||||||
|
|
||||||
#include "connect.hpp"
|
|
||||||
#include "modules.hpp"
|
|
||||||
#include "imports/import_altium.hpp"
|
|
||||||
#include "imports/import_mentor.hpp"
|
|
||||||
#include "imports/import_ods.hpp"
|
|
||||||
|
|
||||||
System::System() : mods(nullptr), conns(nullptr)
|
|
||||||
{
|
|
||||||
mods = new Modules();
|
|
||||||
conns = new Connections();
|
|
||||||
}
|
|
||||||
|
|
||||||
System::~System()
|
|
||||||
{
|
|
||||||
delete mods;
|
|
||||||
delete conns;
|
|
||||||
}
|
|
||||||
|
|
||||||
void System::Load(std::string module_name, std::string file_name, ImportType type)
|
|
||||||
{
|
|
||||||
ImportBase *imp;
|
|
||||||
Module *mod = nullptr;
|
|
||||||
Parts *prts = nullptr;
|
|
||||||
|
|
||||||
// Creation or retrieval of the module.
|
|
||||||
mod = mods->merge(module_name);
|
|
||||||
|
|
||||||
// Parsing of the file based on the import type.
|
|
||||||
if (type == ImportType::IMPORT_MENTOR)
|
|
||||||
{
|
|
||||||
imp = new ImportMentor(file_name);
|
|
||||||
} else if (type == ImportType::IMPORT_ALTIUM)
|
|
||||||
{
|
|
||||||
imp = new ImportAltium(file_name);
|
|
||||||
} else if (type == ImportType::IMPORT_ODS)
|
|
||||||
{
|
|
||||||
imp = new ImportOds(file_name);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw std::runtime_error("Unknown import type");
|
|
||||||
}
|
|
||||||
imp->parse(mod->signals);
|
|
||||||
prts = imp->parts();
|
|
||||||
mod->add(prts);
|
|
||||||
}
|
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
#include "tui/tui.hpp"
|
|
||||||
#include "tui/tui_helpers.hpp"
|
|
||||||
|
|
||||||
#include "imports/ods_writer.hpp"
|
|
||||||
#include "system/connect.hpp"
|
|
||||||
#include "system/modules.hpp"
|
|
||||||
#include "system/parts.hpp"
|
|
||||||
#include "system/pins.hpp"
|
|
||||||
#include "system/signal_type.hpp"
|
|
||||||
#include "system/signals.hpp"
|
|
||||||
#include "system/system.hpp"
|
|
||||||
|
|
||||||
#include <fstream>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
// Minimal CSV quoter — wraps in `"…"` and doubles internal quotes when
|
|
||||||
// the field contains a comma, quote, or newline. Local to this file.
|
|
||||||
std::string csv_quote(const std::string &s) {
|
|
||||||
bool needs = s.find_first_of(",\"\n") != std::string::npos;
|
|
||||||
if (!needs) return s;
|
|
||||||
std::string out = "\"";
|
|
||||||
for (char c : s) { if (c == '"') out += '"'; out += c; }
|
|
||||||
out += '"';
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flatten one pin into the 6 string slots the export row uses.
|
|
||||||
void pin_side(Pin *p, std::string &mod, std::string &part,
|
|
||||||
std::string &pin, std::string &sig,
|
|
||||||
std::string &type, std::string &suspect) {
|
|
||||||
if (!p) { mod = part = pin = sig = type = suspect = ""; return; }
|
|
||||||
mod = (p->prnt && p->prnt->prnt) ? p->prnt->prnt->name : "";
|
|
||||||
part = p->prnt ? p->prnt->name : "";
|
|
||||||
pin = p->name;
|
|
||||||
Signal *s = p->signal();
|
|
||||||
if (!s) {
|
|
||||||
sig = ""; type = "(NC)"; suspect = "";
|
|
||||||
} else {
|
|
||||||
sig = s->name;
|
|
||||||
type = signal_type_name(s->type);
|
|
||||||
suspect = (infer_signal_type(s->name) == SignalType::Power
|
|
||||||
&& s->type == SignalType::Other) ? "yes" : "no";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
void Tui::RegisterExportCommands() {
|
|
||||||
commands["export"] = {
|
|
||||||
{{"kind [connections]", Completion::None},
|
|
||||||
{"filename (.csv)", Completion::Path}},
|
|
||||||
[this](const std::vector<std::string> &args) {
|
|
||||||
if (!sys) { Print("no system: run 'new' first."); return; }
|
|
||||||
if (args.empty()) {
|
|
||||||
// Bare → reuse the generic file dialog. Filters give a
|
|
||||||
// one-keystroke CSV/ODS toggle; picking either rewrites
|
|
||||||
// the filename's extension, and the action below
|
|
||||||
// dispatches on that extension.
|
|
||||||
OpenFileDialog(
|
|
||||||
"Export — connections",
|
|
||||||
"export.connections",
|
|
||||||
"connections.csv",
|
|
||||||
{{"CSV", ".csv"}, {"ODS", ".ods"}},
|
|
||||||
[this](const std::string &path) {
|
|
||||||
Dispatch("export connections " + path);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (args.size() != 2) {
|
|
||||||
Print("usage: export <kind> <file> (or no args for the dialog)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const std::string &kind = args[0];
|
|
||||||
const std::string &path = args[1];
|
|
||||||
|
|
||||||
if (kind == "connections") {
|
|
||||||
// Accepted extensions: `.csv` (flat file) and `.ods`
|
|
||||||
// (one sheet per connection). Anything else is an error.
|
|
||||||
std::string ext;
|
|
||||||
{
|
|
||||||
size_t dot = path.rfind('.');
|
|
||||||
if (dot != std::string::npos) ext = ToLower(path.substr(dot));
|
|
||||||
}
|
|
||||||
bool ods = (ext == ".ods");
|
|
||||||
bool csv = (ext == ".csv");
|
|
||||||
if (!ods && !csv) {
|
|
||||||
ShowError("export: unknown extension '"
|
|
||||||
+ (ext.empty() ? std::string("(none)") : ext)
|
|
||||||
+ "'\nAccepted: .csv, .ods");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ods) {
|
|
||||||
OdsWriter w;
|
|
||||||
int total = 0;
|
|
||||||
for (auto &ckv : *sys->connections()) {
|
|
||||||
Connection *c = ckv.second;
|
|
||||||
// Sheet names: Excel rejects /\?*:[] characters,
|
|
||||||
// ODS forbids < > & in raw cell/table names.
|
|
||||||
// Sanitise to underscores; clip to 31 chars
|
|
||||||
// (Excel's hard limit).
|
|
||||||
std::string sname = c->name;
|
|
||||||
for (char &ch : sname)
|
|
||||||
if (ch == '/' || ch == '\\' || ch == '?' || ch == '*'
|
|
||||||
|| ch == ':' || ch == '[' || ch == ']'
|
|
||||||
|| ch == '<' || ch == '>' || ch == '&') ch = '_';
|
|
||||||
if (sname.size() > 31) sname = sname.substr(0, 31);
|
|
||||||
OdsSheet *s = w.add_sheet(sname);
|
|
||||||
|
|
||||||
// Pull the constants for this connection once.
|
|
||||||
// `transform`, the left module/part, and the
|
|
||||||
// right module/part don't vary across the wires
|
|
||||||
// of a single connection — putting them in
|
|
||||||
// every row was repetitive.
|
|
||||||
std::string lmod, lprt;
|
|
||||||
std::string rmod, rprt;
|
|
||||||
if (c->m1) lmod = c->m1->name;
|
|
||||||
if (c->p1) lprt = c->p1->name;
|
|
||||||
if (c->m2) rmod = c->m2->name;
|
|
||||||
if (c->p2) rprt = c->p2->name;
|
|
||||||
|
|
||||||
// Meta header above the table: 5 rows of label /
|
|
||||||
// value, then a blank, then the column headers
|
|
||||||
// on row 6 (index 5).
|
|
||||||
auto meta = [&](int r, const std::string &k,
|
|
||||||
const std::string &v) {
|
|
||||||
s->set(r, 0, k);
|
|
||||||
s->set(r, 1, v);
|
|
||||||
};
|
|
||||||
meta(0, "Connection", c->name);
|
|
||||||
meta(1, "Transform", c->transform_name);
|
|
||||||
meta(2, "Left", lmod + " / " + lprt);
|
|
||||||
meta(3, "Right", rmod + " / " + rprt);
|
|
||||||
// Row 4 left blank by design.
|
|
||||||
|
|
||||||
const int HDR = 5;
|
|
||||||
s->set_header_row(HDR);
|
|
||||||
const char *hdr[] = {
|
|
||||||
"left_pin", "left_signal", "left_type", "left_suspect",
|
|
||||||
"right_pin", "right_signal", "right_type", "right_suspect",
|
|
||||||
"type_mismatch"};
|
|
||||||
for (int i = 0; i < 9; ++i) s->set(HDR, i, hdr[i]);
|
|
||||||
|
|
||||||
int row = HDR + 1;
|
|
||||||
for (auto &wp : c->pin_map) {
|
|
||||||
std::string lm, lp, ln, ls, lt, lsus;
|
|
||||||
std::string rm, rp, rn, rs, rt, rsus;
|
|
||||||
pin_side(wp.first, lm, lp, ln, ls, lt, lsus);
|
|
||||||
pin_side(wp.second, rm, rp, rn, rs, rt, rsus);
|
|
||||||
// `type_mismatch = yes` when both sides have a
|
|
||||||
// real signal AND their types disagree (e.g.
|
|
||||||
// Power ↔ Gnd, or Power ↔ Other).
|
|
||||||
std::string tm = (lt != "(NC)" && rt != "(NC)" && lt != rt)
|
|
||||||
? "yes" : "no";
|
|
||||||
s->set(row, 0, ln); s->set(row, 1, ls);
|
|
||||||
s->set(row, 2, lt); s->set(row, 3, lsus);
|
|
||||||
s->set(row, 4, rn); s->set(row, 5, rs);
|
|
||||||
s->set(row, 6, rt); s->set(row, 7, rsus);
|
|
||||||
s->set(row, 8, tm);
|
|
||||||
++row; ++total;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
std::string err;
|
|
||||||
if (!w.save(path, err)) {
|
|
||||||
ShowError("export (.ods) failed:\n" + err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Print("export connections (.ods): "
|
|
||||||
+ std::to_string(sys->connections()->size())
|
|
||||||
+ " sheet(s), " + std::to_string(total)
|
|
||||||
+ " wire(s) → " + path);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Classic flat CSV: a single rectangular table — one
|
|
||||||
// header line, N data rows, every row carries the per-
|
|
||||||
// connection constants too. The 9 right-most column
|
|
||||||
// names match the ODS sheet headers exactly; the 5
|
|
||||||
// leading ones (connection, transform, left_module,
|
|
||||||
// left_part, right_module, right_part) correspond to
|
|
||||||
// the ODS meta block (Connection, Transform, Left,
|
|
||||||
// Right). Repeating the constants per row keeps the
|
|
||||||
// file parser-friendly (pandas / awk / spreadsheet).
|
|
||||||
std::ofstream f(path);
|
|
||||||
if (!f) {
|
|
||||||
ShowError("export: cannot open '" + path + "' for writing");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
f << "connection,transform,"
|
|
||||||
"left_module,left_part,"
|
|
||||||
"left_pin,left_signal,left_type,left_suspect,"
|
|
||||||
"right_module,right_part,"
|
|
||||||
"right_pin,right_signal,right_type,right_suspect,"
|
|
||||||
"type_mismatch\n";
|
|
||||||
|
|
||||||
int rows = 0;
|
|
||||||
for (auto &ckv : *sys->connections()) {
|
|
||||||
Connection *c = ckv.second;
|
|
||||||
for (auto &wp : c->pin_map) {
|
|
||||||
std::string lm, lp, ln, ls, lt, lsus;
|
|
||||||
std::string rm, rp, rn, rs, rt, rsus;
|
|
||||||
pin_side(wp.first, lm, lp, ln, ls, lt, lsus);
|
|
||||||
pin_side(wp.second, rm, rp, rn, rs, rt, rsus);
|
|
||||||
std::string tm = "no";
|
|
||||||
if (lt != "(NC)" && rt != "(NC)" && lt != rt) tm = "yes";
|
|
||||||
f << csv_quote(c->name) << ','
|
|
||||||
<< csv_quote(c->transform_name) << ','
|
|
||||||
<< csv_quote(lm) << ',' << csv_quote(lp) << ','
|
|
||||||
<< csv_quote(ln) << ',' << csv_quote(ls) << ','
|
|
||||||
<< csv_quote(lt) << ',' << csv_quote(lsus) << ','
|
|
||||||
<< csv_quote(rm) << ',' << csv_quote(rp) << ','
|
|
||||||
<< csv_quote(rn) << ',' << csv_quote(rs) << ','
|
|
||||||
<< csv_quote(rt) << ',' << csv_quote(rsus) << ','
|
|
||||||
<< tm << '\n';
|
|
||||||
++rows;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Print("export connections (.csv): " + std::to_string(rows)
|
|
||||||
+ " wire(s) → " + path);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ShowError("export: unknown kind '" + kind + "'\n"
|
|
||||||
"Known kinds: connections");
|
|
||||||
},
|
|
||||||
/*prompt_for_missing=*/ false,
|
|
||||||
"export structured data to CSV / ODS (kinds: connections; "
|
|
||||||
"bare form opens the file-picker dialog)",
|
|
||||||
/*scriptable=*/ true,
|
|
||||||
/*interactive=*/ true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
#include <doctest/doctest.h>
|
#include <doctest/doctest.h>
|
||||||
|
|
||||||
#include "system/analysis.hpp"
|
#include "core/domain/analysis.hpp"
|
||||||
#include "system/modules.hpp"
|
#include "core/domain/modules.hpp"
|
||||||
#include "system/parts.hpp"
|
#include "core/domain/parts.hpp"
|
||||||
#include "system/pins.hpp"
|
#include "core/domain/pins.hpp"
|
||||||
#include "system/signals.hpp"
|
#include "core/domain/signals.hpp"
|
||||||
#include "system/system.hpp"
|
#include "core/domain/system.hpp"
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
#include <doctest/doctest.h>
|
#include <doctest/doctest.h>
|
||||||
|
|
||||||
#include "system/analysis.hpp"
|
#include "core/domain/analysis.hpp"
|
||||||
#include "system/bsdl_check.hpp"
|
#include "core/domain/bsdl_check.hpp"
|
||||||
#include "system/bsdl_model.hpp"
|
#include "core/domain/bsdl_model.hpp"
|
||||||
#include "system/parts.hpp"
|
#include "core/domain/parts.hpp"
|
||||||
#include "system/pins.hpp"
|
#include "core/domain/pins.hpp"
|
||||||
#include "system/pin_spec.hpp"
|
#include "core/domain/pin_spec.hpp"
|
||||||
#include "system/system.hpp"
|
#include "core/domain/system.hpp"
|
||||||
#include "system/modules.hpp"
|
#include "core/domain/modules.hpp"
|
||||||
#include "system/persist.hpp"
|
#include "core/domain/persist.hpp"
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
#include <doctest/doctest.h>
|
#include <doctest/doctest.h>
|
||||||
|
|
||||||
#include "system/analysis.hpp"
|
#include "core/domain/analysis.hpp"
|
||||||
#include "system/bsdl_check.hpp"
|
#include "core/domain/bsdl_check.hpp"
|
||||||
#include "system/modules.hpp"
|
#include "core/domain/modules.hpp"
|
||||||
#include "system/parts.hpp"
|
#include "core/domain/parts.hpp"
|
||||||
#include "system/pin_spec.hpp"
|
#include "core/domain/pin_spec.hpp"
|
||||||
#include "system/pins.hpp"
|
#include "core/domain/pins.hpp"
|
||||||
#include "system/signals.hpp"
|
#include "core/domain/signals.hpp"
|
||||||
#include "system/system.hpp"
|
#include "core/domain/system.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#include <doctest/doctest.h>
|
#include <doctest/doctest.h>
|
||||||
|
|
||||||
#include "system/component_kind.hpp"
|
#include "core/domain/component_kind.hpp"
|
||||||
#include "system/parts.hpp"
|
#include "core/domain/parts.hpp"
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
|
|||||||
79
tests/test_connect.cpp
Normal file
79
tests/test_connect.cpp
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
#include <doctest/doctest.h>
|
||||||
|
|
||||||
|
#include "core/app/connect.hpp"
|
||||||
|
#include "core/domain/connect.hpp"
|
||||||
|
#include "core/domain/modules.hpp"
|
||||||
|
#include "core/domain/parts.hpp"
|
||||||
|
#include "core/domain/pins.hpp"
|
||||||
|
#include "core/domain/system.hpp"
|
||||||
|
|
||||||
|
// app::connect_parts is pure core: given two already-resolved parts it looks up
|
||||||
|
// the transform, fills identity NC pins, creates the Connection and returns a
|
||||||
|
// ConnectResult. No Print/dialog/FTXUI. These tests drive it directly.
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// A part with the given pin names, attached to a fresh module.
|
||||||
|
Part *make_part(System &sys, const std::string &mod, const std::string &part,
|
||||||
|
std::initializer_list<const char *> pins,
|
||||||
|
const std::string &type = "")
|
||||||
|
{
|
||||||
|
Module *m = sys.modules()->merge(mod);
|
||||||
|
Part *p = new Part(part);
|
||||||
|
p->connector_type = type;
|
||||||
|
m->add(p);
|
||||||
|
for (const char *pn : pins) p->add(new Pin(pn));
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("connect_parts wires an identity-compatible pair") {
|
||||||
|
System sys;
|
||||||
|
Module *a = sys.modules()->merge("A");
|
||||||
|
Module *b = sys.modules()->merge("B");
|
||||||
|
Part *p1 = make_part(sys, "A", "J1", {"1", "2"});
|
||||||
|
Part *p2 = make_part(sys, "B", "P1", {"1", "2"});
|
||||||
|
|
||||||
|
app::ConnectResult r = app::connect_parts(&sys, a, p1, b, p2);
|
||||||
|
|
||||||
|
CHECK(r.ok);
|
||||||
|
CHECK_FALSE(r.refused);
|
||||||
|
CHECK(r.transform_name == "identity");
|
||||||
|
CHECK(r.wires == 2);
|
||||||
|
CHECK(r.identity_info.empty()); // identical sets → no NC fill, no warning
|
||||||
|
CHECK(r.nc_added == 0);
|
||||||
|
CHECK(r.connection_name == "A/J1 <-> B/P1");
|
||||||
|
CHECK(sys.connections()->size() == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("connect_parts refuses an unknown connector-type pairing") {
|
||||||
|
System sys;
|
||||||
|
Module *a = sys.modules()->merge("A");
|
||||||
|
Module *b = sys.modules()->merge("B");
|
||||||
|
Part *p1 = make_part(sys, "A", "J1", {"1"}, "foo");
|
||||||
|
Part *p2 = make_part(sys, "B", "P1", {"1"}, "bar");
|
||||||
|
|
||||||
|
app::ConnectResult r = app::connect_parts(&sys, a, p1, b, p2);
|
||||||
|
|
||||||
|
CHECK_FALSE(r.ok);
|
||||||
|
CHECK(r.refused);
|
||||||
|
CHECK(r.error.find("no transform") != std::string::npos);
|
||||||
|
CHECK(sys.connections()->size() == 0); // nothing created
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("connect_parts fills NC pins on the subset side and reports it") {
|
||||||
|
System sys;
|
||||||
|
Module *a = sys.modules()->merge("A");
|
||||||
|
Module *b = sys.modules()->merge("B");
|
||||||
|
Part *p1 = make_part(sys, "A", "J1", {"1", "2", "3"}); // larger side
|
||||||
|
Part *p2 = make_part(sys, "B", "P1", {"1", "2"}); // missing "3"
|
||||||
|
|
||||||
|
app::ConnectResult r = app::connect_parts(&sys, a, p1, b, p2);
|
||||||
|
|
||||||
|
CHECK(r.ok);
|
||||||
|
CHECK_FALSE(r.identity_info.empty()); // subset path surfaces a warning
|
||||||
|
CHECK(r.nc_added == 1); // pin "3" materialised on B
|
||||||
|
CHECK(r.wires == 3); // all three now wired
|
||||||
|
CHECK(p2->size() == 3); // the NC pin really got added
|
||||||
|
}
|
||||||
76
tests/test_export.cpp
Normal file
76
tests/test_export.cpp
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
#include <doctest/doctest.h>
|
||||||
|
|
||||||
|
#include "core/app/export.hpp"
|
||||||
|
#include "core/domain/connect.hpp"
|
||||||
|
#include "core/domain/modules.hpp"
|
||||||
|
#include "core/domain/parts.hpp"
|
||||||
|
#include "core/domain/pins.hpp"
|
||||||
|
#include "core/domain/signals.hpp"
|
||||||
|
#include "core/domain/system.hpp"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::string slurp(const std::string &path)
|
||||||
|
{
|
||||||
|
std::ifstream f(path);
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << f.rdbuf();
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("export_format_from_path maps extensions") {
|
||||||
|
app::ExportFormat f;
|
||||||
|
CHECK(app::export_format_from_path("a.csv", f));
|
||||||
|
CHECK(f == app::ExportFormat::Csv);
|
||||||
|
CHECK(app::export_format_from_path("a.ODS", f));
|
||||||
|
CHECK(f == app::ExportFormat::Ods);
|
||||||
|
CHECK_FALSE(app::export_format_from_path("a.txt", f));
|
||||||
|
CHECK_FALSE(app::export_format_from_path("noext", f));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("export_connections writes a flat CSV (no UI needed)") {
|
||||||
|
// Two cards, one wired pin pair via a connection.
|
||||||
|
System sys;
|
||||||
|
Module *a = sys.modules()->merge("A");
|
||||||
|
Module *b = sys.modules()->merge("B");
|
||||||
|
Part *ja = new Part("J1"); a->add(ja);
|
||||||
|
Part *jb = new Part("P1"); b->add(jb);
|
||||||
|
Pin *pa = new Pin("1"); ja->add(pa);
|
||||||
|
Pin *pb = new Pin("1"); jb->add(pb);
|
||||||
|
Signal *sa = a->signals->merge("NETA"); sa->add(pa); pa->connect(sa);
|
||||||
|
Signal *sb = b->signals->merge("NETB"); sb->add(pb); pb->connect(sb);
|
||||||
|
|
||||||
|
Connection *c = new Connection("A.J1<->B.P1", a, ja, b, jb);
|
||||||
|
c->transform_name = "identity";
|
||||||
|
c->pin_map.emplace_back(pa, pb);
|
||||||
|
sys.connections()->add(c);
|
||||||
|
|
||||||
|
const char *path = "test_export_out.csv";
|
||||||
|
app::ExportResult r = app::export_connections(&sys, path, app::ExportFormat::Csv);
|
||||||
|
CHECK(r.ok);
|
||||||
|
CHECK(r.rows == 1);
|
||||||
|
|
||||||
|
std::string out = slurp(path);
|
||||||
|
CHECK(out.find("connection,transform,") == 0); // header present
|
||||||
|
CHECK(out.find("A.J1<->B.P1") != std::string::npos); // connection name
|
||||||
|
CHECK(out.find("identity") != std::string::npos); // transform
|
||||||
|
CHECK(out.find("NETA") != std::string::npos); // left signal
|
||||||
|
CHECK(out.find("NETB") != std::string::npos); // right signal
|
||||||
|
|
||||||
|
std::remove(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("export_connections reports a bad path instead of crashing") {
|
||||||
|
System sys;
|
||||||
|
app::ExportResult r = app::export_connections(
|
||||||
|
&sys, "/nonexistent-dir-xyz/out.csv", app::ExportFormat::Csv);
|
||||||
|
CHECK_FALSE(r.ok);
|
||||||
|
CHECK_FALSE(r.error.empty());
|
||||||
|
}
|
||||||
63
tests/test_load.cpp
Normal file
63
tests/test_load.cpp
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
#include <doctest/doctest.h>
|
||||||
|
|
||||||
|
#include "core/app/load.hpp"
|
||||||
|
#include "core/domain/modules.hpp"
|
||||||
|
#include "core/domain/system.hpp"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <fstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// app::load_module is pure core: import a module, drop singleton signals, infer
|
||||||
|
// signal types, return counts or an error — no Print/dialog/FTXUI. The parse
|
||||||
|
// helper import_type_from_name is likewise UI-free.
|
||||||
|
|
||||||
|
TEST_CASE("import_type_from_name maps names case-insensitively") {
|
||||||
|
ImportType t;
|
||||||
|
CHECK(app::import_type_from_name("mentor", t));
|
||||||
|
CHECK(t == ImportType::IMPORT_MENTOR);
|
||||||
|
CHECK(app::import_type_from_name("ALTIUM", t));
|
||||||
|
CHECK(t == ImportType::IMPORT_ALTIUM);
|
||||||
|
CHECK(app::import_type_from_name("Ods", t));
|
||||||
|
CHECK(t == ImportType::IMPORT_ODS);
|
||||||
|
CHECK_FALSE(app::import_type_from_name("kicad", t));
|
||||||
|
CHECK_FALSE(app::import_type_from_name("", t));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("load_module imports, drops singletons and reports counts") {
|
||||||
|
// Minimal Mentor netlist: two parts; NETA/NETB span both parts (2 pins
|
||||||
|
// each, kept), LONELY sits on one pin only (dropped as a singleton).
|
||||||
|
const char *path = "test_load_in.net";
|
||||||
|
{
|
||||||
|
std::ofstream f(path);
|
||||||
|
f << "COMP: 'C1' 'J1'\n"
|
||||||
|
" Explicit Pin: '1' 'x' 'NETA'\n"
|
||||||
|
" Explicit Pin: '2' 'x' 'NETB'\n"
|
||||||
|
" Explicit Pin: '3' 'x' 'LONELY'\n"
|
||||||
|
"COMP: 'C2' 'J2'\n"
|
||||||
|
" Explicit Pin: '1' 'x' 'NETA'\n"
|
||||||
|
" Explicit Pin: '2' 'x' 'NETB'\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
System sys;
|
||||||
|
app::LoadResult r = app::load_module(&sys, "M", path, ImportType::IMPORT_MENTOR);
|
||||||
|
|
||||||
|
CHECK(r.ok);
|
||||||
|
CHECK(r.error.empty());
|
||||||
|
CHECK(r.parts == 2);
|
||||||
|
CHECK(r.signals == 2); // NETA, NETB — LONELY dropped
|
||||||
|
CHECK(r.dropped == 1); // LONELY
|
||||||
|
|
||||||
|
std::remove(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("load_module fails cleanly on a missing file") {
|
||||||
|
// ImportBase opens read-only and System::Load checks is_open(), so a missing
|
||||||
|
// file is a clean error — and no empty module is left in the system.
|
||||||
|
System sys;
|
||||||
|
app::LoadResult r = app::load_module(
|
||||||
|
&sys, "M", "/nonexistent-dir-xyz/nope.net", ImportType::IMPORT_MENTOR);
|
||||||
|
CHECK_FALSE(r.ok);
|
||||||
|
CHECK(r.error.find("cannot open") != std::string::npos);
|
||||||
|
CHECK(sys.modules()->size() == 0);
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
#include <doctest/doctest.h>
|
#include <doctest/doctest.h>
|
||||||
|
|
||||||
#include "system/modules.hpp"
|
#include "core/domain/modules.hpp"
|
||||||
#include "system/parts.hpp"
|
#include "core/domain/parts.hpp"
|
||||||
#include "system/persist.hpp"
|
#include "core/domain/persist.hpp"
|
||||||
#include "system/pins.hpp"
|
#include "core/domain/pins.hpp"
|
||||||
#include "system/signals.hpp"
|
#include "core/domain/signals.hpp"
|
||||||
#include "system/system.hpp"
|
#include "core/domain/system.hpp"
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
#include <doctest/doctest.h>
|
#include <doctest/doctest.h>
|
||||||
|
|
||||||
#include "system/connect.hpp"
|
#include "core/domain/connect.hpp"
|
||||||
#include "system/modules.hpp"
|
#include "core/domain/modules.hpp"
|
||||||
#include "system/parts.hpp"
|
#include "core/domain/parts.hpp"
|
||||||
#include "system/persist.hpp"
|
#include "core/domain/persist.hpp"
|
||||||
#include "system/pins.hpp"
|
#include "core/domain/pins.hpp"
|
||||||
#include "system/signals.hpp"
|
#include "core/domain/signals.hpp"
|
||||||
#include "system/system.hpp"
|
#include "core/domain/system.hpp"
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
#include <doctest/doctest.h>
|
#include <doctest/doctest.h>
|
||||||
|
|
||||||
#include "system/parts.hpp"
|
#include "core/domain/parts.hpp"
|
||||||
#include "system/pin_model.hpp"
|
#include "core/domain/pin_model.hpp"
|
||||||
#include "system/pin_spec.hpp"
|
#include "core/domain/pin_spec.hpp"
|
||||||
#include "system/pins.hpp"
|
#include "core/domain/pins.hpp"
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
#include <doctest/doctest.h>
|
#include <doctest/doctest.h>
|
||||||
|
|
||||||
#include "system/parts.hpp"
|
#include "core/domain/parts.hpp"
|
||||||
#include "system/pin_name.hpp"
|
#include "core/domain/pin_name.hpp"
|
||||||
#include "system/pins.hpp"
|
#include "core/domain/pins.hpp"
|
||||||
#include "system/transform.hpp"
|
#include "core/domain/transform.hpp"
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#include <doctest/doctest.h>
|
#include <doctest/doctest.h>
|
||||||
|
|
||||||
#include "system/signal_type.hpp"
|
#include "core/domain/signal_type.hpp"
|
||||||
|
|
||||||
TEST_CASE("signal_type_name round-trips with from_name") {
|
TEST_CASE("signal_type_name round-trips with from_name") {
|
||||||
SignalType t;
|
SignalType t;
|
||||||
|
|||||||
95
tests/test_verify.cpp
Normal file
95
tests/test_verify.cpp
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
#include <doctest/doctest.h>
|
||||||
|
|
||||||
|
#include "core/app/verify.hpp"
|
||||||
|
#include "core/domain/connect.hpp"
|
||||||
|
#include "core/domain/modules.hpp"
|
||||||
|
#include "core/domain/parts.hpp"
|
||||||
|
#include "core/domain/pins.hpp"
|
||||||
|
#include "core/domain/signals.hpp"
|
||||||
|
#include "core/domain/system.hpp"
|
||||||
|
|
||||||
|
// app::verify is pure core: it takes a System* and returns a VerifyReport of
|
||||||
|
// structured findings, with no Print/dialog/FTXUI. These tests build small
|
||||||
|
// systems by hand and assert the report — no UI involved.
|
||||||
|
|
||||||
|
TEST_CASE("verify on a null or empty system reports nothing") {
|
||||||
|
app::VerifyReport none = app::verify(nullptr);
|
||||||
|
CHECK(none.typed_pins == 0);
|
||||||
|
CHECK(none.total_nets == 0);
|
||||||
|
CHECK(none.role_mismatches.empty());
|
||||||
|
|
||||||
|
System sys;
|
||||||
|
app::VerifyReport r = app::verify(&sys);
|
||||||
|
CHECK(r.typed_pins == 0);
|
||||||
|
CHECK(r.total_nets == 0);
|
||||||
|
CHECK(r.bridged_nets == 0);
|
||||||
|
CHECK(r.net_inconsistencies.empty());
|
||||||
|
CHECK(r.orphan_total() == 0);
|
||||||
|
CHECK(r.model_total() == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("verify flags a bridged net that mixes Power and GndShield") {
|
||||||
|
// Two cards, one wired pin pair: A.NETA (Power) <-> B.NETB (GndShield).
|
||||||
|
System sys;
|
||||||
|
Module *a = sys.modules()->merge("A");
|
||||||
|
Module *b = sys.modules()->merge("B");
|
||||||
|
Part *ja = new Part("J1"); a->add(ja);
|
||||||
|
Part *jb = new Part("P1"); b->add(jb);
|
||||||
|
Pin *pa = new Pin("1"); ja->add(pa);
|
||||||
|
Pin *pb = new Pin("1"); jb->add(pb);
|
||||||
|
Signal *sa = a->signals->merge("NETA"); sa->type = SignalType::Power;
|
||||||
|
Signal *sb = b->signals->merge("NETB"); sb->type = SignalType::GndShield;
|
||||||
|
sa->add(pa); pa->connect(sa);
|
||||||
|
sb->add(pb); pb->connect(sb);
|
||||||
|
|
||||||
|
Connection *c = new Connection("A.J1<->B.P1", a, ja, b, jb);
|
||||||
|
c->transform_name = "identity";
|
||||||
|
c->pin_map.emplace_back(pa, pb);
|
||||||
|
sys.connections()->add(c);
|
||||||
|
|
||||||
|
app::VerifyReport r = app::verify(&sys);
|
||||||
|
|
||||||
|
CHECK(r.total_nets == 1);
|
||||||
|
CHECK(r.bridged_nets == 1);
|
||||||
|
REQUIRE(r.net_inconsistencies.size() == 1);
|
||||||
|
CHECK(r.net_inconsistencies[0].members.size() == 2);
|
||||||
|
// Both endpoints are present with their declared types.
|
||||||
|
bool seen_power = false, seen_gnd = false;
|
||||||
|
for (const auto &m : r.net_inconsistencies[0].members) {
|
||||||
|
if (m.type == SignalType::Power) seen_power = true;
|
||||||
|
if (m.type == SignalType::GndShield) seen_gnd = true;
|
||||||
|
}
|
||||||
|
CHECK(seen_power);
|
||||||
|
CHECK(seen_gnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("verify counts orphan pins by their import origin") {
|
||||||
|
System sys;
|
||||||
|
Module *m = sys.modules()->merge("M");
|
||||||
|
Part *p = new Part("J1"); m->add(p);
|
||||||
|
Pin *imp = new Pin("1"); imp->nc_origin = NcOrigin::ImportedUnconnected; p->add(imp);
|
||||||
|
Pin *drp = new Pin("2"); drp->nc_origin = NcOrigin::DroppedSingleton; p->add(drp);
|
||||||
|
Pin *wired = new Pin("3"); p->add(wired);
|
||||||
|
Signal *s = m->signals->merge("NET"); s->add(wired); wired->connect(s);
|
||||||
|
|
||||||
|
app::VerifyReport r = app::verify(&sys);
|
||||||
|
|
||||||
|
CHECK(r.orphan_imported == 1);
|
||||||
|
CHECK(r.orphan_dropped == 1);
|
||||||
|
CHECK(r.orphan_total() == 2);
|
||||||
|
|
||||||
|
// Per-pin detail carries the path and origin (the dashboard lists the
|
||||||
|
// dropped ones under the NC health row).
|
||||||
|
REQUIRE(r.orphans.size() == 2);
|
||||||
|
int n_dropped = 0;
|
||||||
|
bool dropped_path_ok = false;
|
||||||
|
for (const auto &o : r.orphans) {
|
||||||
|
if (o.dropped) {
|
||||||
|
++n_dropped;
|
||||||
|
if (o.module == "M" && o.part == "J1" && o.pin == "2")
|
||||||
|
dropped_path_ok = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CHECK(n_dropped == 1);
|
||||||
|
CHECK(dropped_path_ok);
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
#include <doctest/doctest.h>
|
#include <doctest/doctest.h>
|
||||||
|
|
||||||
#include "system/modules.hpp"
|
#include "core/domain/modules.hpp"
|
||||||
#include "system/parts.hpp"
|
#include "core/domain/parts.hpp"
|
||||||
#include "system/pins.hpp"
|
#include "core/domain/pins.hpp"
|
||||||
#include "system/signals.hpp"
|
#include "core/domain/signals.hpp"
|
||||||
#include "system/system.hpp"
|
#include "core/domain/system.hpp"
|
||||||
#include "system/transform.hpp"
|
#include "core/domain/transform.hpp"
|
||||||
#include "system/transform_vpx.hpp"
|
#include "core/domain/transform_vpx.hpp"
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#include <doctest/doctest.h>
|
#include <doctest/doctest.h>
|
||||||
|
|
||||||
#include "tui/tui_helpers.hpp"
|
#include "frontends/tui/tui_helpers.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
Reference in New Issue
Block a user