/* Copyright (C) 2022 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"lora_iu880b.h" #include"serial.h" #include"threads.h" #include #include #include #include #include #include using namespace std; static void buildRequest(int endpoint_id, int msg_id, vector& body, vector& out); struct DeviceInfo_IU880B { // 0x90 = iM880A (obsolete) 0x92 = iM880A-L (128k) 0x93 = iU880A (128k) 0x98 = iM880B 0x99 = iU880B 0xA0 = iM881A 0xA1 = iU881A uchar module_type {}; uint16_t device_address {}; uchar group_address {}; string uid; string str() { string s; if (module_type == 0x90) s += "im880a "; else if (module_type == 0x92) s += "im880al "; else if (module_type == 0x93) s += "iu880a "; else if (module_type == 0x98) s += "im880b "; else if (module_type == 0x99) s += "iu880b "; else if (module_type == 0xa0) s += "im881a "; else if (module_type == 0xa1) s += "iu881a "; else s += "unknown_type("+to_string(module_type)+") "; s += tostrprintf("address %04x/%02x uid %s", device_address, group_address, uid.c_str()); return s; } bool decode(vector &bytes) { if (bytes.size() < 9) return false; int i = 0; module_type = bytes[i++]; device_address = bytes[i] | bytes[i+1]; i+=2; group_address = bytes[i++]; i++; // skip reserved uid = tostrprintf("%02x%02x%02x%02x", bytes[i+3], bytes[i+2], bytes[i+1], bytes[i]); i+=4; return true; } }; struct Firmware_IU880B { uchar minor {}; uchar major {}; uint16_t build_count {}; string image; string str() { return ""+to_string(major)+"."+to_string(minor)+"."+to_string(build_count)+" "+image; } bool decode(vector &bytes) { if (bytes.size() < 4) return false; int i = 0; minor = bytes[i++]; major = bytes[i++]; build_count = bytes[i] | bytes[i+1]; i+=2; image = string(&bytes[0]+i, &bytes[0]+bytes.size()); // Ptr from &[0] is ok since size is > 0 return true; } }; struct RadioConfig_IU880B { string str() { string s; return s; } bool decode(vector &bytes) { return true; } }; struct LoRaIU880B : public virtual BusDeviceCommonImplementation { bool ping(); string getDeviceId(); string getDeviceUniqueId(); string getFirmwareVersion(); LinkModeSet getLinkModes(); void deviceReset(); bool deviceSetLinkModes(LinkModeSet lms); LinkModeSet supportedLinkModes() { return LORA_bit; } int numConcurrentLinkModes() { return 1; } bool canSetLinkModes(LinkModeSet lms) { if (lms.empty()) return false; if (!supportedLinkModes().supports(lms)) return false; // Otherwise its a single link mode. return 1 == countSetBits(lms.asBits()); } bool sendTelegram(LinkMode lm, TelegramFormat format, vector &content); void processSerialData(); void simulate() { } LoRaIU880B(string alias, shared_ptr serial, shared_ptr manager); ~LoRaIU880B() { } static FrameStatus checkIU880BFrame(vector &data, vector &out, size_t *frame_length, int *endpoint_id_out, int *msg_id_out, int *status_out, int *rssi_dbm); private: vector read_buffer_; vector request_; vector response_; bool getDeviceInfoAndFirmware(); bool loaded_device_info_ {}; DeviceInfo_IU880B device_info_; Firmware_IU880B firmware_; RadioConfig_IU880B radio_config_; friend AccessCheck detectIU880B(Detected *detected, shared_ptr manager); void handleDevMgmt(int msgid, vector &payload); void handleRadioLink(int msgid, vector &payload, int rssi_dbm); void handleRadioLinkTest(int msgid, vector &payload); void handleHWTest(int msgid, vector &payload); }; shared_ptr openIU880B(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) { LoRaIU880B *imp = new LoRaIU880B(bus_alias, serial_override, manager); imp->markAsNoLongerSerial(); return shared_ptr(imp); } auto serial = manager->createSerialDeviceTTY(device_file.c_str(), 115200, PARITY::NONE, "iu880b"); LoRaIU880B *imp = new LoRaIU880B(bus_alias, serial, manager); return shared_ptr(imp); } LoRaIU880B::LoRaIU880B(string alias, shared_ptr serial, shared_ptr manager) : BusDeviceCommonImplementation(alias, BusDeviceType::DEVICE_IU880B, manager, serial, true) { reset(); } bool LoRaIU880B::ping() { /* if (serial()->readonly()) return true; // Feeding from stdin or file. LOCK_WMBUS_EXECUTING_COMMAND(ping); request_.resize(4); request_[0] = IU880B_SERIAL_SOF; request_[1] = DEVMGMT_ID; request_[2] = DEVMGMT_MSG_PING_REQ; request_[3] = 0; verbose("(iu880b) ping\n"); bool sent = serial()->send(request_); if (sent) return waitForResponse(DEVMGMT_MSG_PING_RSP); */ return true; } string LoRaIU880B::getDeviceId() { if (serial()->readonly()) return "?"; // Feeding from stdin or file. if (cached_device_id_ != "") return cached_device_id_; bool ok = getDeviceInfoAndFirmware(); if (!ok) return "ER1R"; cached_device_id_ = device_info_.uid; verbose("(iu880b) got device id %s\n", cached_device_id_.c_str()); return cached_device_id_; } string LoRaIU880B::getDeviceUniqueId() { return getDeviceId(); } string LoRaIU880B::getFirmwareVersion() { if (serial()->readonly()) return "?"; // Feeding from stdin or file. bool ok = getDeviceInfoAndFirmware(); if (!ok) return "ER1R"; return firmware_.str(); } LinkModeSet LoRaIU880B::getLinkModes() { return LORA_bit; } void LoRaIU880B::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. } bool LoRaIU880B::deviceSetLinkModes(LinkModeSet lms) { /* if (serial()->readonly()) return; // Feeding from stdin or file. if (!canSetLinkModes(lms)) { string modes = lms.hr(); error("(iu880b) setting link mode(s) %s is not supported for iu880b\n", modes.c_str()); } LOCK_WMBUS_EXECUTING_COMMAND(set_link_modes); vector init; init.resize(30); for (int i=0; i<30; ++i) { init[i] = 0xc0; } // Wake-up the dongle. serial()->send(init); vector body = { 0x02 }; // 2 means listen to all traffic request_.clear(); buildRequest(DEVMGMT_ID, DEVMGMT_MSG_SET_RADIO_MODE_REQ, body, request_); verbose("(iu880b) set link mode lora listen to all\n"); bool sent = serial()->send(request_); if (!sent) return; // tty overridden with stdin/file bool ok = waitForResponse(DEVMGMT_MSG_SET_RADIO_MODE_RSP); if (!ok) return; // timeout */ return true; } FrameStatus LoRaIU880B::checkIU880BFrame(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 LoRaIU880B::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 = checkIU880BFrame(read_buffer_, payload, &frame_length, &endpoint_id, &msg_id, &status_byte, &rssi_dbm); if (status == PartialFrame) { if (read_buffer_.size() > 0) { debugPayload("(iu880b) partial frame, expecting more.", read_buffer_); } break; } if (status == ErrorInFrame) { debugPayload("(iu880b) 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 DEVMGMT_ID: handleDevMgmt(msg_id, payload); break; case RADIOLINK_ID: handleRadioLink(msg_id, payload, rssi_dbm); break; case HWTEST_ID: handleHWTest(msg_id, payload); break; } } } } void LoRaIU880B::handleDevMgmt(int msgid, vector &payload) { switch (msgid) { case DEVMGMT_MSG_PING_RSP: debug("(iu880b) rsp pong\n"); break; case DEVMGMT_MSG_GET_DEVICE_INFO_RSP: debug("(iu880b) rsp got device info\n"); break; case DEVMGMT_MSG_SET_RADIO_MODE_RSP: debug("(iu880b) rsp set radio mode\n"); break; case DEVMGMT_MSG_GET_RADIO_CONFIG_RSP: debug("(iu880b) rsp got radio config\n"); break; case DEVMGMT_MSG_GET_FW_INFO_RSP: debug("(iu880b) rsp got firmware\n"); break; default: warning("(iu880b) Unhandled device management message %d\n", msgid); return; } response_.clear(); response_.insert(response_.end(), payload.begin(), payload.end()); notifyResponseIsHere(msgid); } void LoRaIU880B::handleRadioLink(int msgid, vector &frame, int rssi_dbm) { } void LoRaIU880B::handleRadioLinkTest(int msgid, vector &payload) { } void LoRaIU880B::handleHWTest(int msgid, vector &payload) { } bool LoRaIU880B::getDeviceInfoAndFirmware() { if (loaded_device_info_) return true; LOCK_WMBUS_EXECUTING_COMMAND(get_device_info); vector empty_body; request_.clear(); buildRequest(DEVMGMT_ID, DEVMGMT_MSG_GET_DEVICE_INFO_REQ, empty_body, request_); verbose("(iu880b) get device info\n"); vector init; init.resize(30); for (int i=0; i<30; ++i) { init[i] = 0xc0; } // Wake-up the dongle. serial()->send(init); 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("(iu880b) device info: %s\n", device_info_.str().c_str()); request_.clear(); buildRequest(DEVMGMT_ID, DEVMGMT_MSG_GET_RADIO_CONFIG_REQ, empty_body, request_); serial()->send(init); sent = serial()->send(request_); if (!sent) return false; // tty overridden with stdin/file sleep(1); response_.clear(); //ok = waitForResponse(DEVMGMT_MSG_GET_RADIO_CONFIG_RSP); //if (!ok) return false; // timeout // Now device info response is in response_ vector. radio_config_.decode(response_); verbose("(iu880b) radio config: %s\n", radio_config_.str().c_str()); request_.clear(); buildRequest(DEVMGMT_ID, DEVMGMT_MSG_GET_FW_INFO_REQ, empty_body, request_); serial()->send(init); sent = serial()->send(request_); if (!sent) return false; // tty overridden with stdin/file ok = waitForResponse(DEVMGMT_MSG_GET_FW_INFO_RSP); if (!ok) return false; // timeout // Now device info response is in response_ vector. firmware_.decode(response_); verbose("(iu880b) firmware: %s\n", firmware_.str().c_str()); loaded_device_info_ = true; return true; } bool LoRaIU880B::sendTelegram(LinkMode lm, TelegramFormat format, vector &content) { return false; } 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); } AccessCheck detectIU880B(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 iu880b"); serial->disableCallbacks(); bool ok = serial->open(false); if (!ok) { verbose("(iu880b) 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(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 = LoRaIU880B::checkIU880BFrame(response, payload, &frame_length, &endpoint_id, &msg_id, &status_byte, &rssi_dbm); if (status != FullFrame || endpoint_id != DEVMGMT_ID || msg_id != DEVMGMT_MSG_GET_DEVICE_INFO_RSP) { verbose("(iu880b) are you there? no.\n"); serial->close(); return AccessCheck::NoProperResponse; } serial->close(); debugPayload("(iu880b) device info response", payload); debug("(iu880b) endpoint %02x msg %02x status %02x\n", endpoint_id, msg_id, status_byte); DeviceInfo_IU880B di; di.decode(payload); debug("(iu880b) info: %s\n", di.str().c_str()); detected->setAsFound(di.uid, BusDeviceType::DEVICE_IU880B, 115200, false, detected->specified_device.linkmodes); verbose("(iu880b) are you there? yes %s\n", di.uid.c_str()); return AccessCheck::AccessOK; }