Improve difvif parser and other fixes.

pull/22/head
weetmuts 2018-11-25 12:33:14 +01:00
rodzic ebf9440102
commit 843038a4f9
12 zmienionych plików z 107 dodań i 99 usunięć

Wyświetl plik

@ -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.

Wyświetl plik

@ -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());
}
}

13
main.cc
Wyświetl plik

@ -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);

Wyświetl plik

@ -111,8 +111,7 @@ void MeterIperl::handleTelegram(Telegram *t)
if (useAes()) {
vector<uchar> aeskey = key();
decryptMode5_AES_CBC(t, aeskey, "iperl");
verbose("$\n");
decryptMode5_AES_CBC(t, aeskey);
} else {
t->content = t->payload;
}

Wyświetl plik

@ -174,7 +174,7 @@ void MeterMultical21::handleTelegram(Telegram *t)
if (useAes()) {
vector<uchar> 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<uchar> 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<uchar>::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?

Wyświetl plik

@ -96,7 +96,7 @@ void MeterMultical302::handleTelegram(Telegram *t) {
if (useAes()) {
vector<uchar> aeskey = key();
decryptMode1_AES_CTR(t, aeskey, "multical302");
decryptMode1_AES_CTR(t, aeskey);
} else {
t->content = t->payload;
}

Wyświetl plik

@ -89,7 +89,7 @@ void MeterOmnipower::handleTelegram(Telegram *t) {
if (useAes()) {
vector<uchar> aeskey = key();
decryptMode5_AES_CBC(t, aeskey, "omnipower");
decryptMode5_AES_CBC(t, aeskey);
} else {
t->content = t->payload;
}

Wyświetl plik

@ -110,7 +110,7 @@ void MeterSupercom587::handleTelegram(Telegram *t)
if (useAes()) {
vector<uchar> aeskey = key();
decryptMode1_AES_CTR(t, aeskey, "supercom587");
decryptMode1_AES_CTR(t, aeskey);
} else {
t->content = t->payload;
}

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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;

Wyświetl plik

@ -15,16 +15,17 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef WMBUS_UTILS_H
#define WMBUS_UTILS_H
#include"aes.h"
#include"util.h"
#include"wmbus.h"
void decryptMode1_AES_CTR(Telegram *t, vector<uchar> &aeskey, const char *meter_name)
void decryptMode1_AES_CTR(Telegram *t, vector<uchar> &aeskey)
{
vector<uchar> 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<uchar> &aeskey, const char *meter_
vector<uchar> 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<uchar> &aeskey, const char *meter_
xorit(xordata, &content[0], decrypt, remaining);
vector<uchar> 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<uchar> &aeskey, const char *meter_
incrementIV(iv, sizeof(iv));
vector<uchar> 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<uchar> 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<uchar> &aeskey, const char *meter_name)
void decryptMode5_AES_CBC(Telegram *t, vector<uchar> &aeskey)
{
vector<uchar> 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<uchar> &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<uchar> 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<uchar> 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<uchar> 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<uchar> *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;
}

Wyświetl plik

@ -18,8 +18,9 @@
#ifndef WMBUS_UTILS_H
#define WMBUS_UTILS_H
void decryptMode1_AES_CTR(Telegram *t, vector<uchar> &aeskey, const char *meter_name);
void decryptMode5_AES_CBC(Telegram *t, vector<uchar> &aeskey, const char *meter_name);
void decryptMode1_AES_CTR(Telegram *t, vector<uchar> &aeskey);
void decryptMode5_AES_CBC(Telegram *t, vector<uchar> &aeskey);
string frameTypeKamstrupC1(int ft);
bool loadFormatBytesFromSignature(uint16_t format_signature, vector<uchar> *format_bytes);
#endif