From 843038a4f9e6311131ca9d7f0490905bdc459e1f Mon Sep 17 00:00:00 2001 From: weetmuts Date: Sun, 25 Nov 2018 12:33:14 +0100 Subject: [PATCH] Improve difvif parser and other fixes. --- README.md | 25 ++--------------- dvparser.cc | 37 +++++++++++++++---------- main.cc | 13 +++++---- meter_iperl.cc | 3 +- meter_multical21.cc | 14 +++++----- meter_multical302.cc | 2 +- meter_omnipower.cc | 2 +- meter_supercom587.cc | 2 +- wmbus.cc | 31 +++++++++++++-------- wmbus_simulator.cc | 7 ++--- wmbus_utils.cc | 65 ++++++++++++++++++++++++++------------------ wmbus_utils.h | 5 ++-- 12 files changed, 107 insertions(+), 99 deletions(-) diff --git a/README.md b/README.md index 9518338..042b85f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # wmbusmeters The program receives and decodes C1 or T1 telegrams (using the wireless mbus protocol) to acquire -utility meter readings. +utility meter readings. The readings can then be published using +MQTT, inserted into a database or stored in a log file. | OS/Compiler | Status | | ------------- |:-------------:| @@ -33,28 +34,6 @@ The meter types: multical21,flowiq3100,supercom587,iperl (water meters) are supp The meter types: multical302 (heat), omnipower (electricity) are work in progress. ``` -Currently the meters are hardcoded for the European default setting -that specifies what extra data is sent in the telegrams. If someone -has a non-default meter that sends other extra data, then this will -show up as a warning when a long telegram is received (but not in the -short telegrams, where wrong values might be printed instead!). If -this happens to someone, then we need to implement a way to pass the -meter configuration as a parameter. - -Actually, the mbus (and consequently the wmbus) protocol is a standard -that is self-describing. Thus in reality it should not be necessary -to supply exactly which kind of meter we expect for a given id. This -should be possible to figure out when we receive the first telegram. - -Thus, strictly speaking, it should not be necessary to specify the -exact meter type. A more generic meter type might be just "water", -"heat" or electricity. But for the moment, the separation of meter -types will remain in the code. Thus even though the meter type right -now is named multical302, the other heat meters (multical-402 and -multical-602) might be compatible as well. The same is true for the -omnipower meter type, which might include the electricity meters -Kamstrup-162 Kamstrup-382, Kamstrup-351 etc). - No meter quadruplets means listen for telegram traffic and print any id heard, but you have to specify if you want to listen using radio mode C1 or T1. E.g. diff --git a/dvparser.cc b/dvparser.cc index 6aa7d9e..4f85e25 100644 --- a/dvparser.cc +++ b/dvparser.cc @@ -70,27 +70,25 @@ bool parseDV(Telegram *t, format_bytes.clear(); for (;;) { - DEBUG_PARSER("Remaining format data %ju\n", std::distance(*format,format_end)); + DEBUG_PARSER("(dvparser debug) Remaining format data %ju\n", std::distance(*format,format_end)); if (*format == format_end) break; uchar dif = **format; int datalen = difLenBytes(dif); - DEBUG_PARSER("dif=%02x datalen=%d \"%s\"\n", dif, datalen, difType(dif).c_str()); + DEBUG_PARSER("(dvparser debug) dif=%02x datalen=%d \"%s\"\n", dif, datalen, difType(dif).c_str()); + if (datalen == -2) { + warning("(dvparser) cannot handle dif %02X ignoring rest of telegram.\n", dif); + return false; + } if (dif == 0x2f) { t->addExplanation(*format, 1, "%02X skip", dif); continue; } - if (datalen == -1) { variable_length = true; - warning("(dvparser) variable length dif %02X.\n", dif); } else { variable_length = false; } - if (datalen == -2) { - warning("(dvparser) cannot handle dif %02X ignoring rest of telegram.\n", dif); - return false; - } if (full_header) { format_bytes.push_back(dif); t->addExplanation(*format, 1, "%02X dif (%s)", dif, difType(dif).c_str()); @@ -101,7 +99,7 @@ bool parseDV(Telegram *t, if (*format == format_end) { warning("(dvparser) warning: unexpected end of data (vif expected)"); break; } uchar vif = **format; - DEBUG_PARSER("vif=%02x \"%s\"\n", vif, vifType(vif).c_str()); + DEBUG_PARSER("(dvparser debug) vif=%02x \"%s\"\n", vif, vifType(vif).c_str()); if (full_header) { format_bytes.push_back(vif); t->addExplanation(*format, 1, "%02X vif (%s)", vif, vifType(vif).c_str()); @@ -114,7 +112,7 @@ bool parseDV(Telegram *t, // vif extension if (*format == format_end) { warning("(dvparser) warning: unexpected end of data (vife expected)"); break; } uchar vife = **format; - DEBUG_PARSER("vife=%02x\n", vife); + DEBUG_PARSER("(dvparser debug) vife=%02x\n", vife); if (full_header) { format_bytes.push_back(vife); t->addExplanation(*format, 1, "%02X vife (%s)", vife, vifeType(vif, vife).c_str()); @@ -127,7 +125,7 @@ bool parseDV(Telegram *t, if (overrideDifLen) { int new_len = overrideDifLen(dif, vif, datalen); if (new_len != datalen) { - DEBUG_PARSER("changing len %d to %d for dif=%02x vif=%02x\n", datalen, new_len, dif, vif); + DEBUG_PARSER("(dvparser debug) changing len %d to %d for dif=%02x vif=%02x\n", datalen, new_len, dif, vif); datalen = new_len; } } @@ -138,13 +136,18 @@ bool parseDV(Telegram *t, } else { strprintf(key, "%s", dv.c_str()); } - DEBUG_PARSER("DifVif key is %s\n", key.c_str()); + DEBUG_PARSER("(dvparser debug) DifVif key is %s\n", key.c_str()); int remaining = std::distance(data, data_end); if (variable_length) { - datalen = remaining; + debug("(dvparser) varlen %02x\n", *(data+0)); + if (remaining > 2) { + datalen = *(data); + } else { + datalen = remaining; + } } - DEBUG_PARSER("remaining data %d len=%d\n", remaining, datalen); + DEBUG_PARSER("(dvparser debug) remaining data %d len=%d\n", remaining, datalen); if (remaining < datalen) { warning("(dvparser) warning: unexpected end of data\n"); datalen = remaining; @@ -153,7 +156,13 @@ bool parseDV(Telegram *t, string value = bin2hex(data, datalen); (*values)[key] = { start_parse_here+data-data_start, value }; if (value.length() > 0) { + // Skip the length byte in the variable length data. + if (variable_length) { + data++; + } + // This call increments data with datalen. t->addExplanation(data, datalen, "%s", value.c_str()); + DEBUG_PARSER("(dvparser debug) data \"%s\"\n", value.c_str()); } } diff --git a/main.cc b/main.cc index 097b904..91b4e55 100644 --- a/main.cc +++ b/main.cc @@ -129,30 +129,31 @@ int main(int argc, char **argv) if (cmdline->meters.size() > 0) { for (auto &m : cmdline->meters) { + const char *keymsg = (m.key[0] == 0) ? "not-encrypted" : "encrypted"; switch (toMeterType(m.type)) { case MULTICAL21_METER: m.meter = createMultical21(wmbus, m.name, m.id, m.key, MULTICAL21_METER); - verbose("(multical21) configured \"%s\" \"multical21\" \"%s\" \"%s\"\n", m.name, m.id, m.key); + verbose("(multical21) configured \"%s\" \"multical21\" \"%s\" %s\n", m.name, m.id, keymsg); break; case FLOWIQ3100_METER: m.meter = createMultical21(wmbus, m.name, m.id, m.key, FLOWIQ3100_METER); - verbose("(flowiq3100) configured \"%s\" \"flowiq3100\" \"%s\" \"%s\"\n", m.name, m.id, m.key); + verbose("(flowiq3100) configured \"%s\" \"flowiq3100\" \"%s\" %s\n", m.name, m.id, keymsg); break; case MULTICAL302_METER: m.meter = createMultical302(wmbus, m.name, m.id, m.key); - verbose("(multical302) configured \"%s\" \"multical302\" \"%s\" \"%s\"\n", m.name, m.id, m.key); + verbose("(multical302) configured \"%s\" \"multical302\" \"%s\" %s\n", m.name, m.id, keymsg); break; case OMNIPOWER_METER: m.meter = createOmnipower(wmbus, m.name, m.id, m.key); - verbose("(omnipower) configured \"%s\" \"omnipower\" \"%s\" \"%s\"\n", m.name, m.id, m.key); + verbose("(omnipower) configured \"%s\" \"omnipower\" \"%s\" %s\n", m.name, m.id, keymsg); break; case SUPERCOM587_METER: m.meter = createSupercom587(wmbus, m.name, m.id, m.key); - verbose("(supercom587) configured \"%s\" \"supercom587\" \"%s\" \"%s\"\n", m.name, m.id, m.key); + verbose("(supercom587) configured \"%s\" \"supercom587\" \"%s\" %s\n", m.name, m.id, keymsg); break; case IPERL_METER: m.meter = createIperl(wmbus, m.name, m.id, m.key); - verbose("(iperl) configured \"%s\" \"iperl\" \"%s\" \"%s\"\n", m.name, m.id, m.key); + verbose("(iperl) configured \"%s\" \"iperl\" \"%s\" %s\n", m.name, m.id, keymsg); break; case UNKNOWN_METER: error("No such meter type \"%s\"\n", m.type); diff --git a/meter_iperl.cc b/meter_iperl.cc index 991e6ca..aee3bae 100644 --- a/meter_iperl.cc +++ b/meter_iperl.cc @@ -111,8 +111,7 @@ void MeterIperl::handleTelegram(Telegram *t) if (useAes()) { vector aeskey = key(); - decryptMode5_AES_CBC(t, aeskey, "iperl"); - verbose("$\n"); + decryptMode5_AES_CBC(t, aeskey); } else { t->content = t->payload; } diff --git a/meter_multical21.cc b/meter_multical21.cc index 9f6d6d8..607dc89 100644 --- a/meter_multical21.cc +++ b/meter_multical21.cc @@ -174,7 +174,7 @@ void MeterMultical21::handleTelegram(Telegram *t) if (useAes()) { vector aeskey = key(); - decryptMode1_AES_CTR(t, aeskey, meter_name_); + decryptMode1_AES_CTR(t, aeskey); } else { t->content = t->payload; } @@ -231,13 +231,13 @@ void MeterMultical21::processContent(Telegram *t) t->addExplanation(bytes, 2, "%02x%02x format signature", ecrc0, ecrc1); uint16_t format_signature = ecrc0<<8 | ecrc1; - // The format signature is used to find the proper format string. - // But since the crc calculation is not yet functional. This functionality - // has to wait a bit. So we hardcode the format string here. vector format_bytes; - hex2bin("02FF2004134413", &format_bytes); // Yes, the hash of this string should equal the format signature above. - uint16_t format_hash = crc16_EN13757(&format_bytes[0], 7); - debug("(multical21) format signature %4X format hash %4X\n", format_signature, format_hash); + bool ok = loadFormatBytesFromSignature(format_signature, &format_bytes); + if (!ok) { + warning("(%s) warning: Unknown format signature hash 0x%02x! Cannot decode telegram.\n", + meter_name_, format_signature); + return; + } vector::iterator format = format_bytes.begin(); // 2,3 = crc for payload = hash over both DRH and data bytes. Or is it only over the data bytes? diff --git a/meter_multical302.cc b/meter_multical302.cc index 88722cf..9c58de8 100644 --- a/meter_multical302.cc +++ b/meter_multical302.cc @@ -96,7 +96,7 @@ void MeterMultical302::handleTelegram(Telegram *t) { if (useAes()) { vector aeskey = key(); - decryptMode1_AES_CTR(t, aeskey, "multical302"); + decryptMode1_AES_CTR(t, aeskey); } else { t->content = t->payload; } diff --git a/meter_omnipower.cc b/meter_omnipower.cc index ed006b7..a54fbe9 100644 --- a/meter_omnipower.cc +++ b/meter_omnipower.cc @@ -89,7 +89,7 @@ void MeterOmnipower::handleTelegram(Telegram *t) { if (useAes()) { vector aeskey = key(); - decryptMode5_AES_CBC(t, aeskey, "omnipower"); + decryptMode5_AES_CBC(t, aeskey); } else { t->content = t->payload; } diff --git a/meter_supercom587.cc b/meter_supercom587.cc index addf6e8..421eb1c 100644 --- a/meter_supercom587.cc +++ b/meter_supercom587.cc @@ -110,7 +110,7 @@ void MeterSupercom587::handleTelegram(Telegram *t) if (useAes()) { vector aeskey = key(); - decryptMode1_AES_CTR(t, aeskey, "supercom587"); + decryptMode1_AES_CTR(t, aeskey); } else { t->content = t->payload; } diff --git a/wmbus.cc b/wmbus.cc index f56edc4..19278be 100644 --- a/wmbus.cc +++ b/wmbus.cc @@ -435,16 +435,19 @@ int difLenBytes(int dif) case 0x5: return 4; // 32 Bit Real case 0x6: return 6; // 48 Bit Integer/Binary case 0x7: return 8; // 64 Bit Integer/Binary - case 0x8: return -1; // Selection for Readout (I do not know what this is) + case 0x8: return 0; // Selection for Readout case 0x9: return 1; // 2 digit BCD case 0xA: return 2; // 4 digit BCD case 0xB: return 3; // 6 digit BCD case 0xC: return 4; // 8 digit BCD case 0xD: return -1; // variable length case 0xE: return 6; // 12 digit BCD - case 0xF: return -2; // Special Functions + case 0xF: // Special Functions + if (dif == 0x2f) return 1; // The skip code 0x2f, used for padding. + return -2; } - return -1; + // Bad! + return -2; } string difType(int dif) @@ -471,22 +474,26 @@ string difType(int dif) default: s+= "?"; break; } - t = dif & 0x30; + if (t != 0xf) + { + // Only print these suffixes when we have actual values. + t = dif & 0x30; - switch (t) { - case 0x00: s += " Instantaneous value"; break; - case 0x10: s += " Maximum value"; break; - case 0x20: s += " Minimum value"; break; - case 0x30: s+= " Value during error state"; break; - default: s += "?"; break; + switch (t) { + case 0x00: s += " Instantaneous value"; break; + case 0x10: s += " Maximum value"; break; + case 0x20: s += " Minimum value"; break; + case 0x30: s+= " Value during error state"; break; + default: s += "?"; break; + } } - + /* if (dif & 0x40) { // Kamstrup uses this bit in the Multical21 to signal that for the // full 32 bits of data, the lower 16 bits are from this difvif record, // and the high 16 bits are from a different record. s += " VendorSpecific"; - } + }*/ return s; } diff --git a/wmbus_simulator.cc b/wmbus_simulator.cc index abccf5f..7cee5b0 100644 --- a/wmbus_simulator.cc +++ b/wmbus_simulator.cc @@ -153,16 +153,15 @@ void WMBusSimulator::simulate() if (l[i] == '+') { found_time = i; rel_time = atoi(&l[i+1]); - printf("Found time %ld -%s-\n", rel_time, &l[i+1]); break; } hex += l[i]; } if (found_time) { - verbose("(simulator) from file \"%s\" to trigger at relative time %ld\n", hex.c_str(), rel_time); + debug("(simulator) from file \"%s\" to trigger at relative time %ld\n", hex.c_str(), rel_time); time_t curr = time(NULL); if (curr < start_time+rel_time) { - verbose("(simulator) waiting %d seconds before simulating telegram.\n", (start_time+rel_time)-curr); + debug("(simulator) waiting %d seconds before simulating telegram.\n", (start_time+rel_time)-curr); for (;;) { curr = time(NULL); if (curr > start_time + rel_time) break; @@ -170,7 +169,7 @@ void WMBusSimulator::simulate() } } } else { - verbose("(simulator) from file \"%s\"\n", hex.c_str()); + debug("(simulator) from file \"%s\"\n", hex.c_str()); } } else { continue; diff --git a/wmbus_utils.cc b/wmbus_utils.cc index 1f6dc76..0fbc6a8 100644 --- a/wmbus_utils.cc +++ b/wmbus_utils.cc @@ -15,16 +15,17 @@ along with this program. If not, see . */ -#ifndef WMBUS_UTILS_H -#define WMBUS_UTILS_H #include"aes.h" +#include"util.h" #include"wmbus.h" -void decryptMode1_AES_CTR(Telegram *t, vector &aeskey, const char *meter_name) +void decryptMode1_AES_CTR(Telegram *t, vector &aeskey) { vector content; content.insert(content.end(), t->payload.begin(), t->payload.end()); + debugPayload("(Mode1) decrypting", content); + size_t remaining = content.size(); if (remaining > 16) remaining = 16; @@ -45,7 +46,7 @@ void decryptMode1_AES_CTR(Telegram *t, vector &aeskey, const char *meter_ vector ivv(iv, iv+16); string s = bin2hex(ivv); - debug("(%s) IV %s\n", meter_name, s.c_str()); + debug("(Mode1) IV %s\n", s.c_str()); uchar xordata[16]; AES_ECB_encrypt(iv, &aeskey[0], xordata, 16); @@ -54,11 +55,11 @@ void decryptMode1_AES_CTR(Telegram *t, vector &aeskey, const char *meter_ xorit(xordata, &content[0], decrypt, remaining); vector dec(decrypt, decrypt+remaining); - debugPayload("(C1) decrypted", dec); + debugPayload("(Mode1) decrypted first block", dec); if (content.size() > 22) { - warning("(%s) warning: C1 decryption received too many bytes of content! " - "Got %zu bytes, expected at most 22.\n", meter_name, content.size()); + warning("(Mode1) warning: decryption received too many bytes of content! " + "Got %zu bytes, expected at most 22.\n", content.size()); } if (content.size() > 16) { // Yay! Lets decrypt a second block. Full frame content is 22 bytes. @@ -69,20 +70,21 @@ void decryptMode1_AES_CTR(Telegram *t, vector &aeskey, const char *meter_ incrementIV(iv, sizeof(iv)); vector ivv2(iv, iv+16); string s2 = bin2hex(ivv2); - debug("(%s) IV+1 %s\n", meter_name, s2.c_str()); + debug("(Mode1) IV+1 %s\n", s2.c_str()); AES_ECB_encrypt(iv, &aeskey[0], xordata, 16); xorit(xordata, &content[16], decrypt, remaining); vector dec2(decrypt, decrypt+remaining); - debugPayload("(C1) decrypted", dec2); + debugPayload("(Mode1) decrypted second block", dec2); // Append the second decrypted block to the first. dec.insert(dec.end(), dec2.begin(), dec2.end()); } t->content.clear(); t->content.insert(t->content.end(), dec.begin(), dec.end()); + debugPayload("(Mode1) decrypted", t->content); } string frameTypeKamstrupC1(int ft) { @@ -91,16 +93,18 @@ string frameTypeKamstrupC1(int ft) { return "?"; } -void decryptMode5_AES_CBC(Telegram *t, vector &aeskey, const char *meter_name) +void decryptMode5_AES_CBC(Telegram *t, vector &aeskey) { vector content; content.insert(content.end(), t->payload.begin(), t->payload.end()); + debugPayload("(Mode5) decrypting", content); + // The content should be a multiple of 16 since we are using AES CBC mode. if (content.size() % 16 != 0) { - warning("(%s) warning: T1 decryption received non-multiple of 16 bytes! " + warning("(Mode5) warning: decryption received non-multiple of 16 bytes! " "Got %zu bytes shrinking message to %zu bytes.\n", - meter_name, content.size(), content.size() - content.size() % 16); + content.size(), content.size() - content.size() % 16); while (content.size() % 16 != 0) { content.pop_back(); @@ -114,29 +118,38 @@ void decryptMode5_AES_CBC(Telegram *t, vector &aeskey, const char *meter_ // A-field for (int j=0; j<6; ++j) { iv[i++] = t->a_field[j]; } // ACC - iv[i++] = t->acc; - // SN-field - for (int j=0; j<4; ++j) { iv[i++] = t->acc; } - // FN - iv[i++] = t->acc; iv[i++] = t->acc; - // BC - iv[i++] = t->acc; + for (int j=0; j<8; ++j) { iv[i++] = t->acc; } vector ivv(iv, iv+16); string s = bin2hex(ivv); - verbose("(%s) IV %s\n", meter_name, s.c_str()); + debug("(Mode5) IV %s\n", s.c_str()); - uchar decrypted_data[16]; - AES_CBC_decrypt_buffer(decrypted_data, &content[0], 16, &aeskey[0], iv); - vector decrypted(decrypted_data, decrypted_data+16); + uchar decrypted_data[content.size()]; + AES_CBC_decrypt_buffer(decrypted_data, &content[0], content.size(), &aeskey[0], iv); + vector decrypted(decrypted_data, decrypted_data+content.size()); if (decrypted_data[0] != 0x2F || decrypted_data[1] != 0x2F) { - verbose("(%s) decrypt failed!\n", meter_name); + verbose("(Mode5) decrypt failed!\n"); } t->content.clear(); t->content.insert(t->content.end(), decrypted.begin(), decrypted.end()); - debugPayload("(T1) decrypted", t->content); + debugPayload("(Mode5) decrypted", t->content); } -#endif +bool loadFormatBytesFromSignature(uint16_t format_signature, vector *format_bytes) +{ + // The format signature is used to find the proper format string. + // But since the crc calculation is not yet functional. This functionality + // has to wait a bit. So we hardcode the format string here. + if (format_signature == 0xeda8) + { + hex2bin("02FF2004134413", format_bytes); + // The hash of this string should equal the format signature above. + uint16_t format_hash = crc16_EN13757(&(*format_bytes)[0], 7); + debug("(utils) format signature %4X format hash %4X\n", format_signature, format_hash); + return true; + } + // Unknown format signature. + return false; +} diff --git a/wmbus_utils.h b/wmbus_utils.h index 12c7d3a..520991d 100644 --- a/wmbus_utils.h +++ b/wmbus_utils.h @@ -18,8 +18,9 @@ #ifndef WMBUS_UTILS_H #define WMBUS_UTILS_H -void decryptMode1_AES_CTR(Telegram *t, vector &aeskey, const char *meter_name); -void decryptMode5_AES_CBC(Telegram *t, vector &aeskey, const char *meter_name); +void decryptMode1_AES_CTR(Telegram *t, vector &aeskey); +void decryptMode5_AES_CBC(Telegram *t, vector &aeskey); string frameTypeKamstrupC1(int ft); +bool loadFormatBytesFromSignature(uint16_t format_signature, vector *format_bytes); #endif