├── .gitignore ├── LICENSE ├── README.md ├── crc16.c ├── crc16.h ├── doc ├── DSMR-2.2-P1.pdf ├── DSMR-3.0-P1.pdf ├── DSMR-4.0.0-P1.pdf ├── DSMR-4.2.2-P1.pdf ├── DSMR-4.2.2-main.pdf ├── DSMR-5.0.2-P1.pdf ├── IEC-62056-21-notes.md └── raspberry-pi-p1-interface.gif ├── dsmr-data.h ├── example-data ├── p1-example-3.0.txt ├── p1-example-4.0.txt └── p1-example-5.0.txt ├── logmsg.h ├── make.sh ├── p1-lib.c ├── p1-lib.h ├── p1-parser.h ├── p1-parser.rl ├── p1-test-d0.c ├── p1-test.c └── parser-tools.rl /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Object files 5 | *.o 6 | *.ko 7 | *.obj 8 | *.elf 9 | 10 | # Linker output 11 | *.ilk 12 | *.map 13 | *.exp 14 | 15 | # Precompiled Headers 16 | *.gch 17 | *.pch 18 | 19 | # Libraries 20 | *.lib 21 | *.a 22 | *.la 23 | *.lo 24 | 25 | # Shared objects (inc. Windows DLLs) 26 | *.dll 27 | *.so 28 | *.so.* 29 | *.dylib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | *.i*86 36 | *.x86_64 37 | *.hex 38 | 39 | # Debug files 40 | *.dSYM/ 41 | *.su 42 | *.idb 43 | *.pdb 44 | 45 | # Kernel Module Compile Results 46 | *.mod* 47 | *.cmd 48 | .tmp_versions/ 49 | modules.order 50 | Module.symvers 51 | Mkfile.old 52 | dkms.conf 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dsmr-p1-parser 2 | Ragel-based C-parser for Dutch Smart Meter P1-data 3 | 4 | Author: Levien van Zon (levien at gnuritas.org) 5 | 6 | ## DSMR P1 7 | 8 | DSMR (Dutch Smart Meter Requirements) is a standard for "smart" utility meters used in The Netherlands. Meters that comply with DSMR have several interfaces, including: 9 | 10 | - P1, a send-only serial interface that can be used to connect local devices to an electricity meter. The connected devices can receive data from the electricity meter and its slave devices. 11 | - P2, an [M-bus](https://en.wikipedia.org/wiki/Meter-Bus) interface used for communication between utility meters (e.g. a gas meter, a water meter and an electricity meter, whereby the electricity meter acts as a master and the other meters as slaves). 12 | - P3, an infrastructure to periodically send aggregated meter data from an electricity meter to a central server, using a cellular modem (GPRS, CDMA or LTE), ethernet or power-line communication (PLC). Data is generally sent at most once per day, aggregated at 10-minute intervals, for a subset of variables. 13 | - P4, which is strictly speaking not an interface at the meter level, but a standard for storing data on and retrieving data from a central server architecture. 14 | 15 | In addition to the above interfaces, most DSMR-meters also have additional interfaces, such as an optical [IEC 62056-21](https://en.wikipedia.org/wiki/IEC_62056#IEC_62056-21) serial interface and an S0 (DIN 43864) pulse-output (which is usually not accessible, unfortunately). 16 | 17 | Several versions of the official DSMR P1 standard are included in the `doc/` directory of this repository. Also included there are [notes on the broader IEC 62056-21 standard](doc/IEC-62056-21-notes.md), on which the P1-standard is based. 18 | 19 | 20 | ## The Parser 21 | 22 | This repository contains a parser for DSMR data-telegrams, based on the [Ragel state machine compiler](http://www.colm.net/open-source/ragel/), as well as the DSMR P1 specification documents (in `doc/`) and an example program in C for reading and parsing DSMR-data from a serial port in Linux (on any other POSIX system such as BSD or MacOS). To compile the Ragel parser and the example program, you need to install the [Ragel](http://www.colm.net/open-source/ragel/) state machine compiler (on Debian, Ubuntu and derivatives, simply do: `sudo apt-get install ragel`, on other systems you can `git clone git://colm.net/ragel.git`) and run `./make.sh`. In principle this parser can also be adapted for use on microcontrollers, although I haven't tried this yet. 23 | 24 | In addition to DSMR P1-telegrams, the parser can handle more general IEC 62056-21 telegram data, albeit with a very limited set of OBIS-objects, so that it can be used to obtain energy readings from many non-DSMR smart meters through the optical port. The parser has been tested with example data from all currently known versions of DSMR (2.2, 3.0, 4.x and 5.0.2), and with real data from a Landis-Gyr and Kaifa DSMR 4.2 electricity meters, in some cases with a slave gas meter connected. This code is open-source under the Apache 2.0 licence. 25 | 26 | 27 | ## Hardware specification 28 | 29 | The DSMR P1-port uses an RJ12 (6P6C) [modular connector](https://en.wikipedia.org/wiki/Modular_connector), which has 6 pins: 30 | 31 | 1. Vcc: +5V power supply, max. 250 mA continuous (DSMR >= 4.0) 32 | 2. RTS: data request, connect to +5V (4.0 - 5.5 V) to request data, connect to 0V/GND to stop data transmission 33 | 3. GND: data ground 34 | 4. NC: not connected 35 | 5. TXD: data line (open collector output, logically inverted 5V serial signal, 8N1, 115200 baud for DSMR >= 4.0, 9600 baud for DSMR < 4.0) 36 | 6. VccGND: power ground (DSMR >= 4.0) 37 | 38 | All P1-pins are galvanically isolated from mains through an opto-coupler, and have over-voltage protection. 39 | The data-line is an open-collector serial line output, which means that the signal is inverted relative to a normal serial line, and that it requires a pull-up resistor (1 - 10 kOhm) to be connected between RXD (pin 5) and the serial voltage required (for +5V you can connect it to Vcc / pin 1 or RTS / pin 2 on the P1-port). 40 | 41 | If RTS is set to HIGH, an ASCII text-based data-packet called a "telegram" is sent by the smart meter, either every 10 seconds (DSMR < 5.0) or every second (DSMR >= 5.0). The format of the telegrams is derived from the [IEC 62056-61](https://en.wikipedia.org/wiki/IEC_62056) standard for exchanging data with utility meters. 42 | 43 | 44 | ## Hardware setup 45 | 46 | There are [roughly three ways](http://domoticx.com/p1-poort-slimme-meter-uitlezen-hardware/) to connect the P1-port to a computing device: 47 | 48 | - Directly to a 5V or 3.3V TTL serial interface (e.g. the one on a Raspberry Pi), using a transistor (e.g. BC547) and some resistors (e.g. 1 kOhm and/or 10 kOhm) to invert the signal and to pull it up to the required voltage. There is a possible schematic [here](http://www.uploadarchief.net/files/download/p1%20smartmeter%20interface.gif). 49 | - Directly to a TTL serial interface using an inverter IC (e.g. a [SN7404](http://www.ti.com/product/sn7404)) to invert the signal, as shown in [this example](https://tweakers.net/ext/f/aWGFHpcLoa1bN6KxMARGtNRk/full.jpg). 50 | - Using an RS232-port (or a USB-RS232-converter) and a pull-up resistor. This is often the easiest solution, as this can be done without soldering, cheap USB RS232-converters are easy to find and you can add as many interfaces as you need (e.g. using multiple USB-devices or even a 4-port device based on an [MCS7840](https://www.asix.com.tw/products.php?op=pItemdetail&PItemID=260;74;109&PLine=74) quad serial controller). The RS232 serial standard and the P1 standard both use an inverted serial signal, and altough the LOW voltage levels are technically incompatible (-15V to -3V for RS232, 0V to 1V for P1), most RS232-interfaces accept 0V as LOW and are able to decode the data transmitted by the P1-port without any problems. Just connect the signal ground of the two interfaces, and connect TXD on the P1-port to RXD on the RS232-interface. You will also still need to connect a 1-10 kOhm pull-up resistor between Vcc and the data line, and connect RTS on the P1-port to Vcc. You may also have to connect VccGND and GND. 51 | 52 | You can also get a prefabricated P1 to USB cable (easily ordered online, e.g. from [SOS Solutions](https://www.sossolutions.nl/slimme-meter-kabel), [slimmemeterkabel.nl](https://www.slimmemeterkabel.nl/), [Sinforcon](https://sinforcon999.aliexpress.com/store/group/TTL-RJ-series/401907_258809895.html?spm=a2g0z.12010108.0.0.57c86a08lGHrqB) or [Oloey](https://aliexpress.com/store/group/USB-TTL-Cable/4500072_514143826.html?spm=a2g0z.12010615.0.0.393072acMeJif8)). So far, every USB RS232-converter I tested worked, and most Linux-kernels support the more common converter chips out-of-the-box, and will make the data available on the `/dev/ttyUSB0` serial device. 53 | 54 | If you build your own cable, it's easiest to connect pins 1 and 2 of the P1-interface together, and to connect a 1-10 kOhm pull-up resistor between either of these and pin 5 (TXD). Then simply connect RXD of your serial interface to TXD on the P1-port, and connect the data ground on both interfaces. You may also have to connect pin 6 (VccGND) to the data ground (pin 3) for the cable to work on some meters. Cables can be made without soldering, e.g. by using [Wago 221-412 splicing connectors](https://www.wago.com/global/installation-terminal-blocks-and-connectors/compact-splicing-connectors/p/221-412). I've used a [4-port USB-interface](https://aliexpress.com/item/9pin-RS232-USB-2-0-to-4-ports-Serial-DB9-COM-Controller-Connectors-Adapter-Hub-K400Y/32829639400.html) with these [DB9 female terminal block connectors](https://aliexpress.com/item/High-Quality-DB9-RS232-Serial-Female-Adapter-Plate-to-9-Position-Terminal-Connector-Black-Green-Yellow/32738009438.html) to read up to four meters at once. I made the cables using [three-pair UTP-cable](https://nl.farnell.com/pro-power/cbbr0104/cable-cw1308-3pair-100m/dp/147770) and [RJ12-connectors](https://nl.farnell.com/lumberg/p-128/modular-plug-crimp-rj12-6p6c/dp/1243235), with a [1 kOhm pull-up resistor](https://nl.farnell.com/multicomp/mcf-0-25w-1k/res-1k-5-250mw-axial-carbon-film/dp/9339051) and several [splicing connectors](https://nl.farnell.com/wago/221-412/compact-splicing-connector-2pos/dp/2534732) neatly packed away in a [small junction box](https://nl.farnell.com/spelsberg/334-904-01/box-junction-56x40x23mm-ip20/dp/1615342). 55 | 56 | By default, the software echoes P1-telegrams back to the serial interface, so you can connect other devices that also read P1-data. If you're using RS232, you will need to convert the ca. 15V signal on the TXD-pin to 5V (e.g. using a [L7805 linear converter](https://nl.farnell.com/stmicroelectronics/l7805acv/ic-v-reg-5-0v-7805-to-220-3/dp/1087086)), before handing it on to another device. If you're using a TTL serial interface, you will need to invert the TXD signal using a transistor or inverter IC. Especially if you're using a 3.3V serial interface, it's probably easiest to use an inverter IC with an open collector output, such as the [SN7406](http://www.ti.com/product/sn7406), or an NPN-transistor to invert the signal and create an [open collector output](https://en.wikipedia.org/wiki/Open_collector). 57 | 58 | ## Software setup 59 | 60 | If you're connecting the P1-interface directly to the serial pins on a Raspberry Pi using a transistor, you may need to configure your Raspberry Pi Linux system to leave the serial port alone. This involves removing the references to the serial device (`/dev/ttyAMA0`) in `/boot/cmdline.txt` (disable the kernel debugging output to serial) and `/etc/inittab` (disable the serial command shell), instructions on how to do this can be found [in this guide](https://elinux.org/RPi_Serial_Connection#S.2FW:_Preventing_Linux_from_using_the_serial_port). 61 | 62 | To test if data is coming in and the signal is inverted correctly, just compile and run the test program, and check if it sees any valid telegrams. 63 | 64 | On Raspbian and some other systems, you may have to install a C-compiler to compile the parser and the test program, e.g. `sudo apt-get install build-essential`. You may also have to install Ragel: `sudo apt-get install ragel`. Then clone the git-repository, compile the test program and run it: 65 | 66 | ``` 67 | git clone https://github.com/lvzon/dsmr-p1-parser.git 68 | cd dsmr-p1-parser 69 | ./make.sh 70 | ./p1-test-p1 /dev/ttyUSB0 errors.dat 71 | ``` 72 | 73 | If valid telegrams are coming in, you should see something like this within about 10 seconds: 74 | 75 | ``` 76 | Input device seems to be a serial terminal 77 | Possible telegram found at offset 0 78 | Possible telegram end at offset 624 79 | New-style telegram with length 630 80 | Header: XMX5LGBBFG1012463538 81 | P1 version: 4.2 82 | Timestamp: 1527686221 83 | Equipment ID: E0031003262xxxxxx 84 | Energy in, tariff 1: 1234.000000 kWh 85 | Energy in, tariff 2: 1235.000000 kWh 86 | Energy out, tariff 1: 0.000000 kWh 87 | Energy out, tariff 2: 0.000000 kWh 88 | Tariff: 2 89 | Power in: 0.048000 kW 90 | Power out: 0.000000 kW 91 | Power failures: 2 92 | Long power failures: 1 93 | Power failure events: 1 94 | Power failure event at 1484629982, 6885 s 95 | Voltage sags L1: 0 96 | Voltage swells L1: 0 97 | Current L1: 0 A 98 | Power in L1: 0.048000 kW 99 | Power out L1: 0.000000 kW 100 | Device 1 type: 3 101 | Device 1 ID: G0025003407xxxxxx 102 | Device 1 counter at 1527685200: 123.000000 m3 103 | CRC: 0x9b8d 104 | Parsing successful, data CRC 0x9b8d, telegram CRC 0x9b8d 105 | ``` 106 | 107 | If you get an error, check if the serial converter is connected and you're using the the correct serial device (depending on your setup it can also be `/dev/ttyAMA0`, `/dev/ttyS0`, `/dev/ttyUSB1` or something else, check `dmesg` to be sure). If no valid telegram is seen within 25 seconds or so, hit `CTRL-C` and check `errors.dat`. The program tries to autodetect the baud rate, so if `errors.dat` contains garbage, you probably forgot to invert the signal, or you're inverting it twice. If `errors.dat` is empty, try dumping the serial device data directly (e.g. `cat /dev/ttyUSB1`). If no data comes in, check your cable connections and especially check if your data-line and ground and pull-up resistor are all connected correctly and the request-pin 2 is connected to at least +4V (and at most 5.5V). 108 | 109 | If the cable-length is more than a few metres, this can cause the voltages to drop below 4 V, so you may need to measure this and either use a better cable or connect the pull-up resistor and Vcc-RTS at the P1-side rather than at the serial interface. Also note that older (DSMR 2.x or 3.x) metres do not have a 5V Vcc pin, so in this case you'll need to supply 5V or 3.3V from another source. 110 | 111 | ## Transmitting data 112 | 113 | The `packmsg`-branch contains an experimental application that reads telegrams from a serial device and echoes the telegrams back to the interface. Telegram data is parsed, and energy, power and gas data is sent directly to a server socket, using a very compact encoding based on [MessagePack](https://msgpack.org/). This application is functional but still under development, I use it in a pilot project to collect and act upon smart meter data in real time. I will try to include an example server and more documentation soon. 114 | 115 | ## TODO 116 | 117 | - Include packmsg-server example (Python code). 118 | - Document p1-lib data structure and functions. 119 | - Test with more meters. 120 | - Adapt the parser for use on non-POSIX microcontroller platforms. 121 | 122 | 123 | ## Other resources 124 | 125 | I wrote this parser for use at [Lens](http://lens-energie.nl/), because I needed something that was light-weight and complete, and there currently does not seem to be another full DSMR P1 telegram-parser that is open-source and can be used in regular C programs. However, there are many parsers in many other programming languages: 126 | 127 | - Matthijs Kooijman's [DSMR P1-parser for Arduino](https://github.com/matthijskooijman/arduino-dsmr), written in C++. 128 | - [Go library for reading/parsing P1-data](https://github.com/mhe/dsmr4p1) 129 | - [DSMR P1-parser in C#](https://github.com/peckham/DsmrParser) 130 | - [DSMR 4.2 P1 data collector in NodeJS](https://github.com/aisnoek/dsmr4-collector) 131 | - A very basic [DSMR 4 P1-parser in JavaScript](https://github.com/robertklep/node-dsmr-parser) 132 | - [DSMR P1 parser in Python](https://github.com/ndokter/dsmr_parser). 133 | - [Python module to read/parse P1-data](https://github.com/bwesterb/dsmrp1) 134 | - Another [very basic DSMR P1-reader/parser in Python](https://github.com/jvhaarst/DSMR-P1-telegram-reader) 135 | - Dennis Siemensma's extensive [DSMR reader software and GUI in Python](https://github.com/dennissiemensma/dsmr-reader) 136 | - Arne Kaas' [P1 data logger, using Python and SQLite](https://github.com/arnekaas/DSMR-P1-usb-logger) 137 | - Another [Python P1 data logger](https://github.com/dschutterop/dsmr) 138 | - And yet another [P1 data logger using Python and MySQL](https://github.com/micromys/DSMR) 139 | - A [DSMR 4.2 P1-reader using PHP and MySQL](https://github.com/arnocs/dsmrp1spot) 140 | 141 | 142 | For more information on interfacing with the P1-port (mostly in Dutch): 143 | 144 | - 145 | - 146 | - 147 | - 148 | - 149 | - 150 | 151 | -------------------------------------------------------------------------------- /crc16.c: -------------------------------------------------------------------------------- 1 | /* 2 | File: crc16.c 3 | 4 | Functions to calculate CRC16 5 | */ 6 | 7 | #include 8 | 9 | uint16_t crc16_ccitt (const uint8_t *data, unsigned int length) 10 | { 11 | // Polynomial: x^16 + x^12 + x^5 + 1 (0x8408) 12 | // Initial value: 0xffff 13 | 14 | uint8_t x; 15 | uint16_t crc = 0xffff; 16 | 17 | while (length--) { 18 | x = crc >> 8 ^ *data++; 19 | x ^= x >> 4; 20 | crc = (crc << 8) ^ ((uint16_t)(x << 12)) ^ ((uint16_t)(x <<5)) ^ ((uint16_t)x); 21 | } 22 | 23 | return crc; 24 | } 25 | 26 | 27 | uint16_t crc16 (const uint8_t *data, unsigned int length) 28 | { 29 | // Polynomial: x^16 + x^15 + x^2 + 1 (0xa001) 30 | 31 | uint8_t x; 32 | uint16_t crc = 0; 33 | 34 | while (length--) { 35 | 36 | int i; 37 | 38 | crc ^= *data++; 39 | for (i = 0 ; i < 8 ; ++i) { 40 | if (crc & 1) 41 | crc = (crc >> 1) ^ 0xa001; 42 | else 43 | crc = (crc >> 1); 44 | } 45 | } 46 | 47 | return crc; 48 | } 49 | -------------------------------------------------------------------------------- /crc16.h: -------------------------------------------------------------------------------- 1 | /* 2 | File: crc16.h 3 | 4 | Functions to calculate CRC16 5 | */ 6 | 7 | #include 8 | 9 | uint16_t crc16_ccitt (const uint8_t *data, unsigned int length); 10 | uint16_t crc16 (const uint8_t *data, unsigned int length); 11 | -------------------------------------------------------------------------------- /doc/DSMR-2.2-P1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvzon/dsmr-p1-parser/6323a42f77e70ec3cb8e0efdb9d34bd6dae4a00d/doc/DSMR-2.2-P1.pdf -------------------------------------------------------------------------------- /doc/DSMR-3.0-P1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvzon/dsmr-p1-parser/6323a42f77e70ec3cb8e0efdb9d34bd6dae4a00d/doc/DSMR-3.0-P1.pdf -------------------------------------------------------------------------------- /doc/DSMR-4.0.0-P1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvzon/dsmr-p1-parser/6323a42f77e70ec3cb8e0efdb9d34bd6dae4a00d/doc/DSMR-4.0.0-P1.pdf -------------------------------------------------------------------------------- /doc/DSMR-4.2.2-P1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvzon/dsmr-p1-parser/6323a42f77e70ec3cb8e0efdb9d34bd6dae4a00d/doc/DSMR-4.2.2-P1.pdf -------------------------------------------------------------------------------- /doc/DSMR-4.2.2-main.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvzon/dsmr-p1-parser/6323a42f77e70ec3cb8e0efdb9d34bd6dae4a00d/doc/DSMR-4.2.2-main.pdf -------------------------------------------------------------------------------- /doc/DSMR-5.0.2-P1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvzon/dsmr-p1-parser/6323a42f77e70ec3cb8e0efdb9d34bd6dae4a00d/doc/DSMR-5.0.2-P1.pdf -------------------------------------------------------------------------------- /doc/IEC-62056-21-notes.md: -------------------------------------------------------------------------------- 1 | # IEC 62056-21 and derivatives (e.g. DSMR P1) 2 | 3 | Levien van Zon (levien at gnuritas dot org) 4 | 5 | ### Hardware 6 | 7 | IEC 62056-21 smart meters are generally fitted with an optical "D0" interface, which is used to exchange data with the meter. An optical D0 reading/writing head for IEC 62056-21 can be ordered e.g. here: 8 | 9 | - 10 | 11 | It uses infrared light (between 800 and 1000 nm) for signal transmission and reception, and is generally kept in place with a magnet. The cable should always point downward. 12 | 13 | The Dutch DSMR smart meters do have an optical "D0" interface, but it often seems to be disabled in software. Instead, these meters are fitted with a 5V inverted serial "P1" interface, which can only be used for reading data, in a text-based format that is derived from IEC 62056-21 mode D (see below). 14 | 15 | 16 | ### Serial connection 17 | 18 | The optical IEC 62056-21 interface initially operates at 300 baud, 7N1, even parity. It can be used for both reading and writing, although this should noy be done at the same time (it is half-duplex). 19 | 20 | The DSMR P1 interface is read-only and uses a fixed baud rate of either 9600 baud, or 115200 baud for DSMR version >=4.0, and telegrams are sent every 1 or 10 seconds, as long as the RTS line is high. 21 | 22 | Both interfaces use an inverted serial signal relative to a "normal" serial port. This means that a binary 1 is represented by the electrical or optical signal being low (0V or no IR light), and a binary 0 is represented by +5V or an IR light signal. If you use a regular TTL serial port, extra hardware (e.g. an inverter IC or a transistor) is usually needed to invert the serial signal. RS232 also uses an inverted signal, but operates in the voltage range -15V to +15V, so voltage conversion is usually needed if an RS232-interface is used as serial interface with a smart meter. 23 | 24 | 25 | ### Wake-up 26 | 27 | Especially battery powered meters require a wake-up sequence to be sent before further communication on the optical IEC 62056-21 interface. In most cases, sending a string of 65 zero-characters ('\0') should do the trick, followed by a pause of 1.5 seconds. If serial data is sent asynchronously, make sure you also wait for the data to be sent, which probably requires a total waiting time of 2.7 seconds after sending the wake-up string. 28 | 29 | The DSMR P1 interface does not require wake-up, but it only sends data if the RTS pin is high (>4V). 30 | 31 | 32 | ### IEC 62056-21 Protocol modes 33 | 34 | IEC 62056-21 meters may support several different protocol modes: 35 | 36 | A. Fixed rate (300 baud), bidirectional ASCII protocol. The master device sends a sign-on sequence, the slave device (meter) responds with an identifier followed by a data telegram. The master may optionally enter programming mode after receiving the telegram. 37 | 38 | B. Bidirectional ASCII protocol with baud-rate switching. This is similar to mode A, but after transmitting the identifier at 300 baud, the slave device may switch to a higher baud rate, which is specified in the identifier. 39 | 40 | C. This is similar to mode B, but it allows manufacturer-specific extentions and a device in mode C will not automatically send a data telegram following the identifier. Instead, the master has to switch to readout or programming mode, and may also specify whether the baud rate should be switched or not. 41 | 42 | D. Fixed rate (2400 baud) unidirectional ASCII protocol. Data is either pushed by the meter or requested some other way (e.g. by pushing a button). The meter sends an identifier followed by a data telegram. 43 | 44 | E. This is an extended version of modes A-C, whereby the meter may specify that it supports other (e.g. binary) transmission protocols. 45 | 46 | DSMR P1 is essentially a slightly non-standard variant of IEC 62056-21 protocol mode D, using a higher baud rate, whereby the telegram is requested by a signal on the RTS pin of the P1 port. 47 | 48 | 49 | ### Sign-on sequence 50 | 51 | When using the optical IEC 62056-21 interface in all modes except D, a sign-on sequence is required before a telegram is sent by the meter. This sign-on sequence is sent at 300 baud, and is as follows: `"/?!\r\n"` It may include an optional device address between ? and !. 52 | 53 | After successful reception of the sign-on-sequence, the meter should send a data line, which starts with '/', followed by an identification string and the end-of-line sequence "\r\n" (CR+LF). Example: "/ISk5MT171-0133\r\n". 54 | 55 | The first three characters of the identification string are the vendor ID, which consists of two upper case letters and a third letter which may be either upper or lower case. If the third letter is lower case, the device has a minimum reaction time of 20ms. If the device transmits only upper case letters, a minimum reaction time of 200ms should be assumed, although the device *may* still support 20ms. 56 | 57 | The fourth character is the baud rate identifier: 58 | 59 | 0. 300 Bd 60 | 1. 600 Bd 61 | 2. 1200 Bd 62 | 3. 2400 Bd 63 | 4. 4800 Bd 64 | 5. 9600 Bd 65 | 6. 19200 Bd 66 | 67 | The above baud rate identifier values are used when a meter operates in protocol mode C, D or E. If the meter operates in mode A, the baud rate is always 300 baud and the identifier can be any character except [0-9], [A-I], '/' or '!'. If the meter operates in mode B, the baud rate identifier is: 68 | 69 | A. 300 Bd 70 | B. 600 Bd 71 | C. 1200 Bd 72 | D. 2400 Bd 73 | E. 4800 Bd 74 | F. 9600 Bd 75 | G. 19200 Bd 76 | 77 | The rest of the identification string is the model identifier, up to "\r\n" or (in mode E) an optional `'\'`-character, which acts as sequence-delimiter and may be followed by mode-information, e.g.: `/ISk5\2MT382-1000` 78 | In this case, the character following `\` is the mode identifier, which may be '2' for binary mode (which is based on [HDLC](https://en.wikipedia.org/wiki/High-Level_Data_Link_Control)). The model identifier (with optional mode information) may be up to 16 bytes long. 79 | 80 | If the baud rate identifier is valid for mode C or E, and the optical interface is used, the communication rate can/should be updated. To do this, send an acknowledgement message (still at 300 baud), which starts with an ACK byte (0x06), followed by a protocol control character (use '0' for a normal protocol procedure), the baud rate identifier (see above), a mode control character (use '0' to set the mode to reading data) and "\r\n". Wait 300 ms before changing the baud rate. 81 | 82 | After sign-on (and in mode C/E, acknowledgement of readout mode), the meter will send a telegram, after which it will return to 300 baud. In the case of DSMR P1, sign-on, acknowledgement or baud rate change is not needed. DSMR P1 uses a fixed baud rate of either 9600 baud, or 115200 baud for DSMR version >=4.0, and telegrams are sent every 1 or 10 seconds, as long as the RTS line is high. 83 | 84 | 85 | ### Telegrams 86 | 87 | A telegram starts with '/', followed by an identifier string, as described above. 88 | 89 | In IEC-62056-21, the subsequent data message starts with the STX frame start character 0x02, in DSMR P1 this is left out. 90 | Each subsequent line of a telegram is a data object, which starts with an object identifier, followed by a value, an optional unit and a line terminator "\r\n".Example: `"1-0:1.8.1(000581.161*kWh)\r\n"` 91 | 92 | The section up to the front boundary character '(' is the OBIS object identification (see below), which may have a maximum size of 16 characters, and may include any printable character except "(", ")", "/" and "!". 93 | 94 | After the front boundary character '(' comes a value. This value may have a maximum size of 32 characters, or 128 in protocol mode C. All printable characters are allowed, except "(", " * ", ")", "/" and "!". A decimal point is used, rather than a decimal comma, and this counts in the number of characters. 95 | 96 | The value may be followed by a separator character '*' and a unit of maximum 16 characters, which may contain any printable character except "(", ")", "/" and "!". 97 | 98 | The data set ends with the read boundary character ')', and the line ending "\r\n" 99 | 100 | Some meters (e.g. the ISKRA MT171) seem to include the unit in the value (e.g.: `"1-0:1.8.1*255(0000000 kWh)\r\n"`), which may be technically allowed but makes things harder to parse... 101 | 102 | The telegram ends with '!'. In DSMR P1 v4 and above, this is followed by a CRC16 over the preceding characters (from / to !, both inclusive). The CRC is 4 characters long: 2 bytes, hex-encoded, MSB first. After the optional CRC, one or two line-ends ("\r\n") are usually sent. 103 | In the IEC-62056-21 data message, '!' is followed by "\r\n", the ETX frame end character 0x03 and a block check character BCC. The BCC is calculated over the bytes after STX up to and including the ETX byte, and is an [XOR-based longitudinal redundancy check (LRC)](https://en.wikipedia.org/wiki/Longitudinal_redundancy_check). To calculate this BCC, take the first byte XOR 0xff, XOR this value with the second byte, and so forth up to and including the last byte, and XOR the final value with 0xff. 104 | 105 | 106 | ### OBIS object identifiers 107 | 108 | The OBIS object identifiers are described by [IEC 62056-61: Electricity metering - Data exchange for meter reading – Part 61 : Object identification system (OBIS)](http://webstore.iec.ch/webstore/webstore.nsf/mysearchajax?Openform&key=62056-61&sorting=&start=1&onglet=1). 109 | An OBIS code consists of six value groups: `A-B:C.D.E*F` 110 | The first value A specifies the concept or physical medium that the object refers to: 111 | 112 | 0. Abstract objects 113 | 1. Electricity 114 | 4. Heating costs 115 | 5. Cooling energy 116 | 6. Heat 117 | 7. Gas 118 | 8. Water 119 | 9. Warm water 120 | 121 | The second value B is the channel, which for electricity is generally '0' (not used), but which can also be 'd' (difference), '1' (tentative value), '2' (final value) or 'p' (processing status). 122 | 123 | The third value C specifies the variable type, e.g. general purpose (0), imported active power/energy (1), exported active power/energy (2), frequency (14), phase A/B/C imported active power/energy (21/41/61), phase A/B/C exported active power/energy (22/42/62), phase A/B/C current (31/51/71), phase A/B/C voltage (32/52/72), service variable ('C'), error message ('F'), list object ('L'), etc. 124 | 125 | The fourth value D specifies the measurement or value type, e.g. energy (8), the instantaneous value (7) or various types of minimum, maximum and average values. 126 | 127 | The fifth value E specifies the tariff, whereby 0 is total, 1 is tariff 1, 2 is tariff 2, etc. 128 | 129 | The final separator '*' and value F seem to be optional, and specify the billing periods. 130 | 131 | Common objects: 132 | - 0.0.0 is the 8-character meter number, e.g. `1-0:0.0.0*255(38820967)` 133 | - 0.2.0 is the meter firmware version, e.g. `1-0:0.2.0*255(V1.0)` 134 | - 0.1.2*xx is the timestamp of billing period xx 135 | - F.F is an error message (although I've seen at least one meter incorrectly report this as "`FF(00000000)`") 136 | 137 | 138 | 139 | ### ACK and sign-off 140 | 141 | After receiveing a telegram, the master should send an ACK (0x06) or NAK (0x15) byte to either confirm reception of the telegram, or request a re-send. 142 | 143 | The master may sign off explicitly, using a break sequence: SOH (start of header, 0x01) 'B' (exit command) '0' (complete sign-off) ETX (end of frame, 0x03) 'q' (the BCC block check character, the calculated length parity over the characters of the data message beginning immediately after the STX up to the included ETX.) 144 | 145 | In the case of DSMR P1, telegrams are sent as long as the RTS line is high. 146 | 147 | 148 | ### Notes 149 | 150 | In battery-powered devices (e.g. gas, thermal and water meters), each wake-up and request may reduce the battery life span by several hours to days. 151 | 152 | 153 | Sources: 154 | 155 | - IEC 62056-21, "Electricity metering – Data exchange for meter reading, tariff and load control – Part 21: Direct local data exchange" (which is not available for free, but can be easily found with Google). 156 | - 157 | - 158 | - 159 | 160 | -------------------------------------------------------------------------------- /doc/raspberry-pi-p1-interface.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lvzon/dsmr-p1-parser/6323a42f77e70ec3cb8e0efdb9d34bd6dae4a00d/doc/raspberry-pi-p1-interface.gif -------------------------------------------------------------------------------- /dsmr-data.h: -------------------------------------------------------------------------------- 1 | 2 | // Data structure to hold smart meter data, at least for meters 3 | // that comply with the Dutch DSMR standard (and also for some 4 | // non-DSMR meters that do comply with IEC 62056-21). 5 | 6 | #ifndef DSMR_DATA_H 7 | 8 | #include 9 | 10 | #define MAX_TARIFFS 2 // IEC 62056-21 allows 256, DSMR specifies just 2 11 | #define MAX_PHASES 3 12 | #define MAX_DEVS 4 // DSMR allows up to 4 M-bus devices 13 | #define MAX_EVENTS 10 // DSMR allows max. 10 power failure events to be logged 14 | 15 | #define LEN_HEADER 22 // '/' + 3 bytes vendor ID + 1 byte baud rate ID + max. 16 bytes model ID + '\0' 16 | #define LEN_EQUIPMENT_ID 18 // 5 bytes meter ID + 10 bytes serial + 2 bytes year + '\0' 17 | #define LEN_VALUE 32 // IEC 62056-21 allows max. 32 bytes 18 | #define LEN_UNIT 16 // IEC 62056-21 allows max. 16 bytes 19 | #define LEN_MESSAGE 2048 // DSMR allows max. 2048 bytes 20 | #define LEN_MESSAGE_CODES 8 // DSMR allows max. 8 bytes 21 | 22 | struct dsmr_data_struct { 23 | 24 | char header[LEN_HEADER]; 25 | char equipment_id[LEN_EQUIPMENT_ID]; 26 | 27 | uint32_t timestamp; 28 | 29 | int8_t P1_version_major, P1_version_minor; 30 | uint8_t tariff; 31 | int8_t switchpos; 32 | 33 | double E_in[MAX_TARIFFS + 1], 34 | E_out[MAX_TARIFFS + 1], 35 | P_in_total, P_out_total, P_threshold, 36 | I[MAX_PHASES], 37 | V[MAX_PHASES], 38 | P_in[MAX_PHASES], P_out[MAX_PHASES]; 39 | 40 | char unit_E_in[MAX_TARIFFS + 1][LEN_UNIT + 1], unit_E_out[MAX_TARIFFS + 1][LEN_UNIT + 1], 41 | unit_P_in_total[LEN_UNIT + 1], unit_P_out_total[LEN_UNIT + 1], 42 | unit_P_threshold[LEN_UNIT + 1], 43 | unit_I[MAX_PHASES][LEN_UNIT + 1], 44 | unit_V[MAX_PHASES][LEN_UNIT + 1], 45 | unit_P_in[MAX_PHASES][LEN_UNIT + 1], unit_P_out[MAX_PHASES][LEN_UNIT + 1]; 46 | 47 | uint32_t power_failures, power_failures_long, 48 | V_sags[MAX_PHASES], V_swells[MAX_PHASES]; 49 | 50 | char textmsg[LEN_MESSAGE + 1], textmsg_codes[LEN_MESSAGE_CODES + 1]; 51 | 52 | uint8_t dev_type[MAX_DEVS]; 53 | int8_t dev_valve[MAX_DEVS]; 54 | double dev_counter[MAX_DEVS]; 55 | uint32_t dev_counter_timestamp[MAX_DEVS]; 56 | char unit_dev_counter[MAX_DEVS][LEN_UNIT + 1]; 57 | char dev_id[MAX_DEVS][LEN_EQUIPMENT_ID]; 58 | 59 | uint8_t pfail_events; 60 | uint32_t pfail_event_end_time[MAX_EVENTS]; 61 | uint32_t pfail_event_duration[MAX_EVENTS]; 62 | char unit_pfail_event_duration[MAX_EVENTS][LEN_UNIT + 1]; 63 | }; 64 | 65 | #define DSMR_DATA_H 1 66 | #endif 67 | -------------------------------------------------------------------------------- /example-data/p1-example-3.0.txt: -------------------------------------------------------------------------------- 1 | /ISk5\2MT382-1000 2 | 3 | 0-0:96.1.1(4B384547303034303436333935353037) 4 | 1-0:1.8.1(12345.678*kWh) 5 | 1-0:1.8.2(12345.678*kWh) 6 | 1-0:2.8.1(12345.678*kWh) 7 | 1-0:2.8.2(12345.678*kWh) 8 | 0-0:96.14.0(0002) 9 | 1-0:1.7.0(001.19*kW) 10 | 1-0:2.7.0(000.00*kW) 11 | 0-0:17.0.0(016*A) 12 | 0-0:96.3.10(1) 13 | 0-0:96.13.1(303132333435363738) 14 | 0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F) 15 | 0-1:96.1.0(3232323241424344313233343536373839) 16 | 0-1:24.1.0(03) 17 | 0-1:24.3.0(090212160000)(00)(60)(1)(0-1:24.2.1)(m3)(00000.000) 18 | 0-1:24.4.0(1) 19 | ! 20 | -------------------------------------------------------------------------------- /example-data/p1-example-4.0.txt: -------------------------------------------------------------------------------- 1 | /ISk5\2MT382-1000 2 | 3 | 1-3:0.2.8(40) 4 | 0-0:1.0.0(101209113020W) 5 | 0-0:96.1.1(4B384547303034303436333935353037) 6 | 1-0:1.8.1(123456.789*kWh) 7 | 1-0:1.8.2(123456.789*kWh) 8 | 1-0:2.8.1(123456.789*kWh) 9 | 1-0:2.8.2(123456.789*kWh) 10 | 0-0:96.14.0(0002) 11 | 1-0:1.7.0(01.193*kW) 12 | 1-0:2.7.0(00.000*kW) 13 | 0-0:17.0.0(016.1*kW) 14 | 0-0:96.3.10(1) 15 | 0-0:96.7.21(00004) 16 | 0-0:96.7.9(00002) 17 | 1-0:99.97.0(2)(0-0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(00000000301*s) 18 | 1-0:32.32.0(00002) 19 | 1-0:52.32.0(00001) 20 | 1-0:72.32.0(00000) 21 | 1-0:32.36.0(00000) 22 | 1-0:52.36.0(00003) 23 | 1-0:72.36.0(00000) 24 | 0-0:96.13.1(3031203631203831) 25 | 0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F) 26 | 0-1:24.1.0(03) 27 | 0-1:96.1.0(3232323241424344313233343536373839) 28 | 0-1:24.2.1(101209110000W)(12785.123*m3) 29 | 0-1:24.4.0(1) 30 | !522B 31 | -------------------------------------------------------------------------------- /example-data/p1-example-5.0.txt: -------------------------------------------------------------------------------- 1 | /ISk5\2MT382-1000 2 | 3 | 1-3:0.2.8(50) 4 | 0-0:1.0.0(101209113020W) 5 | 0-0:96.1.1(4B384547303034303436333935353037) 6 | 1-0:1.8.1(123456.789*kWh) 7 | 1-0:1.8.2(123456.789*kWh) 8 | 1-0:2.8.1(123456.789*kWh) 9 | 1-0:2.8.2(123456.789*kWh) 10 | 0-0:96.14.0(0002) 11 | 1-0:1.7.0(01.193*kW) 12 | 1-0:2.7.0(00.000*kW) 13 | 0-0:96.7.21(00004) 14 | 0-0:96.7.9(00002) 15 | 1-0:99.97.0(2)(0-0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(0000000301*s) 16 | 1-0:32.32.0(00002) 17 | 1-0:52.32.0(00001) 18 | 1-0:72.32.0(00000) 19 | 1-0:32.36.0(00000) 20 | 1-0:52.36.0(00003) 21 | 1-0:72.36.0(00000) 22 | 0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F) 23 | 1-0:32.7.0(220.1*V) 24 | 1-0:52.7.0(220.2*V) 25 | 1-0:72.7.0(220.3*V) 26 | 1-0:31.7.0(001*A) 27 | 1-0:51.7.0(002*A) 28 | 1-0:71.7.0(003*A) 29 | 1-0:21.7.0(01.111*kW) 30 | 1-0:41.7.0(02.222*kW) 31 | 1-0:61.7.0(03.333*kW) 32 | 1-0:22.7.0(04.444*kW) 33 | 1-0:42.7.0(05.555*kW) 34 | 1-0:62.7.0(06.666*kW) 35 | 0-1:24.1.0(003) 36 | 0-1:96.1.0(3232323241424344313233343536373839) 37 | 0-1:24.2.1(101209112500W)(12785.123*m3) 38 | !EF2F 39 | -------------------------------------------------------------------------------- /logmsg.h: -------------------------------------------------------------------------------- 1 | /* 2 | Header: logmsg.h 3 | 4 | Macros, functions and structures used for writing messages to a logfile. 5 | 6 | (c)2013-2015, Levien van Zon (levien@zonnetjes.net) 7 | */ 8 | 9 | 10 | #include 11 | 12 | 13 | /* Logging macros */ 14 | 15 | #define LL_FATAL 1 16 | #define LL_ERROR 2 17 | #define LL_WARNING 3 18 | #define LL_NORMAL 4 19 | #define LL_VERBOSE 5 20 | #define LL_DEBUG 6 21 | 22 | typedef struct msglogger_struct { 23 | 24 | char *logfile_name; 25 | FILE *logfile; 26 | int loglevel; 27 | 28 | } messagelogger; 29 | 30 | messagelogger logger; 31 | 32 | static inline void init_msglogger() { 33 | logger.logfile_name = NULL; 34 | logger.logfile = stdout; 35 | logger.loglevel = LL_NORMAL; 36 | } 37 | 38 | 39 | #define logmsg(level, format, args...) { \ 40 | if (level <= logger.loglevel && logger.logfile) { \ 41 | if (level == LL_WARNING) \ 42 | fprintf(logger.logfile, "WARNING: "); \ 43 | else if (level == LL_ERROR) \ 44 | fprintf(logger.logfile, "ERROR: "); \ 45 | else if (level == LL_FATAL) \ 46 | fprintf(logger.logfile, "FATAL ERROR: "); \ 47 | fprintf(logger.logfile, format, ##args); \ 48 | fflush(logger.logfile); \ 49 | } \ 50 | } 51 | 52 | 53 | -------------------------------------------------------------------------------- /make.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ragel -s p1-parser.rl 4 | gcc -Wall -Os -g -o p1-test p1-parser.c p1-lib.c p1-test.c crc16.c 5 | gcc -Wall -Os -g -o d0-test p1-parser.c p1-lib.c p1-test-d0.c crc16.c 6 | 7 | -------------------------------------------------------------------------------- /p1-lib.c: -------------------------------------------------------------------------------- 1 | #define _GNU_SOURCE 1 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include "logmsg.h" 15 | 16 | #include "crc16.h" 17 | 18 | #include "p1-lib.h" 19 | 20 | 21 | uint16_t crc_telegram (const uint8_t *data, unsigned int length) 22 | { 23 | // Calculate the CRC16 of a telegram, for verification 24 | 25 | if (data[length - 3] == '!') { 26 | 27 | // Old-style telegrams end with "!\r\n" and do not contain a CRC, so there's no point in checking it 28 | 29 | return 0; 30 | 31 | } else if (data[length - 7] == '!') { 32 | 33 | // Calculate CRC16 from start of telegram until '!' (inclusive) 34 | // Length is full telegram length minus 2 bytes CR + LF minus 4 bytes hex-encoded CRC16 35 | 36 | return crc16(data, length - 6); 37 | } 38 | 39 | // Invalid telegram 40 | 41 | return 0; 42 | } 43 | 44 | 45 | size_t read_telegram (int fd, uint8_t *buf, size_t bufsize, size_t maxfailbytes) 46 | { 47 | // Try to read a full P1-telegram from a file-handle and store it in a buffer 48 | 49 | int telegram = 0; 50 | uint8_t byte; 51 | size_t offset = 0, failed = 0; 52 | ssize_t len; 53 | 54 | do { 55 | len = read(fd, &byte, 1); 56 | if (len > 0) { 57 | if (!telegram && byte == '/') { 58 | // Possible start of telegram 59 | logmsg(LL_VERBOSE, "Possible telegram found at offset %lu\n", (unsigned long)offset); 60 | telegram = 1; 61 | buf[offset++] = byte; 62 | } else if (telegram && offset < bufsize) { 63 | // Possible telegram content 64 | buf[offset++] = byte; 65 | if (byte == '!') { 66 | // Possible end of telegram, try to read either cr + lf or CRC value 67 | logmsg(LL_VERBOSE, "Possible telegram end at offset %lu\n", (unsigned long)offset); 68 | len = read(fd, buf + offset, 1); 69 | len += read(fd, buf + offset + 1, 1); 70 | if (len == 2) { 71 | if (buf[offset] == '\r') { 72 | // Old-style telegram without CRC 73 | logmsg(LL_VERBOSE, "Old-style telegram with length %lu\n", (unsigned long)offset + len); 74 | return offset + len; 75 | } else { 76 | // Possible start of CRC, try reading 4 more bytes 77 | offset += len; 78 | len = read(fd, buf + offset, 1); // Call read 4 times, because otherwise it can return with <4 bytes 79 | len += read(fd, buf + offset + 1, 1); 80 | len += read(fd, buf + offset + 2, 1); 81 | len += read(fd, buf + offset + 3, 1); 82 | if (len == 4 && buf[offset + 2] == '\r') { 83 | // New style telegram with CRC 84 | logmsg(LL_VERBOSE, "New-style telegram with length %lu\n", (unsigned long)offset + len); 85 | return offset + len; 86 | } 87 | } 88 | } 89 | // If we reach this point, we haven't found a valid telegram, try again 90 | logmsg(LL_VERBOSE, "Invalid telegram, restart scanning\n"); 91 | failed += offset + len; 92 | offset = 0; 93 | telegram = 0; 94 | } 95 | } else if (offset >= bufsize) { 96 | // Buffer overflow before telegram end, restart search for telegrams 97 | logmsg(LL_VERBOSE, "Buffer overflow before valid telegram end, restart scanning\n"); 98 | failed += offset; 99 | offset = 0; 100 | telegram = 0; 101 | } 102 | } 103 | } while (len > 0 && (maxfailbytes == 0 || failed < maxfailbytes)); 104 | 105 | // Return zero if we get a read error, or if we've read the maximum number of non-valid bytes 106 | 107 | return 0; 108 | } 109 | 110 | 111 | int telegram_parser_open (telegram_parser *obj, char *infile, size_t bufsize, int timeout, char *dumpfile) 112 | { 113 | if (obj == NULL) { 114 | return -1; 115 | } 116 | 117 | parser_init(&(obj->parser)); // Initialise Ragel state machine 118 | 119 | obj->data = &(obj->parser.data); 120 | obj->status = 0; 121 | 122 | obj->buffer = NULL; 123 | obj->bufsize = 0; 124 | obj->len = 0; 125 | 126 | obj->fd = -1; 127 | obj->terminal = 0; 128 | 129 | if (timeout <= 0) { 130 | timeout = READ_TIMEOUT; // In seconds 131 | } 132 | 133 | obj->timeout = timeout; 134 | 135 | if (infile) { 136 | obj->fd = open(infile, O_RDWR | O_NOCTTY); // If we open a serial device, make sure it doesn't become the controlling TTY 137 | 138 | if (obj->fd < 0) { 139 | logmsg(LL_ERROR, "Could not open input file/device %s: %s\n", infile, strerror(errno)); 140 | return -2; 141 | } 142 | 143 | if (tcgetattr(obj->fd, &(obj->oldtio)) == 0) { 144 | 145 | logmsg(LL_VERBOSE, "Input device seems to be a serial terminal\n"); 146 | 147 | obj->terminal = 1; // If we can get terminal attributes, assume we're reading from a serial device 148 | 149 | memset(&(obj->newtio), 0, sizeof(struct termios)); /* Clear the new terminal data structure */ 150 | 151 | obj->newtio.c_cflag = B115200 | CS8 | CLOCAL | CREAD; // Start at 115200 baud, 8-bit characters, ignore control lines, enable reading 152 | obj->newtio.c_iflag = 0; 153 | obj->newtio.c_oflag = 0; 154 | obj->newtio.c_lflag = 0; // Set input mode (non-canonical, no echo, etc.) 155 | obj->newtio.c_cc[VTIME] = (timeout * 10); // Inter-character timer or timeout in 0.1s (0 = unused) 156 | obj->newtio.c_cc[VMIN] = 0; // Blocking read until 1 char received or timeout 157 | 158 | tcflush(obj->fd, TCIFLUSH); // Flush any data still left in the input buffer, to avoid confusing the parsers 159 | tcsetattr(obj->fd, TCSANOW, &(obj->newtio)); // Set new terminal attributes 160 | } 161 | } 162 | 163 | if (dumpfile) { 164 | // TODO: use a file descriptor rather than a stdio pointer 165 | obj->dumpfile = fopen(dumpfile, "a"); 166 | if (obj->dumpfile == NULL) { 167 | logmsg(LL_ERROR, "Could not open output file %s\n", dumpfile); 168 | return -3; 169 | } 170 | } else { 171 | obj->dumpfile = NULL; 172 | } 173 | 174 | if (bufsize == 0) { 175 | bufsize = PARSER_BUFLEN; 176 | } 177 | 178 | obj->buffer = malloc(bufsize); 179 | if (obj->buffer) { 180 | obj->bufsize = bufsize; 181 | } else { 182 | logmsg(LL_ERROR, "Could not allocate %lu byte telegram buffer\n", (unsigned long)bufsize); 183 | return -4; 184 | } 185 | 186 | obj->mode = 'P'; 187 | 188 | return 0; 189 | } 190 | 191 | 192 | void telegram_parser_close (telegram_parser *obj) 193 | { 194 | if (obj == NULL) { 195 | return; 196 | } 197 | 198 | if (obj->bufsize && obj->buffer) { 199 | free(obj->buffer); 200 | obj->buffer = NULL; 201 | obj->bufsize = 0; 202 | obj->len = 0; 203 | } 204 | 205 | if (obj->fd > 0) { 206 | if (obj->terminal) { 207 | tcsetattr(obj->fd, TCSANOW, &(obj->oldtio)); // Restore old port settings 208 | } 209 | close(obj->fd); 210 | obj->fd = -1; 211 | obj->terminal = 0; 212 | } 213 | 214 | if (obj->dumpfile) { 215 | fclose(obj->dumpfile); 216 | obj->dumpfile = NULL; 217 | } 218 | } 219 | 220 | 221 | int telegram_parser_read (telegram_parser *obj) 222 | { 223 | uint16_t crc = 0; 224 | 225 | if (obj == NULL) { 226 | return -1; 227 | } 228 | 229 | if (obj->buffer == NULL || obj->bufsize == 0) { 230 | return -2; 231 | } 232 | 233 | if (obj->fd <= 0) { 234 | return -3; 235 | } 236 | 237 | obj->parser.crc16 = 0; 238 | 239 | obj->len = read_telegram(obj->fd, obj->buffer, obj->bufsize, obj->bufsize); 240 | 241 | if (obj->len) { 242 | parser_init(&(obj->parser)); 243 | parser_execute(&(obj->parser), (const char *)(obj->buffer), obj->len, 1); 244 | obj->status = parser_finish(&(obj->parser)); // 1 if final state reached, -1 on error, 0 if final state not reached 245 | if (obj->status == 1) { 246 | crc = crc_telegram(obj->buffer, obj->len); 247 | // TODO: actually report CRC error 248 | logmsg(LL_VERBOSE, "Parsing successful, data CRC 0x%x, telegram CRC 0x%x\n", crc, obj->parser.crc16); 249 | } 250 | if (obj->parser.parse_errors) { 251 | logmsg(LL_VERBOSE, "Parse errors: %d\n", obj->parser.parse_errors); 252 | if (obj->dumpfile) { 253 | fwrite(obj->buffer, 1, obj->len, obj->dumpfile); 254 | fflush(obj->dumpfile); 255 | } 256 | } 257 | } 258 | 259 | if (obj->terminal && obj->len == 0 && obj->mode == 'P') { 260 | 261 | // Try a different baud rate, maybe we have an old DSMR meter that runs at 9600 baud 262 | 263 | speed_t baudrate = cfgetispeed(&(obj->newtio)); 264 | 265 | if (baudrate == B115200) 266 | cfsetispeed(&(obj->newtio), B9600); 267 | else 268 | cfsetispeed(&(obj->newtio), B115200); 269 | 270 | tcflush(obj->fd, TCIFLUSH); // Flush any data still left in the input buffer, to avoid confusing the parsers 271 | tcsetattr(obj->fd, TCSANOW, &(obj->newtio)); // Set new terminal attributes 272 | } 273 | 274 | // TODO: report more errors 275 | 276 | if (obj->parser.crc16 && obj->parser.crc16 != crc) { 277 | logmsg(LL_ERROR, "data CRC 0x%x does not match telegram CRC 0x%x\n", crc, obj->parser.crc16); 278 | return -4; 279 | } 280 | 281 | return 0; 282 | } 283 | 284 | 285 | int telegram_parser_open_d0 (telegram_parser *obj, char *infile, size_t bufsize, int timeout, char *dumpfile) 286 | { 287 | // Initialise a parser object for a serial device (or file) associated with an optical IEC 62056-21 "D0" interface 288 | 289 | if (obj == NULL) { 290 | return -1; 291 | } 292 | 293 | int result = telegram_parser_open (obj, infile, bufsize, timeout, dumpfile); 294 | 295 | if (result < 0) 296 | return result; 297 | 298 | obj->newtio.c_cflag = B300 | CS7 | PARENB | CLOCAL | CREAD; // Start at 300 baud, 7-bit characters, even parity, ignore control lines, enable reading 299 | tcsetattr(obj->fd, TCSANOW, &(obj->newtio)); // Set new terminal attributes 300 | 301 | obj->mode = 0; 302 | 303 | return 0; 304 | } 305 | 306 | 307 | int telegram_parser_read_d0 (telegram_parser *obj, int wakeup) 308 | { 309 | 310 | // Attempt to request data from an optical IEC 62056-21 "D0" interface and parse it 311 | 312 | ssize_t len; 313 | unsigned long idx = 0; 314 | 315 | if (obj == NULL) { 316 | return -1; 317 | } 318 | 319 | if (obj->fd <= 0) { 320 | return -2; 321 | } 322 | 323 | if (obj->terminal && obj->mode != 'P') { 324 | 325 | // We need to send a wake-up and sign-on sequence in order to receive a telegram 326 | 327 | int count; 328 | char zero = 0; 329 | 330 | logmsg(LL_VERBOSE, "Setting baud rate to 300 baud\n"); 331 | cfsetspeed(&(obj->newtio), B300); // Update speed in termio-structure 332 | tcsetattr(obj->fd, TCSANOW, &(obj->newtio)); // Set new terminal attributes 333 | 334 | if (wakeup) { 335 | logmsg(LL_VERBOSE, "Sending wake-up sequence\n"); 336 | for (count = 0 ; count < 65 ; count++) { 337 | if (write(obj->fd, &zero, 1) < 0) { 338 | logmsg(LL_WARNING, "Unable to send wake-up sequence: %s\n", strerror(errno)); 339 | break; 340 | } 341 | } 342 | 343 | tcdrain(obj->fd); // Make sure the data in the output buffer is sent 344 | usleep(2700000UL); // Wait 2.7 seconds 345 | } 346 | 347 | tcflush(obj->fd, TCIFLUSH); // Flush any unread data that may still be in the input buffer 348 | 349 | char signonseq[] = "/?!\r\n"; 350 | logmsg(LL_VERBOSE, "Sending sign-on sequence: %s\n", signonseq); 351 | if (write(obj->fd, signonseq, strlen(signonseq)) < strlen(signonseq)) { 352 | logmsg(LL_WARNING, "Unable to send sign-on sequence.\n"); 353 | return -3; 354 | } 355 | tcdrain(obj->fd); // Make sure the data in the output buffer is sent 356 | 357 | // Try to read first character of meter identification string ('/') 358 | 359 | len = read(obj->fd, obj->buffer, 1); 360 | 361 | if (len < 0) { 362 | logmsg(LL_ERROR, "reading meter ID string: %s\n", strerror(errno)); 363 | return -4; 364 | 365 | } else if (len == 0 || obj->buffer[0] != '/') { 366 | logmsg(LL_ERROR, "Did not receive a valid meter ID string.\n"); 367 | return -5; 368 | 369 | } 370 | 371 | // Try to read full meter identification string 372 | 373 | do { 374 | idx += 1; 375 | len = read(obj->fd, obj->buffer + idx, 1); 376 | } while (idx < obj->bufsize && len == 1 && obj->buffer[idx] != '\n'); 377 | 378 | if (idx < obj->bufsize - 1) { 379 | obj->buffer[idx + 1] = '\0'; 380 | logmsg(LL_VERBOSE, "Meter ID string received, %lu bytes: %s\n", idx, obj->buffer); 381 | } 382 | 383 | obj->mode = 0; 384 | speed_t baudrate = B300; 385 | 386 | if (obj->buffer[idx] == '\n' && obj->buffer[idx - 1] == '\r') { 387 | switch(obj->buffer[4]) { // baud rate and mode identifier 388 | case 'A': 389 | obj->mode = 'B'; 390 | case '0': 391 | baudrate = B300; 392 | break; 393 | case 'B': 394 | obj->mode = 'B'; 395 | case '1': 396 | baudrate = B600; 397 | logmsg(LL_VERBOSE, "Upgrading to 600 baud\n"); 398 | break; 399 | case 'C': 400 | obj->mode = 'B'; 401 | case '2': 402 | baudrate = B1200; 403 | logmsg(LL_VERBOSE, "Upgrading to 1200 baud\n"); 404 | break; 405 | case 'D': 406 | obj->mode = 'B'; 407 | case '3': 408 | baudrate = B2400; 409 | logmsg(LL_VERBOSE, "Upgrading to 2400 baud\n"); 410 | break; 411 | case 'E': 412 | obj->mode = 'B'; 413 | case '4': 414 | baudrate = B4800; 415 | logmsg(LL_VERBOSE, "Upgrading to 4800 baud\n"); 416 | break; 417 | case 'F': 418 | obj->mode = 'B'; 419 | case '5': 420 | baudrate = B9600; 421 | logmsg(LL_VERBOSE, "Upgrading to 9600 baud\n"); 422 | break; 423 | case 'G': 424 | obj->mode = 'B'; 425 | case '6': 426 | baudrate = B19200; 427 | logmsg(LL_VERBOSE, "Upgrading to 19200 baud\n"); 428 | break; 429 | default: 430 | if (obj->buffer[4] >= 0x20 && obj->buffer[4] != '/' && obj->buffer[4] != '!' && obj->buffer[4] <= 0x7e) { 431 | obj->mode = 'A'; // Other printable characters are used to indicate mode A 432 | } 433 | } 434 | 435 | // If we're in mode D, we won't really know, and the meter should send a telegram immediately 436 | // following the identifier. 437 | 438 | if (!obj->mode) { 439 | 440 | // We're in either mode C or E, and we should send an ACK to get data 441 | // (We can also be in mode D, in which case it won't really hurt to send the ACK) 442 | 443 | if (obj->buffer[5] == '\\') { 444 | obj->mode = 'E'; 445 | if (obj->buffer[6] == '2') { 446 | logmsg(LL_ERROR, "This parser does not support the IEC 62056-21 binary HDLC protocol.\n"); 447 | return -8; 448 | } 449 | } else { 450 | obj->mode = 'C'; // We can also be in mode D, but we'll assume C 451 | } 452 | 453 | // Send ACK sequence 454 | char ackseq[6] = {0x06, '0', obj->buffer[4], '0', '\r', '\n'}; // The third character in the ACK message is the baud rate ID 455 | logmsg(LL_VERBOSE, "Sending ACK: \\x06 0 %c 0 \\r \\n\n", obj->buffer[4]); 456 | write(obj->fd, ackseq, 6); 457 | tcdrain(obj->fd); 458 | } 459 | 460 | logmsg(LL_VERBOSE, "Meter detected or assumed to use mode %c\n", obj->mode); 461 | 462 | if (obj->mode != 'A') { 463 | 464 | // Change baud rate 465 | 466 | usleep(300000UL); // Wait 300 ms 467 | logmsg(LL_VERBOSE, "Setting baud rate\n"); 468 | cfsetspeed(&(obj->newtio), baudrate); // Update speed in termio-structure 469 | tcsetattr(obj->fd, TCSANOW, &(obj->newtio)); // Set new terminal attributes 470 | } 471 | 472 | } else { 473 | 474 | if (idx < obj->bufsize - 1) 475 | obj->buffer[idx + 1] = '\0'; 476 | else 477 | obj->buffer[idx] = '\0'; 478 | logmsg(LL_ERROR, "Invalid meter ID string: %s", obj->buffer); 479 | return -6; 480 | } 481 | } 482 | 483 | idx += 1; 484 | 485 | if (idx >= obj->bufsize - 1) { 486 | logmsg(LL_ERROR, "Buffer too small to hold telegram\n"); 487 | return -7; 488 | } 489 | 490 | // Attempt to read telegram data 491 | 492 | int telegram = 0; 493 | unsigned long lrc_start = 0, lrc_end = 0; 494 | 495 | do { 496 | // Read next byte 497 | len = read(obj->fd, obj->buffer + idx, 1); 498 | if (len < 0) { 499 | logmsg(LL_ERROR, "reading telegram data: %s\n", strerror(errno)); 500 | } else if (len == 0) { 501 | logmsg(LL_WARNING, "read() returned no bytes when reading telegram data\n"); 502 | } else { 503 | if (obj->buffer[idx] == 0x02) { 504 | logmsg(LL_VERBOSE, "STX found at offset %lu\n", (unsigned long)idx); 505 | lrc_start = idx; // LRC calculation starts after STX 506 | idx--; // We don't store STX, so overwrite it with the next byte 507 | } else if (obj->buffer[idx] == '!') { 508 | logmsg(LL_VERBOSE, "Telegram terminator found at offset %lu\n", (unsigned long)idx); 509 | telegram = 1; 510 | } else if (obj->buffer[idx] == 0x03) { 511 | logmsg(LL_VERBOSE, "ETX found at offset %lu\n", (unsigned long)idx); 512 | lrc_end = idx; // LRC calculation ends at ETX (included) 513 | break; 514 | } else if ((obj->buffer[idx] < 0x20 || obj->buffer[idx] > 0x7e) && obj->buffer[idx] != '\n' && obj->buffer[idx] != '\r') { 515 | logmsg(LL_WARNING, "Non-printable byte (0x%02x) in telegram at index %lu\n", (int)(obj->buffer[idx]), idx); 516 | } 517 | idx++; 518 | } 519 | 520 | } while (len > 0 && idx < obj->bufsize); 521 | 522 | uint8_t lrc_value = 0; 523 | int lrc_error = 0; 524 | uint8_t lrc_check = 0xff; 525 | 526 | if (!telegram) { 527 | logmsg(LL_WARNING, "No full telegram found, received %lu bytes of data\n", idx); 528 | // TODO: in mode C or E we could send a NAK and request a resend 529 | } else { 530 | if (lrc_start && lrc_end) { 531 | // Try to read the BCC block check byte 532 | len = read(obj->fd, &lrc_value, 1); 533 | if (len <= 0) { 534 | logmsg(LL_WARNING, "Unable to read BCC block check character\n"); 535 | } else { 536 | unsigned long lrc_idx; 537 | for (lrc_idx = lrc_start ; lrc_idx <= lrc_end ; lrc_idx++) { 538 | lrc_check ^= obj->buffer[lrc_idx]; // XOR LRC value with next byte 539 | } 540 | lrc_check ^= 0xff; 541 | } 542 | logmsg(LL_VERBOSE, "BCC received is %u, LRC calculated is %u\n", (unsigned int)lrc_value, (unsigned int)lrc_check); 543 | 544 | if (lrc_value != lrc_check) { 545 | logmsg(LL_WARNING, "BCC/LRC check failed, data may be invalid\n"); 546 | lrc_error = 1; 547 | } 548 | } else { 549 | logmsg(LL_WARNING, "LRC block range invalid: %lu - %lu\n", lrc_start, lrc_end); 550 | } 551 | } 552 | 553 | // If a full telegram is received, we should send an ACK and sign off 554 | 555 | if (telegram && obj->terminal && obj->mode != 'P') { 556 | 557 | // TODO: send NAK if LRC is incorrect 558 | 559 | logmsg(LL_VERBOSE, "Sending ACK and signing off\n"); 560 | const char signoffseq[6] = {0x06, 0x01, 'B', '0', 0x03, 'q'}; // 0x06 is ACK, the other bytes are part of a break sequence (complete sign off) 561 | write(obj->fd, signoffseq, 6); 562 | tcdrain(obj->fd); 563 | } 564 | 565 | // We'll try parsing the telegram (even if we receive only a partial one) 566 | 567 | obj->len = idx; 568 | parser_init(&(obj->parser)); 569 | parser_execute(&(obj->parser), (const char *)(obj->buffer), obj->len, 1); 570 | obj->status = parser_finish(&(obj->parser)); // 1 if final state reached, -1 on error, 0 if final state not reached 571 | if (obj->parser.parse_errors) { 572 | logmsg(LL_VERBOSE, "Parse errors: %d\n", obj->parser.parse_errors); 573 | if (obj->dumpfile) { 574 | fwrite(obj->buffer, 1, obj->len, obj->dumpfile); 575 | fflush(obj->dumpfile); 576 | } 577 | } 578 | 579 | if (! obj->data->timestamp) { 580 | // Set current time, if no timestamp is reported by the meter 581 | obj->data->timestamp = time(NULL); 582 | } 583 | 584 | return lrc_error; 585 | } 586 | -------------------------------------------------------------------------------- /p1-lib.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "p1-parser.h" 5 | #include "dsmr-data.h" 6 | 7 | uint16_t crc_telegram (const uint8_t *data, unsigned int length); 8 | size_t read_telegram (int fd, uint8_t *buf, size_t bufsize, size_t maxfailbytes); 9 | 10 | 11 | // Default size of the buffer used to read and store telegrams, determines maximum telegram size 12 | 13 | #define BUFSIZE_TELEGRAM 4096 14 | 15 | // Default timeout for reading from serial devices, in seconds 16 | 17 | #define READ_TIMEOUT 15 18 | 19 | 20 | typedef struct telegram_parser_struct { 21 | 22 | int fd; // Input file descriptor 23 | int timeout; // Time-out for reading serial data, in seconds 24 | FILE *dumpfile; // File descriptor used to write telegrams with parsing errors 25 | int terminal; // Flag to indicate whether input is a terminal or a file 26 | struct termios oldtio, 27 | newtio; // Terminal settings 28 | 29 | int status; // Ragel parser status 30 | struct parser parser; // Ragel state machine structure 31 | 32 | struct dsmr_data_struct *data; // Smart meter data structure 33 | 34 | size_t bufsize; // Telegram buffer size 35 | size_t len; // Telegram length 36 | uint8_t *buffer; // Telegram buffer pointer 37 | 38 | char mode; // Meter mode (A, B, C, D, E for IEC, or P for DSMR P1) 39 | 40 | } telegram_parser; 41 | 42 | 43 | int telegram_parser_open (telegram_parser *obj, char *infile, size_t bufsize, int timeout, char *dumpfile); 44 | void telegram_parser_close (telegram_parser *obj); 45 | int telegram_parser_read (telegram_parser *obj); 46 | 47 | int telegram_parser_open_d0 (telegram_parser *obj, char *infile, size_t bufsize, int timeout, char *dumpfile); 48 | int telegram_parser_read_d0 (telegram_parser *obj, int wakeup); 49 | -------------------------------------------------------------------------------- /p1-parser.h: -------------------------------------------------------------------------------- 1 | /* 2 | File: p1-parser.h 3 | 4 | Prototypes and structs to help parse Dutch Smart Meter P1-telegrams. 5 | 6 | (c)2017, Levien van Zon (levien at zonnetjes.net, https://github.com/lvzon) 7 | */ 8 | 9 | #define _GNU_SOURCE 1 10 | 11 | #include 12 | 13 | // Data structure to hold meter data 14 | 15 | #include "dsmr-data.h" 16 | 17 | // Default meter timezone is CET (The Netherlands and most of mainland Western Europe) 18 | 19 | #define METER_TIMEZONE "CET-1CEST,M3.5.0/2,M10.5.0/3" 20 | 21 | // Parser buffer used to store strings 22 | 23 | #define PARSER_BUFLEN 4096 24 | 25 | // Parser stack length (maximum number of string/int capture-elements per line) 26 | 27 | #define PARSER_MAXARGS 12 28 | 29 | // Data structure used by the Ragel parser 30 | 31 | struct parser 32 | { 33 | int cs; // Variables needed by Ragel parsers/scanners 34 | const char *pe; 35 | char *ts; 36 | char *te; 37 | int act; 38 | 39 | char buffer[PARSER_BUFLEN+1]; // String capture buffers 40 | int buflen; 41 | 42 | int argc; // Integer capture stack 43 | long long arg[PARSER_MAXARGS]; 44 | int multiplier; 45 | int bitcount; 46 | int decimalpos; 47 | 48 | int strargc; // String capture stack 49 | char *strarg[PARSER_MAXARGS]; 50 | 51 | // Variables specific to the P1-parser 52 | 53 | uint16_t crc16; 54 | char *meter_timezone; 55 | int parse_errors, pfaileventcount; 56 | 57 | unsigned int devcount, timeseries_period_minutes; 58 | uint32_t timeseries_time; 59 | 60 | // Data structure to hold meter data 61 | 62 | struct dsmr_data_struct data; 63 | }; 64 | 65 | 66 | // Lookup table for long long integer powers of ten 67 | 68 | # define MAX_DIVIDER_EXP 18 69 | 70 | static const long long pow10[MAX_DIVIDER_EXP + 1] = { 71 | 1, 10, 100, 1000, 10000, 100000L, 1000000L, 10000000L, 100000000L, 1000000000L, 72 | 10000000000LL, 100000000000LL, 1000000000000LL, 10000000000000LL, 100000000000000LL, 73 | 1000000000000000LL, 10000000000000000LL, 100000000000000000LL, 1000000000000000000LL}; 74 | 75 | 76 | // Function prototypes 77 | 78 | void parser_init( struct parser *fsm ); 79 | void parser_execute(struct parser *fsm, const char *data, int len, int eofflag); 80 | int parser_finish(struct parser *fsm); 81 | -------------------------------------------------------------------------------- /p1-parser.rl: -------------------------------------------------------------------------------- 1 | /* 2 | File: p1-parser.rl 3 | 4 | Ragel state-machine definition and supporting functions 5 | to parse Dutch Smart Meter P1-telegrams (and a subset of generic IEC 62056-21 smart meter telegrams). 6 | 7 | (c)2017-2018, Levien van Zon (levien at zonnetjes.net, https://github.com/lvzon) 8 | */ 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include 17 | 18 | #include "logmsg.h" 19 | 20 | #include "p1-parser.h" 21 | 22 | 23 | long long int TST_to_time (struct parser *fsm, int arg_idx) { 24 | 25 | // Get TST timestamp fields from stack and create a UNIX timestamp 26 | // The TST fields are: YYMMDDhhmmssX, with X = W for winter time or X = S for summer time 27 | 28 | struct tm tm; 29 | time_t time; 30 | 31 | tm.tm_year = fsm->arg[arg_idx] + 100; // Years since 1900, our value was years since 2000 32 | tm.tm_mon = fsm->arg[arg_idx + 1] - 1; // Months since start of year, starts at 0 (for January) 33 | tm.tm_mday = fsm->arg[arg_idx + 2]; // Ordinal day of the month 34 | tm.tm_hour = fsm->arg[arg_idx + 3]; // Hours past midnight, starts at 0 35 | tm.tm_min = fsm->arg[arg_idx + 4]; // Minutes past the hour 36 | tm.tm_sec = fsm->arg[arg_idx + 5]; // Seconds past the minute 37 | 38 | if (fsm->arg[arg_idx + 6] == 'S') // Daylight saving time flag 39 | tm.tm_isdst = 1; // Positive for daylight saving time (summer time) 40 | else if (fsm->arg[arg_idx + 6] == 'W') 41 | tm.tm_isdst = 0; // Zero if DST is not in effect (winter time) 42 | else 43 | tm.tm_isdst = -1; // Negative if DST information is not available 44 | 45 | logmsg(LL_DEBUG, "Time: %d %d %d %d %d %d %d\n", tm.tm_year, tm.tm_mon, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, tm.tm_isdst); 46 | 47 | if (!fsm->meter_timezone) 48 | fsm->meter_timezone = METER_TIMEZONE; 49 | 50 | const char *TZ = "TZ"; 51 | char *oldval_TZ = getenv(TZ); 52 | setenv(TZ, fsm->meter_timezone, 1); // Set TZ timezone environment variable to meter timezone 53 | 54 | time = mktime(&tm); 55 | 56 | if (oldval_TZ) 57 | setenv(TZ, oldval_TZ, 1); // Restore TZ timezone environment variable 58 | 59 | return time; 60 | } 61 | 62 | 63 | /* Ragel state-machine definition */ 64 | 65 | %%{ 66 | machine parser; 67 | access fsm->; 68 | 69 | include parser "parser-tools.rl"; 70 | 71 | rest_of_line := [^\r^\n]* [\r\n]{2} @clearargs @{ fgoto main; }; # Helper machine to parse the rest of the line in case of errors 72 | 73 | # Actions associated with commands 74 | 75 | action header { 76 | logmsg(LL_VERBOSE, "Header: %s\n", fsm->strarg[0]); 77 | strncpy((char *)(fsm->data.header), fsm->strarg[0], LEN_HEADER); 78 | } 79 | 80 | action crc { 81 | if (fsm->arg[0]) { 82 | logmsg(LL_VERBOSE, "CRC: 0x%x\n", (unsigned int)fsm->arg[0]); 83 | } 84 | fsm->crc16 = fsm->arg[0]; 85 | } 86 | 87 | action P1_version { 88 | fsm->data.P1_version_major = fsm->arg[0] >> 4; 89 | fsm->data.P1_version_minor = fsm->arg[0] & 0xf; 90 | logmsg(LL_VERBOSE, "P1 version: %d.%d\n", (int)(fsm->data.P1_version_major), (int)(fsm->data.P1_version_minor)); 91 | } 92 | 93 | action timestamp { 94 | fsm->data.timestamp = TST_to_time(fsm, 0); 95 | logmsg(LL_VERBOSE, "Timestamp: %lu\n", (unsigned long)(fsm->data.timestamp)); 96 | } 97 | 98 | action equipment_id { 99 | logmsg(LL_VERBOSE, "Equipment ID: %s\n", fsm->strarg[0]); 100 | strncpy((char *)(fsm->data.equipment_id), fsm->strarg[0], LEN_EQUIPMENT_ID); 101 | } 102 | 103 | action tariff { 104 | fsm->data.tariff = fsm->arg[0]; 105 | logmsg(LL_VERBOSE, "Tariff: %u\n", (unsigned int)(fsm->data.tariff)); 106 | } 107 | 108 | action switchpos { 109 | fsm->data.switchpos = fsm->arg[0]; 110 | logmsg(LL_VERBOSE, "Switch position: %d\n", (int)(fsm->data.switchpos)); 111 | } 112 | 113 | action E_in { 114 | unsigned int tariff = fsm->arg[0]; 115 | double value = (double)fsm->arg[1] / (double)fsm->arg[2]; 116 | if (tariff > MAX_TARIFFS) { 117 | logmsg(LL_ERROR, "Tariff %u out of range, max. %u, E_in %f %s\n", tariff, MAX_TARIFFS, value, fsm->strarg[0]); 118 | } else { 119 | fsm->data.E_in[tariff] = value; 120 | strncpy((char *)(fsm->data.unit_E_in[tariff]), fsm->strarg[0], LEN_UNIT + 1); 121 | logmsg(LL_VERBOSE, "Energy in, tariff %u: %f %s\n", tariff, value, fsm->strarg[0]); 122 | } 123 | } 124 | 125 | action E_out { 126 | unsigned int tariff = fsm->arg[0]; 127 | double value = (double)fsm->arg[1] / (double)fsm->arg[2]; 128 | if (tariff > MAX_TARIFFS) { 129 | logmsg(LL_ERROR, "Tariff %u out of range, max. %u, E_out %f %s\n", tariff, MAX_TARIFFS, value, fsm->strarg[0]); 130 | } else { 131 | fsm->data.E_out[tariff] = value; 132 | strncpy((char *)(fsm->data.unit_E_out[tariff]), fsm->strarg[0], LEN_UNIT + 1); 133 | logmsg(LL_VERBOSE, "Energy out, tariff %u: %f %s\n", tariff, value, fsm->strarg[0]); 134 | } 135 | } 136 | 137 | # Deprecated hard-coded tariffs, to be removed 138 | action E_in_t1 { logmsg(LL_VERBOSE, "Energy in, tariff 1: %f %s\n", (double)fsm->arg[0] / (double)fsm->arg[1], fsm->strarg[0]); } 139 | action E_in_t2 { logmsg(LL_VERBOSE, "Energy in, tariff 2: %f %s\n", (double)fsm->arg[0] / (double)fsm->arg[1], fsm->strarg[0]); } 140 | action E_out_t1 { logmsg(LL_VERBOSE, "Energy out, tariff 1: %f %s\n", (double)fsm->arg[0] / (double)fsm->arg[1], fsm->strarg[0]); } 141 | action E_out_t2 { logmsg(LL_VERBOSE, "Energy out, tariff 2: %f %s\n", (double)fsm->arg[0] / (double)fsm->arg[1], fsm->strarg[0]); } 142 | 143 | action P_in { 144 | fsm->data.P_in_total = (double)fsm->arg[0] / (double)fsm->arg[1]; 145 | strncpy((char *)(fsm->data.unit_P_in_total), fsm->strarg[0], LEN_UNIT + 1); 146 | logmsg(LL_VERBOSE, "Power in: %f %s\n", fsm->data.P_in_total, fsm->strarg[0]); 147 | } 148 | 149 | action P_out { 150 | fsm->data.P_out_total = (double)fsm->arg[0] / (double)fsm->arg[1]; 151 | strncpy((char *)(fsm->data.unit_P_out_total), fsm->strarg[0], LEN_UNIT + 1); 152 | logmsg(LL_VERBOSE, "Power out: %f %s\n", fsm->data.P_out_total, fsm->strarg[0]); 153 | } 154 | 155 | action P_threshold { 156 | fsm->data.P_threshold = (double)fsm->arg[0] / (double)fsm->arg[1]; 157 | strncpy((char *)(fsm->data.unit_P_threshold), fsm->strarg[0], LEN_UNIT + 1); 158 | logmsg(LL_VERBOSE, "Power threshold: %f %s\n", fsm->data.P_threshold, fsm->strarg[0]); 159 | } 160 | 161 | action I_L1 { 162 | if (MAX_PHASES >= 1) { 163 | fsm->data.I[0] = (double)fsm->arg[0] / (double)fsm->arg[1]; 164 | strncpy((char *)(fsm->data.unit_I[0]), fsm->strarg[0], LEN_UNIT + 1); 165 | logmsg(LL_VERBOSE, "Current L1: %f %s\n", fsm->data.I[0], fsm->strarg[0]); 166 | } 167 | } 168 | 169 | action I_L2 { 170 | if (MAX_PHASES >= 2) { 171 | fsm->data.I[1] = (double)fsm->arg[0] / (double)fsm->arg[1]; 172 | strncpy((char *)(fsm->data.unit_I[1]), fsm->strarg[0], LEN_UNIT + 1); 173 | logmsg(LL_VERBOSE, "Current L2: %f %s\n", fsm->data.I[1], fsm->strarg[0]); 174 | } 175 | } 176 | 177 | action I_L3 { 178 | if (MAX_PHASES >= 3) { 179 | fsm->data.I[2] = (double)fsm->arg[0] / (double)fsm->arg[1]; 180 | strncpy((char *)(fsm->data.unit_I[2]), fsm->strarg[0], LEN_UNIT + 1); 181 | logmsg(LL_VERBOSE, "Current L3: %f %s\n", fsm->data.I[2], fsm->strarg[0]); 182 | } 183 | } 184 | 185 | action V_L1 { 186 | if (MAX_PHASES >= 1) { 187 | fsm->data.V[0] = (double)fsm->arg[0] / (double)fsm->arg[1]; 188 | strncpy((char *)(fsm->data.unit_V[0]), fsm->strarg[0], LEN_UNIT + 1); 189 | logmsg(LL_VERBOSE, "Voltage L1: %f %s\n", fsm->data.V[0], fsm->strarg[0]); 190 | } 191 | } 192 | 193 | action V_L2 { 194 | if (MAX_PHASES >= 2) { 195 | fsm->data.V[1] = (double)fsm->arg[0] / (double)fsm->arg[1]; 196 | strncpy((char *)(fsm->data.unit_V[1]), fsm->strarg[0], LEN_UNIT + 1); 197 | logmsg(LL_VERBOSE, "Voltage L2: %f %s\n", fsm->data.V[1], fsm->strarg[0]); 198 | } 199 | } 200 | 201 | action V_L3 { 202 | if (MAX_PHASES >= 3) { 203 | fsm->data.V[2] = (double)fsm->arg[0] / (double)fsm->arg[1]; 204 | strncpy((char *)(fsm->data.unit_V[2]), fsm->strarg[0], LEN_UNIT + 1); 205 | logmsg(LL_VERBOSE, "Voltage L3: %f %s\n", fsm->data.V[2], fsm->strarg[0]); 206 | } 207 | } 208 | 209 | action P_in_L1 { 210 | if (MAX_PHASES >= 1) { 211 | fsm->data.P_in[0] = (double)fsm->arg[0] / (double)fsm->arg[1]; 212 | strncpy((char *)(fsm->data.unit_P_in[0]), fsm->strarg[0], LEN_UNIT + 1); 213 | logmsg(LL_VERBOSE, "Power in L1: %f %s\n", fsm->data.P_in[0], fsm->strarg[0]); 214 | } 215 | } 216 | 217 | action P_in_L2 { 218 | if (MAX_PHASES >= 2) { 219 | fsm->data.P_in[1] = (double)fsm->arg[0] / (double)fsm->arg[1]; 220 | strncpy((char *)(fsm->data.unit_P_in[1]), fsm->strarg[0], LEN_UNIT + 1); 221 | logmsg(LL_VERBOSE, "Power in L2: %f %s\n", fsm->data.P_in[1], fsm->strarg[0]); 222 | } 223 | } 224 | 225 | action P_in_L3 { 226 | if (MAX_PHASES >= 3) { 227 | fsm->data.P_in[2] = (double)fsm->arg[0] / (double)fsm->arg[1]; 228 | strncpy((char *)(fsm->data.unit_P_in[2]), fsm->strarg[0], LEN_UNIT + 1); 229 | logmsg(LL_VERBOSE, "Power in L3: %f %s\n", fsm->data.P_in[2], fsm->strarg[0]); 230 | } 231 | } 232 | 233 | action P_out_L1 { 234 | if (MAX_PHASES >= 1) { 235 | fsm->data.P_out[0] = (double)fsm->arg[0] / (double)fsm->arg[1]; 236 | strncpy((char *)(fsm->data.unit_P_out[0]), fsm->strarg[0], LEN_UNIT + 1); 237 | logmsg(LL_VERBOSE, "Power out L1: %f %s\n", fsm->data.P_out[0], fsm->strarg[0]); 238 | } 239 | } 240 | 241 | action P_out_L2 { 242 | if (MAX_PHASES >= 2) { 243 | fsm->data.P_out[1] = (double)fsm->arg[0] / (double)fsm->arg[1]; 244 | strncpy((char *)(fsm->data.unit_P_out[1]), fsm->strarg[0], LEN_UNIT + 1); 245 | logmsg(LL_VERBOSE, "Power out L2: %f %s\n", fsm->data.P_out[1], fsm->strarg[0]); 246 | } 247 | } 248 | 249 | action P_out_L3 { 250 | if (MAX_PHASES >= 3) { 251 | fsm->data.P_out[2] = (double)fsm->arg[0] / (double)fsm->arg[1]; 252 | strncpy((char *)(fsm->data.unit_P_out[2]), fsm->strarg[0], LEN_UNIT + 1); 253 | logmsg(LL_VERBOSE, "Power out L3: %f %s\n", fsm->data.P_out[2], fsm->strarg[0]); 254 | } 255 | } 256 | 257 | action pfail { 258 | fsm->data.power_failures = fsm->arg[0]; 259 | logmsg(LL_VERBOSE, "Power failures: %lu\n", (unsigned long)(fsm->data.power_failures)); 260 | } 261 | 262 | action longpfail { 263 | fsm->data.power_failures_long = fsm->arg[0]; 264 | logmsg(LL_VERBOSE, "Long power failures: %lu\n", (unsigned long)(fsm->data.power_failures_long)); 265 | } 266 | 267 | action pfailevents { 268 | fsm->data.pfail_events = fsm->arg[0]; 269 | fsm->pfaileventcount = 0; 270 | logmsg(LL_VERBOSE, "Power failure events: %u\n", (unsigned int)(fsm->data.pfail_events)); 271 | } 272 | 273 | action pfailevent { 274 | uint32_t timestamp = TST_to_time(fsm, 0); 275 | uint32_t duration = fsm->arg[7]; 276 | logmsg(LL_VERBOSE, "Power failure event end time %lu, %lu %s\n", (unsigned long)timestamp, (unsigned long)duration, fsm->strarg[0]); 277 | if (fsm->pfaileventcount < MAX_EVENTS) { 278 | fsm->data.pfail_event_end_time[fsm->pfaileventcount] = timestamp; 279 | fsm->data.pfail_event_duration[fsm->pfaileventcount] = duration; 280 | strncpy((char *)(fsm->data.unit_pfail_event_duration[fsm->pfaileventcount]), fsm->strarg[0], LEN_UNIT + 1); 281 | } else { 282 | logmsg(LL_ERROR, "Power failure event overflow, count %d, max %d\n", fsm->pfaileventcount, MAX_EVENTS); 283 | } 284 | fsm->pfaileventcount++; 285 | } 286 | 287 | action V_sags_L1 { 288 | if (MAX_PHASES >= 1) { 289 | fsm->data.V_sags[0] = fsm->arg[0]; 290 | logmsg(LL_VERBOSE, "Voltage sags L1: %lu\n", (unsigned long)(fsm->data.V_sags[0])); 291 | } 292 | } 293 | 294 | action V_sags_L2 { 295 | if (MAX_PHASES >= 2) { 296 | fsm->data.V_sags[1] = fsm->arg[0]; 297 | logmsg(LL_VERBOSE, "Voltage sags L2: %lu\n", (unsigned long)(fsm->data.V_sags[1])); 298 | } 299 | } 300 | 301 | action V_sags_L3 { 302 | if (MAX_PHASES >= 3) { 303 | fsm->data.V_sags[2] = fsm->arg[0]; 304 | logmsg(LL_VERBOSE, "Voltage sags L3: %lu\n", (unsigned long)(fsm->data.V_sags[2])); 305 | } 306 | } 307 | 308 | action V_swells_L1 { 309 | if (MAX_PHASES >= 1) { 310 | fsm->data.V_swells[0] = fsm->arg[0]; 311 | logmsg(LL_VERBOSE, "Voltage swells L1: %lu\n", (unsigned long)(fsm->data.V_swells[0])); 312 | } 313 | } 314 | 315 | action V_swells_L2 { 316 | if (MAX_PHASES >= 2) { 317 | fsm->data.V_swells[1] = fsm->arg[0]; 318 | logmsg(LL_VERBOSE, "Voltage swells L2: %lu\n", (unsigned long)(fsm->data.V_swells[1])); 319 | } 320 | } 321 | 322 | action V_swells_L3 { 323 | if (MAX_PHASES >= 3) { 324 | fsm->data.V_swells[2] = fsm->arg[0]; 325 | logmsg(LL_VERBOSE, "Voltage swells L3: %lu\n", (unsigned long)(fsm->data.V_swells[2])); 326 | } 327 | } 328 | 329 | action textmsgcodes { 330 | logmsg(LL_VERBOSE, "Text message codes: %s\n", fsm->strarg[0]); 331 | strncpy((char *)(fsm->data.textmsg_codes), fsm->strarg[0], LEN_MESSAGE_CODES + 1); 332 | } 333 | 334 | action textmsg { 335 | logmsg(LL_VERBOSE, "Text message: %s\n", fsm->strarg[0]); 336 | strncpy((char *)(fsm->data.textmsg), fsm->strarg[0], LEN_MESSAGE + 1); 337 | } 338 | 339 | action dev_type { 340 | unsigned int dev = fsm->arg[0] - 1; 341 | unsigned int type = fsm->arg[1]; 342 | if (dev < MAX_DEVS) { 343 | logmsg(LL_VERBOSE, "Device %u type: %u\n", dev + 1, type); 344 | fsm->data.dev_type[dev] = type; 345 | } else { 346 | logmsg(LL_ERROR, "Device ID %u out of range, max %u, type %u\n", dev + 1, MAX_DEVS, type); 347 | } 348 | } 349 | 350 | action dev_id { 351 | unsigned int dev = fsm->arg[0] - 1; 352 | if (dev < MAX_DEVS) { 353 | logmsg(LL_VERBOSE, "Device %u ID: %s\n", dev + 1, fsm->strarg[0]); 354 | strncpy((char *)(fsm->data.dev_id[dev]), fsm->strarg[0], LEN_EQUIPMENT_ID); 355 | } else { 356 | logmsg(LL_ERROR, "Device ID %u out of range, max %u, ID %s\n", dev + 1, MAX_DEVS, fsm->strarg[0]); 357 | } 358 | } 359 | 360 | action dev_valve { 361 | unsigned int dev = fsm->arg[0] - 1; 362 | unsigned int valve = fsm->arg[1]; 363 | if (dev < MAX_DEVS) { 364 | logmsg(LL_VERBOSE, "Device %u valve position: %u\n", dev + 1, valve); 365 | fsm->data.dev_valve[dev] = valve; 366 | } else { 367 | logmsg(LL_ERROR, "Device ID %u out of range, max %u, valve position %u\n", dev + 1, MAX_DEVS, valve); 368 | } 369 | } 370 | 371 | action dev_counter { 372 | unsigned int dev = fsm->arg[0] - 1; 373 | uint32_t timestamp = TST_to_time(fsm, 1); 374 | double value = (double)fsm->arg[8] / (double)fsm->arg[9]; 375 | if (dev < MAX_DEVS) { 376 | logmsg(LL_VERBOSE, "Device %u counter at %lu: %f %s\n", dev + 1, (unsigned long)timestamp, value, fsm->strarg[0]); 377 | fsm->data.dev_counter[dev] = value; 378 | fsm->data.dev_counter_timestamp[dev] = timestamp; 379 | strncpy((char *)(fsm->data.unit_dev_counter[dev]), fsm->strarg[0], LEN_UNIT + 1); 380 | } else { 381 | logmsg(LL_ERROR, "Device ID %u out of range, max %u, counter at %lu: %f %s\n", dev + 1, MAX_DEVS, (unsigned long)timestamp, value, fsm->strarg[0]); 382 | } 383 | } 384 | 385 | 386 | # The dev_timeseries actions provide partial support for the 387 | # "profile generic dataset" used to store counter values of 388 | # other devices in DSMR 3.x. 389 | 390 | action dev_timeseries_head { 391 | 392 | unsigned int dev = fsm->arg[0]; 393 | uint32_t timestamp = TST_to_time(fsm, 1); 394 | int status = fsm->arg[7]; 395 | unsigned int period = fsm->arg[8]; // Recording period in minutes 396 | unsigned int values = fsm->arg[9]; 397 | logmsg(LL_VERBOSE, "Device %u timeseries, starting time %lu, status %d, period %u, values %u:\n", dev, (unsigned long)timestamp, status, period, values); 398 | fsm->devcount = dev - 1; 399 | fsm->timeseries_time = timestamp; 400 | fsm->timeseries_period_minutes = period; 401 | } 402 | 403 | action dev_timeseries_counter_head { 404 | unsigned int dev = fsm->devcount; 405 | logmsg(LL_VERBOSE, "counter values, unit %s\n", fsm->strarg[0]); 406 | if (dev < MAX_DEVS) { 407 | strncpy((char *)(fsm->data.unit_dev_counter[dev]), fsm->strarg[0], LEN_UNIT + 1); 408 | } 409 | } 410 | 411 | action dev_timeseries_counter_cold_head { 412 | unsigned int dev = fsm->devcount; 413 | logmsg(LL_VERBOSE, "cold counter values, unit %s\n", fsm->strarg[0]); 414 | if (dev < MAX_DEVS) { 415 | strncpy((char *)(fsm->data.unit_dev_counter[dev]), fsm->strarg[0], LEN_UNIT + 1); 416 | } 417 | } 418 | 419 | action dev_timeseries_counterval { 420 | 421 | // Note that at this point we only store a single value of the timeseries, 422 | // which will end up being the most recent one... 423 | 424 | unsigned int dev = fsm->devcount; 425 | double value = (double)fsm->arg[0] / (double)fsm->arg[1]; 426 | logmsg(LL_VERBOSE, "counter value: %f\n", value); 427 | if (dev < MAX_DEVS) { 428 | fsm->data.dev_counter[dev] = value; 429 | fsm->data.dev_counter_timestamp[dev] = fsm->timeseries_time; 430 | } 431 | fsm->timeseries_time += (fsm->timeseries_period_minutes * 60); 432 | } 433 | 434 | 435 | # These actions support the old-style gas meter readings in DSMR 2.x 436 | # We will use a fixed device ID for these... 437 | 438 | action gas_id_old { 439 | logmsg(LL_VERBOSE, "Gas meter ID: %s\n", fsm->strarg[0]); 440 | fsm->data.dev_type[0] = 3; // Gas meter 441 | strncpy((char *)(fsm->data.dev_id[0]), fsm->strarg[0], LEN_EQUIPMENT_ID); 442 | } 443 | 444 | action gas_count_old { 445 | unsigned int dev = 0; 446 | double value = (double)fsm->arg[0] / (double)fsm->arg[1]; 447 | logmsg(LL_VERBOSE, "Gas meter counter: %f %s\n", value, fsm->strarg[0]); 448 | fsm->data.dev_counter[dev] = value; 449 | fsm->data.dev_counter_timestamp[dev] = fsm->data.timestamp; 450 | strncpy((char *)(fsm->data.unit_dev_counter[dev]), fsm->strarg[0], LEN_UNIT + 1); 451 | } 452 | 453 | action gas_valve_old { 454 | fsm->data.dev_valve[0] = fsm->arg[0]; 455 | logmsg(LL_VERBOSE, "Gas meter valve position: %d\n", (int)(fsm->data.dev_valve[0])); 456 | } 457 | 458 | action error {logmsg(LL_VERBOSE, "Error while parsing\n"); fsm->parse_errors++ ; fhold ; fgoto rest_of_line; } 459 | 460 | action unknown { logmsg(LL_VERBOSE, "Unknown: %s\n", fsm->strarg[0]); fsm->strargc = 0 ; fsm->buflen = 0; } 461 | 462 | # Helpers that collect arguments 463 | 464 | digitpair = (digit @add_digit){2} >cleararg %addarg; # Parse and store two digits 465 | dst = ([SW] >cleararg @addchar); # Parse and store daylight saving time character 466 | 467 | mbusid = ( [1234] @add_digit ) >cleararg %addarg; # We can have 4 additional MBUS devices: gas meter, water meter, thermal meter, slave meter 468 | 469 | 470 | # Definitions of statements and parts of statements 471 | 472 | crlf = '\r\n'; # Lines are terminated by carriage return + line feed 473 | #crlf = [\r\n]{2}; # Lines are terminated by carriage return + line feed, but we'll also match some converted line ends 474 | 475 | fixedpoint = fpval; # Fixed point value, stored as an integer value and an integer divider 476 | 477 | TST = digitpair{6} dst; 478 | TST_old = digitpair{6}; 479 | unit = [* ] ([^)]+ >addstr $str_append %str_term); # Formally only '*' is a valid unit separator, but some meters use a space (and thus put the unit in the value string) 480 | timeseries_unit = [^)]+ >addstr $str_append %str_term; 481 | 482 | billing_period = ('*' digit+)?; # The billing period specifier is part of IEC 62056-21, but currently isn't present in DSMR telegrams, so we make it optional 483 | 484 | headerstr = ([^\r^\n]+ >addstr $str_append %str_term); 485 | msgstr = hexstring; 486 | idstr = hexstring; 487 | 488 | header = '/' headerstr crlf crlf @header @clearargs; 489 | end = '!' hexint? crlf @crc @clearargs; # Telegram end with optional CRC 490 | 491 | strval = ([^)!]+ >addstr $str_append %str_term); 492 | fixedpointval = '(' fixedpoint unit ')'; # The value can be either integer or non-integer 493 | tstval = '(' TST ')'; 494 | tstval_old = '(' TST_old ')'; 495 | 496 | # COSEM-objects supported by DSMR 497 | 498 | P1_version = '1-3:0.2.8(' hexint ')' crlf @P1_version; # P1 version 499 | timestamp = '0-0:1.0.0' tstval crlf @timestamp; # Telegram timestamp 500 | 501 | equipment_id_p1 = '0-0:96.1.1' billing_period '(' idstr ')' crlf @equipment_id; # Equipment ID in P1-meters 502 | equipment_id_iec = digit '-0:0.0.0' billing_period '(' strval ')' crlf @equipment_id; # Equipment ID in IEC 62056-21 meters 503 | 504 | E_in = '1-0:1.8.' uinteger billing_period fixedpointval crlf @E_in; # Electricity delivered to client 505 | E_out = '1-0:2.8.' uinteger billing_period fixedpointval crlf @E_out; # Electricity delivered by client 506 | 507 | # Deprecated hard-coded tariffs, to be removed 508 | E_in_t1 = '1-0:1.8.1' billing_period fixedpointval crlf @E_in_t1; # Electricity delivered to client in tariff 1 509 | E_in_t2 = '1-0:1.8.2' billing_period fixedpointval crlf @E_in_t2; # Electricity delivered to client in tariff 2 510 | E_out_t1 = '1-0:2.8.1' billing_period fixedpointval crlf @E_out_t1; # Electricity delivered by client in tariff 1 511 | E_out_t2 = '1-0:2.8.2' billing_period fixedpointval crlf @E_out_t2; # Electricity delivered by client in tariff 2 512 | 513 | tariff = '0-0:96.14.0(' uinteger ')' crlf @tariff; # TODO: can be non-integer, in theory? 514 | switchpos = '0-0:' ('96.3.10' | '24.4.0') '(' uinteger ')' crlf @switchpos; # Switch position electricity (in/out/enabled), absent from DSMR>=4.0.7 515 | 516 | P_in = '1-0:1.7.0' fixedpointval crlf @P_in; # Actual power delivered to client 517 | P_out = '1-0:2.7.0' fixedpointval crlf @P_out; # Actual power delivered by client 518 | P_threshold = '0-0:17.0.0' fixedpointval crlf @P_threshold; 519 | 520 | pfail = '0-0:96.7.21(' uinteger ')' crlf @pfail; 521 | longpfail = '0-0:96.7.9(' uinteger ')' crlf @longpfail; 522 | 523 | pfailevents = '1-0:99.97.0(' uinteger ')' @pfailevents @clearargs; # Power failure events 524 | pfailevent = tstval '(' uinteger unit ')' @pfailevent @clearargs; # Single power failure event 525 | pfaileventlog = pfailevents '(0-0:96.7.19)'? pfailevent* crlf; # Power failure event log, with zero or more events 526 | 527 | V_sags_L1 = '1-0:32.32.0(' uinteger ')' crlf @V_sags_L1; 528 | V_sags_L2 = '1-0:52.32.0(' uinteger ')' crlf @V_sags_L2; 529 | V_sags_L3 = '1-0:72.32.0(' uinteger ')' crlf @V_sags_L3; 530 | 531 | V_swells_L1 = '1-0:32.36.0(' uinteger ')' crlf @V_swells_L1; 532 | V_swells_L2 = '1-0:52.36.0(' uinteger ')' crlf @V_swells_L2; 533 | V_swells_L3 = '1-0:72.36.0(' uinteger ')' crlf @V_swells_L3; 534 | 535 | textmsgcodes = '0-0:96.13.1(' msgstr ')' crlf @textmsgcodes; 536 | textmsgcodes_empty = '0-0:96.13.1()' crlf; 537 | textmsg = '0-0:96.13.0(' msgstr ')' crlf @textmsg; 538 | textmsg_empty = '0-0:96.13.0()' crlf; 539 | 540 | I_L1 = '1-0:31.7.0' fixedpointval crlf @I_L1; 541 | I_L2 = '1-0:51.7.0' fixedpointval crlf @I_L2; 542 | I_L3 = '1-0:71.7.0' fixedpointval crlf @I_L3; 543 | 544 | V_L1 = '1-0:32.7.0' fixedpointval crlf @V_L1; 545 | V_L2 = '1-0:52.7.0' fixedpointval crlf @V_L2; 546 | V_L3 = '1-0:72.7.0' fixedpointval crlf @V_L3; 547 | 548 | 549 | P_in_L1 = '1-0:21.7.0' fixedpointval crlf @P_in_L1; 550 | P_in_L2 = '1-0:41.7.0' fixedpointval crlf @P_in_L2; 551 | P_in_L3 = '1-0:61.7.0' fixedpointval crlf @P_in_L3; 552 | 553 | P_out_L1 = '1-0:22.7.0' fixedpointval crlf @P_out_L1; 554 | P_out_L2 = '1-0:42.7.0' fixedpointval crlf @P_out_L2; 555 | P_out_L3 = '1-0:62.7.0' fixedpointval crlf @P_out_L3; 556 | 557 | dev_type = '0-' mbusid ':24.1.0(' uinteger ')' crlf @dev_type; 558 | dev_id = '0-' mbusid ':96.1.0(' idstr ')' crlf @dev_id; 559 | dev_counter = '0-' mbusid ':24.2.1' tstval fixedpointval crlf @dev_counter; 560 | dev_valve = '0-' mbusid ':24.4.0(' uinteger ')' crlf @dev_valve; # Valve position (on/off/released), absent from DSMR>=4.0.7 561 | 562 | # This describes the rather horrible "profile generic dataset" representation used in DSMR 3.x 563 | dev_timeseries_head = '0-' mbusid ':24.3.0' tstval_old '(' uinteger ')' '(' uinteger ')' '(' uinteger ')' @dev_timeseries_head @clearargs; 564 | dev_timeseries_counter_head = '(0-' digit+ ':24.2.1)(' timeseries_unit ')' @dev_timeseries_counter_head @clearargs; 565 | dev_timeseries_counter_cold_head = '(0-' digit+ ':24.3.1)(' timeseries_unit ')' @dev_timeseries_counter_cold_head @clearargs; 566 | dev_timeseries_counterval = '(' fixedpoint ')' @dev_timeseries_counterval @clearargs; 567 | dev_counter_timeseries = dev_timeseries_head dev_timeseries_counter_head dev_timeseries_counterval+ crlf; 568 | dev_counter_cold_timeseries = dev_timeseries_head dev_timeseries_counter_cold_head dev_timeseries_counterval+ crlf; 569 | 570 | gas_id_old = '7-0:0.0.0(' idstr ')' crlf @gas_id_old; 571 | gas_count_old = '7-0:23.1.0' tstval fixedpointval crlf @gas_count_old; 572 | #gas_count_Tcomp_old = '7-0:23.2.0' tstval fixedpointval crlf @gas_count_Tcomp_old; 573 | #gas_valve_old = '7-0:24.4.0(' uinteger ')' crlf @gas_valve_old; 574 | #heat_id_old = '5-0:0.0.0(' idstr ')' crlf @heat_id_old; 575 | #cold_id_old = '6-0:0.0.0(' idstr ')' crlf @cold_id_old; 576 | #water_id_old = '8-0:0.0.0(' idstr ')' crlf @water_id_old; 577 | #heat_count_old = '5-0:1.0.0' tstval fixedpointval crlf @heat_count_old; 578 | #cold_count_old = '6-0:1.0.0' tstval fixedpointval crlf @cold_count_old; 579 | #water_count_old = '8-0:1.0.0' tstval fixedpointval crlf @water_count_old; 580 | 581 | # "Telegram" message components 582 | 583 | equipment_id = equipment_id_p1 | equipment_id_iec; 584 | metadata_object = P1_version | timestamp; 585 | emeter_object = equipment_id | tariff | switchpos | E_in | E_out ; 586 | power_object = P_in | P_out | P_in_L1 | P_out_L1 | P_in_L2 | P_out_L2 | P_in_L3 | P_out_L3 | P_threshold; 587 | current_object = I_L1 | I_L2 | I_L3; 588 | voltage_object = V_L1 | V_L2 | V_L3; 589 | power_quality_object = pfail | longpfail | pfaileventlog | V_sags_L1 | V_swells_L1 | V_sags_L2 | V_swells_L2 | V_sags_L3 | V_swells_L3; 590 | mbusdev_object = dev_type | dev_id | dev_counter | dev_valve | dev_counter_timeseries | dev_counter_cold_timeseries; 591 | slavedev_legacy_object = gas_id_old | gas_count_old; 592 | message_object = textmsgcodes | textmsg | textmsgcodes_empty | textmsg_empty; 593 | 594 | object = metadata_object | emeter_object | power_object | current_object | voltage_object | power_quality_object | 595 | message_object | mbusdev_object | slavedev_legacy_object; 596 | 597 | line = object $err(error) @clearargs; # Clear argument stacks at the end of each line, handle parsing errors 598 | 599 | telegram = header? line* end; # Make header optional, so we can recover from errors in the middle of a telegram 600 | 601 | main := telegram* $err(error); # Parse zero or more telegrams 602 | 603 | }%% 604 | 605 | %% write data; 606 | 607 | 608 | void parser_init( struct parser *fsm ) 609 | { 610 | int arg; 611 | 612 | fsm->buflen = 0; 613 | fsm->argc = 0; 614 | for (arg = 0 ; arg < PARSER_MAXARGS ; arg++) 615 | fsm->arg[arg] = 0; 616 | fsm->multiplier = 1; 617 | fsm->bitcount = 0; 618 | fsm->strargc = 0; 619 | for (arg = 0 ; arg < PARSER_MAXARGS ; arg++) 620 | fsm->strarg[arg] = NULL; 621 | fsm->parse_errors = 0; 622 | fsm->meter_timezone = NULL; 623 | 624 | %% write init; 625 | } 626 | 627 | void parser_execute(struct parser *fsm, const char *data, int len, int eofflag) 628 | { 629 | const char *p = data; 630 | const char *pe = data + len; 631 | const char *eof = 0; 632 | 633 | if (eofflag) 634 | eof = pe; 635 | 636 | %% write exec; 637 | 638 | fsm->pe = pe; 639 | } 640 | 641 | int parser_finish(struct parser *fsm) 642 | { 643 | if ( fsm->cs == parser_error ) // Machine failed before matching 644 | return -1; 645 | if ( fsm->cs >= parser_first_final ) // Final state reached 646 | return 1; 647 | return 0; // Final state not reached 648 | } 649 | 650 | -------------------------------------------------------------------------------- /p1-test-d0.c: -------------------------------------------------------------------------------- 1 | #include "logmsg.h" 2 | 3 | #include "p1-lib.h" 4 | 5 | 6 | int main (int argc, char **argv) 7 | { 8 | 9 | init_msglogger(); 10 | logger.loglevel = LL_VERBOSE; 11 | 12 | char *infile, *dumpfile; 13 | 14 | if (argc < 2) { 15 | logmsg(LL_NORMAL, "Usage: %s []\n", argv[0]); 16 | exit(1); 17 | } 18 | 19 | infile = argv[1]; 20 | dumpfile = NULL; 21 | 22 | if (argc >= 3) 23 | dumpfile = argv[2]; 24 | 25 | telegram_parser parser; 26 | 27 | telegram_parser_open_d0(&parser, infile, 0, 0, dumpfile); 28 | 29 | do { 30 | 31 | telegram_parser_read_d0(&parser, 1); 32 | // TODO: figure out how to handle errors, time-outs, etc. 33 | 34 | } while (parser.terminal); // If we're connected to a serial device, keep reading, otherwise exit 35 | 36 | telegram_parser_close(&parser); 37 | 38 | return 0; 39 | } 40 | 41 | -------------------------------------------------------------------------------- /p1-test.c: -------------------------------------------------------------------------------- 1 | #include "logmsg.h" 2 | 3 | #include "p1-lib.h" 4 | 5 | 6 | int main (int argc, char **argv) 7 | { 8 | 9 | init_msglogger(); 10 | logger.loglevel = LL_VERBOSE; 11 | 12 | char *infile, *dumpfile; 13 | 14 | if (argc < 2) { 15 | logmsg(LL_NORMAL, "Usage: %s []\n", argv[0]); 16 | exit(1); 17 | } 18 | 19 | infile = argv[1]; 20 | dumpfile = NULL; 21 | 22 | if (argc >= 3) 23 | dumpfile = argv[2]; 24 | 25 | telegram_parser parser; 26 | 27 | telegram_parser_open(&parser, infile, 0, 0, dumpfile); 28 | 29 | do { 30 | 31 | telegram_parser_read(&parser); 32 | // TODO: figure out how to handle errors, time-outs, etc. 33 | 34 | } while (parser.terminal); // If we're connected to a serial device, keep reading, otherwise exit 35 | 36 | telegram_parser_close(&parser); 37 | 38 | return 0; 39 | } 40 | 41 | -------------------------------------------------------------------------------- /parser-tools.rl: -------------------------------------------------------------------------------- 1 | /* 2 | File: parser-tools.rl 3 | 4 | Ragel state-machine actions and definitions used to build parsers. 5 | 6 | (c)2015-2017, Levien van Zon (levien at zonnetjes.net, https://github.com/lvzon) 7 | */ 8 | 9 | %%{ 10 | machine parser; 11 | access fsm->; 12 | 13 | # A buffer to collect command arguments 14 | 15 | # Append to the string buffer. 16 | action str_append { 17 | if ( fsm->buflen < PARSER_BUFLEN ) 18 | fsm->buffer[fsm->buflen++] = fc; 19 | } 20 | 21 | # Terminate a buffer. 22 | action str_term { 23 | if ( fsm->buflen < PARSER_BUFLEN ) 24 | fsm->buffer[fsm->buflen++] = 0; 25 | } 26 | 27 | # Clear out the buffer 28 | action clearbuf { fsm->buflen = 0; } 29 | 30 | 31 | # Add a new integer argument to the stack 32 | action addarg { 33 | fsm->arg[fsm->argc] *= fsm->multiplier; 34 | if ( fsm->argc < PARSER_MAXARGS ) 35 | fsm->argc++; 36 | fsm->multiplier = 1; 37 | } 38 | 39 | # Add a new non-integer arguments to the stack, as integer value and divider 40 | action addfparg { 41 | 42 | // Add integer value 43 | fsm->arg[fsm->argc] *= fsm->multiplier; 44 | if ( fsm->argc < PARSER_MAXARGS ) 45 | fsm->argc++; 46 | fsm->multiplier = 1; 47 | 48 | // Add divider, calculated as 10^decimalpos, the decimal position from the end 49 | //printf("Decimal position: %d\n", fsm->decimalpos); 50 | if (fsm->decimalpos >= 0 && fsm->decimalpos <= MAX_DIVIDER_EXP) 51 | fsm->arg[fsm->argc] = pow10[fsm->decimalpos]; 52 | else if (fsm->decimalpos == -1) 53 | fsm->arg[fsm->argc] = 1; 54 | else 55 | fsm->arg[fsm->argc] = 0; 56 | 57 | if ( fsm->argc < PARSER_MAXARGS ) 58 | fsm->argc++; 59 | fsm->decimalpos = -1; 60 | } 61 | 62 | # Push a string onto the string argument stack 63 | action addstr { 64 | if ( fsm->strargc < PARSER_MAXARGS ) 65 | fsm->strarg[fsm->strargc++] = fsm->buffer + fsm->buflen; 66 | } 67 | 68 | # Add a single character argument to the stack 69 | action addchar { 70 | fsm->arg[fsm->argc] = fc; 71 | if ( fsm->argc < PARSER_MAXARGS ) 72 | fsm->argc++; 73 | } 74 | 75 | # Set current argument to zero 76 | action cleararg { 77 | fsm->arg[fsm->argc] = 0; 78 | fsm->bitcount = 0; 79 | fsm->decimalpos = -1; 80 | } 81 | 82 | # Set all arguments to zero 83 | action clearargs { 84 | int arg; 85 | for (arg = 0 ; arg < fsm->argc ; arg++) { 86 | // printf("clearing argument %d of %d\n", arg, fsm->argc); 87 | fsm->arg[arg] = 0; 88 | } 89 | fsm->multiplier = 1; 90 | fsm->argc = 0; 91 | fsm->bitcount = 0; 92 | fsm->decimalpos = -1; 93 | 94 | for (arg = 0 ; arg < fsm->strargc ; arg++) { 95 | fsm->strarg[arg] = NULL; 96 | } 97 | fsm->strargc = 0; 98 | fsm->buflen = 0; 99 | } 100 | 101 | # Set next lowest bit in the current argument 102 | action addbit1_low { 103 | fsm->arg[fsm->argc] = (fsm->arg[fsm->argc] << 1) | 1; 104 | } 105 | 106 | # Clear next lowest bit in the current argument 107 | action addbit0_low { 108 | fsm->arg[fsm->argc] <<= 1; 109 | } 110 | 111 | # Set next highest bit in the current argument 112 | action addbit1_high { 113 | fsm->arg[fsm->argc] = fsm->arg[fsm->argc] | (1 << fsm->bitcount++); 114 | } 115 | 116 | # Clear next highest bit in the current argument 117 | action addbit0_high { 118 | fsm->arg[fsm->argc] = fsm->arg[fsm->argc] & ~(1 << fsm->bitcount++); 119 | } 120 | 121 | # Add a decimal digit to the current argument 122 | action add_digit { 123 | fsm->arg[fsm->argc] = fsm->arg[fsm->argc] * 10 + (fc - '0'); 124 | } 125 | 126 | # Add a decimal digit to the current argument, and track the decimal point position 127 | 128 | action add_fpdigit { 129 | 130 | if (fc == '.') { 131 | fsm->decimalpos = 0; 132 | } else { 133 | 134 | fsm->arg[fsm->argc] = fsm->arg[fsm->argc] * 10 + (fc - '0'); 135 | 136 | if (fsm->decimalpos >= 0) { 137 | // Everytime we see a digit after the decimal point, increase counter 138 | fsm->decimalpos += 1; 139 | } 140 | 141 | } 142 | } 143 | 144 | # Add a hexadecimal digit to the current argument 145 | action add_hexdigit { 146 | 147 | int value; 148 | 149 | if (isdigit(fc)) { 150 | value = fc - '0'; 151 | } else if (isupper(fc)) { 152 | value = fc - 'A' + 10; 153 | } else { 154 | value = fc - 'a' + 10; 155 | } 156 | 157 | fsm->arg[fsm->argc] = fsm->arg[fsm->argc] * 16 + value; 158 | } 159 | 160 | # Negate the current argument 161 | action negate { 162 | fsm->multiplier = -1; 163 | } 164 | 165 | # Get an unsigned int from the stack and append it as byte-value to the byte-buffer. 166 | action byte_append { 167 | 168 | fsm->argc--; 169 | unsigned char byte = fsm->arg[fsm->argc] & 0xff; 170 | unsigned char *dest; 171 | 172 | if ( fsm->buflen < PARSER_BUFLEN ) { 173 | dest = (unsigned char *)(fsm->buffer) + fsm->buflen++; 174 | *dest = byte; 175 | } 176 | 177 | fsm->arg[fsm->argc] = 0; 178 | fsm->bitcount = 0; 179 | } 180 | 181 | # Write a high nibble-value to the last byte of the byte-buffer. 182 | action hexnibblehigh_append { 183 | 184 | unsigned int value; 185 | 186 | if (isdigit(fc)) { 187 | value = fc - '0'; 188 | } else if (isupper(fc)) { 189 | value = fc - 'A' + 10; 190 | } else { 191 | value = fc - 'a' + 10; 192 | } 193 | 194 | unsigned char byte = value << 4; 195 | unsigned char *dest; 196 | 197 | if ( fsm->buflen < PARSER_BUFLEN ) { 198 | dest = (unsigned char *)(fsm->buffer) + fsm->buflen; 199 | *dest = byte; 200 | } 201 | } 202 | 203 | # Append a low nibble-value to the byte-buffer. 204 | action hexnibblelow_append { 205 | 206 | unsigned int value; 207 | 208 | if (isdigit(fc)) { 209 | value = fc - '0'; 210 | } else if (isupper(fc)) { 211 | value = fc - 'A' + 10; 212 | } else { 213 | value = fc - 'a' + 10; 214 | } 215 | 216 | unsigned char nibble = value & 0x0f; 217 | unsigned char *dest; 218 | 219 | if ( fsm->buflen < PARSER_BUFLEN ) { 220 | dest = (unsigned char *)(fsm->buffer) + fsm->buflen++; 221 | *dest = (*dest & 0xf0) | nibble; 222 | } 223 | } 224 | 225 | # Helpers to collect arguments 226 | 227 | string = ^[\0\n;]+ >addstr $str_append %str_term; 228 | integer = ( ('-' @negate) | '+' )? ( digit @add_digit )+ >cleararg %addarg; # Parse and store signed integer argument 229 | uinteger = ( digit @add_digit )+ >cleararg %addarg; # Parse and store unsigned integer arument 230 | fpval = ( ('-' @negate) | '+' )? ( [1234567890.] @add_fpdigit )+ >cleararg %addfparg; # Parse and store non-integer numeric argument 231 | hexint = '0x'? ( xdigit @add_hexdigit )+ >cleararg %addarg; # Parse and store unsigned hexadecimal integer argument 232 | bitmask = ( '0b'? '0'@addbit0_low | '1'@addbit1_low )+ >cleararg %addarg; # Parse and store binary argument (MSB first) 233 | reversebitmask = ( '0'@addbit0_high | '1'@addbit1_high )+ >cleararg %addarg; # Parse and store reversed bitstring argument (LSB first) 234 | bitval = ('0x' hexint | '0b' bitmask | reversebitmask); # A bitmask in hex or binary or as reverse-bitstring 235 | uintval = ('0x' hexint | uinteger); # A numeric value as hex or unsigned integer 236 | bytes = (uintval @byte_append space+?)+; # A sequence of numeric byte values (0x00 - 0xff or 0 - 255) separated by spaces 237 | hexoctet = (xdigit @hexnibblehigh_append) (xdigit @hexnibblelow_append); # A byte represented as two hex digits 238 | hexstring = hexoctet+ >addstr %str_term; # Parse and store unsigned hexadecimal integer argument 239 | }%% 240 | --------------------------------------------------------------------------------