From fd886276678347dc97a689e41e11dbe47f68f0d4 Mon Sep 17 00:00:00 2001 From: weetmuts Date: Wed, 28 Feb 2018 22:14:16 +0100 Subject: [PATCH] Added support for the Amber amb8465 wmbus usb dongle. --- CHANGES | 9 + Makefile | 3 +- README.md | 29 +++- cmdline.cc | 24 ++- cmdline.h | 28 +-- main.cc | 65 ++++--- meter_multical21.cc | 175 +++++++++---------- serial.cc | 190 +++++++++++++-------- serial.h | 8 +- util.cc | 77 ++++++++- util.h | 19 ++- wmbus.cc | 88 +++++++++- wmbus.h | 30 +++- wmbus_amb8465.cc | 406 ++++++++++++++++++++++++++++++++++++++++++++ wmbus_amb8465.h | 22 +++ wmbus_im871a.cc | 231 +++++++++++++++---------- wmbus_im871a.h | 9 +- 17 files changed, 1080 insertions(+), 333 deletions(-) create mode 100644 CHANGES create mode 100644 wmbus_amb8465.cc create mode 100644 wmbus_amb8465.h diff --git a/CHANGES b/CHANGES new file mode 100644 index 0000000..1fc3c5e --- /dev/null +++ b/CHANGES @@ -0,0 +1,9 @@ + + +Version 0.3: + +Added support for wmbus USB receiver Amber AMB8465. + +Version 0.2: + +Initial working release supporting wmbus USB receiver IMST im871a and the meter Multical21. \ No newline at end of file diff --git a/Makefile b/Makefile index d56cb73..3541aae 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ endif $(shell mkdir -p $(BUILD)) -CXXFLAGS := $(DEBUG_FLAGS) -Wall -fmessage-length=0 -std=c++11 -Wno-unused-function "-DWMBUSMETERS_VERSION=\"0.2\"" +CXXFLAGS := $(DEBUG_FLAGS) -Wall -fmessage-length=0 -std=c++11 -Wno-unused-function "-DWMBUSMETERS_VERSION=\"0.3\"" $(BUILD)/%.o: %.cc $(wildcard %.h) $(CXX) $(CXXFLAGS) $< -c -o $@ @@ -42,6 +42,7 @@ METERS_OBJS:=\ $(BUILD)/serial.o \ $(BUILD)/util.o \ $(BUILD)/wmbus.o \ + $(BUILD)/wmbus_amb8465.o \ $(BUILD)/wmbus_im871a.o \ all: $(BUILD)/wmbusmeters diff --git a/README.md b/README.md index 4e07103..347dd70 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ utility meter readings. |Linux G++| [![Build Status](https://travis-ci.org/weetmuts/wmbusmeters.svg?branch=master)](https://travis-ci.org/weetmuts/wmbusmeters) | ``` -wmbusmeters version: 0.2 -Usage: wmbusmeters {options} [usbdevice] { [meter_name] [meter_id] [meter_key] }* +wmbusmeters version: 0.3 +Usage: wmbusmeters {options} (auto | /dev/ttyUSBx)] { [meter_name] [meter_id] [meter_key] }* Add more meter triplets to listen to more meters. Add --verbose for detailed debug information. @@ -17,6 +17,9 @@ Add --verbose for detailed debug information. --meterfiles to create status files below tmp, named /tmp/meter_name, containing the latest reading. --oneshot wait for an update from each meter, then quit. + +Specifying auto as the device will automatically look for usb +wmbus dongles on /dev/im871a and /dev/amb8465 ``` No meter triplets means listen for telegram traffic and print any id heard. @@ -28,16 +31,20 @@ make ./build/wmbusmeters /dev/ttyUSB0 MyTapWater 12345678 00112233445566778899AABBCCDDEEFF ``` +wmbusmeters will detect which kind of dongle is connected to /dev/ttyUSB0. It can be either an IMST 871a dongle or an Amber Wireless AMB8465. If you have setup the udev rules below, then you can use auto instead of /dev/ttyUSB0. + Example output: `MyTapWater 12345678 6.375 m3 2017-08-31 09:09.08 3.040 m3 DRY(dry 22-31 days)` `./build/wmbusmeters --verbose /dev/ttyUSB0 MyTapWater 12345678 00112233445566778899AABBCCDDEEFF` -`./build/wmbusmeters --robot /dev/ttyUSB0 MyTapWater 12345678 00112233445566778899AABBCCDDEEFF` +`./build/wmbusmeters --robot auto /dev/ttyUSB1 12345678 00112233445566778899AABBCCDDEEFF` Robot output: `{"name":"MyTapWater","id":"12345678","total_m3":6.375,"target_m3":3.040,"current_status":"","time_dry":"22-31 days","time_reversed":"","time_leaking":"","time_bursting":"","timestamp":"2017-08-31T09:07:18Z"}` +You can use `--debug` to get both verbose output and the actual data bytes sent back and forth with the wmbus usb dongle. + `make HOST=arm` Binary generated: `./build_arm/wmbusmeters` @@ -63,15 +70,21 @@ Or even better, add this udev rule: Create the file: `/etc/udev/rules.d/99-usb-serial.rules` with the content ``` SUBSYSTEM=="tty", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", SYMLINK+="im871a",MODE="0660", GROUP="yourowngroup" +SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", SYMLINK+="amb8465",MODE="0660", GROUP="yourowngroup" ``` -This will create a symlink named `/dev/im871a` to the particular USB port that the dongle got assigned. + +When you insert the wmbus USB dongle, a properly named symlink will be +created: either `/dev/im871a` or `/dev/amb8465`. These symlinks are +necessary if you want to pass "auto" to wmbusmeters instead of the +exact serial port /dev/ttyUSBx. # Limitations -Currently only supports the USB stick receiver im871A -and the water meter Multical21. The source code is modular -and it should be relatively straightforward to add -more receivers and meters. +Two usb wmbus receivers are supported: IMST im871A and Amber Wireless AMB8465. + +One supported meter: Multical21. + +The source code is modular and it should be relatively straightforward to add more receivers and meters. # Good documents on the wireless mbus protocol: diff --git a/cmdline.cc b/cmdline.cc index a36db45..14ba819 100644 --- a/cmdline.cc +++ b/cmdline.cc @@ -1,15 +1,15 @@ // Copyright (c) 2017 Fredrik Öhrström -// +// // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: -// +// // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. -// +// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -26,7 +26,7 @@ using namespace std; CommandLine *parseCommandLine(int argc, char **argv) { CommandLine * c = new CommandLine; - + int i=1; if (argc < 2) { c->need_help = true; @@ -37,11 +37,21 @@ CommandLine *parseCommandLine(int argc, char **argv) { c->need_help = true; return c; } + if (!strcmp(argv[i], "--silence")) { + c->silence = true; + i++; + continue; + } if (!strcmp(argv[i], "--verbose")) { c->verbose = true; i++; continue; } + if (!strcmp(argv[i], "--debug")) { + c->debug = true; + i++; + continue; + } if (!strcmp(argv[i], "--robot")) { c->robot = true; i++; @@ -63,7 +73,7 @@ CommandLine *parseCommandLine(int argc, char **argv) { } error("Unknown option \"%s\"\n", argv[i]); } - + c->usb_device = argv[i]; i++; if (!c->usb_device) error("You must supply the usb device to which the wmbus dongle is connected.\n"); @@ -79,7 +89,7 @@ CommandLine *parseCommandLine(int argc, char **argv) { char *name = argv[m*3+i+0]; char *id = argv[m*3+i+1]; char *key = argv[m*3+i+2]; - + if (!isValidId(id)) error("Not a valid meter id \"%s\"\n", id); if (!isValidKey(key)) error("Not a valid meter key \"%s\"\n", key); c->meters.push_back(MeterInfo(name,id,key)); @@ -87,5 +97,3 @@ CommandLine *parseCommandLine(int argc, char **argv) { return c; } - - diff --git a/cmdline.h b/cmdline.h index 7de3168..efa10f7 100644 --- a/cmdline.h +++ b/cmdline.h @@ -1,15 +1,15 @@ // Copyright (c) 2017 Fredrik Öhrström -// +// // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: -// +// // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. -// +// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -32,24 +32,26 @@ struct MeterInfo { char *id; char *key; Meter *meter; - + MeterInfo(char *n, char *i, char *k) { name = n; id = i; key = k; } }; - + struct CommandLine { - bool need_help; - bool verbose; - bool meterfiles; - bool robot; - bool oneshot; - char *usb_device; + bool need_help {}; + bool silence {}; + bool verbose {}; + bool debug {}; + bool meterfiles {}; + bool robot {}; + bool oneshot {}; + char *usb_device {}; vector meters; }; - + CommandLine *parseCommandLine(int argc, char **argv); - + #endif diff --git a/main.cc b/main.cc index fcf829e..f383b37 100644 --- a/main.cc +++ b/main.cc @@ -1,15 +1,15 @@ // Copyright (c) 2017 Fredrik Öhrström -// +// // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: -// +// // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. -// +// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -34,59 +34,76 @@ void oneshotCheck(CommandLine *cmdline, SerialCommunicationManager *manager, Met int main(int argc, char **argv) { CommandLine *cmdline = parseCommandLine(argc, argv); - + if (cmdline->need_help) { printf("wmbusmeters version: " WMBUSMETERS_VERSION "\n"); - printf("Usage: wmbusmeters [--verbose] [--robot] [usbdevice] { [meter_name] [meter_id] [meter_key] }* \n"); - printf("\nAdd more meter quadruplets to listen to more meters.\n"); - printf("Add --verbose for detailed debug information.\n"); + printf("Usage: wmbusmeters [options] (auto | /dev/ttyUSBx) { [meter_name] [meter_id] [meter_key] }* \n\n"); + printf("Add more meter triplets to listen to more meters.\n"); + printf("Add --verbose for more detailed information on communication.\n"); printf(" --robot for json output.\n"); printf(" --meterfiles to create status files below tmp,\n" " named /tmp/meter_name, containing the latest reading.\n"); - printf(" --oneshot wait for an update from each meter, then quit.\n"); + printf(" --oneshot wait for an update from each meter, then quit.\n\n"); + printf("Specifying auto as the device will automatically look for usb\n"); + printf("wmbus dongles on /dev/im871a and /dev/amb8465\n\n"); + exit(0); } + // We want the data visible in the log file asap! + setbuf(stdout, NULL); + warningSilenced(cmdline->silence); verboseEnabled(cmdline->verbose); - + debugEnabled(cmdline->debug); + auto manager = createSerialCommunicationManager(); onExit(call(manager,stop)); - - auto wmbus = openIM871A(cmdline->usb_device, manager); - wmbus->setLinkMode(C1a); - if (wmbus->getLinkMode()!=C1a) error("Could not set link mode to C1a\n"); + WMBus *wmbus = NULL; - // We want the data visible in the log file asap! - setbuf(stdout, NULL); + auto type_and_device = detectMBusDevice(cmdline->usb_device, manager); + + switch (type_and_device.first) { + case DEVICE_IM871A: + verbose("(im871a) detected on %s\n", type_and_device.second.c_str()); + wmbus = openIM871A(type_and_device.second, manager); + break; + case DEVICE_AMB8465: + verbose("(amb8465) detected on %s\n", type_and_device.second.c_str()); + wmbus = openAMB8465(type_and_device.second, manager); + break; + case DEVICE_UNKNOWN: + error("No wmbus device found!\n"); + exit(1); + break; + } + + wmbus->setLinkMode(LinkModeC1); + if (wmbus->getLinkMode()!=LinkModeC1) error("Could not set link mode to receive C1 telegrams.\n"); Printer *output = new Printer(cmdline->robot, cmdline->meterfiles); - + if (cmdline->meters.size() > 0) { for (auto &m : cmdline->meters) { - verbose("Configuring meter: \"%s\" \"%s\" \"%s\"\n", m.name, m.id, m.key); m.meter = createMultical21(wmbus, m.name, m.id, m.key); + verbose("(multical21) configured \"%s\" \"%s\" \"%s\"\n", m.name, m.id, m.key); m.meter->onUpdate(calll(output,print,Meter*)); m.meter->onUpdate([cmdline,manager](Meter*meter) { oneshotCheck(cmdline,manager,meter); }); } } else { - printf("No meters configured. Printing id:s of all telegrams heard! \n"); - printf("To configure a meter, add a triplet to the command line: name id key\n"); - printf("Where name is your string identifying the meter.\n"); - printf(" id is the 8 digits printed on the meter.\n"); - printf(" key is 32 hex digits with the aes key.\n\n"); + printf("No meters configured. Printing id:s of all telegrams heard!\n\n"); wmbus->onTelegram([](Telegram *t){t->print();}); } - manager->waitForStop(); + manager->waitForStop(); } void oneshotCheck(CommandLine *cmdline, SerialCommunicationManager *manager, Meter *meter) { if (!cmdline->oneshot) return; - + for (auto &m : cmdline->meters) { if (m.meter->numUpdates() == 0) return; } diff --git a/meter_multical21.cc b/meter_multical21.cc index 42d3f4d..8256af1 100644 --- a/meter_multical21.cc +++ b/meter_multical21.cc @@ -1,15 +1,15 @@ // Copyright (c) 2017 Fredrik Öhrström -// +// // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: -// +// // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. -// +// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -41,7 +41,7 @@ using namespace std; #define INFO_CODE_LEAK_SHIFT (4+6) #define INFO_CODE_BURST 0x08 -#define INFO_CODE_BURST_SHIFT (4+9) +#define INFO_CODE_BURST_SHIFT (4+9) struct MeterMultical21 : public Meter { MeterMultical21(WMBus *bus, const char *name, const char *id, const char *key); @@ -56,62 +56,56 @@ struct MeterMultical21 : public Meter { // We can see which was sent inside the wmbus message! // Target water consumption: The total consumption at the start of the previous 30 day period. float targetWaterConsumption(); - bool hasTargetWaterConsumption(); + bool hasTargetWaterConsumption(); // Max flow during last month or last 24 hours depending on meter configuration. float maxFlow(); bool hasMaxFlow(); - + // statusHumanReadable: DRY,REVERSED,LEAK,BURST if that status is detected right now, followed by // (dry 15-21 days) which means that, even it DRY is not active right now, - // DRY has been active for 15-21 days during the last 30 days. + // DRY has been active for 15-21 days during the last 30 days. string statusHumanReadable(); string status(); string timeDry(); string timeReversed(); string timeLeaking(); string timeBursting(); - + string datetimeOfUpdateHumanReadable(); string datetimeOfUpdateRobot(); void onUpdate(function cb); int numUpdates(); - + private: void handleTelegram(Telegram*t); void processContent(vector &d); string decodeTime(int time); - int info_codes_; + int info_codes_; float total_water_consumption_; bool has_total_water_consumption_; float target_volume_; bool has_target_volume_; float max_flow_; bool has_max_flow_; - + time_t datetime_of_update_; - + string name_; vector id_; vector key_; WMBus *bus_; vector> on_update_; int num_updates_; - bool matches_any_id_; bool use_aes_; }; MeterMultical21::MeterMultical21(WMBus *bus, const char *name, const char *id, const char *key) : info_codes_(0), total_water_consumption_(0), has_total_water_consumption_(false), target_volume_(0), has_target_volume_(false), - max_flow_(0), has_max_flow_(false), name_(name), bus_(bus), num_updates_(0), - matches_any_id_(false), use_aes_(true) + max_flow_(0), has_max_flow_(false), name_(name), bus_(bus), num_updates_(0), use_aes_(true) { - if (strlen(id) == 0) { - matches_any_id_ = true; - } else { - hex2bin(id, &id_); - } + hex2bin(id, &id_); if (strlen(key) == 0) { use_aes_ = false; } else { @@ -182,8 +176,8 @@ string MeterMultical21::datetimeOfUpdateRobot() { char datetime[40]; memset(datetime, 0, sizeof(datetime)); - // This is the date time in the Greenwich timezone, dont get surprised! - strftime(datetime, sizeof(datetime), "%FT%TZ", gmtime(&datetime_of_update_)); + // This is the date time in the Greenwich timezone (Zulu time), dont get surprised! + strftime(datetime, sizeof(datetime), "%FT%TZ", gmtime(&datetime_of_update_)); return string(datetime); } @@ -193,38 +187,35 @@ Meter *createMultical21(WMBus *bus, const char *name, const char *id, const char void MeterMultical21::handleTelegram(Telegram *t) { - if (matches_any_id_ || ( - t->m_field == MANUFACTURER_KAM && - t->a_field_address[3] == id_[3] && - t->a_field_address[2] == id_[2] && - t->a_field_address[1] == id_[1] && - t->a_field_address[0] == id_[0])) { - verbose("Meter %s receives update with id %02x%02x%02x%02x!\n", - name_.c_str(), - t->a_field_address[0], t->a_field_address[1], t->a_field_address[2], - t->a_field_address[3]); - } else { - verbose("Meter %s ignores message with id %02x%02x%02x%02x \n", - name_.c_str(), - t->a_field_address[0], t->a_field_address[1], t->a_field_address[2], - t->a_field_address[3]); + if (t->m_field != MANUFACTURER_KAM || + t->a_field_address[3] != id_[3] || + t->a_field_address[2] != id_[2] || + t->a_field_address[1] != id_[1] || + t->a_field_address[0] != id_[0]) + { + // This telegram is not intended for this meter. return; } + verbose("(multical21) %s %02x%02x%02x%02x ", + name_.c_str(), + t->a_field_address[0], t->a_field_address[1], t->a_field_address[2], + t->a_field_address[3]); + // This is part of the wmbus protocol, should be moved to wmbus source files! int cc_field = t->payload[0]; - verbose("CC field=%02x ( ", cc_field); + verbose("CC-field=%02x ( ", cc_field); if (cc_field & CC_B_BIDIRECTIONAL_BIT) verbose("bidir "); if (cc_field & CC_RD_RESPONSE_DELAY_BIT) verbose("fast_res "); else verbose("slow_res "); if (cc_field & CC_S_SYNCH_FRAME_BIT) verbose("synch "); if (cc_field & CC_R_RELAYED_BIT) verbose("relayed "); // Relayed by a repeater if (cc_field & CC_P_HIGH_PRIO_BIT) verbose("prio "); - verbose(")\n"); - + verbose(") "); + int acc = t->payload[1]; - verbose("ACC field=%02x\n", acc); - + verbose("ACC-field=%02x ", acc); + uchar sn[4]; sn[0] = t->payload[2]; sn[1] = t->payload[3]; @@ -232,16 +223,19 @@ void MeterMultical21::handleTelegram(Telegram *t) { sn[3] = t->payload[5]; verbose("SN=%02x%02x%02x%02x encrypted=", sn[3], sn[2], sn[1], sn[0]); - if ((sn[3] & SN_ENC_BITS) == 0) verbose("no\n"); - else if ((sn[3] & SN_ENC_BITS) == 0x40) verbose("yes\n"); + // Here is a bug, since it always reports no encryption, but the multicals21 + // so far have all have encryption enabled. + if ((sn[3] & SN_ENC_BITS) == 0) verbose("no"); + else if ((sn[3] & SN_ENC_BITS) == 0x40) verbose("yes"); else verbose("? %d\n", sn[3] & SN_ENC_BITS); + verbose("\n"); // The content begins with the Payload CRC at offset 6. vector content; content.insert(content.end(), t->payload.begin()+6, t->payload.end()); size_t remaining = content.size(); if (remaining > 16) remaining = 16; - + uchar iv[16]; int i=0; // M-field @@ -257,26 +251,22 @@ void MeterMultical21::handleTelegram(Telegram *t) { // BC iv[i++] = 0; - if (use_aes_) { vector ivv(iv, iv+16); - verbose("Decrypting\n"); - string s = bin2hex(ivv); - verbose("IV %s\n", s.c_str()); + debug("(multical21) IV %s\n", s.c_str()); uchar xordata[16]; AES_ECB_encrypt(iv, &key_[0], xordata, 16); - + uchar decrypt[16]; xorit(xordata, &content[0], decrypt, remaining); - + vector dec(decrypt, decrypt+remaining); - string answer = bin2hex(dec); - verbose("Decrypted >%s<\n", answer.c_str()); - + debugPayload("(multical21) decrypted", dec); + if (content.size() > 22) { - fprintf(stderr, "Received too many bytes of content from a Multical21 meter!\n" + warning("(multical21) warning: Received too many bytes of content! " "Got %zu bytes, expected at most 22.\n", content.size()); } if (content.size() > 16) { @@ -284,19 +274,18 @@ void MeterMultical21::handleTelegram(Telegram *t) { // So a second block should enough for everyone! remaining = content.size()-16; if (remaining > 16) remaining = 16; // Should not happen. - + incrementIV(iv, sizeof(iv)); vector ivv2(iv, iv+16); string s2 = bin2hex(ivv2); - verbose("IV+1 %s\n", s2.c_str()); - + debug("(multical21) IV+1 %s\n", s2.c_str()); + AES_ECB_encrypt(iv, &key_[0], xordata, 16); - + xorit(xordata, &content[16], decrypt, remaining); - + vector dec2(decrypt, decrypt+remaining); - string answer2 = bin2hex(dec2); - verbose("Decrypted second block >%s<\n", answer2.c_str()); + debugPayload("(multical21) decrypted", dec2); // Append the second decrypted block to the first. dec.insert(dec.end(), dec2.begin(), dec2.end()); @@ -319,23 +308,23 @@ float getScaleFactor(int vif) { case 0x15: return 10.0; case 0x16: return 10.0; } - fprintf(stderr, "Warning! Unknown vif code %d for scale factor.\n", vif); + warning("(multical21) warning: Unknown vif code %d for scale factor, using 1000.0 instead.\n", vif); return 1000.0; } void MeterMultical21::processContent(vector &c) { int crc0 = c[0]; - int crc1 = c[1]; + int crc1 = c[1]; int frame_type = c[2]; - verbose("CRC16: %02x%02x\n", crc1, crc0); + verbose("(multical21) CRC16: %02x%02x\n", crc1, crc0); /* uint16_t crc = crc16(&(c[2]), c.size()-2); - verbose("CRC16 calc: %04x\n", crc); + verbose("(multical21) CRC16 calc: %04x\n", crc); */ if (frame_type == 0x79) { - verbose("Short frame %d bytes\n", c.size()); + verbose("(multical21) Short frame %d bytes\n", c.size()); if (c.size() != 15) { - fprintf(stderr, "Warning! Unexpected length of frame %zu. Expected 15 bytes!\n", c.size()); + warning("(multical21) warning: Unexpected length of frame %zu. Expected 15 bytes!\n", c.size()); } /*int ecrc0 = c[3]; int ecrc1 = c[4]; @@ -351,11 +340,11 @@ void MeterMultical21::processContent(vector &c) { int rec3val1 = c[14]; info_codes_ = rec1val1*256+rec1val0; - verbose("short rec1 %02x %02x info codes\n", rec1val1, rec1val0); + verbose("(multical21) short rec1 %02x %02x info codes\n", rec1val1, rec1val0); int consumption_raw = rec2val3*256*256*256 + rec2val2*256*256 + rec2val1*256 + rec2val0; - verbose("short rec2 %02x %02x %02x %02x = %d total consumption\n", rec2val3, rec2val2, rec2val1, rec2val0, consumption_raw); - + verbose("(multical21) short rec2 %02x %02x %02x %02x = %d total consumption\n", rec2val3, rec2val2, rec2val1, rec2val0, consumption_raw); + // The dif=0x04 vif=0x13 means current volume with scale factor .001 total_water_consumption_ = ((float)consumption_raw) / ((float)1000); has_total_water_consumption_ = true; @@ -363,31 +352,31 @@ void MeterMultical21::processContent(vector &c) { // The short frame target volume supplies two low bytes, // the remaining two hi bytes are >>probably<< picked from rec2! int target_volume_raw = rec2val3*256*256*256 + rec2val2*256*256 + rec3val1*256 + rec3val0; - verbose("short rec3 (%02x %02x) %02x %02x = %d target volume\n", rec2val3, rec2val2, rec3val1, rec3val0, target_volume_raw); + verbose("(multical21) short rec3 (%02x %02x) %02x %02x = %d target volume\n", rec2val3, rec2val2, rec3val1, rec3val0, target_volume_raw); target_volume_ = ((float)target_volume_raw) / ((float)1000); has_target_volume_ = true; - + } else if (frame_type == 0x78) { - verbose("Full frame %d bytes\n", c.size()); + verbose("(multical21) Full frame %d bytes\n", c.size()); if (c.size() != 22) { - fprintf(stderr, "Warning! Unexpected length of frame %zu. Expected 22 bytes!\n", c.size()); + warning("(multical21) warning: Unexpected length of frame %zu. Expected 22 bytes!\n", c.size()); } - int rec1dif = c[3]; + int rec1dif = c[3]; int rec1vif = c[4]; int rec1vife = c[5]; int rec1val0 = c[6]; int rec1val1 = c[7]; - int rec2dif = c[8]; + int rec2dif = c[8]; int rec2vif = c[9]; int rec2val0 = c[10]; int rec2val1 = c[11]; int rec2val2 = c[12]; int rec2val3 = c[13]; - int rec3dif = c[14]; + int rec3dif = c[14]; int rec3vif = c[15]; int rec3val0 = c[16]; int rec3val1 = c[17]; @@ -399,41 +388,41 @@ void MeterMultical21::processContent(vector &c) { int rec4val1 = c[21]; if (rec1dif != 0x02 || rec1vif != 0xff || rec1vife != 0x20 ) { - fprintf(stderr, "Warning unexpected field! Expected info codes\n" + warning("(multical21) warning: Unexpected field! Expected info codes\n" "with dif=0x02 vif=0xff vife=0x20 but got dif=%02x vif=%02x vife=%02x\n", rec1dif, rec1vif, rec1vife); } info_codes_ = rec1val1*256+rec1val0; - verbose("full rec1 dif=%02x vif=%02x vife=%02x\n", rec1dif, rec1vif, rec1vife); - verbose("full rec1 %02x %02x info codes\n", rec1val1, rec1val0); + verbose("(multical21) full rec1 dif=%02x vif=%02x vife=%02x\n", rec1dif, rec1vif, rec1vife); + verbose("(multical21) full rec1 %02x %02x info codes\n", rec1val1, rec1val0); if (rec2dif != 0x04 || rec2vif != 0x13) { - fprintf(stderr, "Warning unexpected field! Expected current volume\n" + warning("(multical21) warning: Unexpected field! Expected current volume\n" "with dif=0x04 vif=0x13 but got dif=%02x vif=%02x\n", rec2dif, rec2vif); } int consumption_raw = rec2val3*256*256*256 + rec2val2*256*256 + rec2val1*256 + rec2val0; - verbose("full rec2 dif=%02x vif=%02x\n", rec2dif, rec2vif); - verbose("full rec2 %02x %02x %02x %02x = %d total consumption\n", rec2val3, rec2val2, rec2val1, rec2val0, consumption_raw); + verbose("(multical21) full rec2 dif=%02x vif=%02x\n", rec2dif, rec2vif); + verbose("(multical21) full rec2 %02x %02x %02x %02x = %d total consumption\n", rec2val3, rec2val2, rec2val1, rec2val0, consumption_raw); // The dif=0x04 vif=0x13 means current volume with scale factor .001 - total_water_consumption_ = ((float)consumption_raw) / ((float)1000); + total_water_consumption_ = ((float)consumption_raw) / ((float)1000); has_total_water_consumption_ = true; - + if (rec3dif != 0x44 || rec3vif != 0x13) { - fprintf(stderr, "Warning unexpecte field! Expected target volume (ie volume recorded on first day of month)\n" + warning("(multical21) warning: Unexpected field! Expected target volume (ie volume recorded on first day of month)\n" "with dif=0x44 vif=0x13 but got dif=%02x vif=%02x\n", rec3dif, rec3vif); } int target_volume_raw = rec3val3*256*256*256 + rec3val2*256*256 + rec3val1*256 + rec3val0; - verbose("full rec3 dif=%02x vif=%02x\n", rec3dif, rec3vif); - verbose("full rec3 %02x %02x %02x %02x = %d target volume\n", rec3val3, rec3val2, rec3val1, rec3val0, target_volume_raw); + verbose("(multical21) full rec3 dif=%02x vif=%02x\n", rec3dif, rec3vif); + verbose("(multical21) full rec3 %02x %02x %02x %02x = %d target volume\n", rec3val3, rec3val2, rec3val1, rec3val0, target_volume_raw); target_volume_ = ((float)target_volume_raw) / ((float)1000); has_target_volume_ = true; - + // To unknown bytes, seems to be very constant. - verbose("full rec4 %02x %02x = unknown\n", rec4val1, rec4val0); + verbose("(multical21) full rec4 %02x %02x = unknown\n", rec4val1, rec4val0); } else { - fprintf(stderr, "Unknown frame %02x\n", frame_type); + warning("(multical21) warning: Unknown frame %02x\n", frame_type); } } @@ -528,7 +517,7 @@ string MeterMultical21::statusHumanReadable() { string MeterMultical21::decodeTime(int time) { if (time>7) { - fprintf(stderr, "Error when decoding time %d\n", time); + warning("(multical21) warning: Cannot decode time %d should be 0-7.\n", time); } switch (time) { case 0: diff --git a/serial.cc b/serial.cc index 20de22a..9d91856 100644 --- a/serial.cc +++ b/serial.cc @@ -1,15 +1,15 @@ // Copyright (c) 2017 Fredrik Öhrström -// +// // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: -// +// // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. -// +// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -21,10 +21,12 @@ #include"util.h" #include"serial.h" +#include #include #include #include #include +#include #include #include #include @@ -42,13 +44,14 @@ struct SerialCommunicationManagerImp : public SerialCommunicationManager { void stop(); void waitForStop(); bool isRunning(); - + void opened(SerialDeviceTTY *sd); - + void closed(SerialDeviceTTY *sd); + private: void *eventLoop(); static void *startLoop(void *); - + bool running_; pthread_t thread_; int max_fd_; @@ -64,21 +67,21 @@ struct SerialDeviceImp : public SerialDevice { struct SerialDeviceTTY : public SerialDeviceImp { SerialDeviceTTY(string device, int baud_rate, SerialCommunicationManagerImp *manager); - - bool open(); + + bool open(bool fail_if_not_ok); void close(); bool send(vector &data); int receive(vector *data); int fd() { return fd_; } SerialCommunicationManager *manager() { return manager_; } - + private: string device_; int baud_rate_; int fd_; pthread_mutex_t write_lock_ = PTHREAD_MUTEX_INITIALIZER; - pthread_mutex_t read_lock_ = PTHREAD_MUTEX_INITIALIZER; + pthread_mutex_t read_lock_ = PTHREAD_MUTEX_INITIALIZER; SerialCommunicationManagerImp *manager_; }; @@ -89,23 +92,36 @@ SerialDeviceTTY::SerialDeviceTTY(string device, int baud_rate, manager_ = manager; } -bool SerialDeviceTTY::open() { +bool SerialDeviceTTY::open(bool fail_if_not_ok) +{ + bool ok = checkCharacterDeviceExists(device_.c_str(), fail_if_not_ok); + if (!ok) return false; fd_ = openSerialTTY(device_.c_str(), baud_rate_); if (fd_ == -1) { - error("Could not open %s with %d baud N81\n", device_.c_str(), baud_rate_); + if (fail_if_not_ok) { + error("Could not open %s with %d baud N81\n", device_.c_str(), baud_rate_); + } else { + return false; + } } manager_->opened(this); + verbose("(serial) opened %s\n", device_.c_str()); return true; } -void SerialDeviceTTY::close() { +void SerialDeviceTTY::close() +{ + ::flock(fd_, LOCK_UN); ::close(fd_); + fd_ = -1; + manager_->closed(this); + verbose("(serial) closed %s\n", device_.c_str()); } bool SerialDeviceTTY::send(vector &data) { if (data.size() == 0) return true; - + pthread_mutex_lock(&write_lock_); bool rc = true; @@ -120,11 +136,16 @@ bool SerialDeviceTTY::send(vector &data) goto end; } if (written == n) break; - } + } + + if (isDebugEnabled()) { + string msg = bin2hex(data); + debug("(serial %s) sent \"%s\"\n", device_.c_str(), msg.c_str()); + } end: pthread_mutex_unlock(&write_lock_); - return rc; + return rc; } int SerialDeviceTTY::receive(vector *data) @@ -139,7 +160,7 @@ int SerialDeviceTTY::receive(vector *data) if (!available) goto end; data->resize(available); - + while (true) { int nr = read(fd_, &((*data)[num_read]), available-num_read); if (nr > 0) num_read += nr; @@ -148,11 +169,15 @@ int SerialDeviceTTY::receive(vector *data) goto end; } if (num_read == available) break; - } + } + if (isDebugEnabled()) { + string msg = bin2hex(*data); + debug("(serial %s) received \"%s\"\n", device_.c_str(), msg.c_str()); + } end: pthread_mutex_unlock(&read_lock_); - return num_read; + return num_read; } SerialCommunicationManagerImp::SerialCommunicationManagerImp() @@ -165,8 +190,8 @@ SerialCommunicationManagerImp::SerialCommunicationManagerImp() void *SerialCommunicationManagerImp::startLoop(void *a) { auto t = (SerialCommunicationManagerImp*)a; - return t->eventLoop(); -} + return t->eventLoop(); +} SerialDevice *SerialCommunicationManagerImp::createSerialDeviceTTY(string device, int baud_rate) { SerialDevice *sd = new SerialDeviceTTY(device, baud_rate, this); @@ -188,6 +213,9 @@ void SerialCommunicationManagerImp::waitForStop() while (running_) { usleep(1000*1000);} pthread_kill(thread_, SIGUSR1); pthread_join(thread_, NULL); + for (SerialDevice *d : devices_) { + d->close(); + } } bool SerialCommunicationManagerImp::isRunning() @@ -201,22 +229,35 @@ void SerialCommunicationManagerImp::opened(SerialDeviceTTY *sd) { pthread_kill(thread_, SIGUSR1); } +void SerialCommunicationManagerImp::closed(SerialDeviceTTY *sd) { + auto p = find(devices_.begin(), devices_.end(), sd); + if (p != devices_.end()) { + devices_.erase(p); + } + max_fd_ = 0; + for (SerialDevice *d : devices_) { + if (d->fd() > max_fd_) { + max_fd_ = d->fd(); + } + } +} + void *SerialCommunicationManagerImp::eventLoop() { fd_set readfds; while (running_) { FD_ZERO(&readfds); - for (SerialDevice *d : devices_) { + for (SerialDevice *d : devices_) { FD_SET(d->fd(), &readfds); } int activity = select(max_fd_+1 , &readfds , NULL , NULL , NULL); if (!running_) break; if (activity < 0 && errno!=EINTR) { - error("Error after select! errno=%d\n", errno); + error("(serial) internal error after select! errno=%d\n", errno); } if (activity > 0) { - for (SerialDevice *d : devices_) { + for (SerialDevice *d : devices_) { if (FD_ISSET(d->fd(), &readfds)) { SerialDeviceImp *si = dynamic_cast(d); if (si->on_data_) si->on_data_(); @@ -224,7 +265,7 @@ void *SerialCommunicationManagerImp::eventLoop() { } } } - verbose("Event loop stopped!\n"); + verbose("(serial) event loop stopped!\n"); return NULL; } @@ -233,57 +274,64 @@ SerialCommunicationManager *createSerialCommunicationManager() return new SerialCommunicationManagerImp(); } +static int openSerialTTY(const char *tty, int baud_rate) +{ + int rc = 0; + speed_t speed = 0; + struct termios tios; -static int openSerialTTY(const char *tty, int baud_rate) { - int rc = 0; - speed_t speed = 0; - struct termios tios; - int fd = open(tty, O_RDWR | O_NOCTTY | O_NDELAY | O_EXCL); - if (fd == -1) { - usleep(1000*1000); - fd = open(tty, O_RDWR | O_NOCTTY | O_NDELAY | O_EXCL); - if (fd == -1) goto err; - } + int fd = open(tty, O_RDWR | O_NOCTTY | O_NDELAY); + if (fd == -1) { + usleep(1000*1000); + fd = open(tty, O_RDWR | O_NOCTTY | O_NDELAY); + if (fd == -1) goto err; + } + rc = flock(fd, LOCK_EX | LOCK_NB); + if (rc == -1) { + // It is already locked by another wmbusmeter process. + warning("Device %s is already in use and locked.\n", tty); + goto err; + } - switch (baud_rate) { - case 9600: speed = B9600; break; - case 19200: speed = B19200; break; - case 38400: speed = B38400; break; - case 57600: speed = B57600; break; - case 115200: speed = B115200;break; - default: - goto err; - } - - memset(&tios, 0, sizeof(tios)); + switch (baud_rate) { + case 9600: speed = B9600; break; + case 19200: speed = B19200; break; + case 38400: speed = B38400; break; + case 57600: speed = B57600; break; + case 115200: speed = B115200;break; + default: + goto err; + } - rc = cfsetispeed(&tios, speed); - if (rc < 0) goto err; - rc = cfsetospeed(&tios, speed); - if (rc < 0) goto err; + memset(&tios, 0, sizeof(tios)); + + rc = cfsetispeed(&tios, speed); + if (rc < 0) goto err; + rc = cfsetospeed(&tios, speed); + if (rc < 0) goto err; - tios.c_cflag |= (CREAD | CLOCAL); - tios.c_cflag &= ~CSIZE; - tios.c_cflag |= CS8; - tios.c_cflag &=~ CSTOPB; - tios.c_cflag &=~ PARENB; - - tios.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); - tios.c_iflag &= ~INPCK; - - tios.c_iflag &= ~(IXON | IXOFF | IXANY); - - tios.c_oflag &=~ OPOST; - tios.c_cc[VMIN] = 0; - tios.c_cc[VTIME] = 0; - - rc = tcsetattr(fd, TCSANOW, &tios); - if (rc < 0) goto err; + tios.c_cflag |= (CREAD | CLOCAL); + tios.c_cflag &= ~CSIZE; + tios.c_cflag |= CS8; + tios.c_cflag &=~ CSTOPB; + tios.c_cflag &=~ PARENB; - return fd; + tios.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); + tios.c_iflag &= ~INPCK; - err: - if (fd != -1) close(fd); - return -1; + tios.c_iflag &= ~(IXON | IXOFF | IXANY); + + tios.c_oflag &=~ OPOST; + tios.c_cc[VMIN] = 0; + tios.c_cc[VTIME] = 0; + + rc = tcsetattr(fd, TCSANOW, &tios); + if (rc < 0) goto err; + + return fd; + +err: + if (fd != -1) close(fd); + return -1; } diff --git a/serial.h b/serial.h index 58f3a9f..c4abde9 100644 --- a/serial.h +++ b/serial.h @@ -1,15 +1,15 @@ // Copyright (c) 2017 Fredrik Öhrström -// +// // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: -// +// // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. -// +// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -32,7 +32,7 @@ using namespace std; struct SerialCommunicationManager; struct SerialDevice { - virtual bool open() = 0; + virtual bool open(bool fail_if_not_ok) = 0; virtual void close() = 0; virtual bool send(vector &data) = 0; virtual int receive(vector *data) = 0; diff --git a/util.cc b/util.cc index 71d0c17..4f0f826 100644 --- a/util.cc +++ b/util.cc @@ -1,15 +1,15 @@ // Copyright (c) 2017 Fredrik Öhrström -// +// // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: -// +// // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. -// +// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -25,6 +25,7 @@ #include #include #include +#include using namespace std; @@ -42,7 +43,7 @@ void onExit(function cb) { exit_handler = cb; struct sigaction new_action, old_action; - + new_action.sa_handler = exitHandler; sigemptyset (&new_action.sa_mask); new_action.sa_flags = 0; @@ -117,12 +118,40 @@ void error(const char* fmt, ...) { exit(1); } +bool warning_enabled_ = true; bool verbose_enabled_ = false; +bool debug_enabled_ = false; + +void warningSilenced(bool b) { + warning_enabled_ = !b; +} void verboseEnabled(bool b) { verbose_enabled_ = b; } +void debugEnabled(bool b) { + debug_enabled_ = b; + if (debug_enabled_) verbose_enabled_ = true; +} + +bool isVerboseEnabled() { + return verbose_enabled_; +} + +bool isDebugEnabled() { + return debug_enabled_; +} + +void warning(const char* fmt, ...) { + if (warning_enabled_) { + va_list args; + va_start(args, fmt); + vprintf(fmt, args); + va_end(args); + } +} + void verbose(const char* fmt, ...) { if (verbose_enabled_) { va_list args; @@ -132,6 +161,15 @@ void verbose(const char* fmt, ...) { } } +void debug(const char* fmt, ...) { + if (debug_enabled_) { + va_list args; + va_start(args, fmt); + vprintf(fmt, args); + va_end(args); + } +} + bool isValidId(char *id) { if (strlen(id) == 0) return true; @@ -163,3 +201,34 @@ void incrementIV(uchar *iv, size_t len) { p--; } } + +bool checkCharacterDeviceExists(const char *tty, bool fail_if_not) +{ + struct stat info; + + int rc = stat(tty, &info); + if (rc != 0) { + if (fail_if_not) { + error("Device %s does not exist.\n", tty); + } else { + return false; + } + } + if (!S_ISCHR(info.st_mode)) { + if (fail_if_not) { + error("Device %s is not a character device.\n", tty); + } else { + return false; + } + } + return true; +} + +void debugPayload(string intro, vector &payload) +{ + if (isDebugEnabled()) + { + string msg = bin2hex(payload); + debug("%s payload \"%s\"\n", intro.c_str(), msg.c_str()); + } +} diff --git a/util.h b/util.h index ec88c1b..bfac6ca 100644 --- a/util.h +++ b/util.h @@ -1,15 +1,15 @@ // Copyright (c) 2017 Fredrik Öhrström -// +// // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: -// +// // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. -// +// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -40,13 +40,24 @@ void xorit(uchar *srca, uchar *srcb, uchar *dest, int len); void error(const char* fmt, ...); void verbose(const char* fmt, ...); +void debug(const char* fmt, ...); +void warning(const char* fmt, ...); +void warningSilenced(bool b); void verboseEnabled(bool b); +void debugEnabled(bool b); + +bool isVerboseEnabled(); +bool isDebugEnabled(); + + +void debugPayload(std::string intro, std::vector &payload); bool isValidId(char *id); bool isValidKey(char *key); void incrementIV(uchar *iv, size_t len); -#endif +bool checkCharacterDeviceExists(const char *tty, bool fail_if_not); +#endif diff --git a/wmbus.cc b/wmbus.cc index 59b59aa..779456d 100644 --- a/wmbus.cc +++ b/wmbus.cc @@ -1,15 +1,15 @@ // Copyright (c) 2017 Fredrik Öhrström -// +// // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: -// +// // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. -// +// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -19,6 +19,7 @@ // SOFTWARE. #include"wmbus.h" +#include const char *LinkModeNames[] = { #define X(name) #name , @@ -53,12 +54,26 @@ Initializer::Initializer() { } void Telegram::print() { - printf("Received telegram from id: %02x%02x%02x%02x\n", + printf("Received telegram from: %02x%02x%02x%02x\n", a_field_address[0], a_field_address[1], a_field_address[2], a_field_address[3]); - printf(" manufacturer: %s\n", + printf(" manufacturer: (%s) %s\n", + manufacturerFlag(m_field).c_str(), manufacturer(m_field).c_str()); - printf(" device type: %s\n", - deviceType(m_field, a_field_device_type).c_str()); + printf(" device type: %s\n", + deviceType(m_field, a_field_device_type).c_str()); +} + +void Telegram::verboseFields() { + string man = manufacturerFlag(m_field); + verbose(" %02x%02x%02x%02x C-field=%02x M-field=%04x (%s) A-field-version=%02x A-field-dev-type=%02x (%s) Ci-field=%02x\n", + a_field_address[0], a_field_address[1], a_field_address[2], a_field_address[3], + c_field, + m_field, + man.c_str(), + a_field_version, + a_field_device_type, + deviceType(m_field, a_field_device_type).c_str(), + ci_field); } string manufacturer(int m_field) { @@ -68,6 +83,18 @@ string manufacturer(int m_field) { return "Unknown"; } +string manufacturerFlag(int m_field) { + char a = (m_field/1024)%32+64; + char b = (m_field/32)%32+64; + char c = (m_field)%32+64; + + string flag; + flag += a; + flag += b; + flag += c; + return flag; +} + string deviceType(int m_field, int a_field_device_type) { switch (a_field_device_type) { case 0: return "Other"; @@ -95,3 +122,50 @@ string deviceType(int m_field, int a_field_device_type) { return "Unknown"; } +bool detectIM871A(string device, SerialCommunicationManager *handler); +bool detectAMB8465(string device, SerialCommunicationManager *handler); + +pair detectMBusDevice(string device, SerialCommunicationManager *handler) +{ + // If auto, then assume that uev has been configured with + // with the file: `/etc/udev/rules.d/99-usb-serial.rules` containing + // SUBSYSTEM=="tty", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", SYMLINK+="im871a",MODE="0660", GROUP="yourowngroup" + // SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", SYMLINK+="amb8465",MODE="0660", GROUP="yourowngroup" + + if (device == "auto") + { + if (detectIM871A("/dev/im871a", handler)) + { + return { DEVICE_IM871A, "/dev/im871a" }; + } + if (detectAMB8465("/dev/amb8465", handler)) + { + return { DEVICE_AMB8465, "/dev/amb8465" }; + } + return { DEVICE_UNKNOWN, "" }; + } + + // If not auto, then test the device, is it a character device? + checkCharacterDeviceExists(device.c_str(), true); + + // If im87a is tested first, a delay of 1s must be inserted + // before amb8465 is tested, lest it will not respond properly. + // It really should not matter, but perhaps is the uart of the amber + // confused by the 57600 speed....or maybe there is some other reason. + // Anyway by testing for the amb8465 first, we can immediately continue + // with the test for the im871a, without the need for a 1s delay. + + // Talk amb8465 with it... + // assumes this device is configured for 9600 bps, which seems to be the default. + if (detectAMB8465(device, handler)) + { + return { DEVICE_AMB8465, device }; + } + // Talk im871a with it... + // assumes this device is configured for 57600 bps, which seems to be the default. + if (detectIM871A(device, handler)) + { + return { DEVICE_IM871A, device }; + } + return { DEVICE_UNKNOWN, "" }; +} diff --git a/wmbus.h b/wmbus.h index f63c515..b1d2d53 100644 --- a/wmbus.h +++ b/wmbus.h @@ -1,15 +1,15 @@ // Copyright (c) 2017 Fredrik Öhrström -// +// // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: -// +// // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. -// +// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -27,8 +27,7 @@ #include -#define LIST_OF_LINK_MODES X(S1)X(S1m)X(S2)X(T1)X(T2)X(R2)X(C1a)X(C1b)X(C2a)X(C2b)\ - X(N1A)X(N2A)X(N1B)X(N2B)X(N1C)X(N2C)X(N1D)X(N2D)X(N1E)X(N2E)X(N1F)X(N2F)X(UNKNOWN_LINKMODE) +#define LIST_OF_LINK_MODES X(LinkModeC1)X(UNKNOWN_LINKMODE) enum LinkMode { #define X(name) name, @@ -49,13 +48,13 @@ LIST_OF_LINK_MODES using namespace std; -extern const char *LinkModeNames[]; +//extern const char *LinkModeNames[]; struct Telegram { int c_field; // 1 byte (0x44=telegram, no response expected!) int m_field; // Manufacturer 2 bytes vector a_field; // A field 6 bytes - // The 6 a field bytes are composed of: + // The 6 a field bytes are composed of: vector a_field_address; // Address in BCD = 8 decimal 00000000...99999999 digits. int a_field_version; // 1 byte int a_field_device_type; // 1 byte @@ -67,6 +66,7 @@ struct Telegram { string id() { return bin2hex(a_field_address); } void print(); + void verboseFields(); }; struct WMBus { @@ -78,9 +78,23 @@ struct WMBus { virtual SerialDevice *serial() = 0; }; -WMBus *openIM871A(string device, SerialCommunicationManager *handler); +#define LIST_OF_MBUS_DEVICES X(DEVICE_IM871A)X(DEVICE_AMB8465)X(DEVICE_UNKNOWN) + +enum MBusDeviceType { +#define X(name) name, +LIST_OF_MBUS_DEVICES +#undef X +}; + +// The detect function can be supplied the device "auto" and will try default locations for the device. +// Returned is the type and the found device string. +pair detectMBusDevice(string device, SerialCommunicationManager *manager); + +WMBus *openIM871A(string device, SerialCommunicationManager *manager); +WMBus *openAMB8465(string device, SerialCommunicationManager *manager); string manufacturer(int m_field); +string manufacturerFlag(int m_field); string deviceType(int a_field, int ); #endif diff --git a/wmbus_amb8465.cc b/wmbus_amb8465.cc new file mode 100644 index 0000000..6353142 --- /dev/null +++ b/wmbus_amb8465.cc @@ -0,0 +1,406 @@ +// Copyright (c) 2018 Fredrik Öhrström +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#include"wmbus.h" +#include"wmbus_amb8465.h" +#include"serial.h" + +#include +#include +#include +#include + +using namespace std; + +enum FrameStatus { PartialFrame, FullFrame, ErrorInFrame }; + +struct WMBusAmber : public WMBus { + bool ping(); + uint32_t getDeviceId(); + LinkMode getLinkMode(); + void setLinkMode(LinkMode lm); + void onTelegram(function cb); + + void processSerialData(); + void getConfiguration(); + SerialDevice *serial() { return serial_; } + + WMBusAmber(SerialDevice *serial, SerialCommunicationManager *manager); +private: + SerialDevice *serial_; + SerialCommunicationManager *manager_; + vector read_buffer_; + pthread_mutex_t command_lock_ = PTHREAD_MUTEX_INITIALIZER; + sem_t command_wait_; + int sent_command_; + int received_command_; + LinkMode link_mode_ = UNKNOWN_LINKMODE; + vector received_payload_; + vector> telegram_listeners_; + + void waitForResponse(); + FrameStatus checkAMB8465Frame(vector &data, + size_t *frame_length, int *msgid_out, int *payload_len_out, int *payload_offset); + void handleMessage(int msgid, vector &payload); +}; + +WMBus *openAMB8465(string device, SerialCommunicationManager *manager) +{ + SerialDevice *serial = manager->createSerialDeviceTTY(device.c_str(), 9600); + WMBusAmber *imp = new WMBusAmber(serial, manager); + return imp; +} + +WMBusAmber::WMBusAmber(SerialDevice *serial, SerialCommunicationManager *manager) : + serial_(serial), manager_(manager) +{ + sem_init(&command_wait_, 0, 0); + manager_->listenTo(serial_,call(this,processSerialData)); + serial_->open(true); + +} + +uchar xorChecksum(vector msg, int len) { + uchar c = 0; + for (int i=0; i msg(4); + msg[0] = AMBER_SERIAL_SOF; + msg[1] = DEVMGMT_ID; + msg[2] = DEVMGMT_MSG_PING_REQ; + msg[3] = 0; + + sent_command_ = DEVMGMT_MSG_PING_REQ; + serial()->send(msg); + + waitForResponse(); + */ + pthread_mutex_unlock(&command_lock_); + return true; +} + +uint32_t WMBusAmber::getDeviceId() { + pthread_mutex_lock(&command_lock_); + + vector msg(4); + msg[0] = AMBER_SERIAL_SOF; + msg[1] = CMD_SERIALNO_REQ; + msg[2] = 0; // No payload + msg[3] = xorChecksum(msg, 3); + + assert(msg[3] == 0xf4); + + sent_command_ = CMD_SERIALNO_REQ; + verbose("(amb8465) get device id\n"); + serial()->send(msg); + + waitForResponse(); + + uint32_t id = 0; + if (received_command_ == (CMD_SERIALNO_REQ | 0x80)) { + id = received_payload_[4] << 24 | + received_payload_[5] << 16 | + received_payload_[6] << 8 | + received_payload_[7]; + verbose("(amb8465) device id %08x\n", id); + } + + pthread_mutex_unlock(&command_lock_); + return id; +} + +LinkMode WMBusAmber::getLinkMode() { + // It is not possible to read the volatile mode set using setLinkMode below. + // (It is possible to read the non-volatile settings, but this software + // does not change those.) So we remember the state for the device. + getConfiguration(); + return link_mode_; +} + +void WMBusAmber::getConfiguration() +{ + pthread_mutex_lock(&command_lock_); + + vector msg(6); + msg[0] = AMBER_SERIAL_SOF; + msg[1] = CMD_GET_REQ; + msg[2] = 0x02; + msg[3] = 0x00; + msg[4] = 0x80; + msg[5] = xorChecksum(msg, 5); + + assert(msg[5] == 0x77); + + verbose("(amb8465) get config\n"); + serial()->send(msg); + + waitForResponse(); + + if (received_command_ == (0x80|CMD_GET_REQ)) + { + // These are the non-volatile values stored inside the dongle. + // However the link mode, radio channel etc might not be the one + // that we are actually using! Since setting the link mode + // is possible without changing the non-volatile memory. + // But there seems to be no way of reading out the set link mode....??? + // Ie there is a disconnect between the flash and the actual running dongle. + // Oh well. + // + // These are just some random config settings store in non-volatile memory. + verbose("(amb8465) config: uart %02x\n", received_payload_[2]); + verbose("(amb8465) config: radio Channel %02x\n", received_payload_[60+2]); + verbose("(amb8465) config: rssi enabled %02x\n", received_payload_[69+2]); + verbose("(amb8465) config: mode Preselect %02x\n", received_payload_[70+2]); + } + + pthread_mutex_unlock(&command_lock_); +} + +void WMBusAmber::setLinkMode(LinkMode lm) { + if (lm != LinkModeC1) { + error("LinkMode %d is not implemented\n", (int)lm); + } + + pthread_mutex_lock(&command_lock_); + + vector msg(8); + msg[0] = AMBER_SERIAL_SOF; + msg[1] = CMD_SET_MODE_REQ; + sent_command_ = msg[1]; + msg[2] = 1; // Len + msg[3] = 0x0E; // Reception of C1 and C2 messages + msg[4] = xorChecksum(msg, 4); + + verbose("(amb8465) set link mode %02x\n", msg[3]); + serial()->send(msg); + + waitForResponse(); + link_mode_ = LinkModeC1; + pthread_mutex_unlock(&command_lock_); +} + +void WMBusAmber::onTelegram(function cb) { + telegram_listeners_.push_back(cb); +} + +void WMBusAmber::waitForResponse() { + while (manager_->isRunning()) { + int rc = sem_wait(&command_wait_); + if (rc==0) break; + if (rc==-1) { + if (errno==EINTR) continue; + break; + } + } +} + +FrameStatus WMBusAmber::checkAMB8465Frame(vector &data, + size_t *frame_length, int *msgid_out, int *payload_len_out, int *payload_offset) +{ + if (data.size() == 0) return PartialFrame; + int payload_len = 0; + if (data[0] == 0xff) { + // A command response begins with 0xff + *msgid_out = data[1]; + payload_len = data[2]; + *payload_len_out = payload_len; + *payload_offset = 3; + *frame_length = 3+payload_len+1; // expect device to have checksumbyte at end enabled. + // Check checksum here! + if (data.size() < *frame_length) return PartialFrame; + + return FullFrame; + } + // If it is not a 0xff we assume it is a message beginning with a length. + // There might be a different mode where the data is wrapped in 0xff. But for the moment + // this is what I see. + payload_len = data[0]; + *msgid_out = 0; // 0 is used to signal + *payload_len_out = payload_len; + *payload_offset = 1; + *frame_length = payload_len+1; + if (data.size() < *frame_length) return PartialFrame; + + return FullFrame; +} + +void WMBusAmber::processSerialData() +{ + vector data; + + // Receive and accumulated serial data until a full frame has been received. + serial_->receive(&data); + + read_buffer_.insert(read_buffer_.end(), data.begin(), data.end()); + + size_t frame_length; + int msgid; + int payload_len, payload_offset; + + FrameStatus status = checkAMB8465Frame(read_buffer_, &frame_length, &msgid, &payload_len, &payload_offset); + + if (status == ErrorInFrame) { + verbose("(amb8465) protocol error in message received!\n"); + string msg = bin2hex(read_buffer_); + debug("(amb8465) protocol error \"%s\"\n", msg.c_str()); + read_buffer_.clear(); + } else + if (status == FullFrame) { + + vector payload; + if (payload_len > 0) { + payload.insert(payload.end(), read_buffer_.begin()+payload_offset, read_buffer_.begin()+payload_offset+payload_len); + } + + read_buffer_.erase(read_buffer_.begin(), read_buffer_.begin()+frame_length); + + handleMessage(msgid, payload); + } +} + +void WMBusAmber::handleMessage(int msgid, vector &payload) +{ + switch (msgid) { + case (0): + { + Telegram t; + t.c_field = payload[0]; + t.m_field = payload[2]<<8 | payload[1]; + t.a_field.resize(6); + t.a_field_address.resize(4); + for (int i=0; i<6; ++i) { + t.a_field[i] = payload[3+i]; + if (i<4) { t.a_field_address[i] = payload[3+3-i]; } + } + t.a_field_version = payload[3+4]; + t.a_field_device_type=payload[3+5]; + t.ci_field=payload[9]; + t.payload.clear(); + // TODO! Figure out why there are 4 extra bytes at the end which are not part + // of the message. Is it wmbus crc bytes that the imst dongle removes but + // the amber dongle allows to be visible? Or some other config specific setting? + t.payload.insert(t.payload.end(), payload.begin()+10, payload.end()-4); + verbose("(amb8465) received telegram"); + t.verboseFields(); + debugPayload("(amb8465) telegram", t.payload); + + for (auto f : telegram_listeners_) { + if (f) f(&t); + } + break; + } + case (0x80|CMD_SET_MODE_REQ): + { + verbose("(amb8465) set link mode completed\n"); + received_command_ = msgid; + received_payload_.clear(); + received_payload_.insert(received_payload_.end(), payload.begin(), payload.end()); + debugPayload("(amb8465) set link mode", received_payload_); + sem_post(&command_wait_); + break; + } + case (0x80|CMD_GET_REQ): + { + verbose("(amb8465) get config completed\n"); + received_command_ = msgid; + received_payload_.clear(); + received_payload_.insert(received_payload_.end(), payload.begin(), payload.end()); + debugPayload("(amb8465) get config", received_payload_); + sem_post(&command_wait_); + break; + } + case (0x80|CMD_SERIALNO_REQ): + { + verbose("(amb8465) get device id completed\n"); + received_command_ = msgid; + received_payload_.clear(); + received_payload_.insert(received_payload_.end(), payload.begin(), payload.end()); + debugPayload("(amb8465) get device id", received_payload_); + sem_post(&command_wait_); + break; + } + default: + verbose("(amb8465) unhandled device message %d\n", msgid); + received_payload_.clear(); + received_payload_.insert(received_payload_.end(), payload.begin(), payload.end()); + debugPayload("(amb8465) unknown", received_payload_); + } +} + +bool detectAMB8465(string device, SerialCommunicationManager *manager) +{ + // Talk to the device and expect a very specific answer. + SerialDevice *serial = manager->createSerialDeviceTTY(device.c_str(), 9600); + bool ok = serial->open(false); + if (!ok) return false; + + vector data; + // First clear out any data in the queue. + serial->receive(&data); + data.clear(); + + vector msg(4); + msg[0] = AMBER_SERIAL_SOF; + msg[1] = CMD_SERIALNO_REQ; + msg[2] = 0; // No payload + msg[3] = xorChecksum(msg, 3); + + assert(msg[3] == 0xf4); + + verbose("(amb8465) are you there?\n"); + serial->send(msg); + // Wait for 100ms so that the USB stick have time to prepare a response. + usleep(1000*100); + serial->receive(&data); + int limit = 0; + while (data.size() > 8 && data[0] != 0xff) { + // Eat bytes until a 0xff appears to get in sync with the proper response. + // Extraneous bytes might be due to a partially read telegram. + data.erase(data.begin()); + vector more; + serial->receive(&more); + if (more.size() > 0) { + data.insert(data.end(), more.begin(), more.end()); + } + if (limit++ > 100) break; // Do not wait too long. + } + + serial->close(); + + string sent = bin2hex(msg); + string recv = bin2hex(data); + + if (data.size() < 8 || + data[0] != 0xff || + data[1] != (0x80 | msg[1]) || + data[2] != 0x04 || + data[7] != xorChecksum(data, 7)) { + return false; + } + return true; +} diff --git a/wmbus_amb8465.h b/wmbus_amb8465.h new file mode 100644 index 0000000..b8beb34 --- /dev/null +++ b/wmbus_amb8465.h @@ -0,0 +1,22 @@ +// Defines documented in the Manual for the AMBER wM-Bus Modules Version 2.7 + +#define AMBER_SERIAL_SOF 0xFF + +#define CMD_DATA_REQ 0x00 +#define CMD_DATARETRY_REQ 0x02 +#define CMD_DATA_IND 0x03 +#define CMD_SET_MODE_REQ 0x04 +#define CMD_RESET_REQ 0x05 +#define CMD_SET_CHANNEL_REQ 0x06 +#define CMD_SET_REQ 0x09 +#define CMD_GET_REQ 0x0A +#define CMD_SERIALNO_REQ 0x0B +#define CMD_FWV_REQ 0x0C +#define CMD_RSSI_REQ 0x0D +#define CMD_SETUARTSPEED_REQ 0x10 +#define CMD_FACTORYRESET_REQ 0x11 +#define CMD_DATA_PRELOAD_REQ 0x30 +#define CMD_DATA_CLR_PRELOAD_REQ 0x31 +#define CMD_SET_AES_KEY_REQ 0x50 +#define CMD_CLR_AES_KEY_REQ 0x51 +#define CMD_GET_AES_DEV_REQ 0x52 diff --git a/wmbus_im871a.cc b/wmbus_im871a.cc index fd12b3c..0dc327d 100644 --- a/wmbus_im871a.cc +++ b/wmbus_im871a.cc @@ -1,15 +1,15 @@ // Copyright (c) 2017 Fredrik Öhrström -// +// // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: -// +// // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. -// +// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -25,6 +25,7 @@ #include #include #include +#include using namespace std; @@ -34,10 +35,10 @@ struct WMBusIM871A : public WMBus { bool ping(); uint32_t getDeviceId(); LinkMode getLinkMode(); - void setLinkMode(LinkMode lm); + void setLinkMode(LinkMode lm); void onTelegram(function cb); - void processMessage(); + void processSerialData(); SerialDevice *serial() { return serial_; } WMBusIM871A(SerialDevice *serial, SerialCommunicationManager *manager); @@ -51,10 +52,12 @@ private: int received_command_; vector received_payload_; vector> telegram_listeners_; - + void waitForResponse(); - FrameStatus checkFrame(vector &data, - size_t *frame_length, int *endpoint_out, int *msgid_out, int *payload_len_out); + static FrameStatus checkFrame(vector &data, + size_t *frame_length, int *endpoint_out, int *msgid_out, + int *payload_len_out, int *payload_offset); + friend bool detectIM871A(string device, SerialCommunicationManager *manager); void handleDevMgmt(int msgid, vector &payload); void handleRadioLink(int msgid, vector &payload); void handleRadioLinkTest(int msgid, vector &payload); @@ -72,75 +75,76 @@ WMBusIM871A::WMBusIM871A(SerialDevice *serial, SerialCommunicationManager *manag serial_(serial), manager_(manager) { sem_init(&command_wait_, 0, 0); - manager_->listenTo(serial_,call(this,processMessage)); - serial_->open(); - + manager_->listenTo(serial_,call(this,processSerialData)); + serial_->open(true); } bool WMBusIM871A::ping() { pthread_mutex_lock(&command_lock_); - + vector msg(4); - msg[0] = WMBUS_SERIAL_SOF; + msg[0] = IM871A_SERIAL_SOF; msg[1] = DEVMGMT_ID; msg[2] = DEVMGMT_MSG_PING_REQ; msg[3] = 0; sent_command_ = DEVMGMT_MSG_PING_REQ; + verbose("(im871a) ping\n"); serial()->send(msg); waitForResponse(); - - pthread_mutex_unlock(&command_lock_); + + pthread_mutex_unlock(&command_lock_); return true; } uint32_t WMBusIM871A::getDeviceId() { pthread_mutex_lock(&command_lock_); - + vector msg(4); - msg[0] = WMBUS_SERIAL_SOF; + msg[0] = IM871A_SERIAL_SOF; msg[1] = DEVMGMT_ID; msg[2] = DEVMGMT_MSG_GET_DEVICEINFO_REQ; msg[3] = 0; sent_command_ = DEVMGMT_MSG_GET_DEVICEINFO_REQ; + verbose("(im871a) get device info\n"); serial()->send(msg); waitForResponse(); uint32_t id = 0; if (received_command_ == DEVMGMT_MSG_GET_DEVICEINFO_RSP) { - verbose("Module Type %02x\n", received_payload_[0]); - verbose( "Device Mode %02x\n", received_payload_[1]); - verbose( "Firmware version %02x\n", received_payload_[2]); - verbose( "HCI Protocol version %02x\n", received_payload_[3]); + verbose("(im871a) device info: module Type %02x\n", received_payload_[0]); + verbose("(im871a) device info: device Mode %02x\n", received_payload_[1]); + verbose("(im871a) device info: firmware version %02x\n", received_payload_[2]); + verbose("(im871a) device info: hci protocol version %02x\n", received_payload_[3]); id = received_payload_[4] << 24 | received_payload_[5] << 16 | received_payload_[6] << 8 | received_payload_[7]; - verbose( "Device id %08x\n", id); + verbose("(im871a) devince info: id %08x\n", id); } - - pthread_mutex_unlock(&command_lock_); + + pthread_mutex_unlock(&command_lock_); return id; } LinkMode WMBusIM871A::getLinkMode() { pthread_mutex_lock(&command_lock_); - + vector msg(4); - msg[0] = WMBUS_SERIAL_SOF; + msg[0] = IM871A_SERIAL_SOF; msg[1] = DEVMGMT_ID; sent_command_ = msg[2] = DEVMGMT_MSG_GET_CONFIG_REQ; msg[3] = 0; + verbose("(im871a) get config\n"); serial()->send(msg); waitForResponse(); LinkMode lm = UNKNOWN_LINKMODE; if (received_command_ == DEVMGMT_MSG_GET_CONFIG_RSP) { - verbose( "Received config size %zu\n", received_payload_.size()); int iff1 = received_payload_[0]; bool has_device_mode = (iff1&1)==1; bool has_link_mode = (iff1&2)==2; @@ -153,37 +157,42 @@ LinkMode WMBusIM871A::getLinkMode() { int offset = 1; if (has_device_mode) { - verbose( "Device mode %02x\n", received_payload_[offset]); + verbose("(im871a) config: device mode %02x\n", received_payload_[offset]); offset++; } if (has_link_mode) { - verbose( "Link mode %02x\n", received_payload_[offset]); - lm = (LinkMode)(int)received_payload_[offset]; + verbose("(im871a) config: link mode %02x\n", received_payload_[offset]); + if (received_payload_[offset] == im871a_C1a) { + lm = LinkModeC1; + } offset++; } if (has_wmbus_c_field) { - verbose( "WMBus C-field %02x\n", received_payload_[offset]); + verbose("(im871a) config: wmbus c-field %02x\n", received_payload_[offset]); offset++; } if (has_wmbus_man_id) { - verbose( "WMBus Manufacture id %02x%02x\n", received_payload_[offset+1], received_payload_[offset+0]); + int flagid = 256*received_payload_[offset+1] +received_payload_[offset+0]; + string flag = manufacturerFlag(flagid); + verbose("(im871a) config: wmbus mfg id %02x%02x (%s)\n", received_payload_[offset+1], received_payload_[offset+0], + flag.c_str()); offset+=2; } if (has_wmbus_device_id) { - verbose( "WMBus Device id %02x%02x%02x%02x\n", received_payload_[offset+3], received_payload_[offset+2], + verbose("(im871a) config: wmbus device id %02x%02x%02x%02x\n", received_payload_[offset+3], received_payload_[offset+2], received_payload_[offset+1], received_payload_[offset+0]); offset+=4; } if (has_wmbus_version) { - verbose( "WMBus Version %02x\n", received_payload_[offset]); + verbose("(im871a) config: wmbus version %02x\n", received_payload_[offset]); offset++; } if (has_wmbus_device_type) { - verbose( "WMBus Device Type %02x\n", received_payload_[offset]); + verbose("(im871a) config: wmbus device type %02x\n", received_payload_[offset]); offset++; } if (has_radio_channel) { - verbose( "Radio channel %02x\n", received_payload_[offset]); + verbose("(im871a) config: radio channel %02x\n", received_payload_[offset]); offset++; } int iff2 = received_payload_[offset]; @@ -197,61 +206,65 @@ LinkMode WMBusIM871A::getLinkMode() { bool has_led_control = (iff2&64)==64; bool has_rtc_control = (iff2&128)==128; if (has_radio_power_level) { - verbose( "Radio power level %02x\n", received_payload_[offset]); + verbose("(im871a) config: radio power level %02x\n", received_payload_[offset]); offset++; } if (has_radio_data_rate) { - verbose( "Radio data rate %02x\n", received_payload_[offset]); + verbose("(im871a) config: radio data rate %02x\n", received_payload_[offset]); offset++; } if (has_radio_rx_window) { - verbose( "Radio rx window %02x\n", received_payload_[offset]); + verbose("(im871a) config: radio rx window %02x\n", received_payload_[offset]); offset++; } if (has_auto_power_saving) { - verbose( "Auto power saving %02x\n", received_payload_[offset]); + verbose("(im871a) config: auto power saving %02x\n", received_payload_[offset]); offset++; } if (has_auto_rssi_attachment) { - verbose( "Auto RSSI attachment %02x\n", received_payload_[offset]); + verbose("(im871a) config: auto RSSI attachment %02x\n", received_payload_[offset]); offset++; } if (has_auto_rx_timestamp_attachment) { - verbose( "Auto rx timestamp attachment %02x\n", received_payload_[offset]); + verbose("(im871a) config: auto rx timestamp attachment %02x\n", received_payload_[offset]); offset++; } if (has_led_control) { - verbose( "LED control %02x\n", received_payload_[offset]); + verbose("(im871a) config: led control %02x\n", received_payload_[offset]); offset++; } if (has_rtc_control) { - verbose( "RTC control %02x\n", received_payload_[offset]); + verbose("(im871a) config: rtc control %02x\n", received_payload_[offset]); offset++; } } - - pthread_mutex_unlock(&command_lock_); + + pthread_mutex_unlock(&command_lock_); return lm; } -void WMBusIM871A::setLinkMode(LinkMode lm) { +void WMBusIM871A::setLinkMode(LinkMode lm) +{ + if (lm != LinkModeC1) { + error("LinkMode %d is not implemented\n", (int)lm); + } pthread_mutex_lock(&command_lock_); - + vector msg(8); - msg[0] = WMBUS_SERIAL_SOF; + msg[0] = IM871A_SERIAL_SOF; msg[1] = DEVMGMT_ID; sent_command_ = msg[2] = DEVMGMT_MSG_SET_CONFIG_REQ; msg[3] = 3; // Len msg[4] = 0; // Temporary msg[5] = 2; // iff1 bits: Set Radio Mode only - msg[6] = (int)lm; // C1a + msg[6] = (int)im871a_C1a; msg[7] = 0; // iff2 bits: Set nothing - verbose( "SET CONFIG REQ to USB im871a\n\n"); + verbose("(im871a) set link mode %02x\n", msg[6]); serial()->send(msg); waitForResponse(); - pthread_mutex_unlock(&command_lock_); + pthread_mutex_unlock(&command_lock_); } void WMBusIM871A::onTelegram(function cb) { @@ -270,7 +283,8 @@ void WMBusIM871A::waitForResponse() { } FrameStatus WMBusIM871A::checkFrame(vector &data, - size_t *frame_length, int *endpoint_out, int *msgid_out, int *payload_len_out) + size_t *frame_length, int *endpoint_out, int *msgid_out, + int *payload_len_out, int *payload_offset) { if (data.size() == 0) return PartialFrame; if (data[0] != 0xa5) return ErrorInFrame; @@ -278,56 +292,64 @@ FrameStatus WMBusIM871A::checkFrame(vector &data, if (ctrlbits & 1) return ErrorInFrame; // Bit 1 is reserved, we do not expect it.... bool has_timestamp = ((ctrlbits&2)==2); bool has_rssi = ((ctrlbits&4)==4); - bool has_crc16 = ((ctrlbits&8)==8); + bool has_crc16 = ((ctrlbits&8)==8); int endpoint = data[1] & 0x0f; if (endpoint != DEVMGMT_ID && endpoint != RADIOLINK_ID && endpoint != RADIOLINKTEST_ID && endpoint != HWTEST_ID) return ErrorInFrame; *endpoint_out = endpoint; - + int msgid = data[2]; if (endpoint == DEVMGMT_ID && (msgid<1 || msgid>0x27)) return ErrorInFrame; if (endpoint == RADIOLINK_ID && (msgid<1 || msgid>0x05)) return ErrorInFrame; if (endpoint == RADIOLINKTEST_ID && (msgid<1 || msgid>0x07)) return ErrorInFrame; // Are 5 and 6 disallowed? if (endpoint == HWTEST_ID && (msgid<1 || msgid>0x02)) return ErrorInFrame; *msgid_out = msgid; - + int payload_len = data[3]; *payload_len_out = payload_len; - - *frame_length = 4+payload_len+(has_timestamp?4:0)+(has_rssi?1:0)+(has_crc16?2:0); + *payload_offset = 4; + + *frame_length = *payload_offset+payload_len+(has_timestamp?4:0)+(has_rssi?1:0)+(has_crc16?2:0); if (data.size() < *frame_length) return PartialFrame; - + return FullFrame; } -void WMBusIM871A::processMessage() { - +void WMBusIM871A::processSerialData() +{ vector data; + + // Receive and accumulated serial data until a full frame has been received. serial_->receive(&data); + read_buffer_.insert(read_buffer_.end(), data.begin(), data.end()); size_t frame_length; int endpoint; int msgid; - int payload_len; + int payload_len, payload_offset; + + FrameStatus status = checkFrame(read_buffer_, &frame_length, &endpoint, &msgid, &payload_len, &payload_offset); - FrameStatus status = checkFrame(read_buffer_, &frame_length, &endpoint, &msgid, &payload_len); - if (status == ErrorInFrame) { - verbose( "Protocol error in message received from iM871A!\n"); + verbose("(im871a) protocol error in message received!\n"); + string msg = bin2hex(read_buffer_); + debug("(im871a) protocol error \"%s\"\n", msg.c_str()); read_buffer_.clear(); } else if (status == FullFrame) { vector payload; if (payload_len > 0) { - payload.insert(payload.begin(), read_buffer_.begin()+4, read_buffer_.begin()+payload_len); + payload.insert(payload.begin(), read_buffer_.begin()+payload_offset, read_buffer_.begin()+payload_len); } read_buffer_.erase(read_buffer_.begin(), read_buffer_.begin()+frame_length); + // We now have a proper message in payload. Let us trigger actions based on it. + // It can be wmbus receiver-dongle messages or wmbus remote meter messages received over the radio. switch (endpoint) { case DEVMGMT_ID: handleDevMgmt(msgid, payload); break; case RADIOLINK_ID: handleRadioLink(msgid, payload); break; @@ -341,34 +363,34 @@ void WMBusIM871A::handleDevMgmt(int msgid, vector &payload) { switch (msgid) { case DEVMGMT_MSG_PING_RSP: // 0x02 - verbose("Received ping response.\n"); + verbose("(im871a) pong\n"); received_command_ = msgid; sem_post(&command_wait_); break; case DEVMGMT_MSG_SET_CONFIG_RSP: // 0x04 - verbose("Received set device config response.\n"); + verbose("(im871a) set config completed\n"); received_command_ = msgid; received_payload_.clear(); received_payload_.insert(received_payload_.end(), payload.begin(), payload.end()); sem_post(&command_wait_); break; case DEVMGMT_MSG_GET_CONFIG_RSP: // 0x06 - verbose("Received get device config response.\n"); + verbose("(im871a) get config completed\n"); received_command_ = msgid; received_payload_.clear(); received_payload_.insert(received_payload_.end(), payload.begin(), payload.end()); sem_post(&command_wait_); break; case DEVMGMT_MSG_GET_DEVICEINFO_RSP: // 0x10 - verbose("Received device info response.\n"); + verbose("(im871a) device info completed\n"); received_command_ = msgid; received_payload_.clear(); received_payload_.insert(received_payload_.end(), payload.begin(), payload.end()); sem_post(&command_wait_); break; default: - verbose("Unhandled device management message %d\n", msgid); - } + verbose("(im871a) Unhandled device management message %d\n", msgid); + } } void WMBusIM871A::handleRadioLink(int msgid, vector &payload) @@ -377,9 +399,6 @@ void WMBusIM871A::handleRadioLink(int msgid, vector &payload) case RADIOLINK_MSG_WMBUSMSG_IND: // 0x03 { Telegram t; - verbose("Radiolink received (%zu bytes): ", payload.size()); - for (auto c : payload) verbose("%02x ", c); - verbose("\n"); t.c_field = payload[0]; t.m_field = payload[2]<<8 | payload[1]; t.a_field.resize(6); @@ -393,36 +412,74 @@ void WMBusIM871A::handleRadioLink(int msgid, vector &payload) t.ci_field=payload[9]; t.payload.clear(); t.payload.insert(t.payload.end(), payload.begin()+10, payload.end()); - verbose("C field: %02x\n", t.c_field); - verbose("M field: %04x\n", t.m_field); - verbose("A field version: %02x\n", t.a_field_version); - verbose("A field dev type: %02x\n", t.a_field_device_type); - verbose("Ci field: %02x\n", t.ci_field); - + verbose("(im871a) received telegram "); + t.verboseFields(); + debugPayload("(im871a) telegram", t.payload); for (auto f : telegram_listeners_) { if (f) f(&t); } } break; default: - verbose("Unhandled radio link message %d\n", msgid); - } + verbose("(im871a) Unhandled radio link message %d\n", msgid); + } } void WMBusIM871A::handleRadioLinkTest(int msgid, vector &payload) { switch (msgid) { default: - verbose("Unhandled radio link test message %d\n", msgid); - } + verbose("(im871a) Unhandled radio link test message %d\n", msgid); + } } void WMBusIM871A::handleHWTest(int msgid, vector &payload) { switch (msgid) { default: - verbose("Unhandled hw test message %d\n", msgid); - } + verbose("(im871a) Unhandled hw test message %d\n", msgid); + } } +bool detectIM871A(string device, SerialCommunicationManager *manager) +{ + // Talk to the device and expect a very specific answer. + SerialDevice *serial = manager->createSerialDeviceTTY(device.c_str(), 57600); + bool ok = serial->open(false); + if (!ok) return false; + vector data; + // First clear out any data in the queue. + serial->receive(&data); + data.clear(); + + vector msg(4); + msg[0] = IM871A_SERIAL_SOF; + msg[1] = DEVMGMT_ID; + msg[2] = DEVMGMT_MSG_PING_REQ; + msg[3] = 0; + + verbose("(im871a) are you there?\n"); + serial->send(msg); + // Wait for 100ms so that the USB stick have time to prepare a response. + usleep(1000*100); + serial->receive(&data); + + serial->close(); + + string sent = bin2hex(msg); + string recv = bin2hex(data); + + size_t frame_length; + int endpoint, msgid, payload_len, payload_offset; + FrameStatus status = WMBusIM871A::checkFrame(data, + &frame_length, &endpoint, &msgid, + &payload_len, &payload_offset); + if (status != FullFrame || + endpoint != 1 || + msgid != 2) + { + return false; + } + return true; +} diff --git a/wmbus_im871a.h b/wmbus_im871a.h index d905bba..f065f47 100644 --- a/wmbus_im871a.h +++ b/wmbus_im871a.h @@ -1,7 +1,7 @@ // Definitions and source code imported from WMBus_HCI_Spec_V1_6.pdf // Found here: https://wireless-solutions.de/products/gateways/wirelessadapter.html -#define WMBUS_SERIAL_SOF 0xA5 +#define IM871A_SERIAL_SOF 0xA5 #define DEVMGMT_ID 0x01 #define RADIOLINK_ID 0x02 @@ -57,4 +57,11 @@ #define HWTEST_MSG_RADIOTEST_REQ 0x01 #define HWTEST_MSG_RADIOTEST_RSP 0x02 +#define LIST_OF_IM871A_LINK_MODES X(S1)X(S1m)X(S2)X(T1)X(T2)X(R2)X(C1a)X(C1b)X(C2a)X(C2b) \ + X(N1A)X(N2A)X(N1B)X(N2B)X(N1C)X(N2C)X(N1D)X(N2D)X(N1E)X(N2E)X(N1F)X(N2F)X(UNKNOWN_LINKMODE) +enum LinkModeIM871A { +#define X(name) im871a_##name, +LIST_OF_IM871A_LINK_MODES +#undef X +};