init commit

This commit is contained in:
oleg 2026-04-09 11:00:31 +03:00
commit dd28afc1ef
45 changed files with 12948 additions and 0 deletions

3
.clang-format Normal file
View File

@ -0,0 +1,3 @@
Language: Cpp
ColumnLimit: 120
IndentPPDirectives: AfterHash

136
CMakeLists.txt Normal file
View File

@ -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})

97
README.md Normal file
View File

@ -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
src/.clangd Normal file
View File

50
src/bitstream.hpp Normal file
View File

@ -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_;
};

34
src/can_data.hpp Normal file
View File

@ -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);

144
src/can_frame.cpp Normal file
View File

@ -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;
}

692
src/can_player_dialog.cpp Normal file
View File

@ -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);
}

657
src/canid_unit.cpp Normal file
View File

@ -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;
}
}
}
}
}

69
src/canid_unit.hpp Normal file
View File

@ -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_ = {};
};

View File

@ -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);
}

7023
src/clipp.hpp Normal file

File diff suppressed because it is too large Load Diff

132
src/filedialog.cpp Normal file
View File

@ -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);
}

193
src/graph.cpp Normal file
View File

@ -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);
}

68
src/headless.cpp Normal file
View File

@ -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());
}
}
}

23
src/headless.hpp Normal file
View File

@ -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_;
};

54
src/json/button.cpp Normal file
View File

@ -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));
}

6
src/json/button.hpp Normal file
View File

@ -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()>);

83
src/json/expander.cpp Normal file
View File

@ -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);
}
}

29
src/json/expander.hpp Normal file
View File

@ -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_;
};

477
src/json/json.cpp Normal file
View File

@ -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);
}

29
src/json/json.hpp Normal file
View File

@ -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);

46
src/json/mytoggle.cpp Normal file
View File

@ -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); }

4
src/json/mytoggle.hpp Normal file
View File

@ -0,0 +1,4 @@
#pragma once
#include "ftxui/component/component.hpp"
ftxui::Component MyToggle(const char *label_on, const char *label_off, bool *state);

446
src/main.cpp Normal file
View File

@ -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;
}

418
src/mainform.cpp Normal file
View File

@ -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);
}

33
src/parsers.cpp Normal file
View File

@ -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

336
src/parsers.hpp Normal file
View File

@ -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));

164
src/recorder.cpp Normal file
View File

@ -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();
}
}
}

47
src/recorder.hpp Normal file
View File

@ -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_;
};

72
src/signals.hpp Normal file
View File

@ -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;

26
src/spnselector.cpp Normal file
View File

@ -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);
}

6
src/tagsettingrow.cpp Normal file
View File

@ -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);
}

93
src/tagsettingrow.hpp Normal file
View File

@ -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_;
};

36
src/tagsettings.hpp Normal file
View File

@ -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>>;

548
src/tagsettingsform.cpp Normal file
View File

@ -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);
}

119
src/tuple.hpp Normal file
View File

@ -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, &currentIndex](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

17
src/types.hpp Normal file
View File

@ -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
}

17
src/utils.cpp Normal file
View File

@ -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

5
src/utils.hpp Normal file
View File

@ -0,0 +1,5 @@
#include "sqlite_modern_cpp.h"
namespace utils {
void backup_db(const sqlite::database &db, const std::string &backup_path);
};

371
src/xlsx.cpp Normal file
View File

@ -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;
}

27
thirdparty/ftxui-empty-container.patch vendored Normal file
View File

@ -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 {

77
thirdparty/ftxui-window.patch vendored Normal file
View File

@ -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

BIN
thirdparty/j1939da_2018.xlsx vendored Normal file

Binary file not shown.

BIN
thirdparty/j1939da_2018_hitachi.xlsx vendored Normal file

Binary file not shown.