Files
P1-wifi-esp12e/P1_gateway_FW/lib/dsmr/parser.h
2021-06-14 08:24:14 +02:00

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