426 lines
13 KiB
C++
426 lines
13 KiB
C++
/**
|
|
* Arduino DSMR parser.
|
|
*
|
|
* This software is licensed under the MIT License.
|
|
*
|
|
* Copyright (c) 2015 Matthijs Kooijman <matthijs@stdin.nl>
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining
|
|
* a copy of this software and associated documentation files (the
|
|
* "Software"), to deal in the Software without restriction, including
|
|
* without limitation the rights to use, copy, modify, merge, publish,
|
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
|
* permit persons to whom the Software is furnished to do so, subject to
|
|
* the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
* SOFTWARE.
|
|
*
|
|
* Message parsing core
|
|
*/
|
|
|
|
#ifndef DSMR_INCLUDE_PARSER_H
|
|
#define DSMR_INCLUDE_PARSER_H
|
|
|
|
#include "crc16.h"
|
|
#include "util.h"
|
|
|
|
namespace dsmr {
|
|
|
|
/**
|
|
* ParsedData is a template for the result of parsing a Dsmr P1 message.
|
|
* You pass the fields you want to add to it as template arguments.
|
|
*
|
|
* This template will then generate a class that extends all the fields
|
|
* passed (the fields really are classes themselves). Since each field
|
|
* class has a single member variable, with the same name as the field
|
|
* class, all of these fields will be available on the generated class.
|
|
*
|
|
* In other words, if I have:
|
|
*
|
|
* using MyData = ParsedData<
|
|
* identification,
|
|
* equipment_id
|
|
* >;
|
|
*
|
|
* MyData data;
|
|
*
|
|
* then I can refer to the fields like data.identification and
|
|
* data.equipment_id normally.
|
|
*
|
|
* Furthermore, this class offers some helper methods that can be used
|
|
* to loop over all the fields inside it.
|
|
*/
|
|
template<typename... Ts>
|
|
struct ParsedData;
|
|
|
|
/**
|
|
* Base case: No fields present.
|
|
*/
|
|
template<>
|
|
struct ParsedData<> {
|
|
ParseResult<void> __attribute__((__always_inline__)) parse_line_inlined(const ObisId& /* id */, const char *str, const char * /* end */) {
|
|
// Parsing succeeded, but found no matching handler (so return
|
|
// set the next pointer to show nothing was parsed).
|
|
return ParseResult<void>().until(str);
|
|
}
|
|
|
|
template<typename F>
|
|
void __attribute__((__always_inline__)) applyEach_inlined(F&& /* f */) {
|
|
// Nothing to do
|
|
}
|
|
|
|
bool all_present_inlined() {
|
|
return true;
|
|
}
|
|
};
|
|
|
|
// Do not use F() for multiply-used strings (including strings used from
|
|
// multiple template instantiations), that would result in multiple
|
|
// instances of the string in the binary
|
|
static constexpr char DUPLICATE_FIELD[] DSMR_PROGMEM = "Duplicate field";
|
|
|
|
/**
|
|
* General case: At least one typename is passed.
|
|
*/
|
|
template<typename T, typename... Ts>
|
|
struct ParsedData<T, Ts...> : public T, ParsedData<Ts...> {
|
|
/**
|
|
* This method is used by the parser to parse a single line. The
|
|
* OBIS id of the line is passed, and this method recursively finds a
|
|
* field with a matching id. If any, it calls it's parse method, which
|
|
* parses the value and stores it in the field.
|
|
*/
|
|
ParseResult<void> parse_line(const ObisId& id, const char *str, const char *end) {
|
|
return parse_line_inlined(id, str, end);
|
|
}
|
|
|
|
/**
|
|
* always_inline version of parse_line. This is a separate method, to
|
|
* allow recursively inlining all calls, but still have a non-inlined
|
|
* top-level parse_line method.
|
|
*/
|
|
ParseResult<void> __attribute__((__always_inline__)) parse_line_inlined(const ObisId& id, const char *str, const char *end) {
|
|
if (id == T::id) {
|
|
if (T::present())
|
|
return ParseResult<void>().fail((const __FlashStringHelper*)DUPLICATE_FIELD, str);
|
|
T::present() = true;
|
|
return T::parse(str, end);
|
|
}
|
|
return ParsedData<Ts...>::parse_line_inlined(id, str, end);
|
|
}
|
|
|
|
template<typename F>
|
|
void applyEach(F&& f) {
|
|
applyEach_inlined(f);
|
|
}
|
|
|
|
template<typename F>
|
|
void __attribute__((__always_inline__)) applyEach_inlined(F&& f) {
|
|
T::apply(f);
|
|
return ParsedData<Ts...>::applyEach_inlined(f);
|
|
}
|
|
|
|
/**
|
|
* Returns true when all defined fields are present.
|
|
*/
|
|
bool all_present() {
|
|
return all_present_inlined();
|
|
}
|
|
|
|
bool all_present_inlined() {
|
|
return T::present() && ParsedData<Ts...>::all_present_inlined();
|
|
}
|
|
};
|
|
|
|
|
|
struct StringParser {
|
|
static ParseResult<String> parse_string(size_t min, size_t max, const char *str, const char *end) {
|
|
ParseResult<String> res;
|
|
if (str >= end || *str != '(')
|
|
return res.fail(F("Missing ("), str);
|
|
|
|
const char *str_start = str + 1; // Skip (
|
|
const char *str_end = str_start;
|
|
|
|
while(str_end < end && *str_end != ')')
|
|
++str_end;
|
|
|
|
if (str_end == end)
|
|
return res.fail(F("Missing )"), str_end);
|
|
|
|
size_t len = str_end - str_start;
|
|
if (len < min || len > max)
|
|
return res.fail(F("Invalid string length"), str_start);
|
|
|
|
concat_hack(res.result, str_start, len);
|
|
|
|
return res.until(str_end + 1); // Skip )
|
|
}
|
|
};
|
|
|
|
// Do not use F() for multiply-used strings (including strings used from
|
|
// multiple template instantiations), that would result in multiple
|
|
// instances of the string in the binary
|
|
static constexpr char INVALID_NUMBER[] DSMR_PROGMEM = "Invalid number";
|
|
static constexpr char INVALID_UNIT[] DSMR_PROGMEM = "Invalid unit";
|
|
|
|
struct NumParser {
|
|
static ParseResult<uint32_t> parse(size_t max_decimals, const char* unit, const char *str, const char *end) {
|
|
ParseResult<uint32_t> res;
|
|
if (str >= end || *str != '(')
|
|
return res.fail(F("Missing ("), str);
|
|
|
|
const char *num_start = str + 1; // Skip (
|
|
const char *num_end = num_start;
|
|
|
|
uint32_t value = 0;
|
|
|
|
// Parse integer part
|
|
while(num_end < end && !strchr("*.)", *num_end)) {
|
|
if (*num_end < '0' || *num_end > '9')
|
|
return res.fail((const __FlashStringHelper*)INVALID_NUMBER, num_end);
|
|
value *= 10;
|
|
value += *num_end - '0';
|
|
++num_end;
|
|
}
|
|
|
|
// Parse decimal part, if any
|
|
if (max_decimals && num_end < end && *num_end == '.') {
|
|
++num_end;
|
|
|
|
while(num_end < end && !strchr("*)", *num_end) && max_decimals--) {
|
|
if (*num_end < '0' || *num_end > '9')
|
|
return res.fail((const __FlashStringHelper*)INVALID_NUMBER, num_end);
|
|
value *= 10;
|
|
value += *num_end - '0';
|
|
++num_end;
|
|
}
|
|
}
|
|
|
|
// Fill in missing decimals with zeroes
|
|
while(max_decimals--)
|
|
value *= 10;
|
|
|
|
if (unit && *unit) {
|
|
if (num_end >= end || *num_end != '*')
|
|
return res.fail(F("Missing unit"), num_end);
|
|
const char *unit_start = ++num_end; // skip *
|
|
while(num_end < end && *num_end != ')' && *unit) {
|
|
if (*num_end++ != *unit++)
|
|
return res.fail((const __FlashStringHelper*)INVALID_UNIT, unit_start);
|
|
}
|
|
if (*unit)
|
|
return res.fail((const __FlashStringHelper*)INVALID_UNIT, unit_start);
|
|
}
|
|
|
|
if (num_end >= end || *num_end != ')')
|
|
return res.fail(F("Extra data"), num_end);
|
|
|
|
return res.succeed(value).until(num_end + 1); // Skip )
|
|
}
|
|
};
|
|
|
|
struct ObisIdParser {
|
|
static ParseResult<ObisId> parse(const char *str, const char *end) {
|
|
// Parse a Obis ID of the form 1-2:3.4.5.6
|
|
// Stops parsing on the first unrecognized character. Any unparsed
|
|
// parts are set to 255.
|
|
ParseResult<ObisId> res;
|
|
ObisId& id = res.result;
|
|
res.next = str;
|
|
uint8_t part = 0;
|
|
while (res.next < end) {
|
|
char c = *res.next;
|
|
|
|
if (c >= '0' && c <= '9') {
|
|
uint8_t digit = c - '0';
|
|
if (id.v[part] > 25 || (id.v[part] == 25 && digit > 5))
|
|
return res.fail(F("Obis ID has number over 255"), res.next);
|
|
id.v[part] = id.v[part] * 10 + digit;
|
|
} else if (part == 0 && c == '-') {
|
|
part++;
|
|
} else if (part == 1 && c == ':') {
|
|
part++;
|
|
} else if (part > 1 && part < 5 && c == '.') {
|
|
part++;
|
|
} else {
|
|
break;
|
|
}
|
|
++res.next;
|
|
}
|
|
|
|
if (res.next == str)
|
|
return res.fail(F("OBIS id Empty"), str);
|
|
|
|
for (++part; part < 6; ++part)
|
|
id.v[part] = 255;
|
|
|
|
return res;
|
|
}
|
|
};
|
|
|
|
struct CrcParser {
|
|
static const size_t CRC_LEN = 4;
|
|
|
|
// Parse a crc value. str must point to the first of the four hex
|
|
// bytes in the CRC.
|
|
static ParseResult<uint16_t> parse(const char *str, const char *end) {
|
|
ParseResult<uint16_t> res;
|
|
// This should never happen with the code in this library, but
|
|
// check anyway
|
|
if (str + CRC_LEN > end)
|
|
return res.fail(F("No checksum found"), str);
|
|
|
|
// A bit of a messy way to parse the checksum, but all
|
|
// integer-parse functions assume nul-termination
|
|
char buf[CRC_LEN + 1];
|
|
memcpy(buf, str, CRC_LEN);
|
|
buf[CRC_LEN] = '\0';
|
|
char *endp;
|
|
uint16_t check = strtoul(buf, &endp, 16);
|
|
|
|
// See if all four bytes formed a valid number
|
|
if (endp != buf + CRC_LEN)
|
|
return res.fail(F("Incomplete or malformed checksum"), str);
|
|
|
|
res.next = str + CRC_LEN;
|
|
return res.succeed(check);
|
|
}
|
|
};
|
|
|
|
struct P1Parser {
|
|
/**
|
|
* Parse a complete P1 telegram. The string passed should start
|
|
* with '/' and run up to and including the ! and the following
|
|
* four byte checksum. It's ok if the string is longer, the .next
|
|
* pointer in the result will indicate the next unprocessed byte.
|
|
*/
|
|
template <typename... Ts>
|
|
static ParseResult<void> parse(ParsedData<Ts...> *data, const char *str, size_t n, bool unknown_error = false) {
|
|
ParseResult<void> res;
|
|
if (!n || str[0] != '/')
|
|
return res.fail(F("Data should start with /"), str);
|
|
|
|
// Skip /
|
|
const char *data_start = str + 1;
|
|
|
|
// Look for ! that terminates the data
|
|
const char *data_end = data_start;
|
|
uint16_t crc = _crc16_update(0, *str); // Include the / in CRC
|
|
while (data_end < str + n && *data_end != '!') {
|
|
crc = _crc16_update(crc, *data_end);
|
|
++data_end;
|
|
}
|
|
|
|
if (data_end >= str + n)
|
|
return res.fail(F("No checksum found"), data_end);
|
|
|
|
crc = _crc16_update(crc, *data_end); // Include the ! in CRC
|
|
|
|
ParseResult<uint16_t> check_res = CrcParser::parse(data_end + 1, str + n);
|
|
if (check_res.err)
|
|
return check_res;
|
|
|
|
// Check CRC
|
|
if (check_res.result != crc)
|
|
return res.fail(F("Checksum mismatch"), data_end + 1);
|
|
|
|
res = parse_data(data, data_start, data_end, unknown_error);
|
|
res.next = check_res.next;
|
|
return res;
|
|
}
|
|
|
|
/**
|
|
* Parse the data part of a message. Str should point to the first
|
|
* character after the leading /, end should point to the ! before the
|
|
* checksum. Does not verify the checksum.
|
|
*/
|
|
template <typename... Ts>
|
|
static ParseResult<void> parse_data(ParsedData<Ts...> *data, const char *str, const char *end, bool unknown_error = false) {
|
|
ParseResult<void> res;
|
|
// Split into lines and parse those
|
|
const char *line_end = str, *line_start = str;
|
|
|
|
// Parse ID line
|
|
while (line_end < end) {
|
|
if (*line_end == '\r' || *line_end == '\n') {
|
|
// The first identification line looks like:
|
|
// XXX5<id string>
|
|
// The DSMR spec is vague on details, but in 62056-21, the X's
|
|
// are a three-leter (registerd) manufacturer ID, the id
|
|
// string is up to 16 chars of arbitrary characters and the
|
|
// '5' is a baud rate indication. 5 apparently means 9600,
|
|
// which DSMR 3.x and below used. It seems that DSMR 2.x
|
|
// passed '3' here (which is mandatory for "mode D"
|
|
// communication according to 62956-21), so we also allow
|
|
// that.
|
|
if (line_start + 3 >= line_end || (line_start[3] != '5' && line_start[3] != '3'))
|
|
return res.fail(F("Invalid identification string"), line_start);
|
|
// Offer it for processing using the all-ones Obis ID, which
|
|
// is not otherwise valid.
|
|
ParseResult<void> tmp = data->parse_line(ObisId(255, 255, 255, 255, 255, 255), line_start, line_end);
|
|
if (tmp.err)
|
|
return tmp;
|
|
line_start = ++line_end;
|
|
break;
|
|
}
|
|
++line_end;
|
|
}
|
|
|
|
// Parse data lines
|
|
while (line_end < end) {
|
|
if (*line_end == '\r' || *line_end == '\n') {
|
|
ParseResult<void> tmp = parse_line(data, line_start, line_end, unknown_error);
|
|
if (tmp.err)
|
|
return tmp;
|
|
line_start = line_end + 1;
|
|
}
|
|
line_end++;
|
|
}
|
|
|
|
if (line_end != line_start)
|
|
return res.fail(F("Last dataline not CRLF terminated"), line_end);
|
|
|
|
return res;
|
|
}
|
|
|
|
template <typename Data>
|
|
static ParseResult<void> parse_line(Data *data, const char *line, const char *end, bool unknown_error) {
|
|
ParseResult<void> res;
|
|
if (line == end)
|
|
return res;
|
|
|
|
ParseResult<ObisId> idres = ObisIdParser::parse(line, end);
|
|
if (idres.err)
|
|
return idres;
|
|
|
|
ParseResult<void> datares = data->parse_line(idres.result, idres.next, end);
|
|
if (datares.err)
|
|
return datares;
|
|
|
|
// If datares.next didn't move at all, there was no parser for
|
|
// this field, that's ok. But if it did move, but not all the way
|
|
// to the end, that's an error.
|
|
if (datares.next != idres.next && datares.next != end)
|
|
return res.fail(F("Trailing characters on data line"), datares.next);
|
|
else if (datares.next == idres.next && unknown_error)
|
|
return res.fail(F("Unknown field"), line);
|
|
|
|
return res.until(end);
|
|
}
|
|
};
|
|
|
|
} // namespace dsmr
|
|
|
|
#endif // DSMR_INCLUDE_PARSER_H
|