Skip to content

Commit 3b699cc

Browse files
authored
Use std::unordered_map in ParsedData::parse_line instead of iterating over all fields to parse each field from a DSMR telegram. (#38)
* Improve the performance of P1Parser::parse method by using std::unordered_map in ParsedData::parse_line instead of iterating over all fields to parse each field from a DSMR telegram. * Small refactoring: accept `ParsedData` by reference instead of by pointer. * Add reference document `IEC.62056-61-2006 - Object identification system (OBIS).pdf` * Move `src/test` to `tests` folder because it is a more idiomatic folder structure for a C++ library. * Add `noexcept` where applicable
1 parent 93a4e51 commit 3b699cc

20 files changed

+114
-98
lines changed
File renamed without changes.

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ include(${cmake_template_SOURCE_DIR}/cmake/CompilerWarnings.cmake)
4040
include(${cmake_template_SOURCE_DIR}/cmake/Sanitizers.cmake)
4141

4242
# dsmr_parser_test
43-
file(GLOB_RECURSE dsmr_parser_test_src_files CONFIGURE_DEPENDS "src/*.h" "src/*.cpp")
43+
file(GLOB_RECURSE dsmr_parser_test_src_files CONFIGURE_DEPENDS "src/*.h" "src/*.cpp" "tests/*.h" "tests/*.cpp")
4444
add_executable(dsmr_parser_test ${dsmr_parser_test_src_files})
4545
target_include_directories(dsmr_parser_test PRIVATE ${CMAKE_SOURCE_DIR}/src ${CMAKE_BINARY_DIR}/doctest)
4646
target_compile_features(dsmr_parser_test PRIVATE cxx_std_20)

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ The primary goal is to make the parser independent of the Arduino framework and
44
# Features
55
* Combines all fixes from [matthijskooijman/arduino-dsmr](https://github.com/matthijskooijman/arduino-dsmr) and [glmnet/arduino-dsmr](https://github.com/glmnet/arduino-dsmr) including unmerged pull requests.
66
* Added an extensive unit test suite
7-
* Small refactoring and code optimizations
7+
* Code optimizations
88
* Supported compilers: MSVC, GCC, Clang
99
* Header-only library, no dependencies
1010
* Code can be used on any platform, not only embedded.
Binary file not shown.

src/dsmr_parser/fields.h

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ struct ParsedField {
2626
f.apply(*static_cast<T*>(this));
2727
}
2828
// By defaults, fields have no unit
29-
static const char* unit() { return ""; }
29+
static const char* unit() noexcept { return ""; }
3030
};
3131

3232
template <typename T, size_t minlen, size_t maxlen>
@@ -54,9 +54,9 @@ struct TimestampField : StringField<T, 13, 13> {};
5454
// efficient integer value. The unit() and int_unit() methods on
5555
// FixedField return the corresponding units for these values.
5656
struct FixedValue {
57-
operator float() const { return val(); }
58-
float val() const { return static_cast<float>(_value) / 1000.0f; }
59-
uint32_t int_val() const { return _value; }
57+
operator float() const noexcept { return val(); }
58+
float val() const noexcept { return static_cast<float>(_value) / 1000.0f; }
59+
uint32_t int_val() const noexcept { return _value; }
6060

6161
uint32_t _value;
6262
};
@@ -90,8 +90,8 @@ struct FixedField : ParsedField<T> {
9090
return res_float;
9191
}
9292

93-
static const char* unit() { return _unit; }
94-
static const char* int_unit() { return _int_unit; }
93+
static const char* unit() noexcept { return _unit; }
94+
static const char* int_unit() noexcept { return _int_unit; }
9595
};
9696

9797
struct TimestampedFixedValue : public FixedValue {
@@ -153,7 +153,7 @@ struct IntField : ParsedField<T> {
153153
return res;
154154
}
155155

156-
static const char* unit() { return _unit; }
156+
static const char* unit() noexcept { return _unit; }
157157
};
158158

159159
// Take the average value of multiple values. Example:

src/dsmr_parser/parser.h

Lines changed: 53 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,10 @@
11
#pragma once
22

33
#include "util.h"
4+
#include <unordered_map>
45

56
namespace dsmr_parser {
67

7-
// uses polynomial x^16+x^15+x^2+1
8-
inline uint16_t crc16_update(uint16_t crc, uint8_t data) {
9-
crc ^= data;
10-
for (size_t i = 0; i < 8; ++i) {
11-
if (crc & 1) {
12-
crc = (crc >> 1) ^ 0xA001;
13-
} else {
14-
crc = (crc >> 1);
15-
}
16-
}
17-
return crc;
18-
}
19-
208
// ParsedData is a template for the result of parsing a Dsmr P1 message.
219
// You pass the fields you want to add to it as template arguments.
2210
//
@@ -41,25 +29,38 @@ inline uint16_t crc16_update(uint16_t crc, uint8_t data) {
4129
// to loop over all the fields inside it.
4230
template <typename... Ts>
4331
struct ParsedData final : Ts... {
44-
ParseResult<void> parse_line(const ObisId& obisId, const char* str, const char* end) {
45-
ParseResult<void> res;
46-
const auto& try_one = [&](auto& field) -> bool {
47-
using FieldType = std::decay_t<decltype(field)>;
48-
if (obisId != FieldType::id) {
49-
return false;
50-
}
51-
52-
if (field.present())
53-
res = ParseResult<void>().fail("Duplicate field", str);
54-
else {
55-
field.present() = true;
56-
res = field.parse(str, end);
57-
}
58-
return true;
59-
};
32+
private:
33+
static const auto& fields_map() {
34+
static const auto& m = []() {
35+
const auto hasher = [](const ObisId& id) noexcept {
36+
std::uint64_t x = 0;
37+
std::memcpy(&x, id.v.data(), 6);
38+
return std::hash<std::uint64_t>{}(x);
39+
};
40+
using FieldParseFunc = ParseResult<void> (*)(ParsedData&, const char*, const char*);
41+
std::unordered_map<ObisId, FieldParseFunc, decltype(hasher)> tmp;
42+
(void)std::initializer_list<int>{(tmp.emplace(Ts::id,
43+
[](ParsedData& self, const char* str, const char* end) {
44+
auto& field = static_cast<Ts&>(self);
45+
ParseResult<void> res;
46+
if (field.present())
47+
return res.fail("Duplicate field", str);
48+
field.present() = true;
49+
return field.parse(str, end);
50+
}),
51+
0)...};
52+
return tmp;
53+
}();
54+
return m;
55+
}
6056

61-
const bool found = (try_one(static_cast<Ts&>(*this)) || ...);
62-
return found ? res : ParseResult<void>().until(str);
57+
public:
58+
ParseResult<void> parse_line(const ObisId& obisId, const char* str, const char* end) {
59+
const auto& m = fields_map();
60+
auto it = m.find(obisId);
61+
if (it == m.end())
62+
return ParseResult<void>().until(str);
63+
return it->second(*this, str, end);
6364
}
6465

6566
template <typename F>
@@ -166,7 +167,7 @@ struct NumParser final {
166167
};
167168

168169
struct ObisIdParser final {
169-
static ParseResult<ObisId> parse(const char* str, const char* end) {
170+
static ParseResult<ObisId> parse(const char* str, const char* end) noexcept {
170171
// Parse a Obis ID of the form 1-2:3.4.5.6
171172
// Stops parsing on the first unrecognized character. Any unparsed
172173
// parts are set to 255.
@@ -208,7 +209,7 @@ struct CrcParser final {
208209
private:
209210
static const size_t CRC_LEN = 4;
210211

211-
static bool hex_nibble(char c, uint8_t& out) {
212+
static bool hex_nibble(char c, uint8_t& out) noexcept {
212213
if (c >= '0' && c <= '9') {
213214
out = static_cast<uint8_t>(c - '0');
214215
return true;
@@ -247,13 +248,27 @@ struct CrcParser final {
247248
};
248249

249250
struct P1Parser final {
251+
private:
252+
// uses polynomial x^16+x^15+x^2+1
253+
static uint16_t crc16_update(uint16_t crc, uint8_t data) noexcept {
254+
crc ^= data;
255+
for (size_t i = 0; i < 8; ++i) {
256+
if (crc & 1) {
257+
crc = (crc >> 1) ^ 0xA001;
258+
} else {
259+
crc = (crc >> 1);
260+
}
261+
}
262+
return crc;
263+
}
250264

265+
public:
251266
// Parse a complete P1 telegram. The string passed should start
252267
// with '/' and run up to and including the ! and the following
253268
// four byte checksum. It's ok if the string is longer, the .next
254269
// pointer in the result will indicate the next unprocessed byte.
255270
template <typename... Ts>
256-
static ParseResult<void> parse(ParsedData<Ts...>* data, const char* str, size_t n, bool unknown_error = false, bool check_crc = true) {
271+
static ParseResult<void> parse(ParsedData<Ts...>& data, const char* str, size_t n, bool unknown_error = false, bool check_crc = true) {
257272
ParseResult<void> res;
258273

259274
const char* const buf_begin = str;
@@ -303,7 +318,7 @@ struct P1Parser final {
303318
// character after the leading /, end should point to the ! before the
304319
// checksum. Does not verify the checksum.
305320
template <typename... Ts>
306-
static ParseResult<void> parse_data(ParsedData<Ts...>* data, const char* str, const char* end, bool unknown_error = false) {
321+
static ParseResult<void> parse_data(ParsedData<Ts...>& data, const char* str, const char* end, bool unknown_error = false) {
307322
// Split into lines and parse those
308323
const char* line_end = str;
309324
const char* line_start = str;
@@ -327,7 +342,7 @@ struct P1Parser final {
327342
//
328343
// Offer it for processing using the all-ones Obis ID, which
329344
// is not otherwise valid.
330-
ParseResult<void> tmp = data->parse_line(ObisId(255, 255, 255, 255, 255, 255), line_start, line_end);
345+
ParseResult<void> tmp = data.parse_line(ObisId(255, 255, 255, 255, 255, 255), line_start, line_end);
331346
if (tmp.err)
332347
return tmp;
333348
line_start = ++line_end;
@@ -383,7 +398,7 @@ struct P1Parser final {
383398
}
384399

385400
template <typename Data>
386-
static ParseResult<void> parse_line(Data* data, const char* line, const char* end, bool unknown_error) {
401+
static ParseResult<void> parse_line(Data& data, const char* line, const char* end, bool unknown_error) {
387402
ParseResult<void> res;
388403
if (line == end)
389404
return res;
@@ -392,7 +407,7 @@ struct P1Parser final {
392407
if (idres.err)
393408
return idres;
394409

395-
ParseResult<void> datares = data->parse_line(idres.result, idres.next, end);
410+
ParseResult<void> datares = data.parse_line(idres.result, idres.next, end);
396411
if (datares.err)
397412
return datares;
398413

@@ -407,5 +422,4 @@ struct P1Parser final {
407422
return res.until(end);
408423
}
409424
};
410-
411425
}

src/dsmr_parser/util.h

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,19 +84,21 @@ struct ParseResult final : public _ParseResult<ParseResult<T>, T> {
8484
const char* err = nullptr;
8585
const char* ctx = nullptr;
8686

87-
ParseResult& fail(const char* error, const char* context = nullptr) {
87+
ParseResult& fail(const char* error, const char* context = nullptr) noexcept {
8888
this->err = error;
8989
this->ctx = context;
9090
return *this;
9191
}
92-
ParseResult& until(const char* nextToken) {
92+
93+
ParseResult& until(const char* nextToken) noexcept {
9394
this->next = nextToken;
9495
return *this;
9596
}
97+
9698
ParseResult() = default;
9799

98100
template <typename T2>
99-
ParseResult(const ParseResult<T2>& other) : next(other.next), err(other.err), ctx(other.ctx) {}
101+
ParseResult(const ParseResult<T2>& other) noexcept : next(other.next), err(other.err), ctx(other.ctx) {}
100102

101103
// Returns the error, including context in a fancy multi-line format.
102104
// The start and end passed are the first and one-past-the-end
@@ -136,10 +138,11 @@ struct ParseResult final : public _ParseResult<ParseResult<T>, T> {
136138
// An OBIS id is 6 bytes, usually noted as a-b:c.d.e.f. Here we put them in an array for easy parsing.
137139
struct ObisId final {
138140
std::array<uint8_t, 6> v{};
139-
constexpr ObisId(const uint8_t a, const uint8_t b = 255, const uint8_t c = 255, const uint8_t d = 255, const uint8_t e = 255, const uint8_t f = 255) noexcept
141+
constexpr explicit ObisId(const uint8_t a, const uint8_t b = 255, const uint8_t c = 255, const uint8_t d = 255, const uint8_t e = 255,
142+
const uint8_t f = 255) noexcept
140143
: v{a, b, c, d, e, f} {};
141144
ObisId() = default;
142-
bool operator==(const ObisId&) const = default;
145+
auto operator<=>(const ObisId&) const = default;
143146
};
144147

145148
}

0 commit comments

Comments
 (0)