Compare commits

...

15 Commits

Author SHA1 Message Date
63ca17d048 build: split core/ from frontends/; prepare for multiple GUI/TUI targets
Reorganise the tree into business vs frontend as separate directories:
  src/core/{domain,imports,app}   (was system/, imports/, app/)
  src/frontends/tui/              (was tui/ + main.cpp)
  tests/tui/                      (the FTXUI-coupled helper test)
All cross-dir #include paths rewritten; same-dir includes untouched.

CMake: essim_core is the frontend-agnostic business library — links libzip,
pugixml and bsdl, NO GUI toolkit. Each frontend is a self-contained
src/frontends/<name>/ (own CMakeLists, toolkit, main.cpp) that links
essim_core, selected with -DESSIM_FRONTEND=<name> (default tui; 'none' = core +
tests only, no toolkit fetched). FTXUI moved into the tui frontend. Tests are
split: essim_tests links essim_core (no FTXUI), essim_tui_tests links essim_tui.

Verified: default tui build green (ctest 2/2); ESSIM_FRONTEND=none builds the
core + tests with FTXUI never fetched and no `essim` binary.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 19:33:06 +02:00
3010bb25eb core/ui: extract export into src/app (frontend-agnostic), thin TUI command
First step of separating business logic from the TUI. The export command built
the CSV/ODS file inside its lambda, mixed with Print/ShowError/dialog calls.
Move all of it — CSV + ODS building, sheet-name sanitising, file writing — into
src/app/export.{hpp,cpp} (namespace app, no FTXUI/console dependency):
export_connections(const System*, path, format) -> ExportResult. The TUI
command is now a thin wrapper (resolve args/dialog, call the core, render). The
core is unit-tested without any UI (test_export); 342 assertions pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 19:23:41 +02:00
ac2edd90c4 tui: show the Computing... progress as a real Modal (was invisible)
The Renderer-overlay approach to the global progress box didn't render. Use a
proper Modal like the palette / file dialog, driven by a plain bool
'computing_open' raised when a source starts and lowered when it ends or
aborts. The tick handler stays ahead of the modal guard, so the script keeps
running (and the screen behind it keeps updating) while the modal is shown;
computing_open is also added to the guard so stray keys are ignored mid-load.
The console screen's own Computing block was already removed, so no duplicate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 19:11:27 +02:00
53eb79c760 source: abort guard compares to the originating screen, not 0
A script opened from the dashboard (screen_idx 4) aborted after its first
line: the guard treated any non-console screen as 'an interactive command
opened a screen'. Record the screen the source started from and abort only
when a sourced line navigates away from it (what a bare interactive command
does). Now 'o' from the dashboard runs the whole script in place — the
dashboard populates live behind the global Computing overlay — while a bare
connect/explore inside a script still aborts. Batch unaffected (BootDispatch
pins screen 0, so origin 0).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 19:06:18 +02:00
29cb353d75 tui: global script progress + dashboard source/restore work
Opening a script from the dashboard did nothing visually: the tick driving the
line-by-line loader was only handled in the console case, and the Computing…
overlay was console-only. Move both to the global layer — ticks now process on
any screen (the dashboard updates live as the script loads) and the Computing…
box overlays whatever screen is active. Add an 'r' dashboard shortcut to
restore a snapshot via the file picker (open mode, like 'o'). Dashboard help
hints (loaded + empty state) updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 19:02:13 +02:00
c70e767cf1 filedialog: add open mode (no overwrite prompt) and use it for 'o'
The picker is built for saving, so it asks 'file exists — overwrite?' on
confirm. That's wrong when opening a script (you want an existing file). Add a
confirm_overwrite flag (default true; FileDialogState + OpenFileDialog param);
ConfirmFileDialog only prompts when it's set. The dashboard 'o' shortcut now
opens the dialog with confirm_overwrite=false. save/export keep the guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 18:52:53 +02:00
527a48145b dashboard: show 'o' (open script) in the no-system help panel too
The empty-dashboard panel (early_help) is a separate hint list from the
loaded-system one, and didn't list the new o/s keys — so 'o' worked but wasn't
advertised when no system is loaded (exactly when you'd use it). Add 'o' to
early_help and mention it in the 'no system loaded' line. (s/x need a system,
so they stay out of the empty-state panel.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 18:49:42 +02:00
60c00eb914 dashboard: o/s shortcuts to open a script and save via a file dialog
Two new dashboard keys, mirroring the existing x=export: 'o' opens a file
picker to run a .essim script (-> source <path>), 's' opens a file picker to
write a system snapshot (-> save <path>, only when a system is loaded). Both
use OpenFileDialog with a per-key persisted last directory; the global
CatchEvent already yields to the file-dialog modal while it is open. Dashboard
help hints updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 18:44:41 +02:00
29242ae016 dashboard: nest the dropped-NC detail under the NC row
The detail block was rendered after *all* health rows, so it dangled below
the new model: row instead of the NC: row it explains — its indentation read
as broken. Build it into health_rows right after the NC row, with a tree
marker (↳ dropped (only 1 pin on the net):) and consistent nesting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 18:38:58 +02:00
7810711fd4 ui: plainer wording across labels and command descriptions
Replace internal jargon and uncommon words in user-facing strings (better for
non-native English readers): drop "TransformRegistry-driven" / "drives
transforms" from the connect/set-connector-type subtitles; "transform lookup"
→ "tells connect how to wire its pins"; "populate pin specs" → "fills in each
pin's role and direction"; "clear the visualization area" → "clear the console
output"; "materialised" → "added"; refresh the verify description. Regenerated
commands.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 18:33:32 +02:00
1a31dd64b6 dashboard: simpler wording for the dropped-NC detail label
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 18:30:20 +02:00
3cee5c2e49 dashboard: clearer label for the dropped-NC detail rows
"dropped detail:" said nothing about what those pins are. They were detached
by drop_singleton_signals because each was the lone pin on its net (nowhere to
connect → NC). Relabel to "dropped — lone pin on its net (→ NC):".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 18:29:11 +02:00
a914b9d7e8 P3: BSDL completeness check (missing device power/ground pins)
check_bsdl_completeness(System*): for each BSDL-attached part, re-parse the
.bsd and report the device power/ground ports with no matching pin on the
netlist part (matched by port name or physical pad) — a rail the schematic
symbol is missing. One aggregated BsdlPinMissing per part; restricted to
power/ground so unused I/O balls don't create noise. Surfaced as a 7th verify
pass and in the analyze/dashboard model counts. 76 cases / 327 assertions
green; the real 8-card system reports 0 (all FPGA rails present). This closes
out P3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 16:21:02 +02:00
c9ac186a20 P3.3: surface model anomalies in analyze + dashboard
The analyze screen's Issues pane now lists the model-driven checks
(check_pin_specs / check_jtag_chain / check_source_conflicts) alongside the
pin-role, net-mix and structural ones, with an "N model" count in the header;
the dashboard gains a "model:" health row. check_pin_specs/check_jtag_chain
take an optional precomputed net list, so verify, analyze and the dashboard
each compute the nets once and reuse them across checks instead of redoing the
transitive closure per check. Unit tests (75) green; verify output unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 16:14:41 +02:00
fe5b2c3d96 P3.2: source precedence + model-vs-netlist conflict check
Rank the spec sources (spec_source_rank: UserOverride > Bsdl > ConnectorModel
> Inferred > Imported); apply_model now refuses to overwrite a spec owned by a
higher-rank source, so one model never clobbers a more authoritative one. New
check_source_conflicts(System*) emits SourceConflict for a pin the BSDL
declares power/ground (a must-connect rail) that the netlist leaves
unconnected — a rail floated in the schematic; surfaced as a sixth `verify`
pass. Unit tests (75 cases) green; the real 8-card system reports 0 conflicts
(its rails are all connected) while the JTAG findings remain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 16:08:28 +02:00
87 changed files with 1031 additions and 541 deletions

View File

@@ -11,49 +11,54 @@ project(essim
include(FetchContent) include(FetchContent)
set(FTXUI_BUILD_DOCS OFF CACHE INTERNAL "") # ----------------------------------------------------------------- core deps
set(FTXUI_BUILD_EXAMPLES OFF CACHE INTERNAL "") # libbsdl — standalone BSDL parser (LGPL-2.1), dynamically linked (EUPL-1.2,
set(FTXUI_BUILD_TESTS OFF CACHE INTERNAL "") # which the LGPL permits). Override its path with -DBSDL_DIR=...
set(FTXUI_ENABLE_INSTALL OFF CACHE INTERNAL "")
FetchContent_Declare(ftxui
GIT_REPOSITORY https://github.com/ArthurSonzogni/FTXUI.git
GIT_TAG v6.1.9
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(ftxui)
find_package(libzip REQUIRED)
find_package(pugixml REQUIRED)
# libbsdl — standalone BSDL parser (LGPL-2.1), dynamically linked from essim
# (EUPL-1.2, which the LGPL permits). Path overridable via -DBSDL_DIR=...;
# its CLI and tests are not needed inside essim's build.
set(BSDL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../libbsdl" CACHE PATH "libbsdl source tree") set(BSDL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../libbsdl" CACHE PATH "libbsdl source tree")
set(BSDL_BUILD_CLI OFF CACHE BOOL "" FORCE) set(BSDL_BUILD_CLI OFF CACHE BOOL "" FORCE)
set(BSDL_BUILD_TESTS OFF CACHE BOOL "" FORCE) set(BSDL_BUILD_TESTS OFF CACHE BOOL "" FORCE)
add_subdirectory(${BSDL_DIR} ${CMAKE_BINARY_DIR}/libbsdl) add_subdirectory(${BSDL_DIR} ${CMAKE_BINARY_DIR}/libbsdl)
# Library target = everything except main.cpp; reused by `essim` and `essim_tests`. find_package(libzip REQUIRED)
file(GLOB_RECURSE LIB_SOURCES "src/*.cpp") find_package(pugixml REQUIRED)
list(REMOVE_ITEM LIB_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp")
add_library(essim_lib STATIC ${LIB_SOURCES}) # =============================================================== essim_core
target_include_directories(essim_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src) # All business logic — domain model, importers, application operations
target_link_libraries(essim_lib # (src/core/{domain,imports,app}). Frontend-agnostic: it links NO GUI/TUI
# toolkit, so every frontend and the test suite share the exact same core.
file(GLOB_RECURSE CORE_SOURCES "src/core/*.cpp")
add_library(essim_core STATIC ${CORE_SOURCES})
target_include_directories(essim_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_link_libraries(essim_core
PUBLIC PUBLIC
ftxui::screen
ftxui::dom
ftxui::component
libzip::zip libzip::zip
pugixml::pugixml pugixml::pugixml
bsdl::bsdl bsdl::bsdl
) )
add_executable(essim src/main.cpp) # =============================================================== frontend(s)
target_link_libraries(essim PRIVATE essim_lib) # Pick the GUI/TUI frontend to build the `essim` binary against. Each frontend
# is a self-contained src/frontends/<name>/ (own CMakeLists, GUI toolkit, and
# main.cpp) that links essim_core. "none" builds the core + tests only — no GUI
# toolkit is fetched. To add a frontend (e.g. a Qt GUI), create
# src/frontends/gui/ and configure with -DESSIM_FRONTEND=gui.
set(ESSIM_FRONTEND "tui" CACHE STRING
"Frontend to build: a directory name under src/frontends/, or 'none'")
set_property(CACHE ESSIM_FRONTEND PROPERTY STRINGS tui none)
# Tests if(ESSIM_FRONTEND STREQUAL "none")
message(STATUS "essim: ESSIM_FRONTEND=none — core + tests only (no frontend, no GUI toolkit)")
elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/frontends/${ESSIM_FRONTEND}/CMakeLists.txt")
message(STATUS "essim: building frontend '${ESSIM_FRONTEND}'")
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 +70,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 +123,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()

View File

@@ -129,9 +129,9 @@ The explore screen shows the type in the signal detail header.
**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/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.
**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`) — so one source never clobbers another's. `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/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`.
**`verify` (five 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). 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/system/analysis.{hpp,cpp}`) is a stateless read-only pass that detects structural signal groups and anomalies. Per-module (signals are module-scoped):
@@ -167,7 +167,7 @@ Exposed as the `analyze` shell command which prints groups (sorted by module + l
**Analyze screen** (`screen_analyze.cpp`, dashboard shortcut `a`, `screen_idx = 7`): unified **verify + analyze** view with a tabbed layout — horizontal tab bar at the top (`Issues (…) │ Groups (…) │ Types: …`), and a single scrollable detail panel below showing the active tab's list. Tab swap is handled at the outer `CatchEvent` (`Tab` / `→` cycle forward, `Shift-Tab` / `←` cycle back). The detail uses `Container::Tab({issues_menu, groups_menu, types_menu}, &analyze_focus_idx)` so `↑/↓` always navigate the visible list; each tab preserves its own selection idx. **Analyze screen** (`screen_analyze.cpp`, dashboard shortcut `a`, `screen_idx = 7`): unified **verify + analyze** view with a tabbed layout — horizontal tab bar at the top (`Issues (…) │ Groups (…) │ Types: …`), and a single scrollable detail panel below showing the active tab's list. Tab swap is handled at the outer `CatchEvent` (`Tab` / `→` cycle forward, `Shift-Tab` / `←` cycle back). The detail uses `Container::Tab({issues_menu, groups_menu, types_menu}, &analyze_focus_idx)` so `↑/↓` always navigate the visible list; each tab preserves its own selection idx.
- **Issues** pane merges: pin-role mismatches (typed pins whose actual signal type disagrees with the role from `connector_type`), bridged-net Power↔Gnd inconsistencies (the BFS check formerly in `verify` pass 2), and the structural anomalies from `analyze_system` (`DiffPairOrphan`, `BusGap`, `DiffBusGap`). Header counts each category. - **Issues** pane merges: pin-role mismatches (typed pins whose actual signal type disagrees with the role from `connector_type`), bridged-net Power↔Gnd inconsistencies (the BFS check formerly in `verify` pass 2), the structural anomalies from `analyze_system` (`DiffPairOrphan`, `BusGap`, `DiffBusGap`), and the model-driven checks (`check_pin_specs` / `check_jtag_chain` / `check_source_conflicts`, tagged `model` in the header). Header counts each category.
- **Groups** pane lists every detected `SignalGroup` sorted by `module / label` with kind tag and member count. - **Groups** pane lists every detected `SignalGroup` sorted by `module / label` with kind tag and member count.
- **Types** pane lists per-signal Power decisions (`[Power confirmed]` / `[Power REFUTED]` / `[Gnd]`) plus a trailing `[NC]` orphan rollup line. The pane header summarises counts (`N pwr-ok, M refuted, K gnd`). - **Types** pane lists per-signal Power decisions (`[Power confirmed]` / `[Power REFUTED]` / `[Gnd]`) plus a trailing `[NC]` orphan rollup line. The pane header summarises counts (`N pwr-ok, M refuted, K gnd`).
@@ -197,7 +197,7 @@ Today the only caller is `export` (`{"CSV", ".csv"}, {"ODS", ".ods"}` filters, k
**Command palette** (`screen_palette.cpp`): a global modal launcher attached to the whole tab tree via `tab | Modal(BuildPaletteModal(), &palette_open)` in `Run()`. Trigger: `Event::CtrlP` (FTXUI Input does not consume Ctrl-P, so the outer `CatchEvent` reliably picks it up first). Behaviour: a single Input bound to `palette_query` plus a result list rebuilt on every frame. Indexes three kinds of entries: commands (from the `commands` map), modules and per-module signals (qualified as `module/signal`). Fuzzy match is subsequence-based, case-insensitive: lower score wins, computed as `first_match_position * 100 + sum_of_gaps`. Kinds are biased by a constant offset (commands +0, modules +1000, signals +2000) so command matches come first when scores tie. Output capped at 20 rows to keep render cheap on big systems. Activation (`Enter`): commands → `Dispatch(name)` (which dispatches like the shell, including opening interactive screens), module → prefill `explore_*` state and jump to `screen_idx = 4`, signal → prefill `net_modules` + seed `net_sig_filter` to the exact signal name and jump to `screen_idx = 5`. `Esc` closes the palette. While the palette is open, the outer `CatchEvent` cedes events to it so Tab/Esc/etc. don't leak into the underlying screen. **Command palette** (`screen_palette.cpp`): a global modal launcher attached to the whole tab tree via `tab | Modal(BuildPaletteModal(), &palette_open)` in `Run()`. Trigger: `Event::CtrlP` (FTXUI Input does not consume Ctrl-P, so the outer `CatchEvent` reliably picks it up first). Behaviour: a single Input bound to `palette_query` plus a result list rebuilt on every frame. Indexes three kinds of entries: commands (from the `commands` map), modules and per-module signals (qualified as `module/signal`). Fuzzy match is subsequence-based, case-insensitive: lower score wins, computed as `first_match_position * 100 + sum_of_gaps`. Kinds are biased by a constant offset (commands +0, modules +1000, signals +2000) so command matches come first when scores tie. Output capped at 20 rows to keep render cheap on big systems. Activation (`Enter`): commands → `Dispatch(name)` (which dispatches like the shell, including opening interactive screens), module → prefill `explore_*` state and jump to `screen_idx = 4`, signal → prefill `net_modules` + seed `net_sig_filter` to the exact signal name and jump to `screen_idx = 5`. `Esc` closes the palette. While the palette is open, the outer `CatchEvent` cedes events to it so Tab/Esc/etc. don't leak into the underlying screen.
**Dashboard** (`screen_dashboard.cpp`, `dashboard` command, `screen_idx = 4`): read-only system overview. Single Renderer, no Input child. Recomputes everything per frame (cheap on realistic sizes): counters (modules/parts/signals/connections), three health rows (verify pin-role mismatches, bridged-net inconsistencies, NC orphans — green check / yellow warning prefix), an analysis summary line (diff pairs / buses / anomaly count, coloured if non-zero), and a per-module table (parts / signals / `connector_type`-tagged parts). Letter shortcuts handled in the outer `CatchEvent`: `c`=console, `p`=plug (connect), `t`=set-connector-type, `e`=explore, `a`=analyze, `q`=quit. `Esc` is swallowed on the dashboard (home). The dashboard is `interactive = true`, `scriptable = false`; running `dashboard` inside `source` aborts the script. **Dashboard** (`screen_dashboard.cpp`, `dashboard` command, `screen_idx = 4`): read-only system overview. Single Renderer, no Input child. Recomputes everything per frame (cheap on realistic sizes): counters (modules/parts/signals/connections), four health rows (verify pin-role mismatches, bridged-net inconsistencies, NC orphans, and BSDL/JTAG model anomalies — green check / yellow warning prefix), an analysis summary line (diff pairs / buses / anomaly count, coloured if non-zero), and a per-module table (parts / signals / `connector_type`-tagged parts). Letter shortcuts handled in the outer `CatchEvent`: `c`=console, `p`=plug (connect), `t`=set-connector-type, `e`=explore, `a`=analyze, `q`=quit. `Esc` is swallowed on the dashboard (home). The dashboard is `interactive = true`, `scriptable = false`; running `dashboard` inside `source` aborts the script.
**Screen titles** (shared idiom): every interactive screen renders a top bar in the form `" essim "` (bold) + `"→ "` (dim) + `"<screen-name>"` (bold) + `" — <short description>"` (dim), followed by a `separator()`. The main screen has its own variant that adds a live `N module(s), M connection(s)` counter on the right. Aim is to make the breadcrumb between essim and the current mode visible at all times. **Screen titles** (shared idiom): every interactive screen renders a top bar in the form `" essim "` (bold) + `"→ "` (dim) + `"<screen-name>"` (bold) + `" — <short description>"` (dim), followed by a `separator()`. The main screen has its own variant that adds a live `N module(s), M connection(s)` counter on the right. Aim is to make the breadcrumb between essim and the current mode visible at all times.
@@ -299,7 +299,7 @@ The analyze screen additionally surfaces two "verify-class" issues, computed the
- **pin-role mismatch** — a pin whose `expected_signal_type()` (derived from its `PinSpec`, set by `set-connector-type` via `pin_role(connector_type, pin_name)`) disagrees with the actual signal type. - **pin-role mismatch** — a pin whose `expected_signal_type()` (derived from its `PinSpec`, set by `set-connector-type` via `pin_role(connector_type, pin_name)`) disagrees with the actual signal type.
- **net-mix** — a bridged net (BFS over `Connection::pin_map`, ≥ 2 members) where `net_type_consistent(net, &dominant)` returns false. Specifically, the net contains both `Power` and `GndShield` signals. - **net-mix** — a bridged net (BFS over `Connection::pin_map`, ≥ 2 members) where `net_type_consistent(net, &dominant)` returns false. Specifically, the net contains both `Power` and `GndShield` signals.
The `verify` command (not the analyze screen, yet) also emits the **model-driven `AnomalyKind`s** from `bsdl_check.{hpp,cpp}`: `DriveContention` / `UndrivenNet` / `NcWired` (`check_pin_specs`) and `JtagTapIncomplete` / `JtagChainBreak` / `JtagBusUnbridged` (`check_jtag_chain`). They consume the BSDL-populated `PinSpec` plus `compute_all_nets`. Surfacing them in the analyze/dashboard Issues pane is a TODO. The `verify` command (not the analyze screen, yet) also emits the **model-driven `AnomalyKind`s** from `bsdl_check.{hpp,cpp}`: `DriveContention` / `UndrivenNet` / `NcWired` (`check_pin_specs`) and `JtagTapIncomplete` / `JtagChainBreak` / `JtagBusUnbridged` (`check_jtag_chain`); `SourceConflict` (`check_source_conflicts` — a BSDL power/ground pin the netlist leaves unconnected); and `BsdlPinMissing` (`check_bsdl_completeness` — a device power/ground port absent from the netlist part, re-parsing the attached `.bsd`). They consume the BSDL-populated `PinSpec` plus `compute_all_nets`, and are surfaced in three places: the `verify` command, the analyze screen's Issues pane (counted as `… N model`), and a `model:` health row on the dashboard. `check_pin_specs`/`check_jtag_chain` accept an optional precomputed net list, so verify, analyze and the dashboard each compute the nets once and reuse them across checks.
### Component kind ### Component kind

View File

@@ -67,7 +67,7 @@ connect a part across two modules (interactive screen if no args)
--- ---
### `set-connector-type` *(interactive)* ### `set-connector-type` *(interactive)*
tag a part's connector type for transform lookup tag a part's connector type (tells connect how to wire its pins)
**Arguments** **Arguments**
@@ -91,7 +91,7 @@ detect signal groups (diff pairs, buses) and structural anomalies
--- ---
### `attach-bsdl` ### `attach-bsdl`
attach a BSDL (.bsd) model to a part and populate pin specs attach a BSDL (.bsd) model to a part (fills in each pin's role and direction)
**Arguments** **Arguments**
@@ -106,7 +106,7 @@ attach a BSDL (.bsd) model to a part and populate pin specs
--- ---
### `clear` ### `clear`
clear the visualization area clear the console output
**No arguments.** **No arguments.**
--- ---
@@ -253,7 +253,7 @@ execute a file of commands line by line (interactive cmds rejected)
--- ---
### `verify` ### `verify`
check pin roles, bridged-net consistency, and model-driven pin specs (contention/undriven/NC) check pin roles, power/gnd net consistency, and (with BSDL) pin and JTAG checks
**No arguments.** **No arguments.**
--- ---

190
src/core/app/export.cpp Normal file
View File

@@ -0,0 +1,190 @@
#include "core/app/export.hpp"
#include "core/imports/ods_writer.hpp"
#include "core/domain/connect.hpp"
#include "core/domain/modules.hpp"
#include "core/domain/parts.hpp"
#include "core/domain/pins.hpp"
#include "core/domain/signal_type.hpp"
#include "core/domain/signals.hpp"
#include "core/domain/system.hpp"
#include <cctype>
#include <fstream>
#include <string>
namespace app {
namespace {
std::string to_lower(std::string s)
{
for (char &c : s) c = (char)std::tolower((unsigned char)c);
return s;
}
// Minimal CSV quoter — wraps in "…" and doubles internal quotes when the field
// contains a comma, quote, or newline.
std::string csv_quote(const std::string &s)
{
if (s.find_first_of(",\"\n") == std::string::npos)
return s;
std::string out = "\"";
for (char c : s) { if (c == '"') out += '"'; out += c; }
out += '"';
return out;
}
// Flatten one pin into the string slots an export row uses.
void pin_side(Pin *p, std::string &mod, std::string &part, std::string &pin,
std::string &sig, std::string &type, std::string &suspect)
{
if (!p) { mod = part = pin = sig = type = suspect = ""; return; }
mod = (p->prnt && p->prnt->prnt) ? p->prnt->prnt->name : "";
part = p->prnt ? p->prnt->name : "";
pin = p->name;
Signal *s = p->signal();
if (!s) {
sig = ""; type = "(NC)"; suspect = "";
} else {
sig = s->name;
type = signal_type_name(s->type);
suspect = (infer_signal_type(s->name) == SignalType::Power
&& s->type == SignalType::Other) ? "yes" : "no";
}
}
// Excel rejects /\?*:[] in sheet names; ODS forbids < > & in raw table names.
// Sanitise to underscores and clip to Excel's 31-char hard limit.
std::string sanitise_sheet_name(std::string name)
{
for (char &ch : name)
if (ch == '/' || ch == '\\' || ch == '?' || ch == '*' || ch == ':'
|| ch == '[' || ch == ']' || ch == '<' || ch == '>' || ch == '&')
ch = '_';
if (name.size() > 31) name = name.substr(0, 31);
return name;
}
ExportResult write_ods(const System *sys, const std::string &path)
{
ExportResult r;
OdsWriter w;
for (auto &ckv : *sys->connections()) {
Connection *c = ckv.second;
OdsSheet *s = w.add_sheet(sanitise_sheet_name(c->name));
std::string lmod = c->m1 ? c->m1->name : "";
std::string lprt = c->p1 ? c->p1->name : "";
std::string rmod = c->m2 ? c->m2->name : "";
std::string rprt = c->p2 ? c->p2->name : "";
// Meta header: label/value rows, a blank row, then the column headers.
auto meta = [&](int row, const std::string &k, const std::string &v) {
s->set(row, 0, k);
s->set(row, 1, v);
};
meta(0, "Connection", c->name);
meta(1, "Transform", c->transform_name);
meta(2, "Left", lmod + " / " + lprt);
meta(3, "Right", rmod + " / " + rprt);
// Row 4 left blank by design.
const int HDR = 5;
s->set_header_row(HDR);
const char *hdr[] = {
"left_pin", "left_signal", "left_type", "left_suspect",
"right_pin", "right_signal", "right_type", "right_suspect",
"type_mismatch"};
for (int i = 0; i < 9; ++i) s->set(HDR, i, hdr[i]);
int row = HDR + 1;
for (auto &wp : c->pin_map) {
std::string lm, lp, ln, ls, lt, lsus;
std::string rm, rp, rn, rs, rt, rsus;
pin_side(wp.first, lm, lp, ln, ls, lt, lsus);
pin_side(wp.second, rm, rp, rn, rs, rt, rsus);
std::string tm = (lt != "(NC)" && rt != "(NC)" && lt != rt) ? "yes" : "no";
s->set(row, 0, ln); s->set(row, 1, ls);
s->set(row, 2, lt); s->set(row, 3, lsus);
s->set(row, 4, rn); s->set(row, 5, rs);
s->set(row, 6, rt); s->set(row, 7, rsus);
s->set(row, 8, tm);
++row; ++r.rows;
}
}
std::string err;
if (!w.save(path, err)) {
r.error = err;
return r;
}
r.ok = true;
r.sheets = (int)sys->connections()->size();
return r;
}
ExportResult write_csv(const System *sys, const std::string &path)
{
ExportResult r;
std::ofstream f(path);
if (!f) {
r.error = "cannot open '" + path + "' for writing";
return r;
}
// One rectangular table: each row repeats the per-connection constants so
// the file stays parser-friendly (pandas / awk / spreadsheet).
f << "connection,transform,"
"left_module,left_part,"
"left_pin,left_signal,left_type,left_suspect,"
"right_module,right_part,"
"right_pin,right_signal,right_type,right_suspect,"
"type_mismatch\n";
for (auto &ckv : *sys->connections()) {
Connection *c = ckv.second;
for (auto &wp : c->pin_map) {
std::string lm, lp, ln, ls, lt, lsus;
std::string rm, rp, rn, rs, rt, rsus;
pin_side(wp.first, lm, lp, ln, ls, lt, lsus);
pin_side(wp.second, rm, rp, rn, rs, rt, rsus);
std::string tm = (lt != "(NC)" && rt != "(NC)" && lt != rt) ? "yes" : "no";
f << csv_quote(c->name) << ','
<< csv_quote(c->transform_name) << ','
<< csv_quote(lm) << ',' << csv_quote(lp) << ','
<< csv_quote(ln) << ',' << csv_quote(ls) << ','
<< csv_quote(lt) << ',' << csv_quote(lsus) << ','
<< csv_quote(rm) << ',' << csv_quote(rp) << ','
<< csv_quote(rn) << ',' << csv_quote(rs) << ','
<< csv_quote(rt) << ',' << csv_quote(rsus) << ','
<< tm << '\n';
++r.rows;
}
}
r.ok = f.good();
if (!r.ok) r.error = "write error on '" + path + "'";
return r;
}
} // namespace
bool export_format_from_path(const std::string &path, ExportFormat &out)
{
size_t dot = path.rfind('.');
std::string ext = (dot == std::string::npos) ? "" : to_lower(path.substr(dot));
if (ext == ".csv") { out = ExportFormat::Csv; return true; }
if (ext == ".ods") { out = ExportFormat::Ods; return true; }
return false;
}
ExportResult export_connections(const System *sys, const std::string &path,
ExportFormat format)
{
ExportResult r;
if (!sys) { r.error = "no system"; return r; }
return (format == ExportFormat::Ods) ? write_ods(sys, path)
: write_csv(sys, path);
}
} // namespace app

34
src/core/app/export.hpp Normal file
View File

@@ -0,0 +1,34 @@
#ifndef _APP_EXPORT_HPP_
#define _APP_EXPORT_HPP_
#include <string>
class System;
// Application layer: UI-independent operations that any frontend (TUI, GUI, …)
// can call. No console, no dialogs, no FTXUI — just System in, result out.
namespace app {
enum class ExportFormat { Csv, Ods };
// Outcome of an export. The only side effect is writing the target file; the
// caller renders `error` / the stats however it likes.
struct ExportResult {
bool ok = false;
std::string error; ///< human-readable, set when !ok
int sheets = 0; ///< ODS: number of sheets (one per connection); 0 for CSV
int rows = 0; ///< wires written
};
// Map a filename extension (.csv / .ods, case-insensitive) to a format.
// Returns false if the extension is neither.
bool export_format_from_path(const std::string &path, ExportFormat &out);
// Export the system's connections to `path` in `format`. Builds the file and
// returns stats or an error. Pure core — safe to call from any frontend.
ExportResult export_connections(const System *sys, const std::string &path,
ExportFormat format);
} // namespace app
#endif // _APP_EXPORT_HPP_

View File

@@ -29,6 +29,8 @@ const char *anomaly_kind_name(AnomalyKind k) {
case AnomalyKind::JtagTapIncomplete: return "jtag-tap-incomplete"; case AnomalyKind::JtagTapIncomplete: return "jtag-tap-incomplete";
case AnomalyKind::JtagChainBreak: return "jtag-chain-break"; case AnomalyKind::JtagChainBreak: return "jtag-chain-break";
case AnomalyKind::JtagBusUnbridged: return "jtag-bus-unbridged"; case AnomalyKind::JtagBusUnbridged: return "jtag-bus-unbridged";
case AnomalyKind::SourceConflict: return "source-conflict";
case AnomalyKind::BsdlPinMissing: return "bsdl-pin-missing";
} }
return "?"; return "?";
} }

View File

@@ -37,6 +37,8 @@ enum class AnomalyKind {
JtagTapIncomplete, ///< A TAP device is missing one of TDI/TDO/TMS/TCK. JtagTapIncomplete, ///< A TAP device is missing one of TDI/TDO/TMS/TCK.
JtagChainBreak, ///< The TDO→TDI daisy chain is broken / not a single path. JtagChainBreak, ///< The TDO→TDI daisy chain is broken / not a single path.
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).
BsdlPinMissing, ///< A BSDL power/ground port has no pin on the netlist part.
}; };
struct Anomaly { struct Anomaly {

View File

@@ -1,5 +1,7 @@
#include "bsdl_check.hpp" #include "bsdl_check.hpp"
#include "bsdl_model.hpp"
#include "connect.hpp"
#include "modules.hpp" #include "modules.hpp"
#include "nets.hpp" #include "nets.hpp"
#include "parts.hpp" #include "parts.hpp"
@@ -10,6 +12,7 @@
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include <unordered_set>
#include <utility> #include <utility>
#include <vector> #include <vector>
@@ -37,13 +40,16 @@ std::string join_labels(const std::vector<Pin *> &pins)
} // namespace } // namespace
std::vector<Anomaly> check_pin_specs(System *sys) std::vector<Anomaly> check_pin_specs(System *sys, const std::vector<Net> *nets)
{ {
std::vector<Anomaly> out; std::vector<Anomaly> out;
if (!sys) if (!sys)
return out; return out;
for (const Net &net : compute_all_nets(sys)) { std::vector<Net> local;
if (!nets)
local = compute_all_nets(sys);
for (const Net &net : (nets ? *nets : local)) {
std::vector<Pin *> pins; std::vector<Pin *> pins;
std::vector<Signal *> sigs; std::vector<Signal *> sigs;
Module *mod = nullptr; Module *mod = nullptr;
@@ -121,14 +127,17 @@ std::vector<Anomaly> check_pin_specs(System *sys)
return out; return out;
} }
std::vector<Anomaly> check_jtag_chain(System *sys) std::vector<Anomaly> check_jtag_chain(System *sys, const std::vector<Net> *nets_in)
{ {
std::vector<Anomaly> out; std::vector<Anomaly> out;
if (!sys) if (!sys)
return out; return out;
// Map every pin to the index of the net it sits on. // Map every pin to the index of the net it sits on.
std::vector<Net> nets = compute_all_nets(sys); std::vector<Net> local;
if (!nets_in)
local = compute_all_nets(sys);
const std::vector<Net> &nets = nets_in ? *nets_in : local;
std::unordered_map<Pin *, int> net_of; std::unordered_map<Pin *, int> net_of;
for (size_t i = 0; i < nets.size(); ++i) for (size_t i = 0; i < nets.size(); ++i)
for (auto &mp : nets[i].members) for (auto &mp : nets[i].members)
@@ -277,3 +286,94 @@ std::vector<Anomaly> check_jtag_chain(System *sys)
return out; return out;
} }
std::vector<Anomaly> check_source_conflicts(System *sys)
{
std::vector<Anomaly> out;
if (!sys)
return out;
// Pins bridged to a peer signal through a connection count as connected.
std::unordered_set<Pin *> bridged;
for (auto &ckv : *sys->connections())
for (auto &wp : ckv.second->pin_map) {
if (wp.first) bridged.insert(wp.first);
if (wp.second) bridged.insert(wp.second);
}
for (auto &mkv : *sys->modules())
for (auto &pkv : *mkv.second)
for (auto &nkv : *pkv.second) {
Pin *pin = nkv.second;
if (pin->spec.source != SpecSource::Bsdl)
continue;
PinFunction f = pin->spec.function;
if (f != PinFunction::Power && f != PinFunction::Ground)
continue; // only must-connect rails are a clear defect
if (pin->connected() || bridged.count(pin))
continue;
Anomaly a;
a.kind = AnomalyKind::SourceConflict;
a.module = (pin->prnt) ? pin->prnt->prnt : nullptr;
a.message = pin_label(pin) + ": BSDL says "
+ (f == PinFunction::Power ? "power" : "ground")
+ " but the netlist leaves it unconnected"
+ (pin->nc_origin == NcOrigin::ImportedUnconnected
? " (marked NC at import)" : "");
out.push_back(std::move(a));
}
return out;
}
std::vector<Anomaly> check_bsdl_completeness(System *sys)
{
std::vector<Anomaly> out;
if (!sys)
return out;
for (auto &mkv : *sys->modules())
for (auto &pkv : *mkv.second) {
Part *part = pkv.second;
if (part->bsdl_path.empty())
continue;
BsdlModel model = BsdlModel::from_file(part->bsdl_path);
if (!model.valid())
continue;
std::vector<std::string> missing; // absent power/ground ports
for (const auto &port : model.ports()) {
if (port.function != PinFunction::Power &&
port.function != PinFunction::Ground)
continue; // only must-have rails — unused I/O may be absent
bool present = (!port.name.empty() && part->exists(port.name)) ||
(!port.pad.empty() && part->exists(port.pad));
if (!present) {
std::string lbl = port.name;
if (!port.pad.empty())
lbl += "@" + port.pad;
missing.push_back(lbl);
}
}
if (!missing.empty()) {
std::string examples;
for (size_t i = 0; i < missing.size() && i < 5; ++i)
examples += (examples.empty() ? "" : ", ") + missing[i];
if (missing.size() > 5)
examples += ", …";
Anomaly a;
a.kind = AnomalyKind::BsdlPinMissing;
a.module = part->prnt;
a.message = mkv.first + "/" + part->name + ": "
+ std::to_string(missing.size())
+ " device power/ground pin(s) absent from the netlist ("
+ examples + ")";
out.push_back(std::move(a));
}
}
return out;
}

View File

@@ -0,0 +1,46 @@
#ifndef _BSDL_CHECK_HPP_
#define _BSDL_CHECK_HPP_
#include "analysis.hpp" // Anomaly, AnomalyKind
#include "nets.hpp" // Net
#include <vector>
class System;
// The net checks below accept an optional precomputed net list: callers that
// already have one (verify, the analyze screen, the dashboard) pass it so the
// transitive-closure pass isn't redone. Pass nullptr to compute it internally.
// Model-driven pin checks over the system's nets, using the PinSpec
// direction/function populated by connector or BSDL models. Emits:
// - DriveContention : a net with ≥2 push-pull output drivers;
// - UndrivenNet : a multi-pin net with input(s) but no driver;
// - NcWired : a no-connect pin wired onto a multi-pin net.
// Read-only; nets with no direction data are skipped (no false positives on
// un-modelled parts).
std::vector<Anomaly> check_pin_specs(System *sys, const std::vector<Net> *nets = nullptr);
// JTAG boundary-scan chain integrity, using pins whose PinSpec.function is a TAP
// role (JtagTdi/Tdo/Tms/Tck/Trst). Resolves each TAP pin to its net and checks:
// - JtagTapIncomplete : a TAP device missing TDI/TDO/TMS/TCK;
// - JtagBusUnbridged : TMS or TCK not common to every TAP device;
// - JtagChainBreak : the TDO→TDI daisy chain dangles, fans out, or is not a
// single path (≠1 head / ≠1 tail).
// Empty when the system has no TAP pins.
std::vector<Anomaly> check_jtag_chain(System *sys, const std::vector<Net> *nets = nullptr);
// Conflicts between a device model and the netlist's own view of a pin. Today:
// a pin the BSDL declares power/ground (a must-connect rail) that the netlist
// leaves unconnected (no signal and not bridged) — i.e. a rail floated in the
// schematic. The reverse (BSDL no-connect wired in the netlist) is `NcWired`.
std::vector<Anomaly> check_source_conflicts(System *sys);
// Completeness: for every part with an attached BSDL, the device's power/ground
// ports that have no matching pin on the netlist part (matched by port name or
// physical pad) — i.e. a power/ground pin the schematic symbol is missing. One
// aggregated anomaly per part. Re-parses each attached `.bsd` (no model cache on
// Part yet), so it's bounded by the number of BSDL-attached parts.
std::vector<Anomaly> check_bsdl_completeness(System *sys);
#endif // _BSDL_CHECK_HPP_

View File

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

View File

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

View File

@@ -30,15 +30,18 @@ ApplyReport apply_model(Part *part, const PinModel &model)
} }
} }
// 2. Set each pin's spec where the model speaks for it. // 2. Set each pin's spec where the model speaks for it — but never overwrite
// a spec already owned by a higher-precedence source (see spec_source_rank).
r.pins_total = (int)part->size(); r.pins_total = (int)part->size();
for (auto &kv : *part) { for (auto &kv : *part) {
PinSpec s = model.spec_for(kv.first); PinSpec s = model.spec_for(kv.first);
if (s.source != SpecSource::None) { if (s.source == SpecSource::None)
continue;
if (spec_source_rank(s.source) < spec_source_rank(kv.second->spec.source))
continue;
kv.second->spec = s; kv.second->spec = s;
++r.set; ++r.set;
} }
}
return r; return r;
} }

View File

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

View File

@@ -51,4 +51,21 @@ inline PinFunction function_from_signal_type(SignalType t)
} }
} }
// Precedence of spec sources: a higher rank wins when two sources speak for the
// same pin. A user override beats any model; a device model (BSDL) beats a
// connector layout; both beat plain import / inference. Used by apply_model to
// avoid clobbering a more authoritative spec.
inline int spec_source_rank(SpecSource s)
{
switch (s) {
case SpecSource::None: return 0;
case SpecSource::Imported: return 1;
case SpecSource::Inferred: return 2;
case SpecSource::ConnectorModel: return 3;
case SpecSource::Bsdl: return 4;
case SpecSource::UserOverride: return 5;
}
return 0;
}
#endif // _PIN_SPEC_HPP_ #endif // _PIN_SPEC_HPP_

View File

@@ -3,9 +3,9 @@
#include "connect.hpp" #include "connect.hpp"
#include "modules.hpp" #include "modules.hpp"
#include "imports/import_altium.hpp" #include "core/imports/import_altium.hpp"
#include "imports/import_mentor.hpp" #include "core/imports/import_mentor.hpp"
#include "imports/import_ods.hpp" #include "core/imports/import_ods.hpp"
System::System() : mods(nullptr), conns(nullptr) System::System() : mods(nullptr), conns(nullptr)
{ {

View File

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

View File

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

View File

@@ -4,8 +4,8 @@
#include <string> #include <string>
#include <fstream> #include <fstream>
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/signals.hpp" #include "core/domain/signals.hpp"
/** /**
* @brief Base class for importing data from a file. * @brief Base class for importing data from a file.

View File

@@ -1,6 +1,6 @@
#include "import_mentor.hpp" #include "import_mentor.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include <cctype> #include <cctype>
#include <vector> #include <vector>

View File

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

View File

@@ -0,0 +1,35 @@
# TUI frontend (FTXUI). Builds the `essim` executable against essim_core.
#
# A frontend is self-contained here: it pulls its own GUI toolkit, compiles its
# sources into a library that links essim_core, and produces the `essim` binary
# from its own entry point (main.cpp). To add another frontend, create a sibling
# src/frontends/<name>/ with the same shape and select it with
# -DESSIM_FRONTEND=<name>.
set(FTXUI_BUILD_DOCS OFF CACHE INTERNAL "")
set(FTXUI_BUILD_EXAMPLES OFF CACHE INTERNAL "")
set(FTXUI_BUILD_TESTS OFF CACHE INTERNAL "")
set(FTXUI_ENABLE_INSTALL OFF CACHE INTERNAL "")
FetchContent_Declare(ftxui
GIT_REPOSITORY https://github.com/ArthurSonzogni/FTXUI.git
GIT_TAG v6.1.9
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(ftxui)
# Frontend library = every .cpp here except the entry point.
file(GLOB TUI_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp")
list(REMOVE_ITEM TUI_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/main.cpp")
add_library(essim_tui STATIC ${TUI_SOURCES})
target_link_libraries(essim_tui
PUBLIC
essim_core
ftxui::screen
ftxui::dom
ftxui::component
)
add_executable(essim "${CMAKE_CURRENT_SOURCE_DIR}/main.cpp")
target_link_libraries(essim PRIVATE essim_tui)

View File

@@ -1,21 +1,21 @@
#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/nets.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/persist.hpp" #include "core/domain/persist.hpp"
#include "system/pin_role.hpp" #include "core/domain/pin_role.hpp"
#include "system/pin_model.hpp" #include "core/domain/pin_model.hpp"
#include "system/bsdl_model.hpp" #include "core/domain/bsdl_model.hpp"
#include "system/bsdl_check.hpp" #include "core/domain/bsdl_check.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include "system/signals.hpp" #include "core/domain/signals.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include "system/transform.hpp" #include "core/domain/transform.hpp"
#include "system/transform_vpx.hpp" #include "core/domain/transform_vpx.hpp"
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
@@ -80,7 +80,7 @@ void Tui::RegisterCommands() {
"list commands (or `help <name>` for one command's details)", "list commands (or `help <name>` for one command's details)",
}; };
commands["clear"] = { {}, [this](auto &) { output.clear(); }, true, commands["clear"] = { {}, [this](auto &) { output.clear(); }, true,
"clear the visualization area" }; "clear the console output" };
// quit / exit work from any screen: set the flag *and* call Exit() on the // quit / exit work from any screen: set the flag *and* call Exit() on the
// captured ScreenInteractive so the FTXUI loop returns immediately. The // captured ScreenInteractive so the FTXUI loop returns immediately. The
// legacy main-screen Renderer also reads `quit` as a belt-and-braces // legacy main-screen Renderer also reads `quit` as a belt-and-braces
@@ -300,20 +300,34 @@ void Tui::RegisterCommands() {
// Model-driven pin checks (drive contention / undriven net / NC-wired) // Model-driven pin checks (drive contention / undriven net / NC-wired)
// from the PinSpec direction/function populated by connector/BSDL models. // from the PinSpec direction/function populated by connector/BSDL models.
auto pin_anoms = check_pin_specs(sys.get()); auto pin_anoms = check_pin_specs(sys.get(), &nets);
for (const auto &a : pin_anoms) 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(pin_anoms.size()) Print("verify: " + std::to_string(pin_anoms.size())
+ " model-driven pin anomaly(ies)."); + " model-driven pin anomaly(ies).");
// JTAG boundary-scan chain integrity (TAP pins → nets). // JTAG boundary-scan chain integrity (TAP pins → nets).
auto jtag_anoms = check_jtag_chain(sys.get()); auto jtag_anoms = check_jtag_chain(sys.get(), &nets);
for (const auto &a : jtag_anoms) for (const auto &a : jtag_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(jtag_anoms.size()) Print("verify: " + std::to_string(jtag_anoms.size())
+ " JTAG chain anomaly(ies)."); + " 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, bridged-net consistency, and model-driven pin specs (contention/undriven/NC)" }; "check pin roles, power/gnd net consistency, and (with BSDL) pin and JTAG checks" };
commands["dashboard"] = { {}, [this](auto &) { commands["dashboard"] = { {}, [this](auto &) {
screen_idx = 4; screen_idx = 4;
@@ -461,11 +475,11 @@ void Tui::RegisterCommands() {
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 (rep.materialised > 0)
Print("set-connector-type: materialised " + std::to_string(rep.materialised) Print("set-connector-type: added " + std::to_string(rep.materialised)
+ " NC pin(s) from connector layout"); + " NC pin(s) from the connector layout");
}, },
/*prompt_for_missing=*/ false, /*prompt_for_missing=*/ false,
"tag a part's connector type for transform lookup", "tag a part's connector type (tells connect how to wire its pins)",
/*scriptable=*/ true, /*scriptable=*/ true,
/*interactive=*/ true, /*interactive=*/ true,
}; };
@@ -516,7 +530,7 @@ void Tui::RegisterCommands() {
+ (r.unbound ? (", " + std::to_string(r.unbound) + " unbound") : "")); + (r.unbound ? (", " + std::to_string(r.unbound) + " unbound") : ""));
}, },
/*prompt_for_missing=*/ false, /*prompt_for_missing=*/ false,
"attach a BSDL (.bsd) model to a part and populate pin specs", "attach a BSDL (.bsd) model to a part (fills in each pin's role and direction)",
/*scriptable=*/ true, /*scriptable=*/ true,
/*interactive=*/ false, /*interactive=*/ false,
}; };
@@ -630,7 +644,7 @@ void Tui::RegisterCommands() {
int added = FillIdentityNCs(p1, p2); int added = FillIdentityNCs(p1, p2);
Print("connect: " + info); Print("connect: " + info);
if (added > 0) if (added > 0)
Print("connect: materialised " + std::to_string(added) Print("connect: added " + std::to_string(added)
+ " NC pin(s) so both sides match"); + " NC pin(s) so both sides match");
} }
} }

View File

@@ -0,0 +1,70 @@
#include "frontends/tui/tui.hpp"
#include "core/app/export.hpp"
#include "core/domain/connect.hpp"
#include "core/domain/system.hpp"
#include <string>
#include <vector>
// Thin UI wrapper around app::export_connections — this file only resolves
// arguments / the file dialog and renders the result. All the actual export
// (CSV / ODS building, file writing) lives in src/app/export.cpp.
void Tui::RegisterExportCommands() {
commands["export"] = {
{{"kind [connections]", Completion::None},
{"filename (.csv)", Completion::Path}},
[this](const std::vector<std::string> &args) {
if (!sys) { Print("no system: run 'new' first."); return; }
if (args.empty()) {
// Bare → the generic file dialog. The CSV/ODS filter rewrites
// the extension; the action below dispatches on it.
OpenFileDialog(
"Export — connections",
"export.connections",
"connections.csv",
{{"CSV", ".csv"}, {"ODS", ".ods"}},
[this](const std::string &path) {
Dispatch("export connections " + path);
});
return;
}
if (args.size() != 2) {
Print("usage: export <kind> <file> (or no args for the dialog)");
return;
}
const std::string &kind = args[0];
const std::string &path = args[1];
if (kind != "connections") {
ShowError("export: unknown kind '" + kind + "'\n"
"Known kinds: connections");
return;
}
app::ExportFormat fmt;
if (!app::export_format_from_path(path, fmt)) {
ShowError("export: unknown extension — accepted: .csv, .ods");
return;
}
app::ExportResult r = app::export_connections(sys.get(), path, fmt);
if (!r.ok) {
ShowError("export failed:\n" + r.error);
return;
}
const bool ods = (fmt == app::ExportFormat::Ods);
std::string msg = ods ? "export connections (.ods): "
: "export connections (.csv): ";
if (ods) msg += std::to_string(r.sheets) + " sheet(s), ";
Print(msg + std::to_string(r.rows) + " wire(s) → " + path);
},
/*prompt_for_missing=*/ false,
"export structured data to CSV / ODS (kinds: connections; "
"bare form opens the file-picker dialog)",
/*scriptable=*/ true,
/*interactive=*/ true,
};
}

View File

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

View File

@@ -1,4 +1,4 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include <fstream> #include <fstream>
#include <iostream> #include <iostream>

View File

@@ -1,14 +1,15 @@
#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/bsdl_check.hpp"
#include "system/modules.hpp" #include "core/domain/connect.hpp"
#include "system/nets.hpp" #include "core/domain/modules.hpp"
#include "system/parts.hpp" #include "core/domain/nets.hpp"
#include "system/pins.hpp" #include "core/domain/parts.hpp"
#include "system/signals.hpp" #include "core/domain/pins.hpp"
#include "system/system.hpp" #include "core/domain/signals.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>
@@ -99,16 +100,35 @@ 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.
std::vector<Anomaly> model_anoms;
{
auto a1 = check_pin_specs(sys.get(), &nets);
auto a2 = check_jtag_chain(sys.get(), &nets);
auto a3 = check_source_conflicts(sys.get());
auto a4 = check_bsdl_completeness(sys.get());
model_anoms.insert(model_anoms.end(), a1.begin(), a1.end());
model_anoms.insert(model_anoms.end(), a2.begin(), a2.end());
model_anoms.insert(model_anoms.end(), a3.begin(), a3.end());
model_anoms.insert(model_anoms.end(), a4.begin(), a4.end());
}
for (const auto &a : model_anoms)
analyze_issues.push_back(std::string("[")
+ anomaly_kind_name(a.kind) + "] "
+ a.message);
int n_model = (int)model_anoms.size();
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;
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()) + (int)rep.anomalies.size() + n_model)
+ ": " + std::to_string(n_role_mismatches) + " pin-role, " + ": " + std::to_string(n_role_mismatches) + " pin-role, "
+ std::to_string(n_inconsistent) + " net-mix, " + std::to_string(n_inconsistent) + " net-mix, "
+ std::to_string(rep.anomalies.size()) + " struct.)"; + std::to_string(rep.anomalies.size()) + " struct, "
+ std::to_string(n_model) + " model)";
// ============================================================ Groups // ============================================================ Groups
analyze_groups.clear(); analyze_groups.clear();

View File

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

View File

@@ -1,11 +1,11 @@
#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/parts.hpp" #include "core/domain/parts.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include "system/transform.hpp" #include "core/domain/transform.hpp"
#include <ftxui/component/component.hpp> #include <ftxui/component/component.hpp>
#include <ftxui/component/component_options.hpp> #include <ftxui/component/component_options.hpp>
@@ -138,7 +138,7 @@ Component Tui::BuildConnectScreen() {
text(" essim ") | bold, text(" essim ") | bold,
text("") | dim, text("") | dim,
text("connect") | bold, text("connect") | bold,
text(" — wire two parts across modules (TransformRegistry-driven)") | dim, text(" — wire two parts across modules") | dim,
}); });
Element help = RenderHelpPanel("connect", { Element help = RenderHelpPanel("connect", {

View File

@@ -1,14 +1,15 @@
#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/bsdl_check.hpp"
#include "system/modules.hpp" #include "core/domain/connect.hpp"
#include "system/nets.hpp" #include "core/domain/modules.hpp"
#include "system/parts.hpp" #include "core/domain/nets.hpp"
#include "system/pins.hpp" #include "core/domain/parts.hpp"
#include "system/signals.hpp" #include "core/domain/pins.hpp"
#include "system/system.hpp" #include "core/domain/signals.hpp"
#include "core/domain/system.hpp"
#include <ftxui/component/component.hpp> #include <ftxui/component/component.hpp>
#include <ftxui/dom/elements.hpp> #include <ftxui/dom/elements.hpp>
@@ -43,6 +44,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"},
@@ -55,8 +58,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(),
@@ -148,6 +151,35 @@ Component Tui::BuildDashboardScreen() {
"NC: " + std::to_string(orph_total) + " orphan pin(s) (" "NC: " + std::to_string(orph_total) + " orphan pin(s) ("
+ std::to_string(orph_imported) + " imported, " + std::to_string(orph_imported) + " imported, "
+ std::to_string(orph_dropped) + " dropped)")); + std::to_string(orph_dropped) + " dropped)"));
// Detail nested directly under the NC row it explains. "dropped" = the
// pin was the only one on its net, so essim detached it (a heuristic;
// listing them lets the user spot a false positive). Imported NCs were
// explicit in the netlist, so they are not expanded.
if (orph_dropped > 0) {
health_rows.push_back(
text(" ↳ dropped (only 1 pin on the net):") | dim);
for (auto &dkv : dropped_by_module) {
std::sort(dkv.second.begin(), dkv.second.end(), NaturalLess);
std::string csv;
for (size_t i = 0; i < dkv.second.size(); ++i) {
if (i) csv += ", ";
csv += dkv.second[i];
}
health_rows.push_back(hbox({
text(" " + dkv.first + ": ") | bold,
text(csv) | dim,
}));
}
}
// Model-driven checks (BSDL pin specs, JTAG chain, source conflicts),
// reusing the nets computed above.
int n_model = (int)(check_pin_specs(sys.get(), &nets).size()
+ check_jtag_chain(sys.get(), &nets).size()
+ check_source_conflicts(sys.get()).size()
+ check_bsdl_completeness(sys.get()).size());
health_rows.push_back(health_line(n_model == 0,
"model: " + std::to_string(n_model) + " BSDL/JTAG anomaly(ies)"));
// ---- analysis summary ---- // ---- analysis summary ----
AnalysisReport rep = analyze_system(sys.get()); AnalysisReport rep = analyze_system(sys.get());
@@ -243,27 +275,6 @@ Component Tui::BuildDashboardScreen() {
lines.push_back(separator()); lines.push_back(separator());
lines.push_back(text(" Health") | bold); lines.push_back(text(" Health") | bold);
for (auto &h : health_rows) lines.push_back(std::move(h)); for (auto &h : health_rows) lines.push_back(std::move(h));
// Detail rows for the dropped-singleton NCs. Imported NCs are not
// expanded — they were already explicit in the netlist. Dropped NCs
// come from a heuristic, so listing them gives the user a chance to
// spot a false positive.
if (orph_dropped > 0) {
lines.push_back(hbox({
text(" dropped detail:") | dim,
}));
for (auto &dkv : dropped_by_module) {
std::sort(dkv.second.begin(), dkv.second.end(), NaturalLess);
std::string csv;
for (size_t i = 0; i < dkv.second.size(); ++i) {
if (i) csv += ", ";
csv += dkv.second[i];
}
lines.push_back(hbox({
text(" " + dkv.first + ": ") | bold,
text(csv),
}));
}
}
lines.push_back(separator()); lines.push_back(separator());
lines.push_back(text(" Analysis") | bold); lines.push_back(text(" Analysis") | bold);
lines.push_back(hbox({text("") | dim, lines.push_back(hbox({text("") | dim,
@@ -300,6 +311,9 @@ Component Tui::BuildDashboardScreen() {
{"a", "analyze (verify + groups)"}, {"a", "analyze (verify + groups)"},
{"h", "help screen"}, {"h", "help screen"},
{"x", "export"}, {"x", "export"},
{"o", "open/run a script"},
{"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"},

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include "tui/tui_helpers.hpp" #include "frontends/tui/tui_helpers.hpp"
#include <ftxui/component/component.hpp> #include <ftxui/component/component.hpp>
#include <ftxui/component/component_options.hpp> #include <ftxui/component/component_options.hpp>
@@ -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);

View File

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

View File

@@ -1,5 +1,5 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include "tui/tui_helpers.hpp" #include "frontends/tui/tui_helpers.hpp"
#include <ftxui/component/component.hpp> #include <ftxui/component/component.hpp>
#include <ftxui/component/component_options.hpp> #include <ftxui/component/component_options.hpp>
@@ -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;
}); });
} }

View File

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

View File

@@ -1,13 +1,13 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include "tui/tui_helpers.hpp" #include "frontends/tui/tui_helpers.hpp"
#include "system/modules.hpp" #include "core/domain/modules.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/pin_model.hpp" #include "core/domain/pin_model.hpp"
#include "system/pin_role.hpp" #include "core/domain/pin_role.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include "system/transform_vpx.hpp" #include "core/domain/transform_vpx.hpp"
#include <ftxui/component/component.hpp> #include <ftxui/component/component.hpp>
#include <ftxui/component/component_options.hpp> #include <ftxui/component/component_options.hpp>
@@ -146,7 +146,7 @@ Component Tui::BuildSettypeScreen() {
text(" essim ") | bold, text(" essim ") | bold,
text("") | dim, text("") | dim,
text("set-connector-type") | bold, text("set-connector-type") | bold,
text(" — tag a part with its connector kind (drives transforms + pin roles)") | dim, text(" — tag a part's connector type (used to wire it correctly)") | dim,
}); });
Element help = RenderHelpPanel("set-connector-type", { Element help = RenderHelpPanel("set-connector-type", {

View File

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

View File

@@ -1,9 +1,9 @@
#include "tui/tui.hpp" #include "frontends/tui/tui.hpp"
#include "tui/tui_helpers.hpp" #include "frontends/tui/tui_helpers.hpp"
#include "system/modules.hpp" #include "core/domain/modules.hpp"
#include "system/signals.hpp" #include "core/domain/signals.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include <cctype> #include <cctype>
#include <chrono> #include <chrono>
@@ -296,8 +296,10 @@ void Tui::Source(const std::string &filename) {
loading_executed = 0; loading_executed = 0;
loading_lineno = 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.
@@ -346,11 +348,12 @@ void Tui::ProcessNextSourceLine() {
Submit(); Submit();
++loading_executed; ++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(loading_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); 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;
return; return;
@@ -363,6 +366,7 @@ void Tui::ProcessNextSourceLine() {
Print("source: " + loading_filename Print("source: " + loading_filename
+ " (" + std::to_string(loading_executed) + " line(s))"); + " (" + 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;
} }

View File

@@ -1,10 +1,11 @@
#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;
@@ -58,10 +59,31 @@ 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 progress = std::to_string(loading_executed) + " / "
+ std::to_string((int)loading_lines.size()) + " lines";
return vbox({
text(" Computing… ") | bold | center,
separator(),
text(loading_filename) | 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; }
@@ -101,6 +123,33 @@ void Tui::Run() {
if (e == Event::Character("a")) { screen_idx = 5; return true; } if (e == Event::Character("a")) { screen_idx = 5; return true; }
if (e == Event::Character("h")) { screen_idx = 6; return true; } if (e == Event::Character("h")) { screen_idx = 6; return true; }
if (e == Event::Character("x")) { Dispatch("export"); return true; } if (e == Event::Character("x")) { Dispatch("export"); return true; }
if (e == Event::Character("o")) { // open / run a script
OpenFileDialog("Open script — run a .essim file",
"dashboard.source", "", {},
[this](const std::string &path) {
Dispatch("source " + path);
},
/*confirm_overwrite=*/false); // opening, not saving
return true;
}
if (e == Event::Character("s")) { // save the system snapshot
if (sys)
OpenFileDialog("Save system snapshot",
"dashboard.save", "system.essim", {},
[this](const std::string &path) {
Dispatch("save " + path);
});
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
@@ -135,7 +184,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;

View File

@@ -103,6 +103,8 @@ class Tui {
int loading_executed; int loading_executed;
int loading_lineno; int loading_lineno;
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 +151,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;
@@ -272,11 +275,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();

View File

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

View File

@@ -1,28 +0,0 @@
#ifndef _BSDL_CHECK_HPP_
#define _BSDL_CHECK_HPP_
#include "analysis.hpp" // Anomaly, AnomalyKind
#include <vector>
class System;
// Model-driven pin checks over the system's nets, using the PinSpec
// direction/function populated by connector or BSDL models. Emits:
// - DriveContention : a net with ≥2 push-pull output drivers;
// - UndrivenNet : a multi-pin net with input(s) but no driver;
// - NcWired : a no-connect pin wired onto a multi-pin net.
// Read-only; nets with no direction data are skipped (no false positives on
// un-modelled parts).
std::vector<Anomaly> check_pin_specs(System *sys);
// JTAG boundary-scan chain integrity, using pins whose PinSpec.function is a TAP
// role (JtagTdi/Tdo/Tms/Tck/Trst). Resolves each TAP pin to its net and checks:
// - JtagTapIncomplete : a TAP device missing TDI/TDO/TMS/TCK;
// - JtagBusUnbridged : TMS or TCK not common to every TAP device;
// - JtagChainBreak : the TDO→TDI daisy chain dangles, fans out, or is not a
// single path (≠1 head / ≠1 tail).
// Empty when the system has no TAP pins.
std::vector<Anomaly> check_jtag_chain(System *sys);
#endif // _BSDL_CHECK_HPP_

View File

@@ -1,235 +0,0 @@
#include "tui/tui.hpp"
#include "tui/tui_helpers.hpp"
#include "imports/ods_writer.hpp"
#include "system/connect.hpp"
#include "system/modules.hpp"
#include "system/parts.hpp"
#include "system/pins.hpp"
#include "system/signal_type.hpp"
#include "system/signals.hpp"
#include "system/system.hpp"
#include <fstream>
#include <string>
#include <vector>
namespace {
// Minimal CSV quoter — wraps in `"…"` and doubles internal quotes when
// the field contains a comma, quote, or newline. Local to this file.
std::string csv_quote(const std::string &s) {
bool needs = s.find_first_of(",\"\n") != std::string::npos;
if (!needs) return s;
std::string out = "\"";
for (char c : s) { if (c == '"') out += '"'; out += c; }
out += '"';
return out;
}
// Flatten one pin into the 6 string slots the export row uses.
void pin_side(Pin *p, std::string &mod, std::string &part,
std::string &pin, std::string &sig,
std::string &type, std::string &suspect) {
if (!p) { mod = part = pin = sig = type = suspect = ""; return; }
mod = (p->prnt && p->prnt->prnt) ? p->prnt->prnt->name : "";
part = p->prnt ? p->prnt->name : "";
pin = p->name;
Signal *s = p->signal();
if (!s) {
sig = ""; type = "(NC)"; suspect = "";
} else {
sig = s->name;
type = signal_type_name(s->type);
suspect = (infer_signal_type(s->name) == SignalType::Power
&& s->type == SignalType::Other) ? "yes" : "no";
}
}
} // namespace
void Tui::RegisterExportCommands() {
commands["export"] = {
{{"kind [connections]", Completion::None},
{"filename (.csv)", Completion::Path}},
[this](const std::vector<std::string> &args) {
if (!sys) { Print("no system: run 'new' first."); return; }
if (args.empty()) {
// Bare → reuse the generic file dialog. Filters give a
// one-keystroke CSV/ODS toggle; picking either rewrites
// the filename's extension, and the action below
// dispatches on that extension.
OpenFileDialog(
"Export — connections",
"export.connections",
"connections.csv",
{{"CSV", ".csv"}, {"ODS", ".ods"}},
[this](const std::string &path) {
Dispatch("export connections " + path);
});
return;
}
if (args.size() != 2) {
Print("usage: export <kind> <file> (or no args for the dialog)");
return;
}
const std::string &kind = args[0];
const std::string &path = args[1];
if (kind == "connections") {
// Accepted extensions: `.csv` (flat file) and `.ods`
// (one sheet per connection). Anything else is an error.
std::string ext;
{
size_t dot = path.rfind('.');
if (dot != std::string::npos) ext = ToLower(path.substr(dot));
}
bool ods = (ext == ".ods");
bool csv = (ext == ".csv");
if (!ods && !csv) {
ShowError("export: unknown extension '"
+ (ext.empty() ? std::string("(none)") : ext)
+ "'\nAccepted: .csv, .ods");
return;
}
if (ods) {
OdsWriter w;
int total = 0;
for (auto &ckv : *sys->connections()) {
Connection *c = ckv.second;
// Sheet names: Excel rejects /\?*:[] characters,
// ODS forbids < > & in raw cell/table names.
// Sanitise to underscores; clip to 31 chars
// (Excel's hard limit).
std::string sname = c->name;
for (char &ch : sname)
if (ch == '/' || ch == '\\' || ch == '?' || ch == '*'
|| ch == ':' || ch == '[' || ch == ']'
|| ch == '<' || ch == '>' || ch == '&') ch = '_';
if (sname.size() > 31) sname = sname.substr(0, 31);
OdsSheet *s = w.add_sheet(sname);
// Pull the constants for this connection once.
// `transform`, the left module/part, and the
// right module/part don't vary across the wires
// of a single connection — putting them in
// every row was repetitive.
std::string lmod, lprt;
std::string rmod, rprt;
if (c->m1) lmod = c->m1->name;
if (c->p1) lprt = c->p1->name;
if (c->m2) rmod = c->m2->name;
if (c->p2) rprt = c->p2->name;
// Meta header above the table: 5 rows of label /
// value, then a blank, then the column headers
// on row 6 (index 5).
auto meta = [&](int r, const std::string &k,
const std::string &v) {
s->set(r, 0, k);
s->set(r, 1, v);
};
meta(0, "Connection", c->name);
meta(1, "Transform", c->transform_name);
meta(2, "Left", lmod + " / " + lprt);
meta(3, "Right", rmod + " / " + rprt);
// Row 4 left blank by design.
const int HDR = 5;
s->set_header_row(HDR);
const char *hdr[] = {
"left_pin", "left_signal", "left_type", "left_suspect",
"right_pin", "right_signal", "right_type", "right_suspect",
"type_mismatch"};
for (int i = 0; i < 9; ++i) s->set(HDR, i, hdr[i]);
int row = HDR + 1;
for (auto &wp : c->pin_map) {
std::string lm, lp, ln, ls, lt, lsus;
std::string rm, rp, rn, rs, rt, rsus;
pin_side(wp.first, lm, lp, ln, ls, lt, lsus);
pin_side(wp.second, rm, rp, rn, rs, rt, rsus);
// `type_mismatch = yes` when both sides have a
// real signal AND their types disagree (e.g.
// Power ↔ Gnd, or Power ↔ Other).
std::string tm = (lt != "(NC)" && rt != "(NC)" && lt != rt)
? "yes" : "no";
s->set(row, 0, ln); s->set(row, 1, ls);
s->set(row, 2, lt); s->set(row, 3, lsus);
s->set(row, 4, rn); s->set(row, 5, rs);
s->set(row, 6, rt); s->set(row, 7, rsus);
s->set(row, 8, tm);
++row; ++total;
}
}
std::string err;
if (!w.save(path, err)) {
ShowError("export (.ods) failed:\n" + err);
return;
}
Print("export connections (.ods): "
+ std::to_string(sys->connections()->size())
+ " sheet(s), " + std::to_string(total)
+ " wire(s) → " + path);
return;
}
// Classic flat CSV: a single rectangular table — one
// header line, N data rows, every row carries the per-
// connection constants too. The 9 right-most column
// names match the ODS sheet headers exactly; the 5
// leading ones (connection, transform, left_module,
// left_part, right_module, right_part) correspond to
// the ODS meta block (Connection, Transform, Left,
// Right). Repeating the constants per row keeps the
// file parser-friendly (pandas / awk / spreadsheet).
std::ofstream f(path);
if (!f) {
ShowError("export: cannot open '" + path + "' for writing");
return;
}
f << "connection,transform,"
"left_module,left_part,"
"left_pin,left_signal,left_type,left_suspect,"
"right_module,right_part,"
"right_pin,right_signal,right_type,right_suspect,"
"type_mismatch\n";
int rows = 0;
for (auto &ckv : *sys->connections()) {
Connection *c = ckv.second;
for (auto &wp : c->pin_map) {
std::string lm, lp, ln, ls, lt, lsus;
std::string rm, rp, rn, rs, rt, rsus;
pin_side(wp.first, lm, lp, ln, ls, lt, lsus);
pin_side(wp.second, rm, rp, rn, rs, rt, rsus);
std::string tm = "no";
if (lt != "(NC)" && rt != "(NC)" && lt != rt) tm = "yes";
f << csv_quote(c->name) << ','
<< csv_quote(c->transform_name) << ','
<< csv_quote(lm) << ',' << csv_quote(lp) << ','
<< csv_quote(ln) << ',' << csv_quote(ls) << ','
<< csv_quote(lt) << ',' << csv_quote(lsus) << ','
<< csv_quote(rm) << ',' << csv_quote(rp) << ','
<< csv_quote(rn) << ',' << csv_quote(rs) << ','
<< csv_quote(rt) << ',' << csv_quote(rsus) << ','
<< tm << '\n';
++rows;
}
}
Print("export connections (.csv): " + std::to_string(rows)
+ " wire(s) → " + path);
return;
}
ShowError("export: unknown kind '" + kind + "'\n"
"Known kinds: connections");
},
/*prompt_for_missing=*/ false,
"export structured data to CSV / ODS (kinds: connections; "
"bare form opens the file-picker dialog)",
/*scriptable=*/ true,
/*interactive=*/ true,
};
}

View File

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

View File

@@ -1,12 +1,14 @@
#include <doctest/doctest.h> #include <doctest/doctest.h>
#include "system/bsdl_model.hpp" #include "core/domain/analysis.hpp"
#include "system/parts.hpp" #include "core/domain/bsdl_check.hpp"
#include "system/pins.hpp" #include "core/domain/bsdl_model.hpp"
#include "system/pin_spec.hpp" #include "core/domain/parts.hpp"
#include "system/system.hpp" #include "core/domain/pins.hpp"
#include "system/modules.hpp" #include "core/domain/pin_spec.hpp"
#include "system/persist.hpp" #include "core/domain/system.hpp"
#include "core/domain/modules.hpp"
#include "core/domain/persist.hpp"
#include <cstdio> #include <cstdio>
#include <fstream> #include <fstream>
@@ -141,3 +143,27 @@ TEST_CASE("attached BSDL path persists and re-applies on restore") {
std::remove(bsd); std::remove(bsd);
std::remove(snap); std::remove(snap);
} }
TEST_CASE("check_bsdl_completeness flags a device power pin absent from the part") {
const char *bsd = "test_complete_demo.bsd";
{ std::ofstream o(bsd); o << DEMO_BSDL; }
System sys;
Module *m = sys.modules()->merge("CARD");
Part *u = new Part("U1");
// All pins EXCEPT VDD (a power port at ball C1) → its port is unmatched.
for (const char *n : {"TCK", "TDI", "TDO", "TMS", "IO1", "GND"})
u->add(new Pin(n));
u->bsdl_path = bsd;
m->add(u);
auto a = check_bsdl_completeness(&sys);
REQUIRE(a.size() == 1);
CHECK(a[0].kind == AnomalyKind::BsdlPinMissing);
// With VDD present (by ball or by name), no completeness issue.
u->add(new Pin("VDD"));
CHECK(check_bsdl_completeness(&sys).empty());
std::remove(bsd);
}

View File

@@ -1,13 +1,13 @@
#include <doctest/doctest.h> #include <doctest/doctest.h>
#include "system/analysis.hpp" #include "core/domain/analysis.hpp"
#include "system/bsdl_check.hpp" #include "core/domain/bsdl_check.hpp"
#include "system/modules.hpp" #include "core/domain/modules.hpp"
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/pin_spec.hpp" #include "core/domain/pin_spec.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include "system/signals.hpp" #include "core/domain/signals.hpp"
#include "system/system.hpp" #include "core/domain/system.hpp"
#include <algorithm> #include <algorithm>
#include <vector> #include <vector>
@@ -159,3 +159,23 @@ TEST_CASE("check_jtag_chain reports an incomplete TAP") {
auto a = check_jtag_chain(&sys); auto a = check_jtag_chain(&sys);
CHECK(count_kind(a, AnomalyKind::JtagTapIncomplete) == 1); CHECK(count_kind(a, AnomalyKind::JtagTapIncomplete) == 1);
} }
TEST_CASE("check_source_conflicts flags a BSDL rail left unconnected") {
System sys;
Module *m = sys.modules()->merge("M");
Part *u = new Part("U1");
m->add(u);
// A BSDL power pin with no signal → conflict (a rail floated in the netlist).
Pin *vcc = new Pin("VCC");
vcc->spec.function = PinFunction::Power;
vcc->spec.source = SpecSource::Bsdl;
u->add(vcc);
// A BSDL ground pin that IS connected → no conflict.
Pin *gnd = mkpin(u, "GND", PinDirection::Power, PinFunction::Ground);
wire(m, "GNDNET", {gnd, mkpin(u, "X", PinDirection::Out, PinFunction::Signal)});
auto a = check_source_conflicts(&sys);
CHECK(count_kind(a, AnomalyKind::SourceConflict) == 1);
}

View File

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

76
tests/test_export.cpp Normal file
View File

@@ -0,0 +1,76 @@
#include <doctest/doctest.h>
#include "core/app/export.hpp"
#include "core/domain/connect.hpp"
#include "core/domain/modules.hpp"
#include "core/domain/parts.hpp"
#include "core/domain/pins.hpp"
#include "core/domain/signals.hpp"
#include "core/domain/system.hpp"
#include <cstdio>
#include <fstream>
#include <sstream>
#include <string>
namespace {
std::string slurp(const std::string &path)
{
std::ifstream f(path);
std::stringstream ss;
ss << f.rdbuf();
return ss.str();
}
} // namespace
TEST_CASE("export_format_from_path maps extensions") {
app::ExportFormat f;
CHECK(app::export_format_from_path("a.csv", f));
CHECK(f == app::ExportFormat::Csv);
CHECK(app::export_format_from_path("a.ODS", f));
CHECK(f == app::ExportFormat::Ods);
CHECK_FALSE(app::export_format_from_path("a.txt", f));
CHECK_FALSE(app::export_format_from_path("noext", f));
}
TEST_CASE("export_connections writes a flat CSV (no UI needed)") {
// Two cards, one wired pin pair via a connection.
System sys;
Module *a = sys.modules()->merge("A");
Module *b = sys.modules()->merge("B");
Part *ja = new Part("J1"); a->add(ja);
Part *jb = new Part("P1"); b->add(jb);
Pin *pa = new Pin("1"); ja->add(pa);
Pin *pb = new Pin("1"); jb->add(pb);
Signal *sa = a->signals->merge("NETA"); sa->add(pa); pa->connect(sa);
Signal *sb = b->signals->merge("NETB"); sb->add(pb); pb->connect(sb);
Connection *c = new Connection("A.J1<->B.P1", a, ja, b, jb);
c->transform_name = "identity";
c->pin_map.emplace_back(pa, pb);
sys.connections()->add(c);
const char *path = "test_export_out.csv";
app::ExportResult r = app::export_connections(&sys, path, app::ExportFormat::Csv);
CHECK(r.ok);
CHECK(r.rows == 1);
std::string out = slurp(path);
CHECK(out.find("connection,transform,") == 0); // header present
CHECK(out.find("A.J1<->B.P1") != std::string::npos); // connection name
CHECK(out.find("identity") != std::string::npos); // transform
CHECK(out.find("NETA") != std::string::npos); // left signal
CHECK(out.find("NETB") != std::string::npos); // right signal
std::remove(path);
}
TEST_CASE("export_connections reports a bad path instead of crashing") {
System sys;
app::ExportResult r = app::export_connections(
&sys, "/nonexistent-dir-xyz/out.csv", app::ExportFormat::Csv);
CHECK_FALSE(r.ok);
CHECK_FALSE(r.error.empty());
}

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
#include <doctest/doctest.h> #include <doctest/doctest.h>
#include "system/parts.hpp" #include "core/domain/parts.hpp"
#include "system/pin_model.hpp" #include "core/domain/pin_model.hpp"
#include "system/pin_spec.hpp" #include "core/domain/pin_spec.hpp"
#include "system/pins.hpp" #include "core/domain/pins.hpp"
#include <string> #include <string>
#include <vector> #include <vector>
@@ -68,3 +68,18 @@ TEST_CASE("apply_model does not overwrite a spec the model is silent about") {
CHECK(part.get("DATA")->spec.function == PinFunction::Signal); CHECK(part.get("DATA")->spec.function == PinFunction::Signal);
CHECK(part.get("DATA")->spec.source == SpecSource::Bsdl); CHECK(part.get("DATA")->spec.source == SpecSource::Bsdl);
} }
TEST_CASE("apply_model never overwrites a higher-precedence source") {
Part part("U3");
Pin *p = new Pin("VCC");
p->spec.function = PinFunction::Ground; // user-set, deliberately != the model
p->spec.source = SpecSource::UserOverride;
part.add(p);
FakeModel m; // would set VCC = Power / Bsdl
apply_model(&part, m);
// UserOverride (rank 5) outranks Bsdl (rank 4): kept untouched.
CHECK(part.get("VCC")->spec.source == SpecSource::UserOverride);
CHECK(part.get("VCC")->spec.function == PinFunction::Ground);
}

View File

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

View File

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

View File

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

View File

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