diff --git a/build-linux.sh b/build-linux.sh index caa59d1..5fc93fc 100755 --- a/build-linux.sh +++ b/build-linux.sh @@ -1,8 +1,6 @@ #!/usr/bin/env bash -# This script requires clang and g++-13 to be installed: +# This script requires clang to be installed: # sudo apt-get update && sudo apt-get install -y clang -# sudo add-apt-repository ppa:ubuntu-toolchain-r/test -# sudo apt-get install -y g++-13 set -o xtrace -o errexit -o nounset -o pipefail @@ -23,8 +21,6 @@ build_and_test() { ctest --test-dir "$buildDir/${target}-$build_type" } -export CC=gcc-13 -export CXX=g++-13 build_and_test Debug linux-gcc build_and_test Release linux-gcc diff --git a/src/dlms_parser/apdu_handler.cpp b/src/dlms_parser/apdu_handler.cpp index 5568a55..3430090 100644 --- a/src/dlms_parser/apdu_handler.cpp +++ b/src/dlms_parser/apdu_handler.cpp @@ -1,5 +1,4 @@ #include "apdu_handler.h" -#include "log.h" #include "utils.h" #include #include @@ -28,8 +27,8 @@ static bool is_known_tag(const uint8_t b) { case DLMS_APDU_DATA_NOTIFICATION: case DLMS_APDU_GENERAL_GLO_CIPHERING: case DLMS_APDU_GENERAL_DED_CIPHERING: - case DLMS_DATA_TYPE_ARRAY: - case DLMS_DATA_TYPE_STRUCTURE: + case static_cast(DlmsDataType::ARRAY): + case static_cast(DlmsDataType::STRUCTURE): return true; default: return false; @@ -51,9 +50,9 @@ std::span parse_apdu_in_place(std::span buf, Aes128GcmDecrypto const uint8_t tag = buf[0]; // --- Raw AXDR (0x01/0x02): done - if (tag == DLMS_DATA_TYPE_ARRAY || tag == DLMS_DATA_TYPE_STRUCTURE) { + if (tag == static_cast(DlmsDataType::ARRAY) || tag == static_cast(DlmsDataType::STRUCTURE)) { Logger::log(LogLevel::VERBOSE, "Found raw AXDR %s (0x%02X) - no APDU wrapper", - tag == DLMS_DATA_TYPE_ARRAY ? "ARRAY" : "STRUCTURE", tag); + tag == static_cast(DlmsDataType::ARRAY) ? "ARRAY" : "STRUCTURE", tag); return buf; } diff --git a/src/dlms_parser/axdr_parser.cpp b/src/dlms_parser/axdr_parser.cpp index 4e8a96a..9bb741a 100644 --- a/src/dlms_parser/axdr_parser.cpp +++ b/src/dlms_parser/axdr_parser.cpp @@ -1,15 +1,169 @@ #include "axdr_parser.h" -#include "log.h" #include "utils.h" +#include +#include +#include #include #include namespace dlms_parser { +std::string_view AxdrCapture::obis_as_string(std::span buffer) const { + const auto len = snprintf(buffer.data(), buffer.size(), "%u.%u.%u.%u.%u.%u", obis[0], obis[1], obis[2], obis[3], obis[4], obis[5]); + return { buffer.data(), len > 0 ? static_cast(len) : 0 }; +} + +bool AxdrCapture::is_numeric() const { + switch (value_type) { + case DlmsDataType::OCTET_STRING: + case DlmsDataType::STRING: + case DlmsDataType::STRING_UTF8: + case DlmsDataType::DATETIME: + return false; + default: + return true; + } +} + +std::string_view AxdrCapture::value_as_string(std::span buffer) const { + if (value.empty()) return {}; + + auto hex_of = [](const std::span input, const std::span output) { + if (output.empty()) return; + output[0] = '\0'; + size_t pos = 0; + for (size_t i = 0; i < input.size() && pos + 2 < output.size(); i++) { + const int written = snprintf(output.data() + pos, output.size() - pos, "%02x", input[i]); + if (written > 0) pos += static_cast(written); + } + }; + + switch (value_type) { + case DlmsDataType::OCTET_STRING: + case DlmsDataType::STRING: + case DlmsDataType::STRING_UTF8: { + const size_t copy_len = std::min(value.size(), buffer.size() - 1); + std::memcpy(buffer.data(), value.data(), copy_len); + buffer[copy_len] = '\0'; + break; + } + case DlmsDataType::DATETIME: + datetime_to_string(value, buffer); + break; + case DlmsDataType::BIT_STRING: + case DlmsDataType::BINARY_CODED_DECIMAL: + case DlmsDataType::DATE: + case DlmsDataType::TIME: + hex_of(value, buffer); + break; + case DlmsDataType::BOOLEAN: + case DlmsDataType::ENUM: + case DlmsDataType::UINT8: + snprintf(buffer.data(), buffer.size(), "%u", static_cast(value[0])); + break; + case DlmsDataType::INT8: + snprintf(buffer.data(), buffer.size(), "%d", static_cast(static_cast(value[0]))); + break; + case DlmsDataType::UINT16: + if (value.size() >= 2) snprintf(buffer.data(), buffer.size(), "%u", be16(value.data())); + break; + case DlmsDataType::INT16: + if (value.size() >= 2) snprintf(buffer.data(), buffer.size(), "%d", static_cast(be16(value.data()))); + break; + case DlmsDataType::UINT32: + if (value.size() >= 4) snprintf(buffer.data(), buffer.size(), "%" PRIu32, be32(value.data())); + break; + case DlmsDataType::INT32: + if (value.size() >= 4) snprintf(buffer.data(), buffer.size(), "%" PRId32, static_cast(be32(value.data()))); + break; + case DlmsDataType::UINT64: + if (value.size() >= 8) snprintf(buffer.data(), buffer.size(), "%" PRIu64, be64(value.data())); + break; + case DlmsDataType::INT64: + if (value.size() >= 8) snprintf(buffer.data(), buffer.size(), "%" PRId64, static_cast(be64(value.data()))); + break; + case DlmsDataType::FLOAT32: + case DlmsDataType::FLOAT64: { + snprintf(buffer.data(), buffer.size(), "%f", static_cast(value_as_float_with_scaler_applied())); + break; + } + default: + break; + } + + return { buffer.data(), std::strlen(buffer.data()) }; +} + +float AxdrCapture::value_as_float_with_scaler_applied() const { + return apply_scaler(value_as_float(), scaler); +} + +float AxdrCapture::value_as_float() const +{ + if (value.empty()) return 0.0f; + const uint8_t* ptr = value.data(); + const auto len = value.size(); + + switch (value_type) { + case DlmsDataType::BOOLEAN: + case DlmsDataType::ENUM: + case DlmsDataType::UINT8: return ptr[0]; + case DlmsDataType::INT8: return static_cast(ptr[0]); + case DlmsDataType::BIT_STRING: return ptr[0]; + case DlmsDataType::UINT16: return len >= 2 ? static_cast(be16(ptr)) : 0.0f; + case DlmsDataType::INT16: return len >= 2 ? static_cast(static_cast(be16(ptr))) : 0.0f; + case DlmsDataType::UINT32: return len >= 4 ? static_cast(be32(ptr)) : 0.0f; + case DlmsDataType::INT32: return len >= 4 ? static_cast(static_cast(be32(ptr))) : 0.0f; + case DlmsDataType::UINT64: return len >= 8 ? static_cast(be64(ptr)) : 0.0f; + case DlmsDataType::INT64: return len >= 8 ? static_cast(static_cast(be64(ptr))) : 0.0f; + case DlmsDataType::FLOAT32: { + if (len < 4) return 0.0f; + const uint32_t i32 = be32(ptr); + float f; + std::memcpy(&f, &i32, sizeof(float)); + return f; + } + case DlmsDataType::FLOAT64: { + if (len < 8) return 0.0f; + const uint64_t i64 = be64(ptr); + double d; + std::memcpy(&d, &i64, sizeof(double)); + return static_cast(d); + } + default: return 0.0f; + } +} + +float AxdrCapture::apply_scaler(const float value, const int8_t scaler) { + if (scaler == 0) return value; + + // Lookup table for 10^0 through 10^9 + static constexpr float pow10_lut[] = { + 1e0f, 1e1f, 1e2f, 1e3f, 1e4f, 1e5f, 1e6f, 1e7f, 1e8f, 1e9f + }; + + // Fast path: use LUT for typical DLMS bounds (-9 to +9) + if (scaler > 0 && scaler <= 9) { return value * pow10_lut[scaler]; } + if (scaler < 0 && scaler >= -9) { return value / pow10_lut[-scaler]; } + + // Fallback path: loop for unusually large scalers + float multiplier = 1.0f; + if (scaler > 0) { + for (int i = 0; i < scaler; ++i) multiplier *= 10.0f; + return value * multiplier; + } + + for (int i = 0; i < -scaler; ++i) multiplier *= 10.0f; + return value / multiplier; +} + // --------------------------------------------------------------------------- // Construction / pattern registry // --------------------------------------------------------------------------- +AxdrParser::AxdrParser(DlmsDataCallback dlmsDataCallback) : dlmsDataCallback_(std::move(dlmsDataCallback)) +{} + void AxdrParser::register_pattern(const char* name, const char* dsl, const int priority) { this->register_pattern_dsl_(name, dsl, priority); } @@ -28,20 +182,19 @@ void AxdrParser::clear_patterns() { // Public parse entry point // --------------------------------------------------------------------------- -ParseResult AxdrParser::parse(const std::span axdr, DlmsDataCallback cooked_cb) { +ParseResult AxdrParser::parse(const std::span axdr) { if (axdr.empty()) return {}; buffer_ = axdr; pos_ = 0; - cooked_cb_ = std::move(cooked_cb); objects_found_ = 0; last_pattern_elements_consumed_ = 0; Logger::log(LogLevel::DEBUG, "AxdrParser: parsing %zu bytes", axdr.size()); while (this->pos_ < this->buffer_.size()) { - const uint8_t type = this->read_byte_(); - if (type != DLMS_DATA_TYPE_STRUCTURE && type != DLMS_DATA_TYPE_ARRAY) { + const auto type = static_cast(this->read_byte_()); + if (type != DlmsDataType::STRUCTURE && type != DlmsDataType::ARRAY) { Logger::log(LogLevel::VERBOSE, "Non-container type 0x%02X at pos %zu - stopping", type, this->pos_ - 1); this->pos_--; // put it back — not consumed break; @@ -84,7 +237,7 @@ uint32_t AxdrParser::read_u32_() { // Traversal // --------------------------------------------------------------------------- -bool AxdrParser::skip_data_(uint8_t type) { +bool AxdrParser::skip_data_(DlmsDataType type) { const int data_size = get_data_type_size(static_cast(type)); if (data_size == 0) return true; @@ -108,26 +261,26 @@ bool AxdrParser::skip_data_(uint8_t type) { } uint32_t skip_bytes = length; - if (type == DLMS_DATA_TYPE_BIT_STRING) { + if (type == DlmsDataType::BIT_STRING) { skip_bytes = (length + 7) / 8; } if (this->pos_ + skip_bytes > this->buffer_.size()) return false; - Logger::log(LogLevel::VERY_VERBOSE, "Skipping %s (%u bytes) at pos %zu", dlms_data_type_to_string(static_cast(type)), skip_bytes, this->pos_); + Logger::log(LogLevel::VERY_VERBOSE, "Skipping %s (%u bytes) at pos %zu", to_string(type), skip_bytes, this->pos_); this->pos_ += skip_bytes; } return true; } -bool AxdrParser::parse_element_(const uint8_t type, const uint8_t depth) { - if (type == DLMS_DATA_TYPE_STRUCTURE || type == DLMS_DATA_TYPE_ARRAY) { +bool AxdrParser::parse_element_(const DlmsDataType type, const uint8_t depth) { + if (type == DlmsDataType::STRUCTURE || type == DlmsDataType::ARRAY) { return this->parse_sequence_(type, depth); } return this->skip_data_(type); } -bool AxdrParser::parse_sequence_(const uint8_t type, const uint8_t depth) { +bool AxdrParser::parse_sequence_(const DlmsDataType type, const uint8_t depth) { const uint8_t elements_count = this->read_byte_(); if (elements_count == 0xFF) { Logger::log(LogLevel::VERY_VERBOSE, "Invalid sequence length at pos %zu", this->pos_ - 1); @@ -135,7 +288,7 @@ bool AxdrParser::parse_sequence_(const uint8_t type, const uint8_t depth) { } Logger::log(LogLevel::VERBOSE, "Parsing %s with %d elements at pos %zu (depth %d)", - type == DLMS_DATA_TYPE_STRUCTURE ? "STRUCTURE" : "ARRAY", + type == DlmsDataType::STRUCTURE ? "STRUCTURE" : "ARRAY", elements_count, this->pos_ - 1, depth); uint8_t elements_consumed = 0; @@ -151,11 +304,11 @@ bool AxdrParser::parse_sequence_(const uint8_t type, const uint8_t depth) { if (this->pos_ >= this->buffer_.size()) { Logger::log(LogLevel::WARNING, "Unexpected end while reading element %d of %s", - elements_consumed + 1, type == DLMS_DATA_TYPE_STRUCTURE ? "STRUCTURE" : "ARRAY"); + elements_consumed + 1, type == DlmsDataType::STRUCTURE ? "STRUCTURE" : "ARRAY"); return false; } - const uint8_t elem_type = this->read_byte_(); + const auto elem_type = static_cast(this->read_byte_()); if (!this->parse_element_(elem_type, depth + 1)) return false; elements_consumed++; @@ -179,11 +332,11 @@ bool AxdrParser::test_if_date_time_12b_(const std::span buf) cons return test_if_date_time_12b(this->buffer_.subspan(this->pos_, 12)); } -bool AxdrParser::capture_generic_value_(AxdrCaptures& c) { - uint8_t vt = this->read_byte_(); - if (!is_value_data_type(static_cast(vt))) return false; +bool AxdrParser::capture_generic_value_(AxdrCapture& c) { + DlmsDataType vt = static_cast(this->read_byte_()); + if (!is_value_data_type(vt)) return false; - const int ds = get_data_type_size(static_cast(vt)); + const auto ds = get_data_type_size(vt); if (ds > 0) { if (this->pos_ + static_cast(ds) > this->buffer_.size()) return false; c.value = this->buffer_.subspan(this->pos_, static_cast(ds)); @@ -206,7 +359,7 @@ bool AxdrParser::capture_generic_value_(AxdrCaptures& c) { } uint32_t data_bytes = length; - if (vt == DLMS_DATA_TYPE_BIT_STRING) { + if (vt == DlmsDataType::BIT_STRING) { data_bytes = (length + 7) / 8; } @@ -216,12 +369,12 @@ bool AxdrParser::capture_generic_value_(AxdrCaptures& c) { } // Auto-detect 12-byte OCTET_STRING as DATETIME - if (vt == DLMS_DATA_TYPE_OCTET_STRING && c.value.size() == 12 && + if (vt == DlmsDataType::OCTET_STRING && c.value.size() == 12 && this->test_if_date_time_12b_(c.value)) { - vt = DLMS_DATA_TYPE_DATETIME; + vt = DlmsDataType::DATETIME; } - c.value_type = static_cast(vt); + c.value_type = vt; return true; } @@ -240,7 +393,7 @@ bool AxdrParser::try_match_patterns_(const uint8_t elem_idx, const uint8_t elem_ bool AxdrParser::match_pattern_(const uint8_t elem_idx, const uint8_t elem_count, const AxdrDescriptorPattern& pat, uint8_t& consumed) { - AxdrCaptures cap{}; + AxdrCapture cap{}; consumed = 0; uint8_t level = 0; auto consume_one = [&] { if (level == 0) consumed++; }; @@ -259,8 +412,8 @@ bool AxdrParser::match_pattern_(const uint8_t elem_idx, const uint8_t elem_count consume_one(); break; case AxdrTokenType::EXPECT_TYPE_U_I_8: { - const uint8_t t = this->read_byte_(); - if (t != DLMS_DATA_TYPE_INT8 && t != DLMS_DATA_TYPE_UINT8) return false; + const auto t = static_cast(this->read_byte_()); + if (t != DlmsDataType::INT8 && t != DlmsDataType::UINT8) return false; consume_one(); break; } @@ -271,7 +424,7 @@ bool AxdrParser::match_pattern_(const uint8_t elem_idx, const uint8_t elem_count break; } case AxdrTokenType::EXPECT_OBIS6_TAGGED: - if (this->read_byte_() != DLMS_DATA_TYPE_OCTET_STRING) return false; + if (this->read_byte_() != static_cast(DlmsDataType::OCTET_STRING)) return false; if (this->read_byte_() != 6) return false; if (this->pos_ + 6 > this->buffer_.size()) return false; cap.obis = this->buffer_.subspan(this->pos_, 6); @@ -281,7 +434,7 @@ bool AxdrParser::match_pattern_(const uint8_t elem_idx, const uint8_t elem_count case AxdrTokenType::EXPECT_OBIS6_TAGGED_WRONG: // Landis+Gyr firmware bug: sends 06 09 instead of 09 06 if (this->read_byte_() != 6) return false; - if (this->read_byte_() != DLMS_DATA_TYPE_OCTET_STRING) return false; + if (this->read_byte_() != static_cast(DlmsDataType::OCTET_STRING)) return false; if (this->pos_ + 6 > this->buffer_.size()) return false; cap.obis = this->buffer_.subspan(this->pos_, 6); this->pos_ += 6; @@ -301,46 +454,46 @@ bool AxdrParser::match_pattern_(const uint8_t elem_idx, const uint8_t elem_count break; case AxdrTokenType::EXPECT_VALUE_DATE_TIME: { // Accepts both: 0x19 (DATETIME tag) + 12 bytes, or 0x09 (OCTET_STRING) + 0x0C + 12 bytes - const uint8_t tag = this->read_byte_(); - if (tag == DLMS_DATA_TYPE_DATETIME) { + const auto tag = static_cast(this->read_byte_()); + if (tag == DlmsDataType::DATETIME) { // Native DATETIME tag (0x19): fixed 12-byte payload, no length byte - } else if (tag == DLMS_DATA_TYPE_OCTET_STRING) { + } else if (tag == DlmsDataType::OCTET_STRING) { if (this->read_byte_() != 12) return false; } else { return false; } if (this->pos_ + 12 > this->buffer_.size()) return false; cap.value = this->buffer_.subspan(this->pos_, 12); - cap.value_type = DLMS_DATA_TYPE_DATETIME; + cap.value_type = DlmsDataType::DATETIME; this->pos_ += 12; consume_one(); break; } case AxdrTokenType::EXPECT_VALUE_OCTET_STRING: { - const uint8_t vt = this->read_byte_(); - if (vt != DLMS_DATA_TYPE_OCTET_STRING && vt != DLMS_DATA_TYPE_STRING && - vt != DLMS_DATA_TYPE_STRING_UTF8) return false; + const auto vt = static_cast(this->read_byte_()); + if (vt != DlmsDataType::OCTET_STRING && vt != DlmsDataType::STRING && + vt != DlmsDataType::STRING_UTF8) return false; const uint8_t slen = this->read_byte_(); if (slen == 0xFF || this->pos_ + slen > this->buffer_.size()) return false; - cap.value_type = static_cast(vt); + cap.value_type = vt; cap.value = this->buffer_.subspan(this->pos_, slen); this->pos_ += slen; consume_one(); break; } case AxdrTokenType::EXPECT_STRUCTURE_N: - if (this->read_byte_() != DLMS_DATA_TYPE_STRUCTURE) return false; + if (this->read_byte_() != static_cast(DlmsDataType::STRUCTURE)) return false; if (this->read_byte_() != param_u8_a) return false; consume_one(); break; case AxdrTokenType::EXPECT_SCALER_TAGGED: - if (this->read_byte_() != DLMS_DATA_TYPE_INT8) return false; + if (this->read_byte_() != static_cast(DlmsDataType::INT8)) return false; cap.scaler = static_cast(this->read_byte_()); cap.has_scaler_unit = true; consume_one(); break; case AxdrTokenType::EXPECT_UNIT_ENUM_TAGGED: - if (this->read_byte_() != DLMS_DATA_TYPE_ENUM) return false; + if (this->read_byte_() != static_cast(DlmsDataType::ENUM)) return false; cap.unit_enum = this->read_byte_(); cap.has_scaler_unit = true; consume_one(); @@ -352,88 +505,23 @@ bool AxdrParser::match_pattern_(const uint8_t elem_idx, const uint8_t elem_count } if (consumed == 0) consumed = 1; - cap.elem_idx = initial_position; - this->emit_object_(pat, cap); - return true; -} - -static constexpr std::array ZERO_OBIS = {0, 0, 0, 0, 0, 0}; - -float AxdrParser::apply_scaler(const float value, const int8_t scaler) { - if (scaler == 0) return value; - - // Lookup table for 10^0 through 10^9 - static constexpr float pow10_lut[] = { - 1e0f, 1e1f, 1e2f, 1e3f, 1e4f, 1e5f, 1e6f, 1e7f, 1e8f, 1e9f - }; - - // Fast path: use LUT for typical DLMS bounds (-9 to +9) - if (scaler > 0 && scaler <= 9) {return value * pow10_lut[scaler];} - if (scaler < 0 && scaler >= -9) {return value / pow10_lut[-scaler];} - - // Fallback path: loop for unusually large scalers - float multiplier = 1.0f; - if (scaler > 0) { - for (int i = 0; i < scaler; ++i) multiplier *= 10.0f; - return value * multiplier; - } - - for (int i = 0; i < -scaler; ++i) multiplier *= 10.0f; - return value / multiplier; -} - -void AxdrParser::emit_object_(const AxdrDescriptorPattern& pat, const AxdrCaptures& c) { - // If no OBIS was captured by the pattern, use 0.0.0.0.0.0 as a placeholder. - // If no OBIS captured, use pattern's default_obis if set, otherwise zero placeholder. - auto [elem_idx, class_id, obis, value_type, value, has_scaler_unit, scaler, unit_enum] = c; - if (obis.empty()) { - obis = pat.has_default_obis ? std::span(pat.default_obis) : std::span(ZERO_OBIS); - } + cap.elem_idx = initial_position; objects_found_++; - if (!this->cooked_cb_) return; - - char obis_str_buf[32]; - obis_to_string(obis, obis_str_buf); - - const float raw_val_f = data_as_float(value_type, value); - float val_f = raw_val_f; - - char val_s_buf[128]; - data_to_string(value_type, value, val_s_buf); - - const bool is_numeric = value_type != DLMS_DATA_TYPE_OCTET_STRING && value_type != DLMS_DATA_TYPE_STRING && - value_type != DLMS_DATA_TYPE_STRING_UTF8 && value_type != DLMS_DATA_TYPE_DATETIME; - - if (has_scaler_unit && is_numeric) { - val_f = apply_scaler(raw_val_f, scaler); + if (cap.obis.empty()) { + // If no OBIS was captured by the pattern, use 0.0.0.0.0.0 as a placeholder. + // If no OBIS captured, use pattern's default_obis if set, otherwise zero placeholder. + static constexpr std::array ZERO_OBIS = { 0, 0, 0, 0, 0, 0 }; + cap.obis = pat.has_default_obis ? std::span(pat.default_obis) : ZERO_OBIS; } - const uint16_t cid = class_id ? class_id : pat.default_class_id; - Logger::log(LogLevel::VERBOSE, "Pattern '%s' matched at pos %u - class_id=%d obis=%s", - pat.name ? pat.name : "UNKNOWN", elem_idx, cid, obis_str_buf); - - if (has_scaler_unit) { - Logger::log(LogLevel::VERBOSE, " type=%s len=%zu scaler=%d unit=%d", - dlms_data_type_to_string(value_type), value.size(), scaler, unit_enum); - } else { - Logger::log(LogLevel::VERBOSE, " type=%s len=%zu", - dlms_data_type_to_string(value_type), value.size()); - } + Logger::log(LogLevel::VERBOSE, "Pattern '%s' matched at pos %u - class_id=%d", pat.name ? pat.name : "UNKNOWN", cap.elem_idx, cap.class_id ? cap.class_id : pat.default_class_id); + Logger::log(LogLevel::VERBOSE, " type=%s len=%zu scaler=%d unit=%d", to_string(cap.value_type), cap.value.size(), cap.scaler, cap.unit_enum); - if (!value.empty()) { - char hex_buf[512]; - format_hex_pretty_to(hex_buf, value); - Logger::log(LogLevel::VERBOSE, " hex : %s", hex_buf); - } - Logger::log(LogLevel::VERBOSE, " str : '%s'", val_s_buf); - Logger::log(LogLevel::VERBOSE, " float: %f", static_cast(raw_val_f)); - if (has_scaler_unit && is_numeric) { - Logger::log(LogLevel::VERBOSE, " scaled: %f", static_cast(val_f)); - } + dlmsDataCallback_(cap); - this->cooked_cb_(obis_str_buf, val_f, val_s_buf, is_numeric); + return true; } // --------------------------------------------------------------------------- @@ -487,7 +575,7 @@ AxdrDescriptorPattern& AxdrParser::register_pattern_dsl_(const char* name, const else if (tok == "L") add_step(AxdrTokenType::EXPECT_TO_BE_LAST); else if (tok == "C") add_step(AxdrTokenType::EXPECT_CLASS_ID_UNTAGGED); else if (tok == "TC") { - add_step(AxdrTokenType::EXPECT_TYPE_EXACT, DLMS_DATA_TYPE_UINT16); + add_step(AxdrTokenType::EXPECT_TYPE_EXACT, static_cast(DlmsDataType::UINT16)); add_step(AxdrTokenType::EXPECT_CLASS_ID_UNTAGGED); } else if (tok == "O") add_step(AxdrTokenType::EXPECT_OBIS6_UNTAGGED); diff --git a/src/dlms_parser/axdr_parser.h b/src/dlms_parser/axdr_parser.h index 5145208..3f778cf 100644 --- a/src/dlms_parser/axdr_parser.h +++ b/src/dlms_parser/axdr_parser.h @@ -44,20 +44,26 @@ struct AxdrDescriptorPattern { std::array default_obis{}; }; -struct AxdrCaptures { +struct AxdrCapture { uint32_t elem_idx{ 0 }; uint16_t class_id{ 0 }; std::span obis{}; - DlmsDataType value_type{ DLMS_DATA_TYPE_NONE }; + DlmsDataType value_type{ DlmsDataType::NONE }; std::span value{}; - bool has_scaler_unit{ false }; int8_t scaler{ 0 }; uint8_t unit_enum{ 0 }; + + std::string_view obis_as_string(std::span buffer) const; + bool is_numeric() const; + float value_as_float_with_scaler_applied() const; + std::string_view value_as_string(std::span buffer) const; +private: + float value_as_float() const; + static float apply_scaler(const float value, const int8_t scaler); }; -// Callback: OBIS code, numeric value, string value, is_numeric flag -using DlmsDataCallback = std::function; +using DlmsDataCallback = std::function; struct ParseResult { size_t count{ 0 }; // number of matched COSEM objects @@ -69,7 +75,7 @@ struct ParseResult { // No knowledge of APDU framing or encryption. class AxdrParser final : NonCopyableAndNonMovable { public: - AxdrParser() = default; + explicit AxdrParser(DlmsDataCallback dlmsDataCallback); // Register a named pattern from the DSL string, e.g. "TC,TO,TS,TV". void register_pattern(const char* name, const char* dsl, int priority = 10); @@ -78,7 +84,7 @@ class AxdrParser final : NonCopyableAndNonMovable { // Parse AXDR bytes. Fires cooked_cb and/or raw_cb for each pattern match. // Either callback may be nullptr. - ParseResult parse(std::span axdr, DlmsDataCallback cooked_cb); + ParseResult parse(std::span axdr); [[nodiscard]] std::span patterns() const { return { patterns_.data(), patterns_count_ }; } [[nodiscard]] size_t patterns_size() const { return patterns_count_; } @@ -94,7 +100,7 @@ class AxdrParser final : NonCopyableAndNonMovable { // Parse-time state — reset at the start of each parse() call std::span buffer_{}; size_t pos_{ 0 }; - DlmsDataCallback cooked_cb_; + DlmsDataCallback dlmsDataCallback_; size_t objects_found_{ 0 }; uint8_t last_pattern_elements_consumed_{ 0 }; @@ -104,17 +110,17 @@ class AxdrParser final : NonCopyableAndNonMovable { uint32_t read_u32_(); // Traversal - bool skip_data_(uint8_t type); - bool parse_element_(uint8_t type, uint8_t depth = 0); - bool parse_sequence_(uint8_t type, uint8_t depth = 0); + bool skip_data_(DlmsDataType type); + bool parse_element_(DlmsDataType type, uint8_t depth = 0); + bool parse_sequence_(DlmsDataType type, uint8_t depth = 0); // Pattern matching bool test_if_date_time_12b_(std::span buf = {}) const; - bool capture_generic_value_(AxdrCaptures& c); + bool capture_generic_value_(AxdrCapture& c); bool try_match_patterns_(uint8_t elem_idx, uint8_t elem_count); bool match_pattern_(uint8_t elem_idx, uint8_t elem_count, const AxdrDescriptorPattern& pat, uint8_t& consumed); static float apply_scaler(float value, int8_t scaler); - void emit_object_(const AxdrDescriptorPattern& pat, const AxdrCaptures& c); + void emit_object_(const AxdrDescriptorPattern& pat, const AxdrCapture& c); }; } diff --git a/src/dlms_parser/decryption/aes_128_gcm_decryptor_bearssl.h b/src/dlms_parser/decryption/aes_128_gcm_decryptor_bearssl.h index 533777a..5cf6545 100644 --- a/src/dlms_parser/decryption/aes_128_gcm_decryptor_bearssl.h +++ b/src/dlms_parser/decryption/aes_128_gcm_decryptor_bearssl.h @@ -10,7 +10,6 @@ #include "aes_128_gcm_decryptor.h" #include "../utils.h" -#include "../log.h" namespace dlms_parser { diff --git a/src/dlms_parser/decryption/aes_128_gcm_decryptor_mbedtls.h b/src/dlms_parser/decryption/aes_128_gcm_decryptor_mbedtls.h index 5308b37..b7cae57 100644 --- a/src/dlms_parser/decryption/aes_128_gcm_decryptor_mbedtls.h +++ b/src/dlms_parser/decryption/aes_128_gcm_decryptor_mbedtls.h @@ -3,7 +3,6 @@ #include #include "aes_128_gcm_decryptor.h" #include "../utils.h" -#include "../log.h" namespace dlms_parser { diff --git a/src/dlms_parser/decryption/aes_128_gcm_decryptor_tfpsa.h b/src/dlms_parser/decryption/aes_128_gcm_decryptor_tfpsa.h index 89c22aa..4ace659 100644 --- a/src/dlms_parser/decryption/aes_128_gcm_decryptor_tfpsa.h +++ b/src/dlms_parser/decryption/aes_128_gcm_decryptor_tfpsa.h @@ -3,7 +3,6 @@ #include #include "aes_128_gcm_decryptor.h" #include "../utils.h" -#include "../log.h" namespace dlms_parser { diff --git a/src/dlms_parser/dlms_parser.cpp b/src/dlms_parser/dlms_parser.cpp index dcf128d..0004d69 100644 --- a/src/dlms_parser/dlms_parser.cpp +++ b/src/dlms_parser/dlms_parser.cpp @@ -1,7 +1,6 @@ #include "dlms_parser.h" #include "apdu_handler.h" #include "hdlc_decoder.h" -#include "log.h" #include "mbus_decoder.h" namespace dlms_parser { @@ -22,7 +21,7 @@ static void log_span_as_hex(const LogLevel level, const std::span } } -DlmsParser::DlmsParser(Aes128GcmDecryptor* decryptor) : decryptor_(decryptor) {} +DlmsParser::DlmsParser(DlmsDataCallback dlmsDataCallback, Aes128GcmDecryptor* decryptor) : decryptor_(decryptor), axdr_parser_(dlmsDataCallback) {} void DlmsParser::set_skip_crc_check(const bool skip) { skip_crc_check_ = skip; @@ -55,7 +54,7 @@ void DlmsParser::register_pattern(const char* name, const char* dsl, const int p axdr_parser_.register_pattern(name, dsl, priority, default_obis); } -ParseResult DlmsParser::parse(std::span buf, const DlmsDataCallback& cooked_cb) { +ParseResult DlmsParser::parse(std::span buf) { if (buf.empty()) { Logger::log(LogLevel::ERROR, "Empty buffer passed to parse()"); return {}; @@ -86,11 +85,15 @@ ParseResult DlmsParser::parse(std::span buf, const DlmsDataCallback& co const auto axdr = parse_apdu_in_place(decoded, decryptor_); if (axdr.empty()) return {}; + Logger::log(LogLevel::VERY_VERBOSE, "Unencrypted AXDR payload:"); + log_span_as_hex(LogLevel::VERY_VERBOSE, axdr); + Logger::log(LogLevel::VERY_VERBOSE, "============"); + // Step 3: AXDR parse — loop over successive top-level containers ParseResult result; size_t offset = 0; while (offset < axdr.size()) { - auto [count, bytes_consumed] = axdr_parser_.parse(axdr.subspan(offset), cooked_cb); + auto [count, bytes_consumed] = axdr_parser_.parse(axdr.subspan(offset)); if (bytes_consumed == 0) break; result.count += count; result.bytes_consumed += bytes_consumed; @@ -101,10 +104,6 @@ ParseResult DlmsParser::parse(std::span buf, const DlmsDataCallback& co Logger::log(LogLevel::ERROR, "No COSEM objects found in AXDR payload"); } - Logger::log(LogLevel::VERY_VERBOSE, "Unencrypted AXDR payload:"); - log_span_as_hex(LogLevel::VERY_VERBOSE, axdr); - Logger::log(LogLevel::VERY_VERBOSE, "============"); - return result; } diff --git a/src/dlms_parser/dlms_parser.h b/src/dlms_parser/dlms_parser.h index 4ca5027..928355c 100644 --- a/src/dlms_parser/dlms_parser.h +++ b/src/dlms_parser/dlms_parser.h @@ -11,7 +11,7 @@ namespace dlms_parser { // Facade — composes frame decoder, APDU handler, decryptor, and AXDR parser. class DlmsParser final : NonCopyableAndNonMovable { public: - explicit DlmsParser(Aes128GcmDecryptor* decryptor = nullptr); + explicit DlmsParser(DlmsDataCallback dlmsDataCallback, Aes128GcmDecryptor* decryptor = nullptr); void set_skip_crc_check(bool skip); void set_decryption_key(const Aes128GcmDecryptionKey& key) const; @@ -29,7 +29,7 @@ class DlmsParser final : NonCopyableAndNonMovable { // Parse a full frame (in-place). buf is modified during parsing. // Fires cooked_cb for each matched COSEM object. // Optionally fires raw_cb with unmodified captures before conversion. - ParseResult parse(std::span buf, const DlmsDataCallback& cooked_cb); + ParseResult parse(std::span buf); private: Aes128GcmDecryptor* decryptor_; diff --git a/src/dlms_parser/hdlc_decoder.cpp b/src/dlms_parser/hdlc_decoder.cpp index 2c1be28..b972d05 100644 --- a/src/dlms_parser/hdlc_decoder.cpp +++ b/src/dlms_parser/hdlc_decoder.cpp @@ -1,5 +1,5 @@ #include "hdlc_decoder.h" -#include "log.h" +#include "utils.h" #include namespace dlms_parser { diff --git a/src/dlms_parser/log.h b/src/dlms_parser/log.h deleted file mode 100644 index dd625f0..0000000 --- a/src/dlms_parser/log.h +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once - -#include -#include - -namespace dlms_parser { - -enum class LogLevel { - DEBUG, - VERY_VERBOSE, - VERBOSE, - INFO, - WARNING, - ERROR, -}; - -class Logger final { -public: - static void set_log_function(std::function func) { _log_function = std::move(func); } - - #if defined(__clang__) || defined(__GNUC__) - __attribute__((format(printf, 2, 3))) - #endif - static void log(LogLevel log_level, const char* fmt, ...) { - va_list args; - va_start(args, fmt); - _log_function(log_level, fmt, args); - va_end(args); - } - -private: - Logger() = default; - - inline static std::function _log_function = [](LogLevel, const char*, va_list) {}; -}; - -} diff --git a/src/dlms_parser/mbus_decoder.cpp b/src/dlms_parser/mbus_decoder.cpp index 9fe79f2..cb8543c 100644 --- a/src/dlms_parser/mbus_decoder.cpp +++ b/src/dlms_parser/mbus_decoder.cpp @@ -1,5 +1,5 @@ #include "mbus_decoder.h" -#include "log.h" +#include "utils.h" #include #include #include diff --git a/src/dlms_parser/utils.cpp b/src/dlms_parser/utils.cpp index 9d57d64..2f75d96 100644 --- a/src/dlms_parser/utils.cpp +++ b/src/dlms_parser/utils.cpp @@ -6,38 +6,33 @@ namespace dlms_parser { -float data_as_float(const DlmsDataType value_type, const std::span data) { - if (data.empty()) return 0.0f; - const uint8_t* ptr = data.data(); - const auto len = data.size(); - - switch (value_type) { - case DLMS_DATA_TYPE_BOOLEAN: - case DLMS_DATA_TYPE_ENUM: - case DLMS_DATA_TYPE_UINT8: return ptr[0]; - case DLMS_DATA_TYPE_INT8: return static_cast(ptr[0]); - case DLMS_DATA_TYPE_BIT_STRING: return ptr[0]; - case DLMS_DATA_TYPE_UINT16: return len >= 2 ? static_cast(be16(ptr)) : 0.0f; - case DLMS_DATA_TYPE_INT16: return len >= 2 ? static_cast(static_cast(be16(ptr))) : 0.0f; - case DLMS_DATA_TYPE_UINT32: return len >= 4 ? static_cast(be32(ptr)) : 0.0f; - case DLMS_DATA_TYPE_INT32: return len >= 4 ? static_cast(static_cast(be32(ptr))) : 0.0f; - case DLMS_DATA_TYPE_UINT64: return len >= 8 ? static_cast(be64(ptr)) : 0.0f; - case DLMS_DATA_TYPE_INT64: return len >= 8 ? static_cast(static_cast(be64(ptr))) : 0.0f; - case DLMS_DATA_TYPE_FLOAT32: { - if (len < 4) return 0.0f; - const uint32_t i32 = be32(ptr); - float f; - std::memcpy(&f, &i32, sizeof(float)); - return f; - } - case DLMS_DATA_TYPE_FLOAT64: { - if (len < 8) return 0.0f; - const uint64_t i64 = be64(ptr); - double d; - std::memcpy(&d, &i64, sizeof(double)); - return static_cast(d); - } - default: return 0.0f; +const char* to_string(const DlmsDataType vt) { + switch (vt) { + case DlmsDataType::NONE: return "NONE"; + case DlmsDataType::ARRAY: return "ARRAY"; + case DlmsDataType::STRUCTURE: return "STRUCTURE"; + case DlmsDataType::BOOLEAN: return "BOOLEAN"; + case DlmsDataType::BIT_STRING: return "BIT_STRING"; + case DlmsDataType::INT32: return "INT32"; + case DlmsDataType::UINT32: return "UINT32"; + case DlmsDataType::OCTET_STRING: return "OCTET_STRING"; + case DlmsDataType::STRING: return "STRING"; + case DlmsDataType::STRING_UTF8: return "STRING_UTF8"; + case DlmsDataType::BINARY_CODED_DECIMAL: return "BINARY_CODED_DECIMAL"; + case DlmsDataType::INT8: return "INT8"; + case DlmsDataType::INT16: return "INT16"; + case DlmsDataType::UINT8: return "UINT8"; + case DlmsDataType::UINT16: return "UINT16"; + case DlmsDataType::COMPACT_ARRAY: return "COMPACT_ARRAY"; + case DlmsDataType::INT64: return "INT64"; + case DlmsDataType::UINT64: return "UINT64"; + case DlmsDataType::ENUM: return "ENUM"; + case DlmsDataType::FLOAT32: return "FLOAT32"; + case DlmsDataType::FLOAT64: return "FLOAT64"; + case DlmsDataType::DATETIME: return "DATETIME"; + case DlmsDataType::DATE: return "DATE"; + case DlmsDataType::TIME: return "TIME"; + default: return "UNKNOWN"; } } @@ -129,74 +124,6 @@ void datetime_to_string(const std::span data, const std::span data, const std::span buffer) { - if (!buffer.empty()) buffer[0] = '\0'; - if (data.empty() || buffer.empty()) return; - - auto hex_of = [](const std::span input, const std::span output) { - if (output.empty()) return; - output[0] = '\0'; - size_t pos = 0; - for (size_t i = 0; i < input.size() && pos + 2 < output.size(); i++) { - const int written = snprintf(output.data() + pos, output.size() - pos, "%02x", input[i]); - if (written > 0) pos += static_cast(written); - } - }; - - switch (value_type) { - case DLMS_DATA_TYPE_OCTET_STRING: - case DLMS_DATA_TYPE_STRING: - case DLMS_DATA_TYPE_STRING_UTF8: { - const size_t copy_len = std::min(data.size(), buffer.size() - 1); - std::memcpy(buffer.data(), data.data(), copy_len); - buffer[copy_len] = '\0'; - break; - } - case DLMS_DATA_TYPE_DATETIME: - datetime_to_string(data, buffer); - break; - case DLMS_DATA_TYPE_BIT_STRING: - case DLMS_DATA_TYPE_BINARY_CODED_DECIMAL: - case DLMS_DATA_TYPE_DATE: - case DLMS_DATA_TYPE_TIME: - hex_of(data, buffer); - break; - case DLMS_DATA_TYPE_BOOLEAN: - case DLMS_DATA_TYPE_ENUM: - case DLMS_DATA_TYPE_UINT8: - snprintf(buffer.data(), buffer.size(), "%u", static_cast(data[0])); - break; - case DLMS_DATA_TYPE_INT8: - snprintf(buffer.data(), buffer.size(), "%d", static_cast(static_cast(data[0]))); - break; - case DLMS_DATA_TYPE_UINT16: - if (data.size() >= 2) snprintf(buffer.data(), buffer.size(), "%u", be16(data.data())); - break; - case DLMS_DATA_TYPE_INT16: - if (data.size() >= 2) snprintf(buffer.data(), buffer.size(), "%d", static_cast(be16(data.data()))); - break; - case DLMS_DATA_TYPE_UINT32: - if (data.size() >= 4) snprintf(buffer.data(), buffer.size(), "%" PRIu32, be32(data.data())); - break; - case DLMS_DATA_TYPE_INT32: - if (data.size() >= 4) snprintf(buffer.data(), buffer.size(), "%" PRId32, static_cast(be32(data.data()))); - break; - case DLMS_DATA_TYPE_UINT64: - if (data.size() >= 8) snprintf(buffer.data(), buffer.size(), "%" PRIu64, be64(data.data())); - break; - case DLMS_DATA_TYPE_INT64: - if (data.size() >= 8) snprintf(buffer.data(), buffer.size(), "%" PRId64, static_cast(be64(data.data()))); - break; - case DLMS_DATA_TYPE_FLOAT32: - case DLMS_DATA_TYPE_FLOAT64: { - snprintf(buffer.data(), buffer.size(), "%f", static_cast(data_as_float(value_type, data))); - break; - } - default: - break; - } -} - uint32_t read_ber_length(const std::span buf, size_t& pos) { if (pos >= buf.size()) return 0; const uint8_t first = buf[pos++]; @@ -211,106 +138,59 @@ uint32_t read_ber_length(const std::span buf, size_t& pos) { return length; } -void obis_to_string(const std::span obis, const std::span buffer) { - if (!buffer.empty()) buffer[0] = '\0'; - if (obis.size() < 6 || buffer.empty()) return; - snprintf(buffer.data(), buffer.size(), "%u.%u.%u.%u.%u.%u", obis[0], obis[1], obis[2], obis[3], obis[4], obis[5]); -} - -const char* dlms_data_type_to_string(const DlmsDataType vt) { - switch (vt) { - case DLMS_DATA_TYPE_NONE: return "NONE"; - case DLMS_DATA_TYPE_ARRAY: return "ARRAY"; - case DLMS_DATA_TYPE_STRUCTURE: return "STRUCTURE"; - case DLMS_DATA_TYPE_BOOLEAN: return "BOOLEAN"; - case DLMS_DATA_TYPE_BIT_STRING: return "BIT_STRING"; - case DLMS_DATA_TYPE_INT32: return "INT32"; - case DLMS_DATA_TYPE_UINT32: return "UINT32"; - case DLMS_DATA_TYPE_OCTET_STRING: return "OCTET_STRING"; - case DLMS_DATA_TYPE_STRING: return "STRING"; - case DLMS_DATA_TYPE_STRING_UTF8: return "STRING_UTF8"; - case DLMS_DATA_TYPE_BINARY_CODED_DECIMAL: return "BINARY_CODED_DECIMAL"; - case DLMS_DATA_TYPE_INT8: return "INT8"; - case DLMS_DATA_TYPE_INT16: return "INT16"; - case DLMS_DATA_TYPE_UINT8: return "UINT8"; - case DLMS_DATA_TYPE_UINT16: return "UINT16"; - case DLMS_DATA_TYPE_COMPACT_ARRAY: return "COMPACT_ARRAY"; - case DLMS_DATA_TYPE_INT64: return "INT64"; - case DLMS_DATA_TYPE_UINT64: return "UINT64"; - case DLMS_DATA_TYPE_ENUM: return "ENUM"; - case DLMS_DATA_TYPE_FLOAT32: return "FLOAT32"; - case DLMS_DATA_TYPE_FLOAT64: return "FLOAT64"; - case DLMS_DATA_TYPE_DATETIME: return "DATETIME"; - case DLMS_DATA_TYPE_DATE: return "DATE"; - case DLMS_DATA_TYPE_TIME: return "TIME"; - default: return "UNKNOWN"; - } -} - int get_data_type_size(const DlmsDataType type) { switch (type) { - case DLMS_DATA_TYPE_NONE: return 0; - case DLMS_DATA_TYPE_BOOLEAN: - case DLMS_DATA_TYPE_INT8: - case DLMS_DATA_TYPE_UINT8: - case DLMS_DATA_TYPE_ENUM: return 1; - case DLMS_DATA_TYPE_INT16: - case DLMS_DATA_TYPE_UINT16: return 2; - case DLMS_DATA_TYPE_INT32: - case DLMS_DATA_TYPE_UINT32: - case DLMS_DATA_TYPE_FLOAT32: return 4; - case DLMS_DATA_TYPE_INT64: - case DLMS_DATA_TYPE_UINT64: - case DLMS_DATA_TYPE_FLOAT64: return 8; - case DLMS_DATA_TYPE_DATETIME: return 12; - case DLMS_DATA_TYPE_DATE: return 5; - case DLMS_DATA_TYPE_TIME: return 4; + case DlmsDataType::NONE: return 0; + case DlmsDataType::BOOLEAN: + case DlmsDataType::INT8: + case DlmsDataType::UINT8: + case DlmsDataType::ENUM: return 1; + case DlmsDataType::INT16: + case DlmsDataType::UINT16: return 2; + case DlmsDataType::INT32: + case DlmsDataType::UINT32: + case DlmsDataType::FLOAT32: return 4; + case DlmsDataType::INT64: + case DlmsDataType::UINT64: + case DlmsDataType::FLOAT64: return 8; + case DlmsDataType::DATETIME: return 12; + case DlmsDataType::DATE: return 5; + case DlmsDataType::TIME: return 4; default: return -1; // Variable or complex } } bool is_value_data_type(const DlmsDataType type) { switch (type) { - case DLMS_DATA_TYPE_ARRAY: - case DLMS_DATA_TYPE_STRUCTURE: - case DLMS_DATA_TYPE_COMPACT_ARRAY: + case DlmsDataType::ARRAY: + case DlmsDataType::STRUCTURE: + case DlmsDataType:: COMPACT_ARRAY: return false; - case DLMS_DATA_TYPE_NONE: - case DLMS_DATA_TYPE_BOOLEAN: - case DLMS_DATA_TYPE_BIT_STRING: - case DLMS_DATA_TYPE_INT32: - case DLMS_DATA_TYPE_UINT32: - case DLMS_DATA_TYPE_OCTET_STRING: - case DLMS_DATA_TYPE_STRING: - case DLMS_DATA_TYPE_BINARY_CODED_DECIMAL: - case DLMS_DATA_TYPE_STRING_UTF8: - case DLMS_DATA_TYPE_INT8: - case DLMS_DATA_TYPE_INT16: - case DLMS_DATA_TYPE_UINT8: - case DLMS_DATA_TYPE_UINT16: - case DLMS_DATA_TYPE_INT64: - case DLMS_DATA_TYPE_UINT64: - case DLMS_DATA_TYPE_ENUM: - case DLMS_DATA_TYPE_FLOAT32: - case DLMS_DATA_TYPE_FLOAT64: - case DLMS_DATA_TYPE_DATETIME: - case DLMS_DATA_TYPE_DATE: - case DLMS_DATA_TYPE_TIME: + case DlmsDataType::NONE: + case DlmsDataType::BOOLEAN: + case DlmsDataType::BIT_STRING: + case DlmsDataType::INT32: + case DlmsDataType::UINT32: + case DlmsDataType::OCTET_STRING: + case DlmsDataType::STRING: + case DlmsDataType::BINARY_CODED_DECIMAL: + case DlmsDataType::STRING_UTF8: + case DlmsDataType::INT8: + case DlmsDataType::INT16: + case DlmsDataType::UINT8: + case DlmsDataType::UINT16: + case DlmsDataType::INT64: + case DlmsDataType::UINT64: + case DlmsDataType::ENUM: + case DlmsDataType::FLOAT32: + case DlmsDataType::FLOAT64: + case DlmsDataType::DATETIME: + case DlmsDataType::DATE: + case DlmsDataType::TIME: return true; default: return false; } } -void format_hex_pretty_to(const std::span out, const std::span data) { - if (out.empty()) return; - out[0] = '\0'; - size_t pos = 0; - for (size_t i = 0; i < data.size() && pos + 3 < out.size(); i++) { - const int written = snprintf(out.data() + pos, out.size() - pos, "%02X.", data[i]); - if (written > 0) pos += static_cast(written); - } - if (pos > 0 && out[pos - 1] == '.') out[pos - 1] = '\0'; -} - } diff --git a/src/dlms_parser/utils.h b/src/dlms_parser/utils.h index 23e2ed7..38ada9d 100644 --- a/src/dlms_parser/utils.h +++ b/src/dlms_parser/utils.h @@ -2,10 +2,40 @@ #include #include +#include #include +#include namespace dlms_parser { +enum class LogLevel { + DEBUG, + VERY_VERBOSE, + VERBOSE, + INFO, + WARNING, + ERROR, +}; + +class Logger final { +public: + static void set_log_function(std::function func) { _log_function = std::move(func); } + +#if defined(__clang__) || defined(__GNUC__) + __attribute__((format(printf, 2, 3))) +#endif + static void log(LogLevel log_level, const char* fmt, ...) { + va_list args; + va_start(args, fmt); + _log_function(log_level, fmt, args); + va_end(args); + } + +private: + Logger() = default; + inline static std::function _log_function = [](LogLevel, const char*, va_list) {}; +}; + class NonCopyable { protected: NonCopyable() = default; @@ -29,32 +59,33 @@ class NonCopyableAndNonMovable : NonCopyable { NonCopyableAndNonMovable& operator=(NonCopyableAndNonMovable&&) = delete; }; -enum DlmsDataType : uint8_t { - DLMS_DATA_TYPE_NONE = 0, - DLMS_DATA_TYPE_ARRAY = 1, - DLMS_DATA_TYPE_STRUCTURE = 2, - DLMS_DATA_TYPE_BOOLEAN = 3, - DLMS_DATA_TYPE_BIT_STRING = 4, - DLMS_DATA_TYPE_INT32 = 5, - DLMS_DATA_TYPE_UINT32 = 6, - DLMS_DATA_TYPE_OCTET_STRING = 9, - DLMS_DATA_TYPE_STRING = 10, - DLMS_DATA_TYPE_STRING_UTF8 = 12, - DLMS_DATA_TYPE_BINARY_CODED_DECIMAL = 13, - DLMS_DATA_TYPE_INT8 = 15, - DLMS_DATA_TYPE_INT16 = 16, - DLMS_DATA_TYPE_UINT8 = 17, - DLMS_DATA_TYPE_UINT16 = 18, - DLMS_DATA_TYPE_COMPACT_ARRAY = 19, - DLMS_DATA_TYPE_INT64 = 20, - DLMS_DATA_TYPE_UINT64 = 21, - DLMS_DATA_TYPE_ENUM = 22, - DLMS_DATA_TYPE_FLOAT32 = 23, - DLMS_DATA_TYPE_FLOAT64 = 24, - DLMS_DATA_TYPE_DATETIME = 25, - DLMS_DATA_TYPE_DATE = 26, - DLMS_DATA_TYPE_TIME = 27 +enum class DlmsDataType : uint8_t { + NONE = 0, + ARRAY = 1, + STRUCTURE = 2, + BOOLEAN = 3, + BIT_STRING = 4, + INT32 = 5, + UINT32 = 6, + OCTET_STRING = 9, + STRING = 10, + STRING_UTF8 = 12, + BINARY_CODED_DECIMAL = 13, + INT8 = 15, + INT16 = 16, + UINT8 = 17, + UINT16 = 18, + COMPACT_ARRAY = 19, + INT64 = 20, + UINT64 = 21, + ENUM = 22, + FLOAT32 = 23, + FLOAT64 = 24, + DATETIME = 25, + DATE = 26, + TIME = 27 }; +const char* to_string(DlmsDataType vt); inline uint16_t be16(const uint8_t* p) { return static_cast(static_cast(p[0]) << 8 | p[1]); } inline uint32_t be32(const uint8_t* p) { @@ -68,12 +99,8 @@ inline uint64_t be64(const uint8_t* p) { static_cast(p[6]) << 8 | static_cast(p[7]); } -float data_as_float(DlmsDataType value_type, std::span data); bool test_if_date_time_12b(std::span p); void datetime_to_string(std::span data, std::span buffer); -void data_to_string(DlmsDataType value_type, std::span data, std::span buffer); -void obis_to_string(std::span obis, std::span buffer); -const char* dlms_data_type_to_string(DlmsDataType vt); // Read a BER-encoded length from buf[pos]. Advances pos past the length bytes. // Returns the decoded length, or 0 if the buffer is too short. @@ -82,6 +109,4 @@ uint32_t read_ber_length(std::span buf, size_t& pos); int get_data_type_size(DlmsDataType type); bool is_value_data_type(DlmsDataType type); -void format_hex_pretty_to(std::span out, std::span data); - } diff --git a/tests/test_axdr_registry.cpp b/tests/test_axdr_registry.cpp index a259881..23c63cb 100644 --- a/tests/test_axdr_registry.cpp +++ b/tests/test_axdr_registry.cpp @@ -3,12 +3,13 @@ #include #include +#include "test_util.h" #include "dlms_parser/axdr_parser.h" using namespace dlms_parser; -TEST_CASE("AxdrParser Pattern Registry - Tokenization and Parsing") { - AxdrParser parser; +TEST_CASE_FIXTURE(LogFixture, "AxdrParser Pattern Registry - Tokenization and Parsing") { + AxdrParser parser([](const auto&) {}); SUBCASE("Basic Tokens") { parser.register_pattern("test", "F,C,L", 10); @@ -75,8 +76,8 @@ TEST_CASE("AxdrParser Pattern Registry - Tokenization and Parsing") { } } -TEST_CASE("AxdrParser Pattern Registry - Comprehensive Token Mapping") { - AxdrParser parser; +TEST_CASE_FIXTURE(LogFixture, "AxdrParser Pattern Registry - Comprehensive Token Mapping") { + AxdrParser parser([](const auto&) {}); // Test all primary token aliases parser.register_pattern("all_tokens", "TC,O,TO,TOW,A,TA,TS,TU,V,TV,TDTM,TSTR,DN,UP,TSU", 10); @@ -86,7 +87,7 @@ TEST_CASE("AxdrParser Pattern Registry - Comprehensive Token Mapping") { size_t i = 0; // TC CHECK(pat.steps[i].type == AxdrTokenType::EXPECT_TYPE_EXACT); - CHECK(pat.steps[i++].param_u8_a == DLMS_DATA_TYPE_UINT16); + CHECK(pat.steps[i++].param_u8_a == static_cast(DlmsDataType::UINT16)); CHECK(pat.steps[i++].type == AxdrTokenType::EXPECT_CLASS_ID_UNTAGGED); // O CHECK(pat.steps[i++].type == AxdrTokenType::EXPECT_OBIS6_UNTAGGED); @@ -126,8 +127,8 @@ TEST_CASE("AxdrParser Pattern Registry - Comprehensive Token Mapping") { CHECK(pat.steps[i].type == AxdrTokenType::END_OF_PATTERN); } -TEST_CASE("AxdrParser Pattern Registry - Structure Expansion S(...)") { - AxdrParser parser; +TEST_CASE_FIXTURE(LogFixture, "AxdrParser Pattern Registry - Structure Expansion S(...)") { + AxdrParser parser([](const auto&) {}); SUBCASE("Simple structure") { parser.register_pattern("struct", "S(TO,TV)", 10); @@ -190,8 +191,8 @@ TEST_CASE("AxdrParser Pattern Registry - Structure Expansion S(...)") { } } -TEST_CASE("AxdrParser Pattern Registry - Priority and Array Management") { - AxdrParser parser; +TEST_CASE_FIXTURE(LogFixture, "AxdrParser Pattern Registry - Priority and Array Management") { + AxdrParser parser([](const auto&) {}); SUBCASE("Priority Sorting") { parser.register_pattern("low", "F", 50); @@ -249,8 +250,8 @@ TEST_CASE("AxdrParser Pattern Registry - Priority and Array Management") { } } -TEST_CASE("AxdrParser Pattern Registry - Limits and Edge Cases") { - AxdrParser parser; +TEST_CASE_FIXTURE(LogFixture, "AxdrParser Pattern Registry - Limits and Edge Cases") { + AxdrParser parser([](const auto&) {}); SUBCASE("Max Patterns Limit (32)") { for (int i = 0; i < 40; i++) { diff --git a/tests/test_example.cpp b/tests/test_example.cpp index fd7d742..746cd5a 100644 --- a/tests/test_example.cpp +++ b/tests/test_example.cpp @@ -36,7 +36,26 @@ long last_read_timestamp = 0; // Timestamp of the last byte received. Needed to Uart uart; // UART connected to the smart meter. -dlms_parser::DlmsParser parser(&decryptor); // DLMS parser instance. +// Define a callback that will be called by the DlmsParser for every parsed value. +void on_dlms_data(const dlms_parser::AxdrCapture& capture) { + std::array obis_buf; + const std::string_view obis_str = capture.obis_as_string(obis_buf); + + if (capture.is_numeric()) { + printf("%.*s = %.3f\n", static_cast(obis_str.size()), obis_str.data(), + static_cast(capture.value_as_float_with_scaler_applied())); + } + else { + std::array str_val_buf; + const std::string_view str_val = capture.value_as_string(str_val_buf); + + printf("%.*s = '%.*s'\n", static_cast(obis_str.size()), obis_str.data(), + static_cast(str_val.size()), str_val.data()); + } +} + +// Create DLMS parser instance. Pass the callback and the decryptor (optional). +dlms_parser::DlmsParser parser(on_dlms_data, &decryptor); // Before you can use the parser, you need to configure it. inline void configure_parser() { @@ -81,17 +100,7 @@ inline void loop() { const size_t frame_len = dlms_packet_buffer_position; // Save length before resetting. dlms_packet_buffer_position = 0; // Reset for the next packet. - // Define a callback that will be called by the DlmsParser for every parsed value. - auto on_value = [](const char* obis_code, float float_val, const char* str_val, bool is_numeric) { - if (is_numeric) { - printf("%s = %.3f\n", obis_code, static_cast(float_val)); - } - else { - printf("%s = '%s'\n", obis_code, str_val); - } - }; - - auto [objects_found, bytes_consumed] = parser.parse({dlms_packet_buffer.data(), frame_len}, on_value); + auto [objects_found, bytes_consumed] = parser.parse({dlms_packet_buffer.data(), frame_len}); printf("%zu objects found\n", objects_found); } } diff --git a/tests/test_hdlc_decoder.cpp b/tests/test_hdlc_decoder.cpp index f72555e..9abf5fc 100644 --- a/tests/test_hdlc_decoder.cpp +++ b/tests/test_hdlc_decoder.cpp @@ -2,6 +2,8 @@ #include #include +#include "test_util.h" + #include "dlms_parser/hdlc_decoder.h" #include "expected/hdlc_iskra550.h" @@ -30,7 +32,7 @@ static const std::vector BASE_FRAME = { 0x7E }; -TEST_CASE("HDLC Decoder - Payload Decoding (decode)") { +TEST_CASE_FIXTURE(LogFixture, "HDLC Decoder - Payload Decoding (decode)") { SUBCASE("Single Frame with LLC Stripping (E6 E6 00)") { auto frame = BASE_FRAME; @@ -157,7 +159,7 @@ TEST_CASE("HDLC Decoder - Payload Decoding (decode)") { } } -TEST_CASE("HDLC Decoder - Address Length Decoding") { +TEST_CASE_FIXTURE(LogFixture, "HDLC Decoder - Address Length Decoding") { SUBCASE("2-Byte Address Parsing") { std::vector frame = { @@ -221,7 +223,7 @@ TEST_CASE("HDLC Decoder - Address Length Decoding") { } } -TEST_CASE("HDLC Decoder - Malformed Frame Handling") { +TEST_CASE_FIXTURE(LogFixture, "HDLC Decoder - Malformed Frame Handling") { SUBCASE("Length field mismatch vs buffer boundaries") { auto frame = BASE_FRAME; diff --git a/tests/test_mbus_decoder.cpp b/tests/test_mbus_decoder.cpp index af7d30c..eaf6101 100644 --- a/tests/test_mbus_decoder.cpp +++ b/tests/test_mbus_decoder.cpp @@ -2,6 +2,8 @@ #include #include +#include "test_util.h" + #include "dlms_parser/mbus_decoder.h" using namespace dlms_parser; @@ -33,7 +35,7 @@ static void build_mbus_frame(std::vector& frame, const std::vector base_frame; build_mbus_frame(base_frame, {0xAA, 0xBB, 0xCC}); @@ -75,7 +77,7 @@ TEST_CASE("MBus Decoder - Payload Decoding (decode)") { } } -TEST_CASE("MBus Decoder - Malformed Frame Handling") { +TEST_CASE_FIXTURE(LogFixture, "MBus Decoder - Malformed Frame Handling") { std::vector base_frame; build_mbus_frame(base_frame, {0xAA, 0xBB, 0xCC}); diff --git a/tests/test_meter_dumps.cpp b/tests/test_meter_dumps.cpp index d25b5aa..a7fb5b4 100644 --- a/tests/test_meter_dumps.cpp +++ b/tests/test_meter_dumps.cpp @@ -7,8 +7,9 @@ #include #include +#include "test_util.h" + #include "dlms_parser/dlms_parser.h" -#include "dlms_parser/log.h" #include "dlms_parser/decryption/aes_128_gcm_decryptor_mbedtls.h" #include "dlms_parser/decryption/aes_128_gcm_decryptor_bearssl.h" #include "dlms_parser/decryption/aes_128_gcm_decryptor_tfpsa.h" @@ -27,67 +28,35 @@ #include "tests/expected/hdlc_kamstrup_omnipower.h" #include "tests/expected/mbus_netz_noe_p1.h" -class LogCapturer : dlms_parser::NonCopyableAndNonMovable { -public: - LogCapturer() { - dlms_parser::Logger::set_log_function([&](const dlms_parser::LogLevel log_level, const char* fmt, va_list args) { - std::array buffer; - vsnprintf(buffer.data(), buffer.size(), fmt, args); - - const char* level_str; - switch (log_level) { - case dlms_parser::LogLevel::DEBUG: level_str = "[DBG] "; break; - case dlms_parser::LogLevel::VERY_VERBOSE: level_str = "[VV] "; break; - case dlms_parser::LogLevel::VERBOSE: level_str = "[VRB] "; break; - case dlms_parser::LogLevel::INFO: level_str = "[INF] "; break; - case dlms_parser::LogLevel::WARNING: level_str = "[WRN] "; break; - case dlms_parser::LogLevel::ERROR: level_str = "[ERR] "; break; - } - - log_messages += std::format("{}{}\n", level_str, buffer.data()); - }); - } - - ~LogCapturer() { - dlms_parser::Logger::set_log_function([](auto, auto, auto) {}); - } - - std::string get_logs() const { - return log_messages; - } - -private: - std::string log_messages; -}; - template void run_meter_test(std::span payload, size_t expected_count, const std::map& expected_strings, const std::map& expected_floats, std::function setup_fn = [](auto&) {}) { - LogCapturer log_capturer; - - Aes128GcmDecryptor decryptor; - dlms_parser::DlmsParser parser(&decryptor); - parser.load_default_patterns(); - setup_fn(parser); - std::map captured_floats; std::map captured_strings; - auto callback = [&](const char* obis_code, const float float_val, const char* str_val, const bool is_numeric) { - if (is_numeric) { - captured_floats[std::string(obis_code)] = float_val; - } else { - captured_strings[std::string(obis_code)] = std::string(str_val); + auto callback = [&](const auto& capture) { + std::array obis_buf; + const std::string_view obis_str = capture.obis_as_string(obis_buf); + + if (capture.is_numeric()) { + captured_floats[std::string(obis_str)] = capture.value_as_float_with_scaler_applied(); + } + else { + std::array str_val_buf; + captured_strings[std::string(obis_str)] = std::string(capture.value_as_string(str_val_buf)); } }; - std::vector mutable_payload(payload.begin(), payload.end()); - auto [objects_found, bytes_consumed] = parser.parse(mutable_payload, callback); + Aes128GcmDecryptor decryptor; + dlms_parser::DlmsParser parser(callback, &decryptor); + parser.load_default_patterns(); + setup_fn(parser); - INFO(log_capturer.get_logs()); + std::vector mutable_payload(payload.begin(), payload.end()); + auto [objects_found, bytes_consumed] = parser.parse(mutable_payload); REQUIRE(objects_found == expected_count); @@ -111,7 +80,7 @@ void run_meter_test(std::span payload, // --------------------------------------------------------- // RAW APDU tests (no frame transport) // --------------------------------------------------------- -TEST_CASE("Integration: RAW APDU") { +TEST_CASE_FIXTURE(LogFixture, "Integration: RAW APDU") { SUBCASE("Sagemcom XT211") { run_meter_test( @@ -181,7 +150,7 @@ TEST_CASE("Integration: RAW APDU") { // --------------------------------------------------------- // HDLC transport tests // --------------------------------------------------------- -TEST_CASE("Integration: HDLC") { +TEST_CASE_FIXTURE(LogFixture, "Integration: HDLC") { SUBCASE("Iskra 550 (3 segmented frames)") { run_meter_test( @@ -210,8 +179,8 @@ TEST_CASE("Integration: HDLC") { const auto half = std::size(dlms::test_data::iskra550_raw_frame) / 2; duplicated_frame.insert(duplicated_frame.end(), std::begin(dlms::test_data::iskra550_raw_frame), std::begin(dlms::test_data::iskra550_raw_frame) + half); dlms_parser::Aes128GcmDecryptorMbedTls decryptor; - dlms_parser::DlmsParser parser(&decryptor); - auto [n, consumed] = parser.parse(duplicated_frame, [](auto, auto, auto, auto) {}); + dlms_parser::DlmsParser parser([](const auto&) {}, &decryptor); + auto [n, consumed] = parser.parse(duplicated_frame); CHECK(n == 0); } @@ -258,10 +227,10 @@ TEST_CASE("Integration: HDLC") { SUBCASE("Landis+Gyr ZMF100 - CRC check rejects bad FCS") { dlms_parser::Aes128GcmDecryptorMbedTls decryptor; - dlms_parser::DlmsParser parser(&decryptor); + dlms_parser::DlmsParser parser([](const auto&) {}, &decryptor); std::vector frame(std::begin(dlms::test_data::hdlc_landis_gyr_zmf100_raw_frame), std::end(dlms::test_data::hdlc_landis_gyr_zmf100_raw_frame)); - auto [n, consumed] = parser.parse(frame, [](auto, auto, auto, auto) {}); + auto [n, consumed] = parser.parse(frame); CHECK(n == 0); } @@ -391,13 +360,13 @@ TEST_CASE("Integration: HDLC") { SUBCASE("Kamstrup Omnipower - wrong auth key rejects frame") { const auto wrong_key = dlms_parser::Aes128GcmAuthenticationKey::from_bytes(std::array{0x00}).value(); dlms_parser::Aes128GcmDecryptorMbedTls decryptor; - dlms_parser::DlmsParser parser(&decryptor); + dlms_parser::DlmsParser parser([](const auto&) {}, &decryptor); parser.set_decryption_key(dlms::test_data::hdlc_kamstrup_omnipower_key); parser.set_authentication_key(wrong_key); parser.load_default_patterns(); std::vector frame(std::begin(dlms::test_data::hdlc_kamstrup_omnipower_raw_frame), std::end(dlms::test_data::hdlc_kamstrup_omnipower_raw_frame)); - auto [n, consumed] = parser.parse(frame, [](auto, auto, auto, auto) {}); + auto [n, consumed] = parser.parse(frame); CHECK(n == 0); } @@ -406,7 +375,7 @@ TEST_CASE("Integration: HDLC") { // --------------------------------------------------------- // M-Bus transport tests // --------------------------------------------------------- -TEST_CASE("Integration: MBus") { +TEST_CASE_FIXTURE(LogFixture, "Integration: MBus") { SUBCASE("Netz NOE P1 (encrypted)") { run_meter_test( @@ -445,8 +414,8 @@ TEST_CASE("Integration: MBus") { const auto half = std::size(dlms::test_data::mbus_netz_noe_p1_raw_frame) / 2; duplicated_frame.insert(duplicated_frame.end(), std::begin(dlms::test_data::mbus_netz_noe_p1_raw_frame), std::begin(dlms::test_data::mbus_netz_noe_p1_raw_frame) + half); dlms_parser::Aes128GcmDecryptorMbedTls decryptor; - dlms_parser::DlmsParser parser(&decryptor); - auto [n, consumed] = parser.parse(duplicated_frame, [](auto, auto, auto, auto) {}); + dlms_parser::DlmsParser parser([](const auto&) {}, &decryptor); + auto [n, consumed] = parser.parse(duplicated_frame); CHECK(n == 0); } } diff --git a/tests/test_util.h b/tests/test_util.h new file mode 100644 index 0000000..289ffb2 --- /dev/null +++ b/tests/test_util.h @@ -0,0 +1,50 @@ +#pragma once + +#include "dlms_parser/utils.h" +#include +#include +#include +#include +#include + +class LogCapturer { +public: + LogCapturer() { + dlms_parser::Logger::set_log_function([this](dlms_parser::LogLevel log_level, const char* fmt, va_list args) { + std::array buf; + vsnprintf(buf.data(), buf.size(), fmt, args); + + const char* level_str; + switch (log_level) { + case dlms_parser::LogLevel::DEBUG: level_str = "[DBG] "; break; + case dlms_parser::LogLevel::VERY_VERBOSE: level_str = "[VV] "; break; + case dlms_parser::LogLevel::VERBOSE: level_str = "[VRB] "; break; + case dlms_parser::LogLevel::INFO: level_str = "[INF] "; break; + case dlms_parser::LogLevel::WARNING: level_str = "[WRN] "; break; + case dlms_parser::LogLevel::ERROR: level_str = "[ERR] "; break; + } + + const auto& msg = std::format("{}{}", level_str, buf.data()); + std::cout << msg << std::endl; + messages.emplace_back(msg); + }); + } + + ~LogCapturer() { dlms_parser::Logger::set_log_function([](dlms_parser::LogLevel, const char*, va_list) {}); } + + bool contains(const std::string& substr) const { + for (const auto& msg : messages) { + if (msg.find(substr) != std::string::npos) + return true; + } + return false; + } + + void clear() { messages.clear(); } + + std::vector messages; +}; + +struct LogFixture { + LogCapturer log; +}; diff --git a/tests/test_utils.cpp b/tests/test_utils.cpp index 3880cbc..7e757a7 100644 --- a/tests/test_utils.cpp +++ b/tests/test_utils.cpp @@ -4,11 +4,12 @@ #include #include +#include "test_util.h" #include "dlms_parser/utils.h" using namespace dlms_parser; -TEST_CASE("Endianness Conversions") { +TEST_CASE_FIXTURE(LogFixture, "Endianness Conversions") { SUBCASE("be16") { constexpr uint8_t data[] = {0x12, 0x34}; CHECK(be16(data) == 0x1234); @@ -25,66 +26,7 @@ TEST_CASE("Endianness Conversions") { } } -TEST_CASE("OBIS String Formatting") { - std::array buffer{}; - - SUBCASE("Valid OBIS code") { - const uint8_t obis[] = {1, 0, 96, 1, 0, 255}; - obis_to_string(obis, buffer); - CHECK(std::string_view(buffer.data()) == "1.0.96.1.0.255"); - } - - SUBCASE("Zeroed OBIS code") { - const uint8_t obis[] = {0, 0, 0, 0, 0, 0}; - obis_to_string(obis, buffer); - CHECK(std::string_view(buffer.data()) == "0.0.0.0.0.0"); - } - - SUBCASE("Max length enforcement") { - const uint8_t obis[] = {1, 0, 96, 1, 0, 255}; - obis_to_string(obis, std::span{buffer.data(), 10}); - // Should safely truncate - CHECK(std::string_view(buffer.data()) == "1.0.96.1."); - } - - SUBCASE("Null pointer safety") { - buffer[0] = 'X'; // Fill with dummy data - obis_to_string({}, buffer); - CHECK(std::string_view(buffer.data()) == ""); // Should be null-terminated at index 0 - } -} - -TEST_CASE("Hex Formatting (format_hex_pretty_to)") { - std::array buffer{}; - - SUBCASE("Normal data") { - constexpr uint8_t data[] = {0xDE, 0xAD, 0xBE, 0xEF}; - format_hex_pretty_to(buffer, data); - CHECK(std::string_view(buffer.data()) == "DE.AD.BE.EF"); - } - - SUBCASE("Empty data") { - format_hex_pretty_to(buffer, {}); - CHECK(std::string_view(buffer.data()) == ""); - } - - SUBCASE("Zero max length") { - constexpr uint8_t data[] = {0xDE, 0xAD}; - buffer[0] = 'X'; - format_hex_pretty_to(std::span{buffer.data(), 0}, data); - CHECK(buffer[0] == 'X'); // Should not have written anything, not even \0 - } - - SUBCASE("Truncated by max length") { - constexpr uint8_t data[] = {0xDE, 0xAD, 0xBE, 0xEF}; - // We pass 7 because it briefly needs space to write "DE.AD." + '\0' (7 bytes) - // before the function strips the trailing dot. - format_hex_pretty_to(std::span{buffer.data(), 7}, data); - CHECK(std::string_view(buffer.data()) == "DE.AD"); - } -} - -TEST_CASE("BER Length Decoding") { +TEST_CASE_FIXTURE(LogFixture, "BER Length Decoding") { SUBCASE("Short form length (<= 127)") { constexpr uint8_t data[] = {0x7F}; // 127 size_t pos = 0; @@ -125,77 +67,29 @@ TEST_CASE("BER Length Decoding") { } } -TEST_CASE("Data Size and Type Properties") { +TEST_CASE_FIXTURE(LogFixture, "Data Size and Type Properties") { SUBCASE("Data Sizes") { - CHECK(get_data_type_size(DLMS_DATA_TYPE_NONE) == 0); - CHECK(get_data_type_size(DLMS_DATA_TYPE_UINT8) == 1); - CHECK(get_data_type_size(DLMS_DATA_TYPE_UINT16) == 2); - CHECK(get_data_type_size(DLMS_DATA_TYPE_FLOAT32) == 4); - CHECK(get_data_type_size(DLMS_DATA_TYPE_FLOAT64) == 8); - CHECK(get_data_type_size(DLMS_DATA_TYPE_DATETIME) == 12); - CHECK(get_data_type_size(DLMS_DATA_TYPE_DATE) == 5); - CHECK(get_data_type_size(DLMS_DATA_TYPE_TIME) == 4); - CHECK(get_data_type_size(DLMS_DATA_TYPE_OCTET_STRING) == -1); // Variable length + CHECK(get_data_type_size(DlmsDataType::NONE) == 0); + CHECK(get_data_type_size(DlmsDataType::UINT8) == 1); + CHECK(get_data_type_size(DlmsDataType::UINT16) == 2); + CHECK(get_data_type_size(DlmsDataType::FLOAT32) == 4); + CHECK(get_data_type_size(DlmsDataType::FLOAT64) == 8); + CHECK(get_data_type_size(DlmsDataType::DATETIME) == 12); + CHECK(get_data_type_size(DlmsDataType::DATE) == 5); + CHECK(get_data_type_size(DlmsDataType::TIME) == 4); + CHECK(get_data_type_size(DlmsDataType::OCTET_STRING) == -1); // Variable length } SUBCASE("Value Type Checks") { - CHECK(is_value_data_type(DLMS_DATA_TYPE_UINT16) == true); - CHECK(is_value_data_type(DLMS_DATA_TYPE_FLOAT32) == true); - CHECK(is_value_data_type(DLMS_DATA_TYPE_STRING) == true); - CHECK(is_value_data_type(DLMS_DATA_TYPE_ARRAY) == false); - CHECK(is_value_data_type(DLMS_DATA_TYPE_STRUCTURE) == false); + CHECK(is_value_data_type(DlmsDataType::UINT16) == true); + CHECK(is_value_data_type(DlmsDataType::FLOAT32) == true); + CHECK(is_value_data_type(DlmsDataType::STRING) == true); + CHECK(is_value_data_type(DlmsDataType::ARRAY) == false); + CHECK(is_value_data_type(DlmsDataType::STRUCTURE) == false); } } -TEST_CASE("Float Conversion (data_as_float)") { - SUBCASE("Null pointer and zero length") { - CHECK(data_as_float(DLMS_DATA_TYPE_UINT8, {}) == 0.0f); - uint8_t dummy = 0; - CHECK(data_as_float(DLMS_DATA_TYPE_UINT8, {&dummy, 0}) == 0.0f); - } - - SUBCASE("Unsigned Integers") { - constexpr uint8_t u8[] = {255}; - CHECK(data_as_float(DLMS_DATA_TYPE_UINT8, u8) == 255.0f); - - constexpr uint8_t u16[] = {0x01, 0x00}; // 256 - CHECK(data_as_float(DLMS_DATA_TYPE_UINT16, u16) == 256.0f); - - constexpr uint8_t u32[] = {0x00, 0x01, 0x00, 0x00}; // 65536 - CHECK(data_as_float(DLMS_DATA_TYPE_UINT32, u32) == 65536.0f); - } - - SUBCASE("Signed Integers") { - constexpr uint8_t i8[] = {0xFF}; // -1 - CHECK(data_as_float(DLMS_DATA_TYPE_INT8, i8) == -1.0f); - - constexpr uint8_t i16[] = {0xFF, 0xFE}; // -2 - CHECK(data_as_float(DLMS_DATA_TYPE_INT16, i16) == -2.0f); - - constexpr uint8_t i32[] = {0xFF, 0xFF, 0xFF, 0xFD}; // -3 - CHECK(data_as_float(DLMS_DATA_TYPE_INT32, i32) == -3.0f); - } - - SUBCASE("Float32") { - constexpr uint8_t f32_pos[] = {0x41, 0x20, 0x00, 0x00}; // 10.0f in IEEE 754 - CHECK(data_as_float(DLMS_DATA_TYPE_FLOAT32, f32_pos) == 10.0f); - - constexpr uint8_t f32_neg[] = {0xC1, 0x20, 0x00, 0x00}; // -10.0f - CHECK(data_as_float(DLMS_DATA_TYPE_FLOAT32, f32_neg) == -10.0f); - } - - SUBCASE("Float64") { - const uint8_t f64[] = {0x40, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; // 10.0 in IEEE 754 double - CHECK(data_as_float(DLMS_DATA_TYPE_FLOAT64, f64) == 10.0f); - } - - SUBCASE("Safe guarding against short lengths") { - constexpr uint8_t f32[] = {0x41, 0x20, 0x00}; // Missing last byte - CHECK(data_as_float(DLMS_DATA_TYPE_FLOAT32, {f32, 3}) == 0.0f); - } -} - -TEST_CASE("DLMS Datetime 12-byte Validation") { +TEST_CASE_FIXTURE(LogFixture, "DLMS Datetime 12-byte Validation") { SUBCASE("Null pointer safety") { CHECK(test_if_date_time_12b({}) == false); } @@ -237,7 +131,7 @@ TEST_CASE("DLMS Datetime 12-byte Validation") { } } -TEST_CASE("DLMS Datetime Formatting") { +TEST_CASE_FIXTURE(LogFixture, "DLMS Datetime Formatting") { std::array buffer{}; SUBCASE("Format fully specified datetime") { @@ -276,45 +170,10 @@ TEST_CASE("DLMS Datetime Formatting") { } } -TEST_CASE("Data to String formatting") { - std::array buffer{}; - - SUBCASE("String types") { - constexpr uint8_t str[] = {'H', 'e', 'l', 'l', 'o'}; - data_to_string(DLMS_DATA_TYPE_STRING, str, buffer); - CHECK(std::string_view(buffer.data()) == "Hello"); - } - - SUBCASE("Numeric types") { - constexpr uint8_t u32[] = {0x00, 0x00, 0x04, 0xD2}; // 1234 - data_to_string(DLMS_DATA_TYPE_UINT32, u32, buffer); - CHECK(std::string_view(buffer.data()) == "1234"); - } - - SUBCASE("Bit strings fallback to hex") { - constexpr uint8_t bits[] = {0xAB, 0xCD}; - data_to_string(DLMS_DATA_TYPE_BIT_STRING, bits, buffer); - CHECK(std::string_view(buffer.data()) == "abcd"); // The function uses %02x - } - - SUBCASE("Float32 formatting") { - constexpr uint8_t f32[] = {0x41, 0x20, 0x00, 0x00}; // 10.0f - data_to_string(DLMS_DATA_TYPE_FLOAT32, f32, buffer); - // Uses %f, so it appends decimal zeros - CHECK(std::string_view(buffer.data()).find("10.00000") == 0); - } - - SUBCASE("Null pointer safety") { - buffer[0] = 'X'; - data_to_string(DLMS_DATA_TYPE_STRING, {}, buffer); - CHECK(std::string_view(buffer.data()) == ""); - } -} - -TEST_CASE("DLMS Data Type to String") { - CHECK(std::string_view(dlms_data_type_to_string(DLMS_DATA_TYPE_NONE)) == "NONE"); - CHECK(std::string_view(dlms_data_type_to_string(DLMS_DATA_TYPE_FLOAT32)) == "FLOAT32"); - CHECK(std::string_view(dlms_data_type_to_string(DLMS_DATA_TYPE_DATETIME)) == "DATETIME"); +TEST_CASE_FIXTURE(LogFixture, "DLMS Data Type to String") { + CHECK(std::string_view(to_string(DlmsDataType::NONE)) == "NONE"); + CHECK(std::string_view(to_string(DlmsDataType::FLOAT32)) == "FLOAT32"); + CHECK(std::string_view(to_string(DlmsDataType::DATETIME)) == "DATETIME"); // Test fallback/default case - CHECK(std::string_view(dlms_data_type_to_string(static_cast(99))) == "UNKNOWN"); + CHECK(std::string_view(to_string(static_cast(99))) == "UNKNOWN"); } diff --git a/tools/decode_readout.cpp b/tools/decode_readout.cpp index 7440e43..96c610f 100644 --- a/tools/decode_readout.cpp +++ b/tools/decode_readout.cpp @@ -32,7 +32,6 @@ #include #include "dlms_parser/dlms_parser.h" -#include "dlms_parser/log.h" #include "dlms_parser/decryption/aes_128_gcm_decryptor_mbedtls.h" // Hex file reader — supports spaced hex, concatenated hex, line continuations @@ -41,7 +40,7 @@ static constexpr bool is_hex_char(char c) { } static std::vector read_hex_file(std::string_view path) { - std::ifstream f(std::string{path}); + std::ifstream f(std::string{ path }); if (!f) { std::cerr << std::format("Error: cannot open '{}'\n", path); return {}; @@ -56,7 +55,8 @@ static std::vector read_hex_file(std::string_view path) { for (size_t i = 0; i < raw.size(); i++) { if (raw[i] == '-' && i + 1 < raw.size() && raw[i + 1] == '\n') { i++; // skip dash + newline - } else { + } + else { text += raw[i]; } } @@ -67,9 +67,9 @@ static std::vector read_hex_file(std::string_view path) { const char* p = text.c_str(); // Skip UTF-8 BOM if present if (text.size() >= 3 && - static_cast(p[0]) == 0xEF && - static_cast(p[1]) == 0xBB && - static_cast(p[2]) == 0xBF) { + static_cast(p[0]) == 0xEF && + static_cast(p[1]) == 0xBB && + static_cast(p[2]) == 0xBF) { p += 3; } while (*p) { @@ -92,7 +92,7 @@ static std::vector read_hex_file(std::string_view path) { // Binary file reader static std::vector read_bin_file(std::string_view path) { - std::ifstream f(std::string{path}, std::ios::binary | std::ios::ate); + std::ifstream f(std::string{ path }, std::ios::binary | std::ios::ate); if (!f) { std::cerr << std::format("Error: cannot open '{}'\n", path); return {}; @@ -108,7 +108,7 @@ static std::vector read_bin_file(std::string_view path) { // Auto-detect file type: if all bytes are hex chars/spaces/newlines, treat as hex static bool looks_like_hex_file(std::string_view path) { - std::ifstream f(std::string{path}); + std::ifstream f(std::string{ path }); if (!f) return false; std::array buf{}; f.read(buf.data(), buf.size() - 1); @@ -116,15 +116,15 @@ static bool looks_like_hex_file(std::string_view path) { size_t i = 0; // Skip UTF-8 BOM if present (EF BB BF) if (n >= 3 && - static_cast(buf[0]) == 0xEF && - static_cast(buf[1]) == 0xBB && - static_cast(buf[2]) == 0xBF) { + static_cast(buf[0]) == 0xEF && + static_cast(buf[1]) == 0xBB && + static_cast(buf[2]) == 0xBF) { i = 3; } for (; i < n; i++) { auto c = static_cast(buf[i]); if (is_hex_char(static_cast(c)) || - c == ' ' || c == '\n' || c == '\r' || c == '\t' || c == '-' || c == ',') { + c == ' ' || c == '\n' || c == '\r' || c == '\t' || c == '-' || c == ',') { continue; } // Non-printable byte (control char or high byte) -- likely raw binary @@ -142,17 +142,17 @@ enum class FrameFormat { RAW, MBUS, HDLC }; static FrameFormat detect_format(const std::span data) { if (data.empty()) return FrameFormat::RAW; switch (data[0]) { - case 0x7E: return FrameFormat::HDLC; - case 0x68: return FrameFormat::MBUS; - default: return FrameFormat::RAW; + case 0x7E: return FrameFormat::HDLC; + case 0x68: return FrameFormat::MBUS; + default: return FrameFormat::RAW; } } static const char* to_string(FrameFormat fmt) { switch (fmt) { - case FrameFormat::HDLC: return "HDLC"; - case FrameFormat::MBUS: return "MBUS"; - case FrameFormat::RAW: return "RAW"; + case FrameFormat::HDLC: return "HDLC"; + case FrameFormat::MBUS: return "MBUS"; + case FrameFormat::RAW: return "RAW"; } return "?"; } @@ -184,19 +184,26 @@ int main(int argc, char* argv[]) { std::string_view arg = argv[i]; if (arg == "-k" && i + 1 < argc) { key_str = argv[++i]; - } else if (arg == "-p" && i + 1 < argc) { + } + else if (arg == "-p" && i + 1 < argc) { custom_patterns.emplace_back(argv[++i]); - } else if (arg == "-P") { + } + else if (arg == "-P") { skip_defaults = true; - } else if (arg == "-C") { + } + else if (arg == "-C") { skip_crc = true; - } else if (arg == "-vv") { + } + else if (arg == "-vv") { verbosity = 2; - } else if (arg == "-v") { + } + else if (arg == "-v") { verbosity = 1; - } else if (!arg.starts_with('-')) { + } + else if (!arg.starts_with('-')) { file_path = arg; - } else { + } + else { std::cerr << std::format("Unknown option: {}\n", arg); return 1; } @@ -204,21 +211,21 @@ int main(int argc, char* argv[]) { if (file_path.empty()) { std::cerr << std::format( - "Usage: {} [options] \n" - "\n" - "Options:\n" - " -k AES-128-GCM decryption key (32 hex chars)\n" - " -p Register a custom pattern (repeatable)\n" - " -P Skip loading default patterns\n" - " -C Skip CRC/checksum validation\n" - " -v Verbose logging\n" - " -vv Very verbose logging\n" - "\n" - "Examples:\n" - " {} tests/dumps/hdlc_norway_han_1phase.log\n" - " {} -k 36C66639E48A8CA4D6BC8B282A793BBB tests/dumps/mbus_netz_noe_p1.log\n" - " {} -p \"TO, TV\" -k 5C316162209EBB790B52EB0E7FC5B11C tests/dumps/hdlc_landis_gyr_e450.log\n", - argv[0], argv[0], argv[0], argv[0]); + "Usage: {} [options] \n" + "\n" + "Options:\n" + " -k AES-128-GCM decryption key (32 hex chars)\n" + " -p Register a custom pattern (repeatable)\n" + " -P Skip loading default patterns\n" + " -C Skip CRC/checksum validation\n" + " -v Verbose logging\n" + " -vv Very verbose logging\n" + "\n" + "Examples:\n" + " {} tests/dumps/hdlc_norway_han_1phase.log\n" + " {} -k 36C66639E48A8CA4D6BC8B282A793BBB tests/dumps/mbus_netz_noe_p1.log\n" + " {} -p \"TO, TV\" -k 5C316162209EBB790B52EB0E7FC5B11C tests/dumps/hdlc_landis_gyr_e450.log\n", + argv[0], argv[0], argv[0], argv[0]); return 1; } @@ -228,27 +235,28 @@ int main(int argc, char* argv[]) { if (verbosity >= 2) min_level = dlms_parser::LogLevel::DEBUG; dlms_parser::Logger::set_log_function( - [min_level](dlms_parser::LogLevel level, const char* fmt, va_list args) { - if (level < min_level) return; - const char* prefix = ""; - switch (level) { - case dlms_parser::LogLevel::DEBUG: prefix = "[DBG] "; break; - case dlms_parser::LogLevel::VERY_VERBOSE: prefix = "[VV] "; break; - case dlms_parser::LogLevel::VERBOSE: prefix = "[VRB] "; break; - case dlms_parser::LogLevel::INFO: prefix = "[INF] "; break; - case dlms_parser::LogLevel::WARNING: prefix = "[WRN] "; break; - case dlms_parser::LogLevel::ERROR: prefix = "[ERR] "; break; - } - fprintf(stderr, "%s", prefix); - vfprintf(stderr, fmt, args); - fprintf(stderr, "\n"); - }); + [min_level](dlms_parser::LogLevel level, const char* fmt, va_list args) { + if (level < min_level) return; + const char* prefix = ""; + switch (level) { + case dlms_parser::LogLevel::DEBUG: prefix = "[DBG] "; break; + case dlms_parser::LogLevel::VERY_VERBOSE: prefix = "[VV] "; break; + case dlms_parser::LogLevel::VERBOSE: prefix = "[VRB] "; break; + case dlms_parser::LogLevel::INFO: prefix = "[INF] "; break; + case dlms_parser::LogLevel::WARNING: prefix = "[WRN] "; break; + case dlms_parser::LogLevel::ERROR: prefix = "[ERR] "; break; + } + fprintf(stderr, "%s", prefix); + vfprintf(stderr, fmt, args); + fprintf(stderr, "\n"); + }); // ---- Read input ---- std::vector data; if (looks_like_hex_file(file_path)) { data = read_hex_file(file_path); - } else { + } + else { data = read_bin_file(file_path); } @@ -257,9 +265,30 @@ int main(int argc, char* argv[]) { return 1; } + size_t obj_count_numeric = 0; + size_t obj_count_string = 0; + + auto cooked_cb = [&](const dlms_parser::AxdrCapture& capture) { + std::array obis_buf; + const auto obis_str = capture.obis_as_string(obis_buf); + + if (capture.is_numeric()) { + obj_count_numeric++; + std::cout << std::format(" [{:2}] {:<20} = {:.4f}\n", obj_count_numeric, obis_str, capture.value_as_float_with_scaler_applied()); + } + else { + obj_count_string++; + + std::array str_val_buf; + const auto str_val = capture.value_as_string(str_val_buf); + + std::cout << std::format(" [{:2}] {:<20} = \"{}\"\n", obj_count_string, obis_str, str_val); + } + }; + // ---- Configure parser ---- dlms_parser::Aes128GcmDecryptorMbedTls decryptor; - dlms_parser::DlmsParser parser(&decryptor); + dlms_parser::DlmsParser parser(cooked_cb, &decryptor); // Frame format (auto-detected) const auto fmt = detect_format(data); @@ -292,21 +321,7 @@ int main(int argc, char* argv[]) { if (!key_str.empty()) std::cout << std::format("Key: {}\n", key_str); std::cout << "\n"; - // ---- Parse ---- - size_t obj_count_numeric = 0; - size_t obj_count_string = 0; - - auto cooked_cb = [&](const char* obis, float val, const char* str, bool is_numeric) { - if (is_numeric) { - obj_count_numeric++; - std::cout << std::format(" [{:2}] {:<20} = {:.4f}\n", obj_count_numeric, obis, static_cast(val)); - } else { - obj_count_string++; - std::cout << std::format(" [{:2}] {:<20} = \"{}\"\n", obj_count_string, obis, str); - } - }; - - auto [count, consumed] = parser.parse(data, cooked_cb); + auto [count, consumed] = parser.parse(data); std::cout << std::format("\nTotal: {} objects matched, {} bytes consumed\n", count, consumed); std::cout << std::format(" Numeric: {}\n", obj_count_numeric);