├── README.md ├── examples ├── minimal_parse │ └── minimal_parse.ino ├── parse │ └── parse.ino └── read │ └── read.ino ├── library.properties ├── specs ├── DSMR v3.0 P1 Companion Standard.pdf ├── DSMR v4.0 Main Document.pdf ├── DSMR v4.0.4 P1 Companion Standard.pdf ├── DSMR v4.0.7 GPRS Companion Standard.pdf ├── DSMR v4.0.7 Main Document.pdf ├── DSMR v4.0.7 P1 Companion Standard.pdf ├── DSMR v4.0.7 P2 Companion Standard.pdf ├── DSMR v4.0.7 P3 Companion Standard.pdf ├── DSMR v4.0.7 Release Notes.pdf ├── DSMR v4.2.2 GPRS Companion Standard.pdf ├── DSMR v4.2.2 Main Document.pdf ├── DSMR v4.2.2 P1 Companion Standard.pdf ├── DSMR v4.2.2 P2 Companion Standard.pdf ├── DSMR v4.2.2 P3 Companion Standard.pdf ├── DSMR v4.2.2 Release Notes.pdf ├── DSMR v5.0.2 P1 Companion Standard.pdf ├── Swedish HAN H1 port v2.0 specification.pdf └── Swedish HAN Interface description Aidon v1.7A.pdf └── src ├── dsmr.h └── dsmr ├── crc16.h ├── fields.cpp ├── fields.h ├── parser.h ├── reader.h └── util.h /README.md: -------------------------------------------------------------------------------- 1 | Arduino Dutch Smart meter (DSMR) parser 2 | ======================================= 3 | This is an Arduino library for interfacing with Dutch smart meters, through 4 | their P1 port. This library can take care of controlling the "request" pin, 5 | reading messages and parsing them. 6 | 7 | This code was written for Arduino, but most of the parsing code it is pretty 8 | generic C++ (except for the Arduino- and AVR-based string handling), so it 9 | should be possible to adapt for use outside of the Arduino environment. 10 | 11 | When using Arduino, version 1.6.6 or above is required because this 12 | library needs C++11 support which was enabled in that version. 13 | 14 | Protocol 15 | -------- 16 | Every smart meter in the Netherlands has to comply with the Dutch Smart 17 | Meter Requirements (DSMR). At the time of writing, DSMR 4.x is the 18 | current version. The DSMR 5.0 P1 specification is available and expected to 19 | be used in smart meters starting in 2016. This code should support both 20 | the 4.x and 5.0 specifications. 3.x meters might also work, but this has 21 | not been verified or tested (feedback welcome). 22 | 23 | The DSMR specifications can be found on [the site of Netbeheer 24 | Nederland][netbeheer], in particular on [this 25 | page][dossier-slimme-meter]. Of particular interest is the "P1 companion 26 | standard" that specifies the P1 port and protocol (though not very 27 | clearly). Specifications can also be found in the `specs` subdirectory 28 | of this repository (including some older versions that are no longer 29 | online, which is why I started collecting them here). 30 | 31 | [netbeheer]: http://www.netbeheernederland.nl 32 | [dossier-slimme-meter]: https://www.netbeheernederland.nl/dossiers/slimme-meter-15/documenten 33 | 34 | According to DSMR, every smart electricity meter needs to have a P1 35 | port. This is a [6p6c socket][6p6c] (commonly, but incorrectly referred 36 | to as RJ11 or RJ12). Telephone plugs will fit, provided that you have 37 | some that actually have 6 pins wired, or you have just 4 and do not need 38 | power from the P1 port. 39 | 40 | [6p6c]: http://en.wikipedia.org/wiki/Modular_connector#6P6C 41 | 42 | Pinouts and electrical specs can best be looked up in the spec (The 5.0 43 | version is the most clear in this regard, though not everything may 44 | apply to 4.x meters). 45 | 46 | Note that the message format for the P1 port is based on the IEC 62056-21 47 | "mode D" format. That spec is not available for free, though there seem 48 | to be [a version available on the net][iec62056-21]. DLMS also seems a 49 | related standard, but that apparently defines a binary format. It does 50 | seem all of these use "OBIS identifiers" and "COSEM data objects" 51 | (DLMS has [some lists of objects][objlists], of which 1001-7 seems to 52 | somewhat match th DSMR specs), to describe the various properties, 53 | though it's not entirely clear how all of these fit together. However, 54 | the DSMR spec has a complete, though sometimes confusing list of fields 55 | used. 56 | 57 | [iec62056-21]: https://www.ungelesen.net/protagWork/media/downloads/solar-steuerung/iec62056-21%7Bed1.0%7Den_.pdf 58 | [objlists]: http://www.dlms.com/documentation/listofstandardobiscodesandmaintenanceproces/index.html 59 | 60 | A typical P1 message looks something like this: 61 | 62 | /KFM5KAIFA-METER 63 | 64 | 1-0:1.8.1(000671.578*kWh) 65 | 1-0:1.7.0(00.318*kW) 66 | !1E1D 67 | 68 | This includes an identification header at the top, a checksum at the 69 | bottom, and one or more lines of data in the middle. This example is 70 | really stripped down, real messages will have more data in them. 71 | 72 | The first part of the line (e.g. `1-0:1.8.1`) is the (OBIS) id of the 73 | field, which defines the meaning and format of the rest of the line. 74 | 75 | Parsing a message 76 | ----------------- 77 | Unlike other solutions floating around (which typically do some pattern 78 | matching to extract the data they need), this code properly parses 79 | messages, verifying the checksum and really parses each line according 80 | to the specifications. This should make for more reliable parsing, and 81 | allows for useful parser error messages: 82 | 83 | 1-0:1.8.1(000671.578*XWh) 84 | ^ 85 | Error: Invalid unit 86 | 87 | 1-0:1.8.1(0006#71.578*kWh) 88 | ^ 89 | Error: Invalid number 90 | 91 | !6F4A 92 | ^ 93 | Checksum mismatch 94 | 95 | This library uses C++ templates extensively. This allows defining a 96 | custom datatype by listing the fields you are interested in, and then 97 | all necessary parsing will happen automatically. The code generated 98 | parses each line in the message in turn and for each line loops over the 99 | fields in the datatype to find one whose ID matches. If found, the value 100 | is parsed and stored into the corresponding field. 101 | 102 | As an example, consider we want to parse the identification and current 103 | power fields in the example message above. We define a datatype: 104 | 105 | using MyData = ParsedData< 106 | /* String */ identification, 107 | /* FixedValue */ power_delivered 108 | >; 109 | 110 | The syntax is a bit weird because of the template magic used, but the 111 | above essentially defines a struct with members for each field to be 112 | parsed. For each field, there is also an associated `xxx_present` 113 | member, which can be used to check whether the field was present in the 114 | parsed data (if it is false, the associated field contains uninitialized 115 | data). There is some extra stuff in the background, but the `MyData` 116 | can be used just like the below struct. It also takes up the same amount 117 | of space. 118 | 119 | struct MyData { 120 | bool identification_present; 121 | String identification; 122 | bool power_delivered_present; 123 | FixedValue power_delivered; 124 | }; 125 | 126 | After this, call the parser. By passing our custom datatype defined 127 | above, the parser knows what fields to look for. 128 | 129 | MyData data; 130 | ParseResult res = P1Parser::parse(&data, msg, lengthof(msg)); 131 | 132 | Finally, we can check if the parsing was succesful and access the parsed 133 | values as members of `data`: 134 | 135 | if (!res.err && res.all_present()) { 136 | // Succesfully parsed, print results: 137 | Serial.println(data.identification); 138 | Serial.print(data.power_delivered.int_val()); 139 | Serial.println("W"); 140 | } 141 | 142 | In this case, we check whether parsing was successful, but also check 143 | that all defined fields were present in the parsed message (using the 144 | `all_present()` method), to prevent printing undefined values. If you 145 | want to support optional fields, you can use the `xxx_present` members 146 | for each field individually instead. 147 | 148 | Additionally, this template approach allows looping over all available 149 | fields in a generic way, for example to print the parse results with 150 | just a few lines of code. See the parse and read examples for how this 151 | works. 152 | 153 | Note that these examples contain the full list of supported fields, 154 | which causes parsing and printing code to be generated for all those 155 | fields, even if they are not present in the output you want to parse. It 156 | is recommended to limit the list of fields to just the ones that you 157 | need, to make the parsing and printing code smaller and faster. 158 | 159 | Parsed value types 160 | ------------------ 161 | Some values are parsed to an Arduino `String` value or C++ integer type, 162 | those should be fairly straightforward. There are two special types 163 | that need some explanation: `FixedValue` and `TimestampedFixedValue`. 164 | 165 | When looking at the DSMR P1 format, it defines a floating point format. 166 | It is described as `Fn(x,y)`, where `n` is the total number of (decimal) 167 | digits, of which at least `x` and at most `y` are behind the decimal 168 | separator (e.g. fractional digits). 169 | 170 | However, this floating point format is a lot more limited than the C 171 | `float` format. For one, it is decimal-based, not binary. Furthermore, 172 | the decimal separator doesn't float very far, the biggest value for `y` 173 | used is 3. Even more, it seems that for any given field, there is no 174 | actual floating involved, fields have `x` equal to `y`, so the number of 175 | fractional digits is fixed. 176 | 177 | Because of this, parsing into a `float` value isn't really useful (and 178 | on the Arduino, which doesn't have an FPU, very inefficient too). For 179 | this reason, we use the `FixedValue` type, which stores the value as an 180 | integer, in thousands of the original unit. This means that a value of 181 | 1.234kWh is stored as 1234 (effectively the value has been translated to 182 | Wh). 183 | 184 | If you access the field directly, it will automatically be converted to 185 | `float`, keeping the original value. Alternatively, if an integer 186 | version is sufficient, you can call the `int_val()` method to get the 187 | integer version returned. 188 | 189 | // Print as float, in kW 190 | Serial.print(data.power_delivered); 191 | // Print as integer, in W 192 | Serial.print(data.power_delivered.int_val()); 193 | 194 | Additionally there is a `TimestampedFixedValue` type, which works 195 | identically, but additionally has a `timestamp` field which stores 196 | the timestamp sent along with the value. 197 | 198 | // Print as float, in m³ 199 | Serial.print(data.gas_delivered); 200 | // Print timestamp as String 201 | Serial.print(data.gas_delivered.timestamp); 202 | 203 | These timestamps are returned as a String object, exactly as present in 204 | the P1 message (YYMMDDhhmmssX, where X is S or W for summer- or 205 | wintertime). Parsing these into something like a UNIX timestamp is 206 | tricky (think leap years and seconds) and of limited use, so this just 207 | keeps the original format. 208 | 209 | Connecting the P1 port 210 | ---------------------- 211 | The P1 port essentially consists of three parts: 212 | - A 5V power supply (this was not present in 3.x). 213 | - A serial TX pin. This sends meter data using 0/5V signalling, using 214 | idle low. Note that this is the voltage level commonly referred to as 215 | "TTL serial", but the polarity is reversed (more like RS232). This 216 | port uses 115200 bps 8N1 (3.x and before used 9600 bps). 217 | - A request pin - 5V needs to be applied to this pin to start 218 | generating output on the TX pin. 219 | 220 | To connect to an Arduino that has an unused hardware serial port (like 221 | an Arduino Mega, Leonardo or Micro), the signal has to inverted. This 222 | can be done using a dedicated inverter IC, or just a transistor and some 223 | resistors. 224 | 225 | It's also possible to do all of the serial reception, including the 226 | inverting, in software using Arduino's SoftwareSerial library. However, 227 | it seems there are still occasional reception errors when using 228 | SoftwareSerial. 229 | 230 | Slave meters 231 | ------------ 232 | In addition to a smart electricity meter, there can be additional 233 | slave meters attached (e.g., gas, water, thermal and slave electricity 234 | meter). These can talk to the main meter using the (wired or wireless) 235 | MBUS protocol to regularly (hourly for 4.x, every 5 minutes for 5.0) 236 | send over their meter readings. Based on the configuration / connection, 237 | each of these slaves gets an MBUS identifier (1-4). 238 | 239 | In the P1 message, this identifier is used as the second number in the 240 | OBIS identifiers for the fields for the slave. Currently, the code has 241 | the assignment from MBUS identifier to device type hardcoded in 242 | `fields.h`. For the most common configuration of an electricity meter with 243 | a single gas meter as slave, this works straight away. Other 244 | configurations might need changes to `fields.h` to work. 245 | 246 | License 247 | ------- 248 | All of the code and documentation in this library is licensed under the 249 | MIT license, with the exception of the examples, which are licensed 250 | under an even more liberal license. 251 | -------------------------------------------------------------------------------- /examples/minimal_parse/minimal_parse.ino: -------------------------------------------------------------------------------- 1 | /* 2 | * Permission is hereby granted, free of charge, to anyone 3 | * obtaining a copy of this document and accompanying files, 4 | * to do whatever they want with them without any restriction, 5 | * including, but not limited to, copying, modification and redistribution. 6 | * NO WARRANTY OF ANY KIND IS PROVIDED. 7 | * 8 | * Example that shows how to parse a P1 message and automatically print 9 | * the result. 10 | */ 11 | 12 | #include "dsmr.h" 13 | 14 | // Data to parse 15 | const char msg[] = 16 | "/KFM5KAIFA-METER\r\n" 17 | "\r\n" 18 | "1-0:1.8.1(000671.578*kWh)\r\n" 19 | "1-0:1.7.0(00.318*kW)\r\n" 20 | "!1E1D\r\n"; 21 | 22 | /** 23 | * Define the data we're interested in, as well as the datastructure to 24 | * hold the parsed data. 25 | * Each template argument below results in a field of the same name. 26 | */ 27 | using MyData = ParsedData< 28 | /* String */ identification, 29 | /* FixedValue */ power_delivered 30 | >; 31 | 32 | void setup() { 33 | Serial.begin(115200); 34 | 35 | MyData data; 36 | ParseResult res = P1Parser::parse(&data, msg, lengthof(msg)); 37 | if (res.err) { 38 | // Parsing error, show it 39 | Serial.println(res.fullError(msg, msg + lengthof(msg))); 40 | } else if (!data.all_present()) { 41 | Serial.println("Some fields are missing"); 42 | } else { 43 | // Succesfully parsed, print results: 44 | Serial.println(data.identification); 45 | Serial.print(data.power_delivered.int_val()); 46 | Serial.println("W"); 47 | } 48 | } 49 | 50 | void loop () { 51 | } 52 | -------------------------------------------------------------------------------- /examples/parse/parse.ino: -------------------------------------------------------------------------------- 1 | /* 2 | * Permission is hereby granted, free of charge, to anyone 3 | * obtaining a copy of this document and accompanying files, 4 | * to do whatever they want with them without any restriction, 5 | * including, but not limited to, copying, modification and redistribution. 6 | * NO WARRANTY OF ANY KIND IS PROVIDED. 7 | * 8 | * Example that shows how to parse a P1 message and automatically print 9 | * the result. 10 | */ 11 | 12 | #include "dsmr.h" 13 | 14 | // Data to parse 15 | const char raw[] = 16 | "/KFM5KAIFA-METER\r\n" 17 | "\r\n" 18 | "1-3:0.2.8(40)\r\n" 19 | "0-0:1.0.0(150117185916W)\r\n" 20 | "0-0:96.1.1(0000000000000000000000000000000000)\r\n" 21 | "1-0:1.8.1(000671.578*kWh)\r\n" 22 | "1-0:1.8.2(000842.472*kWh)\r\n" 23 | "1-0:2.8.1(000000.000*kWh)\r\n" 24 | "1-0:2.8.2(000000.000*kWh)\r\n" 25 | "0-0:96.14.0(0001)\r\n" 26 | "1-0:1.7.0(00.333*kW)\r\n" 27 | "1-0:2.7.0(00.000*kW)\r\n" 28 | "0-0:17.0.0(999.9*kW)\r\n" 29 | "0-0:96.3.10(1)\r\n" 30 | "0-0:96.7.21(00008)\r\n" 31 | "0-0:96.7.9(00007)\r\n" 32 | "1-0:99.97.0(1)(0-0:96.7.19)(000101000001W)(2147483647*s)\r\n" 33 | "1-0:32.32.0(00000)\r\n" 34 | "1-0:32.36.0(00000)\r\n" 35 | "0-0:96.13.1()\r\n" 36 | "0-0:96.13.0()\r\n" 37 | "1-0:31.7.0(001*A)\r\n" 38 | "1-0:21.7.0(00.332*kW)\r\n" 39 | "1-0:22.7.0(00.000*kW)\r\n" 40 | "0-1:24.1.0(003)\r\n" 41 | "0-1:96.1.0(0000000000000000000000000000000000)\r\n" 42 | "0-1:24.2.1(150117180000W)(00473.789*m3)\r\n" 43 | "0-1:24.4.0(1)\r\n" 44 | "!6F4A\r\n"; 45 | 46 | /** 47 | * Define the data we're interested in, as well as the datastructure to 48 | * hold the parsed data. This list shows all supported fields, remove 49 | * any fields you are not using from the below list to make the parsing 50 | * and printing code smaller. 51 | * Each template argument below results in a field of the same name. 52 | */ 53 | using MyData = ParsedData< 54 | /* String */ identification, 55 | /* String */ p1_version, 56 | /* String */ timestamp, 57 | /* String */ equipment_id, 58 | /* FixedValue */ energy_delivered_tariff1, 59 | /* FixedValue */ energy_delivered_tariff2, 60 | /* FixedValue */ energy_returned_tariff1, 61 | /* FixedValue */ energy_returned_tariff2, 62 | /* String */ electricity_tariff, 63 | /* FixedValue */ power_delivered, 64 | /* FixedValue */ power_returned, 65 | /* FixedValue */ electricity_threshold, 66 | /* uint8_t */ electricity_switch_position, 67 | /* uint32_t */ electricity_failures, 68 | /* uint32_t */ electricity_long_failures, 69 | /* String */ electricity_failure_log, 70 | /* uint32_t */ electricity_sags_l1, 71 | /* uint32_t */ electricity_sags_l2, 72 | /* uint32_t */ electricity_sags_l3, 73 | /* uint32_t */ electricity_swells_l1, 74 | /* uint32_t */ electricity_swells_l2, 75 | /* uint32_t */ electricity_swells_l3, 76 | /* String */ message_short, 77 | /* String */ message_long, 78 | /* FixedValue */ voltage_l1, 79 | /* FixedValue */ voltage_l2, 80 | /* FixedValue */ voltage_l3, 81 | /* FixedValue */ current_l1, 82 | /* FixedValue */ current_l2, 83 | /* FixedValue */ current_l3, 84 | /* FixedValue */ power_delivered_l1, 85 | /* FixedValue */ power_delivered_l2, 86 | /* FixedValue */ power_delivered_l3, 87 | /* FixedValue */ power_returned_l1, 88 | /* FixedValue */ power_returned_l2, 89 | /* FixedValue */ power_returned_l3, 90 | /* uint16_t */ gas_device_type, 91 | /* String */ gas_equipment_id, 92 | /* uint8_t */ gas_valve_position, 93 | /* TimestampedFixedValue */ gas_delivered, 94 | /* uint16_t */ thermal_device_type, 95 | /* String */ thermal_equipment_id, 96 | /* uint8_t */ thermal_valve_position, 97 | /* TimestampedFixedValue */ thermal_delivered, 98 | /* uint16_t */ water_device_type, 99 | /* String */ water_equipment_id, 100 | /* uint8_t */ water_valve_position, 101 | /* TimestampedFixedValue */ water_delivered, 102 | /* uint16_t */ slave_device_type, 103 | /* String */ slave_equipment_id, 104 | /* uint8_t */ slave_valve_position, 105 | /* TimestampedFixedValue */ slave_delivered 106 | >; 107 | 108 | /** 109 | * This illustrates looping over all parsed fields using the 110 | * ParsedData::applyEach method. 111 | * 112 | * When passed an instance of this Printer object, applyEach will loop 113 | * over each field and call Printer::apply, passing a reference to each 114 | * field in turn. This passes the actual field object, not the field 115 | * value, so each call to Printer::apply will have a differently typed 116 | * parameter. 117 | * 118 | * For this reason, Printer::apply is a template, resulting in one 119 | * distinct apply method for each field used. This allows looking up 120 | * things like Item::name, which is different for every field type, 121 | * without having to resort to virtual method calls (which result in 122 | * extra storage usage). The tradeoff is here that there is more code 123 | * generated (but due to compiler inlining, it's pretty much the same as 124 | * if you just manually printed all field names and values (with no 125 | * cost at all if you don't use the Printer). 126 | */ 127 | struct Printer { 128 | template 129 | void apply(Item &i) { 130 | if (i.present()) { 131 | Serial.print(Item::get_name()); 132 | Serial.print(F(": ")); 133 | Serial.print(i.val()); 134 | Serial.print(Item::unit()); 135 | Serial.println(); 136 | } 137 | } 138 | }; 139 | 140 | void setup() { 141 | Serial.begin(115200); 142 | 143 | MyData data; 144 | ParseResult res = P1Parser::parse(&data, raw, lengthof(raw), true); 145 | if (res.err) { 146 | // Parsing error, show it 147 | Serial.println(res.fullError(raw, raw + lengthof(raw))); 148 | } else { 149 | // Parsed succesfully, print all values 150 | data.applyEach(Printer()); 151 | } 152 | } 153 | 154 | void loop () { 155 | } 156 | -------------------------------------------------------------------------------- /examples/read/read.ino: -------------------------------------------------------------------------------- 1 | /* 2 | * Permission is hereby granted, free of charge, to anyone 3 | * obtaining a copy of this document and accompanying files, 4 | * to do whatever they want with them without any restriction, 5 | * including, but not limited to, copying, modification and redistribution. 6 | * NO WARRANTY OF ANY KIND IS PROVIDED. 7 | * 8 | * Example that shows how to periodically read a P1 message from a 9 | * serial port and automatically print the result. 10 | */ 11 | 12 | #include "dsmr.h" 13 | 14 | /** 15 | * Define the data we're interested in, as well as the datastructure to 16 | * hold the parsed data. This list shows all supported fields, remove 17 | * any fields you are not using from the below list to make the parsing 18 | * and printing code smaller. 19 | * Each template argument below results in a field of the same name. 20 | */ 21 | using MyData = ParsedData< 22 | /* String */ identification, 23 | /* String */ p1_version, 24 | /* String */ timestamp, 25 | /* String */ equipment_id, 26 | /* FixedValue */ energy_delivered_tariff1, 27 | /* FixedValue */ energy_delivered_tariff2, 28 | /* FixedValue */ energy_returned_tariff1, 29 | /* FixedValue */ energy_returned_tariff2, 30 | /* String */ electricity_tariff, 31 | /* FixedValue */ power_delivered, 32 | /* FixedValue */ power_returned, 33 | /* FixedValue */ electricity_threshold, 34 | /* uint8_t */ electricity_switch_position, 35 | /* uint32_t */ electricity_failures, 36 | /* uint32_t */ electricity_long_failures, 37 | /* String */ electricity_failure_log, 38 | /* uint32_t */ electricity_sags_l1, 39 | /* uint32_t */ electricity_sags_l2, 40 | /* uint32_t */ electricity_sags_l3, 41 | /* uint32_t */ electricity_swells_l1, 42 | /* uint32_t */ electricity_swells_l2, 43 | /* uint32_t */ electricity_swells_l3, 44 | /* String */ message_short, 45 | /* String */ message_long, 46 | /* FixedValue */ voltage_l1, 47 | /* FixedValue */ voltage_l2, 48 | /* FixedValue */ voltage_l3, 49 | /* FixedValue */ current_l1, 50 | /* FixedValue */ current_l2, 51 | /* FixedValue */ current_l3, 52 | /* FixedValue */ power_delivered_l1, 53 | /* FixedValue */ power_delivered_l2, 54 | /* FixedValue */ power_delivered_l3, 55 | /* FixedValue */ power_returned_l1, 56 | /* FixedValue */ power_returned_l2, 57 | /* FixedValue */ power_returned_l3, 58 | /* uint16_t */ gas_device_type, 59 | /* String */ gas_equipment_id, 60 | /* uint8_t */ gas_valve_position, 61 | /* TimestampedFixedValue */ gas_delivered, 62 | /* uint16_t */ thermal_device_type, 63 | /* String */ thermal_equipment_id, 64 | /* uint8_t */ thermal_valve_position, 65 | /* TimestampedFixedValue */ thermal_delivered, 66 | /* uint16_t */ water_device_type, 67 | /* String */ water_equipment_id, 68 | /* uint8_t */ water_valve_position, 69 | /* TimestampedFixedValue */ water_delivered, 70 | /* uint16_t */ slave_device_type, 71 | /* String */ slave_equipment_id, 72 | /* uint8_t */ slave_valve_position, 73 | /* TimestampedFixedValue */ slave_delivered 74 | >; 75 | 76 | /** 77 | * This illustrates looping over all parsed fields using the 78 | * ParsedData::applyEach method. 79 | * 80 | * When passed an instance of this Printer object, applyEach will loop 81 | * over each field and call Printer::apply, passing a reference to each 82 | * field in turn. This passes the actual field object, not the field 83 | * value, so each call to Printer::apply will have a differently typed 84 | * parameter. 85 | * 86 | * For this reason, Printer::apply is a template, resulting in one 87 | * distinct apply method for each field used. This allows looking up 88 | * things like Item::get_name(), which is different for every field type, 89 | * without having to resort to virtual method calls (which result in 90 | * extra storage usage). The tradeoff is here that there is more code 91 | * generated (but due to compiler inlining, it's pretty much the same as 92 | * if you just manually printed all field names and values (with no 93 | * cost at all if you don't use the Printer). 94 | */ 95 | struct Printer { 96 | template 97 | void apply(Item &i) { 98 | if (i.present()) { 99 | Serial.print(Item::get_name()); 100 | Serial.print(F(": ")); 101 | Serial.print(i.val()); 102 | Serial.print(Item::unit()); 103 | Serial.println(); 104 | } 105 | } 106 | }; 107 | 108 | // Set up to read from the second serial port, and use D2 as the request 109 | // pin. On boards with only one (USB) serial port, you can also use 110 | // SoftwareSerial. 111 | #ifdef ARDUINO_ARCH_ESP32 112 | // Create Serial1 connected to UART 1 113 | HardwareSerial Serial1(1); 114 | #endif 115 | P1Reader reader(&Serial1, 2); 116 | 117 | unsigned long last; 118 | 119 | void setup() { 120 | Serial.begin(115200); 121 | Serial1.begin(115200); 122 | #ifdef VCC_ENABLE 123 | // This is needed on Pinoccio Scout boards to enable the 3V3 pin. 124 | pinMode(VCC_ENABLE, OUTPUT); 125 | digitalWrite(VCC_ENABLE, HIGH); 126 | #endif 127 | 128 | // start a read right away 129 | reader.enable(true); 130 | last = millis(); 131 | } 132 | 133 | void loop () { 134 | // Allow the reader to check the serial buffer regularly 135 | reader.loop(); 136 | 137 | // Every minute, fire off a one-off reading 138 | unsigned long now = millis(); 139 | if (now - last > 60000) { 140 | reader.enable(true); 141 | last = now; 142 | } 143 | 144 | if (reader.available()) { 145 | MyData data; 146 | String err; 147 | if (reader.parse(&data, &err)) { 148 | // Parse succesful, print result 149 | data.applyEach(Printer()); 150 | } else { 151 | // Parser error, print error 152 | Serial.println(err); 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /library.properties: -------------------------------------------------------------------------------- 1 | name=Dsmr 2 | author=Matthijs Kooijman 3 | email=matthijs@stdin.nl 4 | sentence=Parser and utilities for Dutch Smart Meters (Implementing DSMR) 5 | paragraph= 6 | category=Data Processing 7 | url=https://github.com/matthijskooijman/arduino-dsmr 8 | architectures=* 9 | version=0.1 10 | dependencies= 11 | core-dependencies= 12 | -------------------------------------------------------------------------------- /specs/DSMR v3.0 P1 Companion Standard.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthijskooijman/arduino-dsmr/5a0b558e251fb6ce935f97c4b349740c594f70ee/specs/DSMR v3.0 P1 Companion Standard.pdf -------------------------------------------------------------------------------- /specs/DSMR v4.0 Main Document.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthijskooijman/arduino-dsmr/5a0b558e251fb6ce935f97c4b349740c594f70ee/specs/DSMR v4.0 Main Document.pdf -------------------------------------------------------------------------------- /specs/DSMR v4.0.4 P1 Companion Standard.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthijskooijman/arduino-dsmr/5a0b558e251fb6ce935f97c4b349740c594f70ee/specs/DSMR v4.0.4 P1 Companion Standard.pdf -------------------------------------------------------------------------------- /specs/DSMR v4.0.7 GPRS Companion Standard.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthijskooijman/arduino-dsmr/5a0b558e251fb6ce935f97c4b349740c594f70ee/specs/DSMR v4.0.7 GPRS Companion Standard.pdf -------------------------------------------------------------------------------- /specs/DSMR v4.0.7 Main Document.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthijskooijman/arduino-dsmr/5a0b558e251fb6ce935f97c4b349740c594f70ee/specs/DSMR v4.0.7 Main Document.pdf -------------------------------------------------------------------------------- /specs/DSMR v4.0.7 P1 Companion Standard.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthijskooijman/arduino-dsmr/5a0b558e251fb6ce935f97c4b349740c594f70ee/specs/DSMR v4.0.7 P1 Companion Standard.pdf -------------------------------------------------------------------------------- /specs/DSMR v4.0.7 P2 Companion Standard.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthijskooijman/arduino-dsmr/5a0b558e251fb6ce935f97c4b349740c594f70ee/specs/DSMR v4.0.7 P2 Companion Standard.pdf -------------------------------------------------------------------------------- /specs/DSMR v4.0.7 P3 Companion Standard.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthijskooijman/arduino-dsmr/5a0b558e251fb6ce935f97c4b349740c594f70ee/specs/DSMR v4.0.7 P3 Companion Standard.pdf -------------------------------------------------------------------------------- /specs/DSMR v4.0.7 Release Notes.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthijskooijman/arduino-dsmr/5a0b558e251fb6ce935f97c4b349740c594f70ee/specs/DSMR v4.0.7 Release Notes.pdf -------------------------------------------------------------------------------- /specs/DSMR v4.2.2 GPRS Companion Standard.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthijskooijman/arduino-dsmr/5a0b558e251fb6ce935f97c4b349740c594f70ee/specs/DSMR v4.2.2 GPRS Companion Standard.pdf -------------------------------------------------------------------------------- /specs/DSMR v4.2.2 Main Document.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthijskooijman/arduino-dsmr/5a0b558e251fb6ce935f97c4b349740c594f70ee/specs/DSMR v4.2.2 Main Document.pdf -------------------------------------------------------------------------------- /specs/DSMR v4.2.2 P1 Companion Standard.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthijskooijman/arduino-dsmr/5a0b558e251fb6ce935f97c4b349740c594f70ee/specs/DSMR v4.2.2 P1 Companion Standard.pdf -------------------------------------------------------------------------------- /specs/DSMR v4.2.2 P2 Companion Standard.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthijskooijman/arduino-dsmr/5a0b558e251fb6ce935f97c4b349740c594f70ee/specs/DSMR v4.2.2 P2 Companion Standard.pdf -------------------------------------------------------------------------------- /specs/DSMR v4.2.2 P3 Companion Standard.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthijskooijman/arduino-dsmr/5a0b558e251fb6ce935f97c4b349740c594f70ee/specs/DSMR v4.2.2 P3 Companion Standard.pdf -------------------------------------------------------------------------------- /specs/DSMR v4.2.2 Release Notes.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthijskooijman/arduino-dsmr/5a0b558e251fb6ce935f97c4b349740c594f70ee/specs/DSMR v4.2.2 Release Notes.pdf -------------------------------------------------------------------------------- /specs/DSMR v5.0.2 P1 Companion Standard.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthijskooijman/arduino-dsmr/5a0b558e251fb6ce935f97c4b349740c594f70ee/specs/DSMR v5.0.2 P1 Companion Standard.pdf -------------------------------------------------------------------------------- /specs/Swedish HAN H1 port v2.0 specification.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthijskooijman/arduino-dsmr/5a0b558e251fb6ce935f97c4b349740c594f70ee/specs/Swedish HAN H1 port v2.0 specification.pdf -------------------------------------------------------------------------------- /specs/Swedish HAN Interface description Aidon v1.7A.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthijskooijman/arduino-dsmr/5a0b558e251fb6ce935f97c4b349740c594f70ee/specs/Swedish HAN Interface description Aidon v1.7A.pdf -------------------------------------------------------------------------------- /src/dsmr.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Arduino DSMR parser. 3 | * 4 | * This software is licensed under the MIT License. 5 | * 6 | * Copyright (c) 2015 Matthijs Kooijman 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining 9 | * a copy of this software and associated documentation files (the 10 | * "Software"), to deal in the Software without restriction, including 11 | * without limitation the rights to use, copy, modify, merge, publish, 12 | * distribute, sublicense, and/or sell copies of the Software, and to 13 | * permit persons to whom the Software is furnished to do so, subject to 14 | * the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be 17 | * included in all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 23 | * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 24 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | * SOFTWARE. 27 | * 28 | * Main included file. If you include this, you'll get everything, 29 | * imported into global scope 30 | */ 31 | 32 | #ifndef DSMR_INCLUDE_DSMR_H 33 | #define DSMR_INCLUDE_DSMR_H 34 | 35 | #include "dsmr/parser.h" 36 | #include "dsmr/reader.h" 37 | #include "dsmr/fields.h" 38 | 39 | // Allow using everything without the namespace prefixes 40 | using namespace dsmr; 41 | using namespace dsmr::fields; 42 | 43 | #endif // DSMR_INCLUDE_DSMR_H 44 | -------------------------------------------------------------------------------- /src/dsmr/crc16.h: -------------------------------------------------------------------------------- 1 | /* CRC compatibility, adapted from the Teensy 3 core at: 2 | https://github.com/PaulStoffregen/cores/tree/master/teensy3 3 | which was in turn adapted by Paul Stoffregen from the C-only comments here: 4 | http://svn.savannah.nongnu.org/viewvc/trunk/avr-libc/include/util/crc16.h?revision=933&root=avr-libc&view=markup */ 5 | 6 | /* Copyright (c) 2002, 2003, 2004 Marek Michalkiewicz 7 | All rights reserved. 8 | 9 | Redistribution and use in source and binary forms, with or without 10 | modification, are permitted provided that the following conditions are met: 11 | 12 | * Redistributions of source code must retain the above copyright 13 | notice, this list of conditions and the following disclaimer. 14 | 15 | * Redistributions in binary form must reproduce the above copyright 16 | notice, this list of conditions and the following disclaimer in 17 | the documentation and/or other materials provided with the 18 | distribution. 19 | 20 | * Neither the name of the copyright holders nor the names of 21 | contributors may be used to endorse or promote products derived 22 | from this software without specific prior written permission. 23 | 24 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 25 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 26 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 27 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 28 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 29 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 30 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 31 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 32 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 34 | POSSIBILITY OF SUCH DAMAGE. */ 35 | 36 | #ifndef _UTIL_CRC16_H_ 37 | #ifdef ARDUINO_ARCH_AVR 38 | #include 39 | #else 40 | #define _UTIL_CRC16_H_ 41 | #include 42 | 43 | static inline uint16_t _crc16_update(uint16_t crc, uint8_t data) __attribute__((always_inline, unused)); 44 | static inline uint16_t _crc16_update(uint16_t crc, uint8_t data) 45 | { 46 | unsigned int i; 47 | 48 | crc ^= data; 49 | for (i = 0; i < 8; ++i) { 50 | if (crc & 1) { 51 | crc = (crc >> 1) ^ 0xA001; 52 | } else { 53 | crc = (crc >> 1); 54 | } 55 | } 56 | return crc; 57 | } 58 | 59 | static inline uint16_t _crc_xmodem_update(uint16_t crc, uint8_t data) __attribute__((always_inline, unused)); 60 | static inline uint16_t _crc_xmodem_update(uint16_t crc, uint8_t data) 61 | { 62 | unsigned int i; 63 | 64 | crc = crc ^ ((uint16_t)data << 8); 65 | for (i=0; i<8; i++) { 66 | if (crc & 0x8000) { 67 | crc = (crc << 1) ^ 0x1021; 68 | } else { 69 | crc <<= 1; 70 | } 71 | } 72 | return crc; 73 | } 74 | 75 | static inline uint16_t _crc_ccitt_update (uint16_t crc, uint8_t data) __attribute__((always_inline, unused)); 76 | static inline uint16_t _crc_ccitt_update (uint16_t crc, uint8_t data) 77 | { 78 | data ^= (crc & 255); 79 | data ^= data << 4; 80 | 81 | return ((((uint16_t)data << 8) | (crc >> 8)) ^ (uint8_t)(data >> 4) 82 | ^ ((uint16_t)data << 3)); 83 | } 84 | 85 | static inline uint8_t _crc_ibutton_update(uint8_t crc, uint8_t data) __attribute__((always_inline, unused)); 86 | static inline uint8_t _crc_ibutton_update(uint8_t crc, uint8_t data) 87 | { 88 | unsigned int i; 89 | 90 | crc = crc ^ data; 91 | for (i = 0; i < 8; i++) { 92 | if (crc & 0x01) { 93 | crc = (crc >> 1) ^ 0x8C; 94 | } else { 95 | crc >>= 1; 96 | } 97 | } 98 | return crc; 99 | } 100 | 101 | #endif 102 | #endif 103 | -------------------------------------------------------------------------------- /src/dsmr/fields.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Arduino DSMR parser. 3 | * 4 | * This software is licensed under the MIT License. 5 | * 6 | * Copyright (c) 2015 Matthijs Kooijman 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining 9 | * a copy of this software and associated documentation files (the 10 | * "Software"), to deal in the Software without restriction, including 11 | * without limitation the rights to use, copy, modify, merge, publish, 12 | * distribute, sublicense, and/or sell copies of the Software, and to 13 | * permit persons to whom the Software is furnished to do so, subject to 14 | * the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be 17 | * included in all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 23 | * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 24 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | * SOFTWARE. 27 | * 28 | * Field parsing functions 29 | */ 30 | 31 | #include "fields.h" 32 | 33 | 34 | using namespace dsmr; 35 | using namespace dsmr::fields; 36 | 37 | // Since C++11 it is possible to define the initial values for static 38 | // const members in the class declaration, but if their address is 39 | // taken, they still need a normal definition somewhere (to allocate 40 | // storage). 41 | constexpr char units::none[]; 42 | constexpr char units::kWh[]; 43 | constexpr char units::Wh[]; 44 | constexpr char units::kW[]; 45 | constexpr char units::W[]; 46 | constexpr char units::V[]; 47 | constexpr char units::mV[]; 48 | constexpr char units::A[]; 49 | constexpr char units::mA[]; 50 | constexpr char units::m3[]; 51 | constexpr char units::dm3[]; 52 | constexpr char units::GJ[]; 53 | constexpr char units::MJ[]; 54 | 55 | constexpr ObisId identification::id; 56 | constexpr char identification::name_progmem[]; 57 | 58 | constexpr ObisId p1_version::id; 59 | constexpr char p1_version::name_progmem[]; 60 | 61 | constexpr ObisId timestamp::id; 62 | constexpr char timestamp::name_progmem[]; 63 | 64 | constexpr ObisId equipment_id::id; 65 | constexpr char equipment_id::name_progmem[]; 66 | 67 | constexpr ObisId energy_delivered_tariff1::id; 68 | constexpr char energy_delivered_tariff1::name_progmem[]; 69 | 70 | constexpr ObisId energy_delivered_tariff2::id; 71 | constexpr char energy_delivered_tariff2::name_progmem[]; 72 | 73 | constexpr ObisId energy_returned_tariff1::id; 74 | constexpr char energy_returned_tariff1::name_progmem[]; 75 | 76 | constexpr ObisId energy_returned_tariff2::id; 77 | constexpr char energy_returned_tariff2::name_progmem[]; 78 | 79 | constexpr ObisId electricity_tariff::id; 80 | constexpr char electricity_tariff::name_progmem[]; 81 | 82 | constexpr ObisId power_delivered::id; 83 | constexpr char power_delivered::name_progmem[]; 84 | 85 | constexpr ObisId power_returned::id; 86 | constexpr char power_returned::name_progmem[]; 87 | 88 | constexpr ObisId electricity_threshold::id; 89 | constexpr char electricity_threshold::name_progmem[]; 90 | 91 | constexpr ObisId electricity_switch_position::id; 92 | constexpr char electricity_switch_position::name_progmem[]; 93 | 94 | constexpr ObisId electricity_failures::id; 95 | constexpr char electricity_failures::name_progmem[]; 96 | 97 | constexpr ObisId electricity_long_failures::id; 98 | constexpr char electricity_long_failures::name_progmem[]; 99 | 100 | constexpr ObisId electricity_failure_log::id; 101 | constexpr char electricity_failure_log::name_progmem[]; 102 | 103 | constexpr ObisId electricity_sags_l1::id; 104 | constexpr char electricity_sags_l1::name_progmem[]; 105 | 106 | constexpr ObisId electricity_sags_l2::id; 107 | constexpr char electricity_sags_l2::name_progmem[]; 108 | 109 | constexpr ObisId electricity_sags_l3::id; 110 | constexpr char electricity_sags_l3::name_progmem[]; 111 | 112 | constexpr ObisId electricity_swells_l1::id; 113 | constexpr char electricity_swells_l1::name_progmem[]; 114 | 115 | constexpr ObisId electricity_swells_l2::id; 116 | constexpr char electricity_swells_l2::name_progmem[]; 117 | 118 | constexpr ObisId electricity_swells_l3::id; 119 | constexpr char electricity_swells_l3::name_progmem[]; 120 | 121 | constexpr ObisId message_short::id; 122 | constexpr char message_short::name_progmem[]; 123 | 124 | constexpr ObisId message_long::id; 125 | constexpr char message_long::name_progmem[]; 126 | 127 | constexpr ObisId voltage_l1::id; 128 | constexpr char voltage_l1::name_progmem[]; 129 | 130 | constexpr ObisId voltage_l2::id; 131 | constexpr char voltage_l2::name_progmem[]; 132 | 133 | constexpr ObisId voltage_l3::id; 134 | constexpr char voltage_l3::name_progmem[]; 135 | 136 | constexpr ObisId current_l1::id; 137 | constexpr char current_l1::name_progmem[]; 138 | 139 | constexpr ObisId current_l2::id; 140 | constexpr char current_l2::name_progmem[]; 141 | 142 | constexpr ObisId current_l3::id; 143 | constexpr char current_l3::name_progmem[]; 144 | 145 | constexpr ObisId power_delivered_l1::id; 146 | constexpr char power_delivered_l1::name_progmem[]; 147 | 148 | constexpr ObisId power_delivered_l2::id; 149 | constexpr char power_delivered_l2::name_progmem[]; 150 | 151 | constexpr ObisId power_delivered_l3::id; 152 | constexpr char power_delivered_l3::name_progmem[]; 153 | 154 | constexpr ObisId power_returned_l1::id; 155 | constexpr char power_returned_l1::name_progmem[]; 156 | 157 | constexpr ObisId power_returned_l2::id; 158 | constexpr char power_returned_l2::name_progmem[]; 159 | 160 | constexpr ObisId power_returned_l3::id; 161 | constexpr char power_returned_l3::name_progmem[]; 162 | 163 | constexpr ObisId gas_device_type::id; 164 | constexpr char gas_device_type::name_progmem[]; 165 | 166 | constexpr ObisId gas_equipment_id::id; 167 | constexpr char gas_equipment_id::name_progmem[]; 168 | 169 | constexpr ObisId gas_valve_position::id; 170 | constexpr char gas_valve_position::name_progmem[]; 171 | 172 | constexpr ObisId gas_delivered::id; 173 | constexpr char gas_delivered::name_progmem[]; 174 | 175 | constexpr ObisId thermal_device_type::id; 176 | constexpr char thermal_device_type::name_progmem[]; 177 | 178 | constexpr ObisId thermal_equipment_id::id; 179 | constexpr char thermal_equipment_id::name_progmem[]; 180 | 181 | constexpr ObisId thermal_valve_position::id; 182 | constexpr char thermal_valve_position::name_progmem[]; 183 | 184 | constexpr ObisId thermal_delivered::id; 185 | constexpr char thermal_delivered::name_progmem[]; 186 | 187 | constexpr ObisId water_device_type::id; 188 | constexpr char water_device_type::name_progmem[]; 189 | 190 | constexpr ObisId water_equipment_id::id; 191 | constexpr char water_equipment_id::name_progmem[]; 192 | 193 | constexpr ObisId water_valve_position::id; 194 | constexpr char water_valve_position::name_progmem[]; 195 | 196 | constexpr ObisId water_delivered::id; 197 | constexpr char water_delivered::name_progmem[]; 198 | 199 | constexpr ObisId slave_device_type::id; 200 | constexpr char slave_device_type::name_progmem[]; 201 | 202 | constexpr ObisId slave_equipment_id::id; 203 | constexpr char slave_equipment_id::name_progmem[]; 204 | 205 | constexpr ObisId slave_valve_position::id; 206 | constexpr char slave_valve_position::name_progmem[]; 207 | 208 | constexpr ObisId slave_delivered::id; 209 | constexpr char slave_delivered::name_progmem[]; 210 | 211 | -------------------------------------------------------------------------------- /src/dsmr/fields.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Arduino DSMR parser. 3 | * 4 | * This software is licensed under the MIT License. 5 | * 6 | * Copyright (c) 2015 Matthijs Kooijman 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining 9 | * a copy of this software and associated documentation files (the 10 | * "Software"), to deal in the Software without restriction, including 11 | * without limitation the rights to use, copy, modify, merge, publish, 12 | * distribute, sublicense, and/or sell copies of the Software, and to 13 | * permit persons to whom the Software is furnished to do so, subject to 14 | * the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be 17 | * included in all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 23 | * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 24 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | * SOFTWARE. 27 | * 28 | * Field parsing functions 29 | */ 30 | 31 | #ifndef DSMR_INCLUDE_FIELDS_H 32 | #define DSMR_INCLUDE_FIELDS_H 33 | 34 | #include "util.h" 35 | #include "parser.h" 36 | 37 | namespace dsmr { 38 | 39 | /** 40 | * Superclass for data items in a P1 message. 41 | */ 42 | template 43 | struct ParsedField { 44 | template 45 | void apply(F& f) { 46 | f.apply(*static_cast(this)); 47 | } 48 | // By defaults, fields have no unit 49 | static const char *unit() { return ""; } 50 | }; 51 | 52 | template 53 | struct StringField : ParsedField { 54 | ParseResult parse(const char *str, const char *end) { 55 | ParseResult res = StringParser::parse_string(minlen, maxlen, str, end); 56 | if (!res.err) 57 | static_cast(this)->val() = res.result; 58 | return res; 59 | } 60 | }; 61 | 62 | // A timestamp is essentially a string using YYMMDDhhmmssX format (where 63 | // X is W or S for wintertime or summertime). Parsing this into a proper 64 | // (UNIX) timestamp is hard to do generically. Parsing it into a 65 | // single integer needs > 4 bytes top fit and isn't very useful (you 66 | // cannot really do any calculation with those values). So we just parse 67 | // into a string for now. 68 | template 69 | struct TimestampField : StringField { }; 70 | 71 | // Value that is parsed as a three-decimal float, but stored as an 72 | // integer (by multiplying by 1000). Supports val() (or implicit cast to 73 | // float) to get the original value, and int_val() to get the more 74 | // efficient integer value. The unit() and int_unit() methods on 75 | // FixedField return the corresponding units for these values. 76 | struct FixedValue { 77 | operator float() { return val();} 78 | float val() { return _value / 1000.0;} 79 | uint32_t int_val() { return _value; } 80 | 81 | uint32_t _value; 82 | }; 83 | 84 | // Floating point numbers in the message never have more than 3 decimal 85 | // digits. To prevent inefficient floating point operations, we store 86 | // them as a fixed-point number: an integer that stores the value in 87 | // thousands. For example, a value of 1.234 kWh is stored as 1234. This 88 | // effectively means that the integer value is the value in Wh. To allow 89 | // automatic printing of these values, both the original unit and the 90 | // integer unit is passed as a template argument. 91 | template 92 | struct FixedField : ParsedField { 93 | ParseResult parse(const char *str, const char *end) { 94 | ParseResult res = NumParser::parse(3, _unit, str, end); 95 | if (!res.err) 96 | static_cast(this)->val()._value = res.result; 97 | return res; 98 | } 99 | 100 | static const char *unit() { return _unit; } 101 | static const char *int_unit() { return _int_unit; } 102 | }; 103 | 104 | struct TimestampedFixedValue : public FixedValue { 105 | String timestamp; 106 | }; 107 | 108 | // Some numerical values are prefixed with a timestamp. This is simply 109 | // both of them concatenated, e.g. 0-1:24.2.1(150117180000W)(00473.789*m3) 110 | template 111 | struct TimestampedFixedField : public FixedField { 112 | ParseResult parse(const char *str, const char *end) { 113 | // First, parse timestamp 114 | ParseResult res = StringParser::parse_string(13, 13, str, end); 115 | if (res.err) 116 | return res; 117 | 118 | static_cast(this)->val().timestamp = res.result; 119 | 120 | // Which is immediately followed by the numerical value 121 | return FixedField::parse(res.next, end); 122 | } 123 | }; 124 | 125 | // A integer number is just represented as an integer. 126 | template 127 | struct IntField : ParsedField { 128 | ParseResult parse(const char *str, const char *end) { 129 | ParseResult res = NumParser::parse(0, _unit, str, end); 130 | if (!res.err) 131 | static_cast(this)->val() = res.result; 132 | return res; 133 | } 134 | 135 | static const char *unit() { return _unit; } 136 | }; 137 | 138 | // A RawField is not parsed, the entire value (including any 139 | // parenthesis around it) is returned as a string. 140 | template 141 | struct RawField : ParsedField { 142 | ParseResult parse(const char *str, const char *end) { 143 | // Just copy the string verbatim value without any parsing 144 | concat_hack(static_cast(this)->val(), str, end - str); 145 | return ParseResult().until(end); 146 | } 147 | }; 148 | 149 | namespace fields { 150 | 151 | struct units { 152 | // These variables are inside a struct, since that allows us to make 153 | // them constexpr and define their values here, but define the storage 154 | // in a cpp file. Global const(expr) variables have implicitly 155 | // internal linkage, meaning each cpp file that includes us will have 156 | // its own copy of the variable. Since we take the address of these 157 | // variables (passing it as a template argument), this would cause a 158 | // compiler warning. By putting these in a struct, this is prevented. 159 | static constexpr char none[] = ""; 160 | static constexpr char kWh[] = "kWh"; 161 | static constexpr char Wh[] = "Wh"; 162 | static constexpr char kW[] = "kW"; 163 | static constexpr char W[] = "W"; 164 | static constexpr char V[] = "V"; 165 | static constexpr char mV[] = "mV"; 166 | static constexpr char A[] = "A"; 167 | static constexpr char mA[] = "mA"; 168 | static constexpr char m3[] = "m3"; 169 | static constexpr char dm3[] = "dm3"; 170 | static constexpr char GJ[] = "GJ"; 171 | static constexpr char MJ[] = "MJ"; 172 | }; 173 | 174 | const uint8_t GAS_MBUS_ID = 1; 175 | const uint8_t WATER_MBUS_ID = 2; 176 | const uint8_t THERMAL_MBUS_ID = 3; 177 | const uint8_t SLAVE_MBUS_ID = 4; 178 | 179 | template 180 | struct NameConverter { 181 | public: 182 | operator const __FlashStringHelper*() const { return reinterpret_cast(&FieldT::name_progmem); } 183 | }; 184 | 185 | #define DEFINE_FIELD(fieldname, value_t, obis, field_t, field_args...) \ 186 | struct fieldname : field_t { \ 187 | value_t fieldname; \ 188 | bool fieldname ## _present = false; \ 189 | static constexpr ObisId id = obis; \ 190 | static constexpr char name_progmem[] DSMR_PROGMEM = #fieldname; \ 191 | /* name field is for compatibility with a __FlashStringHelper *name \ 192 | * field that was previously used, but no longer worked on newer gcc \ 193 | * versions. The get_name() method should be used instead. \ 194 | * TODO (breaking change): Remove name field, rename get_name() to \ 195 | * name() and add deprecated get_name() that calls name(). */ \ 196 | [[gnu::deprecated]] static constexpr NameConverter name = {}; \ 197 | static const __FlashStringHelper *get_name() { return reinterpret_cast(&name_progmem); } \ 198 | value_t& val() { return fieldname; } \ 199 | bool& present() { return fieldname ## _present; } \ 200 | } 201 | 202 | /* Meter identification. This is not a normal field, but a 203 | * specially-formatted first line of the message */ 204 | DEFINE_FIELD(identification, String, ObisId(255, 255, 255, 255, 255, 255), RawField); 205 | 206 | /* Version information for P1 output */ 207 | DEFINE_FIELD(p1_version, String, ObisId(1, 3, 0, 2, 8), StringField, 2, 2); 208 | 209 | /* Date-time stamp of the P1 message */ 210 | DEFINE_FIELD(timestamp, String, ObisId(0, 0, 1, 0, 0), TimestampField); 211 | 212 | /* Equipment identifier */ 213 | DEFINE_FIELD(equipment_id, String, ObisId(0, 0, 96, 1, 1), StringField, 0, 96); 214 | 215 | /* Meter Reading electricity delivered to client (Tariff 1) in 0,001 kWh */ 216 | DEFINE_FIELD(energy_delivered_tariff1, FixedValue, ObisId(1, 0, 1, 8, 1), FixedField, units::kWh, units::Wh); 217 | /* Meter Reading electricity delivered to client (Tariff 2) in 0,001 kWh */ 218 | DEFINE_FIELD(energy_delivered_tariff2, FixedValue, ObisId(1, 0, 1, 8, 2), FixedField, units::kWh, units::Wh); 219 | /* Meter Reading electricity delivered by client (Tariff 1) in 0,001 kWh */ 220 | DEFINE_FIELD(energy_returned_tariff1, FixedValue, ObisId(1, 0, 2, 8, 1), FixedField, units::kWh, units::Wh); 221 | /* Meter Reading electricity delivered by client (Tariff 2) in 0,001 kWh */ 222 | DEFINE_FIELD(energy_returned_tariff2, FixedValue, ObisId(1, 0, 2, 8, 2), FixedField, units::kWh, units::Wh); 223 | 224 | /* Tariff indicator electricity. The tariff indicator can also be used 225 | * to switch tariff dependent loads e.g boilers. This is the 226 | * responsibility of the P1 user */ 227 | DEFINE_FIELD(electricity_tariff, String, ObisId(0, 0, 96, 14, 0), StringField, 4, 4); 228 | 229 | /* Actual electricity power delivered (+P) in 1 Watt resolution */ 230 | DEFINE_FIELD(power_delivered, FixedValue, ObisId(1, 0, 1, 7, 0), FixedField, units::kW, units::W); 231 | /* Actual electricity power received (-P) in 1 Watt resolution */ 232 | DEFINE_FIELD(power_returned, FixedValue, ObisId(1, 0, 2, 7, 0), FixedField, units::kW, units::W); 233 | 234 | /* The actual threshold Electricity in kW. Removed in 4.0.7 / 4.2.2 / 5.0 */ 235 | DEFINE_FIELD(electricity_threshold, FixedValue, ObisId(0, 0, 17, 0, 0), FixedField, units::kW, units::W); 236 | 237 | /* Switch position Electricity (in/out/enabled). Removed in 4.0.7 / 4.2.2 / 5.0 */ 238 | DEFINE_FIELD(electricity_switch_position, uint8_t, ObisId(0, 0, 96, 3, 10), IntField, units::none); 239 | 240 | /* Number of power failures in any phase */ 241 | DEFINE_FIELD(electricity_failures, uint32_t, ObisId(0, 0, 96, 7, 21), IntField, units::none); 242 | /* Number of long power failures in any phase */ 243 | DEFINE_FIELD(electricity_long_failures, uint32_t, ObisId(0, 0, 96, 7, 9), IntField, units::none); 244 | 245 | /* Power Failure Event Log (long power failures) */ 246 | DEFINE_FIELD(electricity_failure_log, String, ObisId(1, 0, 99, 97, 0), RawField); 247 | 248 | /* Number of voltage sags in phase L1 */ 249 | DEFINE_FIELD(electricity_sags_l1, uint32_t, ObisId(1, 0, 32, 32, 0), IntField, units::none); 250 | /* Number of voltage sags in phase L2 (polyphase meters only) */ 251 | DEFINE_FIELD(electricity_sags_l2, uint32_t, ObisId(1, 0, 52, 32, 0), IntField, units::none); 252 | /* Number of voltage sags in phase L3 (polyphase meters only) */ 253 | DEFINE_FIELD(electricity_sags_l3, uint32_t, ObisId(1, 0, 72, 32, 0), IntField, units::none); 254 | 255 | /* Number of voltage swells in phase L1 */ 256 | DEFINE_FIELD(electricity_swells_l1, uint32_t, ObisId(1, 0, 32, 36, 0), IntField, units::none); 257 | /* Number of voltage swells in phase L2 (polyphase meters only) */ 258 | DEFINE_FIELD(electricity_swells_l2, uint32_t, ObisId(1, 0, 52, 36, 0), IntField, units::none); 259 | /* Number of voltage swells in phase L3 (polyphase meters only) */ 260 | DEFINE_FIELD(electricity_swells_l3, uint32_t, ObisId(1, 0, 72, 36, 0), IntField, units::none); 261 | 262 | /* Text message codes: numeric 8 digits (Note: Missing from 5.0 spec) 263 | * */ 264 | DEFINE_FIELD(message_short, String, ObisId(0, 0, 96, 13, 1), StringField, 0, 16); 265 | /* Text message max 2048 characters (Note: Spec says 1024 in comment and 266 | * 2048 in format spec, so we stick to 2048). */ 267 | DEFINE_FIELD(message_long, String, ObisId(0, 0, 96, 13, 0), StringField, 0, 2048); 268 | 269 | /* Instantaneous voltage L1 in 0.1V resolution (Note: Spec says V 270 | * resolution in comment, but 0.1V resolution in format spec. Added in 271 | * 5.0) */ 272 | DEFINE_FIELD(voltage_l1, FixedValue, ObisId(1, 0, 32, 7, 0), FixedField, units::V, units::mV); 273 | /* Instantaneous voltage L2 in 0.1V resolution (Note: Spec says V 274 | * resolution in comment, but 0.1V resolution in format spec. Added in 275 | * 5.0) */ 276 | DEFINE_FIELD(voltage_l2, FixedValue, ObisId(1, 0, 52, 7, 0), FixedField, units::V, units::mV); 277 | /* Instantaneous voltage L3 in 0.1V resolution (Note: Spec says V 278 | * resolution in comment, but 0.1V resolution in format spec. Added in 279 | * 5.0) */ 280 | DEFINE_FIELD(voltage_l3, FixedValue, ObisId(1, 0, 72, 7, 0), FixedField, units::V, units::mV); 281 | 282 | /* Instantaneous current L1 in A resolution */ 283 | DEFINE_FIELD(current_l1, uint16_t, ObisId(1, 0, 31, 7, 0), IntField, units::A); 284 | /* Instantaneous current L2 in A resolution */ 285 | DEFINE_FIELD(current_l2, uint16_t, ObisId(1, 0, 51, 7, 0), IntField, units::A); 286 | /* Instantaneous current L3 in A resolution */ 287 | DEFINE_FIELD(current_l3, uint16_t, ObisId(1, 0, 71, 7, 0), IntField, units::A); 288 | 289 | /* Instantaneous active power L1 (+P) in W resolution */ 290 | DEFINE_FIELD(power_delivered_l1, FixedValue, ObisId(1, 0, 21, 7, 0), FixedField, units::kW, units::W); 291 | /* Instantaneous active power L2 (+P) in W resolution */ 292 | DEFINE_FIELD(power_delivered_l2, FixedValue, ObisId(1, 0, 41, 7, 0), FixedField, units::kW, units::W); 293 | /* Instantaneous active power L3 (+P) in W resolution */ 294 | DEFINE_FIELD(power_delivered_l3, FixedValue, ObisId(1, 0, 61, 7, 0), FixedField, units::kW, units::W); 295 | 296 | /* Instantaneous active power L1 (-P) in W resolution */ 297 | DEFINE_FIELD(power_returned_l1, FixedValue, ObisId(1, 0, 22, 7, 0), FixedField, units::kW, units::W); 298 | /* Instantaneous active power L2 (-P) in W resolution */ 299 | DEFINE_FIELD(power_returned_l2, FixedValue, ObisId(1, 0, 42, 7, 0), FixedField, units::kW, units::W); 300 | /* Instantaneous active power L3 (-P) in W resolution */ 301 | DEFINE_FIELD(power_returned_l3, FixedValue, ObisId(1, 0, 62, 7, 0), FixedField, units::kW, units::W); 302 | 303 | 304 | /* Device-Type */ 305 | DEFINE_FIELD(gas_device_type, uint16_t, ObisId(0, GAS_MBUS_ID, 24, 1, 0), IntField, units::none); 306 | 307 | /* Equipment identifier (Gas) */ 308 | DEFINE_FIELD(gas_equipment_id, String, ObisId(0, GAS_MBUS_ID, 96, 1, 0), StringField, 0, 96); 309 | 310 | /* Valve position Gas (on/off/released) (Note: Removed in 4.0.7 / 4.2.2 / 5.0). */ 311 | DEFINE_FIELD(gas_valve_position, uint8_t, ObisId(0, GAS_MBUS_ID, 24, 4, 0), IntField, units::none); 312 | 313 | /* Last 5-minute value (temperature converted), gas delivered to client 314 | * in m3, including decimal values and capture time (Note: 4.x spec has 315 | * "hourly value") */ 316 | DEFINE_FIELD(gas_delivered, TimestampedFixedValue, ObisId(0, GAS_MBUS_ID, 24, 2, 1), TimestampedFixedField, units::m3, units::dm3); 317 | 318 | 319 | /* Device-Type */ 320 | DEFINE_FIELD(thermal_device_type, uint16_t, ObisId(0, THERMAL_MBUS_ID, 24, 1, 0), IntField, units::none); 321 | 322 | /* Equipment identifier (Thermal: heat or cold) */ 323 | DEFINE_FIELD(thermal_equipment_id, String, ObisId(0, THERMAL_MBUS_ID, 96, 1, 0), StringField, 0, 96); 324 | 325 | /* Valve position (on/off/released) (Note: Removed in 4.0.7 / 4.2.2 / 5.0). */ 326 | DEFINE_FIELD(thermal_valve_position, uint8_t, ObisId(0, THERMAL_MBUS_ID, 24, 4, 0), IntField, units::none); 327 | 328 | /* Last 5-minute Meter reading Heat or Cold in 0,01 GJ and capture time 329 | * (Note: 4.x spec has "hourly meter reading") */ 330 | DEFINE_FIELD(thermal_delivered, TimestampedFixedValue, ObisId(0, THERMAL_MBUS_ID, 24, 2, 1), TimestampedFixedField, units::GJ, units::MJ); 331 | 332 | 333 | /* Device-Type */ 334 | DEFINE_FIELD(water_device_type, uint16_t, ObisId(0, WATER_MBUS_ID, 24, 1, 0), IntField, units::none); 335 | 336 | /* Equipment identifier (Thermal: heat or cold) */ 337 | DEFINE_FIELD(water_equipment_id, String, ObisId(0, WATER_MBUS_ID, 96, 1, 0), StringField, 0, 96); 338 | 339 | /* Valve position (on/off/released) (Note: Removed in 4.0.7 / 4.2.2 / 5.0). */ 340 | DEFINE_FIELD(water_valve_position, uint8_t, ObisId(0, WATER_MBUS_ID, 24, 4, 0), IntField, units::none); 341 | 342 | /* Last 5-minute Meter reading in 0,001 m3 and capture time 343 | * (Note: 4.x spec has "hourly meter reading") */ 344 | DEFINE_FIELD(water_delivered, TimestampedFixedValue, ObisId(0, WATER_MBUS_ID, 24, 2, 1), TimestampedFixedField, units::m3, units::dm3); 345 | 346 | 347 | /* Device-Type */ 348 | DEFINE_FIELD(slave_device_type, uint16_t, ObisId(0, SLAVE_MBUS_ID, 24, 1, 0), IntField, units::none); 349 | 350 | /* Equipment identifier (Thermal: heat or cold) */ 351 | DEFINE_FIELD(slave_equipment_id, String, ObisId(0, SLAVE_MBUS_ID, 96, 1, 0), StringField, 0, 96); 352 | 353 | /* Valve position (on/off/released) (Note: Removed in 4.0.7 / 4.2.2 / 5.0). */ 354 | DEFINE_FIELD(slave_valve_position, uint8_t, ObisId(0, SLAVE_MBUS_ID, 24, 4, 0), IntField, units::none); 355 | 356 | /* Last 5-minute Meter reading Heat or Cold and capture time (e.g. slave 357 | * E meter) (Note: 4.x spec has "hourly meter reading") */ 358 | DEFINE_FIELD(slave_delivered, TimestampedFixedValue, ObisId(0, SLAVE_MBUS_ID, 24, 2, 1), TimestampedFixedField, units::m3, units::dm3); 359 | 360 | } // namespace fields 361 | 362 | } // namespace dsmr 363 | 364 | #endif // DSMR_INCLUDE_FIELDS_H 365 | -------------------------------------------------------------------------------- /src/dsmr/parser.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Arduino DSMR parser. 3 | * 4 | * This software is licensed under the MIT License. 5 | * 6 | * Copyright (c) 2015 Matthijs Kooijman 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining 9 | * a copy of this software and associated documentation files (the 10 | * "Software"), to deal in the Software without restriction, including 11 | * without limitation the rights to use, copy, modify, merge, publish, 12 | * distribute, sublicense, and/or sell copies of the Software, and to 13 | * permit persons to whom the Software is furnished to do so, subject to 14 | * the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be 17 | * included in all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 23 | * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 24 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | * SOFTWARE. 27 | * 28 | * Message parsing core 29 | */ 30 | 31 | #ifndef DSMR_INCLUDE_PARSER_H 32 | #define DSMR_INCLUDE_PARSER_H 33 | 34 | #include "crc16.h" 35 | #include "util.h" 36 | 37 | namespace dsmr { 38 | 39 | /** 40 | * ParsedData is a template for the result of parsing a Dsmr P1 message. 41 | * You pass the fields you want to add to it as template arguments. 42 | * 43 | * This template will then generate a class that extends all the fields 44 | * passed (the fields really are classes themselves). Since each field 45 | * class has a single member variable, with the same name as the field 46 | * class, all of these fields will be available on the generated class. 47 | * 48 | * In other words, if I have: 49 | * 50 | * using MyData = ParsedData< 51 | * identification, 52 | * equipment_id 53 | * >; 54 | * 55 | * MyData data; 56 | * 57 | * then I can refer to the fields like data.identification and 58 | * data.equipment_id normally. 59 | * 60 | * Furthermore, this class offers some helper methods that can be used 61 | * to loop over all the fields inside it. 62 | */ 63 | template 64 | struct ParsedData; 65 | 66 | /** 67 | * Base case: No fields present. 68 | */ 69 | template<> 70 | struct ParsedData<> { 71 | ParseResult __attribute__((__always_inline__)) parse_line_inlined(const ObisId& /* id */, const char *str, const char * /* end */) { 72 | // Parsing succeeded, but found no matching handler (so return 73 | // set the next pointer to show nothing was parsed). 74 | return ParseResult().until(str); 75 | } 76 | 77 | template 78 | void __attribute__((__always_inline__)) applyEach_inlined(F&& /* f */) { 79 | // Nothing to do 80 | } 81 | 82 | bool all_present_inlined() { 83 | return true; 84 | } 85 | }; 86 | 87 | // Do not use F() for multiply-used strings (including strings used from 88 | // multiple template instantiations), that would result in multiple 89 | // instances of the string in the binary 90 | static constexpr char DUPLICATE_FIELD[] DSMR_PROGMEM = "Duplicate field"; 91 | 92 | /** 93 | * General case: At least one typename is passed. 94 | */ 95 | template 96 | struct ParsedData : public T, ParsedData { 97 | /** 98 | * This method is used by the parser to parse a single line. The 99 | * OBIS id of the line is passed, and this method recursively finds a 100 | * field with a matching id. If any, it calls it's parse method, which 101 | * parses the value and stores it in the field. 102 | */ 103 | ParseResult parse_line(const ObisId& id, const char *str, const char *end) { 104 | return parse_line_inlined(id, str, end); 105 | } 106 | 107 | /** 108 | * always_inline version of parse_line. This is a separate method, to 109 | * allow recursively inlining all calls, but still have a non-inlined 110 | * top-level parse_line method. 111 | */ 112 | ParseResult __attribute__((__always_inline__)) parse_line_inlined(const ObisId& id, const char *str, const char *end) { 113 | if (id == T::id) { 114 | if (T::present()) 115 | return ParseResult().fail((const __FlashStringHelper*)DUPLICATE_FIELD, str); 116 | T::present() = true; 117 | return T::parse(str, end); 118 | } 119 | return ParsedData::parse_line_inlined(id, str, end); 120 | } 121 | 122 | template 123 | void applyEach(F&& f) { 124 | applyEach_inlined(f); 125 | } 126 | 127 | template 128 | void __attribute__((__always_inline__)) applyEach_inlined(F&& f) { 129 | T::apply(f); 130 | return ParsedData::applyEach_inlined(f); 131 | } 132 | 133 | /** 134 | * Returns true when all defined fields are present. 135 | */ 136 | bool all_present() { 137 | return all_present_inlined(); 138 | } 139 | 140 | bool all_present_inlined() { 141 | return T::present() && ParsedData::all_present_inlined(); 142 | } 143 | }; 144 | 145 | 146 | struct StringParser { 147 | static ParseResult parse_string(size_t min, size_t max, const char *str, const char *end) { 148 | ParseResult res; 149 | if (str >= end || *str != '(') 150 | return res.fail(F("Missing ("), str); 151 | 152 | const char *str_start = str + 1; // Skip ( 153 | const char *str_end = str_start; 154 | 155 | while(str_end < end && *str_end != ')') 156 | ++str_end; 157 | 158 | if (str_end == end) 159 | return res.fail(F("Missing )"), str_end); 160 | 161 | size_t len = str_end - str_start; 162 | if (len < min || len > max) 163 | return res.fail(F("Invalid string length"), str_start); 164 | 165 | concat_hack(res.result, str_start, len); 166 | 167 | return res.until(str_end + 1); // Skip ) 168 | } 169 | }; 170 | 171 | // Do not use F() for multiply-used strings (including strings used from 172 | // multiple template instantiations), that would result in multiple 173 | // instances of the string in the binary 174 | static constexpr char INVALID_NUMBER[] DSMR_PROGMEM = "Invalid number"; 175 | static constexpr char INVALID_UNIT[] DSMR_PROGMEM = "Invalid unit"; 176 | 177 | struct NumParser { 178 | static ParseResult parse(size_t max_decimals, const char* unit, const char *str, const char *end) { 179 | ParseResult res; 180 | if (str >= end || *str != '(') 181 | return res.fail(F("Missing ("), str); 182 | 183 | const char *num_start = str + 1; // Skip ( 184 | const char *num_end = num_start; 185 | 186 | uint32_t value = 0; 187 | 188 | // Parse integer part 189 | while(num_end < end && !strchr("*.)", *num_end)) { 190 | if (*num_end < '0' || *num_end > '9') 191 | return res.fail((const __FlashStringHelper*)INVALID_NUMBER, num_end); 192 | value *= 10; 193 | value += *num_end - '0'; 194 | ++num_end; 195 | } 196 | 197 | // Parse decimal part, if any 198 | if (max_decimals && num_end < end && *num_end == '.') { 199 | ++num_end; 200 | 201 | while(num_end < end && !strchr("*)", *num_end) && max_decimals) { 202 | --max_decimals; 203 | if (*num_end < '0' || *num_end > '9') 204 | return res.fail((const __FlashStringHelper*)INVALID_NUMBER, num_end); 205 | value *= 10; 206 | value += *num_end - '0'; 207 | ++num_end; 208 | } 209 | } 210 | 211 | // Fill in missing decimals with zeroes 212 | while(max_decimals--) 213 | value *= 10; 214 | 215 | // If a unit was passed, check that the unit in the messages 216 | // messages the unit passed. 217 | if (unit && *unit) { 218 | if (num_end >= end || *num_end != '*') 219 | return res.fail(F("Missing unit"), num_end); 220 | const char *unit_start = ++num_end; // skip * 221 | while(num_end < end && *num_end != ')' && *unit) { 222 | // Next character in units do not match? 223 | if (*num_end++ != *unit++) 224 | return res.fail((const __FlashStringHelper*)INVALID_UNIT, unit_start); 225 | } 226 | // At the end of the message unit, but not the passed unit? 227 | if (*unit) 228 | return res.fail((const __FlashStringHelper*)INVALID_UNIT, unit_start); 229 | } 230 | 231 | if (num_end >= end || *num_end != ')') 232 | return res.fail(F("Extra data"), num_end); 233 | 234 | return res.succeed(value).until(num_end + 1); // Skip ) 235 | } 236 | }; 237 | 238 | struct ObisIdParser { 239 | static ParseResult parse(const char *str, const char *end) { 240 | // Parse a Obis ID of the form 1-2:3.4.5.6 241 | // Stops parsing on the first unrecognized character. Any unparsed 242 | // parts are set to 255. 243 | ParseResult res; 244 | ObisId& id = res.result; 245 | res.next = str; 246 | uint8_t part = 0; 247 | while (res.next < end) { 248 | char c = *res.next; 249 | 250 | if (c >= '0' && c <= '9') { 251 | uint8_t digit = c - '0'; 252 | if (id.v[part] > 25 || (id.v[part] == 25 && digit > 5)) 253 | return res.fail(F("Obis ID has number over 255"), res.next); 254 | id.v[part] = id.v[part] * 10 + digit; 255 | } else if (part == 0 && c == '-') { 256 | part++; 257 | } else if (part == 1 && c == ':') { 258 | part++; 259 | } else if (part > 1 && part < 5 && c == '.') { 260 | part++; 261 | } else { 262 | break; 263 | } 264 | ++res.next; 265 | } 266 | 267 | if (res.next == str) 268 | return res.fail(F("OBIS id Empty"), str); 269 | 270 | for (++part; part < 6; ++part) 271 | id.v[part] = 255; 272 | 273 | return res; 274 | } 275 | }; 276 | 277 | struct CrcParser { 278 | static const size_t CRC_LEN = 4; 279 | 280 | // Parse a crc value. str must point to the first of the four hex 281 | // bytes in the CRC. 282 | static ParseResult parse(const char *str, const char *end) { 283 | ParseResult res; 284 | // This should never happen with the code in this library, but 285 | // check anyway 286 | if (str + CRC_LEN > end) 287 | return res.fail(F("No checksum found"), str); 288 | 289 | // A bit of a messy way to parse the checksum, but all 290 | // integer-parse functions assume nul-termination 291 | char buf[CRC_LEN + 1]; 292 | memcpy(buf, str, CRC_LEN); 293 | buf[CRC_LEN] = '\0'; 294 | char *endp; 295 | uint16_t check = strtoul(buf, &endp, 16); 296 | 297 | // See if all four bytes formed a valid number 298 | if (endp != buf + CRC_LEN) 299 | return res.fail(F("Incomplete or malformed checksum"), str); 300 | 301 | res.next = str + CRC_LEN; 302 | return res.succeed(check); 303 | } 304 | }; 305 | 306 | struct P1Parser { 307 | /** 308 | * Parse a complete P1 telegram. The string passed should start 309 | * with '/' and run up to and including the ! and the following 310 | * four byte checksum. It's ok if the string is longer, the .next 311 | * pointer in the result will indicate the next unprocessed byte. 312 | */ 313 | template 314 | static ParseResult parse(ParsedData *data, const char *str, size_t n, bool unknown_error = false) { 315 | ParseResult res; 316 | if (!n || str[0] != '/') 317 | return res.fail(F("Data should start with /"), str); 318 | 319 | // Skip / 320 | const char *data_start = str + 1; 321 | 322 | // Look for ! that terminates the data 323 | const char *data_end = data_start; 324 | uint16_t crc = _crc16_update(0, *str); // Include the / in CRC 325 | while (data_end < str + n && *data_end != '!') { 326 | crc = _crc16_update(crc, *data_end); 327 | ++data_end; 328 | } 329 | 330 | if (data_end >= str + n) 331 | return res.fail(F("No checksum found"), data_end); 332 | 333 | crc = _crc16_update(crc, *data_end); // Include the ! in CRC 334 | 335 | ParseResult check_res = CrcParser::parse(data_end + 1, str + n); 336 | if (check_res.err) 337 | return check_res; 338 | 339 | // Check CRC 340 | if (check_res.result != crc) 341 | return res.fail(F("Checksum mismatch"), data_end + 1); 342 | 343 | res = parse_data(data, data_start, data_end, unknown_error); 344 | res.next = check_res.next; 345 | return res; 346 | } 347 | 348 | /** 349 | * Parse the data part of a message. Str should point to the first 350 | * character after the leading /, end should point to the ! before the 351 | * checksum. Does not verify the checksum. 352 | */ 353 | template 354 | static ParseResult parse_data(ParsedData *data, const char *str, const char *end, bool unknown_error = false) { 355 | ParseResult res; 356 | // Split into lines and parse those 357 | const char *line_end = str, *line_start = str; 358 | 359 | // Parse ID line 360 | while (line_end < end) { 361 | if (*line_end == '\r' || *line_end == '\n') { 362 | // The first identification line looks like: 363 | // XXX5 364 | // The DSMR spec is vague on details, but in 62056-21, the X's 365 | // are a three-letter (registerd) manufacturer ID, the id 366 | // string is up to 16 chars of arbitrary characters and the 367 | // '5' is a baud rate indication. 5 apparently means 9600, 368 | // which DSMR 3.x and below used. It seems that DSMR 2.x 369 | // passed '3' here (which is mandatory for "mode D" 370 | // communication according to 62956-21), so we also allow 371 | // that. Apparently swedish meters use '9' for 115200. This code 372 | // used to check the format of the line somewhat, but for 373 | // flexibility (and since we do not actually parse the contents 374 | // of the line anyway), just allow anything now. 375 | // 376 | // Offer it for processing using the all-ones Obis ID, which 377 | // is not otherwise valid. 378 | ParseResult tmp = data->parse_line(ObisId(255, 255, 255, 255, 255, 255), line_start, line_end); 379 | if (tmp.err) 380 | return tmp; 381 | line_start = ++line_end; 382 | break; 383 | } 384 | ++line_end; 385 | } 386 | 387 | // Parse data lines 388 | while (line_end < end) { 389 | if (*line_end == '\r' || *line_end == '\n') { 390 | ParseResult tmp = parse_line(data, line_start, line_end, unknown_error); 391 | if (tmp.err) 392 | return tmp; 393 | line_start = line_end + 1; 394 | } 395 | line_end++; 396 | } 397 | 398 | if (line_end != line_start) 399 | return res.fail(F("Last dataline not CRLF terminated"), line_end); 400 | 401 | return res; 402 | } 403 | 404 | template 405 | static ParseResult parse_line(Data *data, const char *line, const char *end, bool unknown_error) { 406 | ParseResult res; 407 | if (line == end) 408 | return res; 409 | 410 | ParseResult idres = ObisIdParser::parse(line, end); 411 | if (idres.err) 412 | return idres; 413 | 414 | ParseResult datares = data->parse_line(idres.result, idres.next, end); 415 | if (datares.err) 416 | return datares; 417 | 418 | // If datares.next didn't move at all, there was no parser for 419 | // this field, that's ok. But if it did move, but not all the way 420 | // to the end, that's an error. 421 | if (datares.next != idres.next && datares.next != end) 422 | return res.fail(F("Trailing characters on data line"), datares.next); 423 | else if (datares.next == idres.next && unknown_error) 424 | return res.fail(F("Unknown field"), line); 425 | 426 | return res.until(end); 427 | } 428 | }; 429 | 430 | } // namespace dsmr 431 | 432 | #endif // DSMR_INCLUDE_PARSER_H 433 | -------------------------------------------------------------------------------- /src/dsmr/reader.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Arduino DSMR parser. 3 | * 4 | * This software is licensed under the MIT License. 5 | * 6 | * Copyright (c) 2015 Matthijs Kooijman 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining 9 | * a copy of this software and associated documentation files (the 10 | * "Software"), to deal in the Software without restriction, including 11 | * without limitation the rights to use, copy, modify, merge, publish, 12 | * distribute, sublicense, and/or sell copies of the Software, and to 13 | * permit persons to whom the Software is furnished to do so, subject to 14 | * the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be 17 | * included in all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 23 | * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 24 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | * SOFTWARE. 27 | * 28 | * P1 reader, that takes care of toggling a request pin, reading data 29 | * from a serial port and parsing it. 30 | */ 31 | 32 | #ifndef DSMR_INCLUDE_READER_H 33 | #define DSMR_INCLUDE_READER_H 34 | 35 | #include 36 | #include "crc16.h" 37 | 38 | #include "parser.h" 39 | 40 | namespace dsmr { 41 | 42 | /** 43 | * Controls the request pin on the P1 port to enable (periodic) 44 | * transmission of messages and reads those messages. 45 | * 46 | * To enable the request pin, call enable(). This lets the Smart Meter 47 | * start periodically sending messages. While the request pin is 48 | * enabled, loop() should be regularly called to read pending bytes. 49 | * 50 | * Once a full and correct message is received, loop() (and available()) 51 | * start returning true, until the message is cleared. You can then 52 | * either read the raw message using raw(), or parse it using parse(). 53 | * 54 | * The message is cleared when: 55 | * - clear() is called 56 | * - parse() is called 57 | * - loop() is called and the start of a new message is available 58 | * 59 | * When disable is called, the request pin is disabled again and any 60 | * partial message is discarded. Any bytes received while disabled are 61 | * dropped. 62 | */ 63 | class P1Reader { 64 | public: 65 | /** 66 | * Create a new P1Reader. The stream passed should be the serial 67 | * port to which the P1 TX pin is connected. The req_pin is the 68 | * pin connected to the request pin. The pin is configured as an 69 | * output, the Stream is assumed to be already set up (e.g. baud 70 | * rate configured). 71 | */ 72 | P1Reader(Stream *stream, uint8_t req_pin) 73 | : stream(stream), req_pin(req_pin), once(false), state(State::DISABLED_STATE) { 74 | pinMode(req_pin, OUTPUT); 75 | digitalWrite(req_pin, LOW); 76 | } 77 | 78 | /** 79 | * Enable the request pin, to request data on the P1 port. 80 | * @param once When true, the request pin is automatically 81 | * disabled once a complete and correct message was 82 | * receivedc. When false, the request pin stays 83 | * enabled, so messages will continue to be sent 84 | * periodically. 85 | */ 86 | void enable(bool once) { 87 | digitalWrite(this->req_pin, HIGH); 88 | this->state = State::WAITING_STATE; 89 | this->once = once; 90 | } 91 | 92 | /* Disable the request pin again, to stop data from being sent on 93 | * the P1 port. This will also clear any incomplete data that was 94 | * previously received, but a complete message will be kept until 95 | * clear() is called. 96 | */ 97 | void disable() { 98 | digitalWrite(this->req_pin, LOW); 99 | this->state = State::DISABLED_STATE; 100 | if (!this->_available) 101 | this->buffer = ""; 102 | // Clear any pending bytes 103 | while(this->stream->read() >= 0) /* nothing */; 104 | } 105 | 106 | /** 107 | * Returns true when a complete and correct message was received, 108 | * until it is cleared. 109 | */ 110 | bool available() { 111 | return this->_available; 112 | } 113 | 114 | /** 115 | * Check for new data to read. Should be called regularly, such as 116 | * once every loop. Returns true if a complete message is available 117 | * (just like available). 118 | */ 119 | bool loop() { 120 | while(true) { 121 | if (state == State::CHECKSUM_STATE) { 122 | // Let the Stream buffer the CRC bytes. Convert to size_t to 123 | // prevent unsigned vs signed comparison 124 | if ((size_t)this->stream->available() < CrcParser::CRC_LEN) 125 | return false; 126 | 127 | char buf[CrcParser::CRC_LEN]; 128 | for (uint8_t i = 0; i < CrcParser::CRC_LEN; ++i) 129 | buf[i] = this->stream->read(); 130 | 131 | ParseResult crc = CrcParser::parse(buf, buf + lengthof(buf)); 132 | 133 | // Prepare for next message 134 | state = State::WAITING_STATE; 135 | 136 | if (!crc.err && crc.result == this->crc) { 137 | // Message complete, checksum correct 138 | this->_available = true; 139 | 140 | if (once) 141 | this->disable(); 142 | 143 | return true; 144 | } 145 | } else { 146 | // For other states, read bytes one by one 147 | int c = this->stream->read(); 148 | if (c < 0) 149 | return false; 150 | 151 | switch (this->state) { 152 | case State::DISABLED_STATE: 153 | // Where did this byte come from? Just toss it 154 | break; 155 | case State::WAITING_STATE: 156 | if (c == '/') { 157 | this->state = State::READING_STATE; 158 | // Include the / in the CRC 159 | this->crc = _crc16_update(0, c); 160 | this->clear(); 161 | } 162 | break; 163 | case State::READING_STATE: 164 | // Include the ! in the CRC 165 | this->crc = _crc16_update(this->crc, c); 166 | if (c == '!') 167 | this->state = State::CHECKSUM_STATE; 168 | else 169 | buffer.concat((char)c); 170 | 171 | break; 172 | case State::CHECKSUM_STATE: 173 | // This cannot happen (given the surrounding if), but the 174 | // compiler is not smart enough to see this, so list this 175 | // case to prevent a warning. 176 | abort(); 177 | break; 178 | } 179 | } 180 | } 181 | return false; 182 | } 183 | 184 | /** 185 | * Returns the data read so far. 186 | */ 187 | const String &raw() { 188 | return buffer; 189 | } 190 | 191 | /** 192 | * If a complete message has been received, parse it and store the 193 | * result into the ParsedData object passed. 194 | * 195 | * After parsing, the message is cleared. 196 | * 197 | * If parsing fails, false is returned. If err is passed, the error 198 | * message is appended to that string. 199 | */ 200 | template 201 | bool parse(ParsedData *data, String *err) { 202 | const char *str = buffer.c_str(), *end = buffer.c_str() + buffer.length(); 203 | ParseResult res = P1Parser::parse_data(data, str, end); 204 | 205 | if (res.err && err) 206 | *err = res.fullError(str, end); 207 | 208 | // Clear the message 209 | this->clear(); 210 | 211 | return res.err == NULL; 212 | } 213 | 214 | /** 215 | * Clear any complete message from the buffer. 216 | */ 217 | void clear() { 218 | if (_available) { 219 | buffer = ""; 220 | _available = false; 221 | } 222 | } 223 | 224 | protected: 225 | Stream *stream; 226 | uint8_t req_pin; 227 | enum class State : uint8_t { 228 | DISABLED_STATE, 229 | WAITING_STATE, 230 | READING_STATE, 231 | CHECKSUM_STATE, 232 | }; 233 | bool _available; 234 | bool once; 235 | State state; 236 | String buffer; 237 | uint16_t crc; 238 | }; 239 | 240 | } // namespace dsmr 241 | 242 | #endif // DSMR_INCLUDE_READER_H 243 | -------------------------------------------------------------------------------- /src/dsmr/util.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Arduino DSMR parser. 3 | * 4 | * This software is licensed under the MIT License. 5 | * 6 | * Copyright (c) 2015 Matthijs Kooijman 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining 9 | * a copy of this software and associated documentation files (the 10 | * "Software"), to deal in the Software without restriction, including 11 | * without limitation the rights to use, copy, modify, merge, publish, 12 | * distribute, sublicense, and/or sell copies of the Software, and to 13 | * permit persons to whom the Software is furnished to do so, subject to 14 | * the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be 17 | * included in all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 23 | * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 24 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | * SOFTWARE. 27 | * 28 | * Various utility functions 29 | */ 30 | 31 | #ifndef DSMR_INCLUDE_UTIL_H 32 | #define DSMR_INCLUDE_UTIL_H 33 | 34 | #ifdef ARDUINO_ARCH_ESP8266 35 | #define DSMR_PROGMEM 36 | #else 37 | #define DSMR_PROGMEM PROGMEM 38 | #endif 39 | 40 | #include 41 | 42 | namespace dsmr { 43 | 44 | /** 45 | * Small utility to get the length of an array at compiletime. 46 | */ 47 | template 48 | inline unsigned int lengthof(const T (&)[sz]) { return sz; } 49 | 50 | // Hack until https://github.com/arduino/Arduino/pull/1936 is merged. 51 | // This appends the given number of bytes from the given C string to the 52 | // given Arduino string, without requiring a trailing NUL. 53 | // Requires that there _is_ room for nul-termination 54 | static void concat_hack(String& s, const char *append, size_t n) { 55 | // Add null termination. Inefficient, but it works... 56 | char buf[n + 1]; 57 | memcpy(buf, append, n); 58 | buf[n] = 0; 59 | s.concat(buf); 60 | } 61 | 62 | /** 63 | * The ParseResult class wraps the result of a parse function. The type 64 | * of the result is passed as a template parameter and can be void to 65 | * not return any result. 66 | * 67 | * A ParseResult can either: 68 | * - Return an error. In this case, err is set to an error message, ctx 69 | * is optionally set to where the error occurred. The result (if any) 70 | * and the next pointer are meaningless. 71 | * - Return succesfully. In this case, err and ctx are NULL, result 72 | * contains the result (if any) and next points one past the last 73 | * byte processed by the parser. 74 | * 75 | * The ParseResult class has some convenience functions: 76 | * - succeed(result): sets the result to the given value and returns 77 | * the ParseResult again. 78 | * - fail(err): Set the err member to the error message passed, 79 | * optionally sets the ctx and return the ParseResult again. 80 | * - until(next): Set the next member and return the ParseResult again. 81 | * 82 | * Furthermore, ParseResults can be implicitely converted to other 83 | * types. In this case, the error message, context and and next pointer are 84 | * conserved, the return value is reset to the default value for the 85 | * target type. 86 | * 87 | * Note that ctx points into the string being parsed, so it does not 88 | * need to be freed, lives as long as the original string and is 89 | * probably way longer that needed. 90 | */ 91 | 92 | // Superclass for ParseResult so we can specialize for void without 93 | // having to duplicate all content 94 | template 95 | struct _ParseResult { 96 | T result; 97 | 98 | P& succeed(T& result) { 99 | this->result = result; return *static_cast(this); 100 | } 101 | P& succeed(T&& result) { 102 | this->result = result; 103 | return *static_cast(this); 104 | } 105 | }; 106 | 107 | // partial specialization for void result 108 | template 109 | struct _ParseResult { 110 | }; 111 | 112 | // Actual ParseResult class 113 | template 114 | struct ParseResult : public _ParseResult, T> { 115 | const char *next = NULL; 116 | const __FlashStringHelper *err = NULL; 117 | const char *ctx = NULL; 118 | 119 | ParseResult& fail(const __FlashStringHelper *err, const char* ctx = NULL) { 120 | this->err = err; 121 | this->ctx = ctx; 122 | return *this; 123 | } 124 | ParseResult& until(const char *next) { 125 | this->next = next; 126 | return *this; 127 | } 128 | ParseResult() = default; 129 | ParseResult(const ParseResult& other) = default; 130 | 131 | template 132 | ParseResult(const ParseResult& other): next(other.next), err(other.err), ctx(other.ctx) { } 133 | 134 | /** 135 | * Returns the error, including context in a fancy multi-line format. 136 | * The start and end passed are the first and one-past-the-end 137 | * characters in the total parsed string. These are needed to properly 138 | * limit the context output. 139 | */ 140 | String fullError(const char* start, const char* end) const { 141 | String res; 142 | if (this->ctx && start && end) { 143 | // Find the entire line surrounding the context 144 | const char *line_end = this->ctx; 145 | while(line_end < end && line_end[0] != '\r' && line_end[0] != '\n') ++line_end; 146 | const char *line_start = this->ctx; 147 | while(line_start > start && line_start[-1] != '\r' && line_start[-1] != '\n') --line_start; 148 | 149 | // We can now predict the context string length, so let String allocate 150 | // memory in advance 151 | res.reserve((line_end - line_start) + 2 + (this->ctx - line_start) + 1 + 2); 152 | 153 | // Write the line 154 | concat_hack(res, line_start, line_end - line_start); 155 | res += "\r\n"; 156 | 157 | // Write a marker to point out ctx 158 | while (line_start++ < this->ctx) 159 | res += ' '; 160 | res += '^'; 161 | res += "\r\n"; 162 | } 163 | res += this->err; 164 | return res; 165 | } 166 | }; 167 | 168 | /** 169 | * An OBIS id is 6 bytes, usually noted as a-b:c.d.e.f. Here we put them 170 | * in an array for easy parsing. 171 | */ 172 | struct ObisId { 173 | uint8_t v[6]; 174 | 175 | constexpr ObisId(uint8_t a, uint8_t b = 255, uint8_t c = 255, uint8_t d = 255, uint8_t e = 255, uint8_t f = 255) 176 | : v{a, b, c, d, e, f} { }; 177 | constexpr ObisId() : v() {} // Zeroes 178 | 179 | bool operator==(const ObisId &other) const { 180 | return memcmp(&v, &other.v, sizeof(v)) == 0; 181 | } 182 | }; 183 | 184 | } // namespace dsmr 185 | 186 | #endif // DSMR_INCLUDE_UTIL_H 187 | --------------------------------------------------------------------------------