diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3c8f90a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +charset = utf-8 +indent_style = space +indent_size = 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f77b0b6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/build/ +.idea/ +/*build*/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..fc3cf75 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,33 @@ +cmake_minimum_required (VERSION 3.20) +project(arduino-dsmr-test LANGUAGES CXX) + +## Download Catch2 test framework +file(DOWNLOAD + https://github.com/catchorg/Catch2/releases/download/v3.4.0/catch_amalgamated.hpp + ${CMAKE_BINARY_DIR}/catch2/catch_amalgamated.hpp + EXPECTED_MD5 b9e33e9a8198294a87b64dcf641dee16) +file(DOWNLOAD + https://github.com/catchorg/Catch2/releases/download/v3.4.0/catch_amalgamated.cpp + ${CMAKE_BINARY_DIR}/catch2/catch_amalgamated.cpp + EXPECTED_MD5 b4ee03064bf6be8f41313df1649ff7a9) + +add_library(catch2 + ${CMAKE_BINARY_DIR}/catch2/catch_amalgamated.hpp + ${CMAKE_BINARY_DIR}/catch2/catch_amalgamated.cpp) +target_compile_features(catch2 PRIVATE cxx_std_23) +target_include_directories(catch2 INTERFACE ${CMAKE_BINARY_DIR}/catch2) +target_compile_options(catch2 PRIVATE -w) + +# Configure arduino_dsmr_test project +file(GLOB_RECURSE arduino_dsmr_test_src_files CONFIGURE_DEPENDS "test/*.h" "test/*.cpp") +add_executable(arduino_dsmr_test + ${arduino_dsmr_test_src_files} + ${CMAKE_SOURCE_DIR}/src/dsmr/util.h + ${CMAKE_SOURCE_DIR}/src/dsmr/fields.h + ${CMAKE_SOURCE_DIR}/src/dsmr/parser.h + ${CMAKE_SOURCE_DIR}/src/dsmr/crc16.h + ${CMAKE_SOURCE_DIR}/src/dsmr/fields.cpp) +target_include_directories(arduino_dsmr_test PRIVATE ${CMAKE_SOURCE_DIR}/src) +target_precompile_headers(arduino_dsmr_test PRIVATE ${CMAKE_SOURCE_DIR}/test/precompiled_header.h) +target_link_libraries(arduino_dsmr_test catch2) +target_compile_options(arduino_dsmr_test PRIVATE -Wall -Wextra) diff --git a/src/dsmr/fields.h b/src/dsmr/fields.h index e016274..54242fa 100644 --- a/src/dsmr/fields.h +++ b/src/dsmr/fields.h @@ -52,7 +52,7 @@ struct ParsedField { template struct StringField : ParsedField { ParseResult parse(const char *str, const char *end) { - ParseResult res = StringParser::parse_string(minlen, maxlen, str, end); + ParseResult res = StringParser::parse_string(minlen, maxlen, str, end); if (!res.err) static_cast(this)->val() = res.result; return res; @@ -102,7 +102,7 @@ struct FixedField : ParsedField { }; struct TimestampedFixedValue : public FixedValue { - String timestamp; + std::string timestamp; }; // Some numerical values are prefixed with a timestamp. This is simply @@ -111,7 +111,7 @@ template struct TimestampedFixedField : public FixedField { ParseResult parse(const char *str, const char *end) { // First, parse timestamp - ParseResult res = StringParser::parse_string(13, 13, str, end); + ParseResult res = StringParser::parse_string(13, 13, str, end); if (res.err) return res; @@ -182,8 +182,8 @@ struct NameConverter { operator const __FlashStringHelper*() const { return reinterpret_cast(&FieldT::name_progmem); } }; -#define DEFINE_FIELD(fieldname, value_t, obis, field_t, field_args...) \ - struct fieldname : field_t { \ +#define DEFINE_FIELD(fieldname, value_t, obis, field_t, ...) \ + struct fieldname : field_t { \ value_t fieldname; \ bool fieldname ## _present = false; \ static constexpr ObisId id = obis; \ @@ -201,16 +201,16 @@ struct NameConverter { /* Meter identification. This is not a normal field, but a * specially-formatted first line of the message */ -DEFINE_FIELD(identification, String, ObisId(255, 255, 255, 255, 255, 255), RawField); +DEFINE_FIELD(identification, std::string, ObisId(255, 255, 255, 255, 255, 255), RawField); /* Version information for P1 output */ -DEFINE_FIELD(p1_version, String, ObisId(1, 3, 0, 2, 8), StringField, 2, 2); +DEFINE_FIELD(p1_version, std::string, ObisId(1, 3, 0, 2, 8), StringField, 2, 2); /* Date-time stamp of the P1 message */ -DEFINE_FIELD(timestamp, String, ObisId(0, 0, 1, 0, 0), TimestampField); +DEFINE_FIELD(timestamp, std::string, ObisId(0, 0, 1, 0, 0), TimestampField); /* Equipment identifier */ -DEFINE_FIELD(equipment_id, String, ObisId(0, 0, 96, 1, 1), StringField, 0, 96); +DEFINE_FIELD(equipment_id, std::string, ObisId(0, 0, 96, 1, 1), StringField, 0, 96); /* Meter Reading electricity delivered to client (Tariff 1) in 0,001 kWh */ DEFINE_FIELD(energy_delivered_tariff1, FixedValue, ObisId(1, 0, 1, 8, 1), FixedField, units::kWh, units::Wh); @@ -224,7 +224,7 @@ DEFINE_FIELD(energy_returned_tariff2, FixedValue, ObisId(1, 0, 2, 8, 2), FixedFi /* Tariff indicator electricity. The tariff indicator can also be used * to switch tariff dependent loads e.g boilers. This is the * responsibility of the P1 user */ -DEFINE_FIELD(electricity_tariff, String, ObisId(0, 0, 96, 14, 0), StringField, 4, 4); +DEFINE_FIELD(electricity_tariff, std::string, ObisId(0, 0, 96, 14, 0), StringField, 4, 4); /* Actual electricity power delivered (+P) in 1 Watt resolution */ DEFINE_FIELD(power_delivered, FixedValue, ObisId(1, 0, 1, 7, 0), FixedField, units::kW, units::W); @@ -243,7 +243,7 @@ DEFINE_FIELD(electricity_failures, uint32_t, ObisId(0, 0, 96, 7, 21), IntField, DEFINE_FIELD(electricity_long_failures, uint32_t, ObisId(0, 0, 96, 7, 9), IntField, units::none); /* Power Failure Event Log (long power failures) */ -DEFINE_FIELD(electricity_failure_log, String, ObisId(1, 0, 99, 97, 0), RawField); +DEFINE_FIELD(electricity_failure_log, std::string, ObisId(1, 0, 99, 97, 0), RawField); /* Number of voltage sags in phase L1 */ DEFINE_FIELD(electricity_sags_l1, uint32_t, ObisId(1, 0, 32, 32, 0), IntField, units::none); @@ -261,10 +261,10 @@ DEFINE_FIELD(electricity_swells_l3, uint32_t, ObisId(1, 0, 72, 36, 0), IntField, /* Text message codes: numeric 8 digits (Note: Missing from 5.0 spec) * */ -DEFINE_FIELD(message_short, String, ObisId(0, 0, 96, 13, 1), StringField, 0, 16); +DEFINE_FIELD(message_short, std::string, ObisId(0, 0, 96, 13, 1), StringField, 0, 16); /* Text message max 2048 characters (Note: Spec says 1024 in comment and * 2048 in format spec, so we stick to 2048). */ -DEFINE_FIELD(message_long, String, ObisId(0, 0, 96, 13, 0), StringField, 0, 2048); +DEFINE_FIELD(message_long, std::string, ObisId(0, 0, 96, 13, 0), StringField, 0, 2048); /* Instantaneous voltage L1 in 0.1V resolution (Note: Spec says V * resolution in comment, but 0.1V resolution in format spec. Added in @@ -305,7 +305,7 @@ DEFINE_FIELD(power_returned_l3, FixedValue, ObisId(1, 0, 62, 7, 0), FixedField, DEFINE_FIELD(gas_device_type, uint16_t, ObisId(0, GAS_MBUS_ID, 24, 1, 0), IntField, units::none); /* Equipment identifier (Gas) */ -DEFINE_FIELD(gas_equipment_id, String, ObisId(0, GAS_MBUS_ID, 96, 1, 0), StringField, 0, 96); +DEFINE_FIELD(gas_equipment_id, std::string, ObisId(0, GAS_MBUS_ID, 96, 1, 0), StringField, 0, 96); /* Valve position Gas (on/off/released) (Note: Removed in 4.0.7 / 4.2.2 / 5.0). */ DEFINE_FIELD(gas_valve_position, uint8_t, ObisId(0, GAS_MBUS_ID, 24, 4, 0), IntField, units::none); @@ -320,7 +320,7 @@ DEFINE_FIELD(gas_delivered, TimestampedFixedValue, ObisId(0, GAS_MBUS_ID, 24, 2, DEFINE_FIELD(thermal_device_type, uint16_t, ObisId(0, THERMAL_MBUS_ID, 24, 1, 0), IntField, units::none); /* Equipment identifier (Thermal: heat or cold) */ -DEFINE_FIELD(thermal_equipment_id, String, ObisId(0, THERMAL_MBUS_ID, 96, 1, 0), StringField, 0, 96); +DEFINE_FIELD(thermal_equipment_id, std::string, ObisId(0, THERMAL_MBUS_ID, 96, 1, 0), StringField, 0, 96); /* Valve position (on/off/released) (Note: Removed in 4.0.7 / 4.2.2 / 5.0). */ DEFINE_FIELD(thermal_valve_position, uint8_t, ObisId(0, THERMAL_MBUS_ID, 24, 4, 0), IntField, units::none); @@ -334,7 +334,7 @@ DEFINE_FIELD(thermal_delivered, TimestampedFixedValue, ObisId(0, THERMAL_MBUS_ID DEFINE_FIELD(water_device_type, uint16_t, ObisId(0, WATER_MBUS_ID, 24, 1, 0), IntField, units::none); /* Equipment identifier (Thermal: heat or cold) */ -DEFINE_FIELD(water_equipment_id, String, ObisId(0, WATER_MBUS_ID, 96, 1, 0), StringField, 0, 96); +DEFINE_FIELD(water_equipment_id, std::string, ObisId(0, WATER_MBUS_ID, 96, 1, 0), StringField, 0, 96); /* Valve position (on/off/released) (Note: Removed in 4.0.7 / 4.2.2 / 5.0). */ DEFINE_FIELD(water_valve_position, uint8_t, ObisId(0, WATER_MBUS_ID, 24, 4, 0), IntField, units::none); @@ -348,7 +348,7 @@ DEFINE_FIELD(water_delivered, TimestampedFixedValue, ObisId(0, WATER_MBUS_ID, 24 DEFINE_FIELD(slave_device_type, uint16_t, ObisId(0, SLAVE_MBUS_ID, 24, 1, 0), IntField, units::none); /* Equipment identifier (Thermal: heat or cold) */ -DEFINE_FIELD(slave_equipment_id, String, ObisId(0, SLAVE_MBUS_ID, 96, 1, 0), StringField, 0, 96); +DEFINE_FIELD(slave_equipment_id, std::string, ObisId(0, SLAVE_MBUS_ID, 96, 1, 0), StringField, 0, 96); /* Valve position (on/off/released) (Note: Removed in 4.0.7 / 4.2.2 / 5.0). */ DEFINE_FIELD(slave_valve_position, uint8_t, ObisId(0, SLAVE_MBUS_ID, 24, 4, 0), IntField, units::none); diff --git a/src/dsmr/parser.h b/src/dsmr/parser.h index 69beca6..d5e9ad1 100644 --- a/src/dsmr/parser.h +++ b/src/dsmr/parser.h @@ -144,8 +144,8 @@ struct ParsedData : public T, ParsedData { struct StringParser { - static ParseResult parse_string(size_t min, size_t max, const char *str, const char *end) { - ParseResult res; + static ParseResult parse_string(size_t min, size_t max, const char *str, const char *end) { + ParseResult res; if (str >= end || *str != '(') return res.fail(F("Missing ("), str); diff --git a/src/dsmr/util.h b/src/dsmr/util.h index c52ca6c..8e96d89 100644 --- a/src/dsmr/util.h +++ b/src/dsmr/util.h @@ -31,13 +31,11 @@ #ifndef DSMR_INCLUDE_UTIL_H #define DSMR_INCLUDE_UTIL_H -#ifdef ARDUINO_ARCH_ESP8266 -#define DSMR_PROGMEM -#else -#define DSMR_PROGMEM PROGMEM -#endif +#include -#include +#define DSMR_PROGMEM +#define F(str) str +using __FlashStringHelper = char; namespace dsmr { @@ -51,12 +49,8 @@ inline unsigned int lengthof(const T (&)[sz]) { return sz; } // This appends the given number of bytes from the given C string to the // given Arduino string, without requiring a trailing NUL. // Requires that there _is_ room for nul-termination -static void concat_hack(String& s, const char *append, size_t n) { - // Add null termination. Inefficient, but it works... - char buf[n + 1]; - memcpy(buf, append, n); - buf[n] = 0; - s.concat(buf); +static void concat_hack(std::string& s, const char *append, size_t n) { + s.append(append, n); } /** @@ -137,8 +131,8 @@ struct ParseResult : public _ParseResult, T> { * characters in the total parsed string. These are needed to properly * limit the context output. */ - String fullError(const char* start, const char* end) const { - String res; + std::string fullError(const char* start, const char* end) const { + std::string res; if (this->ctx && start && end) { // Find the entire line surrounding the context const char *line_end = this->ctx; diff --git a/test/parser_test.cpp b/test/parser_test.cpp new file mode 100644 index 0000000..2c0ea02 --- /dev/null +++ b/test/parser_test.cpp @@ -0,0 +1,174 @@ +#include "dsmr/parser.h" +#include "dsmr/fields.h" + +using namespace dsmr; +using namespace fields; +using namespace Catch::Matchers; + +struct Printer { + template + void apply(Item &i) { + if (i.present()) { + std::cout << Item::get_name() << ": " << i.val() << Item::unit() << std::endl; + } + } +}; + +TEST_CASE("Should parse all fields in the DSMR message correctly") +{ + const auto &dsmr_message = + "/KFM5KAIFA-METER\r\n" + "\r\n" + "1-3:0.2.8(40)\r\n" + "0-0:1.0.0(150117185916W)\r\n" + "0-0:96.1.1(0000000000000000000000000000000000)\r\n" + "1-0:1.8.1(000671.578*kWh)\r\n" + "1-0:1.8.2(000842.472*kWh)\r\n" + "1-0:2.8.1(000000.000*kWh)\r\n" + "1-0:2.8.2(000000.000*kWh)\r\n" + "0-0:96.14.0(0001)\r\n" + "1-0:1.7.0(00.333*kW)\r\n" + "1-0:2.7.0(00.000*kW)\r\n" + "0-0:17.0.0(999.9*kW)\r\n" + "0-0:96.3.10(1)\r\n" + "0-0:96.7.21(00008)\r\n" + "0-0:96.7.9(00007)\r\n" + "1-0:99.97.0(1)(0-0:96.7.19)(000101000001W)(2147483647*s)\r\n" + "1-0:32.32.0(00000)\r\n" + "1-0:32.36.0(00000)\r\n" + "0-0:96.13.1()\r\n" + "0-0:96.13.0()\r\n" + "1-0:31.7.0(001*A)\r\n" + "1-0:21.7.0(00.332*kW)\r\n" + "1-0:22.7.0(00.000*kW)\r\n" + "0-1:24.1.0(003)\r\n" + "0-1:96.1.0(0000000000000000000000000000000000)\r\n" + "0-1:24.2.1(150117180000W)(00473.789*m3)\r\n" + "0-1:24.4.0(1)\r\n" + "!6F4A\r\n"; + + ParsedData< + /* String */ identification, + /* String */ p1_version, + /* String */ timestamp, + /* String */ equipment_id, + /* FixedValue */ energy_delivered_tariff1, + /* FixedValue */ energy_delivered_tariff2, + /* FixedValue */ energy_returned_tariff1, + /* FixedValue */ energy_returned_tariff2, + /* String */ electricity_tariff, + /* FixedValue */ power_delivered, + /* FixedValue */ power_returned, + /* FixedValue */ electricity_threshold, + /* uint8_t */ electricity_switch_position, + /* uint32_t */ electricity_failures, + /* uint32_t */ electricity_long_failures, + /* String */ electricity_failure_log, + /* uint32_t */ electricity_sags_l1, + /* uint32_t */ electricity_sags_l2, + /* uint32_t */ electricity_sags_l3, + /* uint32_t */ electricity_swells_l1, + /* uint32_t */ electricity_swells_l2, + /* uint32_t */ electricity_swells_l3, + /* String */ message_short, + /* String */ message_long, + /* FixedValue */ voltage_l1, + /* FixedValue */ voltage_l2, + /* FixedValue */ voltage_l3, + /* FixedValue */ current_l1, + /* FixedValue */ current_l2, + /* FixedValue */ current_l3, + /* FixedValue */ power_delivered_l1, + /* FixedValue */ power_delivered_l2, + /* FixedValue */ power_delivered_l3, + /* FixedValue */ power_returned_l1, + /* FixedValue */ power_returned_l2, + /* FixedValue */ power_returned_l3, + /* uint16_t */ gas_device_type, + /* String */ gas_equipment_id, + /* uint8_t */ gas_valve_position, + /* TimestampedFixedValue */ gas_delivered, + /* uint16_t */ thermal_device_type, + /* String */ thermal_equipment_id, + /* uint8_t */ thermal_valve_position, + /* TimestampedFixedValue */ thermal_delivered, + /* uint16_t */ water_device_type, + /* String */ water_equipment_id, + /* uint8_t */ water_valve_position, + /* TimestampedFixedValue */ water_delivered, + /* uint16_t */ slave_device_type, + /* String */ slave_equipment_id, + /* uint8_t */ slave_valve_position, + /* TimestampedFixedValue */ slave_delivered> data; + + auto res = P1Parser::parse(&data, dsmr_message, lengthof(dsmr_message), true); + REQUIRE(res.err == nullptr); + + // Print all values + data.applyEach(Printer()); + + // Check that all fields have correct values + REQUIRE(data.identification == "KFM5KAIFA-METER"); + REQUIRE(data.p1_version == "40"); + REQUIRE(data.timestamp == "150117185916W"); + REQUIRE(data.equipment_id == "0000000000000000000000000000000000"); + REQUIRE(data.energy_delivered_tariff1 == 671.578f); + REQUIRE(data.energy_delivered_tariff2 == 842.472f); + REQUIRE(data.energy_returned_tariff1 == 0); + REQUIRE(data.energy_returned_tariff2 == 0); + REQUIRE(data.electricity_tariff == "0001"); + REQUIRE(data.power_delivered == 0.333f); + REQUIRE(data.power_returned == 0); + REQUIRE(data.electricity_threshold == 999.9f); + REQUIRE(data.electricity_switch_position == 1); + REQUIRE(data.electricity_failures == 8); + REQUIRE(data.electricity_long_failures == 7); + REQUIRE(data.electricity_failure_log == "(1)(0-0:96.7.19)(000101000001W)(2147483647*s)"); + REQUIRE(data.electricity_sags_l1 == 0); + REQUIRE(data.electricity_swells_l1 == 0); + REQUIRE(data.message_short.empty()); + REQUIRE(data.message_long.empty()); + REQUIRE(data.current_l1 == 1); + REQUIRE(data.power_delivered_l1 == 0.332f); + REQUIRE(data.power_returned_l1 == 0); + REQUIRE(data.gas_device_type == 3); + REQUIRE(data.gas_equipment_id == "0000000000000000000000000000000000"); + REQUIRE(data.gas_valve_position == 1); + REQUIRE(data.gas_delivered == 473.789f); +} + +TEST_CASE("Should report an error if the crc has incorrect format") +{ + // Data to parse + const auto &dsmr_message = + "/KFM5KAIFA-METER\r\n" + "\r\n" + "1-0:1.8.1(000671.578*kWh)\r\n" + "1-0:1.7.0(00.318*kW)\r\n" + "!1ED\r\n"; + + ParsedData< + /* String */ identification, + /* FixedValue */ power_delivered> data; + + auto res = P1Parser::parse(&data, dsmr_message, lengthof(dsmr_message), true); + CHECK_THAT(res.err, Equals("Incomplete or malformed checksum")); +} + +TEST_CASE("Should report an error if the crc of a package is incorrect") +{ + // Data to parse + const auto &dsmr_message = + "/KFM5KAIFA-METER\r\n" + "\r\n" + "1-0:.8.1(000671.578*kWh)\r\n" + "1-0:1.7.0(00.318*kW)\r\n" + "!1E1D\r\n"; + + ParsedData< + /* String */ identification, + /* FixedValue */ power_delivered> data; + + auto res = P1Parser::parse(&data, dsmr_message, lengthof(dsmr_message), true); + CHECK_THAT(res.err, Equals("Checksum mismatch")); +} diff --git a/test/precompiled_header.h b/test/precompiled_header.h new file mode 100644 index 0000000..38e603e --- /dev/null +++ b/test/precompiled_header.h @@ -0,0 +1,5 @@ +#pragma once + +#include +#include +#include