/** * Arduino DSMR parser. * * This software is licensed under the MIT License. * * Copyright (c) 2015 Matthijs Kooijman * * 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 struct ParsedData; /** * Base case: No fields present. */ template<> struct ParsedData<> { ParseResult __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().until(str); } template 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 struct ParsedData : public T, ParsedData { /** * 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 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 __attribute__((__always_inline__)) parse_line_inlined(const ObisId& id, const char *str, const char *end) { if (id == T::id) { if (T::present()) return ParseResult().fail((const __FlashStringHelper*)DUPLICATE_FIELD, str); T::present() = true; return T::parse(str, end); } return ParsedData::parse_line_inlined(id, str, end); } template void applyEach(F&& f) { applyEach_inlined(f); } template void __attribute__((__always_inline__)) applyEach_inlined(F&& f) { T::apply(f); return ParsedData::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::all_present_inlined(); } }; struct StringParser { 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); 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 parse(size_t max_decimals, const char* unit, const char *str, const char *end) { ParseResult 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 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 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 parse(const char *str, const char *end) { ParseResult 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 static ParseResult parse(ParsedData *data, const char *str, size_t n, bool unknown_error = false) { ParseResult 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 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 static ParseResult parse_data(ParsedData *data, const char *str, const char *end, bool unknown_error = false) { ParseResult 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 // 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 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 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 static ParseResult parse_line(Data *data, const char *line, const char *end, bool unknown_error) { ParseResult res; if (line == end) return res; ParseResult idres = ObisIdParser::parse(line, end); if (idres.err) return idres; ParseResult 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