init commit
This commit is contained in:
commit
dd28afc1ef
|
|
@ -0,0 +1,3 @@
|
|||
Language: Cpp
|
||||
ColumnLimit: 120
|
||||
IndentPPDirectives: AfterHash
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
cmake_minimum_required(VERSION 3.13)
|
||||
set(CMAKE_POLICY_VERSION_MINIMUM 3.5)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 23)
|
||||
set(CMAKE_BUILD_TYPE Debug)
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
set(CMAKE_MESSAGE_LOG_LEVEL TRACE)
|
||||
set(CMAKE_VERBOSE_MAKEFILE OFF)
|
||||
set(CMAKE_CXX_COMPILER /usr/bin/clang++ CACHE INTERNAL "cxx compiler")
|
||||
option(BUILD_SHARED_LIBS "Build using shared libraries" OFF)
|
||||
|
||||
include(FetchContent)
|
||||
set(FETCHCONTENT_QUIET FALSE)
|
||||
|
||||
FetchContent_Declare(
|
||||
tpl
|
||||
GIT_REPOSITORY https://gitlab.com/eidheim/tiny-process-library.git
|
||||
GIT_TAG 8bbb5a211c5c9df8ee69301da9d22fb977b27dc1
|
||||
GIT_PROGRESS TRUE
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(tpl)
|
||||
FetchContent_Declare(
|
||||
ftxui
|
||||
GIT_REPOSITORY https://github.com/ArthurSonzogni/FTXUI.git
|
||||
GIT_TAG v6.0.0
|
||||
GIT_SHALLOW TRUE
|
||||
GIT_PROGRESS TRUE
|
||||
PATCH_COMMAND git apply --check ${CMAKE_SOURCE_DIR}/thirdparty/ftxui-empty-container.patch 2>/dev/null && git apply ${CMAKE_SOURCE_DIR}/thirdparty/ftxui-empty-container.patch || true COMMAND git apply --check ${CMAKE_SOURCE_DIR}/thirdparty/ftxui-window.patch 2>/dev/null && git apply ${CMAKE_SOURCE_DIR}/thirdparty/ftxui-window.patch || true
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(ftxui)
|
||||
FetchContent_Declare(
|
||||
sqlite_modern
|
||||
GIT_REPOSITORY https://github.com/SqliteModernCpp/sqlite_modern_cpp
|
||||
GIT_TAG 6e3009973025e0016d5573529067714201338c80
|
||||
GIT_PROGRESS TRUE
|
||||
)
|
||||
|
||||
FetchContent_GetProperties(sqlite_modern)
|
||||
if(NOT sqlite_modern_POPULATED)
|
||||
FetchContent_Populate(sqlite_modern)
|
||||
endif()
|
||||
|
||||
|
||||
option(STATIC "Set to ON to build xlnt as a static library instead of a shared library" OFF)
|
||||
FetchContent_Declare(
|
||||
xlnt
|
||||
GIT_REPOSITORY https://github.com/xlnt-community/xlnt.git
|
||||
GIT_TAG e165887739147027e7fbab918280b88f9efa5ffb
|
||||
GIT_PROGRESS TRUE
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(xlnt)
|
||||
|
||||
# Suppress warnings in xlnt
|
||||
if(TARGET xlnt)
|
||||
target_compile_options(xlnt PRIVATE
|
||||
-Wno-unsafe-buffer-usage-in-libc-call
|
||||
-Wno-unsafe-buffer-usage
|
||||
-Wno-undefined-reinterpret-cast
|
||||
-Wno-extra-semi-stmt
|
||||
-Wno-sign-conversion
|
||||
-Wno-old-style-cast
|
||||
-Wno-switch-default
|
||||
-Wno-nrvo
|
||||
-Wno-reserved-identifier
|
||||
-Wno-unused-but-set-variable
|
||||
-Wno-missing-prototypes
|
||||
-Wno-character-conversion
|
||||
-Wno-implicit-int-float-conversion
|
||||
-Wno-float-equal
|
||||
-Wno-global-constructors
|
||||
-Wno-unique-object-duplication
|
||||
)
|
||||
endif()
|
||||
|
||||
set(FMT_TEST OFF CACHE BOOL "" FORCE)
|
||||
set(FMT_DOC OFF CACHE BOOL "" FORCE)
|
||||
FetchContent_Declare(
|
||||
fmt
|
||||
GIT_REPOSITORY https://github.com/fmtlib/fmt
|
||||
GIT_TAG 11.1.4
|
||||
GIT_SHALLOW TRUE
|
||||
GIT_PROGRESS TRUE
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(fmt)
|
||||
set(JSON_BuildTests OFF CACHE BOOL "" FORCE)
|
||||
set(JSON_Install OFF CACHE BOOL "" FORCE)
|
||||
FetchContent_Declare(
|
||||
json
|
||||
GIT_REPOSITORY https://github.com/nlohmann/json
|
||||
GIT_TAG v3.12.0
|
||||
GIT_SHALLOW TRUE
|
||||
GIT_PROGRESS TRUE
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(json)
|
||||
set(SPDLOG_FMT_EXTERNAL OFF CACHE BOOL "" FORCE)
|
||||
FetchContent_Declare(
|
||||
spdlog
|
||||
GIT_REPOSITORY https://github.com/gabime/spdlog
|
||||
GIT_TAG v1.15.3
|
||||
GIT_SHALLOW TRUE
|
||||
GIT_PROGRESS TRUE
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(spdlog)
|
||||
|
||||
project(canscope)
|
||||
file(GLOB_RECURSE SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/*.hpp ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
|
||||
add_executable(${CMAKE_PROJECT_NAME} ${SOURCES})
|
||||
target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_SOURCE_DIR}/thirdparty
|
||||
${ftxui_SOURCE_DIR}/include
|
||||
${tpl_SOURCE_DIR}/include
|
||||
${xlnt_SOURCE_DIR}/include
|
||||
${fmt_SOURCE_DIR}/include
|
||||
${json_SOURCE_DIR}/include
|
||||
${sqlite_modern_SOURCE_DIR}/hdr
|
||||
)
|
||||
|
||||
target_link_directories(${CMAKE_PROJECT_NAME}
|
||||
PRIVATE
|
||||
${ftxui_BINARY_DIR}
|
||||
${tpl_BINARY_DIR}
|
||||
${xlnt_BINARY_DIR}
|
||||
${fmt_BINARY_DIR}
|
||||
${json_BINARY_DIR}
|
||||
#{sqlite_modern_BINARY_DIR}
|
||||
)
|
||||
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE ftxui-component ftxui-screen ftxui-dom tiny-process-library xlnt sqlite3 spdlog::spdlog systemd z ${Boost_LIBRARIES})
|
||||
# target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC sqlite3 ${Boost_LIBRARIES})
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
# {canscope}
|
||||
|
||||
CAN bus sniffer and SAE J1939 protocol analyzer. Reads CAN frames from an external process (e.g. `candump`), decodes them using a J1939 Digital Annex (xlsx), and presents results in an interactive terminal UI or as JSON output.
|
||||
|
||||
## Features
|
||||
|
||||
- **TUI mode** -- full-screen interactive terminal interface (FTXUI). Multiple display modes per CAN ID: deployed, brief, verbose, manual, little-endian
|
||||
- **Headless mode** -- JSON output to stdout or file, for scripting and automation
|
||||
- **Recording** -- decoded J1939 SPN values saved to SQLite database with gzip compression and batch flushing
|
||||
- **J1939 decoding** -- PGN/SPN lookup, bit-level value extraction from payload
|
||||
- **CAN playback** -- replay recorded CAN frames
|
||||
- **Custom SPN configuration** -- per-parameter settings, parameter export
|
||||
- **Real-time** -- 30 fps UI refresh
|
||||
|
||||
## Build
|
||||
|
||||
**Requirements:**
|
||||
- clang++ with C++23 support
|
||||
- CMake >= 3.13
|
||||
- System libraries: boost (signals2, spirit, phoenix), sqlite3, systemd, zlib
|
||||
|
||||
The rest of the dependencies are fetched automatically via CMake FetchContent (FTXUI, tiny-process-library, sqlite_modern_cpp, xlnt, fmt, nlohmann/json, spdlog).
|
||||
|
||||
```bash
|
||||
cmake -B build -S . && cmake --build build -j$(nproc)
|
||||
```
|
||||
|
||||
Binary: `build/canscope`
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# TUI mode (default)
|
||||
./build/canscope -e "candump can0" -j1939 thirdparty/j1939da_2018.xlsx
|
||||
|
||||
# Headless -- JSON to stdout
|
||||
./build/canscope -hl -e "candump can0" -j1939 thirdparty/j1939da_2018.xlsx
|
||||
|
||||
# Headless -- JSON to file
|
||||
./build/canscope -hl -e "candump can0" -j1939 thirdparty/j1939da_2018.xlsx -of output.json
|
||||
|
||||
# Record to SQLite database
|
||||
./build/canscope -rec -db recording.db -e "candump can0" -j1939 thirdparty/j1939da_2018.xlsx
|
||||
|
||||
# Record + TUI
|
||||
./build/canscope -rec -db recording.db -tui -e "candump can0" -j1939 thirdparty/j1939da_2018.xlsx
|
||||
```
|
||||
|
||||
### CLI flags
|
||||
|
||||
| Flag | Long form | Description |
|
||||
|------|-----------|-------------|
|
||||
| `-j1939` | `--j1939-document` | **(required)** J1939 Digital Annex xlsx file |
|
||||
| `-e` | `--execute-command` | Command to read CAN frames from (e.g. `"candump can0"`) |
|
||||
| `-hl` | `--headless` | Headless mode (no TUI) |
|
||||
| `-of` | `--output-file` | Output file path (headless mode) |
|
||||
| `-rec` | `--record` | Record decoded values to SQLite |
|
||||
| `-db` | `--database` | SQLite database path (required with `-rec`) |
|
||||
| `-tui` | | Show TUI alongside recording |
|
||||
| `-h` | `--help` | Show help |
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
candump / other CAN source
|
||||
| stdout
|
||||
v
|
||||
aggregator_task ──> shared JSON (mutex-protected)
|
||||
|
|
||||
diff_task (33ms)
|
||||
|
|
||||
"new_entry" signal
|
||||
/ \
|
||||
TUI headless/recorder
|
||||
```
|
||||
|
||||
### Key components
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `main.cpp` | Entry point, CLI parsing (clipp), async task orchestration |
|
||||
| `xlsx.cpp` | Parses J1939 xlsx into in-memory SQLite (pgns, spns, spn_fragments) |
|
||||
| `can_frame.cpp` | Decodes CAN frame against SQLite DB, extracts SPN values |
|
||||
| `parsers.hpp` | Boost.Spirit Qi grammars for J1939 field formats |
|
||||
| `signals.hpp` | Type-safe signal map (boost::signals2) |
|
||||
| `recorder.cpp` | SQLite recording with gzip compression |
|
||||
| `mainform.cpp` | Root FTXUI component |
|
||||
| `canid_unit.cpp` | Per-CAN-ID display component |
|
||||
| `headless.cpp` | Headless JSON output handler |
|
||||
|
||||
### Patterns
|
||||
|
||||
- Components communicate through a type-safe signal map (`signals_map_t`)
|
||||
- `nlohmann::json` as universal data interchange between all layers
|
||||
- Factory functions via `extern` declarations instead of header includes
|
||||
- `std::jthread` / `std::stop_token` for async task management
|
||||
- SIGINT gracefully stops all background tasks
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
class BitStream {
|
||||
|
||||
public:
|
||||
uint8_t *toByteArray() { return m_data_; }
|
||||
|
||||
void openBytes(uint8_t *bytes, size_t size) {
|
||||
m_data_ = bytes;
|
||||
m_size_ = size;
|
||||
}
|
||||
|
||||
void open(size_t size) { m_data_ = reinterpret_cast<uint8_t *>(std::malloc(size)); }
|
||||
void close() { std::free(m_data_); }
|
||||
|
||||
void write(size_t index, size_t bits_size, size_t data) {
|
||||
index += bits_size;
|
||||
while (data) {
|
||||
m_data_[index / UINT8_WIDTH] = chbit_(m_data_[index / UINT8_WIDTH], index % UINT8_WIDTH, data & 1u);
|
||||
data >>= 1u;
|
||||
index--;
|
||||
}
|
||||
}
|
||||
|
||||
size_t read(size_t index, size_t bits_size) {
|
||||
size_t dat = 0;
|
||||
for (int32_t i = index; i < index + bits_size; i++) {
|
||||
dat = dat * 2 + getbit_(m_data_[i / 8], i % 8);
|
||||
}
|
||||
|
||||
return dat;
|
||||
}
|
||||
|
||||
private:
|
||||
bool getbit_(char x, int y) { return (x >> (7 - y)) & 1; }
|
||||
size_t chbit_(size_t x, size_t i, bool v) {
|
||||
if (v) {
|
||||
return x | (1u << (7u - i));
|
||||
}
|
||||
|
||||
return x & ~(1u << (7u - i));
|
||||
}
|
||||
|
||||
uint8_t *m_data_;
|
||||
size_t m_size_;
|
||||
};
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Mutex protecting the J1939 SQLite database from concurrent access
|
||||
extern std::mutex g_j1939_db_mtx;
|
||||
|
||||
struct can_frame_data_s {
|
||||
std::vector<uint8_t> payload;
|
||||
int32_t size = 0;
|
||||
};
|
||||
|
||||
struct can_frame_diff_s {
|
||||
bool is_new_interface = false;
|
||||
bool is_new_canid = false;
|
||||
std::vector<bool> payload_changed;
|
||||
};
|
||||
|
||||
struct can_frame_update_s {
|
||||
std::string iface;
|
||||
std::string canid;
|
||||
can_frame_data_s data;
|
||||
can_frame_diff_s diff;
|
||||
std::shared_ptr<nlohmann::json> verbose;
|
||||
std::shared_ptr<nlohmann::json> brief;
|
||||
};
|
||||
|
||||
// Convert verbose processFrame JSON to export format for a single CAN ID
|
||||
nlohmann::json verboseToExportJson(const nlohmann::json &verbose);
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
#include "can_data.hpp"
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#define FMT_HEADER_ONLY
|
||||
#include <fmt/format.h>
|
||||
#include <fmt/ranges.h>
|
||||
|
||||
// For sqlite
|
||||
#include "sqlite_modern_cpp.h"
|
||||
|
||||
std::pair<nlohmann::json, nlohmann::json> processFrame(sqlite::database &db, const std::string &iface,
|
||||
const std::string &canid, const std::vector<uint8_t> &data) {
|
||||
nlohmann::json verbose, brief;
|
||||
db << R"(SELECT pgn, pg_label, pg_acronym, pg_descr, edp, dp, pf, ps, pg_datalen, pg_priority FROM pgns WHERE pgn = ?;)"
|
||||
<< [&]() -> int32_t {
|
||||
int32_t ret;
|
||||
std::stringstream{} << std::hex << canid.substr(2, 4) >> ret;
|
||||
return ret;
|
||||
}() >> [&](int32_t pgn, const std::string &pg_label, const std::string &pg_acronym, const std::string &pg_descr,
|
||||
int32_t edp, int32_t dp, int32_t pf, int32_t ps, int32_t pd_datalen, int32_t pg_priority) {
|
||||
int32_t pgn_int;
|
||||
std::stringstream{} << std::hex << canid.substr(2, 4) >> pgn_int;
|
||||
|
||||
// Make header
|
||||
verbose = {
|
||||
{"Interface", iface}, {"Can ID", canid}, {"PGN", canid.substr(2, 4)}, {"PGN (integer)", pgn_int},
|
||||
{"Acronym", pg_acronym}, {"Label", pg_label}, {"Description", pg_descr},
|
||||
};
|
||||
|
||||
// Get SPNs
|
||||
nlohmann::json::array_t spns_array;
|
||||
db << R"(SELECT spn, spn_name, spn_pos, spn_length, resolution, offset, data_range, min_value, max_value, units, slot_id, slot_name, spn_type FROM spns WHERE pgn = ?;)"
|
||||
<< pgn_int >>
|
||||
[&](int32_t spn, const std::string &spn_name, const std::string &spn_pos, int32_t spn_length, double resolution,
|
||||
int32_t offset, const std::string &data_range, double min, double max, const std::string &unit,
|
||||
const std::string &slot_id, const std::string &slot_name, const std::string &spn_type) {
|
||||
nlohmann::json spn_json = {
|
||||
{"SPN (integer)", spn},
|
||||
{"SPN name", spn_name},
|
||||
{"SPN position", spn_pos},
|
||||
{"SPN length (bits)", spn_length},
|
||||
{"Resolution", resolution},
|
||||
{"Offset", offset},
|
||||
{"Minimum value", min},
|
||||
{"Maximum value", max},
|
||||
{"Unit", unit},
|
||||
{"SLOT id", slot_id},
|
||||
{"SPN type", spn_type},
|
||||
};
|
||||
|
||||
// Get parts
|
||||
size_t result = 0u, iter = 0u, total_size_bits = 0u;
|
||||
nlohmann::json::array_t parts_array;
|
||||
db << "SELECT byte_offset,bit_offset,size FROM spn_fragments WHERE spn = ? AND pgn = ?" << spn << pgn >>
|
||||
[&, byte_array = data](int32_t byte_offset, int32_t bit_offset, int32_t size_bits) {
|
||||
result <<= size_bits;
|
||||
total_size_bits += size_bits;
|
||||
for (uint32_t i = 0; i < ((size_bits / UINT8_WIDTH) + (size_bits % UINT8_WIDTH ? 1 : 0)); ++i) {
|
||||
const uint8_t &byte = byte_array[byte_offset + i];
|
||||
result |= (((byte << (i * UINT8_WIDTH)) &
|
||||
(size_bits % UINT8_WIDTH
|
||||
? ~((0xff << (size_bits % UINT8_WIDTH + bit_offset + i * UINT8_WIDTH)) |
|
||||
~(0xff << (bit_offset + i * UINT8_WIDTH)))
|
||||
: (0xff << (size_bits % UINT8_WIDTH + bit_offset + i * UINT8_WIDTH)) |
|
||||
~(0xff << (bit_offset + i * UINT8_WIDTH)))) >>
|
||||
(bit_offset));
|
||||
}
|
||||
|
||||
parts_array.push_back(nlohmann::json::parse(
|
||||
fmt::format(R"({{"{}":{{"byte_offset":{},"bit_offset":{},"size_bits":{},"parse_result":"{}"}}}})",
|
||||
fmt::format("Fragment#{}", iter++), byte_offset, bit_offset, size_bits,
|
||||
fmt::format("{:#x}", result))));
|
||||
};
|
||||
|
||||
double result_real = result * resolution + offset;
|
||||
spn_json["Fragments"] = parts_array;
|
||||
spn_json["Value"] = result_real;
|
||||
spns_array.push_back(spn_json);
|
||||
};
|
||||
|
||||
verbose["SPNs"] = spns_array;
|
||||
};
|
||||
|
||||
if (!verbose.is_null()) {
|
||||
brief = {
|
||||
{"PGN", verbose["PGN"]},
|
||||
{"Label", verbose["Label"]},
|
||||
{"Acronym", verbose["Acronym"]},
|
||||
};
|
||||
|
||||
nlohmann::json::array_t spns_array;
|
||||
for (const auto &spn : verbose["SPNs"]) {
|
||||
spns_array.push_back(fmt::format("{}: {:.6g} {}", spn["SPN name"].get<std::string>(), spn["Value"].get<double>(),
|
||||
spn["Unit"].get<std::string>()));
|
||||
}
|
||||
|
||||
brief["SPNs"] = spns_array;
|
||||
}
|
||||
|
||||
return {verbose, brief};
|
||||
}
|
||||
|
||||
nlohmann::json verboseToExportJson(const nlohmann::json &verbose) {
|
||||
nlohmann::json::array_t spns;
|
||||
|
||||
if (verbose.is_null() || !verbose.contains("SPNs")) return spns;
|
||||
|
||||
std::string pgn = verbose.contains("PGN") ? verbose["PGN"].get<std::string>() : "";
|
||||
|
||||
for (const auto &v : verbose["SPNs"]) {
|
||||
nlohmann::json spn = {
|
||||
{"name", v.value("SPN name", "")},
|
||||
{"offset", v.value("Offset", 0)},
|
||||
{"resolution", v.value("Resolution", 0.0)},
|
||||
{"max", v.value("Maximum value", 0.0)},
|
||||
{"min", v.value("Minimum value", 0.0)},
|
||||
{"pgn", pgn},
|
||||
{"value", v.value("Value", 0.0)},
|
||||
{"unit", v.value("Unit", "")},
|
||||
};
|
||||
|
||||
nlohmann::json::array_t frags;
|
||||
if (v.contains("Fragments")) {
|
||||
for (const auto &[k, frag] : v["Fragments"].items()) {
|
||||
auto frag_key = fmt::format("Fragment#{}", k);
|
||||
if (frag.contains(frag_key)) {
|
||||
frags.push_back({{
|
||||
fmt::format("fragment#{}", k),
|
||||
{
|
||||
{"byte_pos", frag[frag_key].value("byte_offset", 0)},
|
||||
{"bit_pos", frag[frag_key].value("bit_offset", 0)},
|
||||
{"bit_size", frag[frag_key].value("size_bits", 0)},
|
||||
},
|
||||
}});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
spn["fragments"] = std::move(frags);
|
||||
spns.push_back(std::move(spn));
|
||||
}
|
||||
|
||||
return spns;
|
||||
}
|
||||
|
|
@ -0,0 +1,692 @@
|
|||
#include "can_data.hpp"
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/component_base.hpp>
|
||||
#include <ftxui/component/component_options.hpp>
|
||||
#include <ftxui/component/mouse.hpp>
|
||||
#include <ftxui/component/screen_interactive.hpp>
|
||||
#include <ftxui/dom/direction.hpp>
|
||||
#include <ftxui/dom/elements.hpp>
|
||||
#include <ftxui/screen/color.hpp>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
|
||||
#include <linux/if.h>
|
||||
#include <linux/sockios.h>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <stop_token>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/socket.h>
|
||||
#include <thread>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <linux/can.h>
|
||||
#include <linux/can/raw.h>
|
||||
|
||||
#define FMT_HEADER_ONLY
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "signals.hpp"
|
||||
#include "sqlite_modern_cpp.h"
|
||||
#include "utils.hpp"
|
||||
#include "json/expander.hpp"
|
||||
#include "json/json.hpp"
|
||||
|
||||
ftxui::Component makeCanPlayerDialog(ftxui::ScreenInteractive *scr, signals_map_t &smap, bool &is_ready) {
|
||||
|
||||
class Impl : public ftxui::ComponentBase {
|
||||
public:
|
||||
Impl(ftxui::ScreenInteractive *scr, signals_map_t &smap, bool &is_ready) {
|
||||
static sqlite::database *database = nullptr;
|
||||
static float canbus_player_focus_relative = 0;
|
||||
|
||||
auto pgnContainer = ftxui::Container::Vertical({});
|
||||
|
||||
struct pgn_parameters_s {
|
||||
bool selected, is_running, pinned, forward = false;
|
||||
uint32_t pgn, priority, datalen;
|
||||
std::string label, acronym, descr, address, ifname, period_ms;
|
||||
std::vector<uint8_t> payload;
|
||||
std::string forward_canid;
|
||||
|
||||
struct {
|
||||
std::unique_ptr<std::mutex> mtx;
|
||||
std::future<void> fut;
|
||||
std::unique_ptr<std::stop_source> ss;
|
||||
} concurrent;
|
||||
};
|
||||
|
||||
struct spn_parameters_s {
|
||||
struct fragment_s {
|
||||
int32_t byte_offset, bit_offset, size;
|
||||
};
|
||||
|
||||
bool checked, little_endian;
|
||||
float resolution, offset, min, max, current;
|
||||
std::string unit;
|
||||
int32_t slider_percent;
|
||||
size_t raw;
|
||||
std::vector<fragment_s> fragments;
|
||||
struct pgn_parameters_s *pg_ref = nullptr;
|
||||
};
|
||||
|
||||
static std::map<int32_t, pgn_parameters_s> pgs;
|
||||
static std::set<uint32_t> received_pgns;
|
||||
static const auto send_frame = [](const pgn_parameters_s &pg) {
|
||||
struct can_frame frame = {};
|
||||
|
||||
const int can_socket = ::socket(AF_CAN, SOCK_RAW, CAN_RAW);
|
||||
if (can_socket < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
ifreq ifr{};
|
||||
|
||||
std::strcpy(ifr.ifr_name, pg.ifname.c_str());
|
||||
if (::ioctl(can_socket, SIOCGIFINDEX, &ifr) < 0) {
|
||||
::close(can_socket);
|
||||
return;
|
||||
}
|
||||
|
||||
sockaddr_can addr = {};
|
||||
addr.can_family = AF_CAN;
|
||||
addr.can_ifindex = ifr.ifr_ifindex;
|
||||
::setsockopt(can_socket, SOL_CAN_RAW, CAN_RAW_FILTER, nullptr, 0);
|
||||
|
||||
if (::bind(can_socket, reinterpret_cast<sockaddr *>(&addr), sizeof(addr)) < 0) {
|
||||
::close(can_socket);
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t address;
|
||||
std::stringstream{} << std::hex << pg.address >> address;
|
||||
|
||||
frame.can_id = ((pg.priority & 0x7u) << 26u) | ((pg.pgn & 0x3FFFFu) << 8u) | (address & 0xFFu);
|
||||
frame.can_dlc = pg.datalen;
|
||||
frame.can_id |= CAN_EFF_FLAG;
|
||||
std::memcpy(frame.data, pg.payload.data(), pg.payload.size());
|
||||
::write(can_socket, &frame, sizeof(frame));
|
||||
::close(can_socket);
|
||||
};
|
||||
|
||||
static const auto calculate_spn = [](spn_parameters_s &spn_params) {
|
||||
// Calculate value from slider percentage
|
||||
spn_params.current =
|
||||
((spn_params.slider_percent / 100.0f) * (spn_params.max - spn_params.min)) + spn_params.offset;
|
||||
|
||||
// Round and clamp by min/max
|
||||
spn_params.current = std::clamp(std::round(spn_params.current), spn_params.min, spn_params.max);
|
||||
spn_params.raw = (spn_params.current - spn_params.offset) / spn_params.resolution;
|
||||
auto raw = spn_params.raw;
|
||||
|
||||
// Swap bytes if needed
|
||||
if (spn_params.little_endian) {
|
||||
// Get size
|
||||
size_t size = 0;
|
||||
for (const auto &frag : spn_params.fragments) {
|
||||
size += frag.size;
|
||||
}
|
||||
|
||||
size /= UINT8_WIDTH;
|
||||
if (size > 1) {
|
||||
auto swapped = std::shared_ptr<uint8_t>(new uint8_t[size], [](auto *p) { delete[] p; });
|
||||
for (size_t i = 0; i < size; ++i) {
|
||||
swapped.get()[i] = reinterpret_cast<uint8_t *>(&raw)[size - i - 1];
|
||||
}
|
||||
|
||||
raw = *reinterpret_cast<decltype(raw) *>(swapped.get());
|
||||
}
|
||||
}
|
||||
|
||||
// Magic here
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(*spn_params.pg_ref->concurrent.mtx);
|
||||
for (const auto &fragment : spn_params.fragments) {
|
||||
for (int32_t i = 0; i < fragment.size / UINT8_WIDTH + (fragment.size % UINT8_WIDTH ? 1 : 0); i++) {
|
||||
|
||||
uint8_t &byte = spn_params.pg_ref->payload[i + fragment.byte_offset];
|
||||
|
||||
// Reset bits in this byte depending on fragment size and fragment bit offset
|
||||
byte &= fragment.size % UINT8_WIDTH
|
||||
? (static_cast<uint8_t>(0xffu << (fragment.size % UINT8_WIDTH + fragment.bit_offset)) |
|
||||
static_cast<uint8_t>(~(0xffu << fragment.bit_offset)))
|
||||
: 0x00u;
|
||||
byte |= static_cast<uint8_t>((raw >> (i * UINT8_WIDTH)) << fragment.bit_offset);
|
||||
}
|
||||
|
||||
raw >>= fragment.size;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static const auto stop_pg = [](pgn_parameters_s &pg) {
|
||||
if (pg.is_running) {
|
||||
pg.concurrent.ss->request_stop();
|
||||
pg.concurrent.fut.wait();
|
||||
pg.concurrent.ss = std::make_unique<std::stop_source>();
|
||||
pg.is_running = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Static connections
|
||||
{
|
||||
static struct on_stopped_connection_s {
|
||||
on_stopped_connection_s(signals_map_t &smap) {
|
||||
smap.get<void()>("canplayer_stopped")->connect([]() {
|
||||
for (auto &[_, pg] : pgs) {
|
||||
stop_pg(pg);
|
||||
}
|
||||
});
|
||||
}
|
||||
} on_stopped_connection(smap);
|
||||
|
||||
static struct forward_connection_s {
|
||||
forward_connection_s(signals_map_t &smap) {
|
||||
smap.get<void(const std::vector<can_frame_update_s> &)>("new_entries_batch")
|
||||
->connect([](const std::vector<can_frame_update_s> &batch) {
|
||||
for (const auto &entry : batch) {
|
||||
uint32_t pgn_num = 0;
|
||||
if (entry.canid.size() >= 6) {
|
||||
auto pgn_str = entry.canid.substr(entry.canid.size() >= 8 ? entry.canid.size() - 6 : 2, 4);
|
||||
std::stringstream ss;
|
||||
ss << std::hex << pgn_str;
|
||||
ss >> pgn_num;
|
||||
}
|
||||
received_pgns.insert(pgn_num);
|
||||
if (pgs.contains(pgn_num) && pgs[pgn_num].forward && pgs[pgn_num].is_running) {
|
||||
auto &pg = pgs[pgn_num];
|
||||
std::lock_guard<std::mutex> lock(*pg.concurrent.mtx);
|
||||
pg.payload = entry.data.payload;
|
||||
pg.payload.resize(pg.datalen, 0);
|
||||
pg.forward_canid = entry.canid;
|
||||
send_frame(pg);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} forward_connection(smap);
|
||||
|
||||
static struct database_ready_connection_s {
|
||||
database_ready_connection_s(signals_map_t &smap, ftxui::ScreenInteractive *scr, ftxui::Component pgnContainer,
|
||||
bool &is_ready) {
|
||||
smap.get<void(sqlite::database &)>("j1939_database_ready")
|
||||
->connect([scr, &is_ready, pgnContainer](sqlite::database &db) {
|
||||
scr->Post([scr, &is_ready, pgnContainer, &db]() {
|
||||
std::lock_guard<std::mutex> db_lock(g_j1939_db_mtx);
|
||||
db << "SELECT pgn, pg_label, pg_acronym, pg_descr, pg_datalen, pg_priority FROM pgns" >>
|
||||
[&, pgnContainer](uint32_t pgn, const std::string &label, const std::string &acronym,
|
||||
const std::string &descr, uint32_t datalen, uint32_t priority) {
|
||||
if (!pgs.contains(pgn)) {
|
||||
pgs.insert(std::pair{
|
||||
pgn,
|
||||
pgn_parameters_s{
|
||||
.selected = false,
|
||||
.pinned = false,
|
||||
.pgn = pgn,
|
||||
.priority = priority,
|
||||
.datalen = datalen,
|
||||
.label = label,
|
||||
.acronym = acronym,
|
||||
.descr = descr,
|
||||
.address = "0xFF",
|
||||
.ifname = "vcan0",
|
||||
.period_ms = "1000",
|
||||
.payload = std::vector<uint8_t>(datalen),
|
||||
.concurrent =
|
||||
{
|
||||
.mtx = std::make_unique<std::mutex>(),
|
||||
.ss = std::make_unique<std::stop_source>(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
auto &pg_ref = pgs[pgn];
|
||||
auto spnContainer = ftxui::Container::Vertical({});
|
||||
db << fmt::format("SELECT id, pgn, spn, spn_name FROM spns WHERE pgn = {};", pgn) >>
|
||||
[spnContainer, &db](int32_t id, int32_t pgn, int32_t spn, const std::string &spn_name) {
|
||||
static std::map<int32_t, spn_parameters_s> spns;
|
||||
db << fmt::format(
|
||||
"SELECT min_value, max_value, resolution, offset, units FROM spns WHERE spn = {};",
|
||||
spn) >>
|
||||
[&](float min, float max, float resolution, float offset, const std::string &unit) {
|
||||
db << fmt::format("SELECT COUNT(*) FROM spn_fragments WHERE spn = {}", spn) >>
|
||||
[&](int32_t count) {
|
||||
std::vector<spn_parameters_s::fragment_s> fragments;
|
||||
fragments.resize(count);
|
||||
auto *fragment_ptr = fragments.data();
|
||||
|
||||
// Fill fragments array
|
||||
db << fmt::format("SELECT byte_offset, bit_offset, size FROM spn_fragments "
|
||||
"WHERE spn = {}",
|
||||
spn) >>
|
||||
[&fragment_ptr](int32_t byte_offset, int32_t bit_offset, int32_t size) {
|
||||
*(fragment_ptr++) = {
|
||||
.byte_offset = byte_offset,
|
||||
.bit_offset = bit_offset,
|
||||
.size = size,
|
||||
};
|
||||
};
|
||||
|
||||
spns.insert_or_assign(spn, spn_parameters_s{
|
||||
.checked = false,
|
||||
.little_endian = false,
|
||||
.resolution = resolution,
|
||||
.offset = offset,
|
||||
.min = min,
|
||||
.max = max,
|
||||
.current = 0.0f,
|
||||
.unit = unit,
|
||||
.slider_percent = 0,
|
||||
.fragments = fragments,
|
||||
.pg_ref = &pgs[pgn],
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
auto &spn_params = spns[spn];
|
||||
spnContainer->Add({
|
||||
ftxui::Container::Horizontal({
|
||||
ftxui::Container::Vertical({
|
||||
ftxui::Checkbox({
|
||||
.checked = &spns[spn].checked,
|
||||
.transform =
|
||||
[spn_name](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
return ftxui::hbox({
|
||||
ftxui::separatorEmpty(),
|
||||
ftxui::text(state.state ? "▼ " : "▶ "),
|
||||
ftxui::text(spn_name),
|
||||
}) |
|
||||
(state.focused
|
||||
? (ftxui::bold | ftxui::bgcolor(ftxui::Color::Grey11))
|
||||
: ftxui::nothing) |
|
||||
ftxui::flex;
|
||||
},
|
||||
}),
|
||||
|
||||
ftxui::Maybe(
|
||||
ftxui::Container::Horizontal({
|
||||
ftxui::Renderer([]() {
|
||||
ftxui::Elements separators(4u, ftxui::separatorEmpty());
|
||||
return ftxui::hbox(separators);
|
||||
}),
|
||||
|
||||
ftxui::Container::Vertical({
|
||||
ftxui::Slider(ftxui::SliderOption<int32_t>{
|
||||
.value = &spn_params.slider_percent,
|
||||
.min = 0,
|
||||
.max = 100,
|
||||
.increment = 1,
|
||||
.on_change =
|
||||
[&spn_params =
|
||||
spns[spn]]() { calculate_spn(spn_params); },
|
||||
}) | ftxui::Renderer([](ftxui::Element inner) {
|
||||
return ftxui::hbox({
|
||||
ftxui::text("Value: ") | ftxui::bold |
|
||||
ftxui::color(ftxui::Color::Yellow),
|
||||
ftxui::text("["),
|
||||
inner,
|
||||
ftxui::text("]"),
|
||||
}) |
|
||||
ftxui::size(ftxui::WIDTH, ftxui::EQUAL, 100u);
|
||||
}),
|
||||
|
||||
ftxui::Container::Horizontal({
|
||||
ftxui::Renderer([]() {
|
||||
return ftxui::text("Endianness: ") | ftxui::bold |
|
||||
ftxui::color(ftxui::Color::Yellow);
|
||||
}),
|
||||
|
||||
ftxui::Checkbox({
|
||||
.checked = &spn_params.little_endian,
|
||||
.transform =
|
||||
[&spn_params](const ftxui::EntryState &state) {
|
||||
auto el = ftxui::hbox({
|
||||
ftxui::text("<"),
|
||||
ftxui::text("little") |
|
||||
(spn_params.little_endian
|
||||
? (ftxui::bold |
|
||||
ftxui::color(ftxui::Color::Red))
|
||||
: ftxui::nothing),
|
||||
ftxui::text(" | "),
|
||||
ftxui::text("big") |
|
||||
(!spn_params.little_endian
|
||||
? (ftxui::bold |
|
||||
ftxui::color(ftxui::Color::Red))
|
||||
: ftxui::nothing),
|
||||
ftxui::text(">"),
|
||||
});
|
||||
|
||||
if (state.focused || state.active) {
|
||||
el = el | ftxui::bold |
|
||||
ftxui::bgcolor(ftxui::Color::Grey11);
|
||||
}
|
||||
|
||||
return el;
|
||||
},
|
||||
.on_change =
|
||||
[&spn_params = spns[spn]]() {
|
||||
calculate_spn(spn_params);
|
||||
},
|
||||
}),
|
||||
}),
|
||||
|
||||
ftxui::Renderer([]() {
|
||||
return ftxui::vbox({
|
||||
ftxui::separatorEmpty(),
|
||||
ftxui::text("SPN info:") |
|
||||
ftxui::color(ftxui::Color::Cyan) | ftxui::bold,
|
||||
});
|
||||
}),
|
||||
|
||||
From(
|
||||
[spn]() -> nlohmann::json {
|
||||
auto &spn_params = spns[spn];
|
||||
auto fragments = nlohmann::json::array({});
|
||||
|
||||
for (const auto &frag : spn_params.fragments) {
|
||||
fragments.push_back(
|
||||
nlohmann::json{{"byte_offset", frag.byte_offset},
|
||||
{"bit_offset", frag.bit_offset},
|
||||
{"size", frag.size}});
|
||||
}
|
||||
|
||||
return {
|
||||
{"fragments", fragments},
|
||||
{"min", spn_params.min},
|
||||
{"max", spn_params.max},
|
||||
{"resolution", spn_params.resolution},
|
||||
{"offset", spn_params.offset},
|
||||
};
|
||||
}(),
|
||||
false, -100, ExpanderImpl::Root()) |
|
||||
ftxui::Renderer([](ftxui::Element inner) {
|
||||
return ftxui::hbox({
|
||||
ftxui::separatorEmpty(),
|
||||
ftxui::separatorEmpty(),
|
||||
inner,
|
||||
});
|
||||
}),
|
||||
|
||||
ftxui::Renderer([]() { return ftxui::separatorEmpty(); }),
|
||||
ftxui::Renderer([&spn_params = spns[spn]]() {
|
||||
return ftxui::vbox({
|
||||
ftxui::hbox({
|
||||
ftxui::text("Value: ") | ftxui::bold |
|
||||
ftxui::color(ftxui::Color::Cyan),
|
||||
ftxui::text(fmt::format("{} {}", spn_params.current,
|
||||
spn_params.unit)),
|
||||
}),
|
||||
|
||||
ftxui::hbox({
|
||||
ftxui::text("Raw: ") | ftxui::bold |
|
||||
ftxui::color(ftxui::Color::Cyan),
|
||||
ftxui::text(fmt::format(
|
||||
"{} (hex:{}) (bin:{})", spn_params.raw,
|
||||
fmt::format("{0:#x}", spn_params.raw),
|
||||
fmt::format("{0:#b}", spn_params.raw))),
|
||||
}),
|
||||
|
||||
ftxui::hbox({
|
||||
ftxui::text("PG payload: ") | ftxui::bold |
|
||||
ftxui::color(ftxui::Color::Cyan),
|
||||
ftxui::text(fmt::format(
|
||||
"[{}]",
|
||||
[&]() {
|
||||
std::string ret;
|
||||
for (const auto &byte :
|
||||
spn_params.pg_ref->payload) {
|
||||
ret += fmt::format("{0:#010b} ", byte);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}())),
|
||||
}),
|
||||
|
||||
ftxui::separatorEmpty(),
|
||||
});
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
|
||||
&spns[spn].checked),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
pgnContainer->Add(ftxui::Container::Vertical({
|
||||
ftxui::Container::Horizontal({
|
||||
ftxui::Checkbox({
|
||||
.checked = &pg_ref.selected,
|
||||
.transform = [&pg_ref =
|
||||
pgs[pgn]](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
return ftxui::hbox({
|
||||
ftxui::text(state.state ? "▼ " : "▶ "),
|
||||
ftxui::text(fmt::format("0x{:x} - {}", pg_ref.pgn, pg_ref.label)) |
|
||||
(pg_ref.is_running ? ftxui::color(ftxui::Color::Green)
|
||||
: ftxui::nothing),
|
||||
ftxui::filler(),
|
||||
}) |
|
||||
(state.focused ? (ftxui::bold | ftxui::bgcolor(ftxui::Color::Grey11))
|
||||
: ftxui::nothing) |
|
||||
ftxui::flex;
|
||||
},
|
||||
}),
|
||||
}),
|
||||
|
||||
ftxui::Maybe(
|
||||
ftxui::Container::Vertical({
|
||||
ftxui::Container::Horizontal({
|
||||
ftxui::Checkbox({
|
||||
.transform = [](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
return ftxui::hbox({
|
||||
ftxui::text(" >[send_frame]< ") |
|
||||
(state.focused ? ftxui::bold : ftxui::nothing) |
|
||||
ftxui::color(ftxui::Color::Cyan) |
|
||||
(state.focused ? ftxui::bgcolor(ftxui::Color::Grey11)
|
||||
: ftxui::nothing),
|
||||
});
|
||||
},
|
||||
|
||||
.on_change =
|
||||
[&pg = pgs[pgn]]() {
|
||||
std::lock_guard<std::mutex> lock(*pg.concurrent.mtx);
|
||||
send_frame(pg);
|
||||
},
|
||||
}),
|
||||
|
||||
ftxui::Checkbox({
|
||||
.transform = [](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
return ftxui::text(" >[run]< ") |
|
||||
(state.focused ? ftxui::bold : ftxui::nothing) |
|
||||
ftxui::color(ftxui::Color::Cyan) |
|
||||
(state.focused ? ftxui::bgcolor(ftxui::Color::Grey11)
|
||||
: ftxui::nothing);
|
||||
},
|
||||
|
||||
.on_change =
|
||||
[&pg = pgs[pgn]]() {
|
||||
if (!pg.is_running) {
|
||||
pg.concurrent.fut = std::async(
|
||||
std::launch::async,
|
||||
[&pg](std::stop_token st) {
|
||||
int32_t period_ms;
|
||||
std::stringstream{} << pg.period_ms >> period_ms;
|
||||
period_ms = std::clamp(period_ms, 50, INT32_MAX);
|
||||
|
||||
while (!st.stop_requested()) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(*pg.concurrent.mtx);
|
||||
send_frame(pg);
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::milliseconds(period_ms));
|
||||
}
|
||||
},
|
||||
|
||||
pg.concurrent.ss->get_token());
|
||||
pg.is_running = true;
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
ftxui::Checkbox({
|
||||
.transform = [](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
return ftxui::text(" >[stop]< ") |
|
||||
(state.focused ? ftxui::bold : ftxui::nothing) |
|
||||
ftxui::color(ftxui::Color::Cyan) |
|
||||
(state.focused ? ftxui::bgcolor(ftxui::Color::Grey11)
|
||||
: ftxui::nothing);
|
||||
},
|
||||
|
||||
.on_change = [&pg = pgs[pgn]]() { stop_pg(pg); },
|
||||
}),
|
||||
}),
|
||||
|
||||
ftxui::Renderer([]() { return ftxui::separator(); }),
|
||||
ftxui::Maybe(
|
||||
ftxui::Checkbox({
|
||||
.checked = &pg_ref.forward,
|
||||
.transform = [&pg_ref](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
auto el =
|
||||
ftxui::text(pg_ref.forward ? " [X] forward " : " [ ] forward ") |
|
||||
ftxui::color(pg_ref.forward ? ftxui::Color::Green
|
||||
: ftxui::Color::Cyan);
|
||||
if (state.focused || state.active)
|
||||
el = el | ftxui::bold | ftxui::bgcolor(ftxui::Color::Grey11);
|
||||
return el;
|
||||
},
|
||||
}),
|
||||
|
||||
[pgn]() { return received_pgns.contains(pgn); }),
|
||||
ftxui::Renderer([]() { return ftxui::separatorEmpty(); }),
|
||||
|
||||
ftxui::Input({
|
||||
.content = &pg_ref.address,
|
||||
.placeholder = "0xFF",
|
||||
.multiline = false,
|
||||
}) | ftxui::Renderer([](ftxui::Element inner) {
|
||||
return ftxui::hbox({
|
||||
ftxui::separatorEmpty(),
|
||||
ftxui::text("Address (hex): ") | ftxui::color(ftxui::Color::Magenta) |
|
||||
ftxui::bold,
|
||||
ftxui::hbox({
|
||||
inner,
|
||||
ftxui::filler(),
|
||||
}),
|
||||
});
|
||||
}),
|
||||
|
||||
ftxui::Input({
|
||||
.content = &pg_ref.ifname,
|
||||
.placeholder = "vcan0",
|
||||
.multiline = false,
|
||||
}) | ftxui::Renderer([](ftxui::Element inner) {
|
||||
return ftxui::hbox({
|
||||
ftxui::separatorEmpty(),
|
||||
ftxui::text("CAN interface name: ") | ftxui::color(ftxui::Color::Magenta) |
|
||||
ftxui::bold,
|
||||
ftxui::hbox({
|
||||
inner,
|
||||
ftxui::filler(),
|
||||
}),
|
||||
});
|
||||
}),
|
||||
|
||||
ftxui::Input({
|
||||
.content = &pg_ref.period_ms,
|
||||
.placeholder = "1000",
|
||||
.multiline = false,
|
||||
}) | ftxui::Renderer([](ftxui::Element inner) {
|
||||
return ftxui::hbox({
|
||||
ftxui::separatorEmpty(),
|
||||
ftxui::text("Send period (ms): ") | ftxui::color(ftxui::Color::Magenta) |
|
||||
ftxui::bold,
|
||||
ftxui::hbox({
|
||||
inner,
|
||||
ftxui::filler(),
|
||||
}),
|
||||
});
|
||||
}),
|
||||
|
||||
ftxui::Renderer([]() { return ftxui::separatorEmpty(); }),
|
||||
spnContainer,
|
||||
}) | ftxui::border,
|
||||
&pg_ref.selected),
|
||||
}));
|
||||
};
|
||||
|
||||
database = &db;
|
||||
is_ready = true;
|
||||
scr->Post(ftxui::Event::Custom);
|
||||
});
|
||||
});
|
||||
}
|
||||
} database_ready_connection(smap, scr, pgnContainer, is_ready);
|
||||
}
|
||||
|
||||
auto main = ftxui::Container::Vertical({
|
||||
(pgnContainer | ftxui::Renderer([](ftxui::Element inner) {
|
||||
return inner | ftxui::focusPositionRelative(0, canbus_player_focus_relative) | ftxui::vscroll_indicator |
|
||||
ftxui::frame | ftxui::flex;
|
||||
})) |
|
||||
ftxui::CatchEvent([pgnContainer](ftxui::Event event) {
|
||||
static const auto increment_focus = [pgnContainer = pgnContainer]() {
|
||||
if (auto n = pgnContainer->ChildCount(); n > 0)
|
||||
canbus_player_focus_relative =
|
||||
std::clamp(canbus_player_focus_relative + 1.0f / static_cast<float>(n), 0.0f, 1.0f);
|
||||
};
|
||||
|
||||
static const auto decrement_focus = [pgnContainer = pgnContainer]() {
|
||||
if (auto n = pgnContainer->ChildCount(); n > 0)
|
||||
canbus_player_focus_relative =
|
||||
std::clamp(canbus_player_focus_relative - 1.0f / static_cast<float>(n), 0.0f, 1.0f);
|
||||
};
|
||||
|
||||
if (!database) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.is_mouse()) {
|
||||
switch (static_cast<enum ftxui::Mouse::Button>(event.mouse().button)) {
|
||||
case ftxui::Mouse::Button::WheelDown: {
|
||||
increment_focus();
|
||||
return true;
|
||||
} break;
|
||||
|
||||
case ftxui::Mouse::Button::WheelUp: {
|
||||
decrement_focus();
|
||||
return true;
|
||||
} break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else if (!event.is_character()) {
|
||||
if (event == ftxui::Event::ArrowDown) {
|
||||
increment_focus();
|
||||
return true;
|
||||
} else if (event == ftxui::Event::ArrowUp) {
|
||||
decrement_focus();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}),
|
||||
});
|
||||
|
||||
Add({main});
|
||||
}
|
||||
};
|
||||
|
||||
return ftxui::Make<Impl>(scr, smap, is_ready);
|
||||
}
|
||||
|
|
@ -0,0 +1,657 @@
|
|||
// #include <algorithm>
|
||||
// #include <cmath>
|
||||
#include <cstdint>
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/component_options.hpp>
|
||||
#include <ftxui/dom/elements.hpp>
|
||||
#include <ftxui/screen/color.hpp>
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
|
||||
#include "canid_unit.hpp"
|
||||
#include "process.hpp"
|
||||
#include "tagsettings.hpp"
|
||||
#include "json/json.hpp"
|
||||
#include <spdlog/sinks/systemd_sink.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
// For sqlite
|
||||
// #include "sqlite_modern_cpp.h"
|
||||
#include "src/json/expander.hpp"
|
||||
|
||||
extern ftxui::Component makeSpnSettingsForm(ftxui::ScreenInteractive *, signals_map_t &, const std::string &,
|
||||
std::string &, ftxui::Component, ftxui::Component, bool &,
|
||||
std::map<std::string, std::map<int32_t, ftxui::Component>> &,
|
||||
spn_settings_map_t &);
|
||||
|
||||
ftxui::Component makeCanIDUnit(const std::string &iface, const std::string &canid, const std::string &protocol,
|
||||
size_t &spn_count, const std::vector<uint8_t> &data, ftxui::ScreenInteractive *screen,
|
||||
signals_map_t &smap, ftxui::Component content, ftxui::Component canids_container,
|
||||
ftxui::Component spn_settings_dialog, ftxui::Component cansettings_dialog,
|
||||
bool is_deployed, bool is_verbose, bool is_brief, bool is_manual,
|
||||
std::string &canid_active, bool &custom_spn_settings_shown,
|
||||
bool &canbus_parameters_export_shown, bool &filedialog_shown,
|
||||
std::map<std::string, std::map<int32_t, ftxui::Component>> &spnSettingsFormMap,
|
||||
spn_settings_map_t &spnSettingsMap) {
|
||||
return ftxui::Make<CanIDUnit>(iface, canid, protocol, spn_count, data, screen, smap, content, canids_container,
|
||||
spn_settings_dialog, cansettings_dialog, is_deployed, is_verbose, is_brief, is_manual,
|
||||
canid_active, custom_spn_settings_shown, canbus_parameters_export_shown,
|
||||
filedialog_shown, spnSettingsFormMap, spnSettingsMap);
|
||||
}
|
||||
|
||||
CanIDUnit::CanIDUnit(const std::string &iface, const std::string &canid, const std::string &protocol, size_t &spn_count,
|
||||
const std::vector<uint8_t> &data, ftxui::ScreenInteractive *screen, signals_map_t &smap,
|
||||
ftxui::Component content, ftxui::Component canids_container, ftxui::Component spn_settings_dialog,
|
||||
ftxui::Component cansettings_dialog, bool is_deployed, bool is_verbose, bool is_brief,
|
||||
bool is_manual, std::string &canid_active, bool &custom_spn_settings_shown,
|
||||
bool &canbus_parameters_export_shown, bool &filedialog_shown,
|
||||
std::map<std::string, std::map<int32_t, ftxui::Component>> &spnSettingsFormMap,
|
||||
spn_settings_map_t &spnSettingsMap)
|
||||
|
||||
: m_canid_(canid), m_iface_(iface), m_data_(data), m_deployed_(is_deployed), m_verbose_(is_verbose),
|
||||
m_brief_(is_brief), m_manual_mode_(is_manual),
|
||||
m_spnSettingsForm_(makeSpnSettingsForm(screen, smap, canid, canid_active, canids_container, spn_settings_dialog,
|
||||
custom_spn_settings_shown, spnSettingsFormMap, spnSettingsMap)) {
|
||||
|
||||
m_cansettings_dialog_ = cansettings_dialog;
|
||||
m_spnSettingsMap_ = &spnSettingsMap;
|
||||
|
||||
m_brief_content_ = ftxui::Container::Vertical({});
|
||||
m_verbose_content_ = ftxui::Container::Vertical({});
|
||||
|
||||
auto arrow = ftxui::Checkbox({
|
||||
.checked = &m_deployed_,
|
||||
.transform = [this](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
return ftxui::hbox({
|
||||
ftxui::text(m_deployed_ ? "▼ " : "▶ "),
|
||||
});
|
||||
},
|
||||
|
||||
.on_change = [&, this]() { canid_active = m_canid_; },
|
||||
});
|
||||
|
||||
auto contentbox = ftxui::Checkbox({
|
||||
.transform = [this](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
ftxui::Elements line;
|
||||
|
||||
// Interface
|
||||
line.push_back(ftxui::text(m_iface_ + " ") |
|
||||
(m_diff_.is_new_interface ? (ftxui::color(ftxui::Color::Red) | ftxui::bold)
|
||||
: ftxui::color(ftxui::Color::Aquamarine1) | ftxui::bold));
|
||||
|
||||
// CAN ID
|
||||
line.push_back(ftxui::text(fmt::format("{:8} ", m_canid_)) |
|
||||
(m_diff_.is_new_canid ? (ftxui::color(ftxui::Color::Red) | ftxui::bold)
|
||||
: ftxui::color(ftxui::Color::GreenLight) | ftxui::bold));
|
||||
|
||||
// Size
|
||||
line.push_back(ftxui::text(fmt::format("{} ", m_data_.size())));
|
||||
|
||||
// Padding for < 8 bytes
|
||||
for (size_t i = m_data_.size(); i < 8; ++i)
|
||||
line.push_back(ftxui::text("---- "));
|
||||
|
||||
// Payload bytes with diff highlighting
|
||||
bool has_updates = false;
|
||||
for (size_t idx = 0; idx < m_data_.size(); ++idx) {
|
||||
bool changed = idx < m_diff_.payload_changed.size() && m_diff_.payload_changed[idx];
|
||||
|
||||
if (changed) {
|
||||
has_updates = true;
|
||||
}
|
||||
|
||||
line.push_back(ftxui::text(fmt::format("0x{:02X} ", m_data_[idx])) |
|
||||
(changed ? (ftxui::color(ftxui::Color::Red) | ftxui::bold) : ftxui::nothing));
|
||||
}
|
||||
|
||||
// Last update time
|
||||
line.push_back(
|
||||
ftxui::text(fmt::format("(updated: {})", m_last_update_time_)) |
|
||||
(has_updates ? (ftxui::color(ftxui::Color::Red) | ftxui::bold) : ftxui::color(ftxui::Color::Cyan)));
|
||||
|
||||
auto row = ftxui::hbox(std::move(line));
|
||||
if (m_hovered_) {
|
||||
row = row | ftxui::bold | ftxui::bgcolor(ftxui::Color::Grey11);
|
||||
}
|
||||
|
||||
return row | ftxui::reflect(m_box_);
|
||||
},
|
||||
|
||||
.on_change =
|
||||
[&canid_active, this]() {
|
||||
canid_active = m_canid_;
|
||||
m_deployed_ = !m_deployed_;
|
||||
},
|
||||
});
|
||||
|
||||
auto label = ftxui::Renderer([this, protocol]() -> ftxui::Element {
|
||||
if (m_data_verbose_->contains("Label")) {
|
||||
|
||||
return ftxui::hbox({
|
||||
ftxui::text(fmt::format(" - {}", (*m_data_verbose_)["Label"].get<std::string>())) |
|
||||
ftxui::color(ftxui::Color::Magenta) | (m_deployed_ ? ftxui::bold : ftxui::nothing),
|
||||
ftxui::filler(),
|
||||
ftxui::text(fmt::format("{}", protocol.empty() ? "Unknown" : protocol)) | ftxui::color(ftxui::Color::Red),
|
||||
});
|
||||
} else {
|
||||
return ftxui::text("");
|
||||
}
|
||||
});
|
||||
|
||||
Add({
|
||||
ftxui::Container::Vertical({
|
||||
ftxui::Container::Horizontal({
|
||||
arrow,
|
||||
contentbox,
|
||||
label | ftxui::flex,
|
||||
}) | ftxui::flex,
|
||||
|
||||
ftxui::Maybe(
|
||||
ftxui::Container::Vertical({
|
||||
|
||||
// Tab switcher: <brief | verbose | manual>
|
||||
ftxui::Container::Horizontal({
|
||||
ftxui::Renderer([]() { return ftxui::text(" "); }),
|
||||
ftxui::Checkbox({
|
||||
.checked = &m_brief_,
|
||||
.transform = [this](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
auto el = ftxui::hbox({
|
||||
ftxui::text("<"),
|
||||
ftxui::text("brief") | (m_brief_ ? ftxui::bold : ftxui::nothing) |
|
||||
ftxui::color(ftxui::Color::Cyan),
|
||||
ftxui::text(" | "),
|
||||
});
|
||||
|
||||
if (state.focused || state.active)
|
||||
el = el | ftxui::bold | ftxui::bgcolor(ftxui::Color::Grey11);
|
||||
return el;
|
||||
},
|
||||
|
||||
.on_change =
|
||||
[this]() {
|
||||
m_brief_ = true;
|
||||
m_verbose_ = false;
|
||||
m_manual_mode_ = false;
|
||||
},
|
||||
}),
|
||||
|
||||
ftxui::Checkbox({
|
||||
.checked = &m_verbose_,
|
||||
.transform = [this](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
auto el = ftxui::text("verbose") | (m_verbose_ ? ftxui::bold : ftxui::nothing) |
|
||||
ftxui::color(ftxui::Color::Cyan);
|
||||
|
||||
if (state.focused || state.active) {
|
||||
el = el | ftxui::bold | ftxui::bgcolor(ftxui::Color::Grey11);
|
||||
}
|
||||
|
||||
return el;
|
||||
},
|
||||
|
||||
.on_change =
|
||||
[this]() {
|
||||
m_brief_ = false;
|
||||
m_verbose_ = true;
|
||||
m_manual_mode_ = false;
|
||||
},
|
||||
}),
|
||||
|
||||
ftxui::Checkbox({
|
||||
.checked = &m_manual_mode_,
|
||||
.transform = [this](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
auto el = ftxui::hbox({
|
||||
ftxui::text(" | "),
|
||||
ftxui::text("manual") | (m_manual_mode_ ? ftxui::bold : ftxui::nothing) |
|
||||
ftxui::color(ftxui::Color::Cyan),
|
||||
ftxui::text(">"),
|
||||
});
|
||||
|
||||
if (state.focused || state.active) {
|
||||
el = el | ftxui::bold | ftxui::bgcolor(ftxui::Color::Grey11);
|
||||
}
|
||||
|
||||
return el;
|
||||
},
|
||||
|
||||
.on_change =
|
||||
[this]() {
|
||||
m_brief_ = false;
|
||||
m_verbose_ = false;
|
||||
m_manual_mode_ = true;
|
||||
},
|
||||
}),
|
||||
|
||||
}),
|
||||
|
||||
// Brief content
|
||||
ftxui::Maybe(m_brief_content_ | ftxui::flex, &m_brief_),
|
||||
|
||||
// Verbose content
|
||||
ftxui::Maybe(m_verbose_content_ | ftxui::flex, &m_verbose_),
|
||||
|
||||
// Manual mode content
|
||||
ftxui::Maybe(m_spnSettingsForm_ | ftxui::flex, &m_manual_mode_),
|
||||
|
||||
}) | ftxui::border,
|
||||
|
||||
&m_deployed_),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
bool CanIDUnit::OnEvent(ftxui::Event event) {
|
||||
static auto log = spdlog::systemd_logger_mt("canidunit", "cansniffer-hover");
|
||||
log->set_level(spdlog::level::debug);
|
||||
|
||||
if (event.is_mouse()) {
|
||||
bool prev = m_hovered_;
|
||||
m_hovered_ = m_box_.Contain(event.mouse().x, event.mouse().y);
|
||||
if (m_hovered_ != prev) {
|
||||
log->debug("{}: hover={} mouse=({},{}) box=({},{},{},{})", m_canid_, m_hovered_, event.mouse().x, event.mouse().y,
|
||||
m_box_.x_min, m_box_.x_max, m_box_.y_min, m_box_.y_max);
|
||||
}
|
||||
}
|
||||
|
||||
return ftxui::ComponentBase::OnEvent(event);
|
||||
}
|
||||
|
||||
void CanIDUnit::update(const can_frame_data_s &data, const can_frame_diff_s &diff,
|
||||
std::shared_ptr<nlohmann::json> verbose, std::shared_ptr<nlohmann::json> brief) {
|
||||
m_data_ = data.payload;
|
||||
m_diff_ = diff;
|
||||
|
||||
// Update timestamp
|
||||
auto t = std::time(nullptr);
|
||||
struct tm tm_buf;
|
||||
localtime_r(&t, &tm_buf);
|
||||
char buf[32];
|
||||
std::strftime(buf, sizeof(buf), "%d-%m-%Y %H-%M-%S", &tm_buf);
|
||||
m_last_update_time_ = buf;
|
||||
|
||||
bool was_null = m_data_short_->is_null();
|
||||
|
||||
if (verbose) {
|
||||
*m_data_verbose_ = std::move(*verbose);
|
||||
}
|
||||
|
||||
if (brief) {
|
||||
*m_data_short_ = std::move(*brief);
|
||||
}
|
||||
|
||||
// Ensure skeleton verbose/brief exist for custom SPN injection
|
||||
if (m_spnSettingsMap_ && m_spnSettingsMap_->contains(m_canid_) && !(*m_spnSettingsMap_)[m_canid_].empty()) {
|
||||
if (m_data_verbose_->is_null()) {
|
||||
*m_data_verbose_ = nlohmann::json{{"SPNs", nlohmann::json::array()}};
|
||||
}
|
||||
|
||||
if (m_data_short_->is_null()) {
|
||||
*m_data_short_ = nlohmann::json{{"SPNs", nlohmann::json::array()}};
|
||||
}
|
||||
|
||||
if (!m_data_verbose_->contains("SPNs")) {
|
||||
(*m_data_verbose_)["SPNs"] = nlohmann::json::array();
|
||||
}
|
||||
|
||||
if (!m_data_short_->contains("SPNs")) {
|
||||
(*m_data_short_)["SPNs"] = nlohmann::json::array();
|
||||
}
|
||||
|
||||
for (const auto &[tag_id, settings] : (*m_spnSettingsMap_)[m_canid_]) {
|
||||
double resolution = 1.0, offset_val = 0.0;
|
||||
try {
|
||||
resolution = std::stod(settings.resolution.empty() ? settings.x_coeff : settings.resolution);
|
||||
} catch (...) {
|
||||
}
|
||||
|
||||
try {
|
||||
offset_val = std::stod(settings.offset);
|
||||
} catch (...) {
|
||||
}
|
||||
|
||||
int64_t result = 0;
|
||||
size_t total_bits = 0;
|
||||
for (const auto &frag : settings.fragments) {
|
||||
int32_t bo = 0, bi = 0, bc = 0;
|
||||
|
||||
try {
|
||||
bo = std::stoi(frag.byte_offset);
|
||||
} catch (...) {
|
||||
}
|
||||
|
||||
try {
|
||||
bi = std::stoi(frag.bit_offset);
|
||||
} catch (...) {
|
||||
}
|
||||
|
||||
try {
|
||||
bc = std::stoi(frag.bit_count);
|
||||
} catch (...) {
|
||||
}
|
||||
|
||||
int32_t byte_cnt = (bc + bi + UINT8_WIDTH - 1) / UINT8_WIDTH;
|
||||
if (bc > 0 && bo >= 0 && static_cast<size_t>(bo + byte_cnt) <= m_data_.size()) {
|
||||
int64_t frag_val = 0;
|
||||
|
||||
for (int32_t i = 0; i < byte_cnt; ++i) {
|
||||
frag_val |= static_cast<int64_t>(m_data_[bo + i]) << (i * UINT8_WIDTH);
|
||||
}
|
||||
|
||||
frag_val = (frag_val >> bi) & ((1LL << bc) - 1);
|
||||
result |= frag_val << total_bits;
|
||||
total_bits += bc;
|
||||
}
|
||||
}
|
||||
if (settings.big_endian && total_bits > 8) {
|
||||
int64_t swapped = 0;
|
||||
size_t total_bytes = (total_bits + 7) / 8;
|
||||
for (size_t i = 0; i < total_bytes; ++i)
|
||||
swapped |= ((result >> (i * 8)) & 0xFF) << ((total_bytes - 1 - i) * 8);
|
||||
result = swapped;
|
||||
}
|
||||
double spn_val = static_cast<double>(result) * resolution + offset_val;
|
||||
|
||||
nlohmann::json::array_t frags_json;
|
||||
for (size_t fi = 0; fi < settings.fragments.size(); ++fi) {
|
||||
const auto &f = settings.fragments[fi];
|
||||
int32_t bo = 0, bi = 0, bc = 0;
|
||||
|
||||
try {
|
||||
bo = std::stoi(f.byte_offset);
|
||||
} catch (...) {
|
||||
}
|
||||
|
||||
try {
|
||||
bi = std::stoi(f.bit_offset);
|
||||
} catch (...) {
|
||||
}
|
||||
|
||||
try {
|
||||
bc = std::stoi(f.bit_count);
|
||||
} catch (...) {
|
||||
}
|
||||
|
||||
frags_json.push_back(
|
||||
{{fmt::format("Fragment#{}", fi), {{"byte_offset", bo}, {"bit_offset", bi}, {"size_bits", bc}}}});
|
||||
}
|
||||
|
||||
nlohmann::json custom_spn = {
|
||||
{"SPN (integer)", -static_cast<int32_t>(tag_id)},
|
||||
{"SPN name", settings.spn_name + " (custom)"},
|
||||
{"Value", spn_val},
|
||||
{"Unit", settings.unit},
|
||||
{"Resolution", resolution},
|
||||
{"Offset", offset_val},
|
||||
{"Fragments", frags_json},
|
||||
};
|
||||
|
||||
if (m_data_verbose_->contains("SPNs")) {
|
||||
(*m_data_verbose_)["SPNs"].push_back(custom_spn);
|
||||
}
|
||||
|
||||
if (!m_data_short_->is_null() && m_data_short_->contains("SPNs")) {
|
||||
(*m_data_short_)["SPNs"].push_back(
|
||||
fmt::format("{}: {:.6g} {}", settings.spn_name + " (custom)", spn_val, settings.unit));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild FromLive when structure changes (first data or SPN count changed)
|
||||
size_t verbose_spn_count =
|
||||
(!m_data_verbose_->is_null() && m_data_verbose_->contains("SPNs")) ? (*m_data_verbose_)["SPNs"].size() : 0;
|
||||
size_t brief_spn_count =
|
||||
(!m_data_short_->is_null() && m_data_short_->contains("SPNs")) ? (*m_data_short_)["SPNs"].size() : 0;
|
||||
bool structure_changed = verbose_spn_count != m_last_verbose_spn_count_ || brief_spn_count != m_last_brief_spn_count_;
|
||||
|
||||
if (structure_changed || (was_null && !m_data_short_->is_null())) {
|
||||
m_brief_content_->DetachAllChildren();
|
||||
if (!m_data_short_->is_null()) {
|
||||
m_brief_content_->Add(FromLive(m_data_short_, nlohmann::json::json_pointer(), true, -100, ExpanderImpl::Root()));
|
||||
}
|
||||
|
||||
m_verbose_content_->DetachAllChildren();
|
||||
if (!m_data_verbose_->is_null()) {
|
||||
m_verbose_content_->Add(
|
||||
FromLive(m_data_verbose_, nlohmann::json::json_pointer(), true, -100, ExpanderImpl::Root()));
|
||||
}
|
||||
|
||||
m_last_verbose_spn_count_ = verbose_spn_count;
|
||||
m_last_brief_spn_count_ = brief_spn_count;
|
||||
}
|
||||
|
||||
// Populate export dialog once when verbose data first becomes available
|
||||
if (!m_data_short_->is_null() && !m_data_verbose_->is_null() && m_cansettings_dialog_) {
|
||||
if (!s_canbus_parameters_export_map_.contains(m_canid_)) {
|
||||
s_canbus_parameters_export_map_.insert({m_canid_, {false, false, {}}});
|
||||
}
|
||||
|
||||
if (!std::get<1u>(s_canbus_parameters_export_map_[m_canid_])) {
|
||||
std::get<1u>(s_canbus_parameters_export_map_[m_canid_]) = true;
|
||||
m_export_selectors_ = ftxui::Container::Vertical({});
|
||||
auto &selectors_container = m_export_selectors_;
|
||||
|
||||
for (const auto &[pgn_key, pgn_val] : m_data_verbose_->items()) {
|
||||
if (pgn_key == "SPNs") {
|
||||
for (const auto &[spns_arr_k, spns_arr_v] : pgn_val.items()) {
|
||||
for (const auto &[spn_k, spn_v] : spns_arr_v.items()) {
|
||||
if (spn_k == "SPN name") {
|
||||
std::string spn_name = spn_v.get<std::string>();
|
||||
if (spn_name.find("(custom)") != std::string::npos)
|
||||
continue;
|
||||
auto &spn_map = std::get<2u>(s_canbus_parameters_export_map_[m_canid_]);
|
||||
|
||||
spn_map.insert_or_assign(spn_name, std::make_tuple(false, false, spns_arr_v));
|
||||
selectors_container
|
||||
->Add(
|
||||
ftxui::Container::Vertical(
|
||||
{
|
||||
ftxui::Container::Horizontal(
|
||||
{
|
||||
ftxui::Checkbox({
|
||||
.checked = &std::get<0u>(spn_map[spn_name]),
|
||||
.transform = [this](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
return ftxui::hbox({ftxui::separatorEmpty(), ftxui::separatorEmpty(),
|
||||
ftxui::text(state.state ? "▼ " : "▶ ")});
|
||||
},
|
||||
}),
|
||||
|
||||
ftxui::Checkbox(
|
||||
{
|
||||
.checked = &std::get<1u>(spn_map[spn_name]),
|
||||
.transform =
|
||||
[spn_name](const ftxui::EntryState state) {
|
||||
return ftxui::hbox({
|
||||
ftxui::text(
|
||||
fmt::format("[{}] ", state.state ? "X" : "")) |
|
||||
(state.state ? ftxui::color(ftxui::Color::Red)
|
||||
: ftxui::color(ftxui::Color::Cyan)),
|
||||
ftxui::text(spn_name),
|
||||
}) |
|
||||
(state.focused ? ftxui::bold : ftxui::nothing) |
|
||||
(state.focused ? ftxui::bgcolor(ftxui::Color::Grey11)
|
||||
: ftxui::nothing) |
|
||||
ftxui::flex;
|
||||
},
|
||||
}),
|
||||
}),
|
||||
|
||||
ftxui::Maybe(
|
||||
ftxui::Container::Horizontal({
|
||||
ftxui::Renderer([]() {
|
||||
return ftxui::hbox({ftxui::separatorEmpty(), ftxui::separatorEmpty(),
|
||||
ftxui::separatorEmpty(), ftxui::separatorEmpty()});
|
||||
}),
|
||||
FromLive(m_data_verbose_, nlohmann::json::json_pointer("/SPNs/" + spns_arr_k),
|
||||
false, -100, ExpanderImpl::Root()),
|
||||
}),
|
||||
&std::get<0u>(spn_map[spn_name])),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto container = ftxui::Container::Vertical({});
|
||||
container->Add(ftxui::Container::Horizontal({
|
||||
ftxui::Checkbox({
|
||||
.checked = &std::get<0u>(s_canbus_parameters_export_map_[m_canid_]),
|
||||
.transform = [this](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
return ftxui::hbox(
|
||||
{ftxui::text(std::get<0u>(s_canbus_parameters_export_map_[m_canid_]) ? "▼ " : "▶ ")});
|
||||
},
|
||||
}),
|
||||
|
||||
ftxui::Checkbox({
|
||||
.checked = &std::get<0u>(s_canbus_parameters_export_map_[m_canid_]),
|
||||
.transform = [this](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
auto &spn_map = std::get<2u>(s_canbus_parameters_export_map_[m_canid_]);
|
||||
size_t selected_cnt = std::count_if(spn_map.begin(), spn_map.end(),
|
||||
[](auto &e) -> bool { return std::get<1u>(e.second); });
|
||||
std::string label =
|
||||
m_data_short_->contains("Label") ? (*m_data_short_)["Label"].get<std::string>() : "";
|
||||
|
||||
return ftxui::hbox({
|
||||
ftxui::text(fmt::format("{:8}", fmt::format("[{}/{}] ", selected_cnt, spn_map.size()))) |
|
||||
(state.focused ? ftxui::bold : ftxui::nothing) | ftxui::color(ftxui::Color::LightGreen),
|
||||
ftxui::text(fmt::format("{} ", m_iface_)) | (state.focused ? ftxui::bold : ftxui::nothing) |
|
||||
ftxui::color(ftxui::Color::Cyan),
|
||||
ftxui::text(fmt::format("{} ", m_canid_)) | (state.focused ? ftxui::bold : ftxui::nothing) |
|
||||
ftxui::color(ftxui::Color::LightGreen),
|
||||
ftxui::text(fmt::format("- {} ", label)) | (state.focused ? ftxui::bold : ftxui::nothing) |
|
||||
ftxui::color(ftxui::Color::Magenta),
|
||||
ftxui::filler(),
|
||||
ftxui::text("J1939 ") | (state.focused ? ftxui::bold : ftxui::nothing) |
|
||||
ftxui::color(ftxui::Color::Red),
|
||||
}) |
|
||||
(state.focused ? ftxui::bgcolor(ftxui::Color::Grey11) : ftxui::nothing) | ftxui::flex;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
// SPN list with border if not empty
|
||||
container->Add(ftxui::Maybe(selectors_container | ftxui::border, [selectors_container, this]() -> bool {
|
||||
return std::get<0u>(s_canbus_parameters_export_map_[m_canid_]) && selectors_container->ChildCount() > 0;
|
||||
}));
|
||||
|
||||
// Add empty separator if PG deployed and empty
|
||||
container->Add(ftxui::Maybe(ftxui::Renderer([]() -> ftxui::Element { return ftxui::separatorEmpty(); }),
|
||||
[selectors_container, this]() -> bool {
|
||||
return std::get<0u>(s_canbus_parameters_export_map_[m_canid_]) &&
|
||||
!selectors_container->ChildCount();
|
||||
}));
|
||||
|
||||
m_cansettings_dialog_->Add(container);
|
||||
}
|
||||
|
||||
// Add custom SPNs to export dialog if not already present (tracked by tag_id)
|
||||
if (m_export_selectors_ && m_spnSettingsMap_ && m_spnSettingsMap_->contains(m_canid_)) {
|
||||
auto &spn_map = std::get<2u>(s_canbus_parameters_export_map_[m_canid_]);
|
||||
for (const auto &[tag_id, settings] : (*m_spnSettingsMap_)[m_canid_]) {
|
||||
std::string key = fmt::format("__custom_{}", tag_id);
|
||||
if (!spn_map.contains(key)) {
|
||||
nlohmann::json custom_data = {{"SPN name", key}};
|
||||
|
||||
// Find matching SPN entry in verbose JSON
|
||||
nlohmann::json spn_verbose;
|
||||
if (m_data_verbose_->contains("SPNs")) {
|
||||
for (const auto &spn_entry : (*m_data_verbose_)["SPNs"]) {
|
||||
if (spn_entry.contains("SPN name") &&
|
||||
spn_entry["SPN name"].get<std::string>().find("(custom)") != std::string::npos) {
|
||||
std::string name_in_verbose = spn_entry["SPN name"].get<std::string>();
|
||||
std::string expected = settings.spn_name.empty() ? " (custom)" : settings.spn_name + " (custom)";
|
||||
|
||||
if (name_in_verbose == expected) {
|
||||
spn_verbose = spn_entry;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
spn_map.insert_or_assign(key,
|
||||
std::make_tuple(false, false, spn_verbose.is_null() ? custom_data : spn_verbose));
|
||||
m_export_selectors_
|
||||
->Add(
|
||||
ftxui::Container::Vertical(
|
||||
{
|
||||
ftxui::Container::Horizontal(
|
||||
{
|
||||
ftxui::Checkbox({
|
||||
.checked = &std::get<0u>(spn_map[key]),
|
||||
.transform = [this](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
return ftxui::hbox({ftxui::separatorEmpty(), ftxui::separatorEmpty(),
|
||||
ftxui::text(state.state ? "▼ " : "▶ ")});
|
||||
},
|
||||
}),
|
||||
|
||||
ftxui::Checkbox(
|
||||
{
|
||||
.checked = &std::get<1u>(spn_map[key]),
|
||||
.transform =
|
||||
[this, tag_id](const ftxui::EntryState state) {
|
||||
std::string display_name = "custom";
|
||||
if (m_spnSettingsMap_ && m_spnSettingsMap_->contains(m_canid_) &&
|
||||
(*m_spnSettingsMap_)[m_canid_].contains(tag_id)) {
|
||||
auto &s = (*m_spnSettingsMap_)[m_canid_][tag_id];
|
||||
display_name = s.spn_name.empty() ? fmt::format("custom_{}", tag_id)
|
||||
: s.spn_name + " (custom)";
|
||||
}
|
||||
|
||||
return ftxui::hbox({
|
||||
ftxui::text(fmt::format("[{}] ", state.state ? "X" : "")) |
|
||||
(state.state ? ftxui::color(ftxui::Color::Red)
|
||||
: ftxui::color(ftxui::Color::Cyan)),
|
||||
ftxui::text(display_name),
|
||||
}) |
|
||||
(state.focused ? ftxui::bold : ftxui::nothing) |
|
||||
(state.focused ? ftxui::bgcolor(ftxui::Color::Grey11)
|
||||
: ftxui::nothing) |
|
||||
ftxui::flex;
|
||||
},
|
||||
}),
|
||||
}),
|
||||
|
||||
ftxui::Maybe(ftxui::Container::Horizontal({
|
||||
ftxui::Renderer([]() {
|
||||
return ftxui::hbox({ftxui::separatorEmpty(), ftxui::separatorEmpty(),
|
||||
ftxui::separatorEmpty(), ftxui::separatorEmpty()});
|
||||
}),
|
||||
[this, tag_id]() -> ftxui::Component {
|
||||
auto wrapper = ftxui::Container::Vertical({});
|
||||
m_export_custom_containers_[tag_id] = {wrapper, 0};
|
||||
return wrapper;
|
||||
}(),
|
||||
}),
|
||||
|
||||
&std::get<0u>(spn_map[key])),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild FromLive for custom SPN export entries when fragment count changes
|
||||
if (m_spnSettingsMap_ && m_spnSettingsMap_->contains(m_canid_) && !m_data_verbose_->is_null() && m_data_verbose_->contains("SPNs")) {
|
||||
for (auto &[tag_id, pair] : m_export_custom_containers_) {
|
||||
auto &[wrapper, prev_frag_count] = pair;
|
||||
size_t cur_frag_count = 0;
|
||||
if ((*m_spnSettingsMap_)[m_canid_].contains(tag_id)) {
|
||||
cur_frag_count = (*m_spnSettingsMap_)[m_canid_][tag_id].fragments.size();
|
||||
}
|
||||
if (cur_frag_count != prev_frag_count) {
|
||||
wrapper->DetachAllChildren();
|
||||
std::string search_name;
|
||||
if ((*m_spnSettingsMap_)[m_canid_].contains(tag_id)) {
|
||||
search_name = (*m_spnSettingsMap_)[m_canid_][tag_id].spn_name + " (custom)";
|
||||
}
|
||||
const auto &spns = (*m_data_verbose_)["SPNs"];
|
||||
for (size_t i = 0; i < spns.size(); ++i) {
|
||||
if (spns[i].contains("SPN name") && spns[i]["SPN name"].get<std::string>() == search_name) {
|
||||
wrapper->Add(FromLive(m_data_verbose_, nlohmann::json::json_pointer("/SPNs/" + std::to_string(i)), false, -100, ExpanderImpl::Root()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
prev_frag_count = cur_frag_count;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
#pragma once
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/component_options.hpp>
|
||||
#include <ftxui/component/mouse.hpp>
|
||||
#include <ftxui/component/screen_interactive.hpp>
|
||||
#include <ftxui/dom/elements.hpp>
|
||||
|
||||
#include <boost/signals2.hpp>
|
||||
|
||||
#define FMT_HEADER_ONLY
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "signals.hpp"
|
||||
#include "tagsettings.hpp"
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
class CanIDUnit : public ftxui::ComponentBase {
|
||||
public:
|
||||
CanIDUnit(const std::string &iface, const std::string &canid, const std::string &protocol, size_t &spn_count, const std::vector<uint8_t> &data, ftxui::ScreenInteractive *screen,
|
||||
signals_map_t &smap, ftxui::Component content, ftxui::Component container, ftxui::Component spn_settings_dialog, ftxui::Component cansettings_dialog, bool is_deployed,
|
||||
bool is_verbose, bool is_brief, bool is_manual, std::string &, bool &, bool &canbus_parameters_export_shown, bool &filedialog_shown,
|
||||
std::map<std::string, std::map<int32_t, ftxui::Component>> &spnSettingsFormMap,
|
||||
spn_settings_map_t &spnSettingsMap);
|
||||
|
||||
inline const std::string &getIfaceName() const { return m_iface_; }
|
||||
inline const std::string &getCanID() const { return m_canid_; }
|
||||
inline size_t getDataSize() const { return m_data_.size(); }
|
||||
inline const std::vector<uint8_t> &getData() const { return m_data_; }
|
||||
|
||||
inline bool getDeployed() const { return m_deployed_; }
|
||||
inline bool getVerbose() const { return m_verbose_; }
|
||||
inline bool getBrief() const { return m_brief_; }
|
||||
inline bool getManual() const { return m_manual_mode_; }
|
||||
inline ftxui::Component getSpnSettingsForm() { return m_spnSettingsForm_; }
|
||||
inline const auto &getParametersExportMap() const { return s_canbus_parameters_export_map_; }
|
||||
|
||||
void update(const can_frame_data_s &data, const can_frame_diff_s &diff,
|
||||
std::shared_ptr<nlohmann::json> verbose, std::shared_ptr<nlohmann::json> brief);
|
||||
|
||||
bool OnEvent(ftxui::Event event) override;
|
||||
|
||||
private:
|
||||
const ftxui::Component m_spnSettingsForm_;
|
||||
const std::string m_canid_, m_iface_;
|
||||
std::vector<uint8_t> m_data_;
|
||||
mutable bool m_deployed_ = false, m_verbose_ = false, m_brief_ = true, m_manual_mode_ = false;
|
||||
can_frame_diff_s m_diff_;
|
||||
std::string m_last_update_time_;
|
||||
bool m_hovered_ = false;
|
||||
ftxui::Box m_box_ = {};
|
||||
std::shared_ptr<nlohmann::json> m_data_verbose_ = std::make_shared<nlohmann::json>(nullptr);
|
||||
std::shared_ptr<nlohmann::json> m_data_short_ = std::make_shared<nlohmann::json>(nullptr);
|
||||
ftxui::Component m_brief_content_, m_verbose_content_;
|
||||
ftxui::Component m_cansettings_dialog_;
|
||||
spn_settings_map_t *m_spnSettingsMap_ = nullptr;
|
||||
size_t m_last_verbose_spn_count_ = 0;
|
||||
size_t m_last_brief_spn_count_ = 0;
|
||||
ftxui::Component m_export_selectors_;
|
||||
std::map<int32_t, std::pair<ftxui::Component, size_t>> m_export_custom_containers_;
|
||||
|
||||
static inline std::map<
|
||||
/* canid */ std::string,
|
||||
std::tuple</* deployed flag */ bool, /* has data flag */ bool,
|
||||
/* Selected spns to export */ std::map</* spn name */ std::string, std::tuple</* deployed */ bool, /* selected */ bool, /* data */ nlohmann::json>>>>
|
||||
s_canbus_parameters_export_map_ = {};
|
||||
};
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
// #include "src/canid_unit.hpp"
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/component_base.hpp>
|
||||
#include <ftxui/component/component_options.hpp>
|
||||
#include <ftxui/component/mouse.hpp>
|
||||
#include <ftxui/component/screen_interactive.hpp>
|
||||
#include <ftxui/dom/elements.hpp>
|
||||
#include <memory>
|
||||
|
||||
#define FMT_HEADER_ONLY
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "signals.hpp"
|
||||
#include <map>
|
||||
#include <optional>
|
||||
|
||||
ftxui::Component makeCanSettingsExportDialog(ftxui::ScreenInteractive *scr, signals_map_t &smap, ftxui::Component canids, bool &shown, bool &file_export_shown) {
|
||||
class Impl : public ftxui::ComponentBase {
|
||||
public:
|
||||
Impl(ftxui::ScreenInteractive *scr, signals_map_t &smap, ftxui::Component canids, bool &shown, bool &file_export_shown) {
|
||||
auto cmps = ftxui::Container::Vertical({});
|
||||
|
||||
for (uint32_t i = 0; i < canids->ChildCount(); ++i) {
|
||||
cmps->Add(canids->ChildAt(i));
|
||||
}
|
||||
|
||||
Add({
|
||||
ftxui::Container::Vertical({
|
||||
cmps,
|
||||
|
||||
ftxui::Container::Horizontal({
|
||||
ftxui::Button({.label = "Export", .on_click = [&file_export_shown]() { file_export_shown = true; }}),
|
||||
ftxui::Button({.label = "Close", .on_click = [&shown]() { shown = false; }}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return ftxui::Make<Impl>(scr, smap, canids, shown, file_export_shown);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,132 @@
|
|||
#include <boost/asio.hpp>
|
||||
#include <condition_variable>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/screen_interactive.hpp>
|
||||
#include <ftxui/dom/elements.hpp>
|
||||
#include <map>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#define FMT_HEADER_ONLY
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "signals.hpp"
|
||||
|
||||
static std::filesystem::path currentPath = std::filesystem::current_path(),
|
||||
pathToExport = std::filesystem::current_path();
|
||||
static std::string fileName = "";
|
||||
|
||||
static void updateFileDialog(ftxui::Component entryList, signals_map_t &smap) {
|
||||
entryList->DetachAllChildren();
|
||||
|
||||
for (const auto &entry : {".", ".."}) {
|
||||
entryList->Add({
|
||||
ftxui::Button({
|
||||
.label = entry,
|
||||
.on_click =
|
||||
[entryList, entry, &smap]() {
|
||||
if (std::string(entry) == "..") {
|
||||
currentPath = currentPath.parent_path();
|
||||
updateFileDialog(entryList, smap);
|
||||
}
|
||||
},
|
||||
|
||||
.transform = [entry](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
return ftxui::text(entry) | ftxui::color(ftxui::Color::Blue) |
|
||||
(state.focused ? ftxui::bgcolor(ftxui::Color::Grey7) : ftxui::nothing);
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
for (const auto &entry : std::filesystem::directory_iterator(currentPath)) {
|
||||
entryList->Add({
|
||||
ftxui::Button({
|
||||
.label = entry.path().filename().c_str(),
|
||||
.on_click =
|
||||
[entry, entryList, &smap]() {
|
||||
if (entry.is_directory()) {
|
||||
currentPath = entry;
|
||||
} else {
|
||||
fileName = entry.path().filename().c_str();
|
||||
pathToExport = entry.path();
|
||||
}
|
||||
|
||||
updateFileDialog(entryList, smap);
|
||||
},
|
||||
|
||||
.transform = [entry](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
return ftxui::text(entry.path().filename().c_str()) |
|
||||
(entry.is_directory() ? ftxui::color(ftxui::Color::Blue) : ftxui::nothing) |
|
||||
(state.focused ? ftxui::bgcolor(ftxui::Color::Grey7) : ftxui::nothing);
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
ftxui::Component makeFileDialog(ftxui::ScreenInteractive *scr, signals_map_t &smap, bool &shown) {
|
||||
class FileDialog : public ftxui::ComponentBase {
|
||||
public:
|
||||
explicit FileDialog(ftxui::ScreenInteractive *scr, signals_map_t &smap, bool &shown) {
|
||||
static bool checked = false;
|
||||
auto entryList = ftxui::Container::Vertical({});
|
||||
|
||||
Add({
|
||||
ftxui::Container::Vertical({
|
||||
ftxui::Renderer([&]() -> ftxui::Element { return ftxui::text("Export file"); }) |
|
||||
ftxui::color(ftxui::Color::Red) | ftxui::hcenter,
|
||||
|
||||
ftxui::Container::Horizontal({
|
||||
ftxui::Renderer([]() -> ftxui::Element {
|
||||
return ftxui::text(fmt::format("{}: ", currentPath.c_str())) | ftxui::bold;
|
||||
}),
|
||||
|
||||
ftxui::Input({
|
||||
.content = &fileName,
|
||||
.multiline = false,
|
||||
.on_change = []() { (pathToExport = currentPath) /= fileName; },
|
||||
}),
|
||||
}),
|
||||
|
||||
ftxui::Renderer([]() { return ftxui::separatorEmpty(); }),
|
||||
|
||||
entryList | ftxui::vscroll_indicator | ftxui::frame | ftxui::flex,
|
||||
|
||||
ftxui::Container::Horizontal({
|
||||
ftxui::Button({
|
||||
.on_click =
|
||||
[&]() {
|
||||
smap.get<void(const std::string &)>("export_file_request")
|
||||
->operator()(pathToExport.c_str());
|
||||
shown = false;
|
||||
},
|
||||
|
||||
.transform = [this](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
return ftxui::text(" >[Export]< ") | (state.focused ? ftxui::bold : ftxui::nothing) |
|
||||
ftxui::color(ftxui::Color::Cyan) |
|
||||
(state.focused ? ftxui::bgcolor(ftxui::Color::Grey11) : ftxui::nothing);
|
||||
},
|
||||
}),
|
||||
|
||||
ftxui::Button({
|
||||
.on_click = [&]() { shown = false; },
|
||||
.transform = [this](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
return ftxui::text(" >[Cancel]< ") | (state.focused ? ftxui::bold : ftxui::nothing) |
|
||||
ftxui::color(ftxui::Color::Cyan) |
|
||||
(state.focused ? ftxui::bgcolor(ftxui::Color::Grey11) : ftxui::nothing);
|
||||
},
|
||||
}),
|
||||
}) | ftxui::hcenter,
|
||||
|
||||
}) | ftxui::border |
|
||||
ftxui::size(ftxui::WIDTH, ftxui::EQUAL, 96) | ftxui::size(ftxui::HEIGHT, ftxui::EQUAL, 48),
|
||||
});
|
||||
|
||||
updateFileDialog(entryList, smap);
|
||||
}
|
||||
};
|
||||
|
||||
return ftxui::Make<FileDialog>(scr, smap, shown);
|
||||
}
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
#include "tuple.hpp"
|
||||
#include <functional>
|
||||
#include <boost/convert.hpp>
|
||||
#include <boost/convert/stream.hpp>
|
||||
#include <boost/functional/hash.hpp>
|
||||
#include <boost/lexical_cast.hpp>
|
||||
#include <boost/signals2.hpp>
|
||||
#include <boost/uuid/uuid.hpp>
|
||||
#include <boost/uuid/uuid_generators.hpp>
|
||||
#include <boost/uuid/uuid_io.hpp>
|
||||
|
||||
#include <condition_variable>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/screen_interactive.hpp>
|
||||
#include <ftxui/dom/elements.hpp>
|
||||
#include <future>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <ranges>
|
||||
#include <unordered_map>
|
||||
|
||||
#define FMT_HEADER_ONLY
|
||||
#include <fmt/format.h>
|
||||
#include <fmt/ranges.h>
|
||||
|
||||
#include "signals.hpp"
|
||||
#include "tagsettings.hpp"
|
||||
|
||||
class GraphId_ {
|
||||
public:
|
||||
GraphId_(const std::string &name, const std::string &uuid = generateUUID_()) : m_hash_(std::hash<std::string>()(uuid)), m_name_(name) {}
|
||||
~GraphId_() = default;
|
||||
|
||||
bool operator==(const GraphId_ &other) const { return m_hash_ == other.hash(); }
|
||||
|
||||
size_t hash() const { return m_hash_; }
|
||||
const std::string &name() const { return m_name_; }
|
||||
|
||||
private:
|
||||
static std::string generateUUID_() { return boost::lexical_cast<std::string>(boost::uuids::random_generator()()); };
|
||||
const size_t m_hash_;
|
||||
const std::string m_name_;
|
||||
};
|
||||
|
||||
template <> struct std::hash<GraphId_> {
|
||||
std::size_t operator()(const GraphId_ &g) const noexcept { return g.hash(); }
|
||||
};
|
||||
|
||||
ftxui::Component makeGraph(const std::string &canid, const struct spn_settings_s &settings, std::function<std::vector<uint8_t>()> data_fn, ftxui::ScreenInteractive *screen,
|
||||
signals_map_t &smap) {
|
||||
static const auto json_array_to_payload = [](const nlohmann::json &settings, const std::vector<std::string> &strvec_bytes) {
|
||||
std::vector<uint8_t> res;
|
||||
|
||||
if (settings.contains("size") && settings.contains("pos") && settings.contains("le")) {
|
||||
boost::cnv::cstream converter;
|
||||
size_t size_setting = settings["size"].get<size_t>();
|
||||
res.reserve(size_setting);
|
||||
converter(std::hex)(std::skipws);
|
||||
auto f = apply<int>(std::ref(converter)).value_or(-1);
|
||||
|
||||
if (auto [pos, size, data_size] =
|
||||
std::tuple<int32_t, int32_t, size_t>{
|
||||
settings["pos"].get<int32_t>(),
|
||||
size_setting,
|
||||
strvec_bytes.size(),
|
||||
};
|
||||
pos < data_size && (pos + size) < data_size) {
|
||||
|
||||
{
|
||||
namespace rv = std::ranges::views;
|
||||
const auto push = [&](const auto &i) { res.push_back(boost::lexical_cast<int32_t>(f(strvec_bytes[i]))); };
|
||||
|
||||
auto seq = rv::iota(pos, pos + size);
|
||||
if (settings["le"].get<bool>()) {
|
||||
for (const auto &i : seq) {
|
||||
|
||||
push(i);
|
||||
}
|
||||
} else {
|
||||
for (const auto &i : seq | rv::reverse) {
|
||||
|
||||
push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
static const auto create_json = [](const std::vector<uint8_t> &data, const struct spn_settings_s &settings) {
|
||||
nlohmann::json array = nlohmann::json::array(), settings_json, json;
|
||||
uint32_t crc = std::accumulate(data.begin(), data.end(), uint32_t{});
|
||||
double spn_val = 0.0f;
|
||||
|
||||
for (auto &d : data) {
|
||||
array.push_back(fmt::format("{:2x}", d));
|
||||
}
|
||||
|
||||
json = {{"crc", crc}, {"data", array}};
|
||||
|
||||
{
|
||||
std::string current_setting;
|
||||
try {
|
||||
|
||||
tp::for_each(
|
||||
std::tuple{
|
||||
std::make_tuple("name", &settings.spn_name, static_cast<std::string *>(nullptr)),
|
||||
std::make_tuple("le", &settings.le, static_cast<bool *>(nullptr)),
|
||||
std::make_tuple("size", &settings.size, static_cast<size_t *>(nullptr)),
|
||||
std::make_tuple("offset", &settings.offset, static_cast<double *>(nullptr)),
|
||||
std::make_tuple("pos", &settings.pos, static_cast<size_t *>(nullptr)),
|
||||
std::make_tuple("x", &settings.x_coeff, static_cast<double *>(nullptr)),
|
||||
std::make_tuple("y", &settings.y_coeff, static_cast<double *>(nullptr)),
|
||||
std::make_tuple("discrete", &settings.discrete, static_cast<bool *>(nullptr)),
|
||||
std::make_tuple("bit_offset", &settings.bit_offset, static_cast<size_t *>(nullptr)),
|
||||
std::make_tuple("bit_count", &settings.bit_count, static_cast<size_t *>(nullptr)),
|
||||
std::make_tuple("uuid", &settings.uuid, static_cast<std::string *>(nullptr)),
|
||||
},
|
||||
|
||||
[&](const auto &e) {
|
||||
current_setting = std::get<0u>(e);
|
||||
settings_json[std::get<0u>(e)] = boost::lexical_cast<std::remove_cvref_t<decltype(*std::get<2u>(e))>>(*std::get<1u>(e));
|
||||
});
|
||||
|
||||
current_setting.clear();
|
||||
|
||||
std::vector<std::string> v = json["data"].is_array() ? json["data"].template get<std::vector<std::string>>() : std::vector<std::string>{};
|
||||
std::vector<uint8_t> bytes = json_array_to_payload(settings_json, v);
|
||||
|
||||
json["payload_bytes"] = [&]() {
|
||||
nlohmann::json::array_t array;
|
||||
for (const auto &byte : bytes) {
|
||||
array.push_back(fmt::format("{:2x}", byte));
|
||||
}
|
||||
|
||||
return array;
|
||||
}();
|
||||
|
||||
int32_t integer = 0;
|
||||
|
||||
for (auto iter = int32_t{0}; const auto &b : bytes) {
|
||||
integer |= b << ((iter++) * UINT8_WIDTH);
|
||||
}
|
||||
|
||||
if (settings_json["x"].get<double>() / settings_json["y"].get<double>() != NAN) {
|
||||
spn_val = static_cast<double>(integer) * settings_json["x"].get<double>() / settings_json["y"].get<double>() + settings_json["offset"].get<double>();
|
||||
}
|
||||
|
||||
json["spn_value"] = spn_val;
|
||||
} catch (const boost::bad_lexical_cast &e) {
|
||||
|
||||
settings_json["warning"] = fmt::format("Bad '{}' setting", current_setting);
|
||||
}
|
||||
}
|
||||
|
||||
return json;
|
||||
};
|
||||
|
||||
class Impl : public ftxui::ComponentBase {
|
||||
public:
|
||||
explicit Impl(const std::string &canid, const struct spn_settings_s &settings, std::function<std::vector<uint8_t>()> data_fn, ftxui::ScreenInteractive *screen, signals_map_t &smap)
|
||||
: m_settings_(settings), m_data_fn_(std::move(data_fn)) {
|
||||
|
||||
auto renderer = ftxui::Renderer([this]() {
|
||||
auto data = m_data_fn_();
|
||||
auto json = create_json(data, m_settings_);
|
||||
return ftxui::vbox({
|
||||
ftxui::text(fmt::format("Raw data: {}", json["data"].dump())) | ftxui::color(ftxui::Color::Cyan) | ftxui::hcenter,
|
||||
ftxui::text(fmt::format("CRC: {}", json["crc"].dump())) | ftxui::color(ftxui::Color::Cyan) | ftxui::hcenter,
|
||||
ftxui::text(fmt::format("Payload: {}", json.contains("payload_bytes") ? json["payload_bytes"].dump() : "[]")) | ftxui::color(ftxui::Color::Cyan) |
|
||||
ftxui::hcenter,
|
||||
|
||||
ftxui::hbox({
|
||||
ftxui::text("Value: ") | ftxui::bold,
|
||||
ftxui::text(fmt::format("{:.6g}", json.contains("tag_value") ? json["tag_value"].template get<double>() : 0.0f)) |
|
||||
ftxui::color(ftxui::Color::IndianRed),
|
||||
}) | ftxui::hcenter,
|
||||
});
|
||||
});
|
||||
|
||||
Add(renderer);
|
||||
}
|
||||
|
||||
private:
|
||||
spn_settings_s m_settings_;
|
||||
std::function<std::vector<uint8_t>()> m_data_fn_;
|
||||
};
|
||||
|
||||
return ftxui::Make<Impl>(canid, settings, std::move(data_fn), screen, smap);
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
#include "headless.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <fstream>
|
||||
|
||||
#define FMT_HEADER_ONLY
|
||||
#include <fmt/format.h>
|
||||
#include <fmt/ranges.h>
|
||||
|
||||
HeadlessHandler::HeadlessHandler(const std::string &output_file)
|
||||
: output_file_(output_file) {}
|
||||
|
||||
void HeadlessHandler::onDatabaseReady(sqlite::database &db) {
|
||||
database_ = &db;
|
||||
}
|
||||
|
||||
void HeadlessHandler::onBatch(const std::vector<can_frame_update_s> &batch) {
|
||||
if (!database_) return;
|
||||
|
||||
extern std::pair<nlohmann::json, nlohmann::json> processFrame(sqlite::database &db, const std::string &iface, const std::string &canid, const std::vector<uint8_t> &data);
|
||||
|
||||
for (const auto &entry : batch) {
|
||||
const auto &iface = entry.iface;
|
||||
const auto &canid = entry.canid;
|
||||
const auto &frame_data = entry.data;
|
||||
|
||||
if (configuration_map_.contains(canid)) continue;
|
||||
|
||||
auto [verbose, brief] = processFrame(*database_, iface, canid, frame_data.payload);
|
||||
configuration_map_.insert({
|
||||
canid,
|
||||
{std::move(verbose), std::move(brief)},
|
||||
});
|
||||
|
||||
nlohmann::json j;
|
||||
j[canid] = verboseToExportJson(configuration_map_.at(canid).first);
|
||||
|
||||
if (!output_file_.empty()) {
|
||||
nlohmann::json file_j;
|
||||
|
||||
{
|
||||
std::ifstream fin(output_file_);
|
||||
if (fin.is_open() && fin.peek() != std::ifstream::traits_type::eof()) {
|
||||
try {
|
||||
fin >> file_j;
|
||||
} catch (const std::exception &e) {
|
||||
fmt::print("{}\r\n", e.what());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &[k, v] : j.items()) {
|
||||
file_j[k] = v;
|
||||
}
|
||||
|
||||
{
|
||||
std::ofstream fout(output_file_, std::ofstream::out | std::ofstream::trunc);
|
||||
if (fout.is_open()) {
|
||||
fout << file_j;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt::print("{}\r\n\r\n", j.dump());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
#pragma once
|
||||
|
||||
#include "can_data.hpp"
|
||||
#include "sqlite_modern_cpp.h"
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
class HeadlessHandler {
|
||||
public:
|
||||
explicit HeadlessHandler(const std::string &output_file);
|
||||
|
||||
void onDatabaseReady(sqlite::database &db);
|
||||
void onBatch(const std::vector<can_frame_update_s> &batch);
|
||||
|
||||
private:
|
||||
std::string output_file_;
|
||||
sqlite::database *database_ = nullptr;
|
||||
std::map<std::string, std::pair<nlohmann::json, nlohmann::json>> configuration_map_;
|
||||
};
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
#include "button.hpp"
|
||||
#include <ftxui/component/captured_mouse.hpp>
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/event.hpp>
|
||||
#include <utility>
|
||||
|
||||
ftxui::Component MyButton(const char *prefix, const char *title, std::function<void()> on_click) {
|
||||
class Impl : public ftxui::ComponentBase {
|
||||
public:
|
||||
Impl(const char *prefix, const char *title, std::function<void()> on_click) : on_click_(std::move(on_click)), prefix_(prefix), title_(title) {}
|
||||
|
||||
// Component implementation:
|
||||
ftxui::Element OnRender() override {
|
||||
auto style = Focused() ? (ftxui::Decorator(ftxui::inverted) | ftxui::focus) : ftxui::nothing;
|
||||
return ftxui::hbox({
|
||||
ftxui::text(prefix_),
|
||||
ftxui::text(title_) | style | ftxui::color(ftxui::Color::GrayDark) | ftxui::reflect(box_),
|
||||
});
|
||||
}
|
||||
|
||||
bool OnEvent(ftxui::Event event) override {
|
||||
if (event.is_mouse() && box_.Contain(event.mouse().x, event.mouse().y)) {
|
||||
if (!CaptureMouse(event))
|
||||
return false;
|
||||
|
||||
TakeFocus();
|
||||
|
||||
if (event.mouse().button == ftxui::Mouse::Left && event.mouse().motion == ftxui::Mouse::Pressed) {
|
||||
on_click_();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event == ftxui::Event::Return) {
|
||||
on_click_();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Focusable() const final { return true; }
|
||||
|
||||
private:
|
||||
std::function<void()> on_click_;
|
||||
const char *prefix_;
|
||||
const char *title_;
|
||||
ftxui::Box box_;
|
||||
};
|
||||
|
||||
return ftxui::Make<Impl>(prefix, title, std::move(on_click));
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <functional>
|
||||
|
||||
ftxui::Component MyButton(const char *prefix, const char *title, std::function<void()>);
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
#include "expander.hpp"
|
||||
#include <algorithm>
|
||||
#include <climits>
|
||||
|
||||
ExpanderImpl::~ExpanderImpl() {
|
||||
// Remove this from parent:
|
||||
if (parent_) {
|
||||
parent_->children_.erase(std::remove(parent_->children_.begin(), parent_->children_.end(), this), parent_->children_.end());
|
||||
parent_ = nullptr;
|
||||
}
|
||||
|
||||
// Remove this from children:
|
||||
for (auto &child : children_) {
|
||||
child->parent_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// static
|
||||
Expander ExpanderImpl::Root() { return std::make_unique<ExpanderImpl>(); }
|
||||
|
||||
Expander ExpanderImpl::Child() {
|
||||
auto expander = Root();
|
||||
expander->parent_ = this;
|
||||
children_.push_back(expander.get());
|
||||
return expander;
|
||||
}
|
||||
|
||||
bool ExpanderImpl::Expand() {
|
||||
int min_level = MinLevel();
|
||||
Expand(min_level + 1);
|
||||
return MinLevel() != min_level;
|
||||
}
|
||||
|
||||
bool ExpanderImpl::Collapse() {
|
||||
int max_level = MaxLevel();
|
||||
Collapse(max_level - 1);
|
||||
return MaxLevel() != max_level;
|
||||
}
|
||||
|
||||
int ExpanderImpl::MinLevel() const {
|
||||
if (!expanded) {
|
||||
return 0;
|
||||
}
|
||||
if (children_.empty()) {
|
||||
return 1;
|
||||
}
|
||||
int min_level = INT_MAX;
|
||||
for (auto &child : children_) {
|
||||
min_level = std::min(min_level, 1 + child->MinLevel());
|
||||
}
|
||||
return min_level;
|
||||
}
|
||||
|
||||
int ExpanderImpl::MaxLevel() const {
|
||||
if (!expanded) {
|
||||
return 0;
|
||||
}
|
||||
int max_level = 1;
|
||||
for (auto &child : children_) {
|
||||
max_level = std::max(max_level, 1 + child->MaxLevel());
|
||||
}
|
||||
return max_level;
|
||||
}
|
||||
|
||||
void ExpanderImpl::Expand(int minLevel) {
|
||||
if (minLevel <= 0)
|
||||
return;
|
||||
expanded = true;
|
||||
minLevel--;
|
||||
for (auto &child : children_) {
|
||||
child->Expand(minLevel);
|
||||
}
|
||||
}
|
||||
|
||||
void ExpanderImpl::Collapse(int maxLevel) {
|
||||
if (maxLevel <= 0) {
|
||||
expanded = false;
|
||||
}
|
||||
maxLevel--;
|
||||
for (auto &child : children_) {
|
||||
child->Collapse(maxLevel);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
class ExpanderImpl;
|
||||
using Expander = std::unique_ptr<ExpanderImpl>;
|
||||
|
||||
class ExpanderImpl {
|
||||
public:
|
||||
~ExpanderImpl();
|
||||
static Expander Root();
|
||||
Expander Child();
|
||||
|
||||
bool Expand();
|
||||
bool Collapse();
|
||||
|
||||
int MinLevel() const;
|
||||
int MaxLevel() const;
|
||||
|
||||
bool expanded = false;
|
||||
|
||||
private:
|
||||
void Expand(int minLevel);
|
||||
void Collapse(int maxLevel);
|
||||
|
||||
ExpanderImpl *parent_ = nullptr;
|
||||
std::vector<ExpanderImpl *> children_;
|
||||
};
|
||||
|
|
@ -0,0 +1,477 @@
|
|||
#include "json.hpp"
|
||||
#include <ftxui/dom/table.hpp>
|
||||
#include <utility>
|
||||
|
||||
#define FMT_HEADER_ONLY
|
||||
#include <fmt/format.h>
|
||||
|
||||
bool ParseJSON(const std::string &input, nlohmann::json &out) {
|
||||
class JsonParser : public nlohmann::detail::json_sax_dom_parser<nlohmann::json, void> {
|
||||
public:
|
||||
explicit JsonParser(nlohmann::json &j) : nlohmann::detail::json_sax_dom_parser<nlohmann::json, void>(j, false) {}
|
||||
static bool parse_error(std::size_t /*position*/, const std::string & /*last_token*/, const nlohmann::json::exception &ex) {
|
||||
std::cerr << std::endl;
|
||||
std::cerr << ex.what() << std::endl;
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
JsonParser parser(out);
|
||||
return nlohmann::json::sax_parse(input, &parser);
|
||||
}
|
||||
|
||||
ftxui::Component From(const nlohmann::json &json, bool is_last, int depth, const Expander &expander) {
|
||||
if (json.is_object())
|
||||
return FromObject(Empty(), json, is_last, depth, expander);
|
||||
if (json.is_array())
|
||||
return FromArrayAny(Empty(), json, is_last, depth, expander);
|
||||
if (json.is_string())
|
||||
return FromString(json, is_last);
|
||||
if (json.is_number())
|
||||
return FromNumber(json, is_last);
|
||||
if (json.is_boolean())
|
||||
return FromBoolean(json, is_last);
|
||||
if (json.is_null())
|
||||
return FromNull(is_last);
|
||||
return Unimplemented();
|
||||
}
|
||||
|
||||
ftxui::Component FromString(const nlohmann::json &json, bool is_last) {
|
||||
std::string value = json;
|
||||
std::string str = "\"" + value + "\"";
|
||||
return Basic(str, ftxui::Color::GreenLight, is_last);
|
||||
}
|
||||
|
||||
ftxui::Component FromNumber(const nlohmann::json &json, bool is_last) {
|
||||
if (json.is_number_float())
|
||||
return Basic(fmt::format("{:.6g}", json.get<double>()), ftxui::Color::CyanLight, is_last);
|
||||
return Basic(json.dump(), ftxui::Color::CyanLight, is_last);
|
||||
}
|
||||
|
||||
ftxui::Component FromBoolean(const nlohmann::json &json, bool is_last) {
|
||||
bool value = json;
|
||||
std::string str = value ? "true" : "false";
|
||||
return Basic(str, ftxui::Color::YellowLight, is_last);
|
||||
}
|
||||
|
||||
ftxui::Component FromNull(bool is_last) { return Basic("null", ftxui::Color::RedLight, is_last); }
|
||||
|
||||
ftxui::Component Unimplemented() {
|
||||
return ftxui::Renderer([] { return ftxui::text("Unimplemented"); });
|
||||
}
|
||||
|
||||
ftxui::Component Empty() {
|
||||
return ftxui::Renderer([] { return ftxui::text(""); });
|
||||
}
|
||||
|
||||
ftxui::Component Basic(const std::string &value, ftxui::Color c, bool is_last) {
|
||||
return ftxui::Renderer([value, c, is_last](bool) {
|
||||
auto element = ftxui::paragraph(value) | color(c);
|
||||
if (!is_last)
|
||||
element = ftxui::hbox({element, ftxui::text(",")});
|
||||
return element;
|
||||
});
|
||||
}
|
||||
|
||||
bool IsSuitableForTableView(const nlohmann::json &) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ftxui::Component Indentation(const ftxui::Component &child) {
|
||||
return ftxui::Renderer(child, [child] {
|
||||
return ftxui::hbox({
|
||||
ftxui::text(" "),
|
||||
child->Render(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ftxui::Component FakeHorizontal(const ftxui::Component &a, const ftxui::Component &b) {
|
||||
auto c = ftxui::Container::Vertical({a, b});
|
||||
c->SetActiveChild(b);
|
||||
|
||||
return ftxui::Renderer(c, [a, b] {
|
||||
return ftxui::hbox({
|
||||
a->Render(),
|
||||
b->Render(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class ComponentExpandable : public ftxui::ComponentBase {
|
||||
public:
|
||||
explicit ComponentExpandable(const Expander &expander) : expander_(expander->Child()) {}
|
||||
|
||||
bool &Expanded() const { return expander_->expanded; }
|
||||
|
||||
bool OnEvent(ftxui::Event event) override {
|
||||
if (event.is_mouse())
|
||||
return ftxui::ComponentBase::OnEvent(event);
|
||||
return false;
|
||||
}
|
||||
|
||||
Expander expander_;
|
||||
};
|
||||
|
||||
ftxui::Component FromObject(const ftxui::Component &prefix, const nlohmann::json &json, bool is_last, int depth, const Expander &expander) {
|
||||
class Impl : public ComponentExpandable {
|
||||
public:
|
||||
Impl(const ftxui::Component &prefix, const nlohmann::json &json, bool is_last, int depth, const Expander &expander) : ComponentExpandable(expander) {
|
||||
Expanded() = (depth <= 1);
|
||||
|
||||
auto children = ftxui::Container::Vertical({});
|
||||
int size = static_cast<int>(json.size());
|
||||
|
||||
for (auto &it : json.items()) {
|
||||
bool is_children_last = --size == 0;
|
||||
children->Add(Indentation(FromKeyValue(it.key(), it.value(), is_children_last, depth + 1, expander_)));
|
||||
}
|
||||
|
||||
if (is_last)
|
||||
children->Add(ftxui::Renderer([] { return ftxui::text("}"); }));
|
||||
else
|
||||
children->Add(ftxui::Renderer([] { return ftxui::text("},"); }));
|
||||
|
||||
auto toggle = MyToggle("{", is_last ? "{...}" : "{...},", &Expanded());
|
||||
Add(ftxui::Container::Vertical({
|
||||
FakeHorizontal(prefix, toggle),
|
||||
Maybe(children, &Expanded()),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
return ftxui::Make<Impl>(prefix, json, is_last, depth, expander);
|
||||
}
|
||||
|
||||
ftxui::Component FromKeyValue(const std::string &key, const nlohmann::json &value, bool is_last, int depth, const Expander &expander) {
|
||||
std::string str = "\"" + key + "\"";
|
||||
if (value.is_object() || value.is_array()) {
|
||||
auto prefix = ftxui::Renderer([str] {
|
||||
return ftxui::hbox({
|
||||
ftxui::text(str) | color(ftxui::Color::BlueLight),
|
||||
ftxui::text(": "),
|
||||
});
|
||||
});
|
||||
|
||||
if (value.is_object())
|
||||
return FromObject(prefix, value, is_last, depth, expander);
|
||||
else
|
||||
return FromArrayAny(prefix, value, is_last, depth, expander);
|
||||
}
|
||||
|
||||
auto child = From(value, is_last, depth, expander);
|
||||
return ftxui::Renderer(child, [str, child] {
|
||||
return ftxui::hbox({
|
||||
ftxui::text(str) | color(ftxui::Color::BlueLight),
|
||||
ftxui::text(": "),
|
||||
child->Render(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ftxui::Component FromArrayAny(const ftxui::Component &prefix, const nlohmann::json &json, bool is_last, int depth, const Expander &expander) {
|
||||
class Impl : public ftxui::ComponentBase {
|
||||
public:
|
||||
Impl(const ftxui::Component &prefix, const nlohmann::json &json, bool is_last, int depth, const Expander &expander) { Add(FromArray(prefix, json, is_last, depth, expander)); }
|
||||
};
|
||||
|
||||
return ftxui::Make<Impl>(prefix, json, is_last, depth, expander);
|
||||
}
|
||||
|
||||
ftxui::Component FromArray(const ftxui::Component &prefix, const nlohmann::json &json, bool is_last, int depth, const Expander &expander) {
|
||||
class Impl : public ComponentExpandable {
|
||||
public:
|
||||
Impl(ftxui::Component prefix, const nlohmann::json &json, bool is_last, int depth, const Expander &expander)
|
||||
: ComponentExpandable(expander), prefix_(std::move(prefix)), json_(json), is_last_(is_last), depth_(depth) {
|
||||
Expanded() = (depth <= 0);
|
||||
auto children = ftxui::Container::Vertical({});
|
||||
int size = static_cast<int>(json_.size());
|
||||
for (auto &it : json_.items()) {
|
||||
bool is_children_last = --size == 0;
|
||||
children->Add(Indentation(From(it.value(), is_children_last, depth + 1, expander_)));
|
||||
}
|
||||
|
||||
if (is_last)
|
||||
children->Add(ftxui::Renderer([] { return ftxui::text("]"); }));
|
||||
else
|
||||
children->Add(ftxui::Renderer([] { return ftxui::text("],"); }));
|
||||
|
||||
auto toggle = MyToggle("[", is_last ? "[...]" : "[...],", &Expanded());
|
||||
|
||||
auto upper = ftxui::Container::Horizontal({
|
||||
FakeHorizontal(prefix_, toggle),
|
||||
});
|
||||
|
||||
// Turn this array into a table.
|
||||
if (IsSuitableForTableView(json)) {
|
||||
auto expand_button = MyButton(" ", "(table view)", [this, &expander] {
|
||||
auto *parent = Parent();
|
||||
auto replacement = FromTable(prefix_, json_, is_last_, depth_, expander);
|
||||
parent->DetachAllChildren(); // Detach this.
|
||||
parent->Add(replacement);
|
||||
});
|
||||
|
||||
upper = ftxui::Container::Horizontal({upper, expand_button});
|
||||
}
|
||||
|
||||
Add(ftxui::Container::Vertical({
|
||||
upper,
|
||||
Maybe(children, &Expanded()),
|
||||
}));
|
||||
}
|
||||
|
||||
ftxui::Component prefix_;
|
||||
const nlohmann::json &json_;
|
||||
bool is_last_;
|
||||
int depth_;
|
||||
};
|
||||
|
||||
return ftxui::Make<Impl>(prefix, json, is_last, depth, expander);
|
||||
}
|
||||
|
||||
ftxui::Component FromTable(const ftxui::Component &prefix, const nlohmann::json &json, bool is_last, int depth, const Expander &expander) {
|
||||
class Impl : public ftxui::ComponentBase {
|
||||
public:
|
||||
Impl(ftxui::Component prefix, const nlohmann::json &json, bool is_last, int depth, const Expander &expander)
|
||||
: prefix_(std::move(prefix)), json_(json), is_last_(is_last), depth_(depth) {
|
||||
std::vector<ftxui::Component> components;
|
||||
|
||||
// Turn this array into a table.
|
||||
expand_button_ = MyButton("", "(array view)", [this, &expander] {
|
||||
auto *parent = Parent();
|
||||
auto replacement = FromArray(prefix_, json_, is_last_, depth_, expander);
|
||||
replacement->OnEvent(ftxui::Event::ArrowRight);
|
||||
parent->DetachAllChildren(); // Detach this.
|
||||
parent->Add(replacement);
|
||||
});
|
||||
|
||||
components.push_back(expand_button_);
|
||||
|
||||
std::map<std::string, int> columns_index;
|
||||
for (auto &row : json_.items()) {
|
||||
children_.emplace_back();
|
||||
auto &children_row = children_.back();
|
||||
for (auto &cell : row.value().items()) {
|
||||
// Does it require a new column?
|
||||
if (!columns_index.count(cell.key())) {
|
||||
columns_index[cell.key()] = columns_.size();
|
||||
columns_.push_back(cell.key());
|
||||
}
|
||||
|
||||
// Does the current row fits in the current column?
|
||||
if ((int)children_row.size() <= columns_index[cell.key()]) {
|
||||
children_row.resize(columns_index[cell.key()] + 1);
|
||||
}
|
||||
|
||||
// Fill in the data
|
||||
auto child = From(cell.value(), /*is_last=*/true, depth_ + 1, expander);
|
||||
children_row[columns_index[cell.key()]] = child;
|
||||
}
|
||||
}
|
||||
// Layout
|
||||
for (auto &rows : children_) {
|
||||
auto row = ftxui::Container::Horizontal({});
|
||||
for (auto &cell : rows) {
|
||||
if (cell)
|
||||
row->Add(cell);
|
||||
}
|
||||
|
||||
components.push_back(row);
|
||||
}
|
||||
|
||||
Add(ftxui::Container::Vertical(std::move(components)));
|
||||
}
|
||||
|
||||
bool OnEvent(ftxui::Event event) override final { return false; }
|
||||
|
||||
private:
|
||||
ftxui::Element OnRender() override {
|
||||
std::vector<std::vector<ftxui::Element>> data;
|
||||
data.push_back({ftxui::text("") | ftxui::color(ftxui::Color::GrayDark)});
|
||||
|
||||
for (auto &title : columns_) {
|
||||
data.back().push_back(ftxui::text(title));
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
|
||||
for (auto &row_children : children_) {
|
||||
std::vector<ftxui::Element> data_row;
|
||||
data_row.push_back(ftxui::text(std::to_string(i++)) | ftxui::color(ftxui::Color::GrayDark));
|
||||
|
||||
for (auto &child : row_children) {
|
||||
if (child) {
|
||||
|
||||
data_row.push_back(child->Render());
|
||||
} else {
|
||||
|
||||
data_row.push_back(ftxui::text(""));
|
||||
}
|
||||
}
|
||||
|
||||
data.push_back(std::move(data_row));
|
||||
}
|
||||
|
||||
auto table = ftxui::Table(std::move(data));
|
||||
table.SelectColumns(1, -1).SeparatorVertical(ftxui::LIGHT);
|
||||
table.SelectColumns(1, -1).Border(ftxui::LIGHT);
|
||||
table.SelectRectangle(1, -1, 0, 0).SeparatorVertical(ftxui::HEAVY);
|
||||
table.SelectRectangle(1, -1, 0, 0).Border(ftxui::HEAVY);
|
||||
|
||||
return ftxui::vbox({
|
||||
ftxui::hbox({
|
||||
prefix_->Render(),
|
||||
expand_button_->Render(),
|
||||
}),
|
||||
|
||||
table.Render(),
|
||||
});
|
||||
}
|
||||
|
||||
std::vector<std::string> columns_;
|
||||
std::vector<std::vector<ftxui::Component>> children_;
|
||||
|
||||
ftxui::Component prefix_;
|
||||
ftxui::Component expand_button_;
|
||||
const nlohmann::json &json_;
|
||||
bool is_last_;
|
||||
int depth_;
|
||||
};
|
||||
|
||||
return ftxui::Make<Impl>(prefix, json, is_last, depth, expander);
|
||||
}
|
||||
|
||||
// --- Live JSON viewer: reads leaf values from shared_ptr on each render ---
|
||||
|
||||
static ftxui::Component LiveLeaf(std::shared_ptr<nlohmann::json> root, nlohmann::json::json_pointer ptr, bool is_last) {
|
||||
return ftxui::Renderer([root, ptr = std::move(ptr), is_last](bool focused) -> ftxui::Element {
|
||||
const auto &val = root->at(ptr);
|
||||
std::string text_str;
|
||||
ftxui::Color c;
|
||||
|
||||
if (val.is_string()) {
|
||||
text_str = "\"" + val.get<std::string>() + "\"";
|
||||
c = ftxui::Color::GreenLight;
|
||||
} else if (val.is_number()) {
|
||||
text_str = val.is_number_float() ? fmt::format("{:.6g}", val.get<double>()) : val.dump();
|
||||
c = ftxui::Color::CyanLight;
|
||||
} else if (val.is_boolean()) {
|
||||
text_str = val.get<bool>() ? "true" : "false";
|
||||
c = ftxui::Color::YellowLight;
|
||||
} else if (val.is_null()) {
|
||||
text_str = "null";
|
||||
c = ftxui::Color::RedLight;
|
||||
} else {
|
||||
text_str = val.dump();
|
||||
c = ftxui::Color::White;
|
||||
}
|
||||
|
||||
auto element = ftxui::paragraph(text_str) | color(c);
|
||||
if (focused)
|
||||
element = element | ftxui::inverted | ftxui::focus;
|
||||
if (!is_last)
|
||||
element = ftxui::hbox({element, ftxui::text(",")});
|
||||
return element;
|
||||
});
|
||||
}
|
||||
|
||||
static ftxui::Component FromLiveKeyValue(std::shared_ptr<nlohmann::json> root, nlohmann::json::json_pointer ptr,
|
||||
const std::string &key, bool is_last, int depth, const Expander &expander);
|
||||
|
||||
static ftxui::Component FromLiveObject(std::shared_ptr<nlohmann::json> root, nlohmann::json::json_pointer ptr,
|
||||
const ftxui::Component &prefix, bool is_last, int depth, const Expander &expander) {
|
||||
class Impl : public ComponentExpandable {
|
||||
public:
|
||||
Impl(std::shared_ptr<nlohmann::json> root, nlohmann::json::json_pointer ptr,
|
||||
const ftxui::Component &prefix, bool is_last, int depth, const Expander &expander) : ComponentExpandable(expander) {
|
||||
Expanded() = (depth <= 1);
|
||||
|
||||
const auto &json = root->at(ptr);
|
||||
auto children = ftxui::Container::Vertical({});
|
||||
int size = static_cast<int>(json.size());
|
||||
|
||||
for (auto &it : json.items()) {
|
||||
bool is_children_last = --size == 0;
|
||||
children->Add(Indentation(FromLiveKeyValue(root, ptr / it.key(), it.key(), is_children_last, depth + 1, expander_)));
|
||||
}
|
||||
|
||||
children->Add(ftxui::Renderer([is_last] { return ftxui::text(is_last ? "}" : "},"); }));
|
||||
|
||||
auto toggle = MyToggle("{", is_last ? "{...}" : "{...},", &Expanded());
|
||||
Add(ftxui::Container::Vertical({
|
||||
FakeHorizontal(prefix, toggle),
|
||||
Maybe(children, &Expanded()),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
return ftxui::Make<Impl>(root, ptr, prefix, is_last, depth, expander);
|
||||
}
|
||||
|
||||
static ftxui::Component FromLiveArray(std::shared_ptr<nlohmann::json> root, nlohmann::json::json_pointer ptr,
|
||||
const ftxui::Component &prefix, bool is_last, int depth, const Expander &expander) {
|
||||
class Impl : public ComponentExpandable {
|
||||
public:
|
||||
Impl(std::shared_ptr<nlohmann::json> root, nlohmann::json::json_pointer ptr,
|
||||
const ftxui::Component &prefix, bool is_last, int depth, const Expander &expander) : ComponentExpandable(expander) {
|
||||
Expanded() = (depth <= 0);
|
||||
|
||||
const auto &json = root->at(ptr);
|
||||
auto children = ftxui::Container::Vertical({});
|
||||
int size = static_cast<int>(json.size());
|
||||
|
||||
for (int i = 0; i < static_cast<int>(json.size()); ++i) {
|
||||
bool is_children_last = --size == 0;
|
||||
children->Add(Indentation(FromLive(root, ptr / i, is_children_last, depth + 1, expander_)));
|
||||
}
|
||||
|
||||
children->Add(ftxui::Renderer([is_last] { return ftxui::text(is_last ? "]" : "],"); }));
|
||||
|
||||
auto toggle = MyToggle("[", is_last ? "[...]" : "[...],", &Expanded());
|
||||
Add(ftxui::Container::Vertical({
|
||||
FakeHorizontal(prefix, toggle),
|
||||
Maybe(children, &Expanded()),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
return ftxui::Make<Impl>(root, ptr, prefix, is_last, depth, expander);
|
||||
}
|
||||
|
||||
static ftxui::Component FromLiveKeyValue(std::shared_ptr<nlohmann::json> root, nlohmann::json::json_pointer ptr,
|
||||
const std::string &key, bool is_last, int depth, const Expander &expander) {
|
||||
const auto &value = root->at(ptr);
|
||||
std::string str = "\"" + key + "\"";
|
||||
|
||||
if (value.is_object()) {
|
||||
auto prefix = ftxui::Renderer([str] {
|
||||
return ftxui::hbox({ftxui::text(str) | color(ftxui::Color::BlueLight), ftxui::text(": ")});
|
||||
});
|
||||
return FromLiveObject(root, ptr, prefix, is_last, depth, expander);
|
||||
}
|
||||
|
||||
if (value.is_array()) {
|
||||
auto prefix = ftxui::Renderer([str] {
|
||||
return ftxui::hbox({ftxui::text(str) | color(ftxui::Color::BlueLight), ftxui::text(": ")});
|
||||
});
|
||||
return FromLiveArray(root, ptr, prefix, is_last, depth, expander);
|
||||
}
|
||||
|
||||
auto child = LiveLeaf(root, ptr, is_last);
|
||||
return ftxui::Renderer(child, [str, child] {
|
||||
return ftxui::hbox({
|
||||
ftxui::text(str) | color(ftxui::Color::BlueLight),
|
||||
ftxui::text(": "),
|
||||
child->Render(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ftxui::Component FromLive(std::shared_ptr<nlohmann::json> root, const nlohmann::json::json_pointer &ptr, bool is_last, int depth, const Expander &expander) {
|
||||
const auto &json = root->at(ptr);
|
||||
if (json.is_object())
|
||||
return FromLiveObject(root, ptr, Empty(), is_last, depth, expander);
|
||||
if (json.is_array())
|
||||
return FromLiveArray(root, ptr, Empty(), is_last, depth, expander);
|
||||
return LiveLeaf(root, ptr, is_last);
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
#pragma once
|
||||
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "button.hpp"
|
||||
#include "expander.hpp"
|
||||
#include "ftxui/component/component.hpp" // for Renderer, ResizableSplitBottom, ResizableSplitLeft, ResizableSplitRight, ResizableSplitTop
|
||||
#include "ftxui/component/screen_interactive.hpp" // for ScreenInteractive
|
||||
#include "ftxui/dom/elements.hpp" // for Element, operator|, text, center, border
|
||||
#include "mytoggle.hpp"
|
||||
|
||||
bool ParseJSON(const std::string &input, nlohmann::json &out);
|
||||
|
||||
ftxui::Component From(const nlohmann::json &json, bool is_last, int depth, const Expander &expander);
|
||||
ftxui::Component FromLive(std::shared_ptr<nlohmann::json> root, const nlohmann::json::json_pointer &ptr, bool is_last, int depth, const Expander &expander);
|
||||
ftxui::Component FromString(const nlohmann::json &json, bool is_last);
|
||||
ftxui::Component FromNumber(const nlohmann::json &json, bool is_last);
|
||||
ftxui::Component FromBoolean(const nlohmann::json &json, bool is_last);
|
||||
ftxui::Component FromNull(bool is_last);
|
||||
ftxui::Component FromObject(const ftxui::Component &prefix, const nlohmann::json &json, bool is_last, int depth, const Expander &expander);
|
||||
ftxui::Component FromArrayAny(const ftxui::Component &prefix, const nlohmann::json &json, bool is_last, int depth, const Expander &expander);
|
||||
ftxui::Component FromArray(const ftxui::Component &prefix, const nlohmann::json &json, bool is_last, int depth, const Expander &expander);
|
||||
ftxui::Component FromTable(const ftxui::Component &prefix, const nlohmann::json &json, bool is_last, int depth, const Expander &expander);
|
||||
ftxui::Component FromKeyValue(const std::string &key, const nlohmann::json &value, bool is_last, int depth, const Expander &expander);
|
||||
ftxui::Component Empty();
|
||||
ftxui::Component Unimplemented();
|
||||
ftxui::Component Basic(const std::string &value, ftxui::Color c, bool is_last);
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
#include "mytoggle.hpp"
|
||||
#include "ftxui/component/event.hpp"
|
||||
|
||||
namespace {
|
||||
class MyToggleImpl : public ftxui::ComponentBase {
|
||||
public:
|
||||
MyToggleImpl(const char *label_on, const char *label_off, bool *state) : label_on_(label_on), label_off_(label_off), state_(state) {}
|
||||
|
||||
private:
|
||||
// Component implementation.
|
||||
ftxui::Element OnRender() override {
|
||||
auto style = hovered_ ? ftxui::bold : ftxui::nothing;
|
||||
ftxui::Element my_text = *state_ ? ftxui::text(label_on_) : ftxui::text(label_off_);
|
||||
return my_text | style | reflect(box_);
|
||||
}
|
||||
|
||||
bool OnEvent(ftxui::Event event) override {
|
||||
if (!event.is_mouse())
|
||||
return false;
|
||||
|
||||
return OnMouseEvent(event);
|
||||
}
|
||||
|
||||
bool OnMouseEvent(ftxui::Event event) {
|
||||
bool was_hovered = hovered_;
|
||||
hovered_ = box_.Contain(event.mouse().x, event.mouse().y);
|
||||
|
||||
if (hovered_ && event.mouse().button == ftxui::Mouse::Left && event.mouse().motion == ftxui::Mouse::Pressed) {
|
||||
*state_ = !*state_;
|
||||
return true;
|
||||
}
|
||||
|
||||
return hovered_ || was_hovered;
|
||||
}
|
||||
|
||||
bool Focusable() const final { return false; }
|
||||
|
||||
const char *label_on_;
|
||||
const char *label_off_;
|
||||
bool *const state_;
|
||||
bool hovered_ = false;
|
||||
ftxui::Box box_;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
ftxui::Component MyToggle(const char *label_on, const char *label_off, bool *state) { return ftxui::Make<MyToggleImpl>(label_on, label_off, state); }
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
#pragma once
|
||||
#include "ftxui/component/component.hpp"
|
||||
|
||||
ftxui::Component MyToggle(const char *label_on, const char *label_off, bool *state);
|
||||
|
|
@ -0,0 +1,446 @@
|
|||
#include <atomic>
|
||||
#include <boost/signals2.hpp>
|
||||
#include <charconv>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <future>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <sstream>
|
||||
#include <stop_token>
|
||||
#include <string>
|
||||
#include <sys/epoll.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define FMT_HEADER_ONLY
|
||||
#include <fmt/format.h>
|
||||
#include <fmt/ranges.h>
|
||||
|
||||
// for XLSX files
|
||||
#include "xlnt/xlnt.hpp"
|
||||
|
||||
// For sqlite
|
||||
#include "sqlite_modern_cpp.h"
|
||||
|
||||
// For parsers
|
||||
#include <boost/spirit/include/phoenix.hpp>
|
||||
#include <boost/spirit/include/qi.hpp>
|
||||
|
||||
#include "clipp.hpp"
|
||||
#include "ftxui/component/component.hpp"
|
||||
#include "ftxui/component/screen_interactive.hpp"
|
||||
#include "ftxui/dom/elements.hpp"
|
||||
#include "headless.hpp"
|
||||
#include "process.hpp"
|
||||
#include "recorder.hpp"
|
||||
#include "signals.hpp"
|
||||
|
||||
std::mutex g_j1939_db_mtx;
|
||||
|
||||
int32_t main(int32_t argc, char *argv[]) {
|
||||
static auto screen = ftxui::ScreenInteractive::Fullscreen();
|
||||
static std::mutex rw_mtx;
|
||||
static std::map<std::string, std::map<std::string, std::shared_ptr<can_frame_data_s>>> aggregated;
|
||||
static signals_s signals;
|
||||
static std::atomic<sqlite::database *> j1939_db{nullptr};
|
||||
static std::stop_source aggregator_task_stop, refresh_task_stop, headless_task_stop;
|
||||
static TinyProcessLib::Process *p = nullptr;
|
||||
std::future<void> xlsx_parser_task, headless_task;
|
||||
extern std::unique_ptr<sqlite::database> parseXlsx(const std::string &file);
|
||||
static std::unique_ptr<sqlite::database> j1939_db_owner;
|
||||
static std::unique_ptr<Recorder> recorder;
|
||||
static std::unique_ptr<HeadlessHandler> headless_handler;
|
||||
|
||||
static struct {
|
||||
std::string docfile, command = "", output_file = "", record_db_path = "";
|
||||
bool show_help = false, headless_mode = false, sync_to_server = false, record_mode = false, tui_mode = false;
|
||||
} cli_opts;
|
||||
|
||||
// Parse cli options
|
||||
{
|
||||
static const auto print_usage = []<typename Man>(const Man &man) {
|
||||
fmt::print("{}\r\n", (std::stringstream{} << man).str());
|
||||
};
|
||||
|
||||
auto cli = (
|
||||
|
||||
clipp::option("-hl", "--headless")
|
||||
.doc("Headless mode, write configuration to stdout if output file is not specified")
|
||||
.set(cli_opts.headless_mode)
|
||||
.call([]() { fmt::println("Headless mode"); }),
|
||||
|
||||
clipp::option("-of", "--output-file") &
|
||||
clipp::value("Output file to write configuration", cli_opts.output_file)
|
||||
.call([&]() { fmt::println("Output file is: {}", cli_opts.output_file); })
|
||||
.doc("Specify output file to write configuration"),
|
||||
|
||||
clipp::option("-rec", "--record")
|
||||
.doc("Record mode: collect decoded J1939 SPN values to SQLite DB")
|
||||
.set(cli_opts.record_mode)
|
||||
.call([]() { fmt::println("Record mode"); }),
|
||||
|
||||
clipp::option("-db", "--database") &
|
||||
clipp::value("SQLite output database path", cli_opts.record_db_path)
|
||||
.call([&]() { fmt::println("Record DB: {}", cli_opts.record_db_path); })
|
||||
.doc("Path for the output SQLite database (used with -rec)"),
|
||||
|
||||
clipp::option("-tui").doc("Enable TUI mode (use with -rec to show UI while recording)").set(cli_opts.tui_mode),
|
||||
|
||||
clipp::option("-e", "--execute-command") &
|
||||
clipp::value("command", cli_opts.command).call([&]() {}).doc("execute cli command to read its output"),
|
||||
|
||||
clipp::required("-j1939", "--j1939-document") &
|
||||
clipp::value("J1939 Document file", cli_opts.docfile)
|
||||
.call([&]() {
|
||||
xlsx_parser_task = std::async(std::launch::async, [&]() {
|
||||
j1939_db_owner = parseXlsx(cli_opts.docfile);
|
||||
j1939_db.store(j1939_db_owner.get());
|
||||
signals.map.get<void(sqlite::database &)>("j1939_database_ready")->operator()(*j1939_db_owner);
|
||||
});
|
||||
})
|
||||
.doc("provide .xlsx document file for J1939 processing"));
|
||||
|
||||
auto man = clipp::make_man_page(cli, argv[0]);
|
||||
auto cli_with_help = (cli | clipp::option("-h", "--help").set(cli_opts.show_help).doc("show this help").call([&]() {
|
||||
print_usage(man);
|
||||
}));
|
||||
|
||||
if (!clipp::parse(argc, argv, cli_with_help)) {
|
||||
print_usage(man);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (cli_opts.record_mode && cli_opts.record_db_path.empty()) {
|
||||
fmt::println(stderr, "Error: -rec requires -db <path>");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse a single candump line and aggregate it
|
||||
static const auto parse_candump_line = [](const std::string &line) {
|
||||
if (line.empty())
|
||||
return;
|
||||
|
||||
std::vector<std::string> words;
|
||||
std::istringstream iss(line);
|
||||
std::string s;
|
||||
|
||||
while (std::getline(iss, s, ' ')) {
|
||||
if (!s.empty()) {
|
||||
words.push_back(s);
|
||||
}
|
||||
}
|
||||
|
||||
if (words.size() >= 4u) { // 0 - interface, 1 - canid, 2 - size, 3 - payload (1 byte minimum)
|
||||
if (words[0].starts_with("can") || words[0].starts_with("vcan")) {
|
||||
can_frame_data_s entry;
|
||||
|
||||
// Parse DLC
|
||||
{
|
||||
auto sv = std::string_view(words[2]).substr(1, words[2].size() - 2);
|
||||
auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), entry.size);
|
||||
if (ec != std::errc{}) {
|
||||
entry.size = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse payload bytes directly
|
||||
entry.payload.reserve(words.size() - 3);
|
||||
for (size_t i = 3; i < words.size(); ++i) {
|
||||
uint8_t byte = 0;
|
||||
std::from_chars(words[i].data(), words[i].data() + words[i].size(), byte, 16);
|
||||
entry.payload.push_back(byte);
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(rw_mtx);
|
||||
aggregated[words[0]][words[1]] = std::make_shared<can_frame_data_s>(std::move(entry));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// If reading from stdin pipe, save the pipe fd and reopen stdin as /dev/tty for FTXUI
|
||||
static int candump_fd = -1;
|
||||
if (cli_opts.command.empty()) {
|
||||
candump_fd = ::dup(STDIN_FILENO);
|
||||
std::freopen("/dev/tty", "r", stdin);
|
||||
}
|
||||
|
||||
// Reads candump data from stdin pipe or subprocess and aggregates it
|
||||
auto aggregator_task = std::async(
|
||||
std::launch::async,
|
||||
[command = cli_opts.command](std::stop_token stop_token) {
|
||||
fmt::println(stderr, "[task] aggregator started");
|
||||
if (command.empty()) {
|
||||
|
||||
// Read from the saved pipe fd using epoll to avoid blocking on stop
|
||||
int epfd = ::epoll_create1(0);
|
||||
if (epfd < 0)
|
||||
return;
|
||||
|
||||
struct epoll_event ev = {.events = EPOLLIN, .data = {.fd = candump_fd}};
|
||||
::epoll_ctl(epfd, EPOLL_CTL_ADD, candump_fd, &ev);
|
||||
|
||||
FILE *pipe_stream = ::fdopen(candump_fd, "r");
|
||||
if (!pipe_stream) {
|
||||
::close(epfd);
|
||||
return;
|
||||
}
|
||||
|
||||
struct epoll_event events[1];
|
||||
char buf[4096];
|
||||
|
||||
while (!stop_token.stop_requested()) {
|
||||
int32_t nfds = ::epoll_wait(epfd, events, 1, 50);
|
||||
if (nfds > 0 && !stop_token.stop_requested()) {
|
||||
if (events[0].events & EPOLLIN) {
|
||||
if (!std::fgets(buf, sizeof(buf), pipe_stream)) {
|
||||
break;
|
||||
}
|
||||
|
||||
std::string line(buf);
|
||||
|
||||
if (!line.empty() && line.back() == '\n') {
|
||||
line.pop_back();
|
||||
}
|
||||
|
||||
parse_candump_line(line);
|
||||
}
|
||||
|
||||
if (events[0].events & (EPOLLHUP | EPOLLERR)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::fclose(pipe_stream);
|
||||
::close(epfd);
|
||||
} else {
|
||||
// Launch subprocess
|
||||
TinyProcessLib::Config cfg = {
|
||||
.buffer_size = PIPE_BUF, .inherit_file_descriptors = true, .on_stdout_close = []() {}};
|
||||
p = new TinyProcessLib::Process(
|
||||
command, "",
|
||||
[stop_token](const char *bytes, size_t n) {
|
||||
if (n > PIPE_BUF || stop_token.stop_requested())
|
||||
return;
|
||||
|
||||
std::string buf(bytes, n), line;
|
||||
std::istringstream ss(buf);
|
||||
while (std::getline(ss, line)) {
|
||||
parse_candump_line(line);
|
||||
}
|
||||
},
|
||||
[](const char *, size_t) {}, false, cfg);
|
||||
|
||||
while (true) {
|
||||
if (stop_token.stop_requested()) {
|
||||
if (p) {
|
||||
p->kill();
|
||||
::kill(-p->get_id(), SIGKILL);
|
||||
::kill(p->get_id(), SIGKILL);
|
||||
p->get_exit_status();
|
||||
delete p;
|
||||
p = nullptr;
|
||||
}
|
||||
break;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
}
|
||||
fmt::println(stderr, "[task] aggregator finished");
|
||||
},
|
||||
|
||||
aggregator_task_stop.get_token());
|
||||
|
||||
// UI refresh task: compares snapshots at ~30fps and emits signals for changed entries
|
||||
auto refresh_task = std::async(
|
||||
std::launch::async,
|
||||
[](std::stop_token stop_token) {
|
||||
fmt::println(stderr, "[task] refresh started");
|
||||
using aggregated_t = std::map<std::string, std::map<std::string, std::shared_ptr<can_frame_data_s>>>;
|
||||
aggregated_t old_data;
|
||||
|
||||
while (!stop_token.stop_requested()) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(33u));
|
||||
|
||||
aggregated_t current;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(rw_mtx);
|
||||
current = aggregated;
|
||||
}
|
||||
|
||||
std::vector<can_frame_update_s> batch;
|
||||
|
||||
for (const auto &[iface, canids] : current) {
|
||||
bool new_iface = !old_data.contains(iface);
|
||||
|
||||
for (const auto &[canid, ptr] : canids) {
|
||||
if (!ptr)
|
||||
continue;
|
||||
|
||||
can_frame_diff_s diff;
|
||||
diff.is_new_interface = new_iface;
|
||||
diff.is_new_canid = new_iface || !old_data[iface].contains(canid);
|
||||
|
||||
if (!diff.is_new_canid) {
|
||||
const auto &old_ptr = old_data[iface][canid];
|
||||
if (old_ptr == ptr)
|
||||
continue;
|
||||
|
||||
const auto &old_entry = *old_ptr;
|
||||
diff.payload_changed.resize(ptr->payload.size(), false);
|
||||
|
||||
for (size_t i = 0; i < ptr->payload.size(); ++i) {
|
||||
diff.payload_changed[i] = (i >= old_entry.payload.size() || ptr->payload[i] != old_entry.payload[i]);
|
||||
}
|
||||
} else {
|
||||
diff.payload_changed.assign(ptr->payload.size(), true);
|
||||
}
|
||||
|
||||
std::shared_ptr<nlohmann::json> verbose, brief;
|
||||
auto *db = j1939_db.load();
|
||||
if (db) {
|
||||
std::lock_guard<std::mutex> db_lock(g_j1939_db_mtx);
|
||||
extern std::pair<nlohmann::json, nlohmann::json> processFrame(
|
||||
sqlite::database & db, const std::string &iface, const std::string &canid,
|
||||
const std::vector<uint8_t> &data);
|
||||
auto [v, b] = processFrame(*db, iface, canid, ptr->payload);
|
||||
|
||||
verbose = std::make_shared<nlohmann::json>(std::move(v));
|
||||
brief = std::make_shared<nlohmann::json>(std::move(b));
|
||||
}
|
||||
|
||||
batch.push_back({iface, canid, *ptr, std::move(diff), std::move(verbose), std::move(brief)});
|
||||
}
|
||||
}
|
||||
|
||||
if (!batch.empty()) {
|
||||
signals.map.get<void(const std::vector<can_frame_update_s> &)>("new_entries_batch")->operator()(batch);
|
||||
}
|
||||
|
||||
old_data.swap(current);
|
||||
}
|
||||
|
||||
fmt::println(stderr, "[task] refresh finished");
|
||||
},
|
||||
|
||||
refresh_task_stop.get_token());
|
||||
|
||||
// Stop all tasks on SIGINT
|
||||
{
|
||||
static auto signal_handler = [](int sig) {
|
||||
fmt::println(stderr, "[signal] SIGINT received, stopping tasks...");
|
||||
for (auto *source : {&aggregator_task_stop, &refresh_task_stop, &headless_task_stop}) {
|
||||
if (!source->stop_requested()) {
|
||||
source->request_stop();
|
||||
}
|
||||
}
|
||||
|
||||
if (candump_fd >= 0) {
|
||||
::close(candump_fd);
|
||||
candump_fd = -1;
|
||||
}
|
||||
};
|
||||
|
||||
::signal(SIGINT, signal_handler);
|
||||
}
|
||||
|
||||
if (cli_opts.record_mode) {
|
||||
bool rec_console = !cli_opts.tui_mode && !cli_opts.headless_mode;
|
||||
recorder = std::make_unique<Recorder>(cli_opts.record_db_path, rec_console);
|
||||
signals.map.get<void(const std::vector<can_frame_update_s> &)>("new_entries_batch")
|
||||
->connect([](const std::vector<can_frame_update_s> &batch) { recorder->onBatch(batch); });
|
||||
}
|
||||
|
||||
if (cli_opts.headless_mode) {
|
||||
headless_handler = std::make_unique<HeadlessHandler>(cli_opts.output_file);
|
||||
|
||||
signals.map.get<void(sqlite::database &)>("j1939_database_ready")->connect([](sqlite::database &db) {
|
||||
headless_handler->onDatabaseReady(db);
|
||||
});
|
||||
|
||||
signals.map.get<void(const std::vector<can_frame_update_s> &)>("new_entries_batch")
|
||||
->connect([](const std::vector<can_frame_update_s> &batch) { headless_handler->onBatch(batch); });
|
||||
}
|
||||
|
||||
// Determine if TUI should run: default on, off if headless, off if rec-only (no -tui)
|
||||
bool run_tui = !cli_opts.headless_mode && (!cli_opts.record_mode || cli_opts.tui_mode);
|
||||
|
||||
if (run_tui) {
|
||||
extern ftxui::Component makeMainForm(ftxui::ScreenInteractive * screen, signals_map_t & smap);
|
||||
screen.Loop(makeMainForm(&screen, signals.map) | ftxui::Renderer([](ftxui::Element inner) -> ftxui::Element {
|
||||
return ftxui::Window(
|
||||
{
|
||||
.inner = ftxui::Renderer([inner]() -> ftxui::Element { return inner | ftxui::flex; }),
|
||||
.title = "canscope",
|
||||
.width = ftxui::Terminal::Size().dimx,
|
||||
.height = ftxui::Terminal::Size().dimy,
|
||||
.resize_left = false,
|
||||
.resize_right = false,
|
||||
.resize_top = false,
|
||||
.resize_down = false,
|
||||
.render = [&](ftxui::WindowRenderState state) -> ftxui::Element {
|
||||
return ftxui::window(ftxui::Renderer([state]() {
|
||||
return ftxui::text(fmt::format(" {{ {} }} ", state.title));
|
||||
})->Render(),
|
||||
state.inner);
|
||||
},
|
||||
})
|
||||
->Render();
|
||||
}));
|
||||
|
||||
signals.map.get<void()>("canplayer_stopped")->operator()();
|
||||
fmt::println(stderr, "[exit] TUI exited, stopping tasks...");
|
||||
for (auto *source : {&aggregator_task_stop, &refresh_task_stop, &headless_task_stop}) {
|
||||
if (!source->stop_requested()) {
|
||||
source->request_stop();
|
||||
}
|
||||
}
|
||||
if (candump_fd >= 0) {
|
||||
::close(candump_fd);
|
||||
candump_fd = -1;
|
||||
}
|
||||
} else {
|
||||
|
||||
// Headless or rec-only: wait for SIGINT
|
||||
headless_task = std::async(
|
||||
std::launch::async,
|
||||
[](std::stop_token st) {
|
||||
while (!st.stop_requested()) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
},
|
||||
|
||||
headless_task_stop.get_token());
|
||||
}
|
||||
|
||||
// Wait for all tasks to finish (with timeout)
|
||||
{
|
||||
const char *names[] = {"xlsx_parser", "aggregator", "refresh", "headless"};
|
||||
int idx = 0;
|
||||
for (auto *task : {&xlsx_parser_task, &aggregator_task, &refresh_task, &headless_task}) {
|
||||
if (task && task->valid()) {
|
||||
fmt::println(stderr, "[exit] waiting for {}...", names[idx]);
|
||||
if (task->wait_for(std::chrono::seconds(3)) == std::future_status::timeout) {
|
||||
fmt::println(stderr, "[exit] {} timed out!", names[idx]);
|
||||
} else {
|
||||
fmt::println(stderr, "[exit] {} done", names[idx]);
|
||||
}
|
||||
}
|
||||
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
|
||||
if (recorder) {
|
||||
fmt::println("Flushing recorded data to database, please wait...");
|
||||
recorder->flushAndClose();
|
||||
fmt::println("Done. Database saved to: {}", cli_opts.record_db_path);
|
||||
recorder.reset();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,418 @@
|
|||
#include "canid_unit.hpp"
|
||||
#include "signals.hpp"
|
||||
#include "tagsettingrow.hpp"
|
||||
#include "tagsettings.hpp"
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/component_base.hpp>
|
||||
#include <ftxui/dom/elements.hpp>
|
||||
#include <ftxui/screen/terminal.hpp>
|
||||
#include <map>
|
||||
#include <ranges>
|
||||
#include <spdlog/sinks/systemd_sink.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
// For sqlite
|
||||
#include "process.hpp"
|
||||
#include "signals.hpp"
|
||||
#include "sqlite_modern_cpp.h"
|
||||
|
||||
ftxui::Component makeMainForm(ftxui::ScreenInteractive *screen, signals_map_t &smap) {
|
||||
class Impl : public ftxui::ComponentBase {
|
||||
public:
|
||||
explicit Impl(ftxui::ScreenInteractive *screen, signals_map_t &smap) {
|
||||
static auto logger = spdlog::systemd_logger_mt("mainform", "cansniffer");
|
||||
|
||||
static bool canbus_params_export_dialog_shown = false, file_dialog_shown = false,
|
||||
canbus_player_dialog_shown = false, canplayer_is_ready = false;
|
||||
;
|
||||
static float focus_relative = 0.15f;
|
||||
static float canbus_params_focus_relative = 0;
|
||||
static std::string canid_active;
|
||||
static size_t tags_count = 0u;
|
||||
static std::unordered_map<std::string, std::shared_ptr<CanIDUnit>> canid_lookup;
|
||||
static std::atomic<sqlite::database *> database_atomic{nullptr};
|
||||
|
||||
extern ftxui::Component makeCanIDUnit(
|
||||
const std::string &, const std::string &, const std::string &, size_t &, const std::vector<uint8_t> &,
|
||||
ftxui::ScreenInteractive *, signals_map_t &, ftxui::Component, ftxui::Component, ftxui::Component,
|
||||
ftxui::Component, bool, bool, bool, bool, std::string &, bool &, bool &, bool &,
|
||||
std::map<std::string, std::map<int32_t, ftxui::Component>> &, spn_settings_map_t &);
|
||||
extern ftxui::Component makeFileDialog(ftxui::ScreenInteractive * scr, signals_map_t & smap, bool &shown);
|
||||
extern ftxui::Component makeCanPlayerDialog(ftxui::ScreenInteractive * scr, signals_map_t & smap, bool &is_ready);
|
||||
|
||||
static auto canidsCont = ftxui::Container::Vertical({});
|
||||
static spn_settings_map_t tagSettingsMap;
|
||||
static std::map<std::string, std::map<int32_t, ftxui::Component>> spnSettingsFormMap;
|
||||
static auto spn_export_dialog = ftxui::Container::Vertical({});
|
||||
static auto canbus_params_export_dialog = ftxui::Container::Vertical({});
|
||||
static auto canbus_player_dialog = ftxui::Container::Vertical({});
|
||||
|
||||
static const auto convertParametersMapToJson = []() {
|
||||
nlohmann::json ret;
|
||||
|
||||
if (canidsCont->ChildCount()) {
|
||||
auto &map = std::static_pointer_cast<CanIDUnit>(canidsCont->ChildAt(0))->getParametersExportMap();
|
||||
|
||||
for (const auto &[canid_k, canid_v] : map) {
|
||||
nlohmann::json::array_t spns_selected;
|
||||
for (const auto &[selected_spn_k, selected_spn_v] : std::get<2u>(canid_v)) {
|
||||
if (std::get<1u>(selected_spn_v)) {
|
||||
const auto &spn_data = std::get<2u>(selected_spn_v);
|
||||
nlohmann::json spn = {
|
||||
{"name", spn_data.value("SPN name", "")},
|
||||
{"offset", spn_data.value("Offset", 0)},
|
||||
{"resolution", spn_data.value("Resolution", 0.0)},
|
||||
{"value", spn_data.value("Value", 0.0)},
|
||||
{"unit", spn_data.value("Unit", "")},
|
||||
};
|
||||
if (spn_data.contains("Fragments")) {
|
||||
nlohmann::json::array_t frags;
|
||||
for (const auto &[k, frag] : spn_data["Fragments"].items()) {
|
||||
auto frag_key = fmt::format("Fragment#{}", k);
|
||||
if (frag.contains(frag_key)) {
|
||||
frags.push_back({{fmt::format("fragment#{}", k),
|
||||
{
|
||||
{"byte_pos", frag[frag_key].value("byte_offset", 0)},
|
||||
{"bit_pos", frag[frag_key].value("bit_offset", 0)},
|
||||
{"bit_size", frag[frag_key].value("size_bits", 0)},
|
||||
}}});
|
||||
}
|
||||
}
|
||||
spn["fragments"] = std::move(frags);
|
||||
}
|
||||
spns_selected.push_back(std::move(spn));
|
||||
}
|
||||
}
|
||||
|
||||
if (!spns_selected.empty()) {
|
||||
ret[canid_k] = spns_selected;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
// Connections
|
||||
{
|
||||
static struct export_file_request_connection_s {
|
||||
export_file_request_connection_s(signals_map_t &smap) {
|
||||
smap.get<void(const std::string &)>("export_file_request")->connect([](const std::string &path) {
|
||||
std::ofstream ofs(path);
|
||||
ofs << convertParametersMapToJson().dump();
|
||||
ofs.close();
|
||||
});
|
||||
}
|
||||
} export_file_request_connection(smap);
|
||||
|
||||
static struct database_ready_connection_s {
|
||||
database_ready_connection_s(signals_map_t &smap) {
|
||||
smap.get<void(sqlite::database &)>("j1939_database_ready")->connect([](sqlite::database &db) {
|
||||
database_atomic.store(&db);
|
||||
});
|
||||
}
|
||||
} database_ready_connection(smap);
|
||||
|
||||
static struct new_entries_batch_connection_s {
|
||||
new_entries_batch_connection_s(signals_map_t &smap, ftxui::ScreenInteractive *screen) {
|
||||
smap.get<void(const std::vector<can_frame_update_s> &)>("new_entries_batch")
|
||||
->connect([screen, &smap](const std::vector<can_frame_update_s> &batch) {
|
||||
screen->Post([screen, &smap, batch]() {
|
||||
for (const auto &entry : batch) {
|
||||
auto it = canid_lookup.find(entry.canid);
|
||||
if (it != canid_lookup.end()) {
|
||||
it->second->update(entry.data, entry.diff, entry.verbose, entry.brief);
|
||||
} else {
|
||||
auto component = ftxui::Renderer([]() { return ftxui::text(""); });
|
||||
auto new_cmp = makeCanIDUnit(
|
||||
entry.iface, entry.canid, "J1939", tags_count, entry.data.payload, screen, smap, component,
|
||||
canidsCont, ftxui::Container::Vertical({}), canbus_params_export_dialog, false, false, true,
|
||||
false, canid_active, file_dialog_shown, canbus_params_export_dialog_shown,
|
||||
file_dialog_shown, spnSettingsFormMap, tagSettingsMap);
|
||||
auto unit = std::static_pointer_cast<CanIDUnit>(new_cmp);
|
||||
unit->update(entry.data, entry.diff, entry.verbose, entry.brief);
|
||||
canid_lookup[entry.canid] = unit;
|
||||
canidsCont->Add(new_cmp);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
screen->Post(ftxui::Event::Custom);
|
||||
});
|
||||
}
|
||||
} new_entries_batch_connection(smap, screen);
|
||||
}
|
||||
|
||||
Add({
|
||||
ftxui::Container::Vertical({
|
||||
|
||||
// Tab bar
|
||||
ftxui::Container::Horizontal({
|
||||
ftxui::Renderer([]() {
|
||||
return ftxui::hbox({
|
||||
ftxui::text(" J1939 database is: "),
|
||||
ftxui::text(fmt::format("{} ", database_atomic.load() ? "ready" : "not ready")) |
|
||||
ftxui::color(database_atomic.load() ? ftxui::Color::Green : ftxui::Color::Red),
|
||||
});
|
||||
}),
|
||||
|
||||
ftxui::Renderer([]() {
|
||||
static auto start = std::chrono::steady_clock::now();
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
std::chrono::duration<double> elapsed = now - start;
|
||||
|
||||
// Convert elapsed time to hours, minutes, and seconds
|
||||
int32_t hours = static_cast<int32_t>(elapsed.count()) / 3600u;
|
||||
int32_t minutes = (static_cast<int32_t>(elapsed.count()) % 3600u) / 60u;
|
||||
int32_t seconds = static_cast<int32_t>(elapsed.count()) % 60u;
|
||||
|
||||
return ftxui::hbox({ftxui::text(
|
||||
fmt::format(" Uptime: {} ", fmt::format("{:02}:{:02}:{:02}", hours, minutes, seconds)))});
|
||||
}),
|
||||
|
||||
ftxui::Renderer([]() {
|
||||
return ftxui::hbox({
|
||||
ftxui::separator(),
|
||||
ftxui::filler(),
|
||||
ftxui::separator(),
|
||||
}) |
|
||||
ftxui::xflex;
|
||||
}),
|
||||
|
||||
ftxui::Checkbox({
|
||||
.transform = [this](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
return ftxui::text(" >[can_player]< ") |
|
||||
(canbus_player_dialog_shown || state.focused ? ftxui::bold : ftxui::nothing) |
|
||||
(canplayer_is_ready ? ftxui::color(ftxui::Color::Cyan)
|
||||
: ftxui::color(ftxui::Color::Grey23)) |
|
||||
(state.focused ? ftxui::bgcolor(ftxui::Color::Grey11) : ftxui::nothing);
|
||||
},
|
||||
|
||||
.on_change = [screen, &smap]() { canbus_player_dialog_shown = canplayer_is_ready; },
|
||||
}),
|
||||
|
||||
ftxui::Checkbox({
|
||||
.checked = &canbus_params_export_dialog_shown,
|
||||
|
||||
.transform = [this](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
return ftxui::text(" >[export_parameters]< ") |
|
||||
(canbus_params_export_dialog_shown || state.focused ? ftxui::bold : ftxui::nothing) |
|
||||
ftxui::color(ftxui::Color::Cyan) |
|
||||
(state.focused ? ftxui::bgcolor(ftxui::Color::Grey11) : ftxui::nothing);
|
||||
},
|
||||
|
||||
.on_change = [screen, &smap]() { canbus_params_export_dialog_shown = true; },
|
||||
}),
|
||||
|
||||
ftxui::Checkbox({
|
||||
.transform = [this](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
return ftxui::text(" >[exit]< ") | (state.focused ? ftxui::bold : ftxui::nothing) |
|
||||
ftxui::color(ftxui::Color::Cyan) |
|
||||
(state.focused ? ftxui::bgcolor(ftxui::Color::Grey11) : ftxui::nothing);
|
||||
},
|
||||
.on_change = [screen]() { screen->Exit(); },
|
||||
}),
|
||||
}),
|
||||
|
||||
ftxui::Renderer([]() { return ftxui::separator(); }),
|
||||
|
||||
// Body
|
||||
(canidsCont | ftxui::Renderer([](ftxui::Element inner) {
|
||||
return inner | ftxui::focusPositionRelative(0, focus_relative) | ftxui::vscroll_indicator |
|
||||
ftxui::frame | ftxui::flex;
|
||||
})) |
|
||||
ftxui::CatchEvent([](ftxui::Event event) {
|
||||
static constexpr float scroll_step = 0.03f;
|
||||
static const auto increment_focus = []() {
|
||||
focus_relative = std::clamp(focus_relative + scroll_step, 0.0f, 1.0f);
|
||||
};
|
||||
|
||||
static const auto decrement_focus = []() {
|
||||
focus_relative = std::clamp(focus_relative - scroll_step, 0.0f, 1.0f);
|
||||
};
|
||||
|
||||
if (event.is_mouse()) {
|
||||
switch (static_cast<enum ftxui::Mouse::Button>(event.mouse().button)) {
|
||||
case ftxui::Mouse::Button::WheelDown: {
|
||||
increment_focus();
|
||||
goto done;
|
||||
} break;
|
||||
|
||||
case ftxui::Mouse::Button::WheelUp: {
|
||||
decrement_focus();
|
||||
goto done;
|
||||
} break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else if (!event.is_character()) {
|
||||
if (event == ftxui::Event::ArrowDown) {
|
||||
|
||||
increment_focus();
|
||||
goto done;
|
||||
} else if (event == ftxui::Event::ArrowUp) {
|
||||
|
||||
decrement_focus();
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
|
||||
forward:
|
||||
return false;
|
||||
|
||||
done:
|
||||
return true;
|
||||
}) |
|
||||
|
||||
ftxui::Modal(
|
||||
ftxui::Container::Vertical({
|
||||
ftxui::Renderer([]() {
|
||||
return ftxui::text("Export parameters") | ftxui::color(ftxui::Color::Red);
|
||||
}) | ftxui::hcenter,
|
||||
ftxui::Renderer([]() { return ftxui::separator(); }),
|
||||
|
||||
(canbus_params_export_dialog | ftxui::Renderer([](ftxui::Element inner) {
|
||||
return inner | ftxui::focusPositionRelative(0, canbus_params_focus_relative) |
|
||||
ftxui::vscroll_indicator | ftxui::frame | ftxui::flex;
|
||||
})),
|
||||
|
||||
ftxui::Renderer([]() { return ftxui::separatorEmpty(); }),
|
||||
|
||||
ftxui::Container::Horizontal({
|
||||
ftxui::Checkbox({
|
||||
.transform = [this](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
return ftxui::text(" >[export_to_file]< ") |
|
||||
(state.focused ? ftxui::bold : ftxui::nothing) |
|
||||
ftxui::color(ftxui::Color::Cyan) |
|
||||
(state.focused ? ftxui::bgcolor(ftxui::Color::Grey11) : ftxui::nothing);
|
||||
},
|
||||
|
||||
.on_change = []() { file_dialog_shown = true; },
|
||||
}),
|
||||
|
||||
ftxui::Checkbox({
|
||||
.transform = [this](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
return ftxui::text(" >[copy_to_clipboard]< ") |
|
||||
(state.focused ? ftxui::bold : ftxui::nothing) |
|
||||
ftxui::color(ftxui::Color::Cyan) |
|
||||
(state.focused ? ftxui::bgcolor(ftxui::Color::Grey11) : ftxui::nothing);
|
||||
},
|
||||
|
||||
.on_change =
|
||||
[this]() {
|
||||
TinyProcessLib::Process(
|
||||
fmt::format("echo '{}' | xsel -bi", convertParametersMapToJson().dump()))
|
||||
.get_exit_status();
|
||||
},
|
||||
}),
|
||||
|
||||
ftxui::Checkbox({
|
||||
.transform = [this](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
return ftxui::text(" >[close]< ") | (state.focused ? ftxui::bold : ftxui::nothing) |
|
||||
ftxui::color(ftxui::Color::Cyan) |
|
||||
(state.focused ? ftxui::bgcolor(ftxui::Color::Grey11) : ftxui::nothing);
|
||||
},
|
||||
|
||||
.on_change = []() { canbus_params_export_dialog_shown = false; },
|
||||
}),
|
||||
}) | ftxui::hcenter,
|
||||
}) | ftxui::size(ftxui::WIDTH, ftxui::EQUAL, 96) |
|
||||
ftxui::size(ftxui::HEIGHT, ftxui::EQUAL, 48) | ftxui::border |
|
||||
ftxui::CatchEvent([](ftxui::Event event) {
|
||||
static constexpr float scroll_step = 0.03f;
|
||||
static const auto increment_focus = []() {
|
||||
canbus_params_focus_relative =
|
||||
std::clamp(canbus_params_focus_relative + scroll_step, 0.0f, 1.0f);
|
||||
};
|
||||
static const auto decrement_focus = []() {
|
||||
canbus_params_focus_relative =
|
||||
std::clamp(canbus_params_focus_relative - scroll_step, 0.0f, 1.0f);
|
||||
};
|
||||
|
||||
if (event.is_mouse()) {
|
||||
switch (static_cast<enum ftxui::Mouse::Button>(event.mouse().button)) {
|
||||
case ftxui::Mouse::Button::WheelDown: {
|
||||
increment_focus();
|
||||
goto done;
|
||||
} break;
|
||||
|
||||
case ftxui::Mouse::Button::WheelUp: {
|
||||
decrement_focus();
|
||||
goto done;
|
||||
} break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else if (!event.is_character()) {
|
||||
if (event == ftxui::Event::ArrowDown) {
|
||||
|
||||
increment_focus();
|
||||
goto done;
|
||||
} else if (event == ftxui::Event::ArrowUp) {
|
||||
|
||||
decrement_focus();
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
|
||||
forward:
|
||||
return false;
|
||||
|
||||
done:
|
||||
return true;
|
||||
}),
|
||||
&canbus_params_export_dialog_shown) |
|
||||
ftxui::Modal(makeFileDialog(screen, smap, file_dialog_shown), &file_dialog_shown) |
|
||||
|
||||
ftxui::Modal(
|
||||
ftxui::Container::Vertical({
|
||||
ftxui::Renderer([]() {
|
||||
return ftxui::text("CAN player") | ftxui::color(ftxui::Color::Red);
|
||||
}) | ftxui::hcenter,
|
||||
ftxui::Renderer([]() { return ftxui::separator(); }),
|
||||
|
||||
// Render window here
|
||||
makeCanPlayerDialog(screen, smap, canplayer_is_ready) | ftxui::flex,
|
||||
|
||||
ftxui::Renderer([]() { return ftxui::separator(); }),
|
||||
|
||||
ftxui::Container::Horizontal({
|
||||
ftxui::Checkbox({
|
||||
.transform = [this](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
return ftxui::text(" >[stop_all]< ") |
|
||||
(state.focused ? ftxui::bold : ftxui::nothing) |
|
||||
ftxui::color(ftxui::Color::Cyan) |
|
||||
(state.focused ? ftxui::bgcolor(ftxui::Color::Grey11) : ftxui::nothing);
|
||||
},
|
||||
|
||||
.on_change = [&smap]() { smap.get<void()>("canplayer_stopped")->operator()(); },
|
||||
}),
|
||||
|
||||
ftxui::Checkbox({
|
||||
.transform = [this](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
return ftxui::text(" >[close]< ") | (state.focused ? ftxui::bold : ftxui::nothing) |
|
||||
ftxui::color(ftxui::Color::Cyan) |
|
||||
(state.focused ? ftxui::bgcolor(ftxui::Color::Grey11) : ftxui::nothing);
|
||||
},
|
||||
|
||||
.on_change = []() { canbus_player_dialog_shown = false; },
|
||||
}),
|
||||
}) | ftxui::hcenter,
|
||||
}) | ftxui::size(ftxui::WIDTH, ftxui::EQUAL, ftxui::Terminal::Size().dimx) |
|
||||
ftxui::size(ftxui::HEIGHT, ftxui::EQUAL, ftxui::Terminal::Size().dimy) | ftxui::border,
|
||||
&canbus_player_dialog_shown),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
~Impl() override = default;
|
||||
};
|
||||
|
||||
return ftxui::Make<Impl>(screen, smap);
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
#include "parsers.hpp"
|
||||
|
||||
namespace parsers {
|
||||
std::optional<struct range_s> parseSpnDataRange(const std::string &str) {
|
||||
struct range_s result;
|
||||
auto begin = str.begin(), end = str.end();
|
||||
return phrase_parse(begin, end, range::range_parser_s<decltype(begin)>{}, ascii::space, result) ? std::optional<range_s>(result) : std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<struct size_s> parseSpnSize(const std::string &str) {
|
||||
struct size_s result;
|
||||
auto begin = str.begin(), end = str.end();
|
||||
return phrase_parse(begin, end, size::size_parser_s<decltype(begin)>{}, ascii::space, result) ? std::optional<size_s>(result) : std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<struct offset_s> parseSpnOffset(const std::string &str) {
|
||||
struct offset_s result;
|
||||
auto begin = str.begin(), end = str.end();
|
||||
return phrase_parse(begin, end, offset::offset_parser_s<decltype(begin)>{}, ascii::space, result) ? std::optional<struct offset_s>(result) : std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<struct resolution_s> parseSpnResolution(const std::string &str) {
|
||||
struct resolution_s result;
|
||||
auto begin = str.begin(), end = str.end();
|
||||
return phrase_parse(begin, end, resolution::resolution_parser_s<decltype(begin)>{}, ascii::space, result) ? std::optional<struct resolution_s>(result) : std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<struct spn_fragments_s> parseSpnPosition(size_t spn_size_bits, const std::string &str) {
|
||||
struct spn_fragments_s result;
|
||||
auto begin = str.begin(), end = str.end();
|
||||
return phrase_parse(begin, end, position::position_parser_s<decltype(begin)>(spn_size_bits), ascii::space, result) ? std::optional<struct spn_fragments_s>(result) : std::nullopt;
|
||||
}
|
||||
}; // namespace parsers
|
||||
|
|
@ -0,0 +1,336 @@
|
|||
#pragma once
|
||||
|
||||
#include <fmt/base.h>
|
||||
#include <algorithm>
|
||||
#include <boost/bind.hpp>
|
||||
#include <boost/spirit/home/qi/numeric/uint.hpp>
|
||||
#include <boost/spirit/include/phoenix.hpp>
|
||||
#include <boost/spirit/include/qi.hpp>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#define FMT_HEADER_ONLY
|
||||
#include <fmt/format.h>
|
||||
#include <fmt/ranges.h>
|
||||
|
||||
namespace parsers {
|
||||
using namespace boost::spirit;
|
||||
|
||||
// Parse data range string to this struct
|
||||
struct range_s {
|
||||
double min = .0f, max = .0f;
|
||||
std::string other;
|
||||
};
|
||||
|
||||
// Parse data range string to this struct
|
||||
struct size_s {
|
||||
size_t size_bytes = 0u, size_bits = 0u;
|
||||
};
|
||||
|
||||
// Parse data range string to this struct
|
||||
struct offset_s {
|
||||
double offset = .0f;
|
||||
};
|
||||
|
||||
// SPN position here
|
||||
struct spn_fragments_s {
|
||||
struct spn_part_s {
|
||||
size_t byte_offset, bit_offset, size;
|
||||
};
|
||||
|
||||
std::vector<spn_part_s> spn_fragments;
|
||||
};
|
||||
|
||||
// Parse resolution string to this struct
|
||||
struct resolution_s {
|
||||
double resolution;
|
||||
};
|
||||
|
||||
namespace _detail {
|
||||
template <typename It, typename Res> struct parser_s : qi::grammar<It, Res(), ascii::space_type> {
|
||||
parser_s() : parser_s::base_type(rule) {}
|
||||
qi::rule<It, Res(), ascii::space_type> rule; // Main rule
|
||||
qi::rule<It, std::string(), ascii::space_type> strnum;
|
||||
qi::rule<It, double(), ascii::space_type> num;
|
||||
};
|
||||
|
||||
// String to double converter
|
||||
struct string_to_double_s {
|
||||
double operator()(const std::string &str) const {
|
||||
std::string num_str = str;
|
||||
num_str.erase(std::remove(num_str.begin(), num_str.end(), ','), num_str.end());
|
||||
return std::stod(num_str);
|
||||
}
|
||||
};
|
||||
|
||||
// String to int32 converter
|
||||
struct string_to_int_s {
|
||||
int32_t operator()(const std::string &str) const {
|
||||
std::string num_str = str;
|
||||
num_str.erase(std::remove(num_str.begin(), num_str.end(), ','), num_str.end());
|
||||
return std::stoi(num_str);
|
||||
}
|
||||
};
|
||||
}; // namespace _detail
|
||||
|
||||
// Data range parsers
|
||||
namespace range {
|
||||
template <typename It> struct range_parser_s : _detail::parser_s<It, range_s> {
|
||||
range_parser_s() : _detail::parser_s<It, range_s>() {
|
||||
|
||||
this->strnum = lexeme[-(qi::char_('+') | qi::char_('-')) >> +(qi::digit | qi::char_(',') | qi::char_('.'))];
|
||||
this->num = this->strnum[_val = boost::phoenix::function<_detail::string_to_double_s>{}(qi::_1)];
|
||||
this->rule = (this->num >> "to" >> this->num) | (-qi::digit >> -qi::digit >> +qi::char_("a-zA-Z-"));
|
||||
}
|
||||
};
|
||||
}; // namespace range
|
||||
|
||||
namespace size {
|
||||
template <typename It> struct size_parser_s : _detail::parser_s<It, size_s> {
|
||||
size_parser_s() : _detail::parser_s<It, size_s>() {
|
||||
|
||||
struct bytes_to_bits_s {
|
||||
size_s operator()(size_t bytes) const {
|
||||
return {
|
||||
.size_bytes = bytes,
|
||||
.size_bits = bytes * UINT8_WIDTH,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
struct as_bits_s {
|
||||
size_s operator()(size_t bits) const {
|
||||
return {
|
||||
.size_bytes = 0u,
|
||||
.size_bits = bits,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
this->rule = ((qi::uint_ >> "byte") | (qi::uint_ >> "bytes"))[_val = boost::phoenix::function<bytes_to_bits_s>{}(qi::_1)] |
|
||||
((qi::uint_ >> "bit") | (qi::uint_ >> "bits"))[_val = boost::phoenix::function<as_bits_s>{}(qi::_1)];
|
||||
}
|
||||
};
|
||||
}; // namespace size
|
||||
|
||||
namespace position {
|
||||
template <typename It> struct position_parser_s : _detail::parser_s<It, spn_fragments_s> {
|
||||
position_parser_s(size_t size_bits) : _detail::parser_s<It, spn_fragments_s>() {
|
||||
|
||||
// just start byte
|
||||
struct rule_v0_handler_s {
|
||||
spn_fragments_s operator()(uint32_t start_byte) const {
|
||||
return {
|
||||
.spn_fragments =
|
||||
{
|
||||
{
|
||||
.byte_offset = (start_byte - 1u), // From zero
|
||||
.bit_offset = 0,
|
||||
.size = UINT8_WIDTH,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// start byte and bit offset (1 part of size - up to 1 byte)
|
||||
struct rule_v1_handler_s {
|
||||
spn_fragments_s operator()(size_t size_bits, uint32_t start_byte, uint32_t bit_offset) const {
|
||||
return {
|
||||
.spn_fragments =
|
||||
{
|
||||
{
|
||||
.byte_offset = (start_byte - 1u), // From zero
|
||||
.bit_offset = (bit_offset - 1u),
|
||||
.size = (size_bits % UINT8_WIDTH) ? (size_bits % UINT8_WIDTH) : UINT8_WIDTH,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// start byte and last_byte (1 part multiple of 1 byte)
|
||||
struct rule_v2_handler_s {
|
||||
spn_fragments_s operator()(uint32_t start_byte, uint32_t last_byte) const {
|
||||
return {
|
||||
.spn_fragments =
|
||||
{
|
||||
{
|
||||
.byte_offset = (start_byte - 1u),
|
||||
.bit_offset = 0u,
|
||||
.size = ((last_byte - start_byte) + 1u) * UINT8_WIDTH,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// start byte and last byte with bit offset (2 parts - first is integer bytes and second with bit offset)
|
||||
struct rule_v3_handler_s {
|
||||
spn_fragments_s operator()(size_t size_bits, uint32_t start_byte, uint32_t last_byte, uint32_t bit_offset) const {
|
||||
return {
|
||||
.spn_fragments =
|
||||
{
|
||||
// First - integer bytes (from start_byte to last_byte-1)
|
||||
{
|
||||
.byte_offset = (start_byte - 1u),
|
||||
.bit_offset = 0u,
|
||||
.size = ((last_byte - start_byte)) * UINT8_WIDTH,
|
||||
},
|
||||
|
||||
// Second - with bit mask
|
||||
{
|
||||
.byte_offset = (last_byte - 1u),
|
||||
.bit_offset = (bit_offset - 1u),
|
||||
.size = (size_bits % UINT8_WIDTH) ? (size_bits % UINT8_WIDTH) : UINT8_WIDTH,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// start with bit offset, last byte (2 parts - first with bit offset and second integer byte)
|
||||
struct rule_v4_handler_s {
|
||||
spn_fragments_s operator()(uint32_t start_byte, uint32_t bit_offset, uint32_t last_byte) const {
|
||||
return {
|
||||
.spn_fragments =
|
||||
{
|
||||
// First - with bit mask
|
||||
{
|
||||
.byte_offset = (start_byte - 1u),
|
||||
.bit_offset = (bit_offset - 1u),
|
||||
.size = UINT8_WIDTH - (bit_offset - 1u),
|
||||
},
|
||||
|
||||
// second - integer byte
|
||||
{
|
||||
.byte_offset = (last_byte - 1u),
|
||||
.bit_offset = 0u,
|
||||
.size = UINT8_WIDTH, // Size is equal to 1 byte
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// start byte, last integer byte and last byte with bit offset
|
||||
struct rule_v5_handler_s {
|
||||
spn_fragments_s operator()(size_t size_bits, uint32_t start_byte, uint32_t last_integer_byte, uint32_t last_byte, uint32_t bit_offset) const {
|
||||
return {
|
||||
.spn_fragments =
|
||||
{
|
||||
{
|
||||
.byte_offset = (start_byte - 1u),
|
||||
.bit_offset = 0u,
|
||||
.size = ((last_integer_byte - start_byte) + 1u) * UINT8_WIDTH,
|
||||
},
|
||||
|
||||
// Second - with bit mask
|
||||
{
|
||||
.byte_offset = (last_byte - 1u),
|
||||
.bit_offset = (bit_offset - 1u),
|
||||
.size = (size_bits % UINT8_WIDTH) ? (size_bits % UINT8_WIDTH) : UINT8_WIDTH,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// start byte with bit offset, first integer byte and last integer byte
|
||||
struct rule_v6_handler_s {
|
||||
spn_fragments_s operator()(uint32_t start_byte, uint32_t bit_offset, uint32_t first_integer_byte, uint32_t last_byte) const {
|
||||
return {
|
||||
.spn_fragments =
|
||||
{
|
||||
// First - with bit mask
|
||||
{
|
||||
.byte_offset = (start_byte - 1u),
|
||||
.bit_offset = (bit_offset - 1u),
|
||||
.size = UINT8_WIDTH - (bit_offset - 1u),
|
||||
},
|
||||
|
||||
// Second - integer bytes
|
||||
{
|
||||
.byte_offset = (first_integer_byte - 1u),
|
||||
.bit_offset = 0u,
|
||||
.size = ((last_byte - first_integer_byte) + 1u) * UINT8_WIDTH,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
position_rule_v0 = (qi::uint_)[qi::_val = boost::phoenix::function<rule_v0_handler_s>{}(qi::_1)];
|
||||
position_rule_v1 = (qi::uint_ >> '.' >> qi::uint_)[qi::_val = boost::phoenix::function<rule_v1_handler_s>{}(size_bits, qi::_1, qi::_2)];
|
||||
position_rule_v2 = (qi::uint_ >> '-' >> qi::uint_)[qi::_val = boost::phoenix::function<rule_v2_handler_s>{}(qi::_1, qi::_2)];
|
||||
position_rule_v3 = (qi::uint_ >> ',' >> qi::uint_ >> '.' >> qi::uint_)[qi::_val = boost::phoenix::function<rule_v3_handler_s>{}(size_bits, qi::_1, qi::_2, qi::_3)];
|
||||
position_rule_v4 = (qi::uint_ >> '.' >> qi::uint_ >> ',' >> qi::uint_)[qi::_val = boost::phoenix::function<rule_v4_handler_s>{}(qi::_1, qi::_2, qi::_3)];
|
||||
position_rule_v5 = (qi::uint_ >> '-' >> qi::uint_ >> ',' >> qi::uint_ >> '.' >>
|
||||
qi::uint_)[qi::_val = boost::phoenix::function<rule_v5_handler_s>{}(size_bits, qi::_1, qi::_2, qi::_3, qi::_4)];
|
||||
position_rule_v6 =
|
||||
(qi::uint_ >> '.' >> qi::uint_ >> ',' >> qi::uint_ >> '-' >> qi::uint_)[qi::_val = boost::phoenix::function<rule_v6_handler_s>{}(qi::_1, qi::_2, qi::_3, qi::_4)];
|
||||
|
||||
// If one of rules works
|
||||
this->rule = position_rule_v6 | position_rule_v5 | position_rule_v4 | position_rule_v3 | position_rule_v2 | position_rule_v1 | position_rule_v0;
|
||||
}
|
||||
|
||||
qi::rule<It, spn_fragments_s(), ascii::space_type> position_rule_v0, position_rule_v1, position_rule_v2, position_rule_v3, position_rule_v4, position_rule_v5, position_rule_v6;
|
||||
};
|
||||
}; // namespace position
|
||||
|
||||
namespace offset {
|
||||
template <typename It> struct offset_parser_s : _detail::parser_s<It, offset_s> {
|
||||
offset_parser_s() : _detail::parser_s<It, offset_s>() {
|
||||
this->strnum = lexeme[-(qi::char_('+') | qi::char_('-')) >> +(qi::digit | qi::char_(',') | qi::char_('.'))];
|
||||
this->num = this->strnum[_val = boost::phoenix::function<_detail::string_to_double_s>{}(qi::_1)];
|
||||
this->rule = this->num;
|
||||
}
|
||||
};
|
||||
}; // namespace offset
|
||||
|
||||
// Resolution parsers
|
||||
namespace resolution {
|
||||
template <typename It> struct resolution_parser_s : _detail::parser_s<It, resolution_s> {
|
||||
resolution_parser_s() : _detail::parser_s<It, resolution_s>() {
|
||||
struct resolution_rule_v0_handler_s {
|
||||
resolution_s operator()(float number) const {
|
||||
return {
|
||||
.resolution = number,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
struct resolution_rule_v1_handler_s {
|
||||
resolution_s operator()(double first, double second) const {
|
||||
return {
|
||||
.resolution = first / second,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
this->strnum = lexeme[-(qi::char_('+') | qi::char_('-')) >> +(qi::digit | qi::char_(',') | qi::char_('.'))];
|
||||
this->num = this->strnum[_val = boost::phoenix::function<_detail::string_to_double_s>{}(qi::_1)];
|
||||
|
||||
resolution_rule_v0 = (this->num >> *qi::char_)[qi::_val = boost::phoenix::function<resolution_rule_v0_handler_s>{}(qi::_1)];
|
||||
resolution_rule_v1 = (qi::uint_ >> '/' >> qi::uint_ >> *qi::char_)[qi::_val = boost::phoenix::function<resolution_rule_v1_handler_s>{}(qi::_1, qi::_2)];
|
||||
|
||||
this->rule = resolution_rule_v1 | resolution_rule_v0;
|
||||
}
|
||||
|
||||
qi::rule<It, resolution_s(), ascii::space_type> resolution_rule_v0, resolution_rule_v1;
|
||||
};
|
||||
}; // namespace resolution
|
||||
|
||||
std::optional<struct range_s> parseSpnDataRange(const std::string &str);
|
||||
std::optional<struct size_s> parseSpnSize(const std::string &str);
|
||||
std::optional<struct offset_s> parseSpnOffset(const std::string &str);
|
||||
std::optional<struct spn_fragments_s> parseSpnPosition(size_t spn_size_bits, const std::string &str);
|
||||
std::optional<struct resolution_s> parseSpnResolution(const std::string &str);
|
||||
}; // namespace parsers
|
||||
|
||||
BOOST_FUSION_ADAPT_STRUCT(parsers::range_s, (double, min)(double, max)(std::string, other));
|
||||
BOOST_FUSION_ADAPT_STRUCT(parsers::size_s, (size_t, size_bytes)(size_t, size_bits));
|
||||
BOOST_FUSION_ADAPT_STRUCT(parsers::spn_fragments_s::spn_part_s, (size_t, byte_offset)(size_t, bit_offset)(size_t, size));
|
||||
BOOST_FUSION_ADAPT_STRUCT(parsers::spn_fragments_s, (std::vector<struct parsers::spn_fragments_s::spn_part_s>, spn_fragments));
|
||||
BOOST_FUSION_ADAPT_STRUCT(parsers::offset_s, (double, offset));
|
||||
BOOST_FUSION_ADAPT_STRUCT(parsers::resolution_s, (double, resolution));
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
#include "recorder.hpp"
|
||||
#include "sqlite_modern_cpp.h"
|
||||
|
||||
#include <zlib.h>
|
||||
#include <sys/resource.h>
|
||||
|
||||
#define FMT_HEADER_ONLY
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <spdlog/sinks/systemd_sink.h>
|
||||
|
||||
Recorder::Recorder(const std::string &db_path, bool console_output)
|
||||
: disk_db_path_(db_path), console_output_(console_output) {
|
||||
log_ = spdlog::systemd_logger_mt("recorder", "cansniffer-rec");
|
||||
log_->set_level(spdlog::level::info);
|
||||
|
||||
flush_task_ = std::async(std::launch::async, [this](std::stop_token st) { background_flush_task(st); }, flush_stop_.get_token());
|
||||
log_->info("recorder initialized, db_path={}", db_path);
|
||||
if (console_output_) fmt::println("Recording to: {}", db_path);
|
||||
}
|
||||
|
||||
Recorder::~Recorder() {
|
||||
flushAndClose();
|
||||
}
|
||||
|
||||
void Recorder::onBatch(const std::vector<can_frame_update_s> &batch) {
|
||||
int64_t now = epoch_ms();
|
||||
std::lock_guard<std::mutex> lock(batch_mtx_);
|
||||
if (batch_ts_start_ == 0) batch_ts_start_ = now;
|
||||
for (const auto &u : batch) {
|
||||
if (u.verbose && !u.verbose->is_null() && u.verbose->contains("SPNs")) {
|
||||
pending_.push_back({now, u.iface, u.canid, u.verbose});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Recorder::flushAndClose() {
|
||||
flush_stop_.request_stop();
|
||||
if (flush_task_.valid()) flush_task_.wait();
|
||||
|
||||
std::lock_guard<std::mutex> lock(batch_mtx_);
|
||||
if (!pending_.empty()) {
|
||||
log_->info("flushing remaining {} frames on exit", pending_.size());
|
||||
compress_batch();
|
||||
}
|
||||
}
|
||||
|
||||
int64_t Recorder::epoch_ms() {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
size_t Recorder::get_rss_mb() {
|
||||
struct rusage ru {};
|
||||
getrusage(RUSAGE_SELF, &ru);
|
||||
return static_cast<size_t>(ru.ru_maxrss) / 1024;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Recorder::gzip_compress(const std::string &src) {
|
||||
z_stream strm{};
|
||||
deflateInit2(&strm, Z_BEST_COMPRESSION, Z_DEFLATED, 15 | 16, 8, Z_DEFAULT_STRATEGY);
|
||||
|
||||
strm.next_in = reinterpret_cast<Bytef *>(const_cast<char *>(src.data()));
|
||||
strm.avail_in = static_cast<uInt>(src.size());
|
||||
|
||||
std::vector<uint8_t> out;
|
||||
out.resize(deflateBound(&strm, static_cast<uLong>(src.size())));
|
||||
|
||||
strm.next_out = out.data();
|
||||
strm.avail_out = static_cast<uInt>(out.size());
|
||||
|
||||
deflate(&strm, Z_FINISH);
|
||||
out.resize(strm.total_out);
|
||||
deflateEnd(&strm);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
void Recorder::compress_batch() {
|
||||
if (pending_.empty()) return;
|
||||
|
||||
nlohmann::json::array_t arr;
|
||||
arr.reserve(pending_.size());
|
||||
|
||||
int64_t ts_start = pending_.front().ts_ms;
|
||||
int64_t ts_end = pending_.back().ts_ms;
|
||||
|
||||
for (const auto &rec : pending_) {
|
||||
nlohmann::json entry;
|
||||
entry["ts"] = rec.ts_ms;
|
||||
entry["canid"] = rec.canid;
|
||||
|
||||
if (rec.verbose && !rec.verbose->is_null()) {
|
||||
const auto &v = *rec.verbose;
|
||||
if (v.contains("PGN")) entry["pgn"] = v["PGN"];
|
||||
|
||||
if (v.contains("SPNs")) {
|
||||
nlohmann::json::array_t spns;
|
||||
for (const auto &spn : v["SPNs"]) {
|
||||
nlohmann::json s;
|
||||
if (spn.contains("SPN (integer)")) s["spn"] = spn["SPN (integer)"];
|
||||
if (spn.contains("SPN name")) s["name"] = spn["SPN name"];
|
||||
if (spn.contains("Value")) s["value"] = spn["Value"];
|
||||
if (spn.contains("Unit")) s["unit"] = spn["Unit"];
|
||||
spns.push_back(std::move(s));
|
||||
}
|
||||
entry["spns"] = std::move(spns);
|
||||
}
|
||||
}
|
||||
|
||||
arr.push_back(std::move(entry));
|
||||
}
|
||||
|
||||
std::string json_str = nlohmann::json(arr).dump();
|
||||
auto compressed = gzip_compress(json_str);
|
||||
|
||||
try {
|
||||
sqlite::database disk_db(disk_db_path_);
|
||||
disk_db << "PRAGMA journal_mode = WAL;";
|
||||
disk_db << R"(
|
||||
CREATE TABLE IF NOT EXISTS batches (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ts_start INTEGER NOT NULL,
|
||||
ts_end INTEGER NOT NULL,
|
||||
frame_count INTEGER NOT NULL,
|
||||
data BLOB NOT NULL
|
||||
);
|
||||
)";
|
||||
disk_db << R"(CREATE INDEX IF NOT EXISTS idx_batches_ts ON batches(ts_start);)";
|
||||
|
||||
disk_db << "INSERT INTO batches (ts_start, ts_end, frame_count, data) VALUES (?, ?, ?, ?);"
|
||||
<< ts_start << ts_end << static_cast<int64_t>(pending_.size()) << compressed;
|
||||
|
||||
auto msg = fmt::format("Flushed batch: {} frames, {:.1f}KB -> {:.1f}KB gzip ({:.0f}% compression)",
|
||||
pending_.size(),
|
||||
static_cast<double>(json_str.size()) / 1024.0,
|
||||
static_cast<double>(compressed.size()) / 1024.0,
|
||||
(1.0 - static_cast<double>(compressed.size()) / static_cast<double>(json_str.size())) * 100.0);
|
||||
log_->info("{}", msg);
|
||||
if (console_output_) fmt::println("{}", msg);
|
||||
} catch (const sqlite::sqlite_exception &e) {
|
||||
log_->error("disk DB write failed: {}", e.what());
|
||||
}
|
||||
|
||||
pending_.clear();
|
||||
batch_ts_start_ = 0;
|
||||
}
|
||||
|
||||
void Recorder::background_flush_task(std::stop_token st) {
|
||||
while (!st.stop_requested()) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(10));
|
||||
|
||||
std::lock_guard<std::mutex> lock(batch_mtx_);
|
||||
if (pending_.empty()) continue;
|
||||
|
||||
int64_t now = epoch_ms();
|
||||
bool time_trigger = (now - batch_ts_start_) >= 60'000;
|
||||
bool mem_trigger = get_rss_mb() >= 500;
|
||||
|
||||
if (time_trigger || mem_trigger) {
|
||||
if (mem_trigger) log_->warn("RSS >= 500MB, forcing flush");
|
||||
compress_batch();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
#pragma once
|
||||
|
||||
#include "can_data.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <stop_token>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
struct FrameRecord {
|
||||
int64_t ts_ms;
|
||||
std::string iface;
|
||||
std::string canid;
|
||||
std::shared_ptr<nlohmann::json> verbose;
|
||||
};
|
||||
|
||||
class Recorder {
|
||||
public:
|
||||
Recorder(const std::string &db_path, bool console_output);
|
||||
~Recorder();
|
||||
|
||||
void onBatch(const std::vector<can_frame_update_s> &batch);
|
||||
void flushAndClose();
|
||||
|
||||
private:
|
||||
static int64_t epoch_ms();
|
||||
static size_t get_rss_mb();
|
||||
static std::vector<uint8_t> gzip_compress(const std::string &src);
|
||||
|
||||
void compress_batch();
|
||||
void background_flush_task(std::stop_token st);
|
||||
|
||||
std::string disk_db_path_;
|
||||
std::mutex batch_mtx_;
|
||||
std::vector<FrameRecord> pending_;
|
||||
int64_t batch_ts_start_ = 0;
|
||||
std::stop_source flush_stop_;
|
||||
std::future<void> flush_task_;
|
||||
bool console_output_ = false;
|
||||
std::shared_ptr<spdlog::logger> log_;
|
||||
};
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
#pragma once
|
||||
|
||||
#include <boost/signals2.hpp>
|
||||
#include <memory>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "can_data.hpp"
|
||||
|
||||
#define FMT_HEADER_ONLY
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "sqlite_modern_cpp.h"
|
||||
|
||||
class signals_map_s {
|
||||
struct signal_holder_base {
|
||||
virtual ~signal_holder_base() = default;
|
||||
};
|
||||
|
||||
template <typename Signature>
|
||||
struct signal_holder : signal_holder_base {
|
||||
boost::signals2::signal<Signature> signal;
|
||||
};
|
||||
|
||||
public:
|
||||
template <typename Signature>
|
||||
void register_signal(const std::string &name) {
|
||||
signals_.emplace(name, std::make_unique<signal_holder<Signature>>());
|
||||
}
|
||||
|
||||
template <typename Signature>
|
||||
auto *get(const std::string &name) {
|
||||
auto it = signals_.find(name);
|
||||
if (it == signals_.end()) {
|
||||
throw std::runtime_error(fmt::format("Signal '{}' not found", name));
|
||||
}
|
||||
auto *holder = dynamic_cast<signal_holder<Signature> *>(it->second.get());
|
||||
if (!holder) {
|
||||
throw std::runtime_error(fmt::format("Signal '{}' type mismatch", name));
|
||||
}
|
||||
return &holder->signal;
|
||||
}
|
||||
|
||||
template <typename Signature>
|
||||
auto *get(const char *name) {
|
||||
return get<Signature>(std::string(name));
|
||||
}
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, std::unique_ptr<signal_holder_base>> signals_;
|
||||
};
|
||||
|
||||
struct signals_s {
|
||||
static inline signals_map_s map = []() {
|
||||
signals_map_s m;
|
||||
m.register_signal<void(const std::string &)>("new_data_recvd");
|
||||
m.register_signal<void(const std::string &, const std::string &, const can_frame_data_s &, const can_frame_diff_s &)>("new_entry");
|
||||
m.register_signal<void(const std::vector<can_frame_update_s> &)>("new_entries_batch");
|
||||
m.register_signal<void(const std::string &)>("show_settings");
|
||||
m.register_signal<void()>("show_file_dialog_request");
|
||||
m.register_signal<void(const std::string &)>("export_file_request");
|
||||
m.register_signal<void(const std::string &, size_t)>("new_tag_request");
|
||||
m.register_signal<void(sqlite::database &)>("j1939_database_ready");
|
||||
m.register_signal<void()>("canplayer_started");
|
||||
m.register_signal<void()>("canplayer_stopped");
|
||||
return m;
|
||||
}();
|
||||
};
|
||||
|
||||
using signals_map_t = signals_map_s;
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/component_base.hpp>
|
||||
#include <ftxui/component/component_options.hpp>
|
||||
#include <ftxui/component/mouse.hpp>
|
||||
#include <ftxui/component/screen_interactive.hpp>
|
||||
#include <ftxui/dom/elements.hpp>
|
||||
|
||||
#include <boost/signals2.hpp>
|
||||
|
||||
#define FMT_HEADER_ONLY
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "signals.hpp"
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include "json/json.hpp"
|
||||
|
||||
ftxui::Component makeSpnSelector(ftxui::ScreenInteractive *screen, signals_map_t &smap, nlohmann::json data) {
|
||||
class Impl : public ftxui::ComponentBase {
|
||||
public:
|
||||
Impl(ftxui::ScreenInteractive *screen, signals_map_t &smap, nlohmann::json data) {}
|
||||
};
|
||||
|
||||
return ftxui::Make<Impl>(screen, smap, data);
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
#include "tagsettingrow.hpp"
|
||||
|
||||
ftxui::Component makeSpnSettingsRow(ftxui::ScreenInteractive *screen, signals_map_t &smap, const std::string &canid, ftxui::Component cmpCont, size_t spn_id,
|
||||
spn_settings_map_t &tagSettingsMap) {
|
||||
return ftxui::Make<SpnSettingRow>(screen, smap, canid, cmpCont, spn_id, tagSettingsMap);
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
#pragma once
|
||||
|
||||
#include <boost/lexical_cast.hpp>
|
||||
#include <boost/signals2.hpp>
|
||||
#include <boost/uuid/uuid.hpp>
|
||||
#include <boost/uuid/uuid_generators.hpp>
|
||||
#include <boost/uuid/uuid_io.hpp>
|
||||
|
||||
#include <condition_variable>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/screen_interactive.hpp>
|
||||
#include <ftxui/dom/elements.hpp>
|
||||
#include <future>
|
||||
#include <map>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#define FMT_HEADER_ONLY
|
||||
#include <fmt/format.h>
|
||||
#include <fmt/ranges.h>
|
||||
|
||||
#include "signals.hpp"
|
||||
#include "tagsettings.hpp"
|
||||
|
||||
class SpnSettingRow : public ftxui::ComponentBase {
|
||||
public:
|
||||
using spn_settings_map_t = std::map<std::string, std::map<int32_t, spn_settings_s>>;
|
||||
|
||||
explicit SpnSettingRow(ftxui::ScreenInteractive *screen, signals_map_t &smap, const std::string &canid, ftxui::Component cmpCont, size_t tag_id,
|
||||
spn_settings_map_t &tagSettingsMap) {
|
||||
Add(ftxui::Container::Vertical({
|
||||
ftxui::Container::Vertical({
|
||||
ftxui::Container::Horizontal({
|
||||
ftxui::Renderer([]() { return ftxui::text(fmt::format("{:32}", "Name: ")) | ftxui::bold | ftxui::color(ftxui::Color::Yellow); }),
|
||||
ftxui::Input({.content = &m_current_settings_.spn_name, .placeholder = "SPN name"}) | ftxui::vcenter | ftxui::xflex,
|
||||
}),
|
||||
|
||||
ftxui::Container::Horizontal({
|
||||
ftxui::Renderer([]() { return ftxui::text(fmt::format("{:32}", "X coefficient: ")) | ftxui::bold | ftxui::color(ftxui::Color::Yellow); }),
|
||||
ftxui::Input({.content = &m_current_settings_.x_coeff, .placeholder = "X coefficient"}) | ftxui::vcenter | ftxui::xflex,
|
||||
}),
|
||||
|
||||
ftxui::Container::Horizontal({
|
||||
ftxui::Renderer([]() { return ftxui::text(fmt::format("{:32}", "Y coefficient: ")) | ftxui::bold | ftxui::color(ftxui::Color::Yellow); }),
|
||||
ftxui::Input({.content = &m_current_settings_.y_coeff, .placeholder = "Y coefficient"}) | ftxui::vcenter | ftxui::xflex,
|
||||
}),
|
||||
|
||||
ftxui::Container::Horizontal({
|
||||
ftxui::Renderer([]() { return ftxui::text(fmt::format("{:32}", "Offset: ")) | ftxui::bold | ftxui::color(ftxui::Color::Yellow); }),
|
||||
ftxui::Input({.content = &m_current_settings_.offset, .placeholder = "Offset"}) | ftxui::vcenter | ftxui::xflex,
|
||||
}),
|
||||
|
||||
ftxui::Container::Horizontal({
|
||||
ftxui::Renderer([]() { return ftxui::text(fmt::format("{:32}", "Size: ")) | ftxui::bold | ftxui::color(ftxui::Color::Yellow); }),
|
||||
ftxui::Input({.content = &m_current_settings_.size, .placeholder = "Size"}) | ftxui::vcenter | ftxui::xflex,
|
||||
}),
|
||||
|
||||
ftxui::Container::Horizontal({
|
||||
ftxui::Renderer([]() { return ftxui::text(fmt::format("{:32}", "Pos: ")) | ftxui::bold | ftxui::color(ftxui::Color::Yellow); }),
|
||||
ftxui::Input({.content = &m_current_settings_.pos, .placeholder = "Pos"}) | ftxui::vcenter | ftxui::xflex,
|
||||
}),
|
||||
|
||||
ftxui::Container::Horizontal({
|
||||
ftxui::Renderer([]() { return ftxui::text(fmt::format("{:32}", "Bit offset: ")) | ftxui::bold | ftxui::color(ftxui::Color::Yellow); }),
|
||||
ftxui::Input({.content = &m_current_settings_.bit_offset, .placeholder = "Bit offset"}) | ftxui::vcenter | ftxui::xflex,
|
||||
}),
|
||||
|
||||
ftxui::Container::Horizontal({
|
||||
ftxui::Renderer([]() { return ftxui::text(fmt::format("{:32}", "Bit count: ")) | ftxui::bold | ftxui::color(ftxui::Color::Yellow); }),
|
||||
ftxui::Input({.content = &m_current_settings_.bit_count, .placeholder = "Bit count"}) | ftxui::vcenter | ftxui::xflex,
|
||||
}),
|
||||
|
||||
ftxui::Checkbox({
|
||||
.label = fmt::format("{:32}", "Little endian"),
|
||||
.checked = &m_current_settings_.le,
|
||||
}) | ftxui::vcenter |
|
||||
ftxui::xflex,
|
||||
|
||||
ftxui::Checkbox({
|
||||
.label = fmt::format("{:32}", "Discrete"),
|
||||
.checked = &m_current_settings_.discrete,
|
||||
}) | ftxui::vcenter |
|
||||
ftxui::xflex,
|
||||
}) | ftxui::xflex,
|
||||
}));
|
||||
}
|
||||
|
||||
const spn_settings_s &exportSettings() const { return m_current_settings_; }
|
||||
|
||||
private:
|
||||
spn_settings_s m_current_settings_;
|
||||
};
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct spn_fragment_s {
|
||||
std::string byte_offset;
|
||||
std::string bit_offset;
|
||||
std::string bit_count;
|
||||
};
|
||||
|
||||
struct spn_settings_s {
|
||||
bool selected = false;
|
||||
std::string spn_name;
|
||||
std::string resolution;
|
||||
std::string offset;
|
||||
bool big_endian = false;
|
||||
std::string unit;
|
||||
std::string uuid;
|
||||
std::vector<spn_fragment_s> fragments = {{}};
|
||||
int32_t active_fragment = 0;
|
||||
|
||||
// Legacy compat
|
||||
std::string x_coeff;
|
||||
std::string y_coeff;
|
||||
std::string size;
|
||||
std::string pos;
|
||||
std::string bit_offset;
|
||||
std::string bit_count;
|
||||
bool le = false;
|
||||
bool discrete = false;
|
||||
};
|
||||
|
||||
using spn_settings_map_t = std::map<std::string, std::map<int32_t, spn_settings_s>>;
|
||||
|
|
@ -0,0 +1,548 @@
|
|||
#include <boost/lexical_cast.hpp>
|
||||
#include <boost/signals2.hpp>
|
||||
#include <boost/uuid/uuid.hpp>
|
||||
#include <boost/uuid/uuid_generators.hpp>
|
||||
#include <boost/uuid/uuid_io.hpp>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
|
||||
#include <condition_variable>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <ftxui/component/component.hpp>
|
||||
#include <ftxui/component/screen_interactive.hpp>
|
||||
#include <ftxui/dom/elements.hpp>
|
||||
#include <future>
|
||||
#include <map>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#define FMT_HEADER_ONLY
|
||||
#include <fmt/format.h>
|
||||
#include <fmt/ranges.h>
|
||||
|
||||
#include "canid_unit.hpp"
|
||||
#include "signals.hpp"
|
||||
#include "tagsettings.hpp"
|
||||
|
||||
ftxui::Component makeSpnSettingsForm(ftxui::ScreenInteractive *screen, signals_map_t &smap, const std::string &canid,
|
||||
std::string &canid_active, ftxui::Component cmpCont, ftxui::Component dialog,
|
||||
bool &modal_shown,
|
||||
std::map<std::string, std::map<int32_t, ftxui::Component>> &spnSettingsFormMap,
|
||||
spn_settings_map_t &spnSettingsMap) {
|
||||
class Impl : public ftxui::ComponentBase {
|
||||
public:
|
||||
explicit Impl(ftxui::ScreenInteractive *screen, signals_map_t &smap, const std::string &canid,
|
||||
std::string &canid_active, ftxui::Component cmpCont, ftxui::Component dialog, bool &modal_shown,
|
||||
std::map<std::string, std::map<int32_t, ftxui::Component>> &spnSettingsFormMap,
|
||||
spn_settings_map_t &spnSettingsMap)
|
||||
: m_canid_(canid), m_screen_(screen), m_smap_(smap), m_cmpCont_(cmpCont),
|
||||
m_spnSettingsFormMap_(spnSettingsFormMap), m_spnSettingsMap_(spnSettingsMap) {
|
||||
static size_t spns_count = 0u;
|
||||
|
||||
for (auto &[k, v] : m_spnSettingsFormMap_[m_canid_]) {
|
||||
addSpnComponent(k);
|
||||
}
|
||||
|
||||
smap.get<void(const std::string &, size_t)>("new_tag_request")
|
||||
->connect([this](const std::string &canid, size_t tag_id) {
|
||||
if (canid == m_canid_) {
|
||||
addSpnComponent(static_cast<int32_t>(tag_id));
|
||||
}
|
||||
});
|
||||
|
||||
Add(ftxui::Container::Vertical({
|
||||
ftxui::Container::Vertical({
|
||||
ftxui::Checkbox({
|
||||
.transform = [this](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
return ftxui::hbox({
|
||||
ftxui::text(" >[add_parameter]< ") | (state.focused ? ftxui::bold : ftxui::nothing) |
|
||||
ftxui::color(ftxui::Color::Cyan) |
|
||||
(state.focused ? ftxui::bgcolor(ftxui::Color::Grey11) : ftxui::nothing),
|
||||
ftxui::filler(),
|
||||
});
|
||||
},
|
||||
|
||||
.on_change =
|
||||
[&, this]() {
|
||||
canid_active = m_canid_;
|
||||
m_spnSettingsMap_[m_canid_].insert_or_assign(
|
||||
static_cast<int32_t>(spns_count),
|
||||
spn_settings_s{
|
||||
.uuid = boost::lexical_cast<std::string>(boost::uuids::random_generator()()),
|
||||
.le = true,
|
||||
});
|
||||
addSpnComponent(static_cast<int32_t>(spns_count));
|
||||
spns_count++;
|
||||
},
|
||||
}),
|
||||
|
||||
ftxui::Checkbox({
|
||||
.transform = [this](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
return ftxui::hbox({
|
||||
ftxui::text(" >[remove_selected_params]< ") |
|
||||
(state.focused ? ftxui::bold : ftxui::nothing) | ftxui::color(ftxui::Color::Cyan) |
|
||||
(state.focused ? ftxui::bgcolor(ftxui::Color::Grey11) : ftxui::nothing),
|
||||
ftxui::filler(),
|
||||
});
|
||||
},
|
||||
|
||||
.on_change =
|
||||
[&, this]() {
|
||||
std::vector<int32_t> keys;
|
||||
|
||||
for (const auto &[k, v] : m_spnSettingsMap_[m_canid_]) {
|
||||
if (v.selected) {
|
||||
keys.push_back(k);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto i : keys) {
|
||||
m_spnSettingsFormMap_[m_canid_].erase(i);
|
||||
m_spnSettingsMap_[m_canid_].erase(i);
|
||||
}
|
||||
|
||||
m_current_spn_settings_->DetachAllChildren();
|
||||
for (auto &[k, v] : m_spnSettingsFormMap_[m_canid_]) {
|
||||
m_current_spn_settings_->Add(v);
|
||||
}
|
||||
},
|
||||
}),
|
||||
}),
|
||||
|
||||
m_current_spn_settings_ | ftxui::xflex,
|
||||
}) |
|
||||
ftxui::flex);
|
||||
}
|
||||
|
||||
private:
|
||||
void addSpnComponent(int32_t k) {
|
||||
auto &s = m_spnSettingsMap_[m_canid_][k];
|
||||
|
||||
auto make_field = [](const char *label, const char *placeholder, std::string *content) {
|
||||
return ftxui::Container::Horizontal({
|
||||
ftxui::Renderer([label]() {
|
||||
return ftxui::text(fmt::format("{:24}", label)) | ftxui::bold | ftxui::color(ftxui::Color::Yellow);
|
||||
}),
|
||||
|
||||
ftxui::Renderer([]() { return ftxui::text("[ "); }),
|
||||
ftxui::Input({
|
||||
.content = content,
|
||||
.placeholder = placeholder,
|
||||
.transform = [](ftxui::InputState state) -> ftxui::Element {
|
||||
auto el = state.element;
|
||||
|
||||
if (state.focused) {
|
||||
el = el | ftxui::bgcolor(ftxui::Color::Grey27) | ftxui::focusCursorBarBlinking;
|
||||
} else if (state.hovered) {
|
||||
el = el | ftxui::bgcolor(ftxui::Color::Grey11);
|
||||
}
|
||||
|
||||
return el;
|
||||
},
|
||||
|
||||
.multiline = false,
|
||||
}) | ftxui::size(ftxui::WIDTH, ftxui::EQUAL, 32),
|
||||
ftxui::Renderer([]() { return ftxui::text(" ]"); }),
|
||||
});
|
||||
};
|
||||
|
||||
auto get_data = [this]() -> std::vector<uint8_t> {
|
||||
for (uint32_t i = 0; i < m_cmpCont_->ChildCount(); i++) {
|
||||
if (std::static_pointer_cast<CanIDUnit>(m_cmpCont_->ChildAt(i))->getCanID() == m_canid_) {
|
||||
return std::static_pointer_cast<CanIDUnit>(m_cmpCont_->ChildAt(i))->getData();
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
// Fragment tab container — shows fields for active fragment
|
||||
auto frag_fields_container = ftxui::Container::Vertical({});
|
||||
|
||||
auto rebuild_frag_fields = [&s, frag_fields_container, make_field]() {
|
||||
frag_fields_container->DetachAllChildren();
|
||||
if (s.active_fragment >= 0 && s.active_fragment < static_cast<int32_t>(s.fragments.size())) {
|
||||
|
||||
auto &f = s.fragments[s.active_fragment];
|
||||
frag_fields_container->Add(make_field("Byte offset:", "enter byte offset here ...", &f.byte_offset));
|
||||
frag_fields_container->Add(make_field("Bit offset:", "enter bit offset here ...", &f.bit_offset));
|
||||
frag_fields_container->Add(make_field("Bit count:", "enter bit count here ...", &f.bit_count));
|
||||
}
|
||||
};
|
||||
|
||||
rebuild_frag_fields();
|
||||
|
||||
// Fragment tab switcher
|
||||
auto frag_tabs = ftxui::Renderer([&s]() {
|
||||
ftxui::Elements tabs;
|
||||
tabs.push_back(ftxui::text("<"));
|
||||
|
||||
for (size_t i = 0; i < s.fragments.size(); ++i) {
|
||||
if (i > 0) {
|
||||
tabs.push_back(ftxui::text(" | "));
|
||||
}
|
||||
|
||||
auto label = ftxui::text(fmt::format("fragment#{}", i));
|
||||
if (static_cast<int32_t>(i) == s.active_fragment) {
|
||||
label = label | ftxui::bold | ftxui::color(ftxui::Color::Red);
|
||||
} else {
|
||||
label = label | ftxui::color(ftxui::Color::Cyan);
|
||||
}
|
||||
|
||||
tabs.push_back(label);
|
||||
}
|
||||
|
||||
tabs.push_back(ftxui::text(">"));
|
||||
return ftxui::hbox(std::move(tabs));
|
||||
});
|
||||
|
||||
// Payload view — highlights ALL fragments simultaneously
|
||||
auto payload_view = ftxui::Renderer([&s, get_data]() {
|
||||
auto data = get_data();
|
||||
|
||||
double resolution = 1.0, offset_val = 0.0;
|
||||
try {
|
||||
resolution = std::stod(s.resolution);
|
||||
} catch (...) {
|
||||
}
|
||||
|
||||
try {
|
||||
offset_val = std::stod(s.offset);
|
||||
} catch (...) {
|
||||
}
|
||||
|
||||
// Collect all bit ranges from all fragments
|
||||
struct bit_range {
|
||||
int32_t global_start, global_end;
|
||||
int32_t byte_off, byte_cnt;
|
||||
};
|
||||
|
||||
std::vector<bit_range> ranges;
|
||||
for (const auto &f : s.fragments) {
|
||||
int32_t bo = 0, bi = 0, bc = 0;
|
||||
|
||||
try {
|
||||
bo = std::stoi(f.byte_offset);
|
||||
} catch (...) {
|
||||
}
|
||||
|
||||
try {
|
||||
bi = std::stoi(f.bit_offset);
|
||||
} catch (...) {
|
||||
}
|
||||
|
||||
try {
|
||||
bc = std::stoi(f.bit_count);
|
||||
} catch (...) {
|
||||
}
|
||||
|
||||
if (bc > 0) {
|
||||
int32_t byte_cnt = (bc + bi + UINT8_WIDTH - 1) / UINT8_WIDTH;
|
||||
ranges.push_back({bo * UINT8_WIDTH + bi, bo * UINT8_WIDTH + bi + bc, bo, byte_cnt});
|
||||
}
|
||||
}
|
||||
|
||||
auto byte_in_range = [&ranges](int32_t idx) {
|
||||
for (const auto &r : ranges) {
|
||||
if (idx >= r.byte_off && idx < r.byte_off + r.byte_cnt) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
auto bit_in_range = [&ranges](int32_t global_bit) {
|
||||
for (const auto &r : ranges) {
|
||||
if (global_bit >= r.global_start && global_bit < r.global_end) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// Hex
|
||||
ftxui::Elements hex_els;
|
||||
hex_els.push_back(ftxui::text("{ "));
|
||||
for (size_t i = 0; i < data.size(); ++i) {
|
||||
auto val = fmt::format("0x{:02X}", data[i]);
|
||||
auto t = ftxui::text(i + 1 < data.size() ? fmt::format("{:<15s}", val + ",") : fmt::format("{:<15s}", val));
|
||||
hex_els.push_back(byte_in_range(static_cast<int32_t>(i)) ? (t | ftxui::color(ftxui::Color::Red) | ftxui::bold)
|
||||
: t);
|
||||
}
|
||||
|
||||
hex_els.push_back(ftxui::text(" }"));
|
||||
|
||||
// Dec
|
||||
ftxui::Elements dec_els;
|
||||
dec_els.push_back(ftxui::text("{ "));
|
||||
|
||||
for (size_t i = 0; i < data.size(); ++i) {
|
||||
auto val = fmt::format("{}", data[i]);
|
||||
auto t = ftxui::text(i + 1 < data.size() ? fmt::format("{:<15s}", val + ",") : fmt::format("{:<15s}", val));
|
||||
dec_els.push_back(byte_in_range(static_cast<int32_t>(i)) ? (t | ftxui::color(ftxui::Color::Red) | ftxui::bold)
|
||||
: t);
|
||||
}
|
||||
|
||||
dec_els.push_back(ftxui::text(" }"));
|
||||
|
||||
// Bin — per-bit highlighting across all fragments
|
||||
ftxui::Elements bin_els;
|
||||
bin_els.push_back(ftxui::text("{ "));
|
||||
|
||||
for (size_t i = 0; i < data.size(); ++i) {
|
||||
ftxui::Elements bits;
|
||||
bits.push_back(ftxui::text("0b"));
|
||||
|
||||
for (int32_t b = 7; b >= 0; --b) {
|
||||
int32_t global_bit = static_cast<int32_t>(i) * UINT8_WIDTH + b;
|
||||
char ch = (data[i] >> b) & 1 ? '1' : '0';
|
||||
auto t = ftxui::text(std::string(1, ch));
|
||||
bits.push_back(bit_in_range(global_bit) ? (t | ftxui::color(ftxui::Color::Red) | ftxui::bold) : t);
|
||||
}
|
||||
|
||||
if (i + 1 < data.size()) {
|
||||
bits.push_back(ftxui::text(","));
|
||||
}
|
||||
|
||||
bin_els.push_back(ftxui::hbox(std::move(bits)));
|
||||
bin_els.push_back(ftxui::text(" "));
|
||||
}
|
||||
|
||||
bin_els.push_back(ftxui::text(" }"));
|
||||
|
||||
// Extract value from all fragments (always LE within each fragment)
|
||||
int64_t result = 0;
|
||||
size_t total_bits = 0;
|
||||
for (const auto &f : s.fragments) {
|
||||
int32_t bo = 0, bi = 0, bc = 0;
|
||||
|
||||
try {
|
||||
bo = std::stoi(f.byte_offset);
|
||||
bi = std::stoi(f.bit_offset);
|
||||
bc = std::stoi(f.bit_count);
|
||||
} catch (...) {
|
||||
}
|
||||
|
||||
int32_t byte_cnt = (bc + bi + UINT8_WIDTH - 1) / UINT8_WIDTH;
|
||||
|
||||
if (bc > 0 && bo >= 0 && static_cast<size_t>(bo + byte_cnt) <= data.size()) {
|
||||
int64_t frag_val = 0;
|
||||
|
||||
for (int32_t i = 0; i < byte_cnt; ++i) {
|
||||
frag_val |= static_cast<int64_t>(data[bo + i]) << (i * UINT8_WIDTH);
|
||||
}
|
||||
|
||||
frag_val = (frag_val >> bi) & ((1LL << bc) - 1);
|
||||
result |= frag_val << total_bits;
|
||||
total_bits += bc;
|
||||
}
|
||||
}
|
||||
// Byteswap the assembled result if big endian
|
||||
if (s.big_endian && total_bits > 8u) {
|
||||
int64_t swapped = 0;
|
||||
size_t total_bytes = (total_bits + 7u) / 8u;
|
||||
|
||||
for (size_t i = 0; i < total_bytes; ++i) {
|
||||
swapped |= ((result >> (i * 8u)) & 0xFFu) << ((total_bytes - 1 - i) * 8u);
|
||||
}
|
||||
|
||||
result = swapped;
|
||||
}
|
||||
|
||||
double value = static_cast<double>(result) * resolution + offset_val;
|
||||
|
||||
return ftxui::vbox({
|
||||
ftxui::separatorEmpty(),
|
||||
ftxui::separatorEmpty(),
|
||||
ftxui::vbox({
|
||||
ftxui::hbox(hex_els),
|
||||
ftxui::hbox(dec_els),
|
||||
ftxui::hbox(bin_els),
|
||||
}) | ftxui::xframe,
|
||||
ftxui::hbox({
|
||||
ftxui::text("Value: ") | ftxui::bold,
|
||||
ftxui::text(fmt::format("({:0{}} | 0x{:0{}X} | 0b{:0{}b}) * {} + {} = ", result,
|
||||
static_cast<int32_t>(fmt::format("{}", (1LL << total_bits) - 1).size()), result,
|
||||
static_cast<int32_t>((total_bits + 3) / 4), result,
|
||||
static_cast<int32_t>(total_bits), resolution, offset_val)),
|
||||
|
||||
ftxui::text(fmt::format("{:.6g} {}", value, s.unit)) | ftxui::color(ftxui::Color::IndianRed) |
|
||||
ftxui::bold,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
// Fragment buttons + switcher (full width, above the main horizontal layout)
|
||||
auto frag_switcher = ftxui::Container::Vertical({
|
||||
ftxui::Container::Horizontal({
|
||||
ftxui::Checkbox({
|
||||
.transform = [](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
auto el = ftxui::text(">[add_fragment]<") | ftxui::color(ftxui::Color::Cyan);
|
||||
if (state.focused || state.active) {
|
||||
el = el | ftxui::bold | ftxui::bgcolor(ftxui::Color::Grey11);
|
||||
}
|
||||
|
||||
return el;
|
||||
},
|
||||
|
||||
.on_change =
|
||||
[&s, rebuild_frag_fields]() {
|
||||
s.fragments.push_back({});
|
||||
s.active_fragment = static_cast<int32_t>(s.fragments.size()) - 1;
|
||||
rebuild_frag_fields();
|
||||
},
|
||||
}),
|
||||
ftxui::Renderer([]() { return ftxui::text(" "); }),
|
||||
ftxui::Checkbox({
|
||||
.transform = [](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
auto el = ftxui::text(">[remove_fragment]<") | ftxui::color(ftxui::Color::Cyan);
|
||||
if (state.focused || state.active)
|
||||
el = el | ftxui::bold | ftxui::bgcolor(ftxui::Color::Grey11);
|
||||
return el;
|
||||
},
|
||||
.on_change =
|
||||
[&s, rebuild_frag_fields]() {
|
||||
if (s.fragments.size() > 1) {
|
||||
s.fragments.erase(s.fragments.begin() + s.active_fragment);
|
||||
if (s.active_fragment >= static_cast<int32_t>(s.fragments.size()))
|
||||
s.active_fragment = static_cast<int32_t>(s.fragments.size()) - 1;
|
||||
rebuild_frag_fields();
|
||||
}
|
||||
},
|
||||
}),
|
||||
}),
|
||||
|
||||
ftxui::Container::Horizontal({
|
||||
ftxui::Checkbox({
|
||||
.transform = [](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
auto el = ftxui::text("[<]") | ftxui::color(ftxui::Color::Cyan);
|
||||
|
||||
if (state.focused || state.active) {
|
||||
el = el | ftxui::bold | ftxui::bgcolor(ftxui::Color::Grey11);
|
||||
}
|
||||
|
||||
return el;
|
||||
},
|
||||
|
||||
.on_change =
|
||||
[&s, rebuild_frag_fields]() {
|
||||
if (s.active_fragment > 0) {
|
||||
s.active_fragment--;
|
||||
rebuild_frag_fields();
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
ftxui::Renderer([&s]() {
|
||||
ftxui::Elements tabs;
|
||||
tabs.push_back(ftxui::text(" "));
|
||||
for (size_t i = 0; i < s.fragments.size(); ++i) {
|
||||
if (i > 0) {
|
||||
tabs.push_back(ftxui::text(" | "));
|
||||
}
|
||||
|
||||
auto label = ftxui::text(fmt::format("fragment#{}", i));
|
||||
|
||||
if (static_cast<int32_t>(i) == s.active_fragment) {
|
||||
label = label | ftxui::bold | ftxui::color(ftxui::Color::Red);
|
||||
|
||||
} else {
|
||||
label = label | ftxui::color(ftxui::Color::Cyan);
|
||||
}
|
||||
|
||||
tabs.push_back(label);
|
||||
}
|
||||
|
||||
tabs.push_back(ftxui::text(" "));
|
||||
return ftxui::hbox(std::move(tabs));
|
||||
}),
|
||||
|
||||
ftxui::Checkbox({
|
||||
.transform = [](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
auto el = ftxui::text("[>]") | ftxui::color(ftxui::Color::Cyan);
|
||||
if (state.focused || state.active)
|
||||
el = el | ftxui::bold | ftxui::bgcolor(ftxui::Color::Grey11);
|
||||
return el;
|
||||
},
|
||||
.on_change =
|
||||
[&s, rebuild_frag_fields]() {
|
||||
if (s.active_fragment < static_cast<int32_t>(s.fragments.size()) - 1) {
|
||||
s.active_fragment++;
|
||||
rebuild_frag_fields();
|
||||
}
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
// Main horizontal: checkbox | settings | payload
|
||||
auto main_row = ftxui::Container::Horizontal({
|
||||
ftxui::Checkbox({
|
||||
.checked = &s.selected,
|
||||
.transform = [&s](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
return ftxui::text(s.selected ? "[X]" : "[ ]") |
|
||||
(s.selected ? ftxui::color(ftxui::Color::Red) : ftxui::color(ftxui::Color::Cyan));
|
||||
},
|
||||
}) | ftxui::vcenter |
|
||||
ftxui::size(ftxui::WIDTH, ftxui::EQUAL, 5),
|
||||
|
||||
ftxui::Container::Vertical({
|
||||
make_field("SPN name:", "enter name here ...", &s.spn_name),
|
||||
make_field("SPN resolution:", "enter resolution here ...", &s.resolution),
|
||||
make_field("SPN offset:", "enter offset here ...", &s.offset),
|
||||
make_field("SPN unit:", "enter unit here ...", &s.unit),
|
||||
frag_fields_container,
|
||||
ftxui::Container::Horizontal({
|
||||
ftxui::Renderer([]() {
|
||||
return ftxui::text(fmt::format("{:24}", "Endianness:")) | ftxui::bold |
|
||||
ftxui::color(ftxui::Color::Yellow);
|
||||
}),
|
||||
|
||||
ftxui::Checkbox({
|
||||
.checked = &s.big_endian,
|
||||
.transform = [&s](const ftxui::EntryState &state) -> ftxui::Element {
|
||||
auto el = ftxui::hbox({
|
||||
ftxui::text("< "),
|
||||
ftxui::text("little") |
|
||||
(!s.big_endian ? (ftxui::bold | ftxui::color(ftxui::Color::Red)) : ftxui::nothing),
|
||||
ftxui::text(" | "),
|
||||
ftxui::text("big") |
|
||||
(s.big_endian ? (ftxui::bold | ftxui::color(ftxui::Color::Red)) : ftxui::nothing),
|
||||
ftxui::text(" >"),
|
||||
});
|
||||
|
||||
if (state.focused || state.active) {
|
||||
el = el | ftxui::bold | ftxui::bgcolor(ftxui::Color::Grey11);
|
||||
}
|
||||
|
||||
return el;
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}) | ftxui::flex,
|
||||
|
||||
ftxui::Renderer([]() { return ftxui::text(" "); }),
|
||||
payload_view | ftxui::flex,
|
||||
});
|
||||
|
||||
auto component = ftxui::Container::Vertical({frag_switcher, main_row}) | ftxui::border | ftxui::flex;
|
||||
|
||||
m_current_spn_settings_->Add({component});
|
||||
m_spnSettingsFormMap_[m_canid_].insert_or_assign(k, component);
|
||||
}
|
||||
|
||||
private:
|
||||
ftxui::Component m_current_spn_settings_ = ftxui::Container::Vertical({});
|
||||
std::string m_canid_;
|
||||
ftxui::ScreenInteractive *m_screen_;
|
||||
signals_map_t &m_smap_;
|
||||
ftxui::Component m_cmpCont_;
|
||||
std::map<std::string, std::map<int32_t, ftxui::Component>> &m_spnSettingsFormMap_;
|
||||
spn_settings_map_t &m_spnSettingsMap_;
|
||||
};
|
||||
|
||||
return ftxui::Make<Impl>(screen, smap, canid, canid_active, cmpCont, dialog, modal_shown, spnSettingsFormMap,
|
||||
spnSettingsMap);
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
|
||||
namespace tp {
|
||||
namespace detail {
|
||||
|
||||
template <typename Func, typename... Args> constexpr Func for_each_arg(Func f, Args &&...args) {
|
||||
std::initializer_list<int>{f(std::forward<Args>(args), 0)...};
|
||||
return f;
|
||||
}
|
||||
|
||||
template <typename Tuple, typename Func, std::size_t... I> constexpr Func for_each_impl(Tuple &&t, Func &&f, std::index_sequence<I...> is) {
|
||||
return std::initializer_list<int>{(std::forward<Func>(f)(std::get<I>(std::forward<Tuple>(t))), 0)...}, f;
|
||||
}
|
||||
|
||||
template <typename... Ts, typename Function, size_t... Is> auto transform_impl(std::tuple<Ts...> const &inputs, Function function, std::index_sequence<Is...> is) {
|
||||
return std::tuple<std::result_of_t<Function(Ts)>...>{function(std::get<Is>(inputs))...};
|
||||
}
|
||||
|
||||
template <typename... T, std::size_t... i> auto subtuple(const std::tuple<T...> &t, std::index_sequence<i...>) { return std::make_tuple(std::get<i>(t)...); }
|
||||
|
||||
// ZIP utilities
|
||||
template <std::size_t I, typename... Tuples> using zip_tuple_at_index_t = std::tuple<std::tuple_element_t<I, std::decay_t<Tuples>>...>;
|
||||
template <std::size_t I, typename... Tuples> zip_tuple_at_index_t<I, Tuples...> zip_tuple_at_index(Tuples &&...tuples) { return {std::get<I>(std::forward<Tuples>(tuples))...}; }
|
||||
template <typename... Tuples, std::size_t... I> std::tuple<zip_tuple_at_index_t<I, Tuples...>...> tuple_zip_impl(Tuples &&...tuples, std::index_sequence<I...>) {
|
||||
return {zip_tuple_at_index<I>(std::forward<Tuples>(tuples)...)...};
|
||||
}
|
||||
}; // namespace detail
|
||||
|
||||
template <class Tuple, class F> constexpr decltype(auto) for_each(Tuple &&tuple, F &&f) {
|
||||
return []<std::size_t... I>(Tuple &&tuple, F &&f, std::index_sequence<I...>) {
|
||||
(f(std::get<I>(tuple)), ...);
|
||||
return f;
|
||||
}(std::forward<Tuple>(tuple), std::forward<F>(f), std::make_index_sequence<std::tuple_size<std::remove_reference_t<Tuple>>::value>{});
|
||||
}
|
||||
|
||||
template <typename... Ts, typename Function> auto transform(std::tuple<Ts...> const &inputs, Function function) {
|
||||
return detail::transform_impl(inputs, function, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
}
|
||||
|
||||
template <typename Tuple, typename Predicate> constexpr size_t find_if(Tuple &&tuple, Predicate pred) {
|
||||
size_t index = std::tuple_size<std::remove_reference_t<Tuple>>::value;
|
||||
size_t currentIndex = 0;
|
||||
bool found = false;
|
||||
|
||||
for_each(tuple, [&](auto &&value) {
|
||||
if (!found && pred(value)) {
|
||||
index = currentIndex;
|
||||
found = true;
|
||||
}
|
||||
|
||||
++currentIndex;
|
||||
});
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
template <typename Tuple, typename Action> void perform(Tuple &&tuple, size_t index, Action action) {
|
||||
size_t currentIndex = 0;
|
||||
for_each(tuple, [&action, index, ¤tIndex](auto &&value) {
|
||||
if (currentIndex == index) {
|
||||
|
||||
action(std::forward<decltype(value)>(value));
|
||||
}
|
||||
|
||||
++currentIndex;
|
||||
});
|
||||
}
|
||||
|
||||
template <typename Tuple, typename Predicate> bool all_of(Tuple &&tuple, Predicate pred) {
|
||||
return find_if(tuple, std::not_fn(pred)) == std::tuple_size<std::decay_t<Tuple>>::value;
|
||||
}
|
||||
|
||||
template <typename Tuple, typename Predicate> bool none_of(Tuple &&tuple, Predicate pred) { return find_if(tuple, pred) == std::tuple_size<std::decay_t<Tuple>>::value; }
|
||||
template <typename Tuple, typename Predicate> bool any_of(Tuple &&tuple, Predicate pred) { return !none_of(tuple, pred); }
|
||||
|
||||
template <typename Tuple, typename Function> Tuple &operator|(Tuple &&tuple, Function func) {
|
||||
for_each(tuple, func);
|
||||
return tuple;
|
||||
}
|
||||
|
||||
template <int trim, typename... T> auto subtuple(const std::tuple<T...> &t) { return detail::subtuple(t, std::make_index_sequence<sizeof...(T) - trim>()); }
|
||||
|
||||
template <size_t starting, size_t elems, class Tuple, class Seq = decltype(std::make_index_sequence<elems>())> struct sub_range;
|
||||
|
||||
template <size_t starting, size_t elems, class... Args, size_t... indx> struct sub_range<starting, elems, std::tuple<Args...>, std::index_sequence<indx...>> {
|
||||
static_assert(elems <= sizeof...(Args) - starting, "sub range is out of bounds!");
|
||||
using tuple = std::tuple<std::tuple_element_t<indx + starting, std::tuple<Args...>>...>;
|
||||
};
|
||||
|
||||
template <typename Tuple, std::size_t... Ints> auto select_tuple(Tuple &&tuple, std::index_sequence<Ints...>) {
|
||||
return std::tuple<std::tuple_element_t<Ints, Tuple>...>(std::get<Ints>(std::forward<Tuple>(tuple))...);
|
||||
}
|
||||
|
||||
template <class T, class Tuple> struct tuple_index;
|
||||
|
||||
template <class T, class... Types> struct tuple_index<T, std::tuple<T, Types...>> {
|
||||
static const std::size_t value = 0;
|
||||
};
|
||||
|
||||
template <class T, class U, class... Types> struct tuple_index<T, std::tuple<U, Types...>> {
|
||||
static const std::size_t value = 1 + tuple_index<T, std::tuple<Types...>>::value;
|
||||
};
|
||||
|
||||
// ZIP
|
||||
template <typename Head, typename... Tail>
|
||||
requires((std::tuple_size_v<std::decay_t<Tail>> == std::tuple_size_v<std::decay_t<Head>>) && ...)
|
||||
auto tuple_zip(Head &&head, Tail &&...tail) {
|
||||
return detail::tuple_zip_impl<Head, Tail...>(std::forward<Head>(head), std::forward<Tail>(tail)..., std::make_index_sequence<std::tuple_size_v<std::decay_t<Head>>>());
|
||||
}
|
||||
}; // namespace tp
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
|
||||
/**
|
||||
* @brief Returns pretty-print name of the type
|
||||
*/
|
||||
template <class T> constexpr std::string_view type_name() {
|
||||
using namespace std;
|
||||
#if defined(__clang__)
|
||||
std::string_view p = __PRETTY_FUNCTION__;
|
||||
return std::string_view(p.data() + 34, p.size() - 34 - 1);
|
||||
#elif defined(__GNUC__)
|
||||
std::string_view p = __PRETTY_FUNCTION__;
|
||||
return std::string_view(p.data() + 49, p.find(';', 49) - 49);
|
||||
#endif
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
#include "utils.hpp"
|
||||
|
||||
namespace utils {
|
||||
void backup_db(const sqlite::database &db, const std::string &backup_path) {
|
||||
// sqlite::database bkp(backup_path);
|
||||
// auto con = db.connection();
|
||||
// auto state = std::unique_ptr<sqlite3_backup, decltype(&sqlite3_backup_finish)>(sqlite3_backup_init(bkp.connection().get(), "main", con.get(), "main"), sqlite3_backup_finish);
|
||||
|
||||
// if (state) {
|
||||
// int rc;
|
||||
|
||||
// do {
|
||||
// rc = sqlite3_backup_step(state.get(), 100);
|
||||
// } while (rc == SQLITE_OK || rc == SQLITE_BUSY || rc == SQLITE_LOCKED);
|
||||
// }
|
||||
}
|
||||
}; // namespace utils
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
#include "sqlite_modern_cpp.h"
|
||||
|
||||
namespace utils {
|
||||
void backup_db(const sqlite::database &db, const std::string &backup_path);
|
||||
};
|
||||
|
|
@ -0,0 +1,371 @@
|
|||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <unistd.h>
|
||||
|
||||
#define FMT_HEADER_ONLY
|
||||
#include <fmt/format.h>
|
||||
#include <fmt/ranges.h>
|
||||
|
||||
// for XLSX files
|
||||
#include <xlnt/xlnt.hpp>
|
||||
|
||||
// For sqlite
|
||||
#include "parsers.hpp"
|
||||
#include "sqlite_modern_cpp.h"
|
||||
|
||||
static void init_db(sqlite::database &db) {
|
||||
try {
|
||||
db << R"(
|
||||
CREATE TABLE IF NOT EXISTS pgns (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
pgn INTEGER UNIQUE,
|
||||
pg_label TEXT,
|
||||
pg_acronym TEXT,
|
||||
pg_descr TEXT,
|
||||
edp INTEGER,
|
||||
dp INTEGER,
|
||||
pf INTEGER,
|
||||
ps INTEGER,
|
||||
pg_datalen INTEGER,
|
||||
pg_priority INTEGER
|
||||
);
|
||||
)";
|
||||
|
||||
db << R"(
|
||||
CREATE TABLE IF NOT EXISTS spns (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
pgn INTEGER,
|
||||
spn INTEGER UNIQUE,
|
||||
spn_name TEXT,
|
||||
spn_pos TEXT,
|
||||
spn_length INTEGER,
|
||||
resolution REAL,
|
||||
offset REAL,
|
||||
data_range TEXT,
|
||||
min_value REAL,
|
||||
max_value REAL,
|
||||
units TEXT,
|
||||
slot_id TEXT,
|
||||
slot_name TEXT,
|
||||
spn_type TEXT
|
||||
);
|
||||
)";
|
||||
|
||||
db << R"(
|
||||
CREATE TABLE IF NOT EXISTS spn_fragments (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
spn INTEGER,
|
||||
pgn INTEGER,
|
||||
byte_offset INTEGER,
|
||||
bit_offset INTEGER,
|
||||
size INTEGER
|
||||
);
|
||||
)";
|
||||
|
||||
db << "PRAGMA journal_mode = OFF";
|
||||
db << "PRAGMA synchronous = OFF";
|
||||
db << "PRAGMA foreign_keys = ON;";
|
||||
} catch (const sqlite::sqlite_exception &e) {
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<sqlite::database> parseXlsx(const std::string &file) {
|
||||
auto db_ptr = std::make_unique<sqlite::database>(":memory:");
|
||||
auto &db = *db_ptr;
|
||||
|
||||
init_db(db);
|
||||
|
||||
static const std::map<std::string_view, std::tuple<size_t, std::string_view>>
|
||||
pgn_mapping_table =
|
||||
{
|
||||
{"PGN", {5u, "pgn"}},
|
||||
{"Parameter Group Label", {6u, "pg_label"}},
|
||||
{"PG Acronym", {7u, "pg_acronym"}},
|
||||
{"PG Description", {8u, "pg_descr"}},
|
||||
{"EDP", {9u, "edp"}},
|
||||
{"DP", {10u, "dp"}},
|
||||
{"PF", {11u, "pf"}},
|
||||
{"PS", {12u, "ps"}},
|
||||
{"PG Data Length", {15u, "pg_datalen"}},
|
||||
{"Default Priority", {16u, "pg_priority"}},
|
||||
},
|
||||
|
||||
spn_mapping_table = {
|
||||
{"SPN", {19u, "spn"}},
|
||||
{"SPN Name", {20u, "spn_name"}},
|
||||
{"SPN Position in PG", {18u, "spn_pos"}},
|
||||
{"SPN Length", {22u, "spn_length"}},
|
||||
{"Offset", {24u, "offset"}},
|
||||
{"Data Range", {25u, "data_range"}},
|
||||
{"Resolution", {23u, "resolution"}},
|
||||
{"Units", {27u, "units"}},
|
||||
{"SLOT Identifier", {28u, "slot_id"}},
|
||||
{"SLOT Name", {29u, "slot_name"}},
|
||||
{"SPN Type", {30u, "spn_type"}},
|
||||
};
|
||||
|
||||
std::map<size_t, std::string> pgn_headers, spn_headers;
|
||||
xlnt::workbook wb;
|
||||
|
||||
if (xlnt::path(file).exists()) {
|
||||
wb = xlnt::workbook(xlnt::path(file));
|
||||
|
||||
if (wb.sheet_count() == 0u) {
|
||||
throw std::runtime_error(fmt::format("Workbook {} is empty", file));
|
||||
}
|
||||
|
||||
wb.active_sheet(0);
|
||||
} else {
|
||||
|
||||
throw std::runtime_error(fmt::format("File {} does not exists", file));
|
||||
}
|
||||
|
||||
xlnt::worksheet ws = wb.active_sheet();
|
||||
for (const auto &col : ws.columns()) {
|
||||
if (auto [cell, value] = std::pair{col[0], col[0].to_string()}; !value.empty()) {
|
||||
if (pgn_mapping_table.contains(value)) {
|
||||
|
||||
auto it = pgn_headers.insert_or_assign(cell.column_index(), value);
|
||||
} else if (spn_mapping_table.contains(value)) {
|
||||
|
||||
auto it = spn_headers.insert_or_assign(cell.column_index(), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &row : ws.rows()) {
|
||||
std::map<std::string, std::string> pgn_row_map, spn_row_map;
|
||||
for (const auto &cell : row) {
|
||||
if (cell.row() != 1u) {
|
||||
|
||||
if (pgn_headers.contains(cell.column_index())) {
|
||||
if (!cell.to_string().empty()) {
|
||||
pgn_row_map.insert_or_assign(
|
||||
[&]() {
|
||||
std::string ret;
|
||||
|
||||
for (const auto &[k, v] : pgn_mapping_table) {
|
||||
if (k == pgn_headers[cell.column_index()] && cell.column_index() == std::get<0u>(v)) {
|
||||
|
||||
ret = std::get<1u>(v);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}(),
|
||||
|
||||
cell.to_string());
|
||||
}
|
||||
} else if (spn_headers.contains(cell.column_index())) {
|
||||
if (!cell.to_string().empty()) {
|
||||
spn_row_map.insert_or_assign(
|
||||
[&]() {
|
||||
std::string ret;
|
||||
|
||||
for (const auto &[k, v] : spn_mapping_table) {
|
||||
if (k == spn_headers[cell.column_index()] && cell.column_index() == std::get<0u>(v)) {
|
||||
|
||||
ret = std::get<1u>(v);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}(),
|
||||
|
||||
cell.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add rows to database
|
||||
if (!pgn_row_map.empty() && !spn_row_map.empty()) {
|
||||
for (const auto &[k, v] : pgn_row_map) {
|
||||
try {
|
||||
auto ps = db << fmt::format(
|
||||
"INSERT OR REPLACE INTO pgns ({}) VALUES ({})",
|
||||
[&]() {
|
||||
std::string ret;
|
||||
|
||||
for (const auto &[k, v] : pgn_mapping_table) {
|
||||
auto end = pgn_mapping_table.end();
|
||||
bool is_last = k == (--end)->first;
|
||||
ret += fmt::format("{}{}", std::get<1u>(v), !is_last ? ", " : "");
|
||||
}
|
||||
|
||||
return ret;
|
||||
}(),
|
||||
|
||||
[&]() {
|
||||
std::string ret;
|
||||
for (const auto &[k, _] : pgn_mapping_table) {
|
||||
auto end = pgn_mapping_table.end();
|
||||
bool is_last = k == (--end)->first;
|
||||
ret += !is_last ? "?, " : "?";
|
||||
}
|
||||
|
||||
return ret;
|
||||
}());
|
||||
|
||||
for (const auto &[k, v] : pgn_mapping_table) {
|
||||
ps << pgn_row_map[std::get<1u>(v).data()];
|
||||
}
|
||||
|
||||
ps.execute();
|
||||
} catch (const sqlite::sqlite_exception &e) {
|
||||
if (e.get_extended_code() == SQLITE_CONSTRAINT_UNIQUE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
struct {
|
||||
bool parts_inserted_flag = false;
|
||||
double min = 0.0, max = 0.0;
|
||||
size_t size_bits = 0u;
|
||||
} spn_settings_calculated;
|
||||
|
||||
// Pre-compute size_bits before the main loop (resolution calculation depends on it)
|
||||
{
|
||||
auto it = spn_mapping_table.find("SPN Length");
|
||||
if (it != spn_mapping_table.end()) {
|
||||
auto size = parsers::parseSpnSize(spn_row_map[std::get<1u>(it->second).data()]);
|
||||
spn_settings_calculated.size_bits = size.has_value() ? size.value().size_bits : 0u;
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &[k, v] : spn_row_map) {
|
||||
try {
|
||||
auto ps = db << fmt::format(
|
||||
"INSERT OR REPLACE INTO spns ({}) VALUES ({})",
|
||||
[&]() {
|
||||
std::string ret;
|
||||
|
||||
ret += "pgn, ";
|
||||
for (const auto &[k, v] : spn_mapping_table) {
|
||||
auto end = spn_mapping_table.end();
|
||||
bool is_last = k == (--end)->first;
|
||||
|
||||
// Split 'data range' to two columns 'min' and 'max'
|
||||
if (std::get<1u>(v) == "data_range") {
|
||||
|
||||
ret += is_last ? ", min_value, max_value" : "min_value, max_value, ";
|
||||
} else {
|
||||
|
||||
ret += fmt::format("{}{}", std::get<1u>(v), !is_last ? ", " : "");
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}(),
|
||||
|
||||
[&]() {
|
||||
std::string ret;
|
||||
|
||||
ret += "?, ";
|
||||
for (const auto &[k, v] : spn_mapping_table) {
|
||||
auto end = spn_mapping_table.end();
|
||||
bool is_last = k == (--end)->first;
|
||||
|
||||
// Split 'data range' to two columns 'min' and 'max'
|
||||
if (std::get<1u>(v) == "data_range") {
|
||||
|
||||
ret += is_last ? ", ?, ?" : "?, ?, ";
|
||||
} else {
|
||||
|
||||
ret += !is_last ? "?, " : "?";
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}());
|
||||
|
||||
ps << pgn_row_map["pgn"];
|
||||
for (const auto &[k, v] : spn_mapping_table) {
|
||||
if (std::get<1u>(v) == "data_range") { // Parse 'data range' string and split it to two columns 'min' and 'max'
|
||||
|
||||
auto range = parsers::parseSpnDataRange(spn_row_map[std::get<1u>(v).data()]);
|
||||
if (range.has_value()) {
|
||||
(spn_settings_calculated.min = range.value().min, spn_settings_calculated.max = range.value().max);
|
||||
|
||||
for (const auto &val : {range.value().min, range.value().max}) {
|
||||
ps << val;
|
||||
}
|
||||
} else {
|
||||
ps << nullptr << nullptr;
|
||||
}
|
||||
} else if (std::get<1u>(v) == "offset") {
|
||||
|
||||
auto offset = parsers::parseSpnOffset(spn_row_map[std::get<1u>(v).data()]);
|
||||
if (offset.has_value()) {
|
||||
ps << offset.value().offset;
|
||||
} else {
|
||||
ps << nullptr;
|
||||
}
|
||||
} else if (std::get<1u>(v) == "spn_length") {
|
||||
|
||||
auto size = parsers::parseSpnSize(spn_row_map[std::get<1u>(v).data()]);
|
||||
spn_settings_calculated.size_bits = size.has_value() ? size.value().size_bits : 0u;
|
||||
ps << (size.has_value() ? size.value().size_bits : 0u);
|
||||
} else if (std::get<1u>(v) == "resolution") {
|
||||
double calculated = (spn_settings_calculated.max - spn_settings_calculated.min) / (std::pow(2.0f, spn_settings_calculated.size_bits) - 1u);
|
||||
|
||||
// If is discrete -- use calculated resolution
|
||||
if (std::fabs(calculated - 1.0) < 1e-9) {
|
||||
|
||||
ps << calculated;
|
||||
} else {
|
||||
|
||||
auto resolution = parsers::parseSpnResolution(spn_row_map[std::get<1u>(v).data()]);
|
||||
ps << (resolution.has_value() ? resolution.value().resolution : 1.0f);
|
||||
}
|
||||
} else if (std::get<1u>(v) == "spn_pos") {
|
||||
ps << spn_row_map[std::get<1u>(v).data()];
|
||||
} else {
|
||||
|
||||
ps << spn_row_map[std::get<1u>(v).data()];
|
||||
}
|
||||
}
|
||||
|
||||
ps.execute();
|
||||
|
||||
// Insert SPN fragments after successful spns INSERT
|
||||
if (!spn_settings_calculated.parts_inserted_flag) {
|
||||
auto size = parsers::parseSpnSize(spn_row_map[std::get<1u>(spn_mapping_table.at("SPN Length")).data()]);
|
||||
|
||||
if (size.has_value()) {
|
||||
auto spn_fragments = parsers::parseSpnPosition(size.value().size_bits, spn_row_map[std::get<1u>(spn_mapping_table.at("SPN Position in PG")).data()]);
|
||||
|
||||
if (spn_fragments.has_value()) {
|
||||
auto spn = std::stoll(spn_row_map["spn"]);
|
||||
for (const auto &part : spn_fragments.value().spn_fragments) {
|
||||
db << R"(INSERT OR REPLACE INTO spn_fragments (spn, pgn, byte_offset, bit_offset, size) VALUES (?, ?, ?, ?, ?);)" << spn << std::stoll(pgn_row_map["pgn"])
|
||||
<< part.byte_offset << part.bit_offset << part.size;
|
||||
}
|
||||
|
||||
spn_settings_calculated.parts_inserted_flag = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (const sqlite::sqlite_exception &e) {
|
||||
if (e.get_extended_code() == SQLITE_CONSTRAINT_UNIQUE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return db_ptr;
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
--- a/src/ftxui/component/container.cpp
|
||||
+++ b/src/ftxui/component/container.cpp
|
||||
@@ -105,7 +105,7 @@
|
||||
}
|
||||
if (elements.empty()) {
|
||||
- return text("Empty container") | reflect(box_);
|
||||
+ return text("") | reflect(box_);
|
||||
}
|
||||
return vbox(std::move(elements)) | reflect(box_);
|
||||
}
|
||||
@@ -189,7 +189,7 @@
|
||||
}
|
||||
if (elements.empty()) {
|
||||
- return text("Empty container");
|
||||
+ return text("");
|
||||
}
|
||||
return hbox(std::move(elements));
|
||||
}
|
||||
@@ -223,7 +223,7 @@
|
||||
if (active_child) {
|
||||
return active_child->Render();
|
||||
}
|
||||
- return text("Empty container");
|
||||
+ return text("");
|
||||
}
|
||||
|
||||
bool Focusable() const override {
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
diff --git a/include/ftxui/component/component_options.hpp b/include/ftxui/component/component_options.hpp
|
||||
index d387fad..dd9173c 100644
|
||||
--- a/include/ftxui/component/component_options.hpp
|
||||
+++ b/include/ftxui/component/component_options.hpp
|
||||
@@ -261,6 +261,7 @@ struct WindowOptions {
|
||||
Ref<bool> resize_right = true; ///< Can the right side be resized?
|
||||
Ref<bool> resize_top = true; ///< Can the top side be resized?
|
||||
Ref<bool> resize_down = true; ///< Can the down side be resized?
|
||||
+ Ref<bool> draggable = false; ///< Can do window dragging?
|
||||
|
||||
/// An optional function to customize how the window looks like:
|
||||
std::function<Element(const WindowRenderState&)> render;
|
||||
diff --git a/include/ftxui/dom/elements.hpp b/include/ftxui/dom/elements.hpp
|
||||
index 63d5bf0..c8e5445 100644
|
||||
--- a/include/ftxui/dom/elements.hpp
|
||||
+++ b/include/ftxui/dom/elements.hpp
|
||||
@@ -87,7 +87,7 @@ Decorator borderStyled(BorderStyle);
|
||||
Decorator borderStyled(BorderStyle, Color);
|
||||
Decorator borderStyled(Color);
|
||||
Decorator borderWith(const Pixel&);
|
||||
-Element window(Element title, Element content, BorderStyle border = ROUNDED);
|
||||
+Element window(Element title, Element content, BorderStyle border = ROUNDED, std::optional<Color> color = std::nullopt);
|
||||
Element spinner(int charset_index, size_t image_index);
|
||||
Element paragraph(const std::string& text);
|
||||
Element paragraphAlignLeft(const std::string& text);
|
||||
diff --git a/src/ftxui/component/window.cpp b/src/ftxui/component/window.cpp
|
||||
index 7690781..7095e91 100644
|
||||
--- a/src/ftxui/component/window.cpp
|
||||
+++ b/src/ftxui/component/window.cpp
|
||||
@@ -256,7 +256,7 @@ class WindowImpl : public ComponentBase, public WindowOptions {
|
||||
drag_start_y = event.mouse().y - top() - box_.y_min;
|
||||
|
||||
// Drag only if we are not resizeing a border yet:
|
||||
- drag_ = !resize_right_ && !resize_down_ && !resize_top_ && !resize_left_;
|
||||
+ drag_ = (!resize_right_ && !resize_down_ && !resize_top_ && !resize_left_) && *draggable;
|
||||
return true;
|
||||
}
|
||||
|
||||
diff --git a/src/ftxui/dom/border.cpp b/src/ftxui/dom/border.cpp
|
||||
index eb793b2..595b6e4 100644
|
||||
--- a/src/ftxui/dom/border.cpp
|
||||
+++ b/src/ftxui/dom/border.cpp
|
||||
@@ -109,11 +109,6 @@ class Border : public Node {
|
||||
p4.automerge = true;
|
||||
}
|
||||
|
||||
- // Draw title.
|
||||
- if (children_.size() == 2) {
|
||||
- children_[1]->Render(screen);
|
||||
- }
|
||||
-
|
||||
// Draw the border color.
|
||||
if (foreground_color_) {
|
||||
for (int x = box_.x_min; x <= box_.x_max; ++x) {
|
||||
@@ -125,6 +120,11 @@ class Border : public Node {
|
||||
screen.PixelAt(box_.x_max, y).foreground_color = *foreground_color_;
|
||||
}
|
||||
}
|
||||
+
|
||||
+ // Draw title.
|
||||
+ if (children_.size() == 2) {
|
||||
+ children_[1]->Render(screen);
|
||||
+ }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -504,8 +504,8 @@ Element borderEmpty(Element child) {
|
||||
/// │content│
|
||||
/// └───────┘
|
||||
/// ```
|
||||
-Element window(Element title, Element content, BorderStyle border) {
|
||||
+ Element window(Element title, Element content, BorderStyle border, std::optional<Color> color) {
|
||||
return std::make_shared<Border>(unpack(std::move(content), std::move(title)),
|
||||
- border);
|
||||
+ border, color);
|
||||
}
|
||||
} // namespace ftxui
|
||||
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue