Compare commits
36 Commits
60c00eb914
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| dceb61237d | |||
| 0b10e1c1b7 | |||
| 9cf43696a2 | |||
| e914c84c18 | |||
| 1943f1f88a | |||
| c2b1f4c4ae | |||
| 794430e86c | |||
| a9039a8eea | |||
| b0e260a2ec | |||
| fc71cce647 | |||
| 184b0d306f | |||
| d4eac9557b | |||
| 19dbec9672 | |||
| fc3ef333fa | |||
| b999446151 | |||
| 7e88f82446 | |||
| 76807b0307 | |||
| 4803d7d01c | |||
| e561c0f960 | |||
| 091ef6fe4b | |||
| 3b6e626c8f | |||
| af36f7c150 | |||
| 0517a82a5c | |||
| 4ef110ab70 | |||
| b36af3167a | |||
| a040cc1957 | |||
| 25939998ab | |||
| e3350b8d95 | |||
| cccc5f131d | |||
| 63ca17d048 | |||
| 3010bb25eb | |||
| ac2edd90c4 | |||
| 53eb79c760 | |||
| 29cb353d75 | |||
| c70e767cf1 | |||
| 527a48145b |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1 +1 @@
|
|||||||
build/
|
build*/
|
||||||
|
|||||||
103
CMakeLists.txt
103
CMakeLists.txt
@@ -11,49 +11,64 @@ project(essim
|
|||||||
|
|
||||||
include(FetchContent)
|
include(FetchContent)
|
||||||
|
|
||||||
set(FTXUI_BUILD_DOCS OFF CACHE INTERNAL "")
|
# Shared CMake helpers (essim_add_frontend — per-frontend target boilerplate).
|
||||||
set(FTXUI_BUILD_EXAMPLES OFF CACHE INTERNAL "")
|
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
|
||||||
set(FTXUI_BUILD_TESTS OFF CACHE INTERNAL "")
|
include(EssimFrontend)
|
||||||
set(FTXUI_ENABLE_INSTALL OFF CACHE INTERNAL "")
|
|
||||||
|
|
||||||
FetchContent_Declare(ftxui
|
# ----------------------------------------------------------------- core deps
|
||||||
GIT_REPOSITORY https://github.com/ArthurSonzogni/FTXUI.git
|
# libbsdl — standalone BSDL parser (LGPL-2.1), dynamically linked (EUPL-1.2,
|
||||||
GIT_TAG v6.1.9
|
# which the LGPL permits). Override its path with -DBSDL_DIR=...
|
||||||
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 wx 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 +80,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 +133,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()
|
||||||
|
|||||||
192
DESIGN.md
192
DESIGN.md
@@ -10,72 +10,120 @@ 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)
|
wx/ -- wxWidgets GUI (builds libessim_wx + the `essim` binary)
|
||||||
screen_filedialog.cpp BuildFileDialog + OpenFileDialog (reusable file picker)
|
CMakeLists.txt find_package(wxWidgets) + essim_add_frontend(wx ...)
|
||||||
screen_error.cpp BuildErrorModal + ShowError (centred error popup)
|
main.cpp entry point: construct WxFrontend, call frontend_main
|
||||||
screen_help.cpp BuildHelpScreen (topic-driven feature reference)
|
wx_frontend.{hpp,cpp} WxFrontend : Frontend (owns System; boots wx in Run())
|
||||||
screen_sigtype_modal.cpp BuildSignalTypeModal (popup attached to explore via Modal())
|
wx_frame.{hpp,cpp} EssimFrame: menu/tree/overview/log over core + app::*
|
||||||
doc/classes.puml -- PlantUML class diagram
|
cmake/EssimFrontend.cmake -- essim_add_frontend(name LIBS ...) per-frontend wiring
|
||||||
|
tests/ -- core tests (link essim_core)
|
||||||
|
tui/ -- frontend tests (link essim_tui)
|
||||||
|
doc/ , test/ -- docs; sample netlists + system.essim bring-up script
|
||||||
```
|
```
|
||||||
|
|
||||||
`include/` and `lib/` are kept empty by design — FTXUI used to live there as precompiled `.a` + headers, now it comes through FetchContent.
|
## Architecture — core vs frontends
|
||||||
|
|
||||||
|
The hard rule: **`src/core/` never depends on a frontend** — no `#include
|
||||||
|
"frontends/…"`, no GUI toolkit. Frontends depend on the core, never the reverse
|
||||||
|
(`essim_core` links libzip / pugixml / bsdl only).
|
||||||
|
|
||||||
|
- **Domain** (`core/domain/`) — the model and the read-only analyses
|
||||||
|
(`analyze_system`, the `check_*` passes, `compute_all_nets`).
|
||||||
|
- **Application** (`core/app/`) — use-case operations a frontend invokes, e.g.
|
||||||
|
`export_connections(System*, path, format) -> ExportResult`. An operation
|
||||||
|
builds its artefact and returns data/stats; it **never** prints or opens a
|
||||||
|
dialog. (Anti-pattern being removed: the export command used to build the file
|
||||||
|
inside its lambda. The TUI command is now a thin wrapper — resolve args/dialog
|
||||||
|
→ call the core op → render the result.)
|
||||||
|
- **Frontends** (`frontends/<name>/`) — thin: map UI events to core calls and
|
||||||
|
render results. Each implements the **`Frontend`** interface
|
||||||
|
(`frontends/frontend.hpp`: `BootDispatch`, `DumpCommandsMd`, `DumpOutput`,
|
||||||
|
`Run`). The process entry is shared and frontend-agnostic:
|
||||||
|
`frontend_main(argc, argv, Frontend&)` (`frontends/frontend_main.cpp`, built
|
||||||
|
into the toolkit-free `essim_frontend` lib) parses the CLI flags and drives the
|
||||||
|
boot → batch/run flow through the interface; a frontend's `main()` is just
|
||||||
|
*construct the concrete Frontend, call `frontend_main`*. Two frontends ship
|
||||||
|
today: **tui** (FTXUI, default) and **wx** (a wxWidgets GUI, menu-driven over
|
||||||
|
`app::*`). Add another by creating `src/frontends/<name>/CMakeLists.txt` — its
|
||||||
|
toolkit setup (FetchContent / find_package) plus one call to
|
||||||
|
`essim_add_frontend(<name> LIBS …)` (the shared helper in
|
||||||
|
`cmake/EssimFrontend.cmake` that builds `essim_<name>` + the `essim` binary) —
|
||||||
|
and configuring `-DESSIM_FRONTEND=<name>`.
|
||||||
|
|
||||||
|
Because the core links no toolkit, the suite links `essim_core` directly and
|
||||||
|
`-DESSIM_FRONTEND=none` builds + tests the whole core with FTXUI never fetched.
|
||||||
|
|
||||||
## Domain conventions
|
## 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 +158,11 @@ Built-in commands: `new`, `set`, `load`, `duplicate`, `save`, `restore`, `source
|
|||||||
|
|
||||||
Pending prompts (from incomplete inline commands) are NOT considered interactive and are filled by subsequent script lines, the way you'd expect. Lines starting with `#` and blank lines are skipped; leading/trailing whitespace is trimmed; `~/` is expanded.
|
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 +171,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 +193,13 @@ Exposed as the `analyze` shell command which prints groups (sorted by module + l
|
|||||||
|
|
||||||
**Component classification**: every `Part` carries a `ComponentKind kind` (`Passive | Semiconductor | IntegratedCircuit | Connector | TestPoint | Switch | Crystal | Mechanical | Other`) inferred at construction by `infer_component_kind(name)` from the leading reference-designator letter(s) (longest-match: `LED/TP/SW/FB/MK/MP/MH/HS/RA/RN/RP/RV` first, then single-letter R/C/L/F/D/Q/U/J/P/Y/X/S). Recomputed on `restore` (no persistence tag). Not yet exposed in TUI commands — branchpoints will be `set-connector-type` guard, `explore` filter, and `explore` header.
|
**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 +236,7 @@ Today the only caller is `export` (`{"CSV", ".csv"}, {"ODS", ".ods"}` filters, k
|
|||||||
|
|
||||||
**Per-key path persistence** (`SaveLastUsed(key, dir, filename)` / `LoadLastUsed(key, &dir, &filename)` in `shell.cpp`): each key writes a tiny two-line file (`dir\nfilename\n`) under `UserDataDir() / <key>.last`. `UserDataDir()` is the cross-platform `XDG_DATA_HOME` / `LOCALAPPDATA` etc. helper also used by the command history file. Free functions, not Tui members, so any module (the file dialog today; could be the script-save buffer or the save command tomorrow) can use them with the same minimal API.
|
**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 +296,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 +312,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 +351,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 +363,7 @@ Recomputed on `restore` (no persistence tag). Currently not used by any decision
|
|||||||
|
|
||||||
`connect` looks up a registered transform for `(p1->connector_type, p2->connector_type)` via `TransformRegistry::lookup`, tried in both directions. Fall-through is `IdentityTransform`:
|
`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.
|
||||||
|
|
||||||
|
|||||||
42
README.md
42
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,21 @@ 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.
|
||||||
|
- **wxWidgets** (≥ 3.2) — only for the **wx** GUI frontend
|
||||||
|
(`-DESSIM_FRONTEND=wx`). Install the development package:
|
||||||
|
- Debian/Ubuntu — `sudo apt install libwxgtk3.2-dev`
|
||||||
|
- Arch — `sudo pacman -S wxwidgets-gtk3`
|
||||||
|
- Fedora — `sudo dnf install wxGTK-devel`
|
||||||
- Optional, only for the `doc` target: **doxygen** and **python3**.
|
- Optional, only for the `doc` target: **doxygen** and **python3**.
|
||||||
|
|
||||||
|
libzip, pugixml and libbsdl are the **core** dependencies; each frontend pulls
|
||||||
|
its own toolkit (FTXUI for tui, wxWidgets for wx), so a `-DESSIM_FRONTEND=none`
|
||||||
|
build needs neither. Pick a GUI/TUI with `-DESSIM_FRONTEND=tui|wx` (default
|
||||||
|
`tui`).
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -53,6 +71,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 +97,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).
|
||||||
|
|||||||
30
cmake/EssimFrontend.cmake
Normal file
30
cmake/EssimFrontend.cmake
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# essim_add_frontend(<name> [LIBS <toolkit link targets>...])
|
||||||
|
#
|
||||||
|
# Builds the boilerplate shared by every frontend under src/frontends/<name>/:
|
||||||
|
# * a static library essim_<name> from every .cpp in the current directory
|
||||||
|
# except main.cpp, linking essim_core plus the frontend's own GUI/TUI
|
||||||
|
# toolkit (LIBS);
|
||||||
|
# * the `essim` executable from main.cpp, linking essim_<name> and the shared,
|
||||||
|
# toolkit-free launcher essim_frontend, emitted at the top of the build tree
|
||||||
|
# (./build/essim) whichever frontend produced it.
|
||||||
|
#
|
||||||
|
# A per-frontend CMakeLists only sets up its toolkit (FetchContent /
|
||||||
|
# find_package, and any directory-scoped include dirs / definitions) and then
|
||||||
|
# calls this with the toolkit's link targets — no target wiring repeated.
|
||||||
|
function(essim_add_frontend name)
|
||||||
|
cmake_parse_arguments(FE "" "" "LIBS" ${ARGN})
|
||||||
|
|
||||||
|
set(dir "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||||
|
|
||||||
|
# Frontend library = every .cpp here except the entry point.
|
||||||
|
file(GLOB FE_SOURCES "${dir}/*.cpp")
|
||||||
|
list(REMOVE_ITEM FE_SOURCES "${dir}/main.cpp")
|
||||||
|
|
||||||
|
add_library(essim_${name} STATIC ${FE_SOURCES})
|
||||||
|
target_link_libraries(essim_${name} PUBLIC essim_core ${FE_LIBS})
|
||||||
|
|
||||||
|
add_executable(essim "${dir}/main.cpp")
|
||||||
|
target_link_libraries(essim PRIVATE essim_${name} essim_frontend)
|
||||||
|
set_target_properties(essim PROPERTIES
|
||||||
|
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}")
|
||||||
|
endfunction()
|
||||||
67
src/core/app/connect.cpp
Normal file
67
src/core/app/connect.cpp
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
#include "core/app/connect.hpp"
|
||||||
|
|
||||||
|
#include "core/domain/connect.hpp"
|
||||||
|
#include "core/domain/modules.hpp"
|
||||||
|
#include "core/domain/parts.hpp"
|
||||||
|
#include "core/domain/system.hpp"
|
||||||
|
#include "core/domain/transform.hpp"
|
||||||
|
|
||||||
|
#include <exception>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace app {
|
||||||
|
|
||||||
|
ConnectResult connect_parts(System *sys, Module *m1, Part *p1,
|
||||||
|
Module *m2, Part *p2)
|
||||||
|
{
|
||||||
|
ConnectResult r;
|
||||||
|
|
||||||
|
auto ® = TransformRegistry::get();
|
||||||
|
Transform *t = reg.lookup(p1->connector_type, p2->connector_type);
|
||||||
|
bool both_empty = p1->connector_type.empty() && p2->connector_type.empty();
|
||||||
|
|
||||||
|
if (t == reg.identity()) {
|
||||||
|
if (!both_empty) {
|
||||||
|
r.refused = true;
|
||||||
|
r.error = "no transform for types '"
|
||||||
|
+ (p1->connector_type.empty() ? std::string("(none)")
|
||||||
|
: p1->connector_type)
|
||||||
|
+ "' ↔ '"
|
||||||
|
+ (p2->connector_type.empty() ? std::string("(none)")
|
||||||
|
: p2->connector_type)
|
||||||
|
+ "'. Set matching types via 'set-connector-type' first.";
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
std::string info;
|
||||||
|
std::string err = CheckIdentityCompatible(p1, p2, &info);
|
||||||
|
if (!err.empty()) {
|
||||||
|
r.refused = true;
|
||||||
|
r.error = err;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
if (!info.empty()) {
|
||||||
|
r.identity_info = info;
|
||||||
|
r.nc_added = FillIdentityNCs(p1, p2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto pin_map = t->apply(p1, p2);
|
||||||
|
|
||||||
|
r.connection_name = m1->name + "/" + p1->name
|
||||||
|
+ " <-> " + m2->name + "/" + p2->name;
|
||||||
|
r.transform_name = t->name;
|
||||||
|
try {
|
||||||
|
Connection *c = new Connection(r.connection_name, m1, p1, m2, p2);
|
||||||
|
c->transform_name = t->name;
|
||||||
|
c->pin_map = std::move(pin_map);
|
||||||
|
sys->connections()->add(c);
|
||||||
|
r.wires = (int)c->pin_map.size();
|
||||||
|
r.ok = true;
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
r.error = e.what();
|
||||||
|
}
|
||||||
|
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace app
|
||||||
42
src/core/app/connect.hpp
Normal file
42
src/core/app/connect.hpp
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#ifndef _APP_CONNECT_HPP_
|
||||||
|
#define _APP_CONNECT_HPP_
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class System;
|
||||||
|
class Module;
|
||||||
|
class Part;
|
||||||
|
|
||||||
|
// Application layer: UI-independent operations that any frontend (TUI, GUI, …)
|
||||||
|
// can call. No console, no dialogs, no FTXUI — just System in, result out.
|
||||||
|
namespace app {
|
||||||
|
|
||||||
|
// Outcome of connecting two parts. The side effects (filling identity NC pins,
|
||||||
|
// creating the Connection and adding it to the system) all happen in core; the
|
||||||
|
// caller only renders the fields.
|
||||||
|
struct ConnectResult {
|
||||||
|
bool ok = false; ///< a Connection was created and added
|
||||||
|
bool refused = false; ///< a business rule rejected it (vs. an exception)
|
||||||
|
std::string error; ///< why refused/failed; empty when ok
|
||||||
|
|
||||||
|
std::string connection_name;
|
||||||
|
std::string transform_name;
|
||||||
|
int wires = 0; ///< pin_map size of the created connection
|
||||||
|
|
||||||
|
// Identity-transform path only: the compatibility info line and how many NC
|
||||||
|
// pins were materialised so both sides match. Empty / 0 otherwise.
|
||||||
|
std::string identity_info;
|
||||||
|
int nc_added = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Wire part `p1` (in module `m1`) to part `p2` (in module `m2`): look up the
|
||||||
|
// transform for their connector types, refuse on an unknown pairing or an
|
||||||
|
// identity-incompatible layout, fill identity NC pins when needed, apply the
|
||||||
|
// transform and create the Connection. Pure core — no resolution of names or
|
||||||
|
// patterns (the frontend turns user input into the Module*/Part* it passes).
|
||||||
|
ConnectResult connect_parts(System *sys, Module *m1, Part *p1,
|
||||||
|
Module *m2, Part *p2);
|
||||||
|
|
||||||
|
} // namespace app
|
||||||
|
|
||||||
|
#endif // _APP_CONNECT_HPP_
|
||||||
137
src/core/app/edit.cpp
Normal file
137
src/core/app/edit.cpp
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
#include "core/app/edit.hpp"
|
||||||
|
|
||||||
|
#include "core/domain/bsdl_model.hpp"
|
||||||
|
#include "core/domain/modules.hpp"
|
||||||
|
#include "core/domain/parts.hpp"
|
||||||
|
#include "core/domain/pin_model.hpp"
|
||||||
|
#include "core/domain/pins.hpp"
|
||||||
|
#include "core/domain/signals.hpp"
|
||||||
|
#include "core/domain/system.hpp"
|
||||||
|
#include "core/domain/transform_vpx.hpp" // ValidatePartForKind
|
||||||
|
|
||||||
|
#include <exception>
|
||||||
|
|
||||||
|
namespace app {
|
||||||
|
|
||||||
|
SetConnectorTypeResult set_connector_type(Part *part, const std::string &kind)
|
||||||
|
{
|
||||||
|
SetConnectorTypeResult r;
|
||||||
|
if (!part) {
|
||||||
|
r.error = "no part";
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string err = ValidatePartForKind(part, kind);
|
||||||
|
if (!err.empty()) {
|
||||||
|
r.error = err;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
part->connector_type = kind;
|
||||||
|
ConnectorModel model(kind);
|
||||||
|
ApplyReport rep = apply_model(part, model);
|
||||||
|
r.materialised = rep.materialised;
|
||||||
|
r.ok = true;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
AttachBsdlResult attach_bsdl(Part *part, const std::string &path)
|
||||||
|
{
|
||||||
|
AttachBsdlResult r;
|
||||||
|
if (!part) {
|
||||||
|
r.error = "no part";
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
BsdlModel model = BsdlModel::from_file(path);
|
||||||
|
if (!model.valid()) {
|
||||||
|
r.error = "cannot parse " + path
|
||||||
|
+ (model.error().empty() ? "" : (": " + model.error()));
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
BsdlApplyReport rep = apply_bsdl(part, model);
|
||||||
|
part->bsdl_path = path;
|
||||||
|
r.entity = model.entity();
|
||||||
|
r.bound = rep.bound;
|
||||||
|
r.unbound = rep.unbound;
|
||||||
|
r.ports_total = (int)model.ports().size();
|
||||||
|
r.ok = true;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetSignalTypeResult set_signal_type(Signal *sig, const std::string &type_name)
|
||||||
|
{
|
||||||
|
SetSignalTypeResult r;
|
||||||
|
if (!sig) {
|
||||||
|
r.error = "no signal";
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
SignalType t;
|
||||||
|
if (!signal_type_from_name(type_name, t)) {
|
||||||
|
r.error = "type must be one of: power, gnd, other (got: " + type_name + ")";
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
sig->type = t;
|
||||||
|
r.type = t;
|
||||||
|
r.ok = true;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
DuplicateResult duplicate_module(System *sys, const std::string &src_name,
|
||||||
|
const std::string &dst_name)
|
||||||
|
{
|
||||||
|
DuplicateResult r;
|
||||||
|
if (!sys) { r.error = "no system"; return r; }
|
||||||
|
|
||||||
|
Module *src;
|
||||||
|
try { src = sys->modules()->get(src_name); }
|
||||||
|
catch (const std::exception &) {
|
||||||
|
r.error = "unknown module: " + src_name;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
if (sys->modules()->exists(dst_name)) {
|
||||||
|
r.error = "duplicate refused: module '" + dst_name + "' already exists.";
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
Module *dst = new Module(dst_name);
|
||||||
|
|
||||||
|
// Signals first (preserve type overrides), so pins can re-wire to them.
|
||||||
|
for (auto &skv : *src->signals) {
|
||||||
|
Signal *ss = skv.second;
|
||||||
|
Signal *ds = new Signal(ss->name);
|
||||||
|
ds->type = ss->type;
|
||||||
|
dst->signals->add(ds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parts, pins (spec + nc_origin), and the pin→signal wiring.
|
||||||
|
for (auto &pkv : *src) {
|
||||||
|
Part *sp = pkv.second;
|
||||||
|
Part *dp = new Part(sp->name);
|
||||||
|
dp->connector_type = sp->connector_type;
|
||||||
|
for (auto &nkv : *sp) {
|
||||||
|
Pin *sn = nkv.second;
|
||||||
|
Pin *dn = new Pin(sn->name);
|
||||||
|
dn->spec = sn->spec;
|
||||||
|
dn->nc_origin = sn->nc_origin;
|
||||||
|
dp->add(dn);
|
||||||
|
if (sn->signal()) {
|
||||||
|
Signal *ds = dst->signals->get(sn->signal()->name);
|
||||||
|
ds->add(dn);
|
||||||
|
dn->connect(ds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dst->add(dp);
|
||||||
|
}
|
||||||
|
|
||||||
|
sys->modules()->add(dst);
|
||||||
|
r.parts = (int)dst->size();
|
||||||
|
r.signals = (int)dst->signals->size();
|
||||||
|
r.ok = true;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace app
|
||||||
73
src/core/app/edit.hpp
Normal file
73
src/core/app/edit.hpp
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
#ifndef _APP_EDIT_HPP_
|
||||||
|
#define _APP_EDIT_HPP_
|
||||||
|
|
||||||
|
#include "core/domain/signal_type.hpp" // SignalType
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class Part;
|
||||||
|
class Signal;
|
||||||
|
class System;
|
||||||
|
|
||||||
|
// Application layer: UI-independent part-editing operations any frontend can
|
||||||
|
// call. No console, no dialogs, no FTXUI — Part in, result struct out.
|
||||||
|
namespace app {
|
||||||
|
|
||||||
|
// Outcome of tagging a part's connector type. The op validates the kind, sets
|
||||||
|
// the type and applies the connector model (which may materialise the layout's
|
||||||
|
// missing NC pins); the caller renders the result.
|
||||||
|
struct SetConnectorTypeResult {
|
||||||
|
bool ok = false;
|
||||||
|
std::string error; ///< set when refused (kind invalid for the part)
|
||||||
|
int materialised = 0; ///< NC pins created from the connector layout
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tag `part`'s connector type and apply the matching connector model. Refuses
|
||||||
|
// (ok=false, error set, no mutation) when the kind is invalid for the part.
|
||||||
|
SetConnectorTypeResult set_connector_type(Part *part, const std::string &kind);
|
||||||
|
|
||||||
|
// Outcome of attaching a BSDL model to a part. On success the part's pin specs
|
||||||
|
// are filled from the model and its bsdl_path is recorded.
|
||||||
|
struct AttachBsdlResult {
|
||||||
|
bool ok = false;
|
||||||
|
std::string error; ///< set when the .bsd cannot be parsed
|
||||||
|
std::string entity; ///< the BSDL entity name
|
||||||
|
int bound = 0; ///< ports matched to a pin
|
||||||
|
int unbound = 0; ///< ports with no matching pin
|
||||||
|
int ports_total = 0; ///< ports declared in the model
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse the BSDL file at `path` and apply it to `part` (fills each pin's role
|
||||||
|
// and direction; records bsdl_path). Fails (ok=false, error set, no mutation)
|
||||||
|
// when the file cannot be parsed.
|
||||||
|
AttachBsdlResult attach_bsdl(Part *part, const std::string &path);
|
||||||
|
|
||||||
|
// Outcome of overriding a signal's type from a user-supplied name.
|
||||||
|
struct SetSignalTypeResult {
|
||||||
|
bool ok = false;
|
||||||
|
std::string error; ///< set when the name isn't power/gnd/other
|
||||||
|
SignalType type = SignalType::Other; ///< the resolved type (for rendering)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set `sig`'s type from `type_name` (power | gnd | other, case-insensitive).
|
||||||
|
// Fails (ok=false, error set, no mutation) on an unrecognised name.
|
||||||
|
SetSignalTypeResult set_signal_type(Signal *sig, const std::string &type_name);
|
||||||
|
|
||||||
|
// Outcome of cloning a module under a new name.
|
||||||
|
struct DuplicateResult {
|
||||||
|
bool ok = false;
|
||||||
|
std::string error; ///< unknown source, or destination name already taken
|
||||||
|
int parts = 0;
|
||||||
|
int signals = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Deep-clone module `src_name` as `dst_name`: parts, pins (spec + nc_origin),
|
||||||
|
// signals (with type overrides) and the pin→signal wiring — but not the
|
||||||
|
// system's connections. Fails (ok=false, error set, no change) when the source
|
||||||
|
// is unknown or the destination name already exists.
|
||||||
|
DuplicateResult duplicate_module(System *sys, const std::string &src_name,
|
||||||
|
const std::string &dst_name);
|
||||||
|
|
||||||
|
} // namespace app
|
||||||
|
|
||||||
|
#endif // _APP_EDIT_HPP_
|
||||||
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_
|
||||||
47
src/core/app/load.cpp
Normal file
47
src/core/app/load.cpp
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#include "core/app/load.hpp"
|
||||||
|
|
||||||
|
#include "core/domain/analysis.hpp" // infer_signal_types, SignalTypeInferenceStats
|
||||||
|
#include "core/domain/modules.hpp"
|
||||||
|
#include "core/domain/signals.hpp" // drop_singleton_signals
|
||||||
|
#include "core/domain/system.hpp"
|
||||||
|
|
||||||
|
#include <cctype>
|
||||||
|
#include <exception>
|
||||||
|
|
||||||
|
namespace app {
|
||||||
|
|
||||||
|
bool import_type_from_name(const std::string &name, ImportType &out)
|
||||||
|
{
|
||||||
|
std::string ls;
|
||||||
|
ls.reserve(name.size());
|
||||||
|
for (char c : name) ls += (char)std::tolower((unsigned char)c);
|
||||||
|
if (ls == "mentor") { out = ImportType::IMPORT_MENTOR; return true; }
|
||||||
|
if (ls == "altium") { out = ImportType::IMPORT_ALTIUM; return true; }
|
||||||
|
if (ls == "ods") { out = ImportType::IMPORT_ODS; return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadResult load_module(System *sys, const std::string &module_name,
|
||||||
|
const std::string &path, ImportType type)
|
||||||
|
{
|
||||||
|
LoadResult r;
|
||||||
|
if (!sys) { r.error = "no system"; return r; }
|
||||||
|
try {
|
||||||
|
sys->Load(module_name, path, type);
|
||||||
|
Module *mod = sys->modules()->get(module_name);
|
||||||
|
r.dropped = drop_singleton_signals(mod->signals);
|
||||||
|
SignalTypeInferenceStats inf = infer_signal_types(sys);
|
||||||
|
r.parts = (int)mod->size();
|
||||||
|
r.signals = (int)mod->signals->size();
|
||||||
|
r.power = inf.power;
|
||||||
|
r.gnd = inf.gnd;
|
||||||
|
r.kept_other = inf.kept_other;
|
||||||
|
r.mgmt = inf.mgmt;
|
||||||
|
r.ok = true;
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
r.error = e.what();
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace app
|
||||||
37
src/core/app/load.hpp
Normal file
37
src/core/app/load.hpp
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#ifndef _APP_LOAD_HPP_
|
||||||
|
#define _APP_LOAD_HPP_
|
||||||
|
|
||||||
|
#include "core/domain/system.hpp" // ImportType
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// Application layer: UI-independent operations that any frontend (TUI, GUI, …)
|
||||||
|
// can call. No console, no dialogs, no FTXUI — just System in, result out.
|
||||||
|
namespace app {
|
||||||
|
|
||||||
|
// Map an import-type name (mentor / altium / ods, case-insensitive) to an
|
||||||
|
// ImportType. Returns false if the name is none of those.
|
||||||
|
bool import_type_from_name(const std::string &name, ImportType &out);
|
||||||
|
|
||||||
|
// Outcome of loading a module: the post-import counts the caller renders.
|
||||||
|
struct LoadResult {
|
||||||
|
bool ok = false;
|
||||||
|
std::string error; ///< human-readable, set when !ok
|
||||||
|
int parts = 0;
|
||||||
|
int signals = 0;
|
||||||
|
int dropped = 0; ///< singleton/NC signals removed after import
|
||||||
|
int power = 0; ///< signals inferred Power (name + structure)
|
||||||
|
int gnd = 0; ///< signals inferred GndShield (name)
|
||||||
|
int kept_other = 0; ///< name said Power but evidence too weak → kept Other
|
||||||
|
int mgmt = 0; ///< power-management signal (rail + SENSE/EN/PG/… token) → Other, not suspect
|
||||||
|
};
|
||||||
|
|
||||||
|
// Import a module from a netlist/pinout file into `sys`, drop singleton signals,
|
||||||
|
// then infer signal types. Returns the counts or an error. Pure core — safe to
|
||||||
|
// call from any frontend.
|
||||||
|
LoadResult load_module(System *sys, const std::string &module_name,
|
||||||
|
const std::string &path, ImportType type);
|
||||||
|
|
||||||
|
} // namespace app
|
||||||
|
|
||||||
|
#endif // _APP_LOAD_HPP_
|
||||||
332
src/core/app/script.cpp
Normal file
332
src/core/app/script.cpp
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
#include "core/app/script.hpp"
|
||||||
|
|
||||||
|
#include "core/app/connect.hpp"
|
||||||
|
#include "core/app/edit.hpp"
|
||||||
|
#include "core/app/export.hpp"
|
||||||
|
#include "core/app/load.hpp"
|
||||||
|
#include "core/app/verify.hpp"
|
||||||
|
|
||||||
|
#include "core/domain/analysis.hpp"
|
||||||
|
#include "core/domain/connect.hpp"
|
||||||
|
#include "core/domain/modules.hpp"
|
||||||
|
#include "core/domain/parts.hpp"
|
||||||
|
#include "core/domain/persist.hpp"
|
||||||
|
#include "core/domain/signal_type.hpp"
|
||||||
|
#include "core/domain/signals.hpp"
|
||||||
|
#include "core/domain/system.hpp"
|
||||||
|
|
||||||
|
#include <cctype>
|
||||||
|
#include <fstream>
|
||||||
|
#include <map>
|
||||||
|
#include <ostream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace app {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Whitespace split with "double quotes" grouping — same rules as the TUI shell.
|
||||||
|
std::vector<std::string> tokenize(const std::string &s) {
|
||||||
|
std::vector<std::string> out;
|
||||||
|
std::string cur;
|
||||||
|
bool in_q = false;
|
||||||
|
bool has = false;
|
||||||
|
for (char c : s) {
|
||||||
|
if (c == '"') { in_q = !in_q; has = true; continue; }
|
||||||
|
if (!in_q && std::isspace((unsigned char)c)) {
|
||||||
|
if (has) { out.push_back(std::move(cur)); cur.clear(); has = false; }
|
||||||
|
} else {
|
||||||
|
cur.push_back(c);
|
||||||
|
has = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (has) out.push_back(std::move(cur));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One script execution: holds the variable table, the System reference (so new/
|
||||||
|
// restore can replace it) and the output stream.
|
||||||
|
class Runner {
|
||||||
|
public:
|
||||||
|
Runner(std::unique_ptr<System> &sys, std::ostream &out) : sys_(sys), out_(out) {}
|
||||||
|
|
||||||
|
// Run a file; `opened` reports whether it could be opened. Returns the count
|
||||||
|
// of effective lines; accumulates command errors into `r.errors`.
|
||||||
|
int run_file(const std::string &path, int depth, ScriptResult &r, bool &opened) {
|
||||||
|
opened = false;
|
||||||
|
if (depth > 32) {
|
||||||
|
emit("source: nesting too deep, skipping " + path);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
std::ifstream f(path);
|
||||||
|
if (!f) return 0;
|
||||||
|
opened = true;
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(f, line)) {
|
||||||
|
std::size_t s = line.find_first_not_of(" \t");
|
||||||
|
if (s == std::string::npos) continue;
|
||||||
|
if (line[s] == '#') continue;
|
||||||
|
std::string t = line.substr(s);
|
||||||
|
while (!t.empty() && std::isspace((unsigned char)t.back())) t.pop_back();
|
||||||
|
if (t.empty()) continue;
|
||||||
|
++count;
|
||||||
|
if (!exec(t, depth, r)) ++r.errors;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void emit(const std::string &line) { out_ << line << '\n'; }
|
||||||
|
|
||||||
|
// $name / ${name} → variable value; unknown names kept literally.
|
||||||
|
std::string expand(const std::string &s) const {
|
||||||
|
std::string out;
|
||||||
|
std::size_t i = 0;
|
||||||
|
while (i < s.size()) {
|
||||||
|
if (s[i] != '$') { out.push_back(s[i++]); continue; }
|
||||||
|
std::size_t j = i + 1;
|
||||||
|
bool braces = (j < s.size() && s[j] == '{');
|
||||||
|
if (braces) ++j;
|
||||||
|
std::size_t start = j;
|
||||||
|
while (j < s.size() && (std::isalnum((unsigned char)s[j]) || s[j] == '_')) ++j;
|
||||||
|
std::string name = s.substr(start, j - start);
|
||||||
|
if (braces) {
|
||||||
|
if (j >= s.size() || s[j] != '}') { out.push_back('$'); ++i; continue; }
|
||||||
|
++j;
|
||||||
|
}
|
||||||
|
if (name.empty()) { out.push_back('$'); ++i; continue; }
|
||||||
|
auto it = vars_.find(name);
|
||||||
|
if (it != vars_.end()) out += it->second;
|
||||||
|
else out += s.substr(i, j - i);
|
||||||
|
i = j;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
Module *resolve_module(const std::string &name) {
|
||||||
|
try { return sys_->modules()->get(name); }
|
||||||
|
catch (const std::exception &) { emit("unknown module: " + name); return nullptr; }
|
||||||
|
}
|
||||||
|
Part *resolve_part(Module *m, const std::string &name) {
|
||||||
|
try { return m->get(name); }
|
||||||
|
catch (const std::exception &) {
|
||||||
|
emit("part in " + m->name + " not found: " + name);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void render_verify() {
|
||||||
|
VerifyReport r = verify(sys_.get());
|
||||||
|
for (const auto &m : r.role_mismatches)
|
||||||
|
emit(" " + m.module + "/" + m.part + "/" + m.pin + ": expected "
|
||||||
|
+ signal_type_name(m.expected) + ", got " + signal_type_name(m.actual)
|
||||||
|
+ " (signal: " + m.signal + ")");
|
||||||
|
emit("verify: " + std::to_string(r.role_mismatches.size())
|
||||||
|
+ " local mismatch(es) over " + std::to_string(r.typed_pins)
|
||||||
|
+ " typed pin(s).");
|
||||||
|
for (const auto &ni : r.net_inconsistencies) {
|
||||||
|
std::string line = " net mixes Power and GndShield:";
|
||||||
|
for (const auto &mem : ni.members)
|
||||||
|
line += " " + mem.module + "/" + mem.signal
|
||||||
|
+ "(" + signal_type_name(mem.type) + ")";
|
||||||
|
emit(line);
|
||||||
|
}
|
||||||
|
emit("verify: " + std::to_string(r.net_inconsistencies.size())
|
||||||
|
+ " inconsistent net(s) over " + std::to_string(r.bridged_nets)
|
||||||
|
+ " bridged net(s) (" + std::to_string(r.total_nets) + " total).");
|
||||||
|
emit("verify: " + std::to_string(r.orphan_total())
|
||||||
|
+ " orphan pin(s) at import (" + std::to_string(r.orphan_imported)
|
||||||
|
+ " imported NC, " + std::to_string(r.orphan_dropped)
|
||||||
|
+ " dropped singleton).");
|
||||||
|
auto grp = [&](const std::vector<Anomaly> &v, const char *tail) {
|
||||||
|
for (const auto &a : v)
|
||||||
|
emit(" [" + std::string(anomaly_kind_name(a.kind)) + "] " + a.message);
|
||||||
|
emit("verify: " + std::to_string(v.size()) + tail);
|
||||||
|
};
|
||||||
|
grp(r.pin_anomalies, " model-driven pin anomaly(ies).");
|
||||||
|
grp(r.jtag_anomalies, " JTAG chain anomaly(ies).");
|
||||||
|
grp(r.conflict_anomalies, " source-conflict(s).");
|
||||||
|
grp(r.completeness_anomalies, " BSDL completeness issue(s).");
|
||||||
|
grp(r.diff_anomalies, " diff-pair crossing anomaly(ies).");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute one already-trimmed line. Returns false on a hard error.
|
||||||
|
bool exec(const std::string &raw, int depth, ScriptResult &top) {
|
||||||
|
std::vector<std::string> tok = tokenize(raw);
|
||||||
|
if (tok.empty()) return true;
|
||||||
|
const std::string cmd = tok[0];
|
||||||
|
std::vector<std::string> a;
|
||||||
|
for (std::size_t i = 1; i < tok.size(); ++i) a.push_back(expand(tok[i]));
|
||||||
|
|
||||||
|
auto need = [&](std::size_t n) {
|
||||||
|
if (a.size() == n) return true;
|
||||||
|
emit(cmd + ": expected " + std::to_string(n) + " argument(s)");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (cmd == "set") {
|
||||||
|
if (a.size() != 2) { emit("set: usage: set <name> <value>"); return false; }
|
||||||
|
vars_[a[0]] = a[1];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (cmd == "new") {
|
||||||
|
sys_ = std::make_unique<System>();
|
||||||
|
emit("system created.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (cmd == "load") {
|
||||||
|
if (!need(3)) return false;
|
||||||
|
ImportType t;
|
||||||
|
if (!import_type_from_name(a[2], t)) { emit("unknown import type: " + a[2]); return false; }
|
||||||
|
LoadResult r = load_module(sys_.get(), a[0], a[1], t);
|
||||||
|
if (!r.ok) { emit("load failed: " + r.error); return false; }
|
||||||
|
emit("loaded '" + a[0] + "' from " + a[1]);
|
||||||
|
emit(" parts: " + std::to_string(r.parts));
|
||||||
|
emit(" signals: " + std::to_string(r.signals)
|
||||||
|
+ (r.dropped ? " (dropped " + std::to_string(r.dropped)
|
||||||
|
+ " singleton/NC signal(s))" : ""));
|
||||||
|
emit(" types: " + std::to_string(r.power) + " power, "
|
||||||
|
+ std::to_string(r.gnd) + " gnd, " + std::to_string(r.kept_other)
|
||||||
|
+ " suspect Power (name only — kept as Other), "
|
||||||
|
+ std::to_string(r.mgmt)
|
||||||
|
+ " power-management (control/measure — kept as Other)");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (cmd == "connect" || cmd == "plug") {
|
||||||
|
if (!need(4)) return false;
|
||||||
|
Module *m1 = resolve_module(a[0]); if (!m1) return false;
|
||||||
|
Part *p1 = resolve_part(m1, a[1]); if (!p1) return false;
|
||||||
|
Module *m2 = resolve_module(a[2]); if (!m2) return false;
|
||||||
|
Part *p2 = resolve_part(m2, a[3]); if (!p2) return false;
|
||||||
|
ConnectResult r = connect_parts(sys_.get(), m1, p1, m2, p2);
|
||||||
|
if (r.refused) { emit("connect refused: " + r.error); return false; }
|
||||||
|
if (!r.identity_info.empty()) {
|
||||||
|
emit("connect: " + r.identity_info);
|
||||||
|
if (r.nc_added > 0)
|
||||||
|
emit("connect: added " + std::to_string(r.nc_added)
|
||||||
|
+ " NC pin(s) so both sides match");
|
||||||
|
}
|
||||||
|
if (!r.ok) { emit("connect failed: " + r.error); return false; }
|
||||||
|
emit("connected: " + r.connection_name + " via " + r.transform_name
|
||||||
|
+ " (" + std::to_string(r.wires) + " wires)");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (cmd == "set-connector-type") {
|
||||||
|
if (!need(3)) return false;
|
||||||
|
Module *m = resolve_module(a[0]); if (!m) return false;
|
||||||
|
Part *p = resolve_part(m, a[1]); if (!p) return false;
|
||||||
|
SetConnectorTypeResult r = set_connector_type(p, a[2]);
|
||||||
|
if (!r.ok) { emit("set-connector-type refused: " + r.error); return false; }
|
||||||
|
emit(m->name + "/" + p->name + ": connector_type = "
|
||||||
|
+ (a[2].empty() ? "(none)" : a[2]));
|
||||||
|
if (r.materialised > 0)
|
||||||
|
emit("set-connector-type: added " + std::to_string(r.materialised)
|
||||||
|
+ " NC pin(s) from the connector layout");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (cmd == "set-signal-type") {
|
||||||
|
if (!need(3)) return false;
|
||||||
|
Module *m = resolve_module(a[0]); if (!m) return false;
|
||||||
|
Signal *sig;
|
||||||
|
try { sig = m->signals->get(a[1]); }
|
||||||
|
catch (const std::exception &) {
|
||||||
|
emit("unknown signal: " + m->name + "/" + a[1]); return false;
|
||||||
|
}
|
||||||
|
SetSignalTypeResult r = set_signal_type(sig, a[2]);
|
||||||
|
if (!r.ok) { emit(r.error); return false; }
|
||||||
|
emit(m->name + "/" + sig->name + ": signal type = " + signal_type_name(r.type));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (cmd == "attach-bsdl") {
|
||||||
|
if (!need(3)) return false;
|
||||||
|
Module *m = resolve_module(a[0]); if (!m) return false;
|
||||||
|
Part *p = resolve_part(m, a[1]); if (!p) return false;
|
||||||
|
AttachBsdlResult r = attach_bsdl(p, a[2]);
|
||||||
|
if (!r.ok) { emit("attach-bsdl: " + r.error); return false; }
|
||||||
|
emit(m->name + "/" + p->name + ": attached BSDL '" + r.entity + "' — "
|
||||||
|
+ std::to_string(r.bound) + "/" + std::to_string(r.ports_total)
|
||||||
|
+ " ports bound" + (r.unbound ? (", " + std::to_string(r.unbound) + " unbound") : ""));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (cmd == "duplicate") {
|
||||||
|
if (!need(2)) return false;
|
||||||
|
DuplicateResult r = duplicate_module(sys_.get(), a[0], a[1]);
|
||||||
|
if (!r.ok) { emit(r.error); return false; }
|
||||||
|
emit("duplicate: '" + a[0] + "' → '" + a[1] + "' ("
|
||||||
|
+ std::to_string(r.parts) + " part(s), "
|
||||||
|
+ std::to_string(r.signals) + " signal(s))");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (cmd == "verify") {
|
||||||
|
render_verify();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (cmd == "export") {
|
||||||
|
if (!need(1)) return false;
|
||||||
|
ExportFormat fmt;
|
||||||
|
if (!export_format_from_path(a[0], fmt)) {
|
||||||
|
emit("export: unknown extension (use .csv or .ods): " + a[0]); return false;
|
||||||
|
}
|
||||||
|
ExportResult r = export_connections(sys_.get(), a[0], fmt);
|
||||||
|
if (!r.ok) { emit("export failed: " + r.error); return false; }
|
||||||
|
emit("exported " + std::to_string(r.rows) + " row(s) to " + a[0]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (cmd == "save") {
|
||||||
|
if (!need(1)) return false;
|
||||||
|
std::string err;
|
||||||
|
if (!save_system(sys_.get(), a[0], err)) { emit("save failed: " + err); return false; }
|
||||||
|
emit("saved to " + a[0]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (cmd == "restore") {
|
||||||
|
if (!need(1)) return false;
|
||||||
|
std::string err;
|
||||||
|
System *fresh = restore_system(a[0], err);
|
||||||
|
if (!fresh) { emit("restore failed: " + err); return false; }
|
||||||
|
sys_.reset(fresh);
|
||||||
|
emit("restored from " + a[0] + " ("
|
||||||
|
+ std::to_string(sys_->modules()->size()) + " module(s), "
|
||||||
|
+ std::to_string(sys_->connections()->size()) + " connection(s))");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (cmd == "source") {
|
||||||
|
if (!need(1)) return false;
|
||||||
|
bool opened;
|
||||||
|
int n = run_file(a[0], depth + 1, top, opened);
|
||||||
|
if (!opened) { emit("source: cannot open " + a[0]); return false; }
|
||||||
|
emit("source: " + a[0] + " (" + std::to_string(n) + " line(s))");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit("script: unsupported command '" + cmd + "'");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<System> &sys_;
|
||||||
|
std::ostream &out_;
|
||||||
|
std::map<std::string, std::string> vars_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ScriptResult run_script(std::unique_ptr<System> &sys, const std::string &path,
|
||||||
|
std::ostream &out)
|
||||||
|
{
|
||||||
|
ScriptResult r;
|
||||||
|
Runner runner(sys, out);
|
||||||
|
bool opened = false;
|
||||||
|
int n = runner.run_file(path, 0, r, opened);
|
||||||
|
if (!opened) {
|
||||||
|
r.error = "cannot open " + path;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
r.ok = true;
|
||||||
|
r.lines = n;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace app
|
||||||
35
src/core/app/script.hpp
Normal file
35
src/core/app/script.hpp
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#ifndef _APP_SCRIPT_HPP_
|
||||||
|
#define _APP_SCRIPT_HPP_
|
||||||
|
|
||||||
|
#include <iosfwd>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class System;
|
||||||
|
|
||||||
|
// Application layer: a frontend-agnostic runner for essim command scripts.
|
||||||
|
// Dispatches the scriptable, system-building commands to the app::* operations
|
||||||
|
// and writes their output to a stream — no console, no dialogs, no FTXUI. Any
|
||||||
|
// frontend (and batch mode) can drive a script through it.
|
||||||
|
namespace app {
|
||||||
|
|
||||||
|
// Outcome of running a script file.
|
||||||
|
struct ScriptResult {
|
||||||
|
bool ok = false; ///< the top-level file opened and ran
|
||||||
|
std::string error; ///< set when the top-level file can't be opened
|
||||||
|
int lines = 0; ///< effective (non-comment, non-blank) lines run
|
||||||
|
int errors = 0; ///< commands that failed / were unsupported
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run the script at `path` against `sys`, writing per-command output to `out`.
|
||||||
|
// `sys` is taken by reference because `new` / `restore` replace the System.
|
||||||
|
// Supported: # comments, blank lines, set + $var/${var} expansion, new, load,
|
||||||
|
// connect, set-connector-type, set-signal-type, attach-bsdl, verify, export,
|
||||||
|
// save, restore, source (nested). Unsupported commands are reported and counted
|
||||||
|
// in `errors`, and execution continues.
|
||||||
|
ScriptResult run_script(std::unique_ptr<System> &sys, const std::string &path,
|
||||||
|
std::ostream &out);
|
||||||
|
|
||||||
|
} // namespace app
|
||||||
|
|
||||||
|
#endif // _APP_SCRIPT_HPP_
|
||||||
107
src/core/app/verify.cpp
Normal file
107
src/core/app/verify.cpp
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
#include "core/app/verify.hpp"
|
||||||
|
|
||||||
|
#include "core/domain/bsdl_check.hpp"
|
||||||
|
#include "core/domain/connect.hpp"
|
||||||
|
#include "core/domain/diff_check.hpp"
|
||||||
|
#include "core/domain/modules.hpp"
|
||||||
|
#include "core/domain/nets.hpp"
|
||||||
|
#include "core/domain/parts.hpp"
|
||||||
|
#include "core/domain/pins.hpp"
|
||||||
|
#include "core/domain/signals.hpp"
|
||||||
|
#include "core/domain/system.hpp"
|
||||||
|
|
||||||
|
#include <unordered_set>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace app {
|
||||||
|
|
||||||
|
VerifyReport verify(System *sys)
|
||||||
|
{
|
||||||
|
VerifyReport r;
|
||||||
|
if (!sys)
|
||||||
|
return r;
|
||||||
|
|
||||||
|
// Pass 1 — typed pins: expected (model) vs actual (net) signal type.
|
||||||
|
for (auto &mkv : *sys->modules()) {
|
||||||
|
Module *mod = mkv.second;
|
||||||
|
for (auto &pkv : *mod) {
|
||||||
|
Part *prt = pkv.second;
|
||||||
|
if (prt->connector_type.empty())
|
||||||
|
continue;
|
||||||
|
for (auto &nkv : *prt) {
|
||||||
|
Pin *pin = nkv.second;
|
||||||
|
++r.typed_pins;
|
||||||
|
SignalType expected = pin->expected_signal_type();
|
||||||
|
if (expected == SignalType::Other)
|
||||||
|
continue;
|
||||||
|
Signal *s = pin->signal();
|
||||||
|
SignalType actual = s ? s->type : SignalType::Other;
|
||||||
|
if (actual == expected)
|
||||||
|
continue;
|
||||||
|
RoleMismatch m;
|
||||||
|
m.module = mod->name;
|
||||||
|
m.part = prt->name;
|
||||||
|
m.pin = pin->name;
|
||||||
|
m.signal = s ? s->name : std::string("(NC)");
|
||||||
|
m.expected = expected;
|
||||||
|
m.actual = actual;
|
||||||
|
r.role_mismatches.push_back(std::move(m));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass 2 — bridged nets: flag Power/GndShield mixing. Compute the nets once
|
||||||
|
// here and reuse them for the model checks below.
|
||||||
|
std::vector<Net> nets = compute_all_nets(sys);
|
||||||
|
r.total_nets = (int)nets.size();
|
||||||
|
for (const Net &n : nets) {
|
||||||
|
if (n.members.size() < 2)
|
||||||
|
continue;
|
||||||
|
++r.bridged_nets;
|
||||||
|
SignalType dom;
|
||||||
|
if (net_type_consistent(n, dom))
|
||||||
|
continue;
|
||||||
|
NetInconsistency ni;
|
||||||
|
for (const auto &mp : n.members)
|
||||||
|
ni.members.push_back({mp.first->name, mp.second->name, mp.second->type});
|
||||||
|
r.net_inconsistencies.push_back(std::move(ni));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass 3 — orphans: pins with no signal and not bridged via a connection.
|
||||||
|
std::unordered_set<Pin *> bridged_pins;
|
||||||
|
for (auto &ckv : *sys->connections())
|
||||||
|
for (auto &wp : ckv.second->pin_map) {
|
||||||
|
if (wp.first) bridged_pins.insert(wp.first);
|
||||||
|
if (wp.second) bridged_pins.insert(wp.second);
|
||||||
|
}
|
||||||
|
for (auto &mkv : *sys->modules())
|
||||||
|
for (auto &pkv : *mkv.second)
|
||||||
|
for (auto &nkv : *pkv.second) {
|
||||||
|
Pin *pin = nkv.second;
|
||||||
|
if (pin->signal() || bridged_pins.count(pin))
|
||||||
|
continue;
|
||||||
|
bool dropped;
|
||||||
|
if (pin->nc_origin == NcOrigin::ImportedUnconnected) {
|
||||||
|
++r.orphan_imported;
|
||||||
|
dropped = false;
|
||||||
|
} else if (pin->nc_origin == NcOrigin::DroppedSingleton) {
|
||||||
|
++r.orphan_dropped;
|
||||||
|
dropped = true;
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
r.orphans.push_back({mkv.first, pkv.first, nkv.first, dropped});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Passes 4-8 — model-driven checks (reuse the nets from pass 2).
|
||||||
|
r.pin_anomalies = check_pin_specs(sys, &nets);
|
||||||
|
r.jtag_anomalies = check_jtag_chain(sys, &nets);
|
||||||
|
r.conflict_anomalies = check_source_conflicts(sys);
|
||||||
|
r.completeness_anomalies = check_bsdl_completeness(sys);
|
||||||
|
r.diff_anomalies = check_diff_crossings(sys, &nets);
|
||||||
|
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace app
|
||||||
71
src/core/app/verify.hpp
Normal file
71
src/core/app/verify.hpp
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#ifndef _APP_VERIFY_HPP_
|
||||||
|
#define _APP_VERIFY_HPP_
|
||||||
|
|
||||||
|
#include "core/domain/analysis.hpp" // Anomaly
|
||||||
|
#include "core/domain/signal_type.hpp" // SignalType
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class System;
|
||||||
|
|
||||||
|
namespace app {
|
||||||
|
|
||||||
|
// One typed-pin role mismatch: the connector/BSDL expectation disagrees with
|
||||||
|
// the actual net type.
|
||||||
|
struct RoleMismatch {
|
||||||
|
std::string module, part, pin;
|
||||||
|
std::string signal; ///< signal name, or "(NC)"
|
||||||
|
SignalType expected = SignalType::Other;
|
||||||
|
SignalType actual = SignalType::Other;
|
||||||
|
};
|
||||||
|
|
||||||
|
// One bridged net mixing Power and GndShield, with its members for display.
|
||||||
|
struct NetInconsistency {
|
||||||
|
struct Member { std::string module, signal; SignalType type; };
|
||||||
|
std::vector<Member> members;
|
||||||
|
};
|
||||||
|
|
||||||
|
// One orphan pin: no signal and not bridged via a connection. `dropped` is true
|
||||||
|
// for a dropped singleton (essim detached it), false for an import-time NC.
|
||||||
|
struct OrphanPin {
|
||||||
|
std::string module, part, pin;
|
||||||
|
bool dropped = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The full result of `verify`: structured data only — no strings beyond the
|
||||||
|
// names, no formatting. Frontends (the verify command, the analyze screen, the
|
||||||
|
// dashboard) render it however they like.
|
||||||
|
struct VerifyReport {
|
||||||
|
int typed_pins = 0; ///< pins with a non-Other expectation considered
|
||||||
|
std::vector<RoleMismatch> role_mismatches;
|
||||||
|
|
||||||
|
int total_nets = 0;
|
||||||
|
int bridged_nets = 0;
|
||||||
|
std::vector<NetInconsistency> net_inconsistencies;
|
||||||
|
|
||||||
|
int orphan_imported = 0;
|
||||||
|
int orphan_dropped = 0;
|
||||||
|
std::vector<OrphanPin> orphans; ///< per-pin detail (both origins)
|
||||||
|
|
||||||
|
std::vector<Anomaly> pin_anomalies; ///< check_pin_specs
|
||||||
|
std::vector<Anomaly> jtag_anomalies; ///< check_jtag_chain
|
||||||
|
std::vector<Anomaly> conflict_anomalies; ///< check_source_conflicts
|
||||||
|
std::vector<Anomaly> completeness_anomalies; ///< check_bsdl_completeness
|
||||||
|
std::vector<Anomaly> diff_anomalies; ///< check_diff_crossings
|
||||||
|
|
||||||
|
int orphan_total() const { return orphan_imported + orphan_dropped; }
|
||||||
|
int model_total() const {
|
||||||
|
return (int)(pin_anomalies.size() + jtag_anomalies.size()
|
||||||
|
+ conflict_anomalies.size() + completeness_anomalies.size()
|
||||||
|
+ diff_anomalies.size());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run every verify pass over the system and return the findings. Pure core —
|
||||||
|
// computes the nets once and feeds them to the net-based checks.
|
||||||
|
VerifyReport verify(System *sys);
|
||||||
|
|
||||||
|
} // namespace app
|
||||||
|
|
||||||
|
#endif // _APP_VERIFY_HPP_
|
||||||
@@ -31,15 +31,15 @@ const char *anomaly_kind_name(AnomalyKind k) {
|
|||||||
case AnomalyKind::JtagBusUnbridged: return "jtag-bus-unbridged";
|
case AnomalyKind::JtagBusUnbridged: return "jtag-bus-unbridged";
|
||||||
case AnomalyKind::SourceConflict: return "source-conflict";
|
case AnomalyKind::SourceConflict: return "source-conflict";
|
||||||
case AnomalyKind::BsdlPinMissing: return "bsdl-pin-missing";
|
case AnomalyKind::BsdlPinMissing: return "bsdl-pin-missing";
|
||||||
|
case AnomalyKind::DiffPolaritySwap: return "diff-polarity-swap";
|
||||||
|
case AnomalyKind::DiffCrossIncomplete: return "diff-cross-incomplete";
|
||||||
}
|
}
|
||||||
return "?";
|
return "?";
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
// Diff-pair suffix detection. Returns true and fills <stem, polarity> if
|
// Diff-pair suffix detection. Returns true and fills <stem, polarity> if
|
||||||
// `name` ends with one of {_P, _N, _p, _n} preceded by a non-suffix char.
|
// `name` ends with one of {_P, _N, _p, _n} preceded by a non-suffix char.
|
||||||
// 'P' / 'N' result is normalised to uppercase.
|
// 'P' / 'N' result is normalised to uppercase. Shared with diff_check.cpp.
|
||||||
bool diff_suffix(const std::string &name, std::string &stem, char &pol) {
|
bool diff_suffix(const std::string &name, std::string &stem, char &pol) {
|
||||||
if (name.size() < 3) return false;
|
if (name.size() < 3) return false;
|
||||||
char last = name.back();
|
char last = name.back();
|
||||||
@@ -52,6 +52,29 @@ bool diff_suffix(const std::string &name, std::string &stem, char &pol) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tool-internal net names we never want to surface to the user (Mentor's
|
||||||
|
// `$Nxxxx` convention, ODS placeholders, etc.). Cheap prefix check.
|
||||||
|
bool is_internal_name(const std::string &n) {
|
||||||
|
return !n.empty() && n[0] == '$';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trailing-integer split: "MDI0" → ("MDI", 0); "PCIE_TX_3" → ("PCIE_TX_", 3);
|
||||||
|
// "USB" → false (no trailing digits). Used for diff-bus aggregation only —
|
||||||
|
// the strict `_` rule from `numeric_suffix` does NOT apply here because the
|
||||||
|
// caller has already stripped a `_P` / `_N` polarity suffix, so we know the
|
||||||
|
// remaining digits are an index rather than part of a longer name.
|
||||||
|
bool split_trailing_index(const std::string &s, std::string &outer, int &idx) {
|
||||||
|
if (s.empty()) return false;
|
||||||
|
size_t i = s.size();
|
||||||
|
while (i > 0 && std::isdigit((unsigned char)s[i - 1])) --i;
|
||||||
|
if (i == s.size() || i == 0) return false;
|
||||||
|
idx = std::atoi(s.c_str() + i);
|
||||||
|
outer = s.substr(0, i);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
// Bus suffix detection. Two accepted forms:
|
// Bus suffix detection. Two accepted forms:
|
||||||
// - bracketed: NAME[12] → stem "NAME", idx 12
|
// - bracketed: NAME[12] → stem "NAME", idx 12
|
||||||
// - underscore: NAME_12 → stem "NAME_", idx 12 (underscore is REQUIRED
|
// - underscore: NAME_12 → stem "NAME_", idx 12 (underscore is REQUIRED
|
||||||
@@ -81,27 +104,6 @@ bool numeric_suffix(const std::string &name, std::string &stem, int &idx,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tool-internal net names we never want to surface to the user (Mentor's
|
|
||||||
// `$Nxxxx` convention, ODS placeholders, etc.). Cheap prefix check.
|
|
||||||
bool is_internal_name(const std::string &n) {
|
|
||||||
return !n.empty() && n[0] == '$';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trailing-integer split: "MDI0" → ("MDI", 0); "PCIE_TX_3" → ("PCIE_TX_", 3);
|
|
||||||
// "USB" → false (no trailing digits). Used for diff-bus aggregation only —
|
|
||||||
// the strict `_` rule from `numeric_suffix` does NOT apply here because the
|
|
||||||
// caller has already stripped a `_P` / `_N` polarity suffix, so we know the
|
|
||||||
// remaining digits are an index rather than part of a longer name.
|
|
||||||
bool split_trailing_index(const std::string &s, std::string &outer, int &idx) {
|
|
||||||
if (s.empty()) return false;
|
|
||||||
size_t i = s.size();
|
|
||||||
while (i > 0 && std::isdigit((unsigned char)s[i - 1])) --i;
|
|
||||||
if (i == s.size() || i == 0) return false;
|
|
||||||
idx = std::atoi(s.c_str() + i);
|
|
||||||
outer = s.substr(0, i);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void analyse_module(Module *mod, AnalysisReport &out) {
|
void analyse_module(Module *mod, AnalysisReport &out) {
|
||||||
// ---- Pass 1: diff pairs ----
|
// ---- Pass 1: diff pairs ----
|
||||||
std::unordered_map<std::string, std::pair<Signal *, Signal *>> dp; // stem -> {P, N}
|
std::unordered_map<std::string, std::pair<Signal *, Signal *>> dp; // stem -> {P, N}
|
||||||
@@ -276,13 +278,21 @@ SignalTypeInferenceStats infer_signal_types(System *sys) {
|
|||||||
Module *mod = mkv.second;
|
Module *mod = mkv.second;
|
||||||
for (auto &skv : *mod->signals) {
|
for (auto &skv : *mod->signals) {
|
||||||
Signal *s = skv.second;
|
Signal *s = skv.second;
|
||||||
SignalType named = infer_signal_type(s->name);
|
NameClassification ncl = classify_signal_name(s->name);
|
||||||
if (named == SignalType::GndShield) {
|
if (ncl.verdict == NameVerdict::GndShield) {
|
||||||
s->type = SignalType::GndShield;
|
s->type = SignalType::GndShield;
|
||||||
++st.gnd;
|
++st.gnd;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (named == SignalType::Power) {
|
if (ncl.verdict == NameVerdict::PowerMgmt) {
|
||||||
|
// A rail token next to a control token (SENSE, EN, PG, …):
|
||||||
|
// a signal about a rail, confidently NOT the rail — never
|
||||||
|
// suspect, whatever the fan-out.
|
||||||
|
s->type = SignalType::Other;
|
||||||
|
++st.mgmt;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ncl.verdict == NameVerdict::Rail) {
|
||||||
int fanout = (int)s->size();
|
int fanout = (int)s->size();
|
||||||
// Hard rule: a "power" net that touches fewer than three
|
// Hard rule: a "power" net that touches fewer than three
|
||||||
// pins cannot physically be a rail (a real rail goes to
|
// pins cannot physically be a rail (a real rail goes to
|
||||||
@@ -39,6 +39,8 @@ enum class AnomalyKind {
|
|||||||
JtagBusUnbridged, ///< TMS or TCK is not common to all TAP devices.
|
JtagBusUnbridged, ///< TMS or TCK is not common to all TAP devices.
|
||||||
SourceConflict, ///< A model contradicts the netlist (e.g. BSDL power pin left NC).
|
SourceConflict, ///< A model contradicts the netlist (e.g. BSDL power pin left NC).
|
||||||
BsdlPinMissing, ///< A BSDL power/ground port has no pin on the netlist part.
|
BsdlPinMissing, ///< A BSDL power/ground port has no pin on the netlist part.
|
||||||
|
DiffPolaritySwap, ///< A diff pair crosses a connection with P and N swapped.
|
||||||
|
DiffCrossIncomplete, ///< A diff pair/bus only partially crosses a connection.
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Anomaly {
|
struct Anomaly {
|
||||||
@@ -62,6 +64,8 @@ struct SignalTypeInferenceStats {
|
|||||||
int power = 0; ///< Signals promoted to Power (name + structural).
|
int power = 0; ///< Signals promoted to Power (name + structural).
|
||||||
int gnd = 0; ///< Signals promoted to GndShield (name only).
|
int gnd = 0; ///< Signals promoted to GndShield (name only).
|
||||||
int kept_other = 0; ///< Name said Power but structural evidence too weak.
|
int kept_other = 0; ///< Name said Power but structural evidence too weak.
|
||||||
|
int mgmt = 0; ///< Power-management signal (rail + SENSE/EN/PG/…
|
||||||
|
///< token) → confidently Other, never suspect.
|
||||||
};
|
};
|
||||||
|
|
||||||
// Thresholds used by `infer_signal_types` (re-exposed so the analyze screen
|
// Thresholds used by `infer_signal_types` (re-exposed so the analyze screen
|
||||||
@@ -71,6 +75,15 @@ inline constexpr int POWER_FANOUT_CONFIRM_MIN = 4; ///< ≥ this confirms Powe
|
|||||||
|
|
||||||
bool has_voltage_pattern(const std::string &name);
|
bool has_voltage_pattern(const std::string &name);
|
||||||
|
|
||||||
|
// Name-parsing helpers shared with the diff-crossing check (diff_check.cpp).
|
||||||
|
// diff_suffix: true if `name` ends with _P/_N (case-insensitive); fills the
|
||||||
|
// stem and the polarity normalised to uppercase 'P'/'N'.
|
||||||
|
// split_trailing_index: "MDI0" → ("MDI", 0); false without trailing digits.
|
||||||
|
// is_internal_name: tool-internal net names never surfaced ($Nxxxx …).
|
||||||
|
bool diff_suffix(const std::string &name, std::string &stem, char &pol);
|
||||||
|
bool split_trailing_index(const std::string &s, std::string &outer, int &idx);
|
||||||
|
bool is_internal_name(const std::string &n);
|
||||||
|
|
||||||
// Best-effort signal-type inference. Sets `Signal::type`:
|
// Best-effort signal-type inference. Sets `Signal::type`:
|
||||||
// - GndShield when the name unambiguously matches GND/SHIELD/CHASSIS/EARTH.
|
// - GndShield when the name unambiguously matches GND/SHIELD/CHASSIS/EARTH.
|
||||||
// - Power when the name suggests Power AND there is structural evidence
|
// - Power when the name suggests Power AND there is structural evidence
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#include "system/component_kind.hpp"
|
#include "core/domain/component_kind.hpp"
|
||||||
|
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <string>
|
#include <string>
|
||||||
194
src/core/domain/diff_check.cpp
Normal file
194
src/core/domain/diff_check.cpp
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
#include "diff_check.hpp"
|
||||||
|
|
||||||
|
#include "modules.hpp"
|
||||||
|
#include "signals.hpp"
|
||||||
|
#include "system.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <map>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// One complete local diff pair with the net ids of its two legs.
|
||||||
|
struct LocalPair {
|
||||||
|
Module *mod = nullptr;
|
||||||
|
std::string stem;
|
||||||
|
Signal *p = nullptr, *n = nullptr;
|
||||||
|
int np = -1, nn = -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string pair_label(const LocalPair &lp) {
|
||||||
|
return lp.mod->name + "/" + lp.stem + "_P/N";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::vector<Anomaly> check_diff_crossings(System *sys,
|
||||||
|
const std::vector<Net> *nets)
|
||||||
|
{
|
||||||
|
std::vector<Anomaly> out;
|
||||||
|
if (!sys || !nets) return out;
|
||||||
|
|
||||||
|
// Signal → net id (compute_all_nets covers every signal, singletons too).
|
||||||
|
std::unordered_map<Signal *, int> net_of;
|
||||||
|
for (size_t i = 0; i < nets->size(); ++i)
|
||||||
|
for (const auto &mp : (*nets)[i].members)
|
||||||
|
net_of[mp.second] = (int)i;
|
||||||
|
|
||||||
|
// Complete local pairs, module by module. Orphan halves (X_P without
|
||||||
|
// X_N) are analysis's DiffPairOrphan business — skipped here.
|
||||||
|
std::vector<LocalPair> pairs;
|
||||||
|
std::unordered_map<Signal *, int> pair_of; // leg signal → index in `pairs`
|
||||||
|
for (auto &mkv : *sys->modules()) {
|
||||||
|
Module *mod = mkv.second;
|
||||||
|
std::map<std::string, LocalPair> by_stem;
|
||||||
|
for (auto &skv : *mod->signals) {
|
||||||
|
if (is_internal_name(skv.first)) continue;
|
||||||
|
std::string stem; char pol;
|
||||||
|
if (!diff_suffix(skv.first, stem, pol)) continue;
|
||||||
|
LocalPair &lp = by_stem[stem];
|
||||||
|
lp.mod = mod; lp.stem = stem;
|
||||||
|
if (pol == 'P') lp.p = skv.second;
|
||||||
|
else lp.n = skv.second;
|
||||||
|
}
|
||||||
|
for (auto &kv : by_stem) {
|
||||||
|
LocalPair lp = kv.second;
|
||||||
|
if (!lp.p || !lp.n) continue;
|
||||||
|
auto ip = net_of.find(lp.p), in = net_of.find(lp.n);
|
||||||
|
if (ip == net_of.end() || in == net_of.end()) continue;
|
||||||
|
lp.np = ip->second;
|
||||||
|
lp.nn = in->second;
|
||||||
|
int idx = (int)pairs.size();
|
||||||
|
pairs.push_back(lp);
|
||||||
|
pair_of[lp.p] = idx;
|
||||||
|
pair_of[lp.n] = idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass 1 — pair against pair. Each unordered couple of pairs is judged
|
||||||
|
// once (dedup set), so A↔B is never also reported as B↔A.
|
||||||
|
std::set<std::pair<int, int>> seen;
|
||||||
|
for (int i = 0; i < (int)pairs.size(); ++i) {
|
||||||
|
const LocalPair &a = pairs[i];
|
||||||
|
if (a.np == a.nn) {
|
||||||
|
// Degenerate: both legs land on one net — only the connections
|
||||||
|
// can do that (two module-local signals are distinct by nature).
|
||||||
|
Anomaly an;
|
||||||
|
an.kind = AnomalyKind::DiffPolaritySwap;
|
||||||
|
an.module = a.mod;
|
||||||
|
an.message = a.mod->name + ": " + a.stem + "_P and " + a.stem
|
||||||
|
+ "_N join the same net (through the connections)";
|
||||||
|
an.involved = {a.p, a.n};
|
||||||
|
out.push_back(std::move(an));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Candidate peers: pairs of OTHER modules with a leg on net np or nn.
|
||||||
|
std::set<int> cands;
|
||||||
|
for (int net : {a.np, a.nn})
|
||||||
|
for (const auto &mp : (*nets)[net].members) {
|
||||||
|
auto it = pair_of.find(mp.second);
|
||||||
|
if (it == pair_of.end()) continue;
|
||||||
|
const LocalPair &b = pairs[it->second];
|
||||||
|
if (b.mod == a.mod) continue; // intra-module: nothing to say
|
||||||
|
if (b.np == b.nn) continue; // degenerate: own anomaly above
|
||||||
|
cands.insert(it->second);
|
||||||
|
}
|
||||||
|
for (int j : cands) {
|
||||||
|
std::pair<int, int> key = std::minmax(i, j);
|
||||||
|
if (!seen.insert(key).second) continue;
|
||||||
|
const LocalPair &b = pairs[j];
|
||||||
|
if (a.np == b.np && a.nn == b.nn) continue; // straight: all good
|
||||||
|
Anomaly an;
|
||||||
|
an.module = a.mod;
|
||||||
|
an.involved = {a.p, a.n, b.p, b.n};
|
||||||
|
if (a.np == b.nn && a.nn == b.np) {
|
||||||
|
an.kind = AnomalyKind::DiffPolaritySwap;
|
||||||
|
an.message = pair_label(a) + " <-> " + pair_label(b)
|
||||||
|
+ ": polarity swapped (P legs meet N legs)";
|
||||||
|
} else {
|
||||||
|
an.kind = AnomalyKind::DiffCrossIncomplete;
|
||||||
|
std::string how;
|
||||||
|
if (a.np == b.np) how = "only the P legs are bridged";
|
||||||
|
else if (a.nn == b.nn) how = "only the N legs are bridged";
|
||||||
|
else if (a.np == b.nn) how = "P leg bridged to N leg; "
|
||||||
|
"the other legs are not";
|
||||||
|
else how = "N leg bridged to P leg; "
|
||||||
|
"the other legs are not";
|
||||||
|
an.message = pair_label(a) + " <-> " + pair_label(b)
|
||||||
|
+ ": " + how;
|
||||||
|
}
|
||||||
|
out.push_back(std::move(an));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass 2 — diff buses at a crossing. Lanes grouped by outer stem. A lane
|
||||||
|
// is "dangling" when it crosses to NO module at all while sibling lanes
|
||||||
|
// do cross — distributed buses (lanes fanned out to different peers, a
|
||||||
|
// backplane classic) are legitimate and stay silent. Lanes crossing
|
||||||
|
// partially are already reported above, so they don't count as dangling.
|
||||||
|
// One aggregated anomaly per bus, per side (each side names its lanes).
|
||||||
|
std::map<std::pair<Module *, std::string>, std::map<int, int>> groups;
|
||||||
|
for (int i = 0; i < (int)pairs.size(); ++i) {
|
||||||
|
std::string outer; int idx;
|
||||||
|
if (!split_trailing_index(pairs[i].stem, outer, idx)) continue;
|
||||||
|
groups[{pairs[i].mod, outer}][idx] = i;
|
||||||
|
}
|
||||||
|
for (auto &gkv : groups) {
|
||||||
|
auto &lanes = gkv.second; // lane index → pair index
|
||||||
|
if (lanes.size() < 2) continue;
|
||||||
|
std::set<int> touching_any; // lanes sharing ≥1 net with a peer
|
||||||
|
std::set<int> complete_any; // lanes fully crossing somewhere
|
||||||
|
std::set<Module *> reached;
|
||||||
|
for (auto &lkv : lanes) {
|
||||||
|
const LocalPair &a = pairs[lkv.second];
|
||||||
|
if (a.np == a.nn) continue;
|
||||||
|
for (int net : {a.np, a.nn})
|
||||||
|
for (const auto &mp : (*nets)[net].members) {
|
||||||
|
auto it = pair_of.find(mp.second);
|
||||||
|
if (it == pair_of.end()) continue;
|
||||||
|
const LocalPair &b = pairs[it->second];
|
||||||
|
if (b.mod == a.mod) continue;
|
||||||
|
touching_any.insert(lkv.first);
|
||||||
|
bool straight = (a.np == b.np && a.nn == b.nn);
|
||||||
|
bool swapped = (a.np == b.nn && a.nn == b.np);
|
||||||
|
if (straight || swapped) {
|
||||||
|
complete_any.insert(lkv.first);
|
||||||
|
reached.insert(b.mod);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (complete_any.empty()) continue; // fully local bus: fine
|
||||||
|
std::vector<int> dangling;
|
||||||
|
for (auto &lkv : lanes)
|
||||||
|
if (!touching_any.count(lkv.first))
|
||||||
|
dangling.push_back(lkv.first);
|
||||||
|
if (dangling.empty()) continue;
|
||||||
|
int lo = lanes.begin()->first;
|
||||||
|
int hi = lanes.rbegin()->first;
|
||||||
|
Anomaly an;
|
||||||
|
an.kind = AnomalyKind::DiffCrossIncomplete;
|
||||||
|
an.module = gkv.first.first;
|
||||||
|
std::string m = gkv.first.first->name + ": " + gkv.first.second
|
||||||
|
+ "[" + std::to_string(lo) + ".."
|
||||||
|
+ std::to_string(hi) + "]_P/N: lane(s)";
|
||||||
|
for (int ix : dangling) m += " " + std::to_string(ix);
|
||||||
|
m += " do not cross (others reach";
|
||||||
|
std::vector<std::string> names;
|
||||||
|
for (Module *mod : reached) names.push_back(mod->name);
|
||||||
|
std::sort(names.begin(), names.end());
|
||||||
|
for (const std::string &nm : names) m += " " + nm;
|
||||||
|
m += ")";
|
||||||
|
an.message = std::move(m);
|
||||||
|
for (auto &lkv : lanes) {
|
||||||
|
an.involved.push_back(pairs[lkv.second].p);
|
||||||
|
an.involved.push_back(pairs[lkv.second].n);
|
||||||
|
}
|
||||||
|
out.push_back(std::move(an));
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
27
src/core/domain/diff_check.hpp
Normal file
27
src/core/domain/diff_check.hpp
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#ifndef _DIFF_CHECK_HPP_
|
||||||
|
#define _DIFF_CHECK_HPP_
|
||||||
|
|
||||||
|
#include "analysis.hpp" // Anomaly, diff_suffix, split_trailing_index
|
||||||
|
#include "nets.hpp" // Net
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class System;
|
||||||
|
|
||||||
|
// Differential-pair crossing checks. Every complete local diff pair
|
||||||
|
// (X_P / X_N, name-based) resolves its two legs to two bridged nets; any
|
||||||
|
// other module whose own pair sits on those nets must match them leg for
|
||||||
|
// leg. Findings:
|
||||||
|
// - DiffPolaritySwap: the peer pair is wired P→N / N→P, or a pair's two
|
||||||
|
// legs end up joined onto one single net through the connections.
|
||||||
|
// - DiffCrossIncomplete: the two pairs share only one leg (the other does
|
||||||
|
// not cross), or some lanes of a diff bus do not reach a module the
|
||||||
|
// other lanes reach.
|
||||||
|
// Name-based on BOTH sides: a peer whose signals carry no _P/_N suffix is
|
||||||
|
// not judged (silent). Polarity swaps are sometimes intentional (routing
|
||||||
|
// compensation, SerDes with configurable polarity) — these are findings to
|
||||||
|
// review, not hard errors. `nets` must come from compute_all_nets(sys).
|
||||||
|
std::vector<Anomaly> check_diff_crossings(System *sys,
|
||||||
|
const std::vector<Net> *nets);
|
||||||
|
|
||||||
|
#endif // _DIFF_CHECK_HPP_
|
||||||
@@ -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>
|
||||||
30
src/core/domain/signal_type.hpp
Normal file
30
src/core/domain/signal_type.hpp
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#ifndef _SIGNAL_TYPE_HPP_
|
||||||
|
#define _SIGNAL_TYPE_HPP_
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
enum class SignalType { Power, GndShield, Other };
|
||||||
|
|
||||||
|
// Name-level verdict, richer than SignalType. `PowerMgmt` is the key
|
||||||
|
// addition: a name holding BOTH a rail token (VCC/VDD/PWR/…) and a control
|
||||||
|
// token (SENSE/EN/PG/FB/…) is a power-management signal — measurement,
|
||||||
|
// enable, power-good — not the rail itself. Its non-Power classification is
|
||||||
|
// therefore confident, where a bare rail name without structural evidence
|
||||||
|
// stays suspect.
|
||||||
|
enum class NameVerdict { Rail, GndShield, PowerMgmt, Other };
|
||||||
|
|
||||||
|
struct NameClassification {
|
||||||
|
NameVerdict verdict = NameVerdict::Other;
|
||||||
|
std::string token; ///< PowerMgmt only: the control token that decided it.
|
||||||
|
};
|
||||||
|
|
||||||
|
NameClassification classify_signal_name(const std::string &signal_name);
|
||||||
|
|
||||||
|
const char *signal_type_name(SignalType t);
|
||||||
|
bool signal_type_from_name(const std::string &s, SignalType &out);
|
||||||
|
// Thin wrapper over classify_signal_name: Rail → Power, GndShield → GndShield,
|
||||||
|
// PowerMgmt/Other → Other.
|
||||||
|
SignalType infer_signal_type(const std::string &signal_name);
|
||||||
|
SignalType next_signal_type(SignalType t); // Power → GndShield → Other → Power
|
||||||
|
|
||||||
|
#endif // _SIGNAL_TYPE_HPP_
|
||||||
198
src/core/domain/signals.cpp
Normal file
198
src/core/domain/signals.cpp
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
|
||||||
|
#include "signals.hpp"
|
||||||
|
#include "parts.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <cstring>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
const char *signal_type_name(SignalType t) {
|
||||||
|
switch (t) {
|
||||||
|
case SignalType::Power: return "power";
|
||||||
|
case SignalType::GndShield: return "gnd";
|
||||||
|
case SignalType::Other: return "other";
|
||||||
|
}
|
||||||
|
return "other";
|
||||||
|
}
|
||||||
|
|
||||||
|
SignalType next_signal_type(SignalType t) {
|
||||||
|
switch (t) {
|
||||||
|
case SignalType::Power: return SignalType::GndShield;
|
||||||
|
case SignalType::GndShield: return SignalType::Other;
|
||||||
|
case SignalType::Other: return SignalType::Power;
|
||||||
|
}
|
||||||
|
return SignalType::Other;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool signal_type_from_name(const std::string &s, SignalType &out) {
|
||||||
|
std::string l = s;
|
||||||
|
std::transform(l.begin(), l.end(), l.begin(),
|
||||||
|
[](unsigned char c) { return std::tolower(c); });
|
||||||
|
if (l == "power" || l == "p") { out = SignalType::Power; return true; }
|
||||||
|
if (l == "gnd" || l == "g" || l == "shield" || l == "ground")
|
||||||
|
{ out = SignalType::GndShield; return true; }
|
||||||
|
if (l == "other" || l == "o" || l == "signal")
|
||||||
|
{ out = SignalType::Other; return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Control/monitoring vocabulary: a name holding both a rail token and one of
|
||||||
|
// these is a signal ABOUT a rail (feedback, enable, power-good, fault, …) —
|
||||||
|
// not the rail itself. Matched against whole separator-delimited tokens
|
||||||
|
// (uppercase, trailing digits stripped so EN1/PG0 still hit). Entries of
|
||||||
|
// length ≥ 4 also match as a token *suffix*, catching fused (VSENSE, PWRGOOD)
|
||||||
|
// and active-low (NFAULT) forms; short entries match exactly, so GREEN or
|
||||||
|
// SENSOR never trip on EN / SENSE.
|
||||||
|
const char *const kPowerControlTokens[] = {
|
||||||
|
"SENSE", "SNS", "KELVIN", // remote / Kelvin sense
|
||||||
|
"FB", "FBK", "FDB", "FDBK", "VFB", // regulator feedback
|
||||||
|
"FEEDBACK",
|
||||||
|
"EN", "ENA", "ENABLE", "INH", "INHIBIT", // enable / inhibit
|
||||||
|
"PG", "PGOOD", "PWRGD", "PWROK", // power-good
|
||||||
|
"GOOD", "OK", "FAIL", "FAULT", "FLT", // status / fault
|
||||||
|
"ALERT", "ALRT", "WARN",
|
||||||
|
"MON", "IMON", "VMON", "PMON", // monitoring
|
||||||
|
"DET", "DETECT", "PRSNT", "PRESENT", // presence detection
|
||||||
|
"OC", "OCP", "OV", "OVP", "UV", "UVP", // protection trips
|
||||||
|
"TRIP", "SHDN", "SHUTDOWN",
|
||||||
|
"ADJ", "ADJUST", "VADJ", "TRIM", // regulator adjust / trim
|
||||||
|
"MARG", "MARGIN", // voltage margining
|
||||||
|
"SET", "VSET", "ISET", // set-point pins
|
||||||
|
"SEQ", "CTRL", "CTL", "CMD", // sequencing / control / command
|
||||||
|
"STAT", "STATUS",
|
||||||
|
"ON", "OFF", "BTN", // on/off request
|
||||||
|
"REF", "VREF", // voltage reference
|
||||||
|
"LED", // indicator drive
|
||||||
|
"CS", "IRQ",
|
||||||
|
};
|
||||||
|
|
||||||
|
bool is_power_control_token(std::string tok) {
|
||||||
|
while (!tok.empty() && std::isdigit((unsigned char)tok.back()))
|
||||||
|
tok.pop_back(); // EN1, PG0, FB2 …
|
||||||
|
if (tok.empty()) return false;
|
||||||
|
for (const char *lex : kPowerControlTokens) {
|
||||||
|
size_t n = std::strlen(lex);
|
||||||
|
if (tok == lex) return true;
|
||||||
|
if (n >= 4 && tok.size() > n
|
||||||
|
&& tok.compare(tok.size() - n, n, lex) == 0)
|
||||||
|
return true; // VSENSE, PWRGOOD, NFAULT …
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split on every non-alphanumeric character. `u` is already uppercase.
|
||||||
|
std::vector<std::string> alnum_tokens(const std::string &u) {
|
||||||
|
std::vector<std::string> out;
|
||||||
|
std::string cur;
|
||||||
|
for (char c : u) {
|
||||||
|
if (std::isalnum((unsigned char)c)) { cur += c; continue; }
|
||||||
|
if (!cur.empty()) { out.push_back(std::move(cur)); cur.clear(); }
|
||||||
|
}
|
||||||
|
if (!cur.empty()) out.push_back(std::move(cur));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// Heuristic. Names like GND, GNDA, SHIELD, CHASSIS → GndShield (name alone is
|
||||||
|
// reliable there — left out of the control-token logic on purpose). Names
|
||||||
|
// containing PWR/POWER/VCC/VDD/VEE/VSS, or starting with VS_/VBAT/+/− → rail
|
||||||
|
// candidates; a rail candidate whose tokens include a control word (SENSE,
|
||||||
|
// EN, PG, …) is downgraded to PowerMgmt. Else Other.
|
||||||
|
NameClassification classify_signal_name(const std::string &name) {
|
||||||
|
NameClassification out;
|
||||||
|
if (name.empty()) return out;
|
||||||
|
std::string u = name;
|
||||||
|
std::transform(u.begin(), u.end(), u.begin(),
|
||||||
|
[](unsigned char c) { return std::toupper(c); });
|
||||||
|
|
||||||
|
auto contains = [&](const char *needle) {
|
||||||
|
return u.find(needle) != std::string::npos;
|
||||||
|
};
|
||||||
|
auto starts_with = [&](const char *needle) {
|
||||||
|
return u.rfind(needle, 0) == 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (u == "GND" || u == "GROUND"
|
||||||
|
|| starts_with("GND_")
|
||||||
|
|| (starts_with("GND") && u.size() >= 4
|
||||||
|
&& std::isalpha((unsigned char)u[3]))
|
||||||
|
|| contains("SHIELD") || contains("CHASSIS") || contains("EARTH")) {
|
||||||
|
out.verdict = NameVerdict::GndShield;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contains("PWR") || contains("POWER")
|
||||||
|
|| contains("VCC") || contains("VDD") || contains("VEE") || contains("VSS")
|
||||||
|
|| starts_with("VS_") || starts_with("VS1_") || starts_with("VS2_")
|
||||||
|
|| starts_with("VS3_") || starts_with("VS4_")
|
||||||
|
|| starts_with("VBAT") || starts_with("VBUS")
|
||||||
|
|| starts_with("+") || starts_with("-")) {
|
||||||
|
for (const std::string &tok : alnum_tokens(u)) {
|
||||||
|
if (is_power_control_token(tok)) {
|
||||||
|
out.verdict = NameVerdict::PowerMgmt;
|
||||||
|
out.token = tok;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.verdict = NameVerdict::Rail;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
SignalType infer_signal_type(const std::string &name) {
|
||||||
|
switch (classify_signal_name(name).verdict) {
|
||||||
|
case NameVerdict::Rail: return SignalType::Power;
|
||||||
|
case NameVerdict::GndShield: return SignalType::GndShield;
|
||||||
|
case NameVerdict::PowerMgmt:
|
||||||
|
case NameVerdict::Other: break;
|
||||||
|
}
|
||||||
|
return SignalType::Other;
|
||||||
|
}
|
||||||
|
|
||||||
|
Signal::Signal(std::string name)
|
||||||
|
: SystemElementContainer<Pin>(name), prnt(nullptr),
|
||||||
|
type(SignalType::Other) {};
|
||||||
|
|
||||||
|
void Signal::add(Pin *pin)
|
||||||
|
{
|
||||||
|
string pname = pin->prnt->name + "." + pin->name;
|
||||||
|
SystemElementContainer<Pin>::add(pname, pin);
|
||||||
|
}
|
||||||
|
|
||||||
|
Signals::Signals(void): SystemElementContainer<Signal>("signals") {}
|
||||||
|
|
||||||
|
Signals::Signals(std::vector<Signal *> signals): SystemElementContainer<Signal>("signals", signals) {}
|
||||||
|
|
||||||
|
Signals::~Signals() {
|
||||||
|
for (const auto& [key, value] : content) {
|
||||||
|
delete value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int drop_singleton_signals(Signals *signals) {
|
||||||
|
if (!signals) return 0;
|
||||||
|
std::vector<Signal *> doomed;
|
||||||
|
for (auto &kv : *signals)
|
||||||
|
if (kv.second->size() == 1) doomed.push_back(kv.second);
|
||||||
|
for (Signal *s : doomed) {
|
||||||
|
// Detach the lone pin so it surfaces as `(NC)` in views.
|
||||||
|
for (auto &pkv : *s) {
|
||||||
|
pkv.second->connect(nullptr);
|
||||||
|
pkv.second->nc_origin = NcOrigin::DroppedSingleton;
|
||||||
|
}
|
||||||
|
signals->remove(s->name);
|
||||||
|
delete s;
|
||||||
|
}
|
||||||
|
return (int)doomed.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Signals::add(Signal *signal)
|
||||||
|
{
|
||||||
|
SystemElementContainer<Signal>::add(signal);
|
||||||
|
signal->prnt = this;
|
||||||
|
}
|
||||||
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_
|
||||||
22
src/frontends/tui/CMakeLists.txt
Normal file
22
src/frontends/tui/CMakeLists.txt
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# 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)
|
||||||
|
|
||||||
|
# Library essim_tui (sources here minus main.cpp) + the `essim` binary.
|
||||||
|
essim_add_frontend(tui LIBS ftxui::screen ftxui::dom ftxui::component)
|
||||||
@@ -1,28 +1,26 @@
|
|||||||
#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/pins.hpp"
|
||||||
#include "system/pin_model.hpp"
|
#include "core/domain/signals.hpp"
|
||||||
#include "system/bsdl_model.hpp"
|
#include "core/domain/system.hpp"
|
||||||
#include "system/bsdl_check.hpp"
|
|
||||||
#include "system/pins.hpp"
|
#include "core/app/connect.hpp"
|
||||||
#include "system/signals.hpp"
|
#include "core/app/edit.hpp"
|
||||||
#include "system/system.hpp"
|
#include "core/app/load.hpp"
|
||||||
#include "system/transform.hpp"
|
#include "core/app/verify.hpp"
|
||||||
#include "system/transform_vpx.hpp"
|
|
||||||
|
|
||||||
#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 +136,25 @@ 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), "
|
||||||
|
+ std::to_string(r.mgmt)
|
||||||
|
+ " power-management (control/measure — 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,44 @@ 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)
|
render(r.diff_anomalies, " diff-pair crossing anomaly(ies).");
|
||||||
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" };
|
||||||
|
|
||||||
@@ -403,14 +337,10 @@ void Tui::RegisterCommands() {
|
|||||||
catch (const std::exception &) {
|
catch (const std::exception &) {
|
||||||
Print("unknown signal: " + mod->name + "/" + args[1]); return;
|
Print("unknown signal: " + mod->name + "/" + args[1]); return;
|
||||||
}
|
}
|
||||||
SignalType t;
|
app::SetSignalTypeResult r = app::set_signal_type(sig, args[2]);
|
||||||
if (!signal_type_from_name(args[2], t)) {
|
if (!r.ok) { Print(r.error); return; }
|
||||||
Print("type must be one of: power, gnd, other (got: " + args[2] + ")");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sig->type = t;
|
|
||||||
Print(mod->name + "/" + sig->name + ": signal type = "
|
Print(mod->name + "/" + sig->name + ": signal type = "
|
||||||
+ signal_type_name(t));
|
+ signal_type_name(r.type));
|
||||||
},
|
},
|
||||||
/*prompt_for_missing=*/ true,
|
/*prompt_for_missing=*/ true,
|
||||||
"override the auto-detected signal type (power | gnd | other)",
|
"override the auto-detected signal type (power | gnd | other)",
|
||||||
@@ -464,18 +394,15 @@ void Tui::RegisterCommands() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
std::string err = ValidatePartForKind(prt, args[2]);
|
app::SetConnectorTypeResult r = app::set_connector_type(prt, args[2]);
|
||||||
if (!err.empty()) {
|
if (!r.ok) {
|
||||||
Print("set-connector-type refused: " + err);
|
Print("set-connector-type refused: " + r.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
prt->connector_type = args[2];
|
|
||||||
ConnectorModel model(args[2]);
|
|
||||||
ApplyReport rep = apply_model(prt, model);
|
|
||||||
Print(mod->name + "/" + prt->name + ": connector_type = "
|
Print(mod->name + "/" + prt->name + ": connector_type = "
|
||||||
+ (args[2].empty() ? "(none)" : args[2]));
|
+ (args[2].empty() ? "(none)" : args[2]));
|
||||||
if (rep.materialised > 0)
|
if (r.materialised > 0)
|
||||||
Print("set-connector-type: added " + std::to_string(rep.materialised)
|
Print("set-connector-type: added " + std::to_string(r.materialised)
|
||||||
+ " NC pin(s) from the connector layout");
|
+ " NC pin(s) from the connector layout");
|
||||||
},
|
},
|
||||||
/*prompt_for_missing=*/ false,
|
/*prompt_for_missing=*/ false,
|
||||||
@@ -516,17 +443,11 @@ void Tui::RegisterCommands() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BsdlModel model = BsdlModel::from_file(args[2]);
|
app::AttachBsdlResult r = app::attach_bsdl(prt, args[2]);
|
||||||
if (!model.valid()) {
|
if (!r.ok) { Print("attach-bsdl: " + r.error); return; }
|
||||||
Print("attach-bsdl: cannot parse " + args[2]
|
Print(mod->name + "/" + prt->name + ": attached BSDL '" + r.entity
|
||||||
+ (model.error().empty() ? "" : (": " + model.error())));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
BsdlApplyReport r = apply_bsdl(prt, model);
|
|
||||||
prt->bsdl_path = args[2];
|
|
||||||
Print(mod->name + "/" + prt->name + ": attached BSDL '" + model.entity()
|
|
||||||
+ "' — " + std::to_string(r.bound) + "/"
|
+ "' — " + std::to_string(r.bound) + "/"
|
||||||
+ std::to_string((int)model.ports().size()) + " ports bound"
|
+ std::to_string(r.ports_total) + " ports bound"
|
||||||
+ (r.unbound ? (", " + std::to_string(r.unbound) + " unbound") : ""));
|
+ (r.unbound ? (", " + std::to_string(r.unbound) + " unbound") : ""));
|
||||||
},
|
},
|
||||||
/*prompt_for_missing=*/ false,
|
/*prompt_for_missing=*/ false,
|
||||||
@@ -622,47 +543,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)",
|
||||||
@@ -701,51 +598,12 @@ void Tui::RegisterCommands() {
|
|||||||
{"new module name", Completion::None}},
|
{"new module name", 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; }
|
||||||
|
app::DuplicateResult r =
|
||||||
Module *src;
|
app::duplicate_module(sys.get(), args[0], args[1]);
|
||||||
try { src = sys->modules()->get(args[0]); }
|
if (!r.ok) { Print(r.error); return; }
|
||||||
catch (const std::exception &) {
|
|
||||||
Print("unknown module: " + args[0]); return;
|
|
||||||
}
|
|
||||||
if (sys->modules()->exists(args[1])) {
|
|
||||||
Print("duplicate refused: module '" + args[1] + "' already exists.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Module *dst = new Module(args[1]);
|
|
||||||
|
|
||||||
// 1. Copy signals (preserve type overrides).
|
|
||||||
for (auto &skv : *src->signals) {
|
|
||||||
Signal *ss = skv.second;
|
|
||||||
Signal *ds = new Signal(ss->name);
|
|
||||||
ds->type = ss->type;
|
|
||||||
dst->signals->add(ds);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Copy parts, pins, and re-wire pin→signal.
|
|
||||||
for (auto &pkv : *src) {
|
|
||||||
Part *sp = pkv.second;
|
|
||||||
Part *dp = new Part(sp->name);
|
|
||||||
dp->connector_type = sp->connector_type;
|
|
||||||
for (auto &nkv : *sp) {
|
|
||||||
Pin *sn = nkv.second;
|
|
||||||
Pin *dn = new Pin(sn->name);
|
|
||||||
dn->spec = sn->spec;
|
|
||||||
dn->nc_origin = sn->nc_origin;
|
|
||||||
dp->add(dn);
|
|
||||||
if (sn->signal()) {
|
|
||||||
Signal *ds = dst->signals->get(sn->signal()->name);
|
|
||||||
ds->add(dn);
|
|
||||||
dn->connect(ds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dst->add(dp);
|
|
||||||
}
|
|
||||||
|
|
||||||
sys->modules()->add(dst);
|
|
||||||
Print("duplicate: '" + args[0] + "' → '" + args[1] + "'"
|
Print("duplicate: '" + args[0] + "' → '" + args[1] + "'"
|
||||||
+ " (" + std::to_string(dst->size()) + " part(s), "
|
+ " (" + std::to_string(r.parts) + " part(s), "
|
||||||
+ std::to_string(dst->signals->size()) + " signal(s))");
|
+ std::to_string(r.signals) + " signal(s))");
|
||||||
},
|
},
|
||||||
/*prompt_for_missing=*/ true,
|
/*prompt_for_missing=*/ true,
|
||||||
"clone a module under a new name (parts, pins, signals; no connections)",
|
"clone a module under a new name (parts, pins, signals; no connections)",
|
||||||
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,26 @@ 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);
|
||||||
}
|
push_anoms(vr.diff_anomalies);
|
||||||
for (const auto &a : model_anoms)
|
int n_model = vr.model_total();
|
||||||
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)
|
||||||
@@ -157,29 +132,39 @@ Component Tui::BuildAnalyzeScreen() {
|
|||||||
// ============================================================= Types
|
// ============================================================= Types
|
||||||
// Power decisions (confirmed / refuted) and NC orphan breakdown.
|
// Power decisions (confirmed / refuted) and NC orphan breakdown.
|
||||||
analyze_types.clear();
|
analyze_types.clear();
|
||||||
int conf_pwr = 0, ref_pwr = 0, gnd = 0;
|
int conf_pwr = 0, ref_pwr = 0, mgmt = 0, gnd = 0;
|
||||||
struct Row { char kind; std::string mod, sig; int fanout; bool voltage; };
|
struct Row { char kind; std::string mod, sig; int fanout; bool voltage;
|
||||||
|
std::string token; };
|
||||||
std::vector<Row> rows;
|
std::vector<Row> rows;
|
||||||
for (auto &mkv : *sys->modules()) {
|
for (auto &mkv : *sys->modules()) {
|
||||||
Module *mod = mkv.second;
|
Module *mod = mkv.second;
|
||||||
for (auto &skv : *mod->signals) {
|
for (auto &skv : *mod->signals) {
|
||||||
Signal *s = skv.second;
|
Signal *s = skv.second;
|
||||||
SignalType named = infer_signal_type(s->name);
|
NameClassification ncl = classify_signal_name(s->name);
|
||||||
char kind = 0;
|
char kind = 0;
|
||||||
if (named == SignalType::GndShield && s->type == SignalType::GndShield) {
|
std::string token;
|
||||||
|
if (ncl.verdict == NameVerdict::GndShield && s->type == SignalType::GndShield) {
|
||||||
kind = 'G'; ++gnd;
|
kind = 'G'; ++gnd;
|
||||||
} else if (named == SignalType::Power && s->type == SignalType::Power) {
|
} else if (ncl.verdict == NameVerdict::Rail && s->type == SignalType::Power) {
|
||||||
kind = 'P'; ++conf_pwr;
|
kind = 'P'; ++conf_pwr;
|
||||||
} else if (named == SignalType::Power && s->type == SignalType::Other) {
|
} else if (ncl.verdict == NameVerdict::Rail && s->type == SignalType::Other) {
|
||||||
kind = 'R'; ++ref_pwr;
|
kind = 'R'; ++ref_pwr;
|
||||||
|
} else if (ncl.verdict == NameVerdict::PowerMgmt) {
|
||||||
|
kind = 'M'; ++mgmt; token = ncl.token;
|
||||||
} else continue;
|
} else continue;
|
||||||
rows.push_back({kind, mod->name, s->name,
|
rows.push_back({kind, mod->name, s->name,
|
||||||
(int)s->size(), has_voltage_pattern(s->name)});
|
(int)s->size(), has_voltage_pattern(s->name),
|
||||||
|
token});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Deliberate display order: confirmed rails, then the suspects (the
|
||||||
|
// actionable residue), then the power-management signals, gnd last.
|
||||||
|
auto rank = [](char k) {
|
||||||
|
return k == 'P' ? 0 : k == 'R' ? 1 : k == 'M' ? 2 : 3;
|
||||||
|
};
|
||||||
std::sort(rows.begin(), rows.end(),
|
std::sort(rows.begin(), rows.end(),
|
||||||
[](const Row &a, const Row &b) {
|
[&](const Row &a, const Row &b) {
|
||||||
if (a.kind != b.kind) return a.kind < b.kind;
|
if (a.kind != b.kind) return rank(a.kind) < rank(b.kind);
|
||||||
if (a.mod != b.mod) return a.mod < b.mod;
|
if (a.mod != b.mod) return a.mod < b.mod;
|
||||||
return a.sig < b.sig;
|
return a.sig < b.sig;
|
||||||
});
|
});
|
||||||
@@ -207,6 +192,10 @@ Component Tui::BuildAnalyzeScreen() {
|
|||||||
else reason = "name only — fan-out "
|
else reason = "name only — fan-out "
|
||||||
+ std::to_string(r.fanout)
|
+ std::to_string(r.fanout)
|
||||||
+ ", no voltage";
|
+ ", no voltage";
|
||||||
|
} else if (r.kind == 'M') {
|
||||||
|
tag = "[Power mgmt] ";
|
||||||
|
reason = "control token '" + r.token
|
||||||
|
+ "' in name — kept as Other";
|
||||||
} else {
|
} else {
|
||||||
tag = "[Gnd] ";
|
tag = "[Gnd] ";
|
||||||
reason = "name match (fan-out " + std::to_string(r.fanout) + ")";
|
reason = "name match (fan-out " + std::to_string(r.fanout) + ")";
|
||||||
@@ -215,33 +204,19 @@ Component Tui::BuildAnalyzeScreen() {
|
|||||||
std::string(tag) + r.mod + "/" + r.sig + " — " + reason);
|
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;
|
||||||
|
|
||||||
std::string types_header = "Types: " + std::to_string(conf_pwr)
|
std::string types_header = "Types: " + std::to_string(conf_pwr)
|
||||||
+ " Power, " + std::to_string(ref_pwr)
|
+ " Power, " + std::to_string(ref_pwr)
|
||||||
+ " Suspect, " + std::to_string(gnd)
|
+ " Suspect, " + std::to_string(mgmt)
|
||||||
|
+ " Pwr-mgmt, " + std::to_string(gnd)
|
||||||
+ " Gnd";
|
+ " Gnd";
|
||||||
|
|
||||||
// Tab bar — horizontal headers, active one inverted.
|
// Tab bar — horizontal headers, active one inverted.
|
||||||
@@ -281,8 +256,14 @@ Component Tui::BuildAnalyzeScreen() {
|
|||||||
"Name suggests Power AND structure agrees: fan-out ≥ 4 pins, "
|
"Name suggests Power AND structure agrees: fan-out ≥ 4 pins, "
|
||||||
"or a voltage pattern in the name (e.g. 3V3, 5V, 12V)."),
|
"or a voltage pattern in the name (e.g. 3V3, 5V, 12V)."),
|
||||||
term("Suspect Power",
|
term("Suspect Power",
|
||||||
"Name suggests Power but the structural check failed — "
|
"Name suggests Power, no control token explains it, but the "
|
||||||
"fan-out too low and no voltage in the name."),
|
"structural check failed — fan-out too low and no voltage "
|
||||||
|
"in the name."),
|
||||||
|
term("Power mgmt",
|
||||||
|
"Name holds a rail token AND a control token (SENSE, EN, PG, "
|
||||||
|
"FB, …): a power-management signal — measurement or control "
|
||||||
|
"of a rail — not the rail itself. Confidently Other, never "
|
||||||
|
"suspect."),
|
||||||
term("Hard floor",
|
term("Hard floor",
|
||||||
"Fan-out below 3 pins forces Other regardless of the name. "
|
"Fan-out below 3 pins forces Other regardless of the name. "
|
||||||
"A real rail physically cannot live on 1-2 pads."),
|
"A real rail physically cannot live on 1-2 pads."),
|
||||||
@@ -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;
|
||||||
@@ -44,6 +41,8 @@ Component Tui::BuildDashboardScreen() {
|
|||||||
|
|
||||||
Element early_help = RenderHelpPanel("dashboard", {
|
Element early_help = RenderHelpPanel("dashboard", {
|
||||||
{"c", "console"},
|
{"c", "console"},
|
||||||
|
{"o", "open/run a script"},
|
||||||
|
{"r", "restore a snapshot"},
|
||||||
{"a", "analyze"},
|
{"a", "analyze"},
|
||||||
{"h", "help screen"},
|
{"h", "help screen"},
|
||||||
{"q", "quit"},
|
{"q", "quit"},
|
||||||
@@ -56,8 +55,8 @@ Component Tui::BuildDashboardScreen() {
|
|||||||
separator(),
|
separator(),
|
||||||
hbox({
|
hbox({
|
||||||
vbox({
|
vbox({
|
||||||
text(" no system loaded — run 'new' or 'restore <file>'") | dim,
|
text(" no system loaded") | dim,
|
||||||
text(" (press 'c' for the console, or Ctrl-P for the palette)") | dim,
|
text(" (press 'o' open a script · 'r' restore a snapshot · 'c' console · Ctrl-P palette)") | dim,
|
||||||
filler(),
|
filler(),
|
||||||
}) | flex,
|
}) | flex,
|
||||||
separator(),
|
separator(),
|
||||||
@@ -75,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({
|
||||||
@@ -142,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,
|
||||||
@@ -170,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)"));
|
||||||
|
|
||||||
@@ -311,6 +272,7 @@ Component Tui::BuildDashboardScreen() {
|
|||||||
{"x", "export"},
|
{"x", "export"},
|
||||||
{"o", "open/run a script"},
|
{"o", "open/run a script"},
|
||||||
{"s", "save system"},
|
{"s", "save system"},
|
||||||
|
{"r", "restore a snapshot"},
|
||||||
{"PgUp", "scroll up"},
|
{"PgUp", "scroll up"},
|
||||||
{"PgDn", "scroll down"},
|
{"PgDn", "scroll down"},
|
||||||
{"Home", "scroll top"},
|
{"Home", "scroll top"},
|
||||||
@@ -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>
|
||||||
@@ -54,11 +54,13 @@ void Tui::OpenFileDialog(std::string title,
|
|||||||
std::string persist_key,
|
std::string persist_key,
|
||||||
std::string default_filename,
|
std::string default_filename,
|
||||||
std::vector<FilenameFilter> filters,
|
std::vector<FilenameFilter> filters,
|
||||||
std::function<void(const std::string &)> on_confirm) {
|
std::function<void(const std::string &)> on_confirm,
|
||||||
|
bool confirm_overwrite) {
|
||||||
file_dialog.title = std::move(title);
|
file_dialog.title = std::move(title);
|
||||||
file_dialog.persist_key = std::move(persist_key);
|
file_dialog.persist_key = std::move(persist_key);
|
||||||
file_dialog.on_confirm = std::move(on_confirm);
|
file_dialog.on_confirm = std::move(on_confirm);
|
||||||
file_dialog.filters = std::move(filters);
|
file_dialog.filters = std::move(filters);
|
||||||
|
file_dialog.confirm_overwrite = confirm_overwrite;
|
||||||
file_dialog.filter_labels.clear();
|
file_dialog.filter_labels.clear();
|
||||||
for (const auto &f : file_dialog.filters)
|
for (const auto &f : file_dialog.filters)
|
||||||
file_dialog.filter_labels.push_back(f.label);
|
file_dialog.filter_labels.push_back(f.label);
|
||||||
@@ -122,7 +124,7 @@ void Tui::ConfirmFileDialog() {
|
|||||||
// Overwrite guard: if the file already exists, ask before letting
|
// Overwrite guard: if the file already exists, ask before letting
|
||||||
// the action proceed. Esc / No cancels; Yes runs the action.
|
// the action proceed. Esc / No cancels; Yes runs the action.
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
if (fs::exists(full, ec) && !ec) {
|
if (file_dialog.confirm_overwrite && fs::exists(full, ec) && !ec) {
|
||||||
ShowConfirm("File '" + full.string() + "' already exists.\n"
|
ShowConfirm("File '" + full.string() + "' already exists.\n"
|
||||||
"Overwrite?",
|
"Overwrite?",
|
||||||
invoke);
|
invoke);
|
||||||
@@ -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>
|
||||||
@@ -89,18 +89,8 @@ Component Tui::BuildMainScreen(ScreenInteractive &screen) {
|
|||||||
}) | flex,
|
}) | flex,
|
||||||
}) | border;
|
}) | border;
|
||||||
|
|
||||||
if (loading) {
|
// The "Computing…" overlay is rendered globally in Run(), so it shows
|
||||||
int total = (int)loading_lines.size();
|
// on whatever screen is active while a script loads.
|
||||||
std::string progress = std::to_string(loading_executed) + " / "
|
|
||||||
+ std::to_string(total) + " lines";
|
|
||||||
auto modal = vbox({
|
|
||||||
text(" Computing… ") | bold | center,
|
|
||||||
separator(),
|
|
||||||
text(loading_filename) | center,
|
|
||||||
text(progress) | center,
|
|
||||||
}) | borderDouble | size(WIDTH, GREATER_THAN, 40);
|
|
||||||
return dbox({base, modal | center});
|
|
||||||
}
|
|
||||||
return base;
|
return base;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -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,12 @@
|
|||||||
#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/app/edit.hpp"
|
||||||
#include "system/parts.hpp"
|
#include "core/domain/modules.hpp"
|
||||||
#include "system/pin_model.hpp"
|
#include "core/domain/parts.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 <ftxui/component/component.hpp>
|
#include <ftxui/component/component.hpp>
|
||||||
#include <ftxui/component/component_options.hpp>
|
#include <ftxui/component/component_options.hpp>
|
||||||
@@ -63,14 +62,11 @@ Component Tui::BuildSettypeScreen() {
|
|||||||
try {
|
try {
|
||||||
Module *mod = sys->modules()->get(settype_modules[settype_m_idx]);
|
Module *mod = sys->modules()->get(settype_modules[settype_m_idx]);
|
||||||
Part *prt = mod->get(settype_p_list[settype_p_idx]);
|
Part *prt = mod->get(settype_p_list[settype_p_idx]);
|
||||||
std::string err = ValidatePartForKind(prt, settype_type);
|
app::SetConnectorTypeResult r = app::set_connector_type(prt, settype_type);
|
||||||
if (!err.empty()) {
|
if (!r.ok) {
|
||||||
settype_status = "refused: " + err;
|
settype_status = "refused: " + r.error;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
prt->connector_type = settype_type;
|
|
||||||
ConnectorModel model(settype_type);
|
|
||||||
apply_model(prt, model);
|
|
||||||
std::string msg = mod->name + "/" + prt->name + " = "
|
std::string msg = mod->name + "/" + prt->name + " = "
|
||||||
+ (settype_type.empty() ? "(none)" : settype_type);
|
+ (settype_type.empty() ? "(none)" : settype_type);
|
||||||
settype_status = "applied: " + msg;
|
settype_status = "applied: " + msg;
|
||||||
@@ -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>
|
||||||
@@ -281,23 +281,34 @@ void Tui::Source(const std::string &filename) {
|
|||||||
if (const char *home = std::getenv("HOME"))
|
if (const char *home = std::getenv("HOME"))
|
||||||
expanded = std::string(home) + expanded.substr(1);
|
expanded = std::string(home) + expanded.substr(1);
|
||||||
}
|
}
|
||||||
|
if (source_stack.size() >= 32) { // same depth guard as the core engine
|
||||||
|
Print("source: nesting too deep, skipping " + filename);
|
||||||
|
return;
|
||||||
|
}
|
||||||
std::ifstream f(expanded);
|
std::ifstream f(expanded);
|
||||||
if (!f) { Print("source failed: cannot open " + filename); return; }
|
if (!f) { Print("source failed: cannot open " + filename); return; }
|
||||||
|
|
||||||
// Slurp the whole file so we can drive line-by-line processing from the
|
// Slurp the whole file so we can drive line-by-line processing from the
|
||||||
// event loop (one line per posted task). This lets the screen redraw
|
// event loop (one line per posted task). This lets the screen redraw
|
||||||
// between lines and surface the "Computing…" modal.
|
// between lines and surface the "Computing…" modal.
|
||||||
loading_lines.clear();
|
SourceFrame fr;
|
||||||
|
fr.filename = filename;
|
||||||
std::string line;
|
std::string line;
|
||||||
while (std::getline(f, line)) loading_lines.push_back(line);
|
while (std::getline(f, line)) fr.lines.push_back(std::move(line));
|
||||||
|
|
||||||
|
// Nested source (a sourced line is itself `source …`): just stack the
|
||||||
|
// frame — the driver already running (ticker thread or headless drain)
|
||||||
|
// picks it up on the next ProcessNextSourceLine, and the caller's frame
|
||||||
|
// resumes when it finishes.
|
||||||
|
bool nested = !source_stack.empty();
|
||||||
|
source_stack.push_back(std::move(fr));
|
||||||
|
if (nested) return;
|
||||||
|
|
||||||
loading_filename = filename;
|
|
||||||
loading_idx = 0;
|
|
||||||
loading_executed = 0;
|
|
||||||
loading_lineno = 0;
|
|
||||||
loading_prev_in_source = in_source;
|
loading_prev_in_source = in_source;
|
||||||
|
source_origin_screen = screen_idx; // a sourced line that leaves this screen aborts
|
||||||
in_source = true;
|
in_source = true;
|
||||||
loading = true;
|
loading = true;
|
||||||
|
computing_open = true; // raise the global "Computing…" progress modal
|
||||||
|
|
||||||
if (!screen_ptr) {
|
if (!screen_ptr) {
|
||||||
// Headless fallback (e.g. tests): drain synchronously.
|
// Headless fallback (e.g. tests): drain synchronously.
|
||||||
@@ -330,9 +341,18 @@ void Tui::Source(const std::string &filename) {
|
|||||||
|
|
||||||
void Tui::ProcessNextSourceLine() {
|
void Tui::ProcessNextSourceLine() {
|
||||||
if (!loading.load()) return;
|
if (!loading.load()) return;
|
||||||
while (loading_idx < loading_lines.size()) {
|
while (!source_stack.empty()) {
|
||||||
const std::string &raw = loading_lines[loading_idx++];
|
if (source_stack.back().idx >= source_stack.back().lines.size()) {
|
||||||
++loading_lineno;
|
// Frame done: summarise it and resume the caller's frame.
|
||||||
|
const SourceFrame &done = source_stack.back();
|
||||||
|
Print("source: " + done.filename
|
||||||
|
+ " (" + std::to_string(done.executed) + " line(s))");
|
||||||
|
source_stack.pop_back();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
SourceFrame &fr = source_stack.back();
|
||||||
|
const std::string raw = fr.lines[fr.idx++];
|
||||||
|
++fr.lineno;
|
||||||
size_t start = raw.find_first_not_of(" \t");
|
size_t start = raw.find_first_not_of(" \t");
|
||||||
if (start == std::string::npos) continue;
|
if (start == std::string::npos) continue;
|
||||||
if (raw[start] == '#') continue;
|
if (raw[start] == '#') continue;
|
||||||
@@ -341,28 +361,28 @@ void Tui::ProcessNextSourceLine() {
|
|||||||
trimmed.pop_back();
|
trimmed.pop_back();
|
||||||
if (trimmed.empty()) continue;
|
if (trimmed.empty()) continue;
|
||||||
|
|
||||||
|
++fr.executed;
|
||||||
|
int lineno = fr.lineno; // copies: Submit can push a nested frame,
|
||||||
|
// which may reallocate and invalidate `fr`.
|
||||||
input = trimmed;
|
input = trimmed;
|
||||||
cursor_pos = (int)input.size();
|
cursor_pos = (int)input.size();
|
||||||
Submit();
|
Submit();
|
||||||
++loading_executed;
|
|
||||||
|
|
||||||
if (screen_idx != 0) {
|
if (screen_idx != source_origin_screen) {
|
||||||
Print("source: line " + std::to_string(loading_lineno)
|
Print("source: line " + std::to_string(lineno)
|
||||||
+ " is interactive (would open a screen) — aborting.");
|
+ " is interactive (would open a screen) — aborting.");
|
||||||
screen_idx = 0;
|
screen_idx = source_origin_screen;
|
||||||
loading.store(false);
|
source_stack.clear(); // an abort cancels the whole chain
|
||||||
tick_in_flight.store(false);
|
break;
|
||||||
in_source = loading_prev_in_source;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
// One effective line per tick — ack so the ticker can pace the next.
|
// One effective line per tick — ack so the ticker can pace the next.
|
||||||
tick_in_flight.store(false);
|
tick_in_flight.store(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Print("source: " + loading_filename
|
// Stack drained (or aborted): close up.
|
||||||
+ " (" + std::to_string(loading_executed) + " line(s))");
|
|
||||||
loading.store(false);
|
loading.store(false);
|
||||||
|
computing_open = false;
|
||||||
tick_in_flight.store(false);
|
tick_in_flight.store(false);
|
||||||
in_source = loading_prev_in_source;
|
in_source = loading_prev_in_source;
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
#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>
|
||||||
#include <ftxui/component/screen_interactive.hpp>
|
#include <ftxui/component/screen_interactive.hpp>
|
||||||
|
#include <ftxui/dom/elements.hpp>
|
||||||
|
|
||||||
using namespace ftxui;
|
using namespace ftxui;
|
||||||
|
|
||||||
Tui::Tui()
|
Tui::Tui()
|
||||||
: cursor_pos(0), history_idx(-1), scroll_offset(0), quit(false), in_source(false),
|
: cursor_pos(0), history_idx(-1), scroll_offset(0), quit(false), in_source(false),
|
||||||
loading(false), tick_in_flight(false),
|
loading(false), tick_in_flight(false),
|
||||||
loading_idx(0), loading_executed(0), loading_lineno(0),
|
|
||||||
loading_prev_in_source(false), screen_ptr(nullptr),
|
loading_prev_in_source(false), screen_ptr(nullptr),
|
||||||
screen_idx(4), // boot to the dashboard; console (screen 0) is now a sub-screen
|
screen_idx(4), // boot to the dashboard; console (screen 0) is now a sub-screen
|
||||||
connect_m1_idx(0), connect_m2_idx(0),
|
connect_m1_idx(0), connect_m2_idx(0),
|
||||||
@@ -58,10 +58,36 @@ void Tui::Run() {
|
|||||||
auto with_error = with_confirm
|
auto with_error = with_confirm
|
||||||
| Modal(BuildErrorModal(), &error_open);
|
| Modal(BuildErrorModal(), &error_open);
|
||||||
|
|
||||||
auto root = CatchEvent(with_error, [this](Event e) {
|
// Global "Computing…" progress modal while a script loads — a proper Modal
|
||||||
|
// (like the palette / file dialog), so it shows on any screen, e.g. when a
|
||||||
|
// script is opened from the dashboard. The Renderer re-reads the live
|
||||||
|
// progress every frame.
|
||||||
|
auto computing_modal = Renderer([this] {
|
||||||
|
std::string fname, progress;
|
||||||
|
if (!source_stack.empty()) { // top frame = the file currently running
|
||||||
|
const SourceFrame &fr = source_stack.back();
|
||||||
|
fname = fr.filename;
|
||||||
|
progress = std::to_string(fr.executed) + " / "
|
||||||
|
+ std::to_string((int)fr.lines.size()) + " lines";
|
||||||
|
}
|
||||||
|
return vbox({
|
||||||
|
text(" Computing… ") | bold | center,
|
||||||
|
separator(),
|
||||||
|
text(fname) | center,
|
||||||
|
text(progress) | center,
|
||||||
|
}) | borderDouble | size(WIDTH, GREATER_THAN, 40);
|
||||||
|
});
|
||||||
|
auto with_loading = with_error | Modal(computing_modal, &computing_open);
|
||||||
|
|
||||||
|
auto root = CatchEvent(with_loading, [this](Event e) {
|
||||||
|
// Source ticks drive the line-by-line loader; handle them on ANY screen
|
||||||
|
// (and before the modal guard) so `source` keeps running while the
|
||||||
|
// Computing modal is up.
|
||||||
|
if (e == Event::Special("\x02tick")) { ProcessNextSourceLine(); return true; }
|
||||||
|
|
||||||
// Modals own their events while open. Error modal sits on top.
|
// Modals own their events while open. Error modal sits on top.
|
||||||
if (error_open || confirm_open || palette_open
|
if (error_open || confirm_open || palette_open
|
||||||
|| sigtype_dialog_open || file_dialog.open) return false;
|
|| sigtype_dialog_open || file_dialog.open || computing_open) return false;
|
||||||
|
|
||||||
// Ctrl-P opens the palette from any screen.
|
// Ctrl-P opens the palette from any screen.
|
||||||
if (e == Event::CtrlP) { OpenPalette(); return true; }
|
if (e == Event::CtrlP) { OpenPalette(); return true; }
|
||||||
@@ -106,7 +132,8 @@ void Tui::Run() {
|
|||||||
"dashboard.source", "", {},
|
"dashboard.source", "", {},
|
||||||
[this](const std::string &path) {
|
[this](const std::string &path) {
|
||||||
Dispatch("source " + path);
|
Dispatch("source " + path);
|
||||||
});
|
},
|
||||||
|
/*confirm_overwrite=*/false); // opening, not saving
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (e == Event::Character("s")) { // save the system snapshot
|
if (e == Event::Character("s")) { // save the system snapshot
|
||||||
@@ -118,6 +145,15 @@ void Tui::Run() {
|
|||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (e == Event::Character("r")) { // restore a saved system
|
||||||
|
OpenFileDialog("Restore system — load a snapshot",
|
||||||
|
"dashboard.restore", "", {},
|
||||||
|
[this](const std::string &path) {
|
||||||
|
Dispatch("restore " + path);
|
||||||
|
},
|
||||||
|
/*confirm_overwrite=*/false); // opening, not saving
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
case 3: // explore
|
case 3: // explore
|
||||||
@@ -152,7 +188,6 @@ void Tui::Run() {
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
default: // 0: main (console / log view)
|
default: // 0: main (console / log view)
|
||||||
if (e == Event::Special("\x02tick")) { ProcessNextSourceLine(); return true; }
|
|
||||||
if (e == Event::Escape) {
|
if (e == Event::Escape) {
|
||||||
if (!pending.empty()) { CancelPending(); return true; }
|
if (!pending.empty()) { CancelPending(); return true; }
|
||||||
screen_idx = 4; return true;
|
screen_idx = 4; return true;
|
||||||
@@ -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 {
|
||||||
@@ -97,12 +99,19 @@ class Tui {
|
|||||||
// ---- Source-file loading (event-driven, one line per tick) ----
|
// ---- Source-file loading (event-driven, one line per tick) ----
|
||||||
std::atomic<bool> loading; ///< true while a script is being processed; read by tick thread.
|
std::atomic<bool> loading; ///< true while a script is being processed; read by tick thread.
|
||||||
std::atomic<bool> tick_in_flight; ///< main thread acks each tick by clearing this; ticker waits.
|
std::atomic<bool> tick_in_flight; ///< main thread acks each tick by clearing this; ticker waits.
|
||||||
std::string loading_filename;
|
// One script being processed. Nested `source` pushes a frame so the
|
||||||
std::vector<std::string> loading_lines;
|
// caller resumes where it left off — the stack IS the call chain.
|
||||||
size_t loading_idx;
|
struct SourceFrame {
|
||||||
int loading_executed;
|
std::string filename;
|
||||||
int loading_lineno;
|
std::vector<std::string> lines;
|
||||||
|
size_t idx = 0; ///< next line to process
|
||||||
|
int executed = 0; ///< effective (non-blank, non-comment) lines run
|
||||||
|
int lineno = 0; ///< current 1-based line, for messages
|
||||||
|
};
|
||||||
|
std::vector<SourceFrame> source_stack;
|
||||||
bool loading_prev_in_source;
|
bool loading_prev_in_source;
|
||||||
|
int source_origin_screen = 0; ///< screen a `source` started from; a sourced line that navigates away (opens an interactive screen) aborts it.
|
||||||
|
bool computing_open = false; ///< drives the global "Computing…" progress modal while a script loads.
|
||||||
ftxui::ScreenInteractive *screen_ptr; ///< set in Run() so Source() can post events.
|
ftxui::ScreenInteractive *screen_ptr; ///< set in Run() so Source() can post events.
|
||||||
|
|
||||||
// ---- Dashboard scroll state (0 = top; grows as the user scrolls down) ----
|
// ---- Dashboard scroll state (0 = top; grows as the user scrolls down) ----
|
||||||
@@ -149,6 +158,7 @@ private:
|
|||||||
std::vector<std::string> filter_labels; ///< parallel to `filters`
|
std::vector<std::string> filter_labels; ///< parallel to `filters`
|
||||||
int filter_idx = 0;
|
int filter_idx = 0;
|
||||||
std::function<void(const std::string &)> on_confirm;
|
std::function<void(const std::string &)> on_confirm;
|
||||||
|
bool confirm_overwrite = true; ///< false in "open" mode — skip the overwrite prompt.
|
||||||
};
|
};
|
||||||
FileDialogState file_dialog;
|
FileDialogState file_dialog;
|
||||||
|
|
||||||
@@ -195,16 +205,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)
|
||||||
@@ -272,11 +282,14 @@ private:
|
|||||||
// dir + filename are stored (one tiny file per key under the user
|
// dir + filename are stored (one tiny file per key under the user
|
||||||
// data directory). `on_confirm` runs when the user presses Enter on
|
// data directory). `on_confirm` runs when the user presses Enter on
|
||||||
// the action button — it receives the absolute path the user picked.
|
// the action button — it receives the absolute path the user picked.
|
||||||
|
// `confirm_overwrite = false` puts the dialog in "open" mode: it skips the
|
||||||
|
// "file exists — overwrite?" prompt (you *want* an existing file to open).
|
||||||
void OpenFileDialog(std::string title,
|
void OpenFileDialog(std::string title,
|
||||||
std::string persist_key,
|
std::string persist_key,
|
||||||
std::string default_filename,
|
std::string default_filename,
|
||||||
std::vector<FilenameFilter> filters,
|
std::vector<FilenameFilter> filters,
|
||||||
std::function<void(const std::string &)> on_confirm);
|
std::function<void(const std::string &)> on_confirm,
|
||||||
|
bool confirm_overwrite = true);
|
||||||
void ConfirmFileDialog();
|
void ConfirmFileDialog();
|
||||||
ftxui::Component BuildSignalTypeModal();
|
ftxui::Component BuildSignalTypeModal();
|
||||||
ftxui::Component BuildPaletteModal();
|
ftxui::Component BuildPaletteModal();
|
||||||
@@ -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>
|
||||||
|
|
||||||
16
src/frontends/wx/CMakeLists.txt
Normal file
16
src/frontends/wx/CMakeLists.txt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# wxWidgets GUI frontend. Builds the `essim` executable against essim_core.
|
||||||
|
#
|
||||||
|
# Self-contained like every frontend: it pulls its own GUI toolkit (here a
|
||||||
|
# system wxWidgets via find_package), then defers the target wiring to the
|
||||||
|
# shared essim_add_frontend() helper. Select it with -DESSIM_FRONTEND=wx.
|
||||||
|
#
|
||||||
|
# Needs the wxWidgets development package, e.g. on Debian/Ubuntu:
|
||||||
|
# sudo apt install libwxgtk3.2-dev
|
||||||
|
|
||||||
|
find_package(wxWidgets REQUIRED COMPONENTS core base)
|
||||||
|
|
||||||
|
# UsewxWidgets sets the include dirs and compile definitions for targets defined
|
||||||
|
# afterwards in this directory — so it must come before essim_add_frontend().
|
||||||
|
include(${wxWidgets_USE_FILE})
|
||||||
|
|
||||||
|
essim_add_frontend(wx LIBS ${wxWidgets_LIBRARIES})
|
||||||
10
src/frontends/wx/main.cpp
Normal file
10
src/frontends/wx/main.cpp
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#include "frontends/frontend_main.hpp"
|
||||||
|
#include "frontends/wx/wx_frontend.hpp"
|
||||||
|
|
||||||
|
// The wx frontend's entry point: construct the concrete Frontend (WxFrontend)
|
||||||
|
// and hand off to the shared, frontend-agnostic launcher. Identical in shape to
|
||||||
|
// the tui frontend's main — only the Frontend type differs.
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
WxFrontend fe;
|
||||||
|
return frontend_main(argc, argv, fe);
|
||||||
|
}
|
||||||
680
src/frontends/wx/wx_frame.cpp
Normal file
680
src/frontends/wx/wx_frame.cpp
Normal file
@@ -0,0 +1,680 @@
|
|||||||
|
#include "frontends/wx/wx_frame.hpp"
|
||||||
|
|
||||||
|
#include "frontends/wx/wx_frontend.hpp"
|
||||||
|
|
||||||
|
#include "core/app/connect.hpp"
|
||||||
|
#include "core/app/edit.hpp"
|
||||||
|
#include "core/app/export.hpp"
|
||||||
|
#include "core/app/load.hpp"
|
||||||
|
#include "core/app/script.hpp"
|
||||||
|
#include "core/app/verify.hpp"
|
||||||
|
#include "core/domain/analysis.hpp"
|
||||||
|
#include "core/domain/connect.hpp"
|
||||||
|
#include "core/domain/modules.hpp"
|
||||||
|
#include "core/domain/parts.hpp"
|
||||||
|
#include "core/domain/persist.hpp"
|
||||||
|
#include "core/domain/pins.hpp"
|
||||||
|
#include "core/domain/signal_type.hpp"
|
||||||
|
#include "core/domain/signals.hpp"
|
||||||
|
#include "core/domain/system.hpp"
|
||||||
|
|
||||||
|
#include <wx/wx.h>
|
||||||
|
#include <wx/choicdlg.h>
|
||||||
|
#include <wx/filedlg.h>
|
||||||
|
#include <wx/filename.h>
|
||||||
|
#include <wx/textdlg.h>
|
||||||
|
#include <wx/treectrl.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
enum {
|
||||||
|
ID_LOAD = wxID_HIGHEST + 1,
|
||||||
|
ID_RESTORE,
|
||||||
|
ID_RUN_SCRIPT,
|
||||||
|
ID_SAVE,
|
||||||
|
ID_EXPORT,
|
||||||
|
ID_SET_CONNECTOR_TYPE,
|
||||||
|
ID_ATTACH_BSDL,
|
||||||
|
ID_SET_SIGNAL_TYPE,
|
||||||
|
ID_CONNECT,
|
||||||
|
ID_DUPLICATE,
|
||||||
|
ID_VERIFY,
|
||||||
|
ID_QUIT,
|
||||||
|
ID_ABOUT,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Core (UTF-8 std::string) -> wxString, and back for paths.
|
||||||
|
inline wxString wx(const std::string &s) { return wxString::FromUTF8(s.c_str()); }
|
||||||
|
|
||||||
|
// Natural order ("2" < "10", "A2" < "A10") so pin/part lists read intuitively.
|
||||||
|
bool natural_less(const std::string &a, const std::string &b) {
|
||||||
|
size_t i = 0, j = 0;
|
||||||
|
while (i < a.size() && j < b.size()) {
|
||||||
|
unsigned char ca = a[i], cb = b[j];
|
||||||
|
if (std::isdigit(ca) && std::isdigit(cb)) {
|
||||||
|
size_t i0 = i, j0 = j;
|
||||||
|
while (i < a.size() && std::isdigit((unsigned char)a[i])) ++i;
|
||||||
|
while (j < b.size() && std::isdigit((unsigned char)b[j])) ++j;
|
||||||
|
std::string na = a.substr(i0, i - i0), nb = b.substr(j0, j - j0);
|
||||||
|
na.erase(0, na.find_first_not_of('0')); // ignore leading zeros
|
||||||
|
nb.erase(0, nb.find_first_not_of('0'));
|
||||||
|
if (na.size() != nb.size()) return na.size() < nb.size();
|
||||||
|
if (na != nb) return na < nb;
|
||||||
|
} else {
|
||||||
|
if (ca != cb) return ca < cb;
|
||||||
|
++i; ++j;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a.size() < b.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
// " (Power)" / " (Gnd)" — only for the meaningful types; "" for Other.
|
||||||
|
wxString type_suffix(SignalType t) {
|
||||||
|
return t == SignalType::Other ? wxString()
|
||||||
|
: " (" + wxString(signal_type_name(t)) + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
// What a tree node stands for, attached to the item so a selection or a
|
||||||
|
// right-click can drive the edit operations on the right domain object.
|
||||||
|
struct NodeData : public wxTreeItemData {
|
||||||
|
enum class Kind { Other, Module, Part, Pin, Signal };
|
||||||
|
Kind kind;
|
||||||
|
Module *module = nullptr;
|
||||||
|
Part *part = nullptr;
|
||||||
|
Signal *signal = nullptr;
|
||||||
|
explicit NodeData(Kind k) : kind(k) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
NodeData *node_of(wxTreeCtrl *tree, const wxTreeItemId &id) {
|
||||||
|
return id.IsOk() ? static_cast<NodeData *>(tree->GetItemData(id)) : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The part of the current selection — a Part node, or the Pin's owning part.
|
||||||
|
Part *selected_part(wxTreeCtrl *tree) {
|
||||||
|
NodeData *d = node_of(tree, tree->GetSelection());
|
||||||
|
if (d && (d->kind == NodeData::Kind::Part || d->kind == NodeData::Kind::Pin))
|
||||||
|
return d->part;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The signal of the current selection (and, via `mod`, its module).
|
||||||
|
Signal *selected_signal(wxTreeCtrl *tree, Module **mod) {
|
||||||
|
NodeData *d = node_of(tree, tree->GetSelection());
|
||||||
|
if (d && d->kind == NodeData::Kind::Signal) {
|
||||||
|
if (mod) *mod = d->module;
|
||||||
|
return d->signal;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
EssimFrame::EssimFrame(WxFrontend &fe)
|
||||||
|
: wxFrame(nullptr, wxID_ANY, "essim — system digital twin",
|
||||||
|
wxDefaultPosition, wxSize(960, 640)),
|
||||||
|
fe_(fe) {
|
||||||
|
auto *file = new wxMenu;
|
||||||
|
file->Append(ID_LOAD, "&Load module…\tCtrl-L");
|
||||||
|
file->Append(ID_RESTORE, "&Restore snapshot…\tCtrl-R");
|
||||||
|
file->Append(ID_RUN_SCRIPT, "&Run script…\tCtrl-U");
|
||||||
|
file->Append(ID_SAVE, "&Save snapshot…\tCtrl-S");
|
||||||
|
file->AppendSeparator();
|
||||||
|
file->Append(ID_EXPORT, "&Export connections…\tCtrl-E");
|
||||||
|
file->AppendSeparator();
|
||||||
|
file->Append(ID_QUIT, "&Quit\tCtrl-Q");
|
||||||
|
|
||||||
|
auto *edit = new wxMenu;
|
||||||
|
edit->Append(ID_SET_CONNECTOR_TYPE, "Set &connector type…\tCtrl-T");
|
||||||
|
edit->Append(ID_ATTACH_BSDL, "Attach &BSDL…\tCtrl-B");
|
||||||
|
edit->Append(ID_SET_SIGNAL_TYPE, "Set &signal type…\tCtrl-G");
|
||||||
|
edit->AppendSeparator();
|
||||||
|
edit->Append(ID_CONNECT, "C&onnect parts…\tCtrl-O");
|
||||||
|
edit->AppendSeparator();
|
||||||
|
edit->Append(ID_DUPLICATE, "&Duplicate module…\tCtrl-D");
|
||||||
|
|
||||||
|
auto *sysm = new wxMenu;
|
||||||
|
sysm->Append(ID_VERIFY, "&Verify\tCtrl-K");
|
||||||
|
|
||||||
|
auto *help = new wxMenu;
|
||||||
|
help->Append(ID_ABOUT, "&About");
|
||||||
|
|
||||||
|
auto *bar = new wxMenuBar;
|
||||||
|
bar->Append(file, "&File");
|
||||||
|
bar->Append(edit, "&Edit");
|
||||||
|
bar->Append(sysm, "&System");
|
||||||
|
bar->Append(help, "&Help");
|
||||||
|
SetMenuBar(bar);
|
||||||
|
|
||||||
|
CreateStatusBar();
|
||||||
|
SetStatusText("essim — wx frontend");
|
||||||
|
|
||||||
|
auto *panel = new wxPanel(this);
|
||||||
|
tree_ = new wxTreeCtrl(panel, wxID_ANY);
|
||||||
|
overview_ = new wxTextCtrl(panel, wxID_ANY, "", wxDefaultPosition,
|
||||||
|
wxDefaultSize, wxTE_MULTILINE | wxTE_READONLY);
|
||||||
|
log_ = new wxTextCtrl(panel, wxID_ANY, "", wxDefaultPosition,
|
||||||
|
wxDefaultSize, wxTE_MULTILINE | wxTE_READONLY);
|
||||||
|
|
||||||
|
wxFont mono(wxFontInfo().Family(wxFONTFAMILY_TELETYPE));
|
||||||
|
overview_->SetFont(mono);
|
||||||
|
log_->SetFont(mono);
|
||||||
|
|
||||||
|
// Cap each control's minimum so its *content* can't inflate the layout's
|
||||||
|
// minimum size: on GTK a full tree/text reports a large natural size, which
|
||||||
|
// would otherwise eat all the vertical space and freeze the log at its
|
||||||
|
// minimum (it stopped resizing once a script populated the tree). With a
|
||||||
|
// modest min, the sizer proportions govern and content scrolls inside.
|
||||||
|
tree_->SetMinSize(wxSize(260, 120));
|
||||||
|
overview_->SetMinSize(wxSize(260, 120));
|
||||||
|
log_->SetMinSize(wxSize(420, 90));
|
||||||
|
|
||||||
|
auto *top = new wxBoxSizer(wxHORIZONTAL);
|
||||||
|
top->Add(tree_, 1, wxEXPAND | wxALL, 4);
|
||||||
|
top->Add(overview_, 1, wxEXPAND | wxALL, 4);
|
||||||
|
|
||||||
|
auto *root = new wxBoxSizer(wxVERTICAL);
|
||||||
|
root->Add(top, 2, wxEXPAND);
|
||||||
|
root->Add(new wxStaticText(panel, wxID_ANY, " Log"), 0, wxLEFT | wxTOP, 6);
|
||||||
|
root->Add(log_, 1, wxEXPAND | wxALL, 4);
|
||||||
|
panel->SetSizer(root);
|
||||||
|
|
||||||
|
// Drive the panel from a frame sizer so it fills the client area and
|
||||||
|
// re-lays-out on every resize (the implicit single-child fill is not
|
||||||
|
// reliable here — without this the log keeps its size when the window grows).
|
||||||
|
auto *frame_sizer = new wxBoxSizer(wxVERTICAL);
|
||||||
|
frame_sizer->Add(panel, 1, wxEXPAND);
|
||||||
|
SetSizer(frame_sizer);
|
||||||
|
|
||||||
|
Bind(wxEVT_MENU, &EssimFrame::OnLoad, this, ID_LOAD);
|
||||||
|
Bind(wxEVT_MENU, &EssimFrame::OnRestore, this, ID_RESTORE);
|
||||||
|
Bind(wxEVT_MENU, &EssimFrame::OnSave, this, ID_SAVE);
|
||||||
|
Bind(wxEVT_MENU, &EssimFrame::OnRunScript, this, ID_RUN_SCRIPT);
|
||||||
|
Bind(wxEVT_MENU, &EssimFrame::OnExport, this, ID_EXPORT);
|
||||||
|
Bind(wxEVT_MENU, &EssimFrame::OnSetConnectorType, this, ID_SET_CONNECTOR_TYPE);
|
||||||
|
Bind(wxEVT_MENU, &EssimFrame::OnAttachBsdl, this, ID_ATTACH_BSDL);
|
||||||
|
Bind(wxEVT_MENU, &EssimFrame::OnSetSignalType, this, ID_SET_SIGNAL_TYPE);
|
||||||
|
Bind(wxEVT_MENU, &EssimFrame::OnConnect, this, ID_CONNECT);
|
||||||
|
Bind(wxEVT_MENU, &EssimFrame::OnDuplicateModule, this, ID_DUPLICATE);
|
||||||
|
Bind(wxEVT_MENU, &EssimFrame::OnVerify, this, ID_VERIFY);
|
||||||
|
Bind(wxEVT_MENU, &EssimFrame::OnQuit, this, ID_QUIT);
|
||||||
|
Bind(wxEVT_MENU, &EssimFrame::OnAbout, this, ID_ABOUT);
|
||||||
|
|
||||||
|
tree_->Bind(wxEVT_TREE_ITEM_MENU, &EssimFrame::OnTreeContextMenu, this);
|
||||||
|
|
||||||
|
RebuildModelView();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EssimFrame::Log(const wxString &line) {
|
||||||
|
log_->AppendText(line + "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
void EssimFrame::RebuildModelView() {
|
||||||
|
System *sys = fe_.system();
|
||||||
|
|
||||||
|
tree_->DeleteAllItems();
|
||||||
|
wxTreeItemId root = tree_->AddRoot("System");
|
||||||
|
|
||||||
|
int n_mods = 0, n_parts = 0, n_sigs = 0;
|
||||||
|
if (sys) {
|
||||||
|
std::vector<std::string> mods;
|
||||||
|
for (auto &mkv : *sys->modules()) mods.push_back(mkv.first);
|
||||||
|
std::sort(mods.begin(), mods.end());
|
||||||
|
n_mods = (int)mods.size();
|
||||||
|
for (const auto &mname : mods) {
|
||||||
|
Module *m = sys->modules()->get(mname);
|
||||||
|
int mp = (int)m->size();
|
||||||
|
int ms = (int)m->signals->size();
|
||||||
|
n_parts += mp;
|
||||||
|
n_sigs += ms;
|
||||||
|
wxTreeItemId mid = tree_->AppendItem(
|
||||||
|
root, wx(mname) + wxString::Format(" — %d part(s), %d signal(s)",
|
||||||
|
mp, ms));
|
||||||
|
{
|
||||||
|
auto *d = new NodeData(NodeData::Kind::Module);
|
||||||
|
d->module = m;
|
||||||
|
tree_->SetItemData(mid, d);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parts → pins (each pin shows the signal it is wired to, or NC).
|
||||||
|
std::vector<std::string> parts;
|
||||||
|
for (auto &pkv : *m) parts.push_back(pkv.first);
|
||||||
|
std::sort(parts.begin(), parts.end(), natural_less);
|
||||||
|
for (const auto &pname : parts) {
|
||||||
|
Part *p = m->get(pname);
|
||||||
|
wxString label = wx(pname)
|
||||||
|
+ wxString::Format(" (%d pin(s))", (int)p->size());
|
||||||
|
if (!p->connector_type.empty())
|
||||||
|
label += " [" + wx(p->connector_type) + "]";
|
||||||
|
wxTreeItemId pid = tree_->AppendItem(mid, label);
|
||||||
|
{
|
||||||
|
auto *d = new NodeData(NodeData::Kind::Part);
|
||||||
|
d->module = m;
|
||||||
|
d->part = p;
|
||||||
|
tree_->SetItemData(pid, d);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> pins;
|
||||||
|
for (auto &nkv : *p) pins.push_back(nkv.first);
|
||||||
|
std::sort(pins.begin(), pins.end(), natural_less);
|
||||||
|
for (const auto &pinname : pins) {
|
||||||
|
Pin *pin = p->get(pinname);
|
||||||
|
wxString pl = wx(pinname) + " -> ";
|
||||||
|
if (Signal *s = pin->signal()) {
|
||||||
|
pl += wx(s->name) + type_suffix(s->type);
|
||||||
|
} else {
|
||||||
|
pl += "(NC";
|
||||||
|
if (pin->nc_origin == NcOrigin::ImportedUnconnected)
|
||||||
|
pl += ", imported";
|
||||||
|
else if (pin->nc_origin == NcOrigin::DroppedSingleton)
|
||||||
|
pl += ", dropped";
|
||||||
|
pl += ")";
|
||||||
|
}
|
||||||
|
wxTreeItemId nid = tree_->AppendItem(pid, pl);
|
||||||
|
auto *d = new NodeData(NodeData::Kind::Pin);
|
||||||
|
d->module = m;
|
||||||
|
d->part = p;
|
||||||
|
tree_->SetItemData(nid, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signals branch (the per-module net view: type + fan-out).
|
||||||
|
if (ms > 0) {
|
||||||
|
wxTreeItemId sid =
|
||||||
|
tree_->AppendItem(mid, wxString::Format("Signals (%d)", ms));
|
||||||
|
std::vector<std::string> sigs;
|
||||||
|
for (auto &skv : *m->signals) sigs.push_back(skv.first);
|
||||||
|
std::sort(sigs.begin(), sigs.end(), natural_less);
|
||||||
|
for (const auto &sname : sigs) {
|
||||||
|
Signal *s = m->signals->get(sname);
|
||||||
|
wxTreeItemId nid = tree_->AppendItem(
|
||||||
|
sid, wx(sname) + type_suffix(s->type)
|
||||||
|
+ wxString::Format(" — %d pin(s)", (int)s->size()));
|
||||||
|
auto *d = new NodeData(NodeData::Kind::Signal);
|
||||||
|
d->module = m;
|
||||||
|
d->signal = s;
|
||||||
|
tree_->SetItemData(nid, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tree_->Expand(mid); // parts + Signals visible; pins/nets on demand
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tree_->Expand(root);
|
||||||
|
|
||||||
|
int n_conn = sys ? (int)sys->connections()->size() : 0;
|
||||||
|
|
||||||
|
wxString ov;
|
||||||
|
ov << "Modules: " << n_mods << "\n"
|
||||||
|
<< "Parts: " << n_parts << "\n"
|
||||||
|
<< "Signals: " << n_sigs << "\n"
|
||||||
|
<< "Connections: " << n_conn << "\n";
|
||||||
|
if (sys) {
|
||||||
|
app::VerifyReport r = app::verify(sys);
|
||||||
|
ov << "\nHealth (verify):\n"
|
||||||
|
<< wxString::Format(" pin-role mismatches: %d / %d typed pin(s)\n",
|
||||||
|
(int)r.role_mismatches.size(), r.typed_pins)
|
||||||
|
<< wxString::Format(" net inconsistencies: %d / %d bridged net(s)\n",
|
||||||
|
(int)r.net_inconsistencies.size(), r.bridged_nets)
|
||||||
|
<< wxString::Format(" orphan pins: %d (%d imported, %d dropped)\n",
|
||||||
|
r.orphan_total(), r.orphan_imported, r.orphan_dropped)
|
||||||
|
<< wxString::Format(" model anomalies: %d\n", r.model_total());
|
||||||
|
}
|
||||||
|
overview_->SetValue(ov);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EssimFrame::OnLoad(wxCommandEvent &) {
|
||||||
|
wxFileDialog dlg(this, "Load a netlist / pinout file", "", "",
|
||||||
|
"All files (*.*)|*.*", wxFD_OPEN | wxFD_FILE_MUST_EXIST);
|
||||||
|
if (dlg.ShowModal() != wxID_OK) return;
|
||||||
|
const wxString path = dlg.GetPath();
|
||||||
|
|
||||||
|
wxString modname = wxGetTextFromUser("Module name:", "Load module",
|
||||||
|
wxFileName(path).GetName(), this);
|
||||||
|
if (modname.empty()) return;
|
||||||
|
|
||||||
|
static const wxString kinds[] = {"mentor", "altium", "ods"};
|
||||||
|
int ki = wxGetSingleChoiceIndex("Import type:", "Load module",
|
||||||
|
WXSIZEOF(kinds), kinds, this);
|
||||||
|
if (ki < 0) return;
|
||||||
|
|
||||||
|
ImportType type;
|
||||||
|
app::import_type_from_name(kinds[ki].ToStdString(), type); // choice is valid
|
||||||
|
app::LoadResult r = app::load_module(
|
||||||
|
fe_.system(), modname.utf8_string(), path.utf8_string(), type);
|
||||||
|
if (!r.ok) {
|
||||||
|
Log("load failed: " + wx(r.error));
|
||||||
|
wxMessageBox(wx(r.error), "Load failed", wxOK | wxICON_ERROR, this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Log(wxString::Format(
|
||||||
|
"loaded '%s' from %s — %d part(s), %d signal(s)"
|
||||||
|
" (dropped %d; types: %d power / %d gnd / %d suspect / %d pwr-mgmt)",
|
||||||
|
modname, path, r.parts, r.signals, r.dropped, r.power, r.gnd,
|
||||||
|
r.kept_other, r.mgmt));
|
||||||
|
RebuildModelView();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EssimFrame::OnRestore(wxCommandEvent &) {
|
||||||
|
wxFileDialog dlg(this, "Restore a system snapshot", "", "",
|
||||||
|
"essim snapshots (*.essim)|*.essim|All files (*.*)|*.*",
|
||||||
|
wxFD_OPEN | wxFD_FILE_MUST_EXIST);
|
||||||
|
if (dlg.ShowModal() != wxID_OK) return;
|
||||||
|
std::string err;
|
||||||
|
System *fresh = restore_system(dlg.GetPath().utf8_string(), err);
|
||||||
|
if (!fresh) {
|
||||||
|
Log("restore failed: " + wx(err));
|
||||||
|
wxMessageBox(wx(err), "Restore failed", wxOK | wxICON_ERROR, this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fe_.set_system(fresh);
|
||||||
|
Log("restored from " + dlg.GetPath());
|
||||||
|
RebuildModelView();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EssimFrame::OnRunScript(wxCommandEvent &) {
|
||||||
|
wxFileDialog dlg(this, "Run an essim script", "", "",
|
||||||
|
"essim scripts (*.essim)|*.essim|All files (*.*)|*.*",
|
||||||
|
wxFD_OPEN | wxFD_FILE_MUST_EXIST);
|
||||||
|
if (dlg.ShowModal() != wxID_OK) return;
|
||||||
|
|
||||||
|
fe_.ensure_system();
|
||||||
|
std::ostringstream out;
|
||||||
|
app::ScriptResult r =
|
||||||
|
app::run_script(fe_.system_ptr(), dlg.GetPath().utf8_string(), out);
|
||||||
|
|
||||||
|
if (!r.ok) {
|
||||||
|
Log("run script: " + wx(r.error));
|
||||||
|
wxMessageBox(wx(r.error), "Run script", wxOK | wxICON_ERROR, this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Echo each line of the script's output into the log pane.
|
||||||
|
std::istringstream lines(out.str());
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(lines, line)) Log(wx(line));
|
||||||
|
Log(wxString::Format("source: %s (%d line(s), %d error(s))",
|
||||||
|
dlg.GetPath(), r.lines, r.errors));
|
||||||
|
RebuildModelView();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EssimFrame::OnSave(wxCommandEvent &) {
|
||||||
|
wxFileDialog dlg(this, "Save system snapshot", "", "system.essim",
|
||||||
|
"essim snapshots (*.essim)|*.essim|All files (*.*)|*.*",
|
||||||
|
wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
|
||||||
|
if (dlg.ShowModal() != wxID_OK) return;
|
||||||
|
std::string err;
|
||||||
|
if (save_system(fe_.system(), dlg.GetPath().utf8_string(), err)) {
|
||||||
|
Log("saved to " + dlg.GetPath());
|
||||||
|
} else {
|
||||||
|
Log("save failed: " + wx(err));
|
||||||
|
wxMessageBox(wx(err), "Save failed", wxOK | wxICON_ERROR, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EssimFrame::OnExport(wxCommandEvent &) {
|
||||||
|
wxFileDialog dlg(this, "Export connections", "", "connections.csv",
|
||||||
|
"CSV (*.csv)|*.csv|OpenDocument sheet (*.ods)|*.ods",
|
||||||
|
wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
|
||||||
|
if (dlg.ShowModal() != wxID_OK) return;
|
||||||
|
const std::string path = dlg.GetPath().utf8_string();
|
||||||
|
|
||||||
|
app::ExportFormat fmt;
|
||||||
|
if (!app::export_format_from_path(path, fmt)) {
|
||||||
|
wxMessageBox("Unknown export extension (use .csv or .ods).",
|
||||||
|
"Export failed", wxOK | wxICON_ERROR, this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
app::ExportResult r = app::export_connections(fe_.system(), path, fmt);
|
||||||
|
if (r.ok) {
|
||||||
|
Log(wxString::Format("exported %d row(s) to %s", r.rows, dlg.GetPath()));
|
||||||
|
} else {
|
||||||
|
Log("export failed: " + wx(r.error));
|
||||||
|
wxMessageBox(wx(r.error), "Export failed", wxOK | wxICON_ERROR, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Module *EssimFrame::PickModule(const wxString &caption) {
|
||||||
|
System *sys = fe_.system();
|
||||||
|
if (!sys || sys->modules()->size() == 0) {
|
||||||
|
wxMessageBox("No modules loaded.", caption,
|
||||||
|
wxOK | wxICON_INFORMATION, this);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
std::vector<std::string> mods;
|
||||||
|
for (auto &mkv : *sys->modules()) mods.push_back(mkv.first);
|
||||||
|
std::sort(mods.begin(), mods.end());
|
||||||
|
wxArrayString choices;
|
||||||
|
for (const auto &m : mods) choices.Add(wx(m));
|
||||||
|
int mi = wxGetSingleChoiceIndex("Module:", caption, choices, this);
|
||||||
|
if (mi < 0) return nullptr;
|
||||||
|
return sys->modules()->get(mods[mi]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Part *EssimFrame::PickPart(const wxString &caption) {
|
||||||
|
Module *m = PickModule(caption);
|
||||||
|
if (!m) return nullptr;
|
||||||
|
if (m->size() == 0) {
|
||||||
|
wxMessageBox("That module has no parts.", caption,
|
||||||
|
wxOK | wxICON_INFORMATION, this);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
std::vector<std::string> parts;
|
||||||
|
for (auto &pkv : *m) parts.push_back(pkv.first);
|
||||||
|
std::sort(parts.begin(), parts.end());
|
||||||
|
wxArrayString choices;
|
||||||
|
for (const auto &p : parts) choices.Add(wx(p));
|
||||||
|
int pi = wxGetSingleChoiceIndex("Part:", caption, choices, this);
|
||||||
|
if (pi < 0) return nullptr;
|
||||||
|
return m->get(parts[pi]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void EssimFrame::OnSetConnectorType(wxCommandEvent &) {
|
||||||
|
Part *p = selected_part(tree_);
|
||||||
|
if (!p) p = PickPart();
|
||||||
|
if (!p) return;
|
||||||
|
|
||||||
|
wxTextEntryDialog dlg(this, "Connector type (empty = none):",
|
||||||
|
"Set connector type", wx(p->connector_type));
|
||||||
|
if (dlg.ShowModal() != wxID_OK) return;
|
||||||
|
const std::string kind = dlg.GetValue().utf8_string();
|
||||||
|
|
||||||
|
app::SetConnectorTypeResult r = app::set_connector_type(p, kind);
|
||||||
|
if (!r.ok) {
|
||||||
|
Log("set-connector-type refused: " + wx(r.error));
|
||||||
|
wxMessageBox(wx(r.error), "Refused", wxOK | wxICON_ERROR, this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wxString who = (p->prnt ? wx(p->prnt->name) + "/" : wxString()) + wx(p->name);
|
||||||
|
Log(who + ": connector_type = " + (kind.empty() ? wxString("(none)") : wx(kind)));
|
||||||
|
if (r.materialised > 0)
|
||||||
|
Log(wxString::Format(" added %d NC pin(s) from the connector layout",
|
||||||
|
r.materialised));
|
||||||
|
RebuildModelView();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EssimFrame::OnAttachBsdl(wxCommandEvent &) {
|
||||||
|
Part *p = selected_part(tree_);
|
||||||
|
if (!p) p = PickPart();
|
||||||
|
if (!p) return;
|
||||||
|
|
||||||
|
wxFileDialog dlg(this, "Attach a BSDL model", "", "",
|
||||||
|
"BSDL files (*.bsd)|*.bsd|All files (*.*)|*.*",
|
||||||
|
wxFD_OPEN | wxFD_FILE_MUST_EXIST);
|
||||||
|
if (dlg.ShowModal() != wxID_OK) return;
|
||||||
|
|
||||||
|
app::AttachBsdlResult r = app::attach_bsdl(p, dlg.GetPath().utf8_string());
|
||||||
|
if (!r.ok) {
|
||||||
|
Log("attach-bsdl: " + wx(r.error));
|
||||||
|
wxMessageBox(wx(r.error), "Attach BSDL failed", wxOK | wxICON_ERROR, this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wxString who = (p->prnt ? wx(p->prnt->name) + "/" : wxString()) + wx(p->name);
|
||||||
|
wxString tail = r.unbound ? wxString::Format(", %d unbound", r.unbound)
|
||||||
|
: wxString();
|
||||||
|
Log(wxString::Format("%s: attached BSDL '%s' — %d/%d ports bound%s",
|
||||||
|
who, wx(r.entity), r.bound, r.ports_total, tail));
|
||||||
|
RebuildModelView();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EssimFrame::OnConnect(wxCommandEvent &) {
|
||||||
|
Part *p1 = selected_part(tree_);
|
||||||
|
if (!p1) p1 = PickPart("Connect — first part");
|
||||||
|
if (!p1) return;
|
||||||
|
Part *p2 = PickPart("Connect — second part");
|
||||||
|
if (!p2) return;
|
||||||
|
if (p1 == p2) {
|
||||||
|
wxMessageBox("Pick two different parts.", "Connect",
|
||||||
|
wxOK | wxICON_INFORMATION, this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// m1/m2 are the parts' parent modules — connect_parts needs them for the
|
||||||
|
// Connection name and ownership.
|
||||||
|
app::ConnectResult r =
|
||||||
|
app::connect_parts(fe_.system(), p1->prnt, p1, p2->prnt, p2);
|
||||||
|
|
||||||
|
if (r.refused) {
|
||||||
|
Log("connect refused: " + wx(r.error));
|
||||||
|
wxMessageBox(wx(r.error), "Connect refused", wxOK | wxICON_ERROR, this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!r.identity_info.empty()) {
|
||||||
|
Log("connect: " + wx(r.identity_info));
|
||||||
|
if (r.nc_added > 0)
|
||||||
|
Log(wxString::Format("connect: added %d NC pin(s) so both sides match",
|
||||||
|
r.nc_added));
|
||||||
|
}
|
||||||
|
if (r.ok) {
|
||||||
|
Log(wxString::Format("connected: %s via %s (%d wires)",
|
||||||
|
wx(r.connection_name), wx(r.transform_name), r.wires));
|
||||||
|
} else {
|
||||||
|
Log("connect failed: " + wx(r.error));
|
||||||
|
wxMessageBox(wx(r.error), "Connect failed", wxOK | wxICON_ERROR, this);
|
||||||
|
}
|
||||||
|
RebuildModelView();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EssimFrame::OnSetSignalType(wxCommandEvent &) {
|
||||||
|
Module *m = nullptr;
|
||||||
|
Signal *sig = selected_signal(tree_, &m);
|
||||||
|
if (!sig) {
|
||||||
|
m = PickModule("Set signal type");
|
||||||
|
if (!m) return;
|
||||||
|
if (m->signals->size() == 0) {
|
||||||
|
wxMessageBox("That module has no signals.", "Set signal type",
|
||||||
|
wxOK | wxICON_INFORMATION, this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::vector<std::string> sigs;
|
||||||
|
for (auto &skv : *m->signals) sigs.push_back(skv.first);
|
||||||
|
std::sort(sigs.begin(), sigs.end(), natural_less);
|
||||||
|
wxArrayString schoices;
|
||||||
|
for (const auto &s : sigs) schoices.Add(wx(s));
|
||||||
|
int si = wxGetSingleChoiceIndex("Signal:", "Set signal type", schoices, this);
|
||||||
|
if (si < 0) return;
|
||||||
|
sig = m->signals->get(sigs[si]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const wxString types[] = {"power", "gnd", "other"};
|
||||||
|
int ti = wxGetSingleChoiceIndex("Type:", "Set signal type",
|
||||||
|
WXSIZEOF(types), types, this);
|
||||||
|
if (ti < 0) return;
|
||||||
|
|
||||||
|
app::SetSignalTypeResult r = app::set_signal_type(sig, types[ti].ToStdString());
|
||||||
|
if (!r.ok) {
|
||||||
|
Log(wx(r.error));
|
||||||
|
wxMessageBox(wx(r.error), "Set signal type", wxOK | wxICON_ERROR, this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Log(wxString::Format("%s/%s: signal type = %s", wx(m->name), wx(sig->name),
|
||||||
|
wx(signal_type_name(r.type))));
|
||||||
|
RebuildModelView();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EssimFrame::OnDuplicateModule(wxCommandEvent &) {
|
||||||
|
Module *m = PickModule("Duplicate module");
|
||||||
|
if (!m) return;
|
||||||
|
const std::string src = m->name; // m may move in the table after the add
|
||||||
|
|
||||||
|
wxTextEntryDialog dlg(this, "New module name:", "Duplicate module",
|
||||||
|
wx(src) + "_copy");
|
||||||
|
if (dlg.ShowModal() != wxID_OK) return;
|
||||||
|
const std::string dst = dlg.GetValue().utf8_string();
|
||||||
|
if (dst.empty()) return;
|
||||||
|
|
||||||
|
app::DuplicateResult r = app::duplicate_module(fe_.system(), src, dst);
|
||||||
|
if (!r.ok) {
|
||||||
|
Log(wx(r.error));
|
||||||
|
wxMessageBox(wx(r.error), "Duplicate module", wxOK | wxICON_ERROR, this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Log(wx("duplicate: '" + src + "' → '" + dst + "' ("
|
||||||
|
+ std::to_string(r.parts) + " part(s), "
|
||||||
|
+ std::to_string(r.signals) + " signal(s))"));
|
||||||
|
RebuildModelView();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EssimFrame::OnVerify(wxCommandEvent &) {
|
||||||
|
app::VerifyReport r = app::verify(fe_.system());
|
||||||
|
|
||||||
|
Log("verify:");
|
||||||
|
Log(wxString::Format(" %d pin-role mismatch(es) over %d typed pin(s)",
|
||||||
|
(int)r.role_mismatches.size(), r.typed_pins));
|
||||||
|
for (const auto &m : r.role_mismatches)
|
||||||
|
Log(wx(" " + m.module + "/" + m.part + "/" + m.pin + ": expected "
|
||||||
|
+ signal_type_name(m.expected) + ", got "
|
||||||
|
+ signal_type_name(m.actual)));
|
||||||
|
|
||||||
|
Log(wxString::Format(" %d inconsistent net(s) over %d bridged net(s)",
|
||||||
|
(int)r.net_inconsistencies.size(), r.bridged_nets));
|
||||||
|
Log(wxString::Format(" %d orphan pin(s) (%d imported, %d dropped)",
|
||||||
|
r.orphan_total(), r.orphan_imported, r.orphan_dropped));
|
||||||
|
|
||||||
|
auto log_anoms = [this](const std::vector<Anomaly> &v, const char *tail) {
|
||||||
|
Log(wxString::Format(" %d %s", (int)v.size(), tail));
|
||||||
|
for (const auto &a : v)
|
||||||
|
Log(wx(std::string(" [") + anomaly_kind_name(a.kind) + "] "
|
||||||
|
+ a.message));
|
||||||
|
};
|
||||||
|
log_anoms(r.pin_anomalies, "model-driven pin anomaly(ies)");
|
||||||
|
log_anoms(r.jtag_anomalies, "JTAG chain anomaly(ies)");
|
||||||
|
log_anoms(r.conflict_anomalies, "source-conflict(s)");
|
||||||
|
log_anoms(r.completeness_anomalies, "BSDL completeness issue(s)");
|
||||||
|
log_anoms(r.diff_anomalies, "diff-pair crossing anomaly(ies)");
|
||||||
|
|
||||||
|
RebuildModelView();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EssimFrame::OnQuit(wxCommandEvent &) { Close(true); }
|
||||||
|
|
||||||
|
void EssimFrame::OnAbout(wxCommandEvent &) {
|
||||||
|
wxMessageBox("essim — system digital twin\n\n"
|
||||||
|
"wxWidgets frontend over essim_core.",
|
||||||
|
"About essim", wxOK | wxICON_INFORMATION, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EssimFrame::OnTreeContextMenu(wxTreeEvent &ev) {
|
||||||
|
wxTreeItemId id = ev.GetItem();
|
||||||
|
if (id.IsOk()) tree_->SelectItem(id); // the edit handlers read the selection
|
||||||
|
NodeData *d = node_of(tree_, id);
|
||||||
|
if (!d) return;
|
||||||
|
|
||||||
|
// Reuse the menu IDs so these route to the same handlers, which now act on
|
||||||
|
// the (just-selected) tree item.
|
||||||
|
wxMenu menu;
|
||||||
|
if (d->kind == NodeData::Kind::Part || d->kind == NodeData::Kind::Pin) {
|
||||||
|
menu.Append(ID_SET_CONNECTOR_TYPE, "Set connector type…");
|
||||||
|
menu.Append(ID_ATTACH_BSDL, "Attach BSDL…");
|
||||||
|
menu.Append(ID_CONNECT, "Connect to…");
|
||||||
|
} else if (d->kind == NodeData::Kind::Signal) {
|
||||||
|
menu.Append(ID_SET_SIGNAL_TYPE, "Set signal type…");
|
||||||
|
}
|
||||||
|
if (menu.GetMenuItemCount() > 0) PopupMenu(&menu);
|
||||||
|
}
|
||||||
56
src/frontends/wx/wx_frame.hpp
Normal file
56
src/frontends/wx/wx_frame.hpp
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
#ifndef _WX_FRAME_HPP_
|
||||||
|
#define _WX_FRAME_HPP_
|
||||||
|
|
||||||
|
#include <wx/frame.h>
|
||||||
|
|
||||||
|
class WxFrontend;
|
||||||
|
class wxTreeCtrl;
|
||||||
|
class wxTextCtrl;
|
||||||
|
class wxCommandEvent;
|
||||||
|
class wxTreeEvent;
|
||||||
|
|
||||||
|
// The essim main window. Holds no domain state of its own: it reads and mutates
|
||||||
|
// the System owned by the WxFrontend, calling the core/app operations directly
|
||||||
|
// (load, verify, export, save, restore) and rendering their results into a
|
||||||
|
// model tree, an overview panel and a log.
|
||||||
|
class EssimFrame : public wxFrame {
|
||||||
|
public:
|
||||||
|
explicit EssimFrame(WxFrontend &fe);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Menu handlers — each is a thin wrapper over a core/app operation.
|
||||||
|
void OnLoad(wxCommandEvent &);
|
||||||
|
void OnRestore(wxCommandEvent &);
|
||||||
|
void OnRunScript(wxCommandEvent &);
|
||||||
|
void OnSave(wxCommandEvent &);
|
||||||
|
void OnExport(wxCommandEvent &);
|
||||||
|
void OnSetConnectorType(wxCommandEvent &);
|
||||||
|
void OnAttachBsdl(wxCommandEvent &);
|
||||||
|
void OnConnect(wxCommandEvent &);
|
||||||
|
void OnSetSignalType(wxCommandEvent &);
|
||||||
|
void OnDuplicateModule(wxCommandEvent &);
|
||||||
|
void OnVerify(wxCommandEvent &);
|
||||||
|
void OnQuit(wxCommandEvent &);
|
||||||
|
void OnAbout(wxCommandEvent &);
|
||||||
|
|
||||||
|
// Right-click on a tree item → context menu of the edit actions valid for
|
||||||
|
// that node (part / signal). The actions reuse the menu IDs, so they run
|
||||||
|
// the same handlers — which read the tree selection.
|
||||||
|
void OnTreeContextMenu(wxTreeEvent &);
|
||||||
|
|
||||||
|
// Modal pickers over the current System. `caption` titles the dialogs (e.g.
|
||||||
|
// to distinguish two picks). Each returns nullptr if there is nothing to
|
||||||
|
// pick or the user cancels.
|
||||||
|
class Module *PickModule(const wxString &caption);
|
||||||
|
class Part *PickPart(const wxString &caption = "Select part");
|
||||||
|
|
||||||
|
void RebuildModelView(); ///< refresh tree + overview from the System
|
||||||
|
void Log(const wxString &line); ///< append a line to the log pane
|
||||||
|
|
||||||
|
WxFrontend &fe_;
|
||||||
|
wxTreeCtrl *tree_ = nullptr;
|
||||||
|
wxTextCtrl *overview_ = nullptr;
|
||||||
|
wxTextCtrl *log_ = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // _WX_FRAME_HPP_
|
||||||
113
src/frontends/wx/wx_frontend.cpp
Normal file
113
src/frontends/wx/wx_frontend.cpp
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
#include "frontends/wx/wx_frontend.hpp"
|
||||||
|
|
||||||
|
#include "frontends/wx/wx_frame.hpp"
|
||||||
|
|
||||||
|
#include "core/app/script.hpp"
|
||||||
|
#include "core/domain/connect.hpp"
|
||||||
|
#include "core/domain/modules.hpp"
|
||||||
|
#include "core/domain/persist.hpp"
|
||||||
|
#include "core/domain/system.hpp"
|
||||||
|
|
||||||
|
#include <wx/app.h>
|
||||||
|
#include <wx/init.h>
|
||||||
|
|
||||||
|
#include <clocale>
|
||||||
|
#include <ostream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Minimal wxApp: on init it shows the main window bound to the frontend.
|
||||||
|
class EssimApp : public wxApp {
|
||||||
|
public:
|
||||||
|
explicit EssimApp(WxFrontend &fe) : fe_(fe) {}
|
||||||
|
bool OnInit() override {
|
||||||
|
// Decode the UTF-8 in our narrow string literals (em dash, ellipsis…)
|
||||||
|
// correctly: wxString converts const char* via the C locale, which is
|
||||||
|
// "C" (ASCII) at startup. Set only LC_CTYPE — leave LC_NUMERIC as "C"
|
||||||
|
// so number formatting stays dot-decimal.
|
||||||
|
std::setlocale(LC_CTYPE, "");
|
||||||
|
(new EssimFrame(fe_))->Show(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
WxFrontend &fe_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
WxFrontend::WxFrontend() = default;
|
||||||
|
WxFrontend::~WxFrontend() = default;
|
||||||
|
|
||||||
|
void WxFrontend::ensure_system() {
|
||||||
|
if (!sys_) sys_.reset(new System());
|
||||||
|
}
|
||||||
|
|
||||||
|
void WxFrontend::set_system(System *fresh) {
|
||||||
|
sys_.reset(fresh);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WxFrontend::BootDispatch(const std::string &raw) {
|
||||||
|
// The GUI has no command shell. Honour the boot commands that make sense
|
||||||
|
// headlessly: `restore <file>` seeds a snapshot; anything else is noted.
|
||||||
|
std::istringstream iss(raw);
|
||||||
|
std::string cmd;
|
||||||
|
iss >> cmd;
|
||||||
|
std::string arg;
|
||||||
|
std::getline(iss, arg);
|
||||||
|
if (std::size_t b = arg.find_first_not_of(" \t"); b != std::string::npos)
|
||||||
|
arg = arg.substr(b);
|
||||||
|
else
|
||||||
|
arg.clear();
|
||||||
|
|
||||||
|
if (cmd == "restore") {
|
||||||
|
std::string err;
|
||||||
|
System *fresh = restore_system(arg, err);
|
||||||
|
if (!fresh) {
|
||||||
|
output_ += "restore failed: " + err + "\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sys_.reset(fresh);
|
||||||
|
output_ += "restored from " + arg + " ("
|
||||||
|
+ std::to_string(sys_->modules()->size()) + " module(s), "
|
||||||
|
+ std::to_string(sys_->connections()->size())
|
||||||
|
+ " connection(s))\n";
|
||||||
|
} else if (cmd == "source") {
|
||||||
|
ensure_system();
|
||||||
|
std::ostringstream out;
|
||||||
|
app::ScriptResult r = app::run_script(sys_, arg, out);
|
||||||
|
output_ += out.str();
|
||||||
|
if (!r.ok)
|
||||||
|
output_ += "source: " + r.error + "\n";
|
||||||
|
else
|
||||||
|
output_ += "source: " + arg + " (" + std::to_string(r.lines)
|
||||||
|
+ " line(s), " + std::to_string(r.errors) + " error(s))\n";
|
||||||
|
} else if (!cmd.empty()) {
|
||||||
|
output_ += "boot: ignored '" + raw + "'.\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WxFrontend::DumpCommandsMd(std::ostream &out) const {
|
||||||
|
out << "# essim — wx frontend\n\n"
|
||||||
|
<< "The wx frontend is menu-driven and exposes no textual command "
|
||||||
|
<< "registry. Generate the command reference from the tui frontend "
|
||||||
|
<< "(`-DESSIM_FRONTEND=tui`, then `essim --commands-md`).\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
void WxFrontend::DumpOutput(std::ostream &out) const {
|
||||||
|
out << output_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WxFrontend::Run() {
|
||||||
|
ensure_system();
|
||||||
|
|
||||||
|
wxApp::SetInstance(new EssimApp(*this));
|
||||||
|
int argc = 0;
|
||||||
|
wxEntryStart(argc, static_cast<char **>(nullptr));
|
||||||
|
if (wxTheApp->CallOnInit())
|
||||||
|
wxTheApp->OnRun();
|
||||||
|
wxTheApp->OnExit();
|
||||||
|
wxEntryCleanup();
|
||||||
|
}
|
||||||
38
src/frontends/wx/wx_frontend.hpp
Normal file
38
src/frontends/wx/wx_frontend.hpp
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#ifndef _WX_FRONTEND_HPP_
|
||||||
|
#define _WX_FRONTEND_HPP_
|
||||||
|
|
||||||
|
#include "frontends/frontend.hpp"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class System;
|
||||||
|
|
||||||
|
// wxWidgets GUI frontend. Implements the shared Frontend interface so the same
|
||||||
|
// launcher (frontend_main) drives it: it owns the System and a console buffer,
|
||||||
|
// handles boot commands headlessly (for --restore/--batch), and Run() opens the
|
||||||
|
// wxWidgets window. The window itself (EssimFrame) drives essim_core / app::*
|
||||||
|
// operations directly — no command shell, no TUI reuse.
|
||||||
|
class WxFrontend : public Frontend {
|
||||||
|
public:
|
||||||
|
WxFrontend();
|
||||||
|
~WxFrontend() override;
|
||||||
|
|
||||||
|
// --- Frontend interface ---
|
||||||
|
void BootDispatch(const std::string &raw) override;
|
||||||
|
void DumpCommandsMd(std::ostream &out) const override;
|
||||||
|
void DumpOutput(std::ostream &out) const override;
|
||||||
|
void Run() override;
|
||||||
|
|
||||||
|
// --- used by the window (EssimFrame) ---
|
||||||
|
System *system() const { return sys_.get(); }
|
||||||
|
std::unique_ptr<System> &system_ptr() { return sys_; } ///< for run_script (new/restore replace it)
|
||||||
|
void set_system(System *fresh); ///< take ownership (used by Restore)
|
||||||
|
void ensure_system(); ///< create an empty System if none yet
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unique_ptr<System> sys_;
|
||||||
|
std::string output_; ///< console buffer surfaced by DumpOutput (batch)
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // _WX_FRONTEND_HPP_
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
#ifndef _SIGNAL_TYPE_HPP_
|
|
||||||
#define _SIGNAL_TYPE_HPP_
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
enum class SignalType { Power, GndShield, Other };
|
|
||||||
|
|
||||||
const char *signal_type_name(SignalType t);
|
|
||||||
bool signal_type_from_name(const std::string &s, SignalType &out);
|
|
||||||
SignalType infer_signal_type(const std::string &signal_name);
|
|
||||||
SignalType next_signal_type(SignalType t); // Power → GndShield → Other → Power
|
|
||||||
|
|
||||||
#endif // _SIGNAL_TYPE_HPP_
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
|
|
||||||
#include "signals.hpp"
|
|
||||||
#include "parts.hpp"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cctype>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
const char *signal_type_name(SignalType t) {
|
|
||||||
switch (t) {
|
|
||||||
case SignalType::Power: return "power";
|
|
||||||
case SignalType::GndShield: return "gnd";
|
|
||||||
case SignalType::Other: return "other";
|
|
||||||
}
|
|
||||||
return "other";
|
|
||||||
}
|
|
||||||
|
|
||||||
SignalType next_signal_type(SignalType t) {
|
|
||||||
switch (t) {
|
|
||||||
case SignalType::Power: return SignalType::GndShield;
|
|
||||||
case SignalType::GndShield: return SignalType::Other;
|
|
||||||
case SignalType::Other: return SignalType::Power;
|
|
||||||
}
|
|
||||||
return SignalType::Other;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool signal_type_from_name(const std::string &s, SignalType &out) {
|
|
||||||
std::string l = s;
|
|
||||||
std::transform(l.begin(), l.end(), l.begin(),
|
|
||||||
[](unsigned char c) { return std::tolower(c); });
|
|
||||||
if (l == "power" || l == "p") { out = SignalType::Power; return true; }
|
|
||||||
if (l == "gnd" || l == "g" || l == "shield" || l == "ground")
|
|
||||||
{ out = SignalType::GndShield; return true; }
|
|
||||||
if (l == "other" || l == "o" || l == "signal")
|
|
||||||
{ out = SignalType::Other; return true; }
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Heuristic. Names like GND, GNDA, SHIELD, CHASSIS → GndShield.
|
|
||||||
// Names containing PWR/POWER/VCC/VDD/VEE/VSS, or matching V±N or +N.NV
|
|
||||||
// patterns, or starting with VS_/VS3_ → Power. Else Other.
|
|
||||||
SignalType infer_signal_type(const std::string &name) {
|
|
||||||
if (name.empty()) return SignalType::Other;
|
|
||||||
std::string u = name;
|
|
||||||
std::transform(u.begin(), u.end(), u.begin(),
|
|
||||||
[](unsigned char c) { return std::toupper(c); });
|
|
||||||
|
|
||||||
auto contains = [&](const char *needle) {
|
|
||||||
return u.find(needle) != std::string::npos;
|
|
||||||
};
|
|
||||||
auto starts_with = [&](const char *needle) {
|
|
||||||
return u.rfind(needle, 0) == 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (u == "GND" || u == "GROUND") return SignalType::GndShield;
|
|
||||||
if (starts_with("GND_")
|
|
||||||
|| (starts_with("GND") && u.size() >= 4
|
|
||||||
&& std::isalpha((unsigned char)u[3]))) {
|
|
||||||
return SignalType::GndShield;
|
|
||||||
}
|
|
||||||
if (contains("SHIELD") || contains("CHASSIS") || contains("EARTH"))
|
|
||||||
return SignalType::GndShield;
|
|
||||||
|
|
||||||
if (contains("PWR") || contains("POWER")
|
|
||||||
|| contains("VCC") || contains("VDD") || contains("VEE") || contains("VSS")
|
|
||||||
|| starts_with("VS_") || starts_with("VS1_") || starts_with("VS2_")
|
|
||||||
|| starts_with("VS3_") || starts_with("VS4_")
|
|
||||||
|| starts_with("VBAT") || starts_with("VBUS")
|
|
||||||
|| starts_with("+") || starts_with("-")) {
|
|
||||||
return SignalType::Power;
|
|
||||||
}
|
|
||||||
return SignalType::Other;
|
|
||||||
}
|
|
||||||
|
|
||||||
Signal::Signal(std::string name)
|
|
||||||
: SystemElementContainer<Pin>(name), prnt(nullptr),
|
|
||||||
type(SignalType::Other) {};
|
|
||||||
|
|
||||||
void Signal::add(Pin *pin)
|
|
||||||
{
|
|
||||||
string pname = pin->prnt->name + "." + pin->name;
|
|
||||||
SystemElementContainer<Pin>::add(pname, pin);
|
|
||||||
}
|
|
||||||
|
|
||||||
Signals::Signals(void): SystemElementContainer<Signal>("signals") {}
|
|
||||||
|
|
||||||
Signals::Signals(std::vector<Signal *> signals): SystemElementContainer<Signal>("signals", signals) {}
|
|
||||||
|
|
||||||
Signals::~Signals() {
|
|
||||||
for (const auto& [key, value] : content) {
|
|
||||||
delete value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int drop_singleton_signals(Signals *signals) {
|
|
||||||
if (!signals) return 0;
|
|
||||||
std::vector<Signal *> doomed;
|
|
||||||
for (auto &kv : *signals)
|
|
||||||
if (kv.second->size() == 1) doomed.push_back(kv.second);
|
|
||||||
for (Signal *s : doomed) {
|
|
||||||
// Detach the lone pin so it surfaces as `(NC)` in views.
|
|
||||||
for (auto &pkv : *s) {
|
|
||||||
pkv.second->connect(nullptr);
|
|
||||||
pkv.second->nc_origin = NcOrigin::DroppedSingleton;
|
|
||||||
}
|
|
||||||
signals->remove(s->name);
|
|
||||||
delete s;
|
|
||||||
}
|
|
||||||
return (int)doomed.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Signals::add(Signal *signal)
|
|
||||||
{
|
|
||||||
SystemElementContainer<Signal>::add(signal);
|
|
||||||
signal->prnt = this;
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user