diff --git a/Makefile b/Makefile index dab0123..875249b 100644 --- a/Makefile +++ b/Makefile @@ -177,6 +177,7 @@ PROG_OBJS:=\ $(BUILD)/wmbus.o \ $(BUILD)/wmbus_amb8465.o \ $(BUILD)/wmbus_im871a.o \ + $(BUILD)/wmbus_iu891a.o \ $(BUILD)/wmbus_cul.o \ $(BUILD)/wmbus_rtlwmbus.o \ $(BUILD)/wmbus_rtl433.o \ diff --git a/README.md b/README.md index 860a1d4..28e39f7 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ Read the wiki for more info on how to use the snap: https://wmbusmeters.github.i Building and installing from source is easy and recommended since the development progresses quickly. First remove the wmbus dongle -(im871a,amb8465(metis),amb3665,cul,rc1180) or the generic rtlsdr dongle (RTL2832U) +(im871a,iu891a,amb8465(metis),amb3665,cul,rc1180) or the generic rtlsdr dongle (RTL2832U) from your computer. Then do: `./configure; make; sudo make install` will install wmbusmeters as a daemon. @@ -98,7 +98,7 @@ from your computer. Then do: # Usage Check the contents of your `/etc/wmbusmeters.conf` file, assuming it -has `device=auto:t1` and you are using a im871a,amb8465(metis),amb3665,rc1180,cul or rtlsdr device, +has `device=auto:t1` and you are using a im871a,iu891a,amb8465(metis),amb3665,rc1180,cul or rtlsdr device, then you can now start the daemon with `sudo systemctl start wmbusmeters` or you can try it from the command line `wmbusmeters auto:t1` @@ -108,11 +108,11 @@ several dongle types, the scan can take some time! Use `auto` for testing and to find your dongle. For production it is very much recommended that you change `auto:t1` to the device name with the full device path -(eg `/dev/ttyUSB0:im871a:c1,t1`). This will skip the slow probing for all possible +(eg `/dev/ttyAMA0:iu891:c1,t1`). This will skip the slow probing for all possible wmbus dongles when wmbusmeters startup. -If the serial device (ttyUSB0) might change you can also use `device=im871a:c1,t1` -which will probe all serial devices but only scans for im871a which also speeds it up. +If the serial device (ttyUSB0) might change you can also use `device=iu891:c1,t1` +which will probe all serial devices but only scans for im891a which also speeds it up. Note that the rtl-sdr devices are not found under the tty devices (e.g. `/dev/tty...`). Instead the rtl-sdr devices are accessed through character device special files named `/dev/swradio0` to `/dev/swradio255`[^kernel_docs_sdr]. Wmbusmeters uses librtsldr to probe these devices. @@ -139,26 +139,26 @@ When using useconfig, the files/dir should be: `/home/me/.config/wmbusmeters/wmbusmeters.d` Check the config file /etc/wmbusmeters.conf and edit the device. For example: -`/dev/ttyUSB1:amb8465:c1,t1` or `im871a:c1,t1` or `im871a[457200101056]:t1`. +`/dev/ttyUSB1:amb8465:c1,t1` or `iu891:c1,t1` or `iu891a[457200101056]:t1`. -Adding a device like auto or im871a will trigger an automatic probe of all serial ttys -to auto find or to find on which tty the im871a resides. +Adding a device like auto or iu891a will trigger an automatic probe of all serial ttys +to auto find or to find on which tty the iu891a resides. -If you specify a full device path like `/dev/ttyUSB0:im871a:c1` or `rtlwmbus` or `rtl433` +If you specify a full device path like `/dev/ttyUSB0:iu891a:c1` or `rtlwmbus` or `rtl433` then it will not probe the serial devices. If you must be really sure that it will not probe something you can add `donotprobe=/dev/ttyUSB0` or `donotprobe=all`. You can specify combinations like: `device=rc1180:t1` `device=auto:c1` to set the rc1180 dongle to t1 but any other auto-detected dongle to c1. -Some dongles have identifiers (im871a,amb8465(metis),amb3665 and rtlsdrs) (for example: rtlsdr can be set with `rtl_eeprom -s myname`) +Some dongles have identifiers (im871a,iu891a,amb8465(metis),amb3665 and rtlsdrs) (for example: rtlsdr can be set with `rtl_eeprom -s myname`) You might have two rtlsdr dongles, one attached to an antenna tuned to 433MHz and the other attached to an antenna tuned for 868.95MHz, then a more complicated setup could look like this: ``` device=rtlwmbus[555555]:433M device=rtlwmbus[112233] -device=/dev/ttyUSB0:im871a[00102759]:c1,t1 +device=/dev/ttyUSB0:iu891a[00102759]:c1,t1 device=/dev/ttyUSB1:rc1180:t1 ``` @@ -169,9 +169,9 @@ here we pick the bus alias MAIN for the mbus using 2400 bps for all meters on th ``` MAIN=/dev/ttyUSB0:mbus:2400 ``` -and here we pick the bus alias RADIOMAIN for an im871a dongle: +and here we pick the bus alias RADIOMAIN for an iu891a dongle: ``` -RADIOMAIN=/dev/ttyUSB1:im871a:c2 +RADIOMAIN=/dev/ttyUSB1:iu891a:c2 ``` The bus alias is then used in the meter driver specification to specify which @@ -192,7 +192,7 @@ wmbusmeters --pollinterval=60s MAIN=/dev/ttyUSB0:mbus:2400 MyTempMeter piigth:MA loglevel=normal # You can use auto:t1 to find the device you have connected to your system. # But do not use auto here since it will cause unnecessary and slow probing of the serial ports. -device=/dev/ttyUSB0:im871a:c1,t1 +device=/dev/ttyUSB0:iu891a:c1,t1 # And mbus device=MAIN=/dev/ttyUSB1:mbus:2400 # But do not probe this serial tty. @@ -234,7 +234,7 @@ pollinterval=60s You can use `driver=auto` to have wmbusmeters automatically detect and use the best driver for your meter, but you should >not< use auto in production. -You can find out which driver is recommended by running `wmbusmeters im871a:t1`. +You can find out which driver is recommended by running `wmbusmeters iu891a:t1`. This will print information like: ``` Received telegram from: 71727374 @@ -518,20 +518,20 @@ As {options} you can use: As device you can use: -`auto:c1`, to have wmbusmeters probe for devices: im871a, amb8465(metis), amb3665, cul, rc1180 or rtlsdr (spawns rtlwmbus). +`auto:c1`, to have wmbusmeters probe for devices: im871a, iu891a, amb8465(metis), amb3665, cul, rc1180 or rtlsdr (spawns rtlwmbus). -`im871a:c1` to start all connected *im871a* devices in *c1* mode, ignore all other devices. +`iu891a:c1` to start all connected *iu891a* devices in *c1* mode, ignore all other devices. `/dev/ttyUSB1:amb8465:c1` to start only this device on this tty. Do not probe for other devices. -If you have two im871a you can supply both of them with their unique id:s and set different listening modes: -`im871a[12345678]:c1` `im871a[11223344]:t1` +If you have two iu891a you can supply both of them with their unique id:s and set different listening modes: +`iu891a[12345678]:c1` `iu891a[11223344]:t1` You can also specify rtlwmbus and if you set the serial in the rtlsdr dongle using `rtl_eeprom -s 1234` you can also refer to a specific rtlsdr dongle like this `rtlwmbus[1234]`. -`/dev/ttyUSB0:amb8465`, if you have an amb8465(metis) dongle assigned to ttyUSB0. Other suffixes are im871a,cul. +`/dev/ttyUSB0:amb8465`, if you have an amb8465(metis) dongle assigned to ttyUSB0. Other suffixes are iu891a,cul. (Note that a plain `/dev/ttyUSB0` no longer works, you have to specify the device expected on the device.) @@ -615,6 +615,7 @@ As meter quadruples you specify: ``` Supported wmbus dongles: IMST 871a (im871a) +IMST 891a (iu891a) Amber 8465-M/8665-M/8626-M/Metis-II (amb8465) 868MHz Amber 3665-M (amb3665) 169MHz CUL family (cul) @@ -724,6 +725,8 @@ also listen to c1 and t1 telegrams at the same time. If you have the older firmware you can download the upgrader here: https://wireless-solutions.de/downloadfile/wireless-m-bus-software/ +The wmbus dongle iu891a can listen to either s1, c1 or t1 or c1,t1 at the same time. + The amb8465 dongle (new model name is Metis-II) can listen to either s1, c1 or t1. It can also listen to c1 and t1 at the same time. diff --git a/drivers/src/elster.xmq b/drivers/src/elster.xmq index 7b5ccbe..8a349df 100644 --- a/drivers/src/elster.xmq +++ b/drivers/src/elster.xmq @@ -8,18 +8,13 @@ driver { } use = actuality_duration_s field { - name = total - quantity = Volume + name = total + quantity = Volume + info = 'The total water consumption.' match { measurement_type = Instantaneous vif_range = Volume } - about { - de = 'Der Gesamtwasserverbrauch.' - en = 'The total water consumption.' - fr = '''La consommation totale d'eau.''' - sv = 'Den totala vattenförbrukningen.' - } } test { args = 'Gas elster 05105025 NOKEY' diff --git a/src/bus.cc b/src/bus.cc index 66e358a..b467031 100644 --- a/src/bus.cc +++ b/src/bus.cc @@ -200,6 +200,10 @@ shared_ptr BusManager::createWmbusObject(Detected *detected, Configur verbose("(im170a) on %s\n", detected->found_file.c_str()); wmbus = openIM170A(*detected, serial_manager_, serial_override); break; + case DEVICE_IU891A: + verbose("(iu891a) on %s\n", detected->found_file.c_str()); + wmbus = openIU891A(*detected, serial_manager_, serial_override); + break; case DEVICE_AMB8465: verbose("(amb8465) on %s\n", detected->found_file.c_str()); wmbus = openAMB8465(*detected, serial_manager_, serial_override); diff --git a/src/wmbus.cc b/src/wmbus.cc index 60835a6..ddae037 100644 --- a/src/wmbus.cc +++ b/src/wmbus.cc @@ -5547,6 +5547,16 @@ Detected detectBusDeviceOnTTY(string tty, } } + // Talk iu891a with it... + // assumes this device is configured for 115200 bps, which seems to be the default. + if (has_auto || probe_for.count(BusDeviceType::DEVICE_IU891A)) + { + if (detectIU891A(&detected, handler) == AccessCheck::AccessOK) + { + return detected; + } + } + // Talk iu880b with it... // assumes this device is configured for 115200 bps, which seems to be the default. if (has_auto || probe_for.count(BusDeviceType::DEVICE_IU880B)) diff --git a/src/wmbus.h b/src/wmbus.h index 1829b02..6542e3d 100644 --- a/src/wmbus.h +++ b/src/wmbus.h @@ -44,6 +44,7 @@ bool trimCRCsFrameFormatB(std::vector &payload); X(CUL,cul,true,false,detectCUL) \ X(IM871A,im871a,true,false,detectIM871AIM170A) \ X(IM170A,im170a,true,false,detectSKIP) \ + X(IU891A,iu891a,true,false,detectIU891A) \ X(RAWTTY,rawtty,true,false,detectRAWTTY) \ X(HEXTTY,hextty,true,false,detectSKIP) \ X(RC1180,rc1180,true,false,detectRC1180) \ @@ -704,6 +705,9 @@ shared_ptr openIM871A(Detected detected, shared_ptr openIM170A(Detected detected, shared_ptr manager, shared_ptr serial_override); +shared_ptr openIU891A(Detected detected, + shared_ptr manager, + shared_ptr serial_override); shared_ptr openIU880B(Detected detected, shared_ptr manager, shared_ptr serial_override); @@ -800,6 +804,7 @@ AccessCheck detectAMB8465AMB3665(Detected *detected, shared_ptr handler); AccessCheck detectD1TC(Detected *detected, shared_ptr manager); AccessCheck detectIM871AIM170A(Detected *detected, shared_ptr handler); +AccessCheck detectIU891A(Detected *detected, shared_ptr handler); AccessCheck detectIU880B(Detected *detected, shared_ptr handler); AccessCheck detectRAWTTY(Detected *detected, shared_ptr handler); AccessCheck detectMBUS(Detected *detected, shared_ptr handler); diff --git a/src/wmbus_im871a.cc b/src/wmbus_im871a.cc index 78412a8..3132ff7 100644 --- a/src/wmbus_im871a.cc +++ b/src/wmbus_im871a.cc @@ -731,9 +731,9 @@ bool WMBusIM871aIM170A::deviceSetLinkModes(LinkModeSet lms) } FrameStatus WMBusIM871aIM170A::checkIM871AFrame(vector &data, - size_t *frame_length, int *endpoint_out, int *msgid_out, - int *payload_len_out, int *payload_offset, - int *rssi_dbm) + size_t *frame_length, int *endpoint_out, int *msgid_out, + int *payload_len_out, int *payload_offset, + int *rssi_dbm) { if (data.size() == 0) return PartialFrame; diff --git a/src/wmbus_im871a.h b/src/wmbus_im871a.h index e8d53ac..5c91f18 100644 --- a/src/wmbus_im871a.h +++ b/src/wmbus_im871a.h @@ -1,6 +1,6 @@ -// Copyright (C) 2017 Fredrik Öhrström (CC0-1.0) -// Definitions from WMBus_HCI_Spec_V1_6.pdf -// Found here: https://wireless-solutions.de/products/gateways/wirelessadapter.html +// Copyright (C) 2017 Fredrik Öhrström (CC0-1.0) Definitions from +// WMBus_HCI_Spec_V1_6.pdf Found here: +// https://wireless-solutions.de/products/gateways/wirelessadapter.html #define IM871A_SERIAL_SOF 0xA5 diff --git a/src/wmbus_iu891a.cc b/src/wmbus_iu891a.cc new file mode 100644 index 0000000..90ea566 --- /dev/null +++ b/src/wmbus_iu891a.cc @@ -0,0 +1,847 @@ +/* + Copyright (C) 2017-2024 Fredrik Öhrström (gpl-3.0-or-later) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include"wmbus.h" +#include"wmbus_common_implementation.h" +#include"wmbus_utils.h" +#include"wmbus_iu891a.h" +#include"serial.h" +#include"threads.h" + +#include +#include +#include +#include +#include +#include + +using namespace std; + +struct DeviceInfo_IU891A +{ + uchar module_type; // 109=0x6d=iM891A-XL 110=0x6e=iU891A-XL 163=0xa3=iM881A-XL + uint32_t uid; + string uids; + string product_type; + string product_id; + + string str() + { + string s; +/* s += "status="; + string st = tostrprintf( "%02x", status);*/ + //s += st; + s += " type="; + string mt = tostrprintf("%02x", module_type); + if (module_type == 0x6d) s+="im891a "; + else if (module_type == 0x6e) s+="iu891a "; + else if (module_type == 0xa3) s+="im881 "; + else s+="unknown_type("+mt+") "; + + string ss; + strprintf(&ss, "uid=%08x", uid); + return s+ss; + } + + bool decode(vector &bytes) + { + if (bytes.size() < 8) return false; + int i = 0; + module_type = bytes[i++]; + uid = bytes[i+3]<<24|bytes[i+2]<<16|bytes[i+1]<<8|bytes[i]; i+=4; + strprintf(&uids, "%08x", uid); + return true; + } +}; + +struct WMBusAddressInfo_IU891A +{ + uint16_t mfct {}; + uint32_t id {}; + uint8_t version {}; + uint8_t type {}; + + string dongleId() + { + string s; + strprintf(&s, "%08x", id); + return s; + } + + bool decode(vector &bytes) + { + if (bytes.size() < 8) return false; + int i = 0; + mfct = bytes[i] << 8 | bytes[i+1]; + i += 2; + id = bytes[i]<<24|bytes[i+1]<<16|bytes[i+1]<<8|bytes[i+3]; + i+=4; + version = bytes[i++]; + type = bytes[i++]; + + return true; + } + +}; + +struct Config_IU891A +{ + LinkModeSet link_modes; + uint16_t option_bits {}; + uint16_t ui_option_bits {}; + uint16_t led_flash_timing {}; + uint32_t recalibrate_in_ms {}; + + string str() + { + string s; + + if (option_bits & 0x01) s += "RCV_FILTER "; + else s += "RCV_ALL "; + + if (option_bits & 0x02) s += "RCV_IND "; + if (option_bits & 0x04) s += "SND_IND "; + if (option_bits & 0x08) s += "RECALIB "; + + if (ui_option_bits & 0x01) s += "ASSERT_PIN24_ON_TELEGRAM_ARRIVAL "; + if (ui_option_bits & 0x02) s += "PIN24_POLARITY_REVERSED "; + if (ui_option_bits & 0x04) s += "ASSERT_PIN25_ON_TELEGRAM_SENT "; + if (ui_option_bits & 0x08) s += "PIN25_POLARITY_REVERSED "; + + s += tostrprintf("led flash: %d ms ", led_flash_timing); + s += tostrprintf("recalibrate: %d ms ", recalibrate_in_ms); + + s.pop_back(); + return s; + } + + void encode(vector &bytes, uchar lm) + { + bytes.clear(); + bytes.resize(11); + size_t i = 0; + bytes[i++] = lm; + bytes[i++] = option_bits & 0xff; + bytes[i++] = (option_bits >> 8) & 0xff; + bytes[i++] = ui_option_bits & 0xff; + bytes[i++] = (ui_option_bits >> 8) & 0xff; + bytes[i++] = led_flash_timing & 0xff; + bytes[i++] = (led_flash_timing >> 8) & 0xff; + bytes[i++] = recalibrate_in_ms & 0xff; + bytes[i++] = (recalibrate_in_ms >> 8) & 0xff; + bytes[i++] = (recalibrate_in_ms >> 16) & 0xff; + bytes[i++] = (recalibrate_in_ms >> 24) & 0xff; + } + + bool decode(vector &bytes) + { + if (bytes.size() < 11) return false; + size_t i = 0; + uchar c = bytes[i++]; + link_modes.clear(); + if (c == LINK_MODE_OFF) + { + } + else if (c == LINK_MODE_S) + { + link_modes.addLinkMode(LinkMode::S1); + } + else if (c == LINK_MODE_T) + { + link_modes.addLinkMode(LinkMode::T1); + } + else if (c == LINK_MODE_CT) + { + link_modes.addLinkMode(LinkMode::C1).addLinkMode(LinkMode::T1); + } + else if (c == LINK_MODE_C) + { + link_modes.addLinkMode(LinkMode::C1); + } + option_bits = bytes[i+1]<<8 | bytes[i+0]; + i += 2; + ui_option_bits = bytes[i+1]<<8 | bytes[i+0]; + i += 2; + led_flash_timing = bytes[i+1]<<8 | bytes[i+0]; + i += 2; + recalibrate_in_ms = + bytes[i+3]<<24 | bytes[i+2] <<16 | + bytes[i+1]<<8 | bytes[i+0]; + + return true; + } +}; + +const char *toString(ErrorCodeIU891ADevMgmt ec) +{ + switch (ec) + { +#define X(num,text,info) case ErrorCodeIU891ADevMgmt::text: return #info; +LIST_OF_IU891A_DEVMGMT_ERROR_CODES +#undef X + default: return "Unknown"; + } +} + +ErrorCodeIU891ADevMgmt toErrorCodeIU891ADevMgmt(uchar c) +{ + switch (c) + { +#define X(num,text,info) case num: return ErrorCodeIU891ADevMgmt::text; +LIST_OF_IU891A_DEVMGMT_ERROR_CODES +#undef X + default: return ErrorCodeIU891ADevMgmt::UNKNOWN; + } +} + +const char *toString(ErrorCodeIU891AWMBUSGW ec) +{ + switch (ec) + { +#define X(num,text,info) case ErrorCodeIU891AWMBUSGW::text: return #info; +LIST_OF_IU891A_WMBUSGW_ERROR_CODES +#undef X + default: return "Unknown"; + } +} + +ErrorCodeIU891AWMBUSGW toErrorCodeIU891AWMBUSGW(uchar c) +{ + switch (c) + { +#define X(num,text,info) case num: return ErrorCodeIU891AWMBUSGW::text; +LIST_OF_IU891A_WMBUSGW_ERROR_CODES +#undef X + default: return ErrorCodeIU891AWMBUSGW::UNKNOWN; + } +} + +struct WMBusIU891A : public virtual BusDeviceCommonImplementation +{ + bool ping(); + string getDeviceId(); + string getDeviceUniqueId(); + uchar getFirmwareVersion(); + LinkModeSet getLinkModes(); + void deviceReset(); + bool deviceSetLinkModes(LinkModeSet lms); + LinkModeSet supportedLinkModes() + { + return + C1_bit | + C2_bit | + S1_bit | + S1m_bit | + T1_bit | + T2_bit; + } + + int numConcurrentLinkModes() + { + return 2; + } + + bool canSetLinkModes(LinkModeSet lms) + { + if (lms.empty()) return false; + if (!supportedLinkModes().supports(lms)) return false; + // Ok, the supplied link modes are compatible. + // For iu891a + if (2 == countSetBits(lms.asBits()) && + lms.has(LinkMode::C1) && + lms.has(LinkMode::T1)) + { + return true; + } + // Otherwise its a single link mode. + return 1 == countSetBits(lms.asBits()); + } + bool sendTelegram(LinkMode lm, TelegramFormat format, vector &content); + + void processSerialData(); + void simulate() { } + + WMBusIU891A(BusDeviceType type, string alias, shared_ptr serial, shared_ptr manager); + ~WMBusIU891A() { + } + + static FrameStatus checkIU891AFrame(vector &data, + vector &out, + size_t *frame_length, + int *endpoint_id_out, + int *msg_id_out, + int *status_out, + int *rssi_dbm); + + static void extractFrame(vector &payload, int *rssi_dbm, vector *frame); + +private: + + DeviceInfo_IU891A device_info_ {}; + WMBusAddressInfo_IU891A device_wmbus_address_ {}; + Config_IU891A device_config_ {}; + + uchar last_set_link_mode_ { 0x00 }; + + vector read_buffer_; + vector request_; + vector response_; + + bool getDeviceInfo(); + bool loaded_device_info_ {}; + + bool getConfig(); + + friend AccessCheck detectIU891A(Detected *detected, shared_ptr manager); + void handleDevMgmt(int msgid, vector &payload); + void handleWMbusGateway(int msgid, vector &payload); + +}; + +shared_ptr openIU891A(BusDeviceType type, Detected detected, shared_ptr manager, shared_ptr serial_override) +{ + string bus_alias = detected.specified_device.bus_alias; + string device_file = detected.found_file; + assert(device_file != ""); + if (serial_override) + { + WMBusIU891A *imp = new WMBusIU891A(type, bus_alias, serial_override, manager); + imp->markAsNoLongerSerial(); + return shared_ptr(imp); + } + + auto serial = manager->createSerialDeviceTTY(device_file.c_str(), 115200, PARITY::NONE, "iu891a"); + WMBusIU891A *imp = new WMBusIU891A(type, bus_alias, serial, manager); + return shared_ptr(imp); +} + +shared_ptr openIU891A(Detected detected, shared_ptr manager, shared_ptr serial_override) +{ + return openIU891A(BusDeviceType::DEVICE_IU891A, detected, manager, serial_override); +} + +shared_ptr open(Detected detected, shared_ptr manager, shared_ptr serial_override) +{ + return openIU891A(BusDeviceType::DEVICE_IU891A, detected, manager, serial_override); +} + +WMBusIU891A::WMBusIU891A(BusDeviceType type, string alias, shared_ptr serial, shared_ptr manager) : + BusDeviceCommonImplementation(alias, type, manager, serial, true) +{ + reset(); +} + +static void buildRequest(int endpoint_id, int msg_id, vector& body, vector& out) +{ + vector request; + request.push_back(endpoint_id); + request.push_back(msg_id); + request.insert(request.end(), body.begin(), body.end()); + + uint16_t crc = ~crc16_CCITT(&request[0], request.size()); // Safe to use &[0] since request size > 0 + request.push_back(crc & 0xff); + request.push_back(crc >> 8); + + addSlipFraming(request, out); +} + +bool WMBusIU891A::ping() +{ + if (serial()->readonly()) return true; // Feeding from stdin or file. + return true; +} + +string WMBusIU891A::getDeviceId() +{ + if (serial()->readonly()) return "?"; // Feeding from stdin or file. + if (cached_device_id_ != "") return cached_device_id_; + + bool ok = getDeviceInfo(); + if (!ok) return "ERR"; + + ok = getConfig(); + if (!ok) return "ERR"; + + cached_device_id_ = device_wmbus_address_.dongleId(); + + verbose("(iu891a) got device id %s\n", cached_device_id_.c_str()); + + return cached_device_id_; +} + +string WMBusIU891A::getDeviceUniqueId() +{ + if (serial()->readonly()) return "?"; // Feeding from stdin or file. + if (cached_device_unique_id_ != "") return cached_device_unique_id_; + + bool ok = getDeviceInfo(); + if (!ok) return "ERR"; + + cached_device_unique_id_ = tostrprintf("%08x", device_info_.uid); + + verbose("(im871a) got device unique id %s\n", cached_device_unique_id_.c_str()); + + return cached_device_unique_id_; +} + +uchar WMBusIU891A::getFirmwareVersion() +{ + if (serial()->readonly()) return 0x15; // Feeding from stdin or file. + +// bool ok = getDeviceInfo(); + // if (!ok) return 255; + + return 0; // device_info_.firmware_version; +} + +LinkModeSet WMBusIU891A::getLinkModes() +{ + if (serial()->readonly()) { return Any_bit; } // Feeding from stdin or file. + + bool ok = getConfig(); + if (!ok) return LinkModeSet(); + + return device_config_.link_modes; +} + +void WMBusIU891A::deviceReset() +{ + // No device specific settings needed right now. + // The common code in wmbus.cc reset() + // will open the serial device and potentially + // set the link modes properly. +} + +uchar setupIMSTBusDeviceToReceiveTelegrams(LinkModeSet lms) +{ + if (lms.has(LinkMode::C1) && lms.has(LinkMode::T1)) + { + return LINK_MODE_CT; + } + else if (lms.has(LinkMode::C1)) + { + return LINK_MODE_C; + } + else if (lms.has(LinkMode::C2)) + { + return LINK_MODE_C; + } + else if (lms.has(LinkMode::S1)) + { + return LINK_MODE_S; + } + else if (lms.has(LinkMode::S1m)) + { + return LINK_MODE_S; + } + else if (lms.has(LinkMode::T1)) + { + return LINK_MODE_T; + } + else if (lms.has(LinkMode::T2)) + { + return LINK_MODE_T; + } + else + { + return LINK_MODE_C; // Defaults to C + } + + assert(false); + + // Error + return 0xff; +} + + +bool WMBusIU891A::deviceSetLinkModes(LinkModeSet lms) +{ + if (serial()->readonly()) return true; // Feeding from stdin or file. + + if (!canSetLinkModes(lms)) + { + string modes = lms.hr(); + error("(iu871a) setting link mode(s) %s is not supported for iu891a\n", modes.c_str()); + } + + LOCK_WMBUS_EXECUTING_COMMAND(set_link_modes); + + vector body; + vector request; + + device_config_.option_bits &= 0xfffe; // forward all received telegrams to wmbusmeters. + device_config_.option_bits |= 0x0006; // get notified when received and sent. + device_config_.encode(body, setupIMSTBusDeviceToReceiveTelegrams(lms)); + + buildRequest(SAP_WMBUSGW_ID, WMBUSGW_SET_ACTIVE_CONFIGURATION_REQ, body, request); + + verbose("(iu891a) set config\n"); + bool sent = serial()->send(request); + if (!sent) return false; + + bool ok = waitForResponse(WMBUSGW_SET_ACTIVE_CONFIGURATION_RSP); + if (!ok) return false; // timeout + + verbose("(iu871a) set config to set link mode %02x\n", body[0]); + + return true; +} + +void WMBusIU891A::extractFrame(vector &payload, int *rssi_dbm, vector *frame) +{ + if (payload.size() < 10) return; + *rssi_dbm = (int8_t)payload[7]; + frame->clear(); + frame->insert(frame->begin(), payload.begin()+8, payload.end()); +} + +FrameStatus WMBusIU891A::checkIU891AFrame(vector &data, + vector &out, + size_t *frame_length_out, + int *endpoint_id_out, + int *msg_id_out, + int *status_byte_out, + int *rssi_dbm) +{ + vector msg; + + removeSlipFraming(data, frame_length_out, msg); + + if (msg.size() < 5) return PartialFrame; + + *endpoint_id_out = msg[0]; + *msg_id_out = msg[1]; + *status_byte_out = msg[2]; + + uint16_t crc = ~crc16_CCITT(&msg[0], msg.size()-2); + uchar crc_lo = crc & 0xff; + uchar crc_hi = crc >> 8; + + if (msg[msg.size()-2] != crc_lo || msg[msg.size()-1] != crc_hi) + { + debug("(iu880b) bad crc got %02x%02x expected %02x%02x\n", + msg[msg.size()-1], msg[msg.size()-2], crc_hi, crc_lo); + + return ErrorInFrame; + } + + int payload_offset = 3; + int payload_len = msg.size()-2; + out.clear(); + out.insert(out.end(), msg.begin()+payload_offset, msg.begin()+payload_len); + + return FullFrame; +} + +void WMBusIU891A::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_id; + int msg_id; + int status_byte; + int rssi_dbm = 0; + + vector payload; + + for (;;) + { + FrameStatus status = checkIU891AFrame(read_buffer_, + payload, + &frame_length, + &endpoint_id, + &msg_id, + &status_byte, + &rssi_dbm); + + if (status == PartialFrame) + { + if (read_buffer_.size() > 0) + { + debugPayload("(iu891a) partial frame, expecting more.", read_buffer_); + } + break; + } + if (status == ErrorInFrame) + { + debugPayload("(iu891a) bad frame, clearing.", read_buffer_); + read_buffer_.clear(); + break; + } + if (status == FullFrame) + { + 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_id) { + case SAP_DEVMGMT_ID: handleDevMgmt(msg_id, payload); break; + case SAP_WMBUSGW_ID: handleWMbusGateway(msg_id, payload); break; + } + } + } +} + +bool WMBusIU891A::getDeviceInfo() +{ + if (loaded_device_info_) return true; + + LOCK_WMBUS_EXECUTING_COMMAND(get_device_info); + + vector body; + vector request; + + buildRequest(SAP_DEVMGMT_ID, DEVMGMT_MSG_GET_DEVICE_INFO_REQ, body, request); + + verbose("(iu891a) get device info\n"); + bool sent = serial()->send(request); + if (!sent) return false; // tty overridden with stdin/file + + bool ok = waitForResponse(DEVMGMT_MSG_GET_DEVICE_INFO_RSP); + if (!ok) return false; // timeout + + // Now device info response is in response_ vector. + device_info_.decode(response_); + + verbose("(iu891a) device info: %s\n", device_info_.str().c_str()); + + body.clear(); + request.clear(); + buildRequest(SAP_WMBUSGW_ID, WMBUSGW_GET_WMBUS_ADDRESS_REQ, body, request); + + verbose("(iu891a) get wmbus address\n"); + sent = serial()->send(request); + if (!sent) return false; // tty overridden with stdin/file + + ok = waitForResponse(WMBUSGW_GET_WMBUS_ADDRESS_RSP); + if (!ok) return false; // timeout + + // Now device info response is in response_ vector. + device_wmbus_address_.decode(response_); + + loaded_device_info_ = true; + verbose("(iu891a) device info: %s %s\n", device_wmbus_address_.dongleId().c_str(), device_info_.str().c_str()); + + return true; +} + +bool WMBusIU891A::getConfig() +{ + if (serial()->readonly()) return true; + + LOCK_WMBUS_EXECUTING_COMMAND(get_config); + + vector body; + vector request; + + buildRequest(SAP_WMBUSGW_ID, WMBUSGW_GET_ACTIVE_CONFIGURATION_REQ, body, request); + + verbose("(iu891a) get config\n"); + bool sent = serial()->send(request); + if (!sent) return false; // tty overridden with stdin/file + + bool ok = waitForResponse(WMBUSGW_GET_ACTIVE_CONFIGURATION_RSP); + if (!ok) return false; // timeout + + // Now device info response is in response_ vector. + device_config_.decode(response_); + + verbose("(iu891a) config: %s link modes: %s\n", + device_config_.str().c_str(), + device_config_.link_modes.hr().c_str()); + + return true; +} + +bool WMBusIU891A::sendTelegram(LinkMode lm, TelegramFormat format, vector &content) +{ + if (serial()->readonly()) return true; + if (content.size() > 250) return false; + + return false; +} + +AccessCheck detectIU891A(Detected *detected, shared_ptr manager) +{ + assert(detected->found_file != ""); + + // Talk to the device and expect a very specific answer. + auto serial = manager->createSerialDeviceTTY(detected->found_file.c_str(), 115200, PARITY::NONE, "detect iu891a"); + serial->disableCallbacks(); + bool ok = serial->open(false); + if (!ok) + { + verbose("(iu891a) could not open tty %s for detection\n", detected->found_file.c_str()); + return AccessCheck::NoSuchDevice; + } + + vector response; + // First clear out any data in the queue. + serial->receive(&response); + response.clear(); + + vector init; + init.resize(30); + for (int i=0; i<30; ++i) + { + init[i] = 0xc0; + } + // Wake-up the dongle. + serial->send(init); + + vector body; + vector request; + buildRequest(SAP_DEVMGMT_ID, DEVMGMT_MSG_GET_DEVICE_INFO_REQ, body, request); + serial->send(request); + + // Wait for 100ms so that the USB stick have time to prepare a response. + usleep(100*1000); + serial->receive(&response); + + int endpoint_id = 0; + int msg_id = 0; + int status_byte = 0; + size_t frame_length = 0; + int rssi_dbm = 0; + vector payload; + + FrameStatus status = WMBusIU891A::checkIU891AFrame(response, + payload, + &frame_length, + &endpoint_id, + &msg_id, + &status_byte, + &rssi_dbm); + + if (status != FullFrame || + endpoint_id != SAP_DEVMGMT_ID || + msg_id != DEVMGMT_MSG_GET_DEVICE_INFO_RSP) + { + verbose("(iu891a) are you there? no.\n"); + serial->close(); + return AccessCheck::NoProperResponse; + } + + debugPayload("(iu891a) device info response", payload); + + debug("(iu891a) endpoint %02x msg %02x status %02x\n", endpoint_id, msg_id, status_byte); + + DeviceInfo_IU891A di; + di.decode(payload); + + debug("(iu891a) info: %s\n", di.str().c_str()); + + body.clear(); + request.clear(); + buildRequest(SAP_WMBUSGW_ID, WMBUSGW_GET_WMBUS_ADDRESS_REQ, body, request); + serial->send(request); + + // Wait for 100ms so that the USB stick have time to prepare a response. + usleep(100*1000); + serial->receive(&response); + + status = WMBusIU891A::checkIU891AFrame(response, + payload, + &frame_length, + &endpoint_id, + &msg_id, + &status_byte, + &rssi_dbm); + + if (status != FullFrame || + endpoint_id != SAP_WMBUSGW_ID || + msg_id != WMBUSGW_GET_WMBUS_ADDRESS_RSP) + { + verbose("(iu891a) are you there? I thought so, but no.\n"); + serial->close(); + return AccessCheck::NoProperResponse; + } + + debugPayload("(iu891a) wmbus address response", payload); + + WMBusAddressInfo_IU891A wa; + wa.decode(payload); + + serial->close(); + + detected->setAsFound(wa.dongleId(), BusDeviceType::DEVICE_IU891A, 115200, false, detected->specified_device.linkmodes); + + verbose("(iu891a) are you there? yes %s\n", wa.dongleId().c_str()); + + return AccessCheck::AccessOK; +} + +void WMBusIU891A::handleDevMgmt(int msgid, vector &payload) +{ + switch (msgid) { + case DEVMGMT_MSG_PING_RSP: + debug("(iu891a) rsp pong\n"); + break; + case DEVMGMT_MSG_GET_DEVICE_INFO_RSP: + debug("(iu891a) rsp got device info\n"); + break; + case DEVMGMT_MSG_GET_FW_INFO_RSP: + debug("(iu891a) rsp got firmware\n"); + break; + default: + warning("(iu891a) Unhandled device management message %d\n", msgid); + return; + } + response_.clear(); + response_.insert(response_.end(), payload.begin(), payload.end()); + notifyResponseIsHere(msgid); +} + + +void WMBusIU891A::handleWMbusGateway(int msgid, vector &payload) +{ + switch (msgid) { + case WMBUSGW_GET_WMBUS_ADDRESS_RSP: + debug("(iu891a) rsp got wmbus address\n"); + break; + case WMBUSGW_GET_ACTIVE_CONFIGURATION_RSP: + debug("(iu891a) rsp got active config\n"); + break; + case WMBUSGW_SET_ACTIVE_CONFIGURATION_RSP: + debug("(iu891a) rsp set active config\n"); + break; + case WMBUSGW_RX_MESSAGE_IND: + { + // Invoke common telegram reception code in BusDeviceCommonImplementation. + vector frame; + int rssi_dbm; + + extractFrame(payload, &rssi_dbm, &frame); + AboutTelegram about("iu891a["+cached_device_id_+"]", rssi_dbm, FrameType::WMBUS); + handleTelegram(about, frame); + return; + } + break; + default: + warning("(iu891a) Unhandled wmbus gateway message %d\n", msgid); + return; + } + response_.clear(); + response_.insert(response_.end(), payload.begin(), payload.end()); + notifyResponseIsHere(msgid); +} diff --git a/src/wmbus_iu891a.h b/src/wmbus_iu891a.h new file mode 100644 index 0000000..228deb0 --- /dev/null +++ b/src/wmbus_iu891a.h @@ -0,0 +1,152 @@ +// Copyright (C) 2024 Fredrik Öhrström (gpl-3.0-or-later) + +// Service access endpoints inside the iu891a dongle that can receive send messages. +#define SAP_DEVMGMT_ID 0x01 +#define SAP_WMBUSGW_ID 0x09 + +#define LINK_MODE_OFF 0 +#define LINK_MODE_S 1 +#define LINK_MODE_T 2 +#define LINK_MODE_CT 3 +#define LINK_MODE_C 5 +#define LINK_MODE_ENHANCED_T 6 + +#define DEVMGMT_MSG_PING_REQ 0x01 +#define DEVMGMT_MSG_PING_RSP 0x02 +#define DEVMGMT_MSG_GET_DEVICE_INFO_REQ 0x03 +#define DEVMGMT_MSG_GET_DEVICE_INFO_RSP 0x04 +#define DEVMGMT_MSG_GET_FW_INFO_REQ 0x05 +#define DEVMGMT_MSG_GET_FW_INFO_RSP 0x06 +#define DEVMGMT_MSG_RESTART_REQ 0x07 +#define DEVMGMT_MSG_RESTART_RSP 0x08 +#define DEVMGMT_MSG_SET_SYS_OP_MODE_REQ 0x09 +#define DEVMGMT_MSG_SET_SYS_OP_MODE_RSP 0x0a +#define DEVMGMT_MSG_GET_SYS_OP_MODE_REQ 0x0b +#define DEVMGMT_MSG_GET_SYS_OP_MODE_RSP 0x0c +#define DEVMGMT_MSG_SET_SYS_DATE_TIME_REQ 0x0d +#define DEVMGMT_MSG_SET_SYS_DATE_TIME_RSP 0x0e +#define DEVMGMT_MSG_GET_SYS_DATE_TIME_REQ 0x0f +#define DEVMGMT_MSG_GET_SYS_DATE_TIME_RSP 0x10 +#define DEVMGMT_MSG_SET_SYS_OPTIONS_REQ 0xf7 +#define DEVMGMT_MSG_SET_SYS_OPTIONS_RSP 0xf8 +#define DEVMGMT_MSG_GET_SYS_OPTIONS_REQ 0xf9 +#define DEVMGMT_MSG_GET_SYS_OPTIONS_RSP 0xfa + +#define LIST_OF_IU891A_DEVMGMT_ERROR_CODES \ + X(0, OK, "ok") \ + X(1, ERROR, "error") \ + X(2, COMMAND_NOT_SUPPORTED, "command not supported") \ + X(3, WRONG_PARAMETER, "wrong parameter") \ + X(4, WRONG_APPLICATION, "wrong application / device mode") \ + X(5, RESERVED, "reserved") \ + X(6, BUSY, "application / device busy, try later") \ + X(7, WRONG_MSG_LENGTH, "wrong message length") \ + X(8, NVM_WRITE_ERROR, "NVM write error") \ + X(9, NVM_READ_ERROR, "NVM read error ( NVM content is invalid )") \ + X(10, COMMAND_REJECTED, "command rejected") \ + X(11, RESERVED2, "reserved2") \ + X(12, UNEXPECTED_MSG_FORMAT, "unexpected message format") + +enum class ErrorCodeIU891ADevMgmt { +#define X(num,text,info) text, +LIST_OF_IU891A_DEVMGMT_ERROR_CODES +#undef X + UNKNOWN +}; + +const char *toString(ErrorCodeIU891ADevMgmt ec); +ErrorCodeIU891ADevMgmt toErrorCodeIU891ADevMgmt(uchar c); + +#define WMBUSGW_GET_ACTIVE_CONFIGURATION_REQ 0x01 +#define WMBUSGW_GET_ACTIVE_CONFIGURATION_RSP 0x02 +#define WMBUSGW_SET_ACTIVE_CONFIGURATION_REQ 0x03 +#define WMBUSGW_SET_ACTIVE_CONFIGURATION_RSP 0x04 +#define WMBUSGW_GET_DEFAULT_CONFIGURATION_REQ 0x05 +#define WMBUSGW_GET_DEFAULT_CONFIGURATION_RSP 0x06 +#define WMBUSGW_SET_DEFAULT_CONFIGURATION_REQ 0x07 +#define WMBUSGW_SET_DEFAULT_CONFIGURATION_RSP 0x08 +#define WMBUSGW_RESET_DEFAULT_CONFIGURATION_REQ 0x09 +#define WMBUSGW_RESET_DEFAULT_CONFIGURATION_RSP 0x0a + +#define WMBUSGW_CLEAR_WMBUS_DEVICE_LIST_REQ 0x11 +#define WMBUSGW_CLEAR_WMBUS_DEVICE_LIST_RSP 0x12 +#define WMBUSGW_APPEND_WMBUS_DEVICE_LIST_REQ 0x13 +#define WMBUSGW_APPEND_WMBUS_DEVICE_LIST_RSP 0x14 +#define WMBUSGW_READ_WMBUS_DEVICE_LIST_REQ 0x15 +#define WMBUSGW_READ_WMBUS_DEVICE_LIST_RSP 0x16 +#define WMBUSGW_SAVE_WMBUS_DEVICE_LIST_REQ 0x17 +#define WMBUSGW_SAVE_WMBUS_DEVICE_LIST_RSP 0x18 +#define WMBUSGW_LOAD_WMBUS_DEVICE_LIST_REQ 0x19 +#define WMBUSGW_LOAD_WMBUS_DEVICE_LIST_RSP 0x1a + +#define WMBUSGW_RX_MESSAGE_IND 0x20 + +#define WMBUSGW_ENABLE_DISABLE_SCAN_MODE_REQ 0x21 +#define WMBUSGW_ENABLE_DISABLE_SCAN_MODE_RSP 0x22 + +#define WMBUSGW_SCAN_MODE_PACKET_IND 0x24 + +// Only iM881-XL and iM891A-XL +#define WMBUSGW_SEND_MESSAGE_REQ 0x31 +#define WMBUSGW_SEND_MESSAGE_RSP 0x32 + +#define WMBUSGW_MESSAGE_PACKET_TRANSMITTED_IND 0x34 + +// Only iM881-XL and iM891A-XL +#define WMBUSGW_ENCRYPT_SEND_MESSAGE_REQ 0x35 +#define WMBUSGW_ENCRYPT_SEND_MESSAGE_RSP 0x36 + +#define WMBUSGW_ENCRYPTED_MESSAGE_PACKET_TRANSMITTED_IND 0x38 + +// OnlyiU891A-XL +#define WMBUSGW_SEND_PACKET_REQ 0x39 +#define WMBUSGW_SEND_PACKET_RSP 0x3A + +// Only iU891A-XL +#define WMBUSGW_ENCRYPT_SEND_PACKET_REQ 0x3b +#define WMBUSGW_ENCRYPT_SEND_PACKET_RSP 0x3c + +#define WMBUSGW_GET_APPLICATION_STATUS_REQ 0x41 +#define WMBUSGW_GET_APPLICATION_STATUS_RSP 0x42 + +#define WMBUSGW_RESET_APPLICATION_STATUS_REQ 0x43 +#define WMBUSGW_RESET_APPLICATION_STATUS_RSP 0x44 + +// Only iM881A-XL, iM891A-XL +#define WMBUSGW_GET_RADIO_CONTROL_CONFIG_REQ 0x51 +#define WMBUSGW_GET_RADIO_CONTROL_CONFIG_RSP 0x52 +#define WMBUSGW_SET_RADIO_CONTROL_CONFIG_REQ 0x53 +#define WMBUSGW_SET_RADIO_CONTROL_CONFIG_RSP 0x54 + +// Only iU891A-XL +#define WMBUSGW_GET_WMBUS_ADDRESS_REQ 0x81 +#define WMBUSGW_GET_WMBUS_ADDRESS_RSP 0x82 + +#define LIST_OF_IU891A_WMBUSGW_ERROR_CODES \ + X(0, OK, "ok") \ + X(1, ERROR, "error") \ + X(2, COMMAND_NOT_SUPPORTED, "command not supported") \ + X(3, WRONG_PARAMETER, "wrong parameter") \ + X(4, WRONG_APPLICATION_MODE, "wrong application mode") \ + X(5, NO_MORE_DATA, "no more data") \ + X(6, APPLICATION_BUSY, "application busy, try later") \ + X(7, WRONG_MSG_LENGTH, "wrong message length") \ + X(8, NVM_WRITE_ERROR, "NVM write error") \ + X(9, NVM_READ_ERROR, "NVM read error ( NVM content is invalid )") \ + X(10, COMMAND_REJECTED, "command rejected, execution not possible in current application state") \ + X(11, ACCESS_DENIED, "access denied, operation may need another system operation mode") \ + X(12, DATA_TRUNCATED, "data truncated") \ + X(13, UNSUPPORTED_ENCRYPTION_MODE, "unsupported encryption mode") \ + X(14, NO_ENCRYPTION_KEY_FOUND, "no encryption key found for given meter address") \ + X(15, ENCRYPTION_PARAMETER_MISSING, "encryption info / parameter missing") \ + X(16, ENCRYPTION_ERROR, "encryption error") + +enum class ErrorCodeIU891AWMBUSGW { +#define X(num,text,info) text, +LIST_OF_IU891A_WMBUSGW_ERROR_CODES +#undef X + UNKNOWN +}; + +const char *toString(ErrorCodeIU891AWMBUSGW ec); +ErrorCodeIU891AWMBUSGW toErrorCodeIU891AWMBUSGW(uchar c);