// 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 // 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 #include const char *LinkModeNames[] = { #define X(name) #name , LIST_OF_LINK_MODES #undef X }; struct Manufacturer { char code[4]; int m_field; char name[64]; }; Manufacturer manufacturers[] = { #define X(key,code,name) {#key,code,#name}, LIST_OF_MANUFACTURERS #undef X {"",0,""} }; struct Initializer { Initializer(); }; static Initializer initializser_; Initializer::Initializer() { for (auto &m : manufacturers) { m.m_field = \ (m.code[0]-64)*1024 + (m.code[1]-64)*32 + (m.code[2]-64); } } void Telegram::print() { 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) %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()); } 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 (%s)", 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, ciType(ci_field).c_str()); if (ci_field == 0x78) { // No data error and no encryption possible. } if (ci_field == 0x7a) { // Short data header verbose(" CC-field=%02x (%s) ACC=%02x ", cc_field, ccType(cc_field).c_str(), acc, sn[3],sn[2],sn[1],sn[0]); } if (ci_field == 0x8d) { verbose(" CC-field=%02x (%s) ACC=%02x SN=%02x%02x%02x%02x", cc_field, ccType(cc_field).c_str(), acc, sn[3],sn[2],sn[1],sn[0]); } if (ci_field == 0x8c) { verbose(" CC-field=%02x (%s) ACC=%02x", cc_field, ccType(cc_field).c_str(), acc); } verbose("\n"); } string manufacturer(int m_field) { for (auto &m : manufacturers) { if (m.m_field == m_field) return m.name; } 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"; case 1: return "Oil meter"; case 2: return "Electricity meter"; case 3: return "Gas meter"; case 4: return "Heat meter"; case 5: return "Steam meter"; case 6: return "Warm Water (30°C-90°C) meter"; case 7: return "Water meter"; case 8: return "Heat Cost Allocator"; case 9: return "Compressed air meter"; case 0x0a: return "Cooling load volume at outlet meter"; case 0x0b: return "Cooling load volume at inlet meter"; case 0x0c: return "Heat volume at inlet meter"; case 0x0d: return "Heat/Cooling load meter"; case 0x0e: return "Bus/System component"; case 0x0f: return "Unknown"; case 0x15: return "Hot water (>=90°C) meter"; case 0x16: return "Cold water meter"; case 0x17: return "Hot/Cold water meter"; case 0x18: return "Pressure meter"; case 0x19: return "A/D converter"; } return "Unknown"; } string mediaType(int m_field, int a_field_device_type) { switch (a_field_device_type) { case 0: return "other"; case 1: return "oil"; case 2: return "electricity"; case 3: return "gas"; case 4: return "heat"; case 5: return "steam"; case 6: return "warm water"; case 7: return "water"; case 8: return "heat cost"; case 9: return "compressed air"; case 0x0a: return "cooling load volume at outlet"; case 0x0b: return "cooling load volume at inlet"; case 0x0c: return "heat volume at inlet"; case 0x0d: return "heat/cooling load"; case 0x0e: return "bus/system component"; case 0x0f: return "unknown"; case 0x15: return "hot water"; case 0x16: return "cold water"; case 0x17: return "hot/cold water"; case 0x18: return "pressure"; case 0x19: return "a/d converter"; } 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 (checkIfSimulationFile(device.c_str())) { return { DEVICE_SIMULATOR, device }; } // 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, "" }; } string ciType(int ci_field) { if (ci_field >= 0xA0 && ci_field <= 0xB7) { return "Mfct specific"; } switch (ci_field) { case 0x60: return "COSEM Data sent by the Readout device to the meter with long Transport Layer"; case 0x61: return "COSEM Data sent by the Readout device to the meter with short Transport Layer"; case 0x64: return "Reserved for OBIS-based Data sent by the Readout device to the meter with long Transport Layer"; case 0x65: return "Reserved for OBIS-based Data sent by the Readout device to the meter with short Transport Layer"; case 0x69: return "EN 13757-3 Application Layer with Format frame and no Transport Layer"; case 0x6A: return "EN 13757-3 Application Layer with Format frame and with short Transport Layer"; case 0x6B: return "EN 13757-3 Application Layer with Format frame and with long Transport Layer"; case 0x6C: return "Clock synchronisation (absolute)"; case 0x6D: return "Clock synchronisation (relative)"; case 0x6E: return "Application error from device with short Transport Layer"; case 0x6F: return "Application error from device with long Transport Layer"; case 0x70: return "Application error from device without Transport Layer"; case 0x71: return "Reserved for Alarm Report"; case 0x72: return "EN 13757-3 Application Layer with long Transport Layer"; case 0x73: return "EN 13757-3 Application Layer with Compact frame and long Transport Layer"; case 0x74: return "Alarm from device with short Transport Layer"; case 0x75: return "Alarm from device with long Transport Layer"; case 0x78: return "EN 13757-3 Application Layer without Transport Layer (to be defined)"; case 0x79: return "EN 13757-3 Application Layer with Compact frame and no header"; case 0x7A: return "EN 13757-3 Application Layer with short Transport Layer"; case 0x7B: return "EN 13757-3 Application Layer with Compact frame and short header"; case 0x7C: return "COSEM Application Layer with long Transport Layer"; case 0x7D: return "COSEM Application Layer with short Transport Layer"; case 0x7E: return "Reserved for OBIS-based Application Layer with long Transport Layer"; case 0x7F: return "Reserved for OBIS-based Application Layer with short Transport Layer"; case 0x80: return "EN 13757-3 Transport Layer (long) from other device to the meter"; case 0x81: return "Network Layer data"; case 0x82: return "For future use"; case 0x83: return "Network Management application"; case 0x8A: return "EN 13757-3 Transport Layer (short) from the meter to the other device"; case 0x8B: return "EN 13757-3 Transport Layer (long) from the meter to the other device"; case 0x8C: return "Extended Link Layer I (2 Byte)"; case 0x8D: return "Extended Link Layer II (8 Byte)"; } return "?"; } void Telegram::addExplanation(vector &payload, int len, const char* fmt, ...) { char buf[1024]; buf[1023] = 0; va_list args; va_start(args, fmt); vsnprintf(buf, 1023, fmt, args); va_end(args); explanations.push_back({parsed.size(), buf}); parsed.insert(parsed.end(), payload.begin()+parsed.size(), payload.begin()+parsed.size()+len); } void Telegram::parse(vector &frame) { parsed.clear(); len = frame[0]; addExplanation(frame, 1, "%02x length (%d bytes)", len, len); c_field = frame[1]; addExplanation(frame, 1, "%02x c-field (%s)", c_field, cType(c_field).c_str()); m_field = frame[3]<<8 | frame[2]; string man = manufacturerFlag(m_field); addExplanation(frame, 2, "%02x%02x m-field (%02x=%s)", frame[2], frame[3], m_field, man.c_str()); a_field.resize(6); a_field_address.resize(4); for (int i=0; i<6; ++i) { a_field[i] = frame[4+i]; if (i<4) { a_field_address[i] = frame[4+3-i]; } } addExplanation(frame, 4, "%02x%02x%02x%02x a-field-addr (%02x%02x%02x%02x)", frame[4], frame[5], frame[6], frame[7], frame[7], frame[6], frame[5], frame[4]); a_field_version = frame[4+4]; a_field_device_type = frame[4+5]; addExplanation(frame, 1, "%02x a-field-version", frame[8]); addExplanation(frame, 1, "%02x a-field-type (%s)", frame[9], deviceType(m_field, a_field_device_type).c_str()); ci_field=frame[10]; addExplanation(frame, 1, "%02x ci-field (%s)", ci_field, ciType(ci_field).c_str()); int header_size = 0; if (ci_field == 0x78) { header_size = 0; // And no encryption possible. } else if (ci_field == 0x7a) { acc = frame[11]; addExplanation(frame, 1, "%02x acc", acc); status = frame[12]; addExplanation(frame, 1, "%02x status ()", status); configuration = frame[13]<<8 | frame[14]; addExplanation(frame, 2, "%02x%02x configuration ()", frame[13], frame[14]); header_size = 4; } else if (ci_field == 0x8d || ci_field == 0x8c) { cc_field = frame[11]; addExplanation(frame, 1, "%02x cc-field (%s)", cc_field, ccType(cc_field).c_str()); acc = frame[12]; addExplanation(frame, 1, "%02x acc", acc); header_size = 2; if (ci_field == 0x8d) { sn[0] = frame[13]; sn[1] = frame[14]; sn[2] = frame[15]; sn[3] = frame[16]; addExplanation(frame, 4, "%02x%02x%02x%02x sn", sn[0], sn[1], sn[2], sn[3]); header_size = 6; } } else { warning("Unknown ci-field %02x\n", ci_field); } payload.clear(); payload.insert(payload.end(), frame.begin()+(11+header_size), frame.end()); verbose("(wmbus) received telegram"); verboseFields(); debugPayload("(wmbus) frame", frame); debugPayload("(wmbus) payload", payload); if (isDebugEnabled()) { explainParse("(wmbus)", 0); } } void Telegram::explainParse(string intro, int from) { for (auto& p : explanations) { if (p.first < from) continue; printf("%s ", intro.c_str()); for (int i=0; i