harden candump parser, detect bus errors, classify SPN value encoding

This commit is contained in:
oleg 2026-04-14 11:18:05 +03:00
parent 0d2668bac5
commit 66fc4aeb37
9 changed files with 362 additions and 138 deletions

View File

@ -70,8 +70,11 @@ make docker-run ARGS='-e "candump can0" -j1939-xlsx thirdparty/j1939da_2018.xlsx
# TUI mode - remote CAN interface via SSH (no data if will ask password - use public key access or sshpass utility) # TUI mode - remote CAN interface via SSH (no data if will ask password - use public key access or sshpass utility)
make docker-run ARGS='-e "ssh user@remote candump can0" -j1939-xlsx thirdparty/j1939da_2018.xlsx' make docker-run ARGS='-e "ssh user@remote candump can0" -j1939-xlsx thirdparty/j1939da_2018.xlsx'
# Headless mode - create report about collected PGNs and SPNs # Discover mode - find out what PGNs/SPNs are on the bus
make docker-run ARGS='-hl -e "candump can0" -j1939-xlsx thirdparty/j1939da_2018.xlsx -of output.json' make docker-run ARGS='-discover -e "candump can0" -j1939-xlsx thirdparty/j1939da_2018.xlsx'
# Headless mode - stream decoded values
make docker-run ARGS='-hl -e "candump can0" -j1939-xlsx thirdparty/j1939da_2018.xlsx'
``` ```
### Cross-compile for arm64 ### Cross-compile for arm64
@ -85,28 +88,37 @@ Requires Docker. SSH keys from `~/.ssh` and `/etc/hosts` are forwarded into the
## Usage ## Usage
All operating modes are mutually exclusive. If none is specified, TUI mode is used.
```bash ```bash
# TUI mode (default) # TUI mode (default) — interactive terminal interface
canscope -e "candump can0" -j1939-xlsx thirdparty/j1939da_2018.xlsx canscope -e "candump can0" -j1939-xlsx thirdparty/j1939da_2018.xlsx
# Headless - JSON to stdout # Discover mode — output PGN/SPN structure (no values) to stdout or file
canscope -discover -e "candump can0" -j1939-xlsx thirdparty/j1939da_2018.xlsx
canscope -discover -of discovered.json -e "candump can0" -j1939-csv thirdparty/j1939da_2018.csv
# Headless mode — stream all decoded values (NDJSON) to stdout
canscope -hl -e "candump can0" -j1939-xlsx thirdparty/j1939da_2018.xlsx canscope -hl -e "candump can0" -j1939-xlsx thirdparty/j1939da_2018.xlsx
# Headless - JSON to file # Record mode — write all decoded values + timestamps to SQLite
canscope -hl -e "candump can0" -j1939-xlsx thirdparty/j1939da_2018.xlsx -of output.json
# Read from stdin (pipe)
candump can0 | canscope -j1939-xlsx thirdparty/j1939da_2018.xlsx
# Record to SQLite database
canscope -rec -db recording.db -e "candump can0" -j1939-xlsx thirdparty/j1939da_2018.xlsx canscope -rec -db recording.db -e "candump can0" -j1939-xlsx thirdparty/j1939da_2018.xlsx
# Record + TUI # Read from stdin (pipe)
canscope -rec -db recording.db -tui -e "candump can0" -j1939-xlsx thirdparty/j1939da_2018.xlsx candump can0 | canscope -j1939-csv thirdparty/j1939da_2018.csv
``` ```
> **Note:** J1939 decoding has only been tested with the Digital Annex 2018 edition. Other editions may work but are not guaranteed. > **Note:** J1939 decoding has only been tested with the Digital Annex 2018 edition. Other editions may work but are not guaranteed.
### Modes
| Mode | Flag | Description |
|------|------|-------------|
| TUI | *(default)* | Interactive full-screen terminal UI |
| Discover | `-discover` | Output PGN/SPN structure (no values) to stdout or file (`-of`) |
| Headless | `-hl` | Stream all decoded PGN/SPN values as NDJSON to stdout |
| Record | `-rec` | Write all decoded PGN/SPN values + timestamps to SQLite (`-db`) |
### CLI flags ### CLI flags
| Flag | Long form | Description | | Flag | Long form | Description |
@ -114,11 +126,11 @@ canscope -rec -db recording.db -tui -e "candump can0" -j1939-xlsx thirdparty/j19
| `-j1939-xlsx` | | J1939 Digital Annex xlsx file | | `-j1939-xlsx` | | J1939 Digital Annex xlsx file |
| `-j1939-csv` | | J1939 Digital Annex csv file (faster parsing) | | `-j1939-csv` | | J1939 Digital Annex csv file (faster parsing) |
| `-e` | `--execute-command` | Command to read CAN frames from (e.g. `"candump can0"`) | | `-e` | `--execute-command` | Command to read CAN frames from (e.g. `"candump can0"`) |
| `-hl` | `--headless` | Headless mode (no TUI) | | `-discover` | | Discover mode |
| `-of` | `--output-file` | Output file path (headless mode) | | `-hl` | `--headless` | Headless mode |
| `-rec` | `--record` | Record decoded values to SQLite | | `-rec` | `--record` | Record mode |
| `-of` | `--output-file` | Output file path (used with `-discover`) |
| `-db` | `--database` | SQLite database path (required with `-rec`) | | `-db` | `--database` | SQLite database path (required with `-rec`) |
| `-tui` | | Show TUI alongside recording |
| `-h` | `--help` | Show help | | `-h` | `--help` | Show help |
## Roadmap ## Roadmap

View File

@ -1,5 +1,6 @@
#pragma once #pragma once
#include <atomic>
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
@ -10,9 +11,14 @@
// Mutex protecting the J1939 SQLite database from concurrent access // Mutex protecting the J1939 SQLite database from concurrent access
extern std::mutex g_j1939_db_mtx; extern std::mutex g_j1939_db_mtx;
// Count of CAN error frames seen on the bus (SocketCAN ERRORFRAME markers)
extern std::atomic<uint64_t> g_error_frame_count;
struct can_frame_data_s { struct can_frame_data_s {
std::vector<uint8_t> payload; std::vector<uint8_t> payload;
int32_t size = 0; int32_t size = 0;
bool is_error_frame = false;
bool is_remote_request = false;
}; };
struct can_frame_diff_s { struct can_frame_diff_s {

View File

@ -11,6 +11,7 @@
std::pair<nlohmann::json, nlohmann::json> processFrame(sqlite::database &db, const std::string &iface, std::pair<nlohmann::json, nlohmann::json> processFrame(sqlite::database &db, const std::string &iface,
const std::string &canid, const std::vector<uint8_t> &data) { const std::string &canid, const std::vector<uint8_t> &data) {
nlohmann::json verbose, brief; 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 = ?;)" 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 {
int32_t ret; int32_t ret;
@ -29,11 +30,12 @@ std::pair<nlohmann::json, nlohmann::json> processFrame(sqlite::database &db, con
// Get SPNs // Get SPNs
nlohmann::json::array_t spns_array; 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 = ?;)" 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, value_encoding FROM spns WHERE pgn = ?;)"
<< pgn_int >> << pgn_int >>
[&](int32_t spn, const std::string &spn_name, const std::string &spn_pos, int32_t spn_length, double resolution, [&](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, 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) { const std::string &slot_id, const std::string &slot_name, const std::string &spn_type,
const std::string &value_encoding) {
nlohmann::json spn_json = { nlohmann::json spn_json = {
{"SPN (integer)", spn}, {"SPN (integer)", spn},
{"SPN name", spn_name}, {"SPN name", spn_name},
@ -46,15 +48,33 @@ std::pair<nlohmann::json, nlohmann::json> processFrame(sqlite::database &db, con
{"Unit", unit}, {"Unit", unit},
{"SLOT id", slot_id}, {"SLOT id", slot_id},
{"SPN type", spn_type}, {"SPN type", spn_type},
{"Encoding", value_encoding},
}; };
// Get parts // Get parts
size_t result = 0u, iter = 0u, total_size_bits = 0u; size_t result = 0u, iter = 0u, total_size_bits = 0u;
std::vector<uint8_t> ascii_bytes;
nlohmann::json::array_t parts_array; nlohmann::json::array_t parts_array;
db << "SELECT byte_offset,bit_offset,size FROM spn_fragments WHERE spn = ? AND pgn = ?" << spn << pgn >> 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) { [&, byte_array = data](int32_t byte_offset, int32_t bit_offset, int32_t size_bits) {
if (value_encoding == "ascii") {
// ASCII SPNs are byte-aligned; collect raw bytes directly — size may exceed 64 bits.
const size_t nbytes = size_bits / UINT8_WIDTH;
for (size_t i = 0; i < nbytes && (byte_offset + i) < byte_array.size(); ++i) {
ascii_bytes.push_back(byte_array[byte_offset + i]);
}
parts_array.push_back(nlohmann::json::parse(fmt::format(
R"({{"{}":{{"byte_offset":{},"bit_offset":{},"size_bits":{},"parse_result":"ascii"}}}})",
fmt::format("Fragment#{}", iter++), byte_offset, bit_offset, size_bits)));
return;
}
result <<= size_bits; result <<= size_bits;
total_size_bits += size_bits; total_size_bits += size_bits;
for (uint32_t i = 0; i < ((size_bits / UINT8_WIDTH) + (size_bits % UINT8_WIDTH ? 1 : 0)); ++i) { 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]; const uint8_t &byte = byte_array[byte_offset + i];
result |= (((byte << (i * UINT8_WIDTH)) & result |= (((byte << (i * UINT8_WIDTH)) &
@ -72,9 +92,20 @@ std::pair<nlohmann::json, nlohmann::json> processFrame(sqlite::database &db, con
fmt::format("{:#x}", result)))); fmt::format("{:#x}", result))));
}; };
double result_real = result * resolution + offset;
spn_json["Fragments"] = parts_array; spn_json["Fragments"] = parts_array;
spn_json["Value"] = result_real; if (value_encoding == "ascii") {
std::string text;
text.reserve(ascii_bytes.size());
for (uint8_t b : ascii_bytes) {
text += (b >= 0x20 && b < 0x7f) ? static_cast<char>(b) : '.';
}
spn_json["Value"] = text;
} else {
spn_json["Value"] = result * resolution + offset;
}
spns_array.push_back(spn_json); spns_array.push_back(spn_json);
}; };
@ -90,8 +121,17 @@ std::pair<nlohmann::json, nlohmann::json> processFrame(sqlite::database &db, con
nlohmann::json::array_t spns_array; nlohmann::json::array_t spns_array;
for (const auto &spn : verbose["SPNs"]) { for (const auto &spn : verbose["SPNs"]) {
spns_array.push_back(fmt::format("{}: {:.6g} {}", spn["SPN name"].get<std::string>(), spn["Value"].get<double>(), const auto name = spn["SPN name"].get<std::string>();
spn["Unit"].get<std::string>())); const auto unit = spn["Unit"].get<std::string>();
const auto encoding = spn.value("Encoding", std::string{"numeric"});
if (encoding == "ascii") {
spns_array.push_back(fmt::format("{}: \"{}\"", name, spn["Value"].get<std::string>()));
} else if (encoding == "binary") {
spns_array.push_back(fmt::format("{}: {}", name, spn["Value"].get<double>() != 0.0 ? "yes" : "no"));
} else {
spns_array.push_back(fmt::format("{}: {:.6g} {}", name, spn["Value"].get<double>(), unit));
}
} }
brief["SPNs"] = spns_array; brief["SPNs"] = spns_array;
@ -103,26 +143,25 @@ std::pair<nlohmann::json, nlohmann::json> processFrame(sqlite::database &db, con
nlohmann::json verboseToExportJson(const nlohmann::json &verbose) { nlohmann::json verboseToExportJson(const nlohmann::json &verbose) {
nlohmann::json::array_t spns; nlohmann::json::array_t spns;
if (verbose.is_null() || !verbose.contains("SPNs")) return spns; if (verbose.is_null() || !verbose.contains("SPNs"))
return spns;
std::string pgn = verbose.contains("PGN") ? verbose["PGN"].get<std::string>() : ""; std::string pgn = verbose.contains("PGN") ? verbose["PGN"].get<std::string>() : "";
for (const auto &v : verbose["SPNs"]) { for (const auto &v : verbose["SPNs"]) {
nlohmann::json spn = { nlohmann::json spn = {
{"name", v.value("SPN name", "")}, {"name", v.value("SPN name", "")}, {"offset", v.value("Offset", 0)},
{"offset", v.value("Offset", 0)}, {"resolution", v.value("Resolution", 0.0)}, {"max", v.value("Maximum value", 0.0)},
{"resolution", v.value("Resolution", 0.0)}, {"min", v.value("Minimum value", 0.0)}, {"pgn", pgn},
{"max", v.value("Maximum value", 0.0)}, {"value", v.value("Value", 0.0)}, {"unit", v.value("Unit", "")},
{"min", v.value("Minimum value", 0.0)},
{"pgn", pgn},
{"value", v.value("Value", 0.0)},
{"unit", v.value("Unit", "")},
}; };
nlohmann::json::array_t frags; nlohmann::json::array_t frags;
if (v.contains("Fragments")) { if (v.contains("Fragments")) {
for (const auto &[k, frag] : v["Fragments"].items()) { for (const auto &[k, frag] : v["Fragments"].items()) {
auto frag_key = fmt::format("Fragment#{}", k); auto frag_key = fmt::format("Fragment#{}", k);
if (frag.contains(frag_key)) { if (frag.contains(frag_key)) {
frags.push_back({{ frags.push_back({{
fmt::format("fragment#{}", k), fmt::format("fragment#{}", k),

View File

@ -8,24 +8,23 @@
#include <fmt/format.h> #include <fmt/format.h>
#include <fmt/ranges.h> #include <fmt/ranges.h>
HeadlessHandler::HeadlessHandler(const std::string &output_file) HeadlessHandler::HeadlessHandler(const std::string &output_file) : output_file_(output_file) {}
: output_file_(output_file) {} void HeadlessHandler::onDatabaseReady(sqlite::database &db) { database_ = &db; }
void HeadlessHandler::onDatabaseReady(sqlite::database &db) {
database_ = &db;
}
void HeadlessHandler::onBatch(const std::vector<can_frame_update_s> &batch) { void HeadlessHandler::onBatch(const std::vector<can_frame_update_s> &batch) {
if (!database_) return; 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); 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) { for (const auto &entry : batch) {
const auto &iface = entry.iface; const auto &iface = entry.iface;
const auto &canid = entry.canid; const auto &canid = entry.canid;
const auto &frame_data = entry.data; const auto &frame_data = entry.data;
if (configuration_map_.contains(canid)) continue; if (configuration_map_.contains(canid))
continue;
auto [verbose, brief] = processFrame(*database_, iface, canid, frame_data.payload); auto [verbose, brief] = processFrame(*database_, iface, canid, frame_data.payload);
configuration_map_.insert({ configuration_map_.insert({

View File

@ -39,7 +39,8 @@ void initJ1939Database(sqlite::database &db) {
units TEXT, units TEXT,
slot_id TEXT, slot_id TEXT,
slot_name TEXT, slot_name TEXT,
spn_type TEXT spn_type TEXT,
value_encoding TEXT
); );
)"; )";
@ -67,19 +68,26 @@ std::string buildPgnInsertSql() {
const auto &pgn_mapping = J1939MappingTables::pgn(); const auto &pgn_mapping = J1939MappingTables::pgn();
std::string cols, placeholders; std::string cols, placeholders;
for (const auto &[k, v] : pgn_mapping) { for (const auto &[k, v] : pgn_mapping) {
if (!cols.empty()) { cols += ", "; placeholders += ", "; } if (!cols.empty()) {
cols += ", ";
placeholders += ", ";
}
cols += std::get<1u>(v); cols += std::get<1u>(v);
placeholders += "?"; placeholders += "?";
} }
return fmt::format("INSERT OR REPLACE INTO pgns ({}) VALUES ({})", cols, placeholders); return fmt::format("INSERT OR REPLACE INTO pgns ({}) VALUES ({})", cols, placeholders);
} }
std::string buildSpnInsertSql() { std::string buildSpnInsertSql() {
const auto &spn_mapping = J1939MappingTables::spn(); const auto &spn_mapping = J1939MappingTables::spn();
std::string cols = "pgn", placeholders = "?"; std::string cols = "pgn", placeholders = "?";
for (const auto &[k, v] : spn_mapping) { for (const auto &[k, v] : spn_mapping) {
cols += ", "; cols += ", ";
placeholders += ", "; placeholders += ", ";
if (std::get<1u>(v) == "data_range") { if (std::get<1u>(v) == "data_range") {
cols += "min_value, max_value"; cols += "min_value, max_value";
placeholders += "?, ?"; placeholders += "?, ?";
@ -88,6 +96,10 @@ std::string buildSpnInsertSql() {
placeholders += "?"; placeholders += "?";
} }
} }
// value_encoding is derived from Resolution format, not from a mapped CSV column.
cols += ", value_encoding";
placeholders += ", ?";
return fmt::format("INSERT OR REPLACE INTO spns ({}) VALUES ({})", cols, placeholders); return fmt::format("INSERT OR REPLACE INTO spns ({}) VALUES ({})", cols, placeholders);
} }
@ -102,9 +114,12 @@ void insertJ1939Row(sqlite::database &db, const std::string &pgn_insert_sql, con
for (const auto &[k, v] : pgn_mapping) { for (const auto &[k, v] : pgn_mapping) {
ps << pgn_row_map[std::get<1u>(v).data()]; ps << pgn_row_map[std::get<1u>(v).data()];
} }
ps.execute(); ps.execute();
} catch (const sqlite::sqlite_exception &e) { } catch (const sqlite::sqlite_exception &e) {
if (e.get_extended_code() != SQLITE_CONSTRAINT_UNIQUE) throw; if (e.get_extended_code() != SQLITE_CONSTRAINT_UNIQUE) {
throw;
}
} }
// Insert SPN // Insert SPN
@ -112,6 +127,7 @@ void insertJ1939Row(sqlite::database &db, const std::string &pgn_insert_sql, con
bool parts_inserted_flag = false; bool parts_inserted_flag = false;
double min = 0.0, max = 0.0; double min = 0.0, max = 0.0;
size_t size_bits = 0u; size_t size_bits = 0u;
parsers::resolution_s::type_e encoding = parsers::resolution_s::type_e::numeric;
} calc; } calc;
// Pre-compute size_bits // Pre-compute size_bits
@ -131,6 +147,7 @@ void insertJ1939Row(sqlite::database &db, const std::string &pgn_insert_sql, con
for (const auto &[k, v] : spn_mapping) { for (const auto &[k, v] : spn_mapping) {
if (std::get<1u>(v) == "data_range") { if (std::get<1u>(v) == "data_range") {
auto range = parsers::parseSpnDataRange(spn_row_map[std::get<1u>(v).data()]); auto range = parsers::parseSpnDataRange(spn_row_map[std::get<1u>(v).data()]);
if (range.has_value()) { if (range.has_value()) {
calc.min = range.value().min; calc.min = range.value().min;
calc.max = range.value().max; calc.max = range.value().max;
@ -139,18 +156,23 @@ void insertJ1939Row(sqlite::database &db, const std::string &pgn_insert_sql, con
ps << nullptr << nullptr; ps << nullptr << nullptr;
} }
} else if (std::get<1u>(v) == "offset") { } else if (std::get<1u>(v) == "offset") {
auto offset = parsers::parseSpnOffset(spn_row_map[std::get<1u>(v).data()]); auto offset = parsers::parseSpnOffset(spn_row_map[std::get<1u>(v).data()]);
ps << (offset.has_value() ? offset.value().offset : 0.0); ps << (offset.has_value() ? offset.value().offset : 0.0);
} else if (std::get<1u>(v) == "spn_length") { } else if (std::get<1u>(v) == "spn_length") {
auto size = parsers::parseSpnSize(spn_row_map[std::get<1u>(v).data()]); auto size = parsers::parseSpnSize(spn_row_map[std::get<1u>(v).data()]);
calc.size_bits = size.has_value() ? size.value().size_bits : 0u; calc.size_bits = size.has_value() ? size.value().size_bits : 0u;
ps << calc.size_bits; ps << calc.size_bits;
} else if (std::get<1u>(v) == "resolution") { } else if (std::get<1u>(v) == "resolution") {
auto resolution = parsers::parseSpnResolution(spn_row_map[std::get<1u>(v).data()]);
calc.encoding = resolution.has_value() ? resolution.value().type : parsers::resolution_s::type_e::numeric;
double calculated = (calc.max - calc.min) / (std::pow(2.0, calc.size_bits) - 1.0); double calculated = (calc.max - calc.min) / (std::pow(2.0, calc.size_bits) - 1.0);
if (std::fabs(calculated - 1.0) < 1e-9) { if (std::fabs(calculated - 1.0) < 1e-9) {
ps << calculated; ps << calculated;
} else { } else {
auto resolution = parsers::parseSpnResolution(spn_row_map[std::get<1u>(v).data()]);
ps << (resolution.has_value() ? resolution.value().resolution : 1.0); ps << (resolution.has_value() ? resolution.value().resolution : 1.0);
} }
} else { } else {
@ -158,26 +180,33 @@ void insertJ1939Row(sqlite::database &db, const std::string &pgn_insert_sql, con
} }
} }
ps << parsers::resolutionTypeName(calc.encoding);
ps.execute(); ps.execute();
// Insert SPN fragments // Insert SPN fragments
if (!calc.parts_inserted_flag) { if (!calc.parts_inserted_flag) {
auto size = parsers::parseSpnSize(spn_row_map[std::get<1u>(spn_mapping.at("SPN Length")).data()]); auto size = parsers::parseSpnSize(spn_row_map[std::get<1u>(spn_mapping.at("SPN Length")).data()]);
if (size.has_value()) { if (size.has_value()) {
auto spn_fragments = parsers::parseSpnPosition( auto spn_fragments = parsers::parseSpnPosition(
size.value().size_bits, spn_row_map[std::get<1u>(spn_mapping.at("SPN Position in PG")).data()]); size.value().size_bits, spn_row_map[std::get<1u>(spn_mapping.at("SPN Position in PG")).data()]);
if (spn_fragments.has_value()) { if (spn_fragments.has_value()) {
auto spn = std::stoll(spn_row_map["spn"]); auto spn = std::stoll(spn_row_map["spn"]);
for (const auto &part : spn_fragments.value().spn_fragments) { 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 (?, ?, ?, ?, ?);)" 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 << std::stoll(pgn_row_map["pgn"]) << part.byte_offset << part.bit_offset << part.size;
} }
calc.parts_inserted_flag = true; calc.parts_inserted_flag = true;
} }
} }
} }
} catch (const sqlite::sqlite_exception &e) { } catch (const sqlite::sqlite_exception &e) {
if (e.get_extended_code() != SQLITE_CONSTRAINT_UNIQUE) throw; if (e.get_extended_code() != SQLITE_CONSTRAINT_UNIQUE) {
throw;
}
} }
} }
} }

View File

@ -1,5 +1,6 @@
#include <atomic> #include <atomic>
#include <boost/signals2.hpp> #include <boost/signals2.hpp>
#include <cctype>
#include <charconv> #include <charconv>
#include <cstdint> #include <cstdint>
#include <cstdio> #include <cstdio>
@ -7,7 +8,7 @@
#include <map> #include <map>
#include <memory> #include <memory>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <sstream> #include <ranges>
#include <stop_token> #include <stop_token>
#include <string> #include <string>
#include <sys/epoll.h> #include <sys/epoll.h>
@ -31,12 +32,14 @@
#include "ftxui/component/screen_interactive.hpp" #include "ftxui/component/screen_interactive.hpp"
#include "ftxui/dom/elements.hpp" #include "ftxui/dom/elements.hpp"
#include "headless.hpp" #include "headless.hpp"
#include "headless_streamer.hpp"
#include "process.hpp" #include "process.hpp"
#include "recorder.hpp" #include "recorder.hpp"
#include "signals.hpp" #include "signals.hpp"
#include <clipp.h> #include <clipp.h>
std::mutex g_j1939_db_mtx; std::mutex g_j1939_db_mtx;
std::atomic<uint64_t> g_error_frame_count{0};
int32_t main(int32_t argc, char *argv[]) { int32_t main(int32_t argc, char *argv[]) {
static auto screen = ftxui::ScreenInteractive::Fullscreen(); static auto screen = ftxui::ScreenInteractive::Fullscreen();
@ -52,10 +55,13 @@ int32_t main(int32_t argc, char *argv[]) {
static std::unique_ptr<sqlite::database> j1939_db_owner; static std::unique_ptr<sqlite::database> j1939_db_owner;
static std::unique_ptr<Recorder> recorder; static std::unique_ptr<Recorder> recorder;
static std::unique_ptr<HeadlessHandler> headless_handler; static std::unique_ptr<HeadlessHandler> headless_handler;
static std::unique_ptr<HeadlessStreamer> headless_streamer;
enum class Mode { tui, discover, record, headless } mode = Mode::tui;
static struct { static struct {
std::string xlsx_file, csv_file, command = "", output_file = "", record_db_path = ""; std::string xlsx_file, csv_file, command = "", output_file = "", record_db_path = "";
bool show_help = false, headless_mode = false, sync_to_server = false, record_mode = false, tui_mode = false; bool show_help = false;
} cli_opts; } cli_opts;
// Parse cli options // Parse cli options
@ -66,42 +72,39 @@ int32_t main(int32_t argc, char *argv[]) {
auto cli = ( auto cli = (
clipp::option("-hl", "--headless") clipp::option("-dscvr, --discovery-mode")
.doc("Headless mode, write configuration to stdout if output file is not specified") .doc("Discover mode: output PGN/SPN structure (only first received falue) to stdout or file")
.set(cli_opts.headless_mode) .call([&]() { mode = Mode::discover; }),
.call([]() { fmt::println("Headless mode"); }),
clipp::option("-of", "--output-file") & clipp::option("-hl", "--headless")
clipp::value("Output file to write configuration", cli_opts.output_file) .doc("Headless mode: stream all decoded PGN/SPN values to stdout")
.call([&]() { fmt::println("Output file is: {}", cli_opts.output_file); }) .call([&]() { mode = Mode::headless; }),
.doc("Specify output file to write configuration"),
clipp::option("-rec", "--record") clipp::option("-rec", "--record")
.doc("Record mode: collect decoded J1939 SPN values to SQLite DB") .doc("Record mode: write all decoded PGN/SPN values + timestamps to SQLite DB")
.set(cli_opts.record_mode) .call([&]() { mode = Mode::record; }),
.call([]() { fmt::println("Record mode"); }),
clipp::option("-db", "--database") & clipp::option("-of", "--output-file") &
clipp::value("SQLite output database path", cli_opts.record_db_path) clipp::value("Output file", cli_opts.output_file).doc("Output file path (used with -discover)"),
.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("-db", "--database") & clipp::value("SQLite output database path", cli_opts.record_db_path)
.doc("SQLite database path (used with -rec)"),
clipp::option("-e", "--execute-command") & clipp::option("-e", "--execute-command") &
clipp::value("command", cli_opts.command).call([&]() {}).doc("execute cli command to read its output"), clipp::value("command", cli_opts.command).call([&]() {}).doc("execute cli command to read its output"),
(clipp::option("-j1939-xlsx") & (clipp::option("-j1939-xlsx") &
clipp::value("J1939 XLSX file", cli_opts.xlsx_file) clipp::value("J1939 XLSX file", cli_opts.xlsx_file)
.call([&]() { .call([&]() {
j1939_parser_task = std::async(std::launch::async, [&]() { j1939_parser_task = std::async(std::launch::async, [&]() {
j1939_db_owner = parseXlsx(cli_opts.xlsx_file); j1939_db_owner = parseXlsx(cli_opts.xlsx_file);
j1939_db.store(j1939_db_owner.get()); j1939_db.store(j1939_db_owner.get());
signals.map.get<void(sqlite::database &)>("j1939_database_ready")->operator()(*j1939_db_owner); signals.map.get<void(sqlite::database &)>("j1939_database_ready")->operator()(*j1939_db_owner);
}); });
}) })
.doc("J1939 Digital Annex .xlsx file")) | .doc("J1939 Digital Annex .xlsx file")) |
(clipp::option("-j1939-csv") &
(clipp::option("-j1939-csv") &
clipp::value("J1939 CSV file", cli_opts.csv_file) clipp::value("J1939 CSV file", cli_opts.csv_file)
.call([&]() { .call([&]() {
j1939_parser_task = std::async(std::launch::async, [&]() { j1939_parser_task = std::async(std::launch::async, [&]() {
@ -122,7 +125,7 @@ int32_t main(int32_t argc, char *argv[]) {
return -1; return -1;
} }
if (cli_opts.record_mode && cli_opts.record_db_path.empty()) { if (mode == Mode::record && cli_opts.record_db_path.empty()) {
fmt::println(stderr, "Error: -rec requires -db <path>"); fmt::println(stderr, "Error: -rec requires -db <path>");
return -1; return -1;
} }
@ -130,44 +133,105 @@ int32_t main(int32_t argc, char *argv[]) {
// Parse a single candump line and aggregate it // Parse a single candump line and aggregate it
static const auto parse_candump_line = [](const std::string &line) { static const auto parse_candump_line = [](const std::string &line) {
if (line.empty()) enum class field_e : size_t {
INTERFACE = 0,
CANID = 1,
DLC = 2,
PAYLOAD_BEGIN = 3,
};
constexpr auto idx = [](enum field_e f) consteval { return static_cast<size_t>(f); };
if (line.empty()) {
return; return;
}
std::vector<std::string> words; std::vector<std::string_view> words;
std::istringstream iss(line); for (auto part : std::string_view(line) | std::views::split(' ')) {
std::string s; if (!part.empty()) {
words.emplace_back(part.begin(), part.end());
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.size() > idx(field_e::PAYLOAD_BEGIN)) {
if (words[0].starts_with("can") || words[0].starts_with("vcan")) { auto &iface = words[idx(field_e::INTERFACE)];
can_frame_data_s entry; can_frame_data_s entry;
auto &canid = words[idx(field_e::CANID)];
// Parse DLC // Validate CAN ID: 3 hex digits (SFF, 11-bit) or 8 (EFF, 29-bit)
{ {
auto sv = std::string_view(words[2]).substr(1, words[2].size() - 2); constexpr auto sff_length_bytes = 3u, eff_length_bytes = 8u;
auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), entry.size);
if (ec != std::errc{}) { // Check CAN_ID length
entry.size = 0; if (canid.size() != sff_length_bytes && canid.size() != eff_length_bytes) {
return;
}
// Check CAN_ID symbols
for (const char &c : canid) {
if (!std::isxdigit(static_cast<uint8_t>(c))) {
return;
} }
} }
}
// Parse payload bytes directly // Parse DLC
entry.payload.reserve(words.size() - 3); {
for (size_t i = 3; i < words.size(); ++i) { auto &dlc = words[idx(field_e::DLC)];
uint8_t byte = 0; if (dlc.size() < 3 /* [${size}] format */ || dlc.front() != '[' || dlc.back() != ']') {
std::from_chars(words[i].data(), words[i].data() + words[i].size(), byte, 16); return;
entry.payload.push_back(byte);
} }
{ auto sv = dlc.substr(1, dlc.size() - 2);
std::lock_guard<std::mutex> lock(rw_mtx); if (auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), entry.size);
aggregated[words[0]][words[1]] = std::make_shared<can_frame_data_s>(std::move(entry)); ec != std::errc{} || ptr != sv.data() + sv.size()) {
return;
} }
// Max payload per CAN frame: 8 (Classic CAN) / 64 (CAN FD). Use 64 as upper bound.
constexpr int32_t max_payload_bytes = 64;
if (entry.size < 0 || entry.size > max_payload_bytes) {
return;
}
}
// Detect ERRORFRAME marker (SocketCAN diagnostic pseudo-frame): count and drop
if (words.back() == "ERRORFRAME") {
g_error_frame_count.fetch_add(1, std::memory_order_relaxed);
return;
}
// Detect RTR: candump prints "remote request" in place of payload bytes. Drop silently.
if (words[idx(field_e::PAYLOAD_BEGIN)] == "remote") {
return;
}
// Parse payload bytes directly
entry.payload.reserve(words.size() - idx(field_e::PAYLOAD_BEGIN));
for (size_t i = idx(field_e::PAYLOAD_BEGIN); i < words.size(); ++i) {
if (words[i].size() != 2) { // each byte must be exactly 2 hex digits
return;
}
uint8_t byte = 0;
auto *first = words[i].data();
auto *last = first + words[i].size();
if (auto [ptr, ec] = std::from_chars(first, last, byte, 16 /* HEX format */);
ec != std::errc{} || ptr != last) {
return;
}
entry.payload.push_back(byte);
}
// DLC must match the actual number of payload bytes
if (entry.payload.size() != entry.size) {
return;
}
{
std::lock_guard<std::mutex> lock(rw_mtx);
aggregated[std::string(iface)][std::string(canid)] = std::make_shared<can_frame_data_s>(std::move(entry));
} }
} }
}; };
@ -353,14 +417,13 @@ int32_t main(int32_t argc, char *argv[]) {
::signal(SIGINT, signal_handler); ::signal(SIGINT, signal_handler);
} }
if (cli_opts.record_mode) { if (mode == Mode::record) {
bool rec_console = !cli_opts.tui_mode && !cli_opts.headless_mode; recorder = std::make_unique<Recorder>(cli_opts.record_db_path, true);
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") 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); }); ->connect([](const std::vector<can_frame_update_s> &batch) { recorder->onBatch(batch); });
} }
if (cli_opts.headless_mode) { if (mode == Mode::discover) {
headless_handler = std::make_unique<HeadlessHandler>(cli_opts.output_file); headless_handler = std::make_unique<HeadlessHandler>(cli_opts.output_file);
signals.map.get<void(sqlite::database &)>("j1939_database_ready")->connect([](sqlite::database &db) { signals.map.get<void(sqlite::database &)>("j1939_database_ready")->connect([](sqlite::database &db) {
@ -371,8 +434,13 @@ int32_t main(int32_t argc, char *argv[]) {
->connect([](const std::vector<can_frame_update_s> &batch) { headless_handler->onBatch(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) if (mode == Mode::headless) {
bool run_tui = !cli_opts.headless_mode && (!cli_opts.record_mode || cli_opts.tui_mode); headless_streamer = std::make_unique<HeadlessStreamer>();
signals.map.get<void(const std::vector<can_frame_update_s> &)>("new_entries_batch")
->connect([](const std::vector<can_frame_update_s> &batch) { headless_streamer->onBatch(batch); });
}
bool run_tui = (mode == Mode::tui);
if (run_tui) { if (run_tui) {
extern ftxui::Component makeMainForm(ftxui::ScreenInteractive * screen, signals_map_t & smap); extern ftxui::Component makeMainForm(ftxui::ScreenInteractive * screen, signals_map_t & smap);

View File

@ -188,6 +188,15 @@ ftxui::Component makeMainForm(ftxui::ScreenInteractive *screen, signals_map_t &s
fmt::format(" Uptime: {} ", fmt::format("{:02}:{:02}:{:02}", hours, minutes, seconds)))}); fmt::format(" Uptime: {} ", fmt::format("{:02}:{:02}:{:02}", hours, minutes, seconds)))});
}), }),
ftxui::Renderer([]() {
const auto errors = g_error_frame_count.load(std::memory_order_relaxed);
return ftxui::hbox({
ftxui::text(" Errors: "),
ftxui::text(fmt::format("{} ", errors)) |
ftxui::color(errors ? ftxui::Color::Red : ftxui::Color::GrayDark),
});
}),
ftxui::Renderer([]() { return ftxui::separator(); }), ftxui::Renderer([]() { return ftxui::separator(); }),
ftxui::Container::Horizontal({ ftxui::Container::Horizontal({
ftxui::Input({ ftxui::Input({

View File

@ -4,30 +4,40 @@ namespace parsers {
std::optional<struct range_s> parseSpnDataRange(const std::string &str) { std::optional<struct range_s> parseSpnDataRange(const std::string &str) {
struct range_s result; struct range_s result;
auto begin = str.begin(), end = str.end(); 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; 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) { std::optional<struct size_s> parseSpnSize(const std::string &str) {
struct size_s result; struct size_s result;
auto begin = str.begin(), end = str.end(); 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; 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) { std::optional<struct offset_s> parseSpnOffset(const std::string &str) {
struct offset_s result; struct offset_s result;
auto begin = str.begin(), end = str.end(); 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; 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) { std::optional<struct resolution_s> parseSpnResolution(const std::string &str) {
struct resolution_s result; struct resolution_s result;
auto begin = str.begin(), end = str.end(); 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; 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) { std::optional<struct spn_fragments_s> parseSpnPosition(size_t spn_size_bits, const std::string &str) {
struct spn_fragments_s result; struct spn_fragments_s result;
auto begin = str.begin(), end = str.end(); 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; 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 }; // namespace parsers

View File

@ -1,11 +1,11 @@
#pragma once #pragma once
#include <fmt/base.h>
#include <algorithm> #include <algorithm>
#include <boost/bind.hpp> #include <boost/bind.hpp>
#include <boost/spirit/home/qi/numeric/uint.hpp> #include <boost/spirit/home/qi/numeric/uint.hpp>
#include <boost/spirit/include/phoenix.hpp> #include <boost/spirit/include/phoenix.hpp>
#include <boost/spirit/include/qi.hpp> #include <boost/spirit/include/qi.hpp>
#include <fmt/base.h>
#include <optional> #include <optional>
#include <string> #include <string>
@ -43,9 +43,26 @@ struct spn_fragments_s {
// Parse resolution string to this struct // Parse resolution string to this struct
struct resolution_s { struct resolution_s {
double resolution; enum class type_e { numeric, enum_states, binary, ascii };
double resolution = 1.0;
type_e type = type_e::numeric;
}; };
// Map resolution_s::type_e to a lowercase string tag used in SQLite / JSON
inline const char *resolutionTypeName(resolution_s::type_e t) {
switch (t) {
case resolution_s::type_e::numeric:
return "numeric";
case resolution_s::type_e::enum_states:
return "enum";
case resolution_s::type_e::binary:
return "binary";
case resolution_s::type_e::ascii:
return "ascii";
}
return "numeric";
}
namespace _detail { namespace _detail {
template <typename It, typename Res> struct parser_s : qi::grammar<It, Res(), ascii::space_type> { template <typename It, typename Res> struct parser_s : qi::grammar<It, Res(), ascii::space_type> {
parser_s() : parser_s::base_type(rule) {} parser_s() : parser_s::base_type(rule) {}
@ -107,8 +124,9 @@ template <typename It> struct size_parser_s : _detail::parser_s<It, size_s> {
} }
}; };
this->rule = ((qi::uint_ >> "byte") | (qi::uint_ >> "bytes"))[_val = boost::phoenix::function<bytes_to_bits_s>{}(qi::_1)] | this->rule =
((qi::uint_ >> "bit") | (qi::uint_ >> "bits"))[_val = boost::phoenix::function<as_bits_s>{}(qi::_1)]; ((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 size
@ -215,7 +233,8 @@ template <typename It> struct position_parser_s : _detail::parser_s<It, spn_frag
// start byte, last integer byte and last byte with bit offset // start byte, last integer byte and last byte with bit offset
struct rule_v5_handler_s { 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 { 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 { return {
.spn_fragments = .spn_fragments =
{ {
@ -238,7 +257,8 @@ template <typename It> struct position_parser_s : _detail::parser_s<It, spn_frag
// start byte with bit offset, first integer byte and last integer byte // start byte with bit offset, first integer byte and last integer byte
struct rule_v6_handler_s { 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 { spn_fragments_s operator()(uint32_t start_byte, uint32_t bit_offset, uint32_t first_integer_byte,
uint32_t last_byte) const {
return { return {
.spn_fragments = .spn_fragments =
{ {
@ -261,20 +281,29 @@ template <typename It> struct position_parser_s : _detail::parser_s<It, spn_frag
}; };
position_rule_v0 = (qi::uint_)[qi::_val = boost::phoenix::function<rule_v0_handler_s>{}(qi::_1)]; 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_v1 = (qi::uint_ >> '.' >>
position_rule_v2 = (qi::uint_ >> '-' >> qi::uint_)[qi::_val = boost::phoenix::function<rule_v2_handler_s>{}(qi::_1, qi::_2)]; qi::uint_)[qi::_val = boost::phoenix::function<rule_v1_handler_s>{}(size_bits, 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_v2 =
position_rule_v4 = (qi::uint_ >> '.' >> qi::uint_ >> ',' >> qi::uint_)[qi::_val = boost::phoenix::function<rule_v4_handler_s>{}(qi::_1, qi::_2, qi::_3)]; (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_ >> '.' >> 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)]; qi::uint_)[qi::_val = boost::phoenix::function<rule_v5_handler_s>{}(size_bits, qi::_1, qi::_2,
qi::_3, qi::_4)];
position_rule_v6 = 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)]; (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 // 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; 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; 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 position
@ -294,30 +323,51 @@ template <typename It> struct resolution_parser_s : _detail::parser_s<It, resolu
resolution_parser_s() : _detail::parser_s<It, resolution_s>() { resolution_parser_s() : _detail::parser_s<It, resolution_s>() {
struct resolution_rule_v0_handler_s { struct resolution_rule_v0_handler_s {
resolution_s operator()(float number) const { resolution_s operator()(float number) const {
return { return {.resolution = number, .type = resolution_s::type_e::numeric};
.resolution = number,
};
} }
}; };
struct resolution_rule_v1_handler_s { struct resolution_rule_v1_handler_s {
resolution_s operator()(double first, double second) const { resolution_s operator()(double first, double second) const {
return { return {.resolution = first / second, .type = resolution_s::type_e::numeric};
.resolution = first / second,
};
} }
}; };
// `N states/M bit` — enum SPN: raw value is the state index, no scaling.
struct resolution_rule_v2_handler_s {
resolution_s operator()() const { return {.resolution = 1.0, .type = resolution_s::type_e::enum_states}; }
};
// `Binary` — single-bit boolean flag.
struct resolution_rule_binary_handler_s {
resolution_s operator()() const { return {.resolution = 1.0, .type = resolution_s::type_e::binary}; }
};
// `ASCII` — variable/fixed byte sequence to be rendered as text.
struct resolution_rule_ascii_handler_s {
resolution_s operator()() const { return {.resolution = 1.0, .type = resolution_s::type_e::ascii}; }
};
this->strnum = lexeme[-(qi::char_('+') | qi::char_('-')) >> +(qi::digit | qi::char_(',') | qi::char_('.'))]; 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->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_v0 =
resolution_rule_v1 = (qi::uint_ >> '/' >> qi::uint_ >> *qi::char_)[qi::_val = boost::phoenix::function<resolution_rule_v1_handler_s>{}(qi::_1, qi::_2)]; (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)];
resolution_rule_v2 =
(this->num >> "states" >> *qi::char_)[qi::_val = boost::phoenix::function<resolution_rule_v2_handler_s>{}()];
resolution_rule_binary =
qi::lit("Binary")[qi::_val = boost::phoenix::function<resolution_rule_binary_handler_s>{}()];
resolution_rule_ascii = qi::lit("ASCII")[qi::_val = boost::phoenix::function<resolution_rule_ascii_handler_s>{}()];
this->rule = resolution_rule_v1 | resolution_rule_v0; this->rule =
resolution_rule_binary | resolution_rule_ascii | resolution_rule_v2 | resolution_rule_v1 | resolution_rule_v0;
} }
qi::rule<It, resolution_s(), ascii::space_type> resolution_rule_v0, resolution_rule_v1; qi::rule<It, resolution_s(), ascii::space_type> resolution_rule_v0, resolution_rule_v1, resolution_rule_v2,
resolution_rule_binary, resolution_rule_ascii;
}; };
}; // namespace resolution }; // namespace resolution
@ -330,7 +380,9 @@ std::optional<struct resolution_s> parseSpnResolution(const std::string &str);
BOOST_FUSION_ADAPT_STRUCT(parsers::range_s, (double, min)(double, max)(std::string, other)); 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::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::spn_part_s,
BOOST_FUSION_ADAPT_STRUCT(parsers::spn_fragments_s, (std::vector<struct parsers::spn_fragments_s::spn_part_s>, spn_fragments)); (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::offset_s, (double, offset));
BOOST_FUSION_ADAPT_STRUCT(parsers::resolution_s, (double, resolution)); BOOST_FUSION_ADAPT_STRUCT(parsers::resolution_s, (double, resolution));