/*
 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 <http://www.gnu.org/licenses/>.
*/

#include"aescmac.h"
#include"sha256.h"
#include"timings.h"
#include"wmbus.h"
#include"wmbus_common_implementation.h"
#include"wmbus_utils.h"
#include"dvparser.h"
#include"manufacturer_specificities.h"
#include<assert.h>
#include<cmath>
#include<semaphore.h>
#include<stdarg.h>
#include<string.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<unistd.h>

#include<deque>
#include<algorithm>

struct LinkModeInfo
{
    LinkMode mode;
    const char *name;
    const char *lcname;
    const char *option;
    uint64_t val;
};

LinkModeInfo link_modes_[] = {
#define X(name,lcname,option,val) { LinkMode::name, #name , #lcname, #option, val },
LIST_OF_LINK_MODES
#undef X
};

const char *toString(LinkMode lm)
{
#define X(name,lcname,option,val) if (lm == LinkMode::name) return #lcname;
LIST_OF_LINK_MODES
#undef X

    return "unknown";
}

LinkModeInfo *getLinkModeInfo(LinkMode lm);
LinkModeInfo *getLinkModeInfoFromBit(int bit);

LinkModeInfo *getLinkModeInfo(LinkMode lm)
{
    for (auto& s : link_modes_)
    {
        if (s.mode == lm)
        {
            return &s;
        }
    }
    assert(0);
    return NULL;
}

LinkModeInfo *getLinkModeInfoFromBit(uint64_t bit)
{
    for (auto& s : link_modes_)
    {
        if (s.val == bit)
        {
            return &s;
        }
    }
    assert(0);
    return NULL;
}

LinkMode isLinkModeOption(const char *arg)
{
    for (auto& s : link_modes_) {
        if (!strcmp(arg, s.option)) {
            return s.mode;
        }
    }
    return LinkMode::UNKNOWN;
}

LinkMode toLinkMode(const char *arg)
{
    for (auto& s : link_modes_) {
        if (!strcmp(arg, s.lcname)) {
            return s.mode;
        }
    }
    return LinkMode::UNKNOWN;
}

LinkModeSet parseLinkModes(string m)
{
    LinkModeSet lms;
    char buf[m.length()+1];
    strcpy(buf, m.c_str());
    char *saveptr {};
    const char *tok = strtok_r(buf, ",", &saveptr);
    while (tok != NULL)
    {
        LinkMode lm = toLinkMode(tok);
        if (lm == LinkMode::UNKNOWN)
        {
            error("(wmbus) not a valid link mode: %s\n", tok);
        }
        lms.addLinkMode(lm);
        tok = strtok_r(NULL, ",", &saveptr);
    }
    return lms;
}

bool isValidLinkModes(string m)
{
    LinkModeSet lms;
    char buf[m.length()+1];
    strcpy(buf, m.c_str());
    char *saveptr {};
    const char *tok = strtok_r(buf, ",", &saveptr);
    while (tok != NULL)
    {
        LinkMode lm = toLinkMode(tok);
        if (lm == LinkMode::UNKNOWN)
        {
            return false;
        }
        lms.addLinkMode(lm);
        tok = strtok_r(NULL, ",", &saveptr);
    }
    return true;
}

LinkModeSet &LinkModeSet::addLinkMode(LinkMode lm)
{
    for (auto& s : link_modes_) {
        if (s.mode == lm) {
            set_ |= s.val;
        }
    }
    return *this;
}

void LinkModeSet::unionLinkModeSet(LinkModeSet lms)
{
    set_ |= lms.set_;
}

void LinkModeSet::disjunctionLinkModeSet(LinkModeSet lms)
{
    set_ &= lms.set_;
}

bool LinkModeSet::supports(LinkModeSet lms)
{
    // Will return false, if lms is UKNOWN (=0).
    return (set_ & lms.set_) != 0;
}

bool LinkModeSet::has(LinkMode lm)
{
    LinkModeInfo *lmi = getLinkModeInfo(lm);
    return (set_ & lmi->val) != 0;
}

bool LinkModeSet::hasAll(LinkModeSet lms)
{
    return (set_ & lms.set_) == lms.set_;
}

string LinkModeSet::hr()
{
    string r;
    if (set_ == Any_bit) return "any";
    if (set_ == 0) return "none";
    for (auto& s : link_modes_)
    {
        if (s.mode == LinkMode::Any) continue;
        if (set_ & s.val)
        {
            r += s.lcname;
            r += ",";
        }
    }
    r.pop_back();
    return r;
}

struct Manufacturer {
    const char *code;
    int m_field;
    const char *name;

    Manufacturer(const char *c, int m, const char *n) {
        code = c;
        m_field = m;
        name = n;
    }
};

vector<Manufacturer> manufacturers_;

struct Initializer { Initializer(); };

static Initializer initializser_;

Initializer::Initializer() {

#define X(key,code,name) manufacturers_.push_back(Manufacturer(#key,code,name));
LIST_OF_MANUFACTURERS
#undef X

}

void Telegram::addAddressMfctFirst(const vector<uchar>::iterator &pos)
{
    Address a;
    a.decodeMfctFirst(pos);
    addresses.push_back(a);
}

void Telegram::addAddressIdFirst(const vector<uchar>::iterator &pos)
{
    Address a;
    a.decodeIdFirst(pos);
    addresses.push_back(a);
}

void Telegram::print()
{
    uchar a=0, b=0, c=0, d=0;
    if (dll_id.size() >= 4)
    {
        a = dll_id[0];
        b = dll_id[1];
        c = dll_id[2];
        d = dll_id[3];
    }
    const char *enc = "";

    if (ell_sec_mode != ELLSecurityMode::NoSecurity ||
        tpl_sec_mode != TPLSecurityMode::NoSecurity)
    {
        enc = " encrypted";
    }

    notice("Received telegram from: %02x%02x%02x%02x\n", a,b,c,d);
    notice("          manufacturer: (%s) %s (0x%02x)\n",
           manufacturerFlag(dll_mfct).c_str(),
           manufacturer(dll_mfct).c_str(),
           dll_mfct);
    notice("                  type: %s (0x%02x)%s\n", mediaType(dll_type, dll_mfct).c_str(), dll_type, enc);

    notice("                   ver: 0x%02x\n", dll_version);

    if (tpl_id_found)
    {
        notice("      Concerning meter: %02x%02x%02x%02x\n", tpl_id_b[3],tpl_id_b[2],tpl_id_b[1],tpl_id_b[0]);
        notice("          manufacturer: (%s) %s (0x%02x)\n",
           manufacturerFlag(tpl_mfct).c_str(),
           manufacturer(tpl_mfct).c_str(),
           tpl_mfct);
        notice("                  type: %s (0x%02x)%s\n", mediaType(tpl_type, dll_mfct).c_str(), tpl_type, enc);

        notice("                   ver: 0x%02x\n", tpl_version);
    }
    if (about.device != "")
    {
        notice("                device: %s\n", about.device.c_str());
        notice("                  rssi: %d dBm\n", about.rssi_dbm);
        if (about.link_mode != LinkMode::UNKNOWN)
        {
            notice("             link mode: %s\n", toString(about.link_mode));
        }
    }
    string possible_drivers = autoDetectPossibleDrivers();
    notice("                driver: %s\n", possible_drivers.c_str());
}

void Telegram::printDLL()
{
    if (about.type == FrameType::WMBUS)
    {
        string possible_drivers = autoDetectPossibleDrivers();

        string man = manufacturerFlag(dll_mfct);
        verbose("(telegram) DLL L=%02x C=%02x (%s) M=%04x (%s) A=%02x%02x%02x%02x VER=%02x TYPE=%02x (%s) (driver %s) DEV=%s RSSI=%d\n",
            dll_len,
            dll_c, cType(dll_c).c_str(),
            dll_mfct,
            man.c_str(),
            dll_id[0], dll_id[1], dll_id[2], dll_id[3],
            dll_version,
            dll_type,
            mediaType(dll_type, dll_mfct).c_str(),
            possible_drivers.c_str(),
            about.device.c_str(),
            about.rssi_dbm);
    }

    if (about.type == FrameType::MBUS)
    {
        verbose("(telegram) DLL L=%02x C=%02x (%s) A=%02x\n",
                dll_len,
                dll_c, cType(dll_c).c_str(),
                mbus_primary_address);
    }

}

void Telegram::printELL()
{
    if (ell_ci == 0) return;

    string ell_cc_info = ccType(ell_cc);
    verbose("(telegram) ELL CI=%02x CC=%02x (%s) ACC=%02x",
            ell_ci, ell_cc, ell_cc_info.c_str(), ell_acc);

    if (ell_ci == 0x8d || ell_ci == 0x8f)
    {
        string ell_sn_info = toStringFromELLSN(ell_sn);

        verbose(" SN=%02x%02x%02x%02x (%s) CRC=%02x%02x",
                ell_sn_b[0], ell_sn_b[1], ell_sn_b[2], ell_sn_b[3], ell_sn_info.c_str(),
                ell_pl_crc_b[0], ell_pl_crc_b[1]);
    }
    if (ell_ci == 0x8e || ell_ci == 0x8f)
    {
        string man = manufacturerFlag(ell_mfct);
        verbose(" M=%02x%02x (%s) ID=%02x%02x%02x%02x",
                ell_mfct_b[0], ell_mfct_b[1], man.c_str(),
                ell_id_b[0], ell_id_b[1], ell_id_b[2], ell_id_b[3]);
    }
    verbose("\n");
}

void Telegram::printNWL()
{
    if (nwl_ci == 0) return;

    verbose("(telegram) NWL CI=%02x\n",
            nwl_ci);
}

void Telegram::printAFL()
{
    if (afl_ci == 0) return;

    verbose("(telegram) AFL CI=%02x\n",
            afl_ci);

}

void Telegram::printTPL()
{
    if (tpl_ci == 0) return;

    verbose("(telegram) TPL CI=%02x", tpl_ci);

    if (tpl_ci == 0x7a || tpl_ci == 0x72)
    {
        string tpl_cfg_info = toStringFromTPLConfig(tpl_cfg);
        verbose(" ACC=%02x STS=%02x CFG=%04x (%s)",
                tpl_acc, tpl_sts, tpl_cfg, tpl_cfg_info.c_str());
    }

    if (tpl_ci == 0x72)
    {
        string info = mediaType(tpl_type, tpl_mfct);
        verbose(" ID=%02x%02x%02x%02x MFT=%02x%02x VER=%02x TYPE=%02x (%s)",
                tpl_id_b[0], tpl_id_b[1], tpl_id_b[2], tpl_id_b[3],
                tpl_mfct_b[0], tpl_mfct_b[1],
                tpl_version, tpl_type, info.c_str());
    }

    verbose("\n");
}

// Store the hashes of the last 10 telegrams here.
deque<SHA256_HASH> seen_telegrams;

bool seen_this_telegram_before(vector<uchar> &frame)
{
    SHA256_HASH hash;
    Sha256Calculate(safeButUnsafeVectorPtr(frame), frame.size(), &hash);

    auto i = std::find(seen_telegrams.begin(), seen_telegrams.end(), hash);

    if (i != seen_telegrams.end())
    {
        // Found it!
        return true;
    }

    if (seen_telegrams.size() >= 10)
    {
        seen_telegrams.pop_front();
    }
    seen_telegrams.push_back(hash);

    return false;
}

// Store the dll_a (6 bytes composed of 4 id + 1 ver + 1 media )
// for telegrams that has been warned about!
deque<vector<uchar>> warning_printed_for_telegrams;

bool warned_for_telegram_before(Telegram *t, vector<uchar> &dll_a)
{
    auto i = std::find(warning_printed_for_telegrams.begin(), warning_printed_for_telegrams.end(), dll_a);

    if (i != warning_printed_for_telegrams.end())
    {
        // Found it!
        if (t->triggered_warning)
        {
            // This is another warning for the same telegram, that triggered the first warning.
            // We want to print all warnings for the first telegram, return false to print it.
            return false;
        }
        // This is a second telegram, ie not the telegram that triggered the first warning.
        // So lets report true, because we have warned for this telegram id before.
        return true;
    }

    // Limit size of memory to 100 odd meters...
    if (warning_printed_for_telegrams.size() >= 100)
    {
        warning_printed_for_telegrams.pop_front();
    }
    warning_printed_for_telegrams.push_back(dll_a);
    // Print all warnings for this telegram.
    t->triggered_warning = true;
    return false;
}

string manufacturer(int m_field) {
    for (auto &m : manufacturers_) {
	if (m.m_field == m_field) return m.name;
    }
    // Some weird meters send the first char in lower case aPT iTW. Fix and try again.
    m_field &= 0x7fff;
    for (auto &m : manufacturers_) {
	if (m.m_field == m_field) return m.name;
    }
    return "Unknown";
}

string mediaType(int a_field_device_type, int m_field) {
    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";
    case 0x1A: return "Smoke detector";
    case 0x1B: return "Room sensor (eg temperature or humidity)";
    case 0x1C: return "Gas detector";
    case 0x1D: return "Reserved for sensors";
    case 0x1F: return "Reserved for sensors";
    case 0x20: return "Breaker (electricity)";
    case 0x21: return "Valve (gas or water)";
    case 0x22: return "Reserved for switching devices";
    case 0x23: return "Reserved for switching devices";
    case 0x24: return "Reserved for switching devices";
    case 0x25: return "Customer unit (display device)";
    case 0x26: return "Reserved for customer units";
    case 0x27: return "Reserved for customer units";
    case 0x28: return "Waste water";
    case 0x29: return "Garbage";
    case 0x2A: return "Reserved for Carbon dioxide";
    case 0x2B: return "Reserved for environmental meter";
    case 0x2C: return "Reserved for environmental meter";
    case 0x2D: return "Reserved for environmental meter";
    case 0x2E: return "Reserved for environmental meter";
    case 0x2F: return "Reserved for environmental meter";
    case 0x30: return "Reserved for system devices";
    case 0x31: return "Reserved for communication controller";
    case 0x32: return "Reserved for unidirectional repeater";
    case 0x33: return "Reserved for bidirectional repeater";
    case 0x34: return "Reserved for system devices";
    case 0x35: return "Reserved for system devices";
    case 0x36: return "Radio converter (system side)";
    case 0x37: return "Radio converter (meter side)";
    case 0x38: return "Reserved for system devices";
    case 0x39: return "Reserved for system devices";
    case 0x3A: return "Reserved for system devices";
    case 0x3B: return "Reserved for system devices";
    case 0x3C: return "Reserved for system devices";
    case 0x3D: return "Reserved for system devices";
    case 0x3E: return "Reserved for system devices";
    case 0x3F: return "Reserved for system devices";
    }

    if (m_field == MANUFACTURER_TCH)
    {
        switch (a_field_device_type) {
        // Techem MK Radio 3/4 manufacturer specific.
        case 0x62: return "Warm water"; // MKRadio3/MKRadio4
        case 0x72: return "Cold water"; // MKRadio3/MKRadio4
        // Techem FHKV.
        case 0x80: return "Heat Cost Allocator"; // FHKV data ii/iii
        // Techem Vario 4 Typ 4.5.1 manufacturer specific.
        case 0xC3: return "Heat meter";
        // Techem V manufacturer specific.
        case 0x43: return "Heat meter";
        case 0xf0: return "Smoke detector";
        }
    }
    return "Unknown";
}

string mediaTypeJSON(int a_field_device_type, int m_field)
{
    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 allocation";
    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";
    case 0x1A: return "smoke detector";
    case 0x1B: return "room sensor";
    case 0x1C: return "gas detector";
    case 0x1D: return "reserved";
    case 0x1F: return "reserved";
    case 0x20: return "breaker";
    case 0x21: return "valve";
    case 0x22: return "reserved";
    case 0x23: return "reserved";
    case 0x24: return "reserved";
    case 0x25: return "customer unit (display device)";
    case 0x26: return "reserved";
    case 0x27: return "reserved";
    case 0x28: return "waste water";
    case 0x29: return "garbage";
    case 0x2A: return "reserved";
    case 0x2B: return "reserved";
    case 0x2C: return "reserved";
    case 0x2D: return "reserved";
    case 0x2E: return "reserved";
    case 0x2F: return "reserved";
    case 0x30: return "reserved";
    case 0x31: return "reserved";
    case 0x32: return "reserved";
    case 0x33: return "reserved";
    case 0x34: return "reserved";
    case 0x35: return "reserved";
    case 0x36: return "radio converter (system side)";
    case 0x37: return "radio converter (meter side)";
    case 0x38: return "reserved";
    case 0x39: return "reserved";
    case 0x3A: return "reserved";
    case 0x3B: return "reserved";
    case 0x3C: return "reserved";
    case 0x3D: return "reserved";
    case 0x3E: return "reserved";
    case 0x3F: return "reserved";
    }

    if (m_field == MANUFACTURER_TCH)
    {
        switch (a_field_device_type) {
        // Techem MK Radio 3/4 manufacturer specific.
        case 0x62: return "warm water"; // MKRadio3/MKRadio4
        case 0x72: return "cold water"; // MKRadio3/MKRadio4
        // Techem FHKV.
        case 0x80: return "heat cost allocator"; // FHKV data ii/iii
        // Techem Vario 4 Typ 4.5.1 manufacturer specific.
        case 0xC3: return "heat";
        // Techem V manufacturer specific.
        case 0x43: return "heat";
        case 0xf0: return "smoke detector";
        }
    }
    return "Unknown";
}

/*
    X(0x73, TPL_73,  "TPL: long header compact APL follows", 0, CI_TYPE::TPL, "") \
*/

#define LIST_OF_CI_FIELDS \
    X(0x51, TPL_51,  "TPL: APL follows", 0, CI_TYPE::TPL, "")       \
    X(0x72, TPL_72,  "TPL: long header APL follows", 0, CI_TYPE::TPL, "") \
    X(0x78, TPL_78,  "TPL: no header APL follows", 0, CI_TYPE::TPL, "") \
    X(0x79, TPL_79,  "TPL: compact APL follows", 0, CI_TYPE::TPL, "") \
    X(0x7A, TPL_7A,  "TPL: short header APL follows", 0, CI_TYPE::TPL, "") \
    X(0x81, NWL_81,  "NWL: TPL or APL follows?", 0, CI_TYPE::NWL, "") \
    X(0x8C, ELL_I,   "ELL: I",    2, CI_TYPE::ELL, "CC, ACC") \
    X(0x8D, ELL_II,  "ELL: II",   8, CI_TYPE::ELL, "CC, ACC, SN, Payload CRC") \
    X(0x8E, ELL_III, "ELL: III", 10, CI_TYPE::ELL, "CC, ACC, M2, A2") \
    X(0x8F, ELL_IV,  "ELL: IV",  16, CI_TYPE::ELL, "CC, ACC, M2, A2, SN, Payload CRC") \
    X(0x86, ELL_V,   "ELL: V",   -1, CI_TYPE::ELL, "Variable length") \
    X(0x90, AFL,     "AFL", 10, CI_TYPE::AFL, "")

enum CI_Field_Values {
#define X(val,name,cname,len,citype,explain) name = val,
LIST_OF_CI_FIELDS
#undef X
};

bool isCiFieldOfType(int ci_field, CI_TYPE type)
{
#define X(val,name,cname,len,citype,explain) if (ci_field == val && type == citype) return true;
LIST_OF_CI_FIELDS
#undef X
    return false;
}

int ciFieldLength(int ci_field)
{
#define X(val,name,cname,len,citype,explain) if (ci_field == val) return len;
LIST_OF_CI_FIELDS
#undef X
    return -2;
}

bool isCiFieldManufacturerSpecific(int ci_field)
{
    return ci_field >= 0xA0 && ci_field <= 0xB7;
}

string ciType(int ci_field)
{
    if (ci_field >= 0xA0 && ci_field <= 0xB7) {
        return "Mfct specific";
    }
    if (ci_field >= 0x00 && ci_field <= 0x1f) {
        return "Reserved for DLMS";
    }

    if (ci_field >= 0x20 && ci_field <= 0x4f) {
        return "Reserved";
    }

    switch (ci_field) {
    case 0x50: return "Application reset or select to device (no tplh)";
    case 0x51: return "Command to device (no tplh)"; // Only for mbus, not wmbus.
    case 0x52: return "Selection of device (no tplh)";
    case 0x53: return "Application reset or select to device (long tplh)";
    case 0x54: return "Request of selected application to device (no tplh)";
    case 0x55: return "Request of selected application to device (long tplh)";
    case 0x56: return "Reserved";
    case 0x57: return "Reserved";
    case 0x58: return "Reserved";
    case 0x59: return "Reserved";
    case 0x5a: return "Command to device (short tplh)";
    case 0x5b: return "Command to device (long tplh)";
    case 0x5c: return "Sync action (no tplh)";
    case 0x5d: return "Reserved";
    case 0x5e: return "Reserved";
    case 0x5f: return "Specific usage";
    case 0x60: return "COSEM Data sent by the Readout device to the meter (long tplh)";
    case 0x61: return "COSEM Data sent by the Readout device to the meter (short tplh)";
    case 0x62: return "?";
    case 0x63: return "?";
    case 0x64: return "Reserved for OBIS-based Data sent by the Readout device to the meter (long tplh)";
    case 0x65: return "Reserved for OBIS-based Data sent by the Readout device to the meter (short tplh)";
    case 0x66: return "Response of selected application from device (no tplh)";
    case 0x67: return "Response of selected application from device (short tplh)";
    case 0x68: return "Response of selected application from device (long tplh)";
    case 0x69: return "EN 13757-3 Application Layer with Format frame (no tplh)";
    case 0x6A: return "EN 13757-3 Application Layer with Format frame (short tplh)";
    case 0x6B: return "EN 13757-3 Application Layer with Format frame (long tplh)";
    case 0x6C: return "Clock synchronisation (absolute) (long tplh)";
    case 0x6D: return "Clock synchronisation (relative) (long tplh)";
    case 0x6E: return "Application error from device (short tplh)";
    case 0x6F: return "Application error from device (long tplh)";
    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 (long tplh)";
    case 0x73: return "EN 13757-3 Application Layer with Compact frame and long Transport Layer";
    case 0x74: return "Alarm from device (short tplh)";
    case 0x75: return "Alarm from device (long tplh)";
    case 0x76: return "?";
    case 0x77: return "?";
    case 0x78: return "EN 13757-3 Application Layer (no tplh)";
    case 0x79: return "EN 13757-3 Application Layer with Compact frame (no tplh)";
    case 0x7A: return "EN 13757-3 Application Layer (short tplh)";
    case 0x7B: return "EN 13757-3 Application Layer with Compact frame (short tplh)";
    case 0x7C: return "COSEM Application Layer (long tplh)";
    case 0x7D: return "COSEM Application Layer (short tplh)";
    case 0x7E: return "Reserved for OBIS-based Application Layer (long tplh)";
    case 0x7F: return "Reserved for OBIS-based Application Layer (short tplh)";
    case 0x80: return "EN 13757-3 Transport Layer (long tplh) from other device to the meter";

    case 0x81: return "Network Layer data";
    case 0x82: return "Network management data to device (short tplh)";
    case 0x83: return "Network Management data to device (no tplh)";
    case 0x84: return "Transport layer to device (compact frame) (long tplh)";
    case 0x85: return "Transport layer to device (format frame) (long tplh)";
    case 0x86: return "Extended Link Layer V (variable length)";
    case 0x87: return "Network management data from device (long tplh)";
    case 0x88: return "Network management data from device (short tplh)";
    case 0x89: return "Network management data from device (no tplh)";
    case 0x8A: return "EN 13757-3 Transport Layer (short tplh) from the meter to the other device"; // No application layer, e.g. ACK
    case 0x8B: return "EN 13757-3 Transport Layer (long tplh) from the meter to the other device"; // No application layer, e.g. ACK

    case 0x8C: return "ELL: Extended Link Layer I (2 Byte)"; // CC, ACC
    case 0x8D: return "ELL: Extended Link Layer II (8 Byte)"; // CC, ACC, SN, Payload CRC
    case 0x8E: return "ELL: Extended Link Layer III (10 Byte)"; // CC, ACC, M2, A2
    case 0x8F: return "ELL: Extended Link Layer IV (16 Byte)"; // CC, ACC, M2, A2, SN, Payload CRC

    case 0x90: return "AFL: Authentication and Fragmentation Sublayer";
    case 0x91: return "Reserved";
    case 0x92: return "Reserved";
    case 0x93: return "Reserved";
    case 0x94: return "Reserved";
    case 0x95: return "Reserved";
    case 0x96: return "Reserved";
    case 0x97: return "Reserved";
    case 0x98: return "?";
    case 0x99: return "?";

    case 0xB8: return "Set baud rate to 300";
    case 0xB9: return "Set baud rate to 600";
    case 0xBA: return "Set baud rate to 1200";
    case 0xBB: return "Set baud rate to 2400";
    case 0xBC: return "Set baud rate to 4800";
    case 0xBD: return "Set baud rate to 9600";
    case 0xBE: return "Set baud rate to 19200";
    case 0xBF: return "Set baud rate to 38400";
    case 0xC0: return "Image transfer to device (long tplh)";
    case 0xC1: return "Image transfer from device (short tplh)";
    case 0xC2: return "Image transfer from device (long tplh)";
    case 0xC3: return "Security info transfer to device (long tplh)";
    case 0xC4: return "Security info transfer from device (short tplh)";
    case 0xC5: return "Security info transfer from device (long tplh)";
    }
    return "?";
}

void Telegram::addExplanationAndIncrementPos(vector<uchar>::iterator &pos, int len, KindOfData k, Understanding u, const char* fmt, ...)
{
    char buf[1024];
    buf[1023] = 0;

    va_list args;
    va_start(args, fmt);
    vsnprintf(buf, 1023, fmt, args);
    va_end(args);

    Explanation e(parsed.size(), len, buf, k, u);
    explanations.push_back(e);
    parsed.insert(parsed.end(), pos, pos+len);
    pos += len;
}

void Telegram::setExplanation(vector<uchar>::iterator &pos, int len, KindOfData k, Understanding u, const char* fmt, ...)
{
    char buf[1024];
    buf[1023] = 0;

    va_list args;
    va_start(args, fmt);
    vsnprintf(buf, 1023, fmt, args);
    va_end(args);

    Explanation e(distance(frame.begin(),pos), len, buf, k, u);
    explanations.push_back(e);
}

void Telegram::addMoreExplanation(int pos, string json)
{
    addMoreExplanation(pos, " (%s)", json.c_str());
}

void Telegram::addMoreExplanation(int pos, const char* fmt, ...)
{
    char buf[1024];

    buf[1023] = 0;

    va_list args;
    va_start(args, fmt);
    vsnprintf(buf, 1023, fmt, args);
    va_end(args);

    bool found = false;
    for (auto& p : explanations) {
        if (p.pos == pos)
        {
            // Append more information.
            p.info = p.info+buf;
            // Since we are adding more information, we assume that we have a full understanding.
            p.understanding = Understanding::FULL;
            found = true;
        }
    }

    if (!found) {
        debug("(wmbus) warning: cannot find offset %d to add more explanation \"%s\"\n", pos, buf);
    }
}

void Telegram::addSpecialExplanation(int offset, int len, KindOfData k, Understanding u, 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({offset, len, buf, k, u});
}

bool expectedMore(int line)
{
    verbose("(wmbus) parser expected more data! (%d)\n", line);
    return false;
}

bool Telegram::parseMBusDLLandTPL(vector<uchar>::iterator &pos)
{
    int remaining = distance(pos, frame.end());

    if (remaining == 1 && *pos == 0xE5)
    {
        addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "E5");
        return true;
    }

    if (remaining < 5) {
        return expectedMore(__LINE__);
    }

    debug("(mbus) parse MBUS DLL @%d %d\n", distance(frame.begin(), pos), remaining);
    debugPayload("(mbus) ", frame);

    if (*pos != 0x68) return false;
    addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "68 start");

    dll_len = *pos;
    addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x length (%d bytes)", dll_len, dll_len);

    // Two identical length bytes are expected!
    if (*pos != dll_len) return false;
    addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x length again (%d bytes)", dll_len, dll_len);

    if (*pos != 0x68) return false;
    addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "68 start");

    if (remaining < dll_len) return expectedMore(__LINE__);

    // Last byte should be 0x16
    auto end = frame.end();
    end--;
    if (*end != 0x16) return false;
    setExplanation(end, 1, KindOfData::PROTOCOL, Understanding::FULL, "16 end");

    // Second last byte should be crc. Should have been checked before! No need to check again here?
    end--;
    setExplanation(end, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02X crc", *end);

    // Mark crc and end as suffix, ie should not be parsed by dvparser.
    suffix_size = 2;

    dll_c = *pos;
    addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x dll-c (%s)", dll_c, mbusCField(dll_c));

    mbus_primary_address = *pos;
    addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x dll-a primary (%d)", mbus_primary_address, mbus_primary_address);

    // Add dll_id to ids.
    string id = tostrprintf("p%d", mbus_primary_address);
    Address a;
    a.id = id;
    addresses.push_back(a);

    mbus_ci = *pos;
    addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x tpl-ci (%s)", mbus_ci, mbusCiField(mbus_ci));

    if (mbus_ci == 0x72)
    {
        return parse_TPL_72(pos);
    }

    return false;
}

bool Telegram::parseDLL(vector<uchar>::iterator &pos)
{
    int remaining = distance(pos, frame.end());
    if (remaining == 0) return expectedMore(__LINE__);

    debug("(wmbus) parseDLL @%d %d\n", distance(frame.begin(), pos), remaining);
    dll_len = *pos;
    if (remaining < dll_len) return expectedMore(__LINE__);
    addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x length (%d bytes)", dll_len, dll_len);

    dll_c = *pos;
    addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x dll-c (%s)", dll_c, cType(dll_c).c_str());

    CHECK(8)
    addAddressMfctFirst(pos);

    dll_mfct_b[0] = *(pos+0);
    dll_mfct_b[1] = *(pos+1);
    dll_mfct = dll_mfct_b[1] <<8 | dll_mfct_b[0];
    string man = manufacturerFlag(dll_mfct);
    addExplanationAndIncrementPos(pos, 2, KindOfData::PROTOCOL, Understanding::FULL, "%02x%02x dll-mfct (%s)",
                                  dll_mfct_b[0], dll_mfct_b[1], man.c_str());

    dll_a.resize(6);
    dll_id.resize(4);
    for (int i=0; i<6; ++i)
    {
        dll_a[i] = *(pos+i);
        if (i<4)
        {
            dll_id_b[i] = *(pos+i);
            dll_id[i] = *(pos+3-i);
        }
    }
    // Add dll_id to ids.
    addExplanationAndIncrementPos(pos, 4, KindOfData::PROTOCOL, Understanding::FULL, "%02x%02x%02x%02x dll-id (%s)",
                                  *(pos+0), *(pos+1), *(pos+2), *(pos+3), addresses.back().id.c_str());

    dll_version = *(pos+0);
    dll_type = *(pos+1);
    addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x dll-version", dll_version);
    addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x dll-type (%s)", dll_type,
                                  mediaType(dll_type, dll_mfct).c_str());

    return true;
}

string Telegram::toStringFromELLSN(int sn)
{
    int session = (sn >> 0)  & 0x0f; // lowest 4 bits
    int time = (sn >> 4)  & 0x1ffffff; // next 25 bits
    int sec  = (sn >> 29) & 0x7; // next 3 bits.
    string info;
    ELLSecurityMode esm = fromIntToELLSecurityMode(sec);
    info += toString(esm);
    info += " session=";
    info += to_string(session);
    info += " time=";
    info += to_string(time);
    return info;
}

bool Telegram::parseELL(vector<uchar>::iterator &pos)
{
    int remaining = distance(pos, frame.end());
    if (remaining == 0) return false;

    debug("(wmbus) parseELL @%d %d\n", distance(frame.begin(), pos), remaining);
    int ci_field = *pos;
    if (!isCiFieldOfType(ci_field, CI_TYPE::ELL)) return true;
    addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x ell-ci-field (%s)",
                                  ci_field, ciType(ci_field).c_str());
    ell_ci = ci_field;
    int len = ciFieldLength(ell_ci);

    if (remaining < len+1) return expectedMore(__LINE__);

    // All ELL:s (including ELL I) start with cc,acc.

    ell_cc = *pos;
    addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x ell-cc (%s)", ell_cc, ccType(ell_cc).c_str());

    ell_acc = *pos;
    addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x ell-acc", ell_acc);

    bool has_target_mft_address = false;
    bool has_session_number_pl_crc = false;

    switch (ell_ci)
    {
    case CI_Field_Values::ELL_I:
        // Already handled above.
        break;
    case CI_Field_Values::ELL_II:
        has_session_number_pl_crc = true;
        break;
    case CI_Field_Values::ELL_III:
        has_target_mft_address = true;
        break;
    case CI_Field_Values::ELL_IV:
        has_session_number_pl_crc = true;
        has_target_mft_address = true;
        break;
    case CI_Field_Values::ELL_V:
        verbose("ELL V not yet handled\n");
        return false;
    }

    if (has_target_mft_address)
    {
        CHECK(8);
        addAddressMfctFirst(pos);

        ell_mfct_b[0] = *(pos+0);
        ell_mfct_b[1] = *(pos+1);
        ell_mfct = ell_mfct_b[1] << 8 | ell_mfct_b[0];
        string man = manufacturerFlag(ell_mfct);
        addExplanationAndIncrementPos(pos, 2, KindOfData::PROTOCOL, Understanding::FULL, "%02x%02x ell-mfct (%s)",
                                      ell_mfct_b[0], ell_mfct_b[1], man.c_str());

        ell_id_found = true;
        ell_id_b[0] = *(pos+0);
        ell_id_b[1] = *(pos+1);
        ell_id_b[2] = *(pos+2);
        ell_id_b[3] = *(pos+3);

        // Add ell_id to ids.
        addExplanationAndIncrementPos(pos, 4, KindOfData::PROTOCOL, Understanding::FULL, "%02x%02x%02x%02x ell-id",
                                      ell_id_b[0], ell_id_b[1], ell_id_b[2], ell_id_b[3]);

        ell_version = *pos;
        addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x ell-version", ell_version);

        ell_type = *pos;
        addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x ell-type", ell_type);
    }

    if (has_session_number_pl_crc)
    {
        string sn_info;
        ell_sn_b[0] = *(pos+0);
        ell_sn_b[1] = *(pos+1);
        ell_sn_b[2] = *(pos+2);
        ell_sn_b[3] = *(pos+3);
        ell_sn = ell_sn_b[3]<<24 | ell_sn_b[2]<<16 | ell_sn_b[1] << 8 | ell_sn_b[0];

        ell_sn_session = (ell_sn >> 0)  & 0x0f; // lowest 4 bits
        ell_sn_time = (ell_sn >> 4)  & 0x1ffffff; // next 25 bits
        ell_sn_sec = (ell_sn >> 29) & 0x7; // next 3 bits.
        ell_sec_mode = fromIntToELLSecurityMode(ell_sn_sec);
        string info = toString(ell_sec_mode);
        addExplanationAndIncrementPos(pos, 4, KindOfData::PROTOCOL, Understanding::FULL, "%02x%02x%02x%02x sn (%s)",
                                      ell_sn_b[0], ell_sn_b[1], ell_sn_b[2], ell_sn_b[3], info.c_str());

        if (ell_sec_mode == ELLSecurityMode::AES_CTR)
        {
            if (meter_keys)
            {
                decrypt_ELL_AES_CTR(this, frame, pos, meter_keys->confidentiality_key);
                // Actually this ctr decryption always succeeds, if wrong key, it will decrypt to garbage.
            }
            // Now the frame from pos and onwards has been decrypted, perhaps.
        }

        ell_pl_crc_b[0] = *(pos+0);
        ell_pl_crc_b[1] = *(pos+1);
        ell_pl_crc = (ell_pl_crc_b[1] << 8) | ell_pl_crc_b[0];

        int dist = distance(frame.begin(), pos+2);
        int len = distance(pos+2, frame.end());
        uint16_t check = crc16_EN13757(&(frame[dist]), len);

        addExplanationAndIncrementPos(pos, 2, KindOfData::PROTOCOL, Understanding::FULL,
                                      "%02x%02x payload crc (calculated %02x%02x %s)",
                                      ell_pl_crc_b[0], ell_pl_crc_b[1],
                                      check  & 0xff, check >> 8, (ell_pl_crc==check?"OK":"ERROR"));


        if (ell_pl_crc == check || FUZZING)
        {
        }
        else
        {
            // Ouch, checksum of the payload does not match.
            // A wrong key, or no key was probably used for decryption.
            decryption_failed = true;

            // Log the content as failed decryption.
            int num_encrypted_bytes = frame.end()-pos;
            string info = bin2hex(pos, frame.end(), num_encrypted_bytes);
            info += " failed decryption. Wrong key?";
            addExplanationAndIncrementPos(pos, num_encrypted_bytes, KindOfData::CONTENT, Understanding::ENCRYPTED, info.c_str());

            if (parser_warns_)
            {
                if (!beingAnalyzed() && (isVerboseEnabled() || isDebugEnabled() || !warned_for_telegram_before(this, dll_a)))
                {
                    // Print this warning only once! Unless you are using verbose or debug.
                    warning("(wmbus) WARNING! decrypted payload crc failed check, did you use the correct decryption key? "
                            "%02x%02x payload crc (calculated %02x%02x) "
                            "Permanently ignoring telegrams from id: %02x%02x%02x%02x mfct: (%s) %s (0x%02x) type: %s (0x%02x) ver: 0x%02x\n",
                            ell_pl_crc_b[0], ell_pl_crc_b[1],
                            check  & 0xff, check >> 8,
                            dll_id_b[3], dll_id_b[2], dll_id_b[1], dll_id_b[0],
                            manufacturerFlag(dll_mfct).c_str(),
                            manufacturer(dll_mfct).c_str(),
                            dll_mfct,
                            mediaType(dll_type, dll_mfct).c_str(), dll_type,
                            dll_version);
                }
            }
        }
    }

    return true;
}

bool Telegram::parseNWL(vector<uchar>::iterator &pos)
{
    int remaining = distance(pos, frame.end());
    if (remaining == 0) return false;

    debug("(wmbus) parseNWL @%d %d\n", distance(frame.begin(), pos), remaining);
    int ci_field = *pos;
    if (!isCiFieldOfType(ci_field, CI_TYPE::NWL)) return true;
    addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x nwl-ci-field (%s)",
                                  ci_field, ciType(ci_field).c_str());
    nwl_ci = ci_field;
    // We have only seen 0x81 0x1d so far.
    int len = 1; // ciFieldLength(nwl_ci);

    if (remaining < len+1) return expectedMore(__LINE__);

    uchar nwl = *pos;
    addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x nwl?", nwl);

    return true;
}

bool Telegram::parseAFL(vector<uchar>::iterator &pos)
{
    // 90 0F (len) 002C (fc) 25 (mc) 49EE 0A00 77C1 9D3D 1A08 ABCD --- 729067296179161102F
    // 90 0F (len) 002C (fc) 25 (mc) 0C39 0000 ED17 6BBB B159 1ADB --- 7A1D003007103EA

    int remaining = distance(pos, frame.end());
    if (remaining == 0) return false;

    debug("(wmbus) parseAFL @%d %d\n", distance(frame.begin(), pos), remaining);

    int ci_field = *pos;
    if (!isCiFieldOfType(ci_field, CI_TYPE::AFL)) return true;
    addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x afl-ci-field (%s)",
                                  ci_field, ciType(ci_field).c_str());
    afl_ci = ci_field;

    afl_len = *pos;
    addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x afl-len (%d)",
                                  afl_len, afl_len);

    int len = ciFieldLength(afl_ci);
    if (remaining < len) return expectedMore(__LINE__);

    afl_fc_b[0] = *(pos+0);
    afl_fc_b[1] = *(pos+1);
    afl_fc = afl_fc_b[1] << 8 | afl_fc_b[0];
    string afl_fc_info = toStringFromAFLFC(afl_fc);
    addExplanationAndIncrementPos(pos, 2, KindOfData::PROTOCOL, Understanding::FULL, "%02x%02x afl-fc (%s)",
                                  afl_fc_b[0], afl_fc_b[1], afl_fc_info.c_str());

    bool has_key_info = afl_fc & 0x0200;
    bool has_mac = afl_fc & 0x0400;
    bool has_counter = afl_fc & 0x0800;
    //bool has_len = afl_fc & 0x1000;
    bool has_control = afl_fc & 0x2000;
    //bool has_more_fragments = afl_fc & 0x4000;

    if (has_control)
    {
        afl_mcl = *pos;
        string afl_mcl_info = toStringFromAFLMC(afl_mcl);
        addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x afl-mcl (%s)",
                                      afl_mcl, afl_mcl_info.c_str());
    }

    if (has_key_info)
    {
        afl_ki_b[0] = *(pos+0);
        afl_ki_b[1] = *(pos+1);
        afl_ki = afl_ki_b[1] << 8 | afl_ki_b[0];
        string afl_ki_info = "";
        addExplanationAndIncrementPos(pos, 2, KindOfData::PROTOCOL, Understanding::FULL, "%02x%02x afl-ki (%s)",
                                      afl_ki_b[0], afl_ki_b[1], afl_ki_info.c_str());
    }

    if (has_counter)
    {
        afl_counter_b[0] = *(pos+0);
        afl_counter_b[1] = *(pos+1);
        afl_counter_b[2] = *(pos+2);
        afl_counter_b[3] = *(pos+3);
        afl_counter = afl_counter_b[3] << 24 |
            afl_counter_b[2] << 16 |
            afl_counter_b[1] << 8 |
            afl_counter_b[0];

        addExplanationAndIncrementPos(pos, 4, KindOfData::PROTOCOL, Understanding::FULL, "%02x%02x%02x%02x afl-counter (%u)",
                                      afl_counter_b[0],afl_counter_b[1],
                                      afl_counter_b[2],afl_counter_b[3],
                                      afl_counter);
    }

    if (has_mac)
    {
        int at = afl_mcl & 0x0f;
        AFLAuthenticationType aat = fromIntToAFLAuthenticationType(at);
        int len = toLen(aat);
        if (len != 2 &&
            len != 4 &&
            len != 8 &&
            len != 12 &&
            len != 16)
        {
            if (parser_warns_)
            {
                warning("(wmbus) WARNING! bad length of mac\n");
            }
            return false;
        }
        afl_mac_b.clear();
        for (int i=0; i<len; ++i)
        {
            afl_mac_b.insert(afl_mac_b.end(), *(pos+i));
        }
        string s = bin2hex(afl_mac_b);
        addExplanationAndIncrementPos(pos, len, KindOfData::PROTOCOL, Understanding::FULL, "%s afl-mac %d bytes", s.c_str(), len);
        must_check_mac = true;
    }

    return true;
}

string Telegram::toStringFromAFLFC(int fc)
{
    string info = "";
    int fid = fc & 0x00ff; // Fragmend id
    info += to_string(fid);
    info += " ";
    if (fc & 0x0200) info += "KeyInfoInFragment ";
    if (fc & 0x0400) info += "MACInFragment ";
    if (fc & 0x0800) info += "MessCounterInFragment ";
    if (fc & 0x1000) info += "MessLenInFragment ";
    if (fc & 0x2000) info += "MessControlInFragment ";
    if (fc & 0x4000) info += "MoreFragments ";
    else             info += "LastFragment ";
    if (info.length() > 0) info.pop_back();
    return info;
}

string Telegram::toStringFromAFLMC(int mc)
{
    string info = "";
    int at = mc & 0x0f;
    AFLAuthenticationType aat = fromIntToAFLAuthenticationType(at);
    info += toString(aat);
    info += " ";
    if (mc & 0x10) info += "KeyInfo ";
    if (mc & 0x20) info += "MessCounter ";
    if (mc & 0x40) info += "MessLen ";
    if (info.length() > 0) info.pop_back();
    return info;
}

string Telegram::toStringFromTPLConfig(int cfg)
{
    string info = "";
    if (cfg & 0x8000) info += "bidirectional ";
    if (cfg & 0x4000) info += "accessibility ";
    if (cfg & 0x2000) info += "synchronous ";
    if (cfg & 0x1f00)
    {
        int m = (cfg >> 8) & 0x1f;
        TPLSecurityMode tsm = fromIntToTPLSecurityMode(m);
        info += toString(tsm);
        info += " ";
        if (tsm == TPLSecurityMode::AES_CBC_IV)
        {
            int num_blocks = (cfg & 0x00f0) >> 4;
            int cntn = (cfg & 0x000c) >> 2;
            int ra = (cfg & 0x0002) >> 1;
            int hc = cfg & 0x0001;
            info += "nb="+to_string(num_blocks);
            info += " cntn="+to_string(cntn);
            info += " ra="+to_string(ra);
            info += " hc="+to_string(hc);
            info += " ";
        }
    }
    if (info.length() > 0) info.pop_back();
    return info;
}

bool Telegram::parseTPLConfig(std::vector<uchar>::iterator &pos)
{
    CHECK(2);
    uchar cfg1 = *(pos+0);
    uchar cfg2 = *(pos+1);
    tpl_cfg = cfg2 << 8 | cfg1;

    if (tpl_cfg & 0x1f00)
    {
        int m = (tpl_cfg >> 8) & 0x1f;
        tpl_sec_mode = fromIntToTPLSecurityMode(m);
    }
    bool has_cfg_ext = false;
    string info = toStringFromTPLConfig(tpl_cfg);
    info += " ";
    if (tpl_sec_mode == TPLSecurityMode::AES_CBC_IV) // Security mode 5
    {
        tpl_num_encr_blocks = (tpl_cfg >> 4) & 0x0f;
    }
    if (tpl_sec_mode == TPLSecurityMode::AES_CBC_NO_IV) // Security mode 7
    {
        tpl_num_encr_blocks = (tpl_cfg >> 4) & 0x0f;
        has_cfg_ext = true;
    }
    addExplanationAndIncrementPos(pos, 2, KindOfData::PROTOCOL, Understanding::FULL,
                                  "%02x%02x tpl-cfg %04x (%s)", cfg1, cfg2, tpl_cfg, info.c_str());

    if (has_cfg_ext)
    {
        CHECK(1);
        tpl_cfg_ext = *(pos+0);
        tpl_kdf_selection = (tpl_cfg_ext >> 4) & 3;

        addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL,
                                      "%02x tpl-cfg-ext (KDFS=%d)", tpl_cfg_ext, tpl_kdf_selection);

        if (tpl_kdf_selection == 1)
        {
            vector<uchar> input;
            vector<uchar> mac;
            mac.resize(16);

            // DC C ID 0x07 0x07 0x07 0x07 0x07 0x07 0x07
            // Derivation Constant DC = 0x00 = encryption from meter.
            //                          0x01 = mac from meter.
            //                          0x10 = encryption from communication partner.
            //                          0x11 = mac from communication partner.
            input.insert(input.end(), 0x00); // DC 00 = generate ephemereal encryption key from meter.
            // If there is a tpl_counter, then use it, else use afl_counter.
            input.insert(input.end(), afl_counter_b, afl_counter_b+4);
            // If there is a tpl_id, then use it, else use ddl_id.
            if (tpl_id_found)
            {
                input.insert(input.end(), tpl_id_b, tpl_id_b+4);
            }
            else
            {
                input.insert(input.end(), dll_id_b, dll_id_b+4);
            }

            // Pad.
            for (int i=0; i<7; ++i) input.insert(input.end(), 0x07);

            debugPayload("(wmbus) input to kdf for enc", input);

            if (meter_keys == NULL || meter_keys->confidentiality_key.size() != 16)
            {
                if (isSimulated())
                {
                    debug("(wmbus) simulation without keys, not generating Kmac and Kenc.\n");
                    return true;
                }
                debug("(wmbus) no key, thus cannot execute kdf.\n");
                return false;
            }
            AES_CMAC(safeButUnsafeVectorPtr(meter_keys->confidentiality_key),
                     safeButUnsafeVectorPtr(input), 16,
                     safeButUnsafeVectorPtr(mac));
            string s = bin2hex(mac);
            debug("(wmbus) ephemereal Kenc %s\n", s.c_str());
            tpl_generated_key.clear();
            tpl_generated_key.insert(tpl_generated_key.end(), mac.begin(), mac.end());

            input[0] = 0x01; // DC 01 = generate ephemereal mac key from meter.
            mac.clear();
            mac.resize(16);
            debugPayload("(wmbus) input to kdf for mac", input);
            AES_CMAC(safeButUnsafeVectorPtr(meter_keys->confidentiality_key),
                     safeButUnsafeVectorPtr(input), 16,
                     safeButUnsafeVectorPtr(mac));
            s = bin2hex(mac);
            debug("(wmbus) ephemereal Kmac %s\n", s.c_str());
            tpl_generated_mac_key.clear();
            tpl_generated_mac_key.insert(tpl_generated_mac_key.end(), mac.begin(), mac.end());
        }
    }

    return true;
}

bool Telegram::parseShortTPL(std::vector<uchar>::iterator &pos)
{
    CHECK(1);

    tpl_acc = *pos;
    addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL,
                                  "%02x tpl-acc-field", tpl_acc);

    CHECK(1);
    tpl_sts = *pos;
    tpl_sts_offset = distance(frame.begin(), pos);
    addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL,
                                  "%02x tpl-sts-field (%s)", tpl_sts, decodeTPLStatusByteOnlyStandardBits(tpl_sts).c_str());
    bool ok = parseTPLConfig(pos);
    if (!ok) return false;

    return true;
}

bool Telegram::parseLongTPL(std::vector<uchar>::iterator &pos)
{
    CHECK(8);
    addAddressIdFirst(pos);

    tpl_id_found = true;
    tpl_id_b[0] = *(pos+0);
    tpl_id_b[1] = *(pos+1);
    tpl_id_b[2] = *(pos+2);
    tpl_id_b[3] = *(pos+3);

    tpl_a.resize(6);
    for (int i=0; i<4; ++i)
    {
        tpl_a[i] = *(pos+i);
    }

    addExplanationAndIncrementPos(pos, 4, KindOfData::PROTOCOL, Understanding::FULL,
                                  "%02x%02x%02x%02x tpl-id (%02x%02x%02x%02x)",
                                  tpl_id_b[0], tpl_id_b[1], tpl_id_b[2], tpl_id_b[3],
                                  tpl_id_b[3], tpl_id_b[2], tpl_id_b[1], tpl_id_b[0]);

    CHECK(2);
    tpl_mfct_b[0] = *(pos+0);
    tpl_mfct_b[1] = *(pos+1);
    tpl_mfct = tpl_mfct_b[1] << 8 | tpl_mfct_b[0];
    string man = manufacturerFlag(tpl_mfct);
    addExplanationAndIncrementPos(pos, 2, KindOfData::PROTOCOL, Understanding::FULL, "%02x%02x tpl-mfct (%s)", tpl_mfct_b[0], tpl_mfct_b[1], man.c_str());

    CHECK(1);
    tpl_version = *(pos+0);
    tpl_a[4] = *(pos+0);
    addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x tpl-version", tpl_version);

    CHECK(1);
    tpl_type = *(pos+0);
    tpl_a[5] = *(pos+0);
    string info = mediaType(tpl_type, tpl_mfct);
    addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x tpl-type (%s)", tpl_type, info.c_str());

    bool ok = parseShortTPL(pos);

    return ok;
}

bool Telegram::checkMAC(std::vector<uchar> &frame,
                        std::vector<uchar>::iterator from,
                        std::vector<uchar>::iterator to,
                        std::vector<uchar> &inmac,
                        std::vector<uchar> &mackey)
{
    vector<uchar> input;
    vector<uchar> mac;
    mac.resize(16);

    if (mackey.size() != 16) return false;
    if (inmac.size() == 0) return false;

    // AFL.MAC = CMAC (Kmac/Lmac,
    //                 AFL.MCL || AFL.MCR || {AFL.ML || } NextCI || ... || Last Byte of message)

    input.insert(input.end(), afl_mcl);
    input.insert(input.end(), afl_counter_b, afl_counter_b+4);
    input.insert(input.end(), from, to);
    string s = bin2hex(input);
    debug("(wmbus) input to mac %s\n", s.c_str());
    AES_CMAC(safeButUnsafeVectorPtr(mackey),
             safeButUnsafeVectorPtr(input), input.size(),
             safeButUnsafeVectorPtr(mac));
    string calculated = bin2hex(mac);
    debug("(wmbus) calculated mac %s\n", calculated.c_str());
    string received = bin2hex(inmac);
    debug("(wmbus) received   mac %s\n", received.c_str());
    string truncated = calculated.substr(0, received.length());
    bool ok = truncated == received;
    if (ok)
    {
        debug("(wmbus) mac ok!\n");
    }
    else
    {
        debug("(wmbus) mac NOT ok!\n");
        explainParse("BADMAC", 0);
    }
    return ok;
}

bool loadFormatBytesFromSignature(uint16_t format_signature, vector<uchar> *format_bytes);

bool Telegram::alreadyDecryptedCBC(vector<uchar>::iterator &pos)
{
    CHECK(2);
    if (*(pos+0) != 0x2f || *(pos+1) != 0x2f) return false;
    addExplanationAndIncrementPos(pos, 2, KindOfData::PROTOCOL, Understanding::FULL, "%02x%02x already decrypted check bytes", *(pos+0), *(pos+1));
    return true;
}

bool Telegram::potentiallyDecrypt(vector<uchar>::iterator &pos)
{
    if (tpl_sec_mode == TPLSecurityMode::AES_CBC_IV)
    {
        if (alreadyDecryptedCBC(pos))
        {
            if (meter_keys && meter_keys->hasConfidentialityKey())
            {
                // Oups! This telegram is already decrypted (but the header still says it should be encrypted)
                // this is probably a replay telegram from --logtelegrams.
                // But since we have specified a key! Do not accept this telegram!
                warning("(wmbus) WARNING!! telegram should have been fully encrypted, but was not! "
                        "id: %02x%02x%02x%02x mfct: (%s) %s (0x%02x) type: %s (0x%02x) ver: 0x%02x\n",
                            dll_id_b[3], dll_id_b[2], dll_id_b[1], dll_id_b[0],
                            manufacturerFlag(dll_mfct).c_str(),
                            manufacturer(dll_mfct).c_str(),
                            dll_mfct,
                            mediaType(dll_type, dll_mfct).c_str(), dll_type,
                            dll_version);
                return false;
            }
            return true;
        }
        if (!meter_keys) return false;
        if (!meter_keys->hasConfidentialityKey())
        {
            addDefaultManufacturerKeyIfAny(frame, tpl_sec_mode, meter_keys);
        }
        int num_encrypted_bytes = 0;
        int num_not_encrypted_at_end = 0;

        bool ok = decrypt_TPL_AES_CBC_IV(this, frame, pos, meter_keys->confidentiality_key,
                                         &num_encrypted_bytes, &num_not_encrypted_at_end);
        if (!ok)
        {
            // No key supplied.
            string info =  bin2hex(pos, frame.end(), num_encrypted_bytes);
            info += " encrypted";
            addExplanationAndIncrementPos(pos, num_encrypted_bytes, KindOfData::CONTENT, Understanding::ENCRYPTED, info.c_str());
            if (parser_warns_)
            {
                if (!beingAnalyzed() && (isVerboseEnabled() || isDebugEnabled() || !warned_for_telegram_before(this, dll_a)))
                {
                    // Print this warning only once! Unless you are using verbose or debug.
                    warning("(wmbus) WARNING! no key to decrypt payload! "
                            "Permanently ignoring telegrams from id: %02x%02x%02x%02x mfct: (%s) %s (0x%02x) type: %s (0x%02x) ver: 0x%02x\n",
                            dll_id_b[3], dll_id_b[2], dll_id_b[1], dll_id_b[0],
                            manufacturerFlag(dll_mfct).c_str(),
                            manufacturer(dll_mfct).c_str(),
                            dll_mfct,
                            mediaType(dll_type, dll_mfct).c_str(), dll_type,
                            dll_version);
                }
            }
            return false;
        }
        // Now the frame from pos and onwards has been decrypted.

        CHECK(2);
        uchar a = *(pos+0);
        uchar b = *(pos+1);

        addExplanationAndIncrementPos(pos, 2, KindOfData::PROTOCOL, Understanding::FULL,
                                      "%02x%02x decrypt check bytes (%s)", *(pos+0), *(pos+1),
                                      (*(pos+0) == 0x2f && *(pos+1) == 0x2f) ? "OK":"ERROR should be 2f2f");

        if ((a != 0x2f || b != 0x2f) && !FUZZING)
        {
            // Wrong key supplied.
            int num_bytes = distance(pos, frame.end());
            string info = bin2hex(pos, frame.end(), num_bytes);
            info += " failed decryption. Wrong key?";
            addExplanationAndIncrementPos(pos, num_bytes, KindOfData::CONTENT, Understanding::ENCRYPTED, info.c_str());

            if (parser_warns_)
            {
                if (!beingAnalyzed() && (isVerboseEnabled() || isDebugEnabled() || !warned_for_telegram_before(this, dll_a)))
                {
                    // Print this warning only once! Unless you are using verbose or debug.
                    warning("(wmbus) WARNING!! decrypted content failed check, did you use the correct decryption key? "
                            "Permanently ignoring telegrams from id: %02x%02x%02x%02x mfct: (%s) %s (0x%02x) type: %s (0x%02x) ver: 0x%02x\n",
                            dll_id_b[3], dll_id_b[2], dll_id_b[1], dll_id_b[0],
                            manufacturerFlag(dll_mfct).c_str(),
                            manufacturer(dll_mfct).c_str(),
                            dll_mfct,
                            mediaType(dll_type, dll_mfct).c_str(), dll_type,
                            dll_version);
                }
            }
            return false;
        }
    }
    else if (tpl_sec_mode == TPLSecurityMode::AES_CBC_NO_IV)
    {
        if (alreadyDecryptedCBC(pos))
        {
            if (meter_keys && meter_keys->hasConfidentialityKey())
            {
                // Oups! This telegram is already decrypted (but the header still says it should be encrypted)
                // this is probably a replay telegram from --logtelegrams.
                // But since we have specified a key! Do not accept this telegram!
                warning("(wmbus) WARNING! telegram should have been fully encrypted, but was not! "
                        "id: %02x%02x%02x%02x mfct: (%s) %s (0x%02x) type: %s (0x%02x) ver: 0x%02x\n",
                            dll_id_b[3], dll_id_b[2], dll_id_b[1], dll_id_b[0],
                            manufacturerFlag(dll_mfct).c_str(),
                            manufacturer(dll_mfct).c_str(),
                            dll_mfct,
                            mediaType(dll_type, dll_mfct).c_str(), dll_type,
                            dll_version);
                return false;
            }
            return true;
        }

        bool mac_ok = checkMAC(frame, tpl_start, frame.end(), afl_mac_b, tpl_generated_mac_key);

        // Do not attempt to decrypt if the mac has failed!
        if (!mac_ok)
        {
            if (parser_warns_)
            {
                if (!beingAnalyzed() && (isVerboseEnabled() || isDebugEnabled() || !warned_for_telegram_before(this, dll_a)))
                {
                    // Print this warning only once! Unless you are using verbose or debug.
                    warning("(wmbus) WARNING! telegram mac check failed, did you use the correct decryption key? "
                            "Permanently ignoring telegrams from id: %02x%02x%02x%02x mfct: (%s) %s (0x%02x) type: %s (0x%02x) ver: 0x%02x\n",
                            dll_id_b[3], dll_id_b[2], dll_id_b[1], dll_id_b[0],
                            manufacturerFlag(dll_mfct).c_str(),
                            manufacturer(dll_mfct).c_str(),
                            dll_mfct,
                            mediaType(dll_type, dll_mfct).c_str(), dll_type,
                            dll_version);
                    return false;
                }

                string info =  bin2hex(pos, frame.end(), frame.end()-pos);
                info += " encrypted mac failed";
                addExplanationAndIncrementPos(pos, frame.end()-pos, KindOfData::CONTENT, Understanding::ENCRYPTED, info.c_str());
                if (meter_keys->confidentiality_key.size() > 0)
                {
                    // Only fail if we gave an explicit key.
                    return false;
                }
                return true;
            }
            return false;
        }

        int num_encrypted_bytes = 0;
        int num_not_encrypted_at_end = 0;
        bool ok = decrypt_TPL_AES_CBC_NO_IV(this, frame, pos, tpl_generated_key,
                                            &num_encrypted_bytes,
                                            &num_not_encrypted_at_end);
        if (!ok)
        {
            addExplanationAndIncrementPos(pos, num_encrypted_bytes, KindOfData::CONTENT, Understanding::FULL,
                                          "encrypted data");
            return false;
        }

        // Now the frame from pos and onwards has been decrypted.
        CHECK(2);
        uchar a = *(pos+0);
        uchar b = *(pos+1);
        addExplanationAndIncrementPos(pos, 2, KindOfData::PROTOCOL, Understanding::FULL,
                                      "%02x%02x decrypt check bytes (%s)", a, b,
                                      (a == 0x2f && b == 0x2f) ? "OK":"ERROR should be 2f2f");

        if ((a != 0x2f || b != 0x2f) && !FUZZING)
        {
            // Wrong key supplied.
            string info = bin2hex(pos, frame.end(), num_encrypted_bytes);
            info += " failed decryption. Wrong key?";
            addExplanationAndIncrementPos(pos, num_encrypted_bytes, KindOfData::CONTENT, Understanding::ENCRYPTED, info.c_str());

            if (parser_warns_)
            {
                if (!beingAnalyzed() && (isVerboseEnabled() || isDebugEnabled() || !warned_for_telegram_before(this, dll_a)))
                {
                    // Print this warning only once! Unless you are using verbose or debug.
                    warning("(wmbus) WARNING!!! decrypted content failed check, did you use the correct decryption key? "
                            "Permanently ignoring telegrams from id: %02x%02x%02x%02x mfct: (%s) %s (0x%02x) type: %s (0x%02x) ver: 0x%02x\n",
                            dll_id_b[3], dll_id_b[2], dll_id_b[1], dll_id_b[0],
                            manufacturerFlag(dll_mfct).c_str(),
                            manufacturer(dll_mfct).c_str(),
                            dll_mfct,
                            mediaType(dll_type, dll_mfct).c_str(), dll_type,
                            dll_version);
                }
            }
            return false;
        }
    }
    else if (tpl_sec_mode == TPLSecurityMode::SPECIFIC_16_31)
    {
        debug("(wmbus) non-standard security mode 16_31\n");
        if (mustDecryptDiehlRealData(frame))
        {
            debug("(diehl) must decode frame\n");
            if (!meter_keys) return false;
            bool ok = decryptDielhRealData(this, frame, pos, meter_keys->confidentiality_key);
            // If this telegram is simulated, the content might already be decrypted and the
            // decruption will fail. But we can assume all is well anyway!
            if (!ok && isSimulated()) return true;
            if (!ok) return false;
            // Now the frame from pos and onwards has been decrypted.
            debug("(diehl) decryption successful\n");
        }
    }
    else
    if (meter_keys && meter_keys->hasConfidentialityKey())
    {
        // Oups! This telegram is NOT encrypted, but we have specified a key!
        // Do not accept this telegram!
        warning("(wmbus) WARNING!!! telegram should have been encrypted, but was not! "
                "id: %02x%02x%02x%02x mfct: (%s) %s (0x%02x) type: %s (0x%02x) ver: 0x%02x\n",
                dll_id_b[3], dll_id_b[2], dll_id_b[1], dll_id_b[0],
                manufacturerFlag(dll_mfct).c_str(),
                manufacturer(dll_mfct).c_str(),
                dll_mfct,
                mediaType(dll_type, dll_mfct).c_str(), dll_type,
                dll_version);
        return false;
    }

    return true;
}

bool Telegram::parse_TPL_72(vector<uchar>::iterator &pos)
{
    bool ok = parseLongTPL(pos);
    if (!ok) return false;

    bool decrypt_ok = potentiallyDecrypt(pos);

    header_size = distance(frame.begin(), pos);
    int remaining = distance(pos, frame.end())-suffix_size;

    if (decrypt_ok)
    {
        parseDV(this, frame, pos, remaining, &dv_entries);
    }
    else
    {
        decryption_failed = true;
    }

    return true;
}

bool Telegram::parse_TPL_78(vector<uchar>::iterator &pos)
{
    header_size = distance(frame.begin(), pos);
    int remaining = distance(pos, frame.end())-suffix_size;
    parseDV(this, frame, pos, remaining, &dv_entries);

    return true;
}

bool Telegram::parse_TPL_79(vector<uchar>::iterator &pos)
{
    // Compact frame
    CHECK(2);
    uchar ecrc0 = *(pos+0);
    uchar ecrc1 = *(pos+1);
    size_t offset = distance(frame.begin(), pos);
    addExplanationAndIncrementPos(pos, 2, KindOfData::PROTOCOL, Understanding::FULL,
                                  "%02x%02x format signature", ecrc0, ecrc1);
    format_signature = ecrc1<<8 | ecrc0;

    vector<uchar> format_bytes;
    bool ok = loadFormatBytesFromSignature(format_signature, &format_bytes);
    if (!ok) {
        // We have not yet seen a long frame, but we know the formats for some
        // meter specific hashes.
        ok = findFormatBytesFromKnownMeterSignatures(&format_bytes);
        if (!ok)
        {
            addMoreExplanation(offset, " (unknown)");
            int num_compressed_bytes = distance(pos, frame.end());
            string info = bin2hex(pos, frame.end(), num_compressed_bytes);
            info += " compressed and signature unknown";
            addExplanationAndIncrementPos(pos, distance(pos, frame.end()), KindOfData::CONTENT, Understanding::COMPRESSED, info.c_str());

            verbose("(wmbus) ignoring compressed telegram since format signature hash 0x%02x is yet unknown.\n"
                    "     this is not a problem, since you only need wait for at most 8 telegrams\n"
                    "     (8*16 seconds) until an full length telegram arrives and then we know\n"
                    "     the format giving this hash and start decoding the telegrams properly.\n",
                    format_signature);
            return false;
        }
    }
    vector<uchar>::iterator format = format_bytes.begin();

    // 2,3 = crc for payload = hash over both DRH and data bytes. Or is it only over the data bytes?
    CHECK(2);
    int ecrc2 = *(pos+0);
    int ecrc3 = *(pos+1);
    addExplanationAndIncrementPos(pos, 2, KindOfData::PROTOCOL, Understanding::FULL,
                                  "%02x%02x data crc", ecrc2, ecrc3);

    header_size = distance(frame.begin(), pos);
    int remaining = distance(pos, frame.end())-suffix_size;

    parseDV(this, frame, pos, remaining, &dv_entries, &format, format_bytes.size());

    return true;
}

bool Telegram::parse_TPL_7A(vector<uchar>::iterator &pos)
{
    bool ok = parseShortTPL(pos);
    if (!ok) return false;

    bool decrypt_ok = potentiallyDecrypt(pos);

    header_size = distance(frame.begin(), pos);
    int remaining = distance(pos, frame.end())-suffix_size;

    if (decrypt_ok)
    {
        parseDV(this, frame, pos, remaining, &dv_entries);
    }
    else
    {
        decryption_failed = true;
    }
    return true;
}

bool Telegram::parseTPL(vector<uchar>::iterator &pos)
{
    int remaining = distance(pos, frame.end());
    if (remaining == 0) return false;

    debug("(wmbus) parseTPL @%d %d\n", distance(frame.begin(), pos), remaining);
    CHECK(1);
    int ci_field = *pos;
    int mfct_specific = isCiFieldManufacturerSpecific(ci_field);

    if (!isCiFieldOfType(ci_field, CI_TYPE::TPL) && !mfct_specific)
    {
        addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::NONE,
                                      "%02x unknown ci-field",
                                      ci_field);
        if (parser_warns_)
        {
            warning("(wmbus) Unknown tpl-ci-field %02x\n", ci_field);
        }
        return false;
    }
    tpl_ci = ci_field;
    tpl_start = pos;

    addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL,
                                  "%02x tpl-ci-field (%s)",
                                  tpl_ci, ciType(tpl_ci).c_str());
    int len = ciFieldLength(tpl_ci);

    if (remaining < len+1 && ! mfct_specific) return expectedMore(__LINE__);

    switch (tpl_ci)
    {
        case CI_Field_Values::TPL_72: return parse_TPL_72(pos);
        case CI_Field_Values::TPL_78: return parse_TPL_78(pos);
        case CI_Field_Values::TPL_79: return parse_TPL_79(pos);
        case CI_Field_Values::TPL_7A: return parse_TPL_7A(pos);
        default:
        {
            // A0 to B7 are manufacturer specific.
            header_size = distance(frame.begin(), pos);
            int num_mfct_bytes = frame.end()-pos-suffix_size;
            string info =  bin2hex(pos, frame.end(), num_mfct_bytes);
            info += " mfct specific";
            addExplanationAndIncrementPos(pos, num_mfct_bytes, KindOfData::CONTENT, Understanding::NONE, info.c_str());

            return true; // Manufacturer specific telegram payload. Oh well....
        }
    }

    header_size = distance(frame.begin(), pos);
    if (parser_warns_)
    {
        warning("(wmbus) Not implemented tpl-ci %02x\n", tpl_ci);
    }
    return false;
}

void Telegram::preProcess()
{
    DiehlAddressTransformMethod diehl_method = mustTransformDiehlAddress(frame);
    if (diehl_method != DiehlAddressTransformMethod::NONE)
    {
        debug("(diehl) preprocess necessary %s\n", toString(diehl_method));
        original = vector<uchar>(frame.begin(), frame.begin() + 10);
        transformDiehlAddress(frame, diehl_method);
    }
}

bool Telegram::parse(vector<uchar> &input_frame, MeterKeys *mk, bool warn)
{
    switch (about.type)
    {
    case FrameType::WMBUS: return parseWMBUS(input_frame, mk, warn);
    case FrameType::MBUS: return parseMBUS(input_frame, mk, warn);
    case FrameType::HAN: return parseHAN(input_frame, mk, warn);
    }
    assert(0);
    return false;
}

bool Telegram::parseHeader(vector<uchar> &input_frame)
{
    switch (about.type)
    {
    case FrameType::WMBUS: return parseWMBUSHeader(input_frame);
    case FrameType::MBUS: return parseMBUSHeader(input_frame);
    case FrameType::HAN: return parseHANHeader(input_frame);
    }
    assert(0);
    return false;
}

bool Telegram::parseWMBUSHeader(vector<uchar> &input_frame)
{
    assert(about.type == FrameType::WMBUS);

    bool ok;
    // Parsing the header is used to extract the ids, so that we can
    // match the telegram towards any known ids and thus keys.
    // No need to warn.
    parser_warns_ = false;
    decryption_failed = false;
    explanations.clear();
    suffix_size = 0;
    frame = input_frame;
    vector<uchar>::iterator pos = frame.begin();
    // Parsed accumulates parsed bytes.
    parsed.clear();
    // Fixes quirks from non-compliant meters to make telegram compatible with the standard
    preProcess();

    ok = parseDLL(pos);
    if (!ok) return false;

    // At the worst, only the DLL is parsed. That is fine.
    ok = parseELL(pos);
    if (!ok) return true;
    // Could not decrypt stop here.
    if (decryption_failed) return true;

    ok = parseNWL(pos);
    if (!ok) return true;

    ok = parseAFL(pos);
    if (!ok) return true;

    ok = parseTPL(pos);
    if (!ok) return true;

    return true;
}

bool Telegram::parseWMBUS(vector<uchar> &input_frame, MeterKeys *mk, bool warn)
{
    assert(about.type == FrameType::WMBUS);

    parser_warns_ = warn;
    decryption_failed = false;
    explanations.clear();
    suffix_size = 0;
    meter_keys = mk;
    assert(meter_keys != NULL);
    bool ok;
    frame = input_frame;
    vector<uchar>::iterator pos = frame.begin();
    // Parsed accumulates parsed bytes.
    parsed.clear();
    // Fixes quirks from non-compliant meters to make telegram compatible with the standard
    preProcess();
    //     ┌──────────────────────────────────────────────┐
    //     │                                              │
    //     │ Parse DLL Data Link Layer for Wireless MBUS. │
    //     │                                              │
    //     └──────────────────────────────────────────────┘

    ok = parseDLL(pos);
    if (!ok) return false;

    printDLL();

    //     ┌──────────────────────────────────────────────┐
    //     │                                              │
    //     │ Is this an ELL block?                        │
    //     │                                              │
    //     └──────────────────────────────────────────────┘

    ok = parseELL(pos);
    if (!ok) return false;

    printELL();
    if (decryption_failed) return false;

    //     ┌──────────────────────────────────────────────┐
    //     │                                              │
    //     │ Is this an NWL block?                        │
    //     │                                              │
    //     └──────────────────────────────────────────────┘

    ok = parseNWL(pos);
    if (!ok) return false;

    printNWL();

    //     ┌──────────────────────────────────────────────┐
    //     │                                              │
    //     │ Is this an AFL block?                        │
    //     │                                              │
    //     └──────────────────────────────────────────────┘

    ok = parseAFL(pos);
    if (!ok) return false;

    printAFL();

    //     ┌──────────────────────────────────────────────┐
    //     │                                              │
    //     │ Is this a TPL block? It ought to be!         │
    //     │                                              │
    //     └──────────────────────────────────────────────┘

    ok = parseTPL(pos);
    if (!ok) return false;

    printTPL();
    if (decryption_failed) return false;

    return true;
}

bool Telegram::parseMBUSHeader(vector<uchar> &input_frame)
{
    assert(about.type == FrameType::MBUS);

    bool ok;
    // Parsing the header is used to extract the ids, so that we can
    // match the telegram towards any known ids and thus keys.
    // No need to warn.
    parser_warns_ = false;
    decryption_failed = false;
    explanations.clear();
    suffix_size = 0;
    frame = input_frame;
    vector<uchar>::iterator pos = frame.begin();
    // Parsed accumulates parsed bytes.
    parsed.clear();

    ok = parseMBusDLLandTPL(pos);
    if (!ok) return false;

    return true;
}

bool Telegram::parseMBUS(vector<uchar> &input_frame, MeterKeys *mk, bool warn)
{
    assert(about.type == FrameType::MBUS);

    parser_warns_ = warn;
    decryption_failed = false;
    explanations.clear();
    suffix_size = 0;
    meter_keys = mk;
    assert(meter_keys != NULL);
    bool ok;
    frame = input_frame;
    vector<uchar>::iterator pos = frame.begin();
    // Parsed accumulates parsed bytes.
    parsed.clear();

    //     ┌──────────────────────────────────────────────┐
    //     │                                              │
    //     │ Parse DLL Data Link Layer for Wireless MBUS. │
    //     │                                              │
    //     └──────────────────────────────────────────────┘

    ok = parseMBusDLLandTPL(pos);
    if (!ok) return false;

    return true;
}

bool Telegram::parseHANHeader(vector<uchar> &input_frame)
{
    assert(about.type == FrameType::HAN);

    return false;
}

bool Telegram::parseHAN(vector<uchar> &input_frame, MeterKeys *mk, bool warn)
{
    assert(about.type == FrameType::HAN);

    return false;
}

void Telegram::explainParse(string intro, int from)
{
    for (auto& p : explanations)
    {
        // Protocol or content?
        const char *c = p.kind == KindOfData::PROTOCOL ? " " : "C";
        const char *u = "?";
        if (p.understanding == Understanding::FULL) u = "!";
        if (p.understanding == Understanding::PARTIAL) u = "p";
        if (p.understanding == Understanding::ENCRYPTED) u = "E";
        if (p.understanding == Understanding::COMPRESSED) u = "C";

        // Do not print ok for understood protocol, it is implicit.
        // However if a protocol is not full understood then print p or ?.
        if (p.kind == KindOfData::PROTOCOL && p.understanding == Understanding::FULL) u = " ";

        debug("%s %03d %s%s: %s\n", intro.c_str(), p.pos, c, u, p.info.c_str());
    }
}

string renderAnalysisAsText(vector<Explanation> &explanations, OutputFormat of)
{
    string s;

    const char *green;
    const char *yellow;
    const char *red;
    const char *reset;

    if (of == OutputFormat::TERMINAL)
    {
        green = "\033[0;97m\033[1;42m";
        yellow = "\033[0;97m\033[0;43m";
        red = "\033[0;97m\033[0;41m\033[1;37m";
        reset = "\033[0m";
    }
    else if (of == OutputFormat::HTML)
    {
        green = "<span style=\"color:white;background-color:#008450;\">";
        yellow = "<span style=\"color:white;background-color:#efb700;\">";
        red = "<span style=\"color:white;background-color:#b81d13;\">";
        reset = "</span>";
    }
    else
    {
        green = "";
        yellow = "";
        red = "";
        reset = "";
    }

    for (auto& p : explanations)
    {
        // Protocol or content?
        const char *c = p.kind == KindOfData::PROTOCOL ? " " : "C";
        const char *u = "?";
        if (p.understanding == Understanding::FULL) u = "!";
        if (p.understanding == Understanding::PARTIAL) u = "p";
        if (p.understanding == Understanding::ENCRYPTED) u = "E";
        if (p.understanding == Understanding::COMPRESSED) u = "C";

        // Do not print ok for understood protocol, it is implicit.
        // However if a protocol is not full understood then print p or ?.
        if (p.kind == KindOfData::PROTOCOL && p.understanding == Understanding::FULL) u = " ";

        const char *pre;
        const char *post = reset;

        if (*u == '!')
        {
            pre = green;
        }
        else if (*u == 'p')
        {
            pre = yellow;
        }
        else if (*u == ' ')
        {
            pre = "";
            post = "";
        }
        else
        {
            pre = red;
        }

        s += tostrprintf("%03d %s%s: %s%s%s\n", p.pos, c, u, pre, p.info.c_str(), post);
    }
    return s;
}

string renderAnalysisAsJson(vector<Explanation> &explanations)
{
    return "{ \"TODO\": true }\n";
}

string Telegram::analyzeParse(OutputFormat format, int *content_length, int *understood_content_length)
{
    int u = 0;
    int l = 0;

    sort(explanations.begin(), explanations.end(),
         [](const Explanation & a, const Explanation & b) -> bool { return a.pos < b.pos; });

    // Calculate how much is understood.
    for (auto& e : explanations)
    {
        if (e.kind == KindOfData::CONTENT)
        {
            l += e.len;
            if (e.understanding == Understanding::PARTIAL ||
                e.understanding == Understanding::FULL)
            {
                // Its content and we have at least some understanding.
                u += e.len;
            }
        }
    }
    *content_length = l;
    *understood_content_length = u;

    switch(format)
    {
    case OutputFormat::PLAIN :
    case OutputFormat::HTML :
    case OutputFormat::TERMINAL:
    {
        return renderAnalysisAsText(explanations, format);
        break;
    }
    case OutputFormat::JSON:
        return renderAnalysisAsJson(explanations);
        break;
    case OutputFormat::NONE:
        // Do nothing
        return "";
        break;
    }
    return "ERROR";
}

void detectMeterDrivers(int manufacturer, int version, int type, std::vector<std::string> *drivers);

string Telegram::autoDetectPossibleDrivers()
{
    vector<string> drivers;
    detectMeterDrivers(dll_mfct, dll_version, dll_type, &drivers);
    if (tpl_id_found)
    {
        detectMeterDrivers(tpl_mfct, tpl_version, tpl_type, &drivers);
    }
    string possibles;
    for (string d : drivers) possibles = possibles+d+" ";
    if (possibles != "") possibles.pop_back();
    else possibles = "unknown!";

    return possibles;
}

const char *mbusCField(uchar c_field)
{
    string s;
    switch (c_field)
    {
    case 0x08: return "RSP_UD2";
    }
    return "?";
}

const char *mbusCiField(uchar c_field)
{
    string s;
    switch (c_field)
    {
    case 0x78: return "no header";
    case 0x7a: return "short header";
    case 0x72: return "long header";
    case 0x79: return "no header compact frame";
    case 0x7b: return "short header compact frame";
    case 0x73: return "long header compact frame";
    case 0x69: return "no header format frame";
    case 0x6a: return "short header format frame";
    case 0x6b: return "long header format frame";
    }
    return "?";
}

string cType(int c_field)
{
    string s;
    if (c_field & 0x80)
    {
        s += "relayed ";
    }

    if (c_field & 0x40)
    {
        s += "from meter ";
    }
    else
    {
        s += "to meter ";
    }

    int code = c_field & 0x0f;

    switch (code) {
    case 0x0: s += "SND_NKE"; break; // to meter, link reset
    case 0x3: s += "SND_UD2"; break; // to meter, command = user data
    case 0x4: s += "SND_NR"; break; // from meter, unsolicited data, no response expected
    case 0x5: s += "SND_UD3"; break; // to multiple meters, command = user data, no response expected
    case 0x6: s += "SND_IR"; break; // from meter, installation request/data
    case 0x7: s += "ACC_NR"; break; // from meter, unsolicited offers to access the meter
    case 0x8: s += "ACC_DMD"; break; // from meter, unsolicited demand to access the meter
    case 0xa: s += "REQ_UD1"; break; // to meter, alarm request
    case 0xb: s += "REQ_UD2"; break; // to meter, data request
    }

    return s;
}

bool isValidWMBusCField(int c_field)
{
    // These are the currently seen valid C fields for wmbus telegrams.
    // 0x46 is only from an ei6500 meter.... all else is ox44
    // However in the future we might see relayed telegrams which will perhaps have
    // some other c field.
    return
        c_field == 0x44 ||
        c_field == 0x46;
}

bool isValidMBusCField(int c_field)
{
    return false;
}

string ccType(int cc_field)
{
    string s = "";
    if (cc_field & CC_B_BIDIRECTIONAL_BIT) s += "bidir ";
    if (cc_field & CC_RD_RESPONSE_DELAY_BIT) s += "fast_resp ";
    else s += "slow_resp ";
    if (cc_field & CC_S_SYNCH_FRAME_BIT) s += "sync ";
    if (cc_field & CC_R_RELAYED_BIT) s+= "relayed "; // Relayed by a repeater
    if (cc_field & CC_P_HIGH_PRIO_BIT) s+= "prio ";

    if (s.size() > 0 && s.back() == ' ') s.pop_back();
    return s;
}


int difLenBytes(int dif)
{
    int t = dif & 0x0f;
    switch (t) {
    case 0x0: return 0; // No data
    case 0x1: return 1; // 8 Bit Integer/Binary
    case 0x2: return 2; // 16 Bit Integer/Binary
    case 0x3: return 3; // 24 Bit Integer/Binary
    case 0x4: return 4; // 32 Bit Integer/Binary
    case 0x5: return 4; // 32 Bit Real
    case 0x6: return 6; // 48 Bit Integer/Binary
    case 0x7: return 8; // 64 Bit Integer/Binary
    case 0x8: return 0; // Selection for Readout
    case 0x9: return 1; // 2 digit BCD
    case 0xA: return 2; // 4 digit BCD
    case 0xB: return 3; // 6 digit BCD
    case 0xC: return 4; // 8 digit BCD
    case 0xD: return -1; // variable length
    case 0xE: return 6; // 12 digit BCD
    case 0xF: // Special Functions
        if (dif == 0x2f) return 1; // The skip code 0x2f, used for padding.
        return -2;
    }
    // Bad!
    return -2;
}

string difType(int dif)
{
    string s;
    int t = dif & 0x0f;
    switch (t) {
    case 0x0: s+= "No data"; break;
    case 0x1: s+= "8 Bit Integer/Binary"; break;
    case 0x2: s+= "16 Bit Integer/Binary"; break;
    case 0x3: s+= "24 Bit Integer/Binary"; break;
    case 0x4: s+= "32 Bit Integer/Binary"; break;
    case 0x5: s+= "32 Bit Real"; break;
    case 0x6: s+= "48 Bit Integer/Binary"; break;
    case 0x7: s+= "64 Bit Integer/Binary"; break;
    case 0x8: s+= "Selection for Readout"; break;
    case 0x9: s+= "2 digit BCD"; break;
    case 0xA: s+= "4 digit BCD"; break;
    case 0xB: s+= "6 digit BCD"; break;
    case 0xC: s+= "8 digit BCD"; break;
    case 0xD: s+= "variable length"; break;
    case 0xE: s+= "12 digit BCD"; break;
    case 0xF: s+= "Special Functions"; break;
    default: s+= "?"; break;
    }

    if (t != 0xf)
    {
        // Only print these suffixes when we have actual values.
        t = dif & 0x30;

        switch (t) {
        case 0x00: s += " Instantaneous value"; break;
        case 0x10: s += " Maximum value"; break;
        case 0x20: s += " Minimum value"; break;
        case 0x30: s+= " Value during error state"; break;
        default: s += "?"; break;
        }
    }
    if (dif & 0x40) {
        // This is the lsb of the storage nr.
        s += " storagenr=1";
    }
    return s;
}

MeasurementType difMeasurementType(int dif)
{
    int t = dif & 0x30;
    switch (t) {
    case 0x00: return MeasurementType::Instantaneous;
    case 0x10: return MeasurementType::Maximum;
    case 0x20: return MeasurementType::Minimum;
    case 0x30: return MeasurementType::AtError;
    }
    assert(0);
}

string vifType(int vif)
{
    // Remove any remaining 0x80 top bits.
    vif &= 0x7f7f;

    switch (vif)
    {
    case 0x00: return "Energy mWh";
    case 0x01: return "Energy 10⁻² Wh";
    case 0x02: return "Energy 10⁻¹ Wh";
    case 0x03: return "Energy Wh";
    case 0x04: return "Energy 10¹ Wh";
    case 0x05: return "Energy 10² Wh";
    case 0x06: return "Energy kWh";
    case 0x07: return "Energy 10⁴ Wh";

    case 0x08: return "Energy J";
    case 0x09: return "Energy 10¹ J";
    case 0x0A: return "Energy 10² J";
    case 0x0B: return "Energy kJ";
    case 0x0C: return "Energy 10⁴ J";
    case 0x0D: return "Energy 10⁵ J";
    case 0x0E: return "Energy MJ";
    case 0x0F: return "Energy 10⁷ J";

    case 0x10: return "Volume cm³";
    case 0x11: return "Volume 10⁻⁵ m³";
    case 0x12: return "Volume 10⁻⁴ m³";
    case 0x13: return "Volume l";
    case 0x14: return "Volume 10⁻² m³";
    case 0x15: return "Volume 10⁻¹ m³";
    case 0x16: return "Volume m³";
    case 0x17: return "Volume 10¹ m³";

    case 0x18: return "Mass g";
    case 0x19: return "Mass 10⁻² kg";
    case 0x1A: return "Mass 10⁻¹ kg";
    case 0x1B: return "Mass kg";
    case 0x1C: return "Mass 10¹ kg";
    case 0x1D: return "Mass 10² kg";
    case 0x1E: return "Mass t";
    case 0x1F: return "Mass 10⁴ kg";

    case 0x20: return "On time seconds";
    case 0x21: return "On time minutes";
    case 0x22: return "On time hours";
    case 0x23: return "On time days";

    case 0x24: return "Operating time seconds";
    case 0x25: return "Operating time minutes";
    case 0x26: return "Operating time hours";
    case 0x27: return "Operating time days";

    case 0x28: return "Power mW";
    case 0x29: return "Power 10⁻² W";
    case 0x2A: return "Power 10⁻¹ W";
    case 0x2B: return "Power W";
    case 0x2C: return "Power 10¹ W";
    case 0x2D: return "Power 10² W";
    case 0x2E: return "Power kW";
    case 0x2F: return "Power 10⁴ W";

    case 0x30: return "Power J/h";
    case 0x31: return "Power 10¹ J/h";
    case 0x32: return "Power 10² J/h";
    case 0x33: return "Power kJ/h";
    case 0x34: return "Power 10⁴ J/h";
    case 0x35: return "Power 10⁵ J/h";
    case 0x36: return "Power MJ/h";
    case 0x37: return "Power 10⁷ J/h";

    case 0x38: return "Volume flow cm³/h";
    case 0x39: return "Volume flow 10⁻⁵ m³/h";
    case 0x3A: return "Volume flow 10⁻⁴ m³/h";
    case 0x3B: return "Volume flow l/h";
    case 0x3C: return "Volume flow 10⁻² m³/h";
    case 0x3D: return "Volume flow 10⁻¹ m³/h";
    case 0x3E: return "Volume flow m³/h";
    case 0x3F: return "Volume flow 10¹ m³/h";

    case 0x40: return "Volume flow ext. 10⁻⁷ m³/min";
    case 0x41: return "Volume flow ext. cm³/min";
    case 0x42: return "Volume flow ext. 10⁻⁵ m³/min";
    case 0x43: return "Volume flow ext. 10⁻⁴ m³/min";
    case 0x44: return "Volume flow ext. l/min";
    case 0x45: return "Volume flow ext. 10⁻² m³/min";
    case 0x46: return "Volume flow ext. 10⁻¹ m³/min";
    case 0x47: return "Volume flow ext. m³/min";

    case 0x48: return "Volume flow ext. mm³/s";
    case 0x49: return "Volume flow ext. 10⁻⁸ m³/s";
    case 0x4A: return "Volume flow ext. 10⁻⁷ m³/s";
    case 0x4B: return "Volume flow ext. cm³/s";
    case 0x4C: return "Volume flow ext. 10⁻⁵ m³/s";
    case 0x4D: return "Volume flow ext. 10⁻⁴ m³/s";
    case 0x4E: return "Volume flow ext. l/s";
    case 0x4F: return "Volume flow ext. 10⁻² m³/s";

    case 0x50: return "Mass g/h";
    case 0x51: return "Mass 10⁻² kg/h";
    case 0x52: return "Mass 10⁻¹ kg/h";
    case 0x53: return "Mass kg/h";
    case 0x54: return "Mass 10¹ kg/h";
    case 0x55: return "Mass 10² kg/h";
    case 0x56: return "Mass t/h";
    case 0x57: return "Mass 10⁴ kg/h";

    case 0x58: return "Flow temperature 10⁻³ °C";
    case 0x59: return "Flow temperature 10⁻² °C";
    case 0x5A: return "Flow temperature 10⁻¹ °C";
    case 0x5B: return "Flow temperature °C";

    case 0x5C: return "Return temperature 10⁻³ °C";
    case 0x5D: return "Return temperature 10⁻² °C";
    case 0x5E: return "Return temperature 10⁻¹ °C";
    case 0x5F: return "Return temperature °C";

    case 0x60: return "Temperature difference 10⁻³ K/°C";
    case 0x61: return "Temperature difference 10⁻² K/°C";
    case 0x62: return "Temperature difference 10⁻¹ K/°C";
    case 0x63: return "Temperature difference K/°C";

    case 0x64: return "External temperature 10⁻³ °C";
    case 0x65: return "External temperature 10⁻² °C";
    case 0x66: return "External temperature 10⁻¹ °C";
    case 0x67: return "External temperature °C";

    case 0x68: return "Pressure mbar";
    case 0x69: return "Pressure 10⁻² bar";
    case 0x6A: return "Pressure 10⁻¹ bar";
    case 0x6B: return "Pressure bar";

    case 0x6C: return "Date type G";
    case 0x6D: return "Date and time type";

    case 0x6E: return "Units for H.C.A.";
    case 0x6F: return "Third extension 6F of VIF-codes";

    case 0x70: return "Averaging duration seconds";
    case 0x71: return "Averaging duration minutes";
    case 0x72: return "Averaging duration hours";
    case 0x73: return "Averaging duration days";

    case 0x74: return "Actuality duration seconds";
    case 0x75: return "Actuality duration minutes";
    case 0x76: return "Actuality duration hours";
    case 0x77: return "Actuality duration days";

    case 0x78: return "Fabrication no";
    case 0x79: return "Enhanced identification";

    case 0x7B: return "First extension FB of VIF-codes";
    case 0x7C: return "VIF in following string (length in first byte)";
    case 0x7D: return "Second extension FD of VIF-codes";

    case 0x7E: return "Any VIF";
    case 0x7F: return "Manufacturer specific";

    case 0x7B00: return "Active Energy 0.1 MWh";
    case 0x7B01: return "Active Energy 1 MWh";

    case 0x7B1A: return "Relative humidity 0.1%";
    case 0x7B1B: return "Relative humidity 1%";

    default: return "?";
    }
}

double vifScale(int vif)
{
    // Remove any remaining 0x80 top bits.
    vif &= 0x7f7f;

    switch (vif) {
        // wmbusmeters always returns enery as kwh
    case 0x00: return 1000000.0; // Energy mWh
    case 0x01: return 100000.0;  // Energy 10⁻² Wh
    case 0x02: return 10000.0;   // Energy 10⁻¹ Wh
    case 0x03: return 1000.0;    // Energy Wh
    case 0x04: return 100.0;     // Energy 10¹ Wh
    case 0x05: return 10.0;      // Energy 10² Wh
    case 0x06: return 1.0;       // Energy kWh
    case 0x07: return 0.1;       // Energy 10⁴ Wh

        // or wmbusmeters always returns energy as MJ
    case 0x08: return 1000000.0; // Energy J
    case 0x09: return 100000.0;  // Energy 10¹ J
    case 0x0A: return 10000.0;   // Energy 10² J
    case 0x0B: return 1000.0;    // Energy kJ
    case 0x0C: return 100.0;     // Energy 10⁴ J
    case 0x0D: return 10.0;      // Energy 10⁵ J
    case 0x0E: return 1.0;       // Energy MJ
    case 0x0F: return 0.1;       // Energy 10⁷ J

        // wmbusmeters always returns volume as m3
    case 0x10: return 1000000.0; // Volume cm³
    case 0x11: return 100000.0; // Volume 10⁻⁵ m³
    case 0x12: return 10000.0; // Volume 10⁻⁴ m³
    case 0x13: return 1000.0; // Volume l
    case 0x14: return 100.0; // Volume 10⁻² m³
    case 0x15: return 10.0; // Volume 10⁻¹ m³
    case 0x16: return 1.0; // Volume m³
    case 0x17: return 0.1; // Volume 10¹ m³

        // wmbusmeters always returns weight in kg
    case 0x18: return 1000.0; // Mass g
    case 0x19: return 100.0;  // Mass 10⁻² kg
    case 0x1A: return 10.0;   // Mass 10⁻¹ kg
    case 0x1B: return 1.0;    // Mass kg
    case 0x1C: return 0.1;    // Mass 10¹ kg
    case 0x1D: return 0.01;   // Mass 10² kg
    case 0x1E: return 0.001;  // Mass t
    case 0x1F: return 0.0001; // Mass 10⁴ kg

        // wmbusmeters always returns time in hours
    case 0x20: return 3600.0;     // On time seconds
    case 0x21: return 60.0;       // On time minutes
    case 0x22: return 1.0;        // On time hours
    case 0x23: return (1.0/24.0); // On time days

    case 0x24: return 3600.0;     // Operating time seconds
    case 0x25: return 60.0;       // Operating time minutes
    case 0x26: return 1.0;        // Operating time hours
    case 0x27: return (1.0/24.0); // Operating time days

        // wmbusmeters always returns power in kw
    case 0x28: return 1000000.0; // Power mW
    case 0x29: return 100000.0; // Power 10⁻² W
    case 0x2A: return 10000.0; // Power 10⁻¹ W
    case 0x2B: return 1000.0; // Power W
    case 0x2C: return 100.0; // Power 10¹ W
    case 0x2D: return 10.0; // Power 10² W
    case 0x2E: return 1.0; // Power kW
    case 0x2F: return 0.1; // Power 10⁴ W

        // or wmbusmeters always returns power in MJh
    case 0x30: return 1000000.0; // Power J/h
    case 0x31: return 100000.0; // Power 10¹ J/h
    case 0x32: return 10000.0; // Power 10² J/h
    case 0x33: return 1000.0; // Power kJ/h
    case 0x34: return 100.0; // Power 10⁴ J/h
    case 0x35: return 10.0; // Power 10⁵ J/h
    case 0x36: return 1.0; // Power MJ/h
    case 0x37: return 0.1; // Power 10⁷ J/h

        // wmbusmeters always returns volume flow in m3h
    case 0x38: return 1000000.0; // Volume flow cm³/h
    case 0x39: return 100000.0; // Volume flow 10⁻⁵ m³/h
    case 0x3A: return 10000.0; // Volume flow 10⁻⁴ m³/h
    case 0x3B: return 1000.0; // Volume flow l/h
    case 0x3C: return 100.0; // Volume flow 10⁻² m³/h
    case 0x3D: return 10.0; // Volume flow 10⁻¹ m³/h
    case 0x3E: return 1.0; // Volume flow m³/h
    case 0x3F: return 0.1; // Volume flow 10¹ m³/h

        // wmbusmeters always returns volume flow in m3h
    case 0x40: return 600000000.0; // Volume flow ext. 10⁻⁷ m³/min
    case 0x41: return 60000000.0; // Volume flow ext. cm³/min
    case 0x42: return 6000000.0; // Volume flow ext. 10⁻⁵ m³/min
    case 0x43: return 600000.0; // Volume flow ext. 10⁻⁴ m³/min
    case 0x44: return 60000.0; // Volume flow ext. l/min
    case 0x45: return 6000.0; // Volume flow ext. 10⁻² m³/min
    case 0x46: return 600.0; // Volume flow ext. 10⁻¹ m³/min
    case 0x47: return 60.0; // Volume flow ext. m³/min

        // this flow numbers will be small in the m3h unit, but it
        // does not matter since double stores the scale factor in its exponent.
    case 0x48: return 1000000000.0*3600; // Volume flow ext. mm³/s
    case 0x49: return 100000000.0*3600; // Volume flow ext. 10⁻⁸ m³/s
    case 0x4A: return 10000000.0*3600; // Volume flow ext. 10⁻⁷ m³/s
    case 0x4B: return 1000000.0*3600; // Volume flow ext. cm³/s
    case 0x4C: return 100000.0*3600; // Volume flow ext. 10⁻⁵ m³/s
    case 0x4D: return 10000.0*3600; // Volume flow ext. 10⁻⁴ m³/s
    case 0x4E: return 1000.0*3600; // Volume flow ext. l/s
    case 0x4F: return 100.0*3600; // Volume flow ext. 10⁻² m³/s

        // wmbusmeters always returns mass flow as kgh
    case 0x50: return 1000.0; // Mass g/h
    case 0x51: return 100.0; // Mass 10⁻² kg/h
    case 0x52: return 10.0; // Mass 10⁻¹ kg/h
    case 0x53: return 1.0; // Mass kg/h
    case 0x54: return 0.1; // Mass 10¹ kg/h
    case 0x55: return 0.01; // Mass 10² kg/h
    case 0x56: return 0.001; // Mass t/h
    case 0x57: return 0.0001; // Mass 10⁴ kg/h

        // wmbusmeters always returns temperature in °C
    case 0x58: return 1000.0; // Flow temperature 10⁻³ °C
    case 0x59: return 100.0; // Flow temperature 10⁻² °C
    case 0x5A: return 10.0; // Flow temperature 10⁻¹ °C
    case 0x5B: return 1.0; // Flow temperature °C

        // wmbusmeters always returns temperature in c
    case 0x5C: return 1000.0;  // Return temperature 10⁻³ °C
    case 0x5D: return 100.0; // Return temperature 10⁻² °C
    case 0x5E: return 10.0; // Return temperature 10⁻¹ °C
    case 0x5F: return 1.0; // Return temperature °C

        // or if Kelvin is used as a temperature, in K
        // what kind of meter cares about -273.15 °C
        // a flow pump for liquid helium perhaps?
    case 0x60: return 1000.0; // Temperature difference mK
    case 0x61: return 100.0; // Temperature difference 10⁻² K
    case 0x62: return 10.0; // Temperature difference 10⁻¹ K
    case 0x63: return 1.0; // Temperature difference K

        // wmbusmeters always returns temperature in c
    case 0x64: return 1000.0; // External temperature 10⁻³ °C
    case 0x65: return 100.0; // External temperature 10⁻² °C
    case 0x66: return 10.0; // External temperature 10⁻¹ °C
    case 0x67: return 1.0; // External temperature °C

        // wmbusmeters always returns pressure in bar
    case 0x68: return 1000.0; // Pressure mbar
    case 0x69: return 100.0; // Pressure 10⁻² bar
    case 0x6A: return 10.0; // Pressure 10⁻¹ bar
    case 0x6B: return 1.0; // Pressure bar

    case 0x6C: return 1.0; // Date type G
    case 0x6D: return 1.0; // Date&Time type F
    case 0x6E: return 1.0; // Units for H.C.A. are never scaled
    case 0x6F: warning("(wmbus) warning: do not scale a reserved type!\n"); return -1.0; // Reserved

        // wmbusmeters always returns time in hours
    case 0x70: return 3600.0; // Averaging duration seconds
    case 0x71: return 60.0; // Averaging duration minutes
    case 0x72: return 1.0; // Averaging duration hours
    case 0x73: return (1.0/24.0); // Averaging duration days

        // wmbusmeters always returns time in hours
    case 0x74: return 3600.0; // Actuality duration seconds
    case 0x75: return 60.0; // Actuality duration minutes
    case 0x76: return 1.0; // Actuality duration hours
    case 0x77: return (1.0/24.0); // Actuality duration days

        // Active energy 0.1 or 1 MWh normalize to 100 KWh or 1000 KWh
        // 7b00 33632 -> 3363.2 MWh -> 3363200 KWh
        // 7b01 33632 -> 33632 MWh -> 33632000 KWh
    case 0x7b00:
    case 0x7b01: { double exp = (vif & 0x1)+2; return pow(10.0, -exp); }

        // Active energy 0.1 or 1 GJ normalize to 100 MJ or 1000 MJ
        // 7b08 19 -> 1.9 G -> 1 900 KWh
        // 7b09 19 -> 19 GJ -> 19 000 MJ
    case 0x7b08:
    case 0x7b09: { double exp = (vif & 0x1)+2; return pow(10.0, -exp); }

        // relative humidity is a dimensionless value.
    case 0x7b1a: return 10.0; // Relative humidity 0.1 %
    case 0x7b1b: return 1.0;  // Relative humidity 1 %

        // wmbusmeters always returns time in hours
        // 0x7d30 is not supposed to be used according to spec.
    case 0x7d31: return 60.0; // Duration tariff minutes
    case 0x7d32: return 1.0; // Duration tariff hours
    case 0x7d33: return (1.0/24.0); // Duration tariff days

        // wmbusmeters always voltage in volts
    case 0x7d40:
    case 0x7d41:
    case 0x7d42:
    case 0x7d43:
    case 0x7d44:
    case 0x7d45:
    case 0x7d46:
    case 0x7d47:
    case 0x7d48:
    case 0x7d49:
    case 0x7d4a:
    case 0x7d4b:
    case 0x7d4c:
    case 0x7d4d:
    case 0x7d4e:
    case 0x7d4f: { double exp = (vif & 0xf)-9; return pow(10.0, -exp); }

        // wmbusmeters always return current in ampere
    case 0x7d50:
    case 0x7d51:
    case 0x7d52:
    case 0x7d53:
    case 0x7d54:
    case 0x7d55:
    case 0x7d56:
    case 0x7d57:
    case 0x7d58:
    case 0x7d59:
    case 0x7d5a:
    case 0x7d5b:
    case 0x7d5c:
    case 0x7d5d:
    case 0x7d5e:
    case 0x7d5f: { double exp = (vif & 0xf)-12; return pow(10.0, -exp); }

        // for remaining battery wmbusmeters returns number of days.
    case 0x7d74: { return 1.0; }

        // wmbusmeters always returns time in hours
    case 0x7d2c: return 3600.0; // Duration since readout seconds
    case 0x7d2d: return 60.0; // Duration since readout minutes
    case 0x7d2e: return 1.0; // Duration since readout hours
    case 0x7d2f: return (1.0/24.0); // Duration since readout days

        /*
    case 0x78: // Fabrication no
    case 0x79: // Enhanced identification
    case 0x80: // Address

    case 0x7C: // VIF in following string (length in first byte)
    case 0x7E: // Any VIF
    case 0x7F: // Manufacturer specific
        */

    default: warning("(wmbus) warning: type 0x%x cannot be scaled!\n", vif);
        return -1;
    }
}

string vifKey(int vif)
{
    int t = vif & 0x7f;

    switch (t) {

    case 0x00:
    case 0x01:
    case 0x02:
    case 0x03:
    case 0x04:
    case 0x05:
    case 0x06:
    case 0x07: return "energy";

    case 0x08:
    case 0x09:
    case 0x0A:
    case 0x0B:
    case 0x0C:
    case 0x0D:
    case 0x0E:
    case 0x0F: return "energy";

    case 0x10:
    case 0x11:
    case 0x12:
    case 0x13:
    case 0x14:
    case 0x15:
    case 0x16:
    case 0x17: return "volume";

    case 0x18:
    case 0x19:
    case 0x1A:
    case 0x1B:
    case 0x1C:
    case 0x1D:
    case 0x1E:
    case 0x1F: return "mass";

    case 0x20:
    case 0x21:
    case 0x22:
    case 0x23: return "on_time";

    case 0x24:
    case 0x25:
    case 0x26:
    case 0x27: return "operating_time";

    case 0x28:
    case 0x29:
    case 0x2A:
    case 0x2B:
    case 0x2C:
    case 0x2D:
    case 0x2E:
    case 0x2F: return "power";

    case 0x30:
    case 0x31:
    case 0x32:
    case 0x33:
    case 0x34:
    case 0x35:
    case 0x36:
    case 0x37: return "power";

    case 0x38:
    case 0x39:
    case 0x3A:
    case 0x3B:
    case 0x3C:
    case 0x3D:
    case 0x3E:
    case 0x3F: return "volume_flow";

    case 0x40:
    case 0x41:
    case 0x42:
    case 0x43:
    case 0x44:
    case 0x45:
    case 0x46:
    case 0x47: return "volume_flow_ext";

    case 0x48:
    case 0x49:
    case 0x4A:
    case 0x4B:
    case 0x4C:
    case 0x4D:
    case 0x4E:
    case 0x4F: return "volume_flow_ext";

    case 0x50:
    case 0x51:
    case 0x52:
    case 0x53:
    case 0x54:
    case 0x55:
    case 0x56:
    case 0x57: return "mass_flow";

    case 0x58:
    case 0x59:
    case 0x5A:
    case 0x5B: return "flow_temperature";

    case 0x5C:
    case 0x5D:
    case 0x5E:
    case 0x5F: return "return_temperature";

    case 0x60:
    case 0x61:
    case 0x62:
    case 0x63: return "temperature_difference";

    case 0x64:
    case 0x65:
    case 0x66:
    case 0x67: return "external_temperature";

    case 0x68:
    case 0x69:
    case 0x6A:
    case 0x6B: return "pressure";

    case 0x6C: return "date"; // Date type G
    case 0x6E: return "hca"; // Units for H.C.A.
    case 0x6F: return "reserved"; // Reserved

    case 0x70:
    case 0x71:
    case 0x72:
    case 0x73: return "average_duration";

    case 0x74:
    case 0x75:
    case 0x76:
    case 0x77: return "actual_duration";

    case 0x78: return "fabrication_no"; // Fabrication no
    case 0x79: return "enhanced_identification"; // Enhanced identification

    case 0x7C: // VIF in following string (length in first byte)
    case 0x7E: // Any VIF
    case 0x7F: // Manufacturer specific

    default: warning("(wmbus) warning: generic type %d cannot be scaled!\n", t);
        return "unknown";
    }
}

string vifUnit(int vif)
{
    int t = vif & 0x7f;

    switch (t) {

    case 0x00:
    case 0x01:
    case 0x02:
    case 0x03:
    case 0x04:
    case 0x05:
    case 0x06:
    case 0x07: return "kwh";

    case 0x08:
    case 0x09:
    case 0x0A:
    case 0x0B:
    case 0x0C:
    case 0x0D:
    case 0x0E:
    case 0x0F: return "MJ";

    case 0x10:
    case 0x11:
    case 0x12:
    case 0x13:
    case 0x14:
    case 0x15:
    case 0x16:
    case 0x17: return "m3";

    case 0x18:
    case 0x19:
    case 0x1A:
    case 0x1B:
    case 0x1C:
    case 0x1D:
    case 0x1E:
    case 0x1F: return "kg";

    case 0x20:
    case 0x21:
    case 0x22:
    case 0x23:
    case 0x24:
    case 0x25:
    case 0x26:
    case 0x27: return "h";

    case 0x28:
    case 0x29:
    case 0x2A:
    case 0x2B:
    case 0x2C:
    case 0x2D:
    case 0x2E:
    case 0x2F: return "kw";

    case 0x30:
    case 0x31:
    case 0x32:
    case 0x33:
    case 0x34:
    case 0x35:
    case 0x36:
    case 0x37: return "MJ";

    case 0x38:
    case 0x39:
    case 0x3A:
    case 0x3B:
    case 0x3C:
    case 0x3D:
    case 0x3E:
    case 0x3F: return "m3/h";

    case 0x40:
    case 0x41:
    case 0x42:
    case 0x43:
    case 0x44:
    case 0x45:
    case 0x46:
    case 0x47: return "m3/h";

    case 0x48:
    case 0x49:
    case 0x4A:
    case 0x4B:
    case 0x4C:
    case 0x4D:
    case 0x4E:
    case 0x4F: return "m3/h";

    case 0x50:
    case 0x51:
    case 0x52:
    case 0x53:
    case 0x54:
    case 0x55:
    case 0x56:
    case 0x57: return "kg/h";

    case 0x58:
    case 0x59:
    case 0x5A:
    case 0x5B: return "c";

    case 0x5C:
    case 0x5D:
    case 0x5E:
    case 0x5F: return "c";

    case 0x60:
    case 0x61:
    case 0x62:
    case 0x63: return "k";

    case 0x64:
    case 0x65:
    case 0x66:
    case 0x67: return "c";

    case 0x68:
    case 0x69:
    case 0x6A:
    case 0x6B: return "bar";

    case 0x6C: return ""; // Date type G
    case 0x6D: return ""; // ??
    case 0x6E: return ""; // Units for H.C.A.
    case 0x6F: return ""; // Reserved

    case 0x70:
    case 0x71:
    case 0x72:
    case 0x73: return "h";

    case 0x74:
    case 0x75:
    case 0x76:
    case 0x77: return "h";

    case 0x78: return ""; // Fabrication no
    case 0x79: return ""; // Enhanced identification

    case 0x7C: // VIF in following string (length in first byte)
    case 0x7E: // Any VIF
    case 0x7F: // Manufacturer specific

    default: warning("(wmbus) warning: generic type %d cannot be scaled!\n", t);
        return "unknown";
    }
}

const char *timeNN(int nn) {
    switch (nn) {
    case 0: return "second(s)";
    case 1: return "minute(s)";
    case 2: return "hour(s)";
    case 3: return "day(s)";
    }
    return "?";
}

const char *timePP(int nn) {
    switch (nn) {
    case 0: return "hour(s)";
    case 1: return "day(s)";
    case 2: return "month(s)";
    case 3: return "year(s)";
    }
    return "?";
}

string vif_7B_FirstExtensionType(uchar dif, uchar vif, uchar vife)
{
    assert(vif == 0xfb);

    if ((vife & 0x7e) == 0x00) {
        int n = vife & 0x01;
        string s;
        strprintf(&s, "10^%d MWh", n-1);
        return s;
    }

    if (((vife & 0x7e) == 0x02) ||
        ((vife & 0x7c) == 0x04)) {
        return "Reserved";
    }

    if ((vife & 0x7e) == 0x08) {
        int n = vife & 0x01;
        string s;
        strprintf(&s, "10^%d GJ", n-1);
        return s;
    }

    if ((vife & 0x7e) == 0x0a ||
        (vife & 0x7c) == 0x0c) {
        return "Reserved";
    }

    if ((vife & 0x7e) == 0x10) {
        int n = vife & 0x01;
        string s;
        strprintf(&s, "10^%d m3", n+2);
        return s;
    }

    if ((vife & 0x7e) == 0x12 ||
        (vife & 0x7c) == 0x14) {
        return "Reserved";
    }

    if ((vife & 0x7e) == 0x18) {
        int n = vife & 0x01;
        string s;
        strprintf(&s, "10^%d ton", n+2);
        return s;
    }

    if ((vife & 0x7e) == 0x1a) {
        int n = vife & 0x01;
        string s;
        strprintf(&s, "Relative Humidity 10^%d %%", n-1);
        return s;
    }

    if ( (vif & 0x7e) >= 0x1a && (vif & 0x7e) <= 0x20) {
        return "Reserved";
    }

    if ((vife & 0x7f) == 0x21) {
        return "0.1 feet^3";
    }

    if ((vife & 0x7f) == 0x22) {
        return "0.1 american gallon";
    }

    if ((vife & 0x7f) == 0x23) {
        return "american gallon";
    }

    if ((vife & 0x7f) == 0x24) {
        return "0.001 american gallon/min";
    }

    if ((vife & 0x7f) == 0x25) {
        return "american gallon/min";
    }

    if ((vife & 0x7f) == 0x26) {
        return "american gallon/h";
    }

    if ((vife & 0x7f) == 0x27) {
        return "Reserved";
    }

    if ((vife & 0x7f) == 0x20) {
        return "Volume feet";
    }

    if ((vife & 0x7f) == 0x21) {
        return "Volume 0.1 feet";
    }

    if ((vife & 0x7e) == 0x28) {
        // Come again? A unit of 1MW...do they intend to use m-bus to track the
        // output from a nuclear power plant?
        int n = vife & 0x01;
        string s;
        strprintf(&s, "10^%d MW", n-1);
        return s;
    }

    if ((vife & 0x7f) == 0x29 ||
        (vife & 0x7c) == 0x2c) {
        return "Reserved";
    }

    if ((vife & 0x7e) == 0x30) {
        int n = vife & 0x01;
        string s;
        strprintf(&s, "10^%d GJ/h", n-1);
        return s;
    }

    if ((vife & 0x7f) >= 0x32 && (vife & 0x7c) <= 0x57) {
        return "Reserved";
    }

    if ((vife & 0x7c) == 0x58) {
        int nn = vife & 0x03;
        string s;
        strprintf(&s, "Flow temperature 10^%d Fahrenheit", nn-3);
        return s;
    }

    if ((vife & 0x7c) == 0x5c) {
        int nn = vife & 0x03;
        string s;
        strprintf(&s, "Return temperature 10^%d Fahrenheit", nn-3);
        return s;
    }

    if ((vife & 0x7c) == 0x60) {
        int nn = vife & 0x03;
        string s;
        strprintf(&s, "Temperature difference 10^%d Fahrenheit", nn-3);
        return s;
    }

    if ((vife & 0x7c) == 0x64) {
        int nn = vife & 0x03;
        string s;
        strprintf(&s, "External temperature 10^%d Fahrenheit", nn-3);
        return s;
    }

    if ((vife & 0x78) == 0x68) {
        return "Reserved";
    }

    if ((vife & 0x7c) == 0x70) {
        int nn = vife & 0x03;
        string s;
        strprintf(&s, "Cold / Warm Temperature Limit 10^%d Fahrenheit", nn-3);
        return s;
    }

    if ((vife & 0x7c) == 0x74) {
        int nn = vife & 0x03;
        string s;
        strprintf(&s, "Cold / Warm Temperature Limit 10^%d Celsius", nn-3);
        return s;
    }

    if ((vife & 0x78) == 0x78) {
        int nnn = vife & 0x07;
        string s;
        strprintf(&s, "Cumulative count max power 10^%d W", nnn-3);
        return s;
    }

    return "?";
}

string vif_7D_SecondExtensionType(uchar dif, uchar vif, uchar vife)
{
    assert(vif == 0xfd);

    if ((vife & 0x7c) == 0x00) {
        int nn = vife & 0x03;
        string s;
        strprintf(&s, "Credit of 10^%d of the nominal local legal currency units", nn-3);
        return s;
    }

    if ((vife & 0x7c) == 0x04) {
        int nn = vife & 0x03;
        string s;
        strprintf(&s, "Debit of 10^%d of the nominal local legal currency units", nn-3);
        return s;
    }

    if ((vife & 0x7f) == 0x08) {
        return "Access Number (transmission count)";
    }

    if ((vife & 0x7f) == 0x09) {
        return "Medium (as in fixed header)";
    }

    if ((vife & 0x7f) == 0x0a) {
        return "Manufacturer (as in fixed header)";
    }

    if ((vife & 0x7f) == 0x0b) {
        return "Parameter set identification";
    }

    if ((vife & 0x7f) == 0x0c) {
        return "Model/Version";
    }

    if ((vife & 0x7f) == 0x0d) {
        return "Hardware version #";
    }

    if ((vife & 0x7f) == 0x0e) {
        return "Firmware version #";
    }

    if ((vife & 0x7f) == 0x0f) {
        return "Software version #";
    }

    if ((vife & 0x7f) == 0x10) {
        return "Customer location";
    }

    if ((vife & 0x7f) == 0x11) {
        return "Customer";
    }

    if ((vife & 0x7f) == 0x12) {
        return "Access Code User";
    }

    if ((vife & 0x7f) == 0x13) {
        return "Access Code Operator";
    }

    if ((vife & 0x7f) == 0x14) {
        return "Access Code System Operator";
    }

    if ((vife & 0x7f) == 0x15) {
        return "Access Code Developer";
    }

    if ((vife & 0x7f) == 0x16) {
        return "Password";
    }

    if ((vife & 0x7f) == 0x17) {
        return "Error flags (binary)";
    }

    if ((vife & 0x7f) == 0x18) {
        return "Error mask";
    }

    if ((vife & 0x7f) == 0x19) {
        return "Reserved";
    }

    if ((vife & 0x7f) == 0x1a) {
        return "Digital Output (binary)";
    }

    if ((vife & 0x7f) == 0x1b) {
        return "Digital Input (binary)";
    }

    if ((vife & 0x7f) == 0x1c) {
        return "Baudrate [Baud]";
    }

    if ((vife & 0x7f) == 0x1d) {
        return "Response delay time [bittimes]";
    }

    if ((vife & 0x7f) == 0x1e) {
        return "Retry";
    }

    if ((vife & 0x7f) == 0x1f) {
        return "Reserved";
    }

    if ((vife & 0x7f) == 0x20) {
        return "First storage # for cyclic storage";
    }

    if ((vife & 0x7f) == 0x21) {
        return "Last storage # for cyclic storage";
    }

    if ((vife & 0x7f) == 0x22) {
        return "Size of storage block";
    }

    if ((vife & 0x7f) == 0x23) {
        return "Reserved";
    }

    if ((vife & 0x7c) == 0x24) {
        int nn = vife & 0x03;
        string s;
        strprintf(&s, "Storage interval [%s]", timeNN(nn));
        return s;
    }

    if ((vife & 0x7f) == 0x28) {
        return "Storage interval month(s)";
    }

    if ((vife & 0x7f) == 0x29) {
        return "Storage interval year(s)";
    }

    if ((vife & 0x7f) == 0x2a) {
        return "Reserved";
    }

    if ((vife & 0x7f) == 0x2b) {
        return "Reserved";
    }

    if ((vife & 0x7c) == 0x2c) {
        int nn = vife & 0x03;
        string s;
        strprintf(&s, "Duration since last readout [%s]", timeNN(nn));
        return s;
    }

    if ((vife & 0x7f) == 0x30) {
        return "Start (date/time) of tariff";
    }

    if ((vife & 0x7c) == 0x30) {
        int nn = vife & 0x03;
        string s;
        // nn == 0 (seconds) is not expected here! According to m-bus spec.
        strprintf(&s, "Duration of tariff [%s]", timeNN(nn));
        return s;
    }

    if ((vife & 0x7c) == 0x34) {
        int nn = vife & 0x03;
        string s;
        strprintf(&s, "Period of tariff [%s]", timeNN(nn));
        return s;
    }

    if ((vife & 0x7f) == 0x38) {
        return "Period of tariff months(s)";
    }

    if ((vife & 0x7f) == 0x39) {
        return "Period of tariff year(s)";
    }

    if ((vife & 0x7f) == 0x3a) {
        return "Dimensionless / no VIF";
    }

    if ((vife & 0x7f) == 0x3b) {
        return "Reserved";
    }

    if ((vife & 0x7c) == 0x3c) {
        // int xx = vife & 0x03;
        return "Reserved";
    }

    if ((vife & 0x70) == 0x40) {
        int nnnn = vife & 0x0f;
        string s;
        strprintf(&s, "10^%d Volts", nnnn-9);
        return s;
    }

    if ((vife & 0x70) == 0x50) {
        int nnnn = vife & 0x0f;
        string s;
        strprintf(&s, "10^%d Ampere", nnnn-12);
        return s;
    }

    if ((vife & 0x7f) == 0x60) {
        return "Reset counter";
    }

    if ((vife & 0x7f) == 0x61) {
        return "Cumulation counter";
    }

    if ((vife & 0x7f) == 0x62) {
        return "Control signal";
    }

    if ((vife & 0x7f) == 0x63) {
        return "Day of week";
    }

    if ((vife & 0x7f) == 0x64) {
        return "Week number";
    }

    if ((vife & 0x7f) == 0x65) {
        return "Time point of day change";
    }

    if ((vife & 0x7f) == 0x66) {
        return "State of parameter activation";
    }

    if ((vife & 0x7f) == 0x67) {
        return "Special supplier information";
    }

    if ((vife & 0x7c) == 0x68) {
        int pp = vife & 0x03;
        string s;
        strprintf(&s, "Duration since last cumulation [%s]", timePP(pp));
        return s;
    }

    if ((vife & 0x7c) == 0x6c) {
        int pp = vife & 0x03;
        string s;
        strprintf(&s, "Operating time battery [%s]", timePP(pp));
        return s;
    }

    if ((vife & 0x7f) == 0x70) {
        return "Date and time of battery change";
    }

    if ((vife & 0x7f) >= 0x71) {
        return "Reserved";
    }

    if ((vife & 0x7f) == 0x74) {
        return "Remaining battery in days";
    }

    return "?";
}

string vif_6F_ThirdExtensionType(uchar dif, uchar vif, uchar vife)
{
    assert(vif == 0xef);
    return "?";
}

string vif_7F_ManufacturerExtensionType(uchar dif, uchar vif, uchar vife)
{
    assert(vif == 0xff);
    return "?";
}

string vifeType(int dif, int vif, int vife)
{
    if (vif == 0xfb) { // 0x7b without high bit
        return vif_7B_FirstExtensionType(dif, vif, vife);
    }
    if (vif == 0xfd) { // 0x7d without high bit
        return vif_7D_SecondExtensionType(dif, vif, vife);
    }
    if (vif == 0xef) { // 0x6f without high bit
        return vif_6F_ThirdExtensionType(dif, vif, vife);
    }
    if (vif == 0xff) { // 0x7f without high bit
        return vif_7F_ManufacturerExtensionType(dif, vif, vife);
    }
    vife = vife & 0x7f; // Strip the bit signifying more vifes after this.
    if (vife == 0x1f) {
        return "Compact profile without register";
    }
    if (vife == 0x13) {
        return "Reverse compact profile without register";
    }
    if (vife == 0x1e) {
        return "Compact profile with register";
    }
    if (vife == 0x20) {
        return "per second";
    }
    if (vife == 0x21) {
        return "per minute";
    }
    if (vife == 0x22) {
        return "per hour";
    }
    if (vife == 0x23) {
        return "per day";
    }
    if (vife == 0x24) {
        return "per week";
    }
    if (vife == 0x25) {
        return "per month";
    }
    if (vife == 0x26) {
        return "per year";
    }
    if (vife == 0x27) {
        return "per revolution/measurement";
    }
    if (vife == 0x28) {
        return "incr per input pulse on input channel 0";
    }
    if (vife == 0x29) {
        return "incr per input pulse on input channel 1";
    }
    if (vife == 0x2a) {
        return "incr per output pulse on input channel 0";
    }
    if (vife == 0x2b) {
        return "incr per output pulse on input channel 1";
    }
    if (vife == 0x2c) {
        return "per litre";
    }
    if (vife == 0x2d) {
        return "per m3";
    }
    if (vife == 0x2e) {
        return "per kg";
    }
    if (vife == 0x2f) {
        return "per kelvin";
    }
    if (vife == 0x30) {
        return "per kWh";
    }
    if (vife == 0x31) {
        return "per GJ";
    }
    if (vife == 0x32) {
        return "per kW";
    }
    if (vife == 0x33) {
        return "per kelvin*litre";
    }
    if (vife == 0x34) {
        return "per volt";
    }
    if (vife == 0x35) {
        return "per ampere";
    }
    if (vife == 0x36) {
        return "multiplied by s";
    }
    if (vife == 0x37) {
        return "multiplied by s/V";
    }
    if (vife == 0x38) {
        return "multiplied by s/A";
    }
    if (vife == 0x39) {
        return "start date/time of a,b";
    }
    if (vife == 0x3a) {
        return "uncorrected meter unit";
    }
    if (vife == 0x3b) {
        return "forward flow";
    }
    if (vife == 0x3c) {
        return "backward flow";
    }
    if (vife == 0x3d) {
        return "reserved for non-metric unit systems";
    }
    if (vife == 0x3e) {
        return "value at base conditions c";
    }
    if (vife == 0x3f) {
        return "obis-declaration";
    }
    if (vife == 0x40) {
        return "obis-declaration";
    }
    if (vife == 0x40) {
        return "lower limit";
    }
    if (vife == 0x48) {
        return "upper limit";
    }
    if (vife == 0x41) {
        return "number of exceeds of lower limit";
    }
    if (vife == 0x49) {
        return "number of exceeds of upper limit";
    }
    if ((vife & 0x72) == 0x42) {
        string msg = "date/time of ";
        if (vife & 0x01) msg += "end ";
        else msg += "beginning ";
        msg +=" of ";
        if (vife & 0x04) msg += "last ";
        else msg += "first ";
        if (vife & 0x08) msg += "upper ";
        else msg += "lower ";
        msg += "limit exceed";
        return msg;
    }
    if ((vife & 0x70) == 0x50) {
        string msg = "duration of limit exceed ";
        if (vife & 0x04) msg += "last ";
        else msg += "first ";
        if (vife & 0x08) msg += "upper ";
        else msg += "lower ";
        int nn = vife & 0x03;
        msg += " is "+to_string(nn);
        return msg;
    }
    if ((vife & 0x78) == 0x60) {
        string msg = "duration of a,b ";
        if (vife & 0x04) msg += "last ";
        else msg += "first ";
        int nn = vife & 0x03;
        msg += " is "+to_string(nn);
        return msg;
    }
    if ((vife & 0x7B) == 0x68) {
        string msg = "value during ";
        if (vife & 0x04) msg += "upper ";
        else msg += "lower ";
        msg += "limit exceed";
        return msg;
    }
    if (vife == 0x69) {
        return "leakage values";
    }
    if (vife == 0x6d) {
        return "overflow values";
    }
    if ((vife & 0x7a) == 0x6a) {
        string msg = "date/time of a: ";
        if (vife & 0x01) msg += "end ";
        else msg += "beginning ";
        msg +=" of ";
        if (vife & 0x04) msg += "last ";
        else msg += "first ";
        if (vife & 0x08) msg += "upper ";
        else msg += "lower ";
        return msg;
    }
    if ((vife & 0x78) == 0x70) {
        int nnn = vife & 0x07;
        return "multiplicative correction factor: 10^"+to_string(nnn-6);
    }
    if ((vife & 0x78) == 0x78) {
        int nn = vife & 0x03;
        return "additive correction constant: unit of VIF * 10^"+to_string(nn-3);
    }
    if (vife == 0x7c) {
        return "extension of combinable vife";
    }
    if (vife == 0x7d) {
        return "multiplicative correction factor for value";
    }
    if (vife == 0x7e) {
        return "future value";
    }
    if (vif == 0x7f) {
        return "manufacturer specific";
    }
    return "?";
}

double toDoubleFromBytes(vector<uchar> &bytes, int len) {
    double d = 0;
    for (int i=0; i<len; ++i) {
        double x = bytes[i];
        d += x*(256^i);
    }
    return d;
}

double toDoubleFromBCD(vector<uchar> &bytes, int len) {
    double d = 0;
    for (int i=0; i<len; ++i) {
        double x = bytes[i]&0xf;
        d += x*(10^(i*2));
        double xx = bytes[i]>>4;
        d += xx*(10^(1+i*2));
    }
    return d;
}

double dataAsDouble(int dif, int vif, int vife, string data)
{
    vector<uchar> bytes;
    hex2bin(data, &bytes);

    int t = dif & 0x0f;
    switch (t) {
    case 0x0: return 0.0;
    case 0x1: return toDoubleFromBytes(bytes, 1);
    case 0x2: return toDoubleFromBytes(bytes, 2);
    case 0x3: return toDoubleFromBytes(bytes, 3);
    case 0x4: return toDoubleFromBytes(bytes, 4);
    case 0x5: return -1;  //  How is REAL stored?
    case 0x6: return toDoubleFromBytes(bytes, 6);
        // Note that for 64 bit data, storing it into a double might lose precision
        // since the mantissa is less than 64 bit. It is unlikely that anyone
        // really needs true 64 bit precision in their measurements from a physical meter though.
    case 0x7: return toDoubleFromBytes(bytes, 8);
    case 0x8: return -1; // Selection for Readout?
    case 0x9: return toDoubleFromBCD(bytes, 1);
    case 0xA: return toDoubleFromBCD(bytes, 2);
    case 0xB: return toDoubleFromBCD(bytes, 3);
    case 0xC: return toDoubleFromBCD(bytes, 4);
    case 0xD: return -1; // variable length
    case 0xE: return toDoubleFromBCD(bytes, 6);
    case 0xF: return -1; // Special Functions
    }
    return -1;
}

uint64_t dataAsUint64(int dif, int vif, int vife, string data)
{
    vector<uchar> bytes;
    hex2bin(data, &bytes);

    int t = dif & 0x0f;
    switch (t) {
    case 0x0: return 0.0;
    case 0x1: return toDoubleFromBytes(bytes, 1);
    case 0x2: return toDoubleFromBytes(bytes, 2);
    case 0x3: return toDoubleFromBytes(bytes, 3);
    case 0x4: return toDoubleFromBytes(bytes, 4);
    case 0x5: return -1;  //  How is REAL stored?
    case 0x6: return toDoubleFromBytes(bytes, 6);
        // Note that for 64 bit data, storing it into a double might lose precision
        // since the mantissa is less than 64 bit. It is unlikely that anyone
        // really needs true 64 bit precision in their measurements from a physical meter though.
    case 0x7: return toDoubleFromBytes(bytes, 8);
    case 0x8: return -1; // Selection for Readout?
    case 0x9: return toDoubleFromBCD(bytes, 1);
    case 0xA: return toDoubleFromBCD(bytes, 2);
    case 0xB: return toDoubleFromBCD(bytes, 3);
    case 0xC: return toDoubleFromBCD(bytes, 4);
    case 0xD: return -1; // variable length
    case 0xE: return toDoubleFromBCD(bytes, 6);
    case 0xF: return -1; // Special Functions
    }
    return -1;
}

string linkModeName(LinkMode link_mode)
{

    for (auto& s : link_modes_) {
        if (link_mode == s.mode) {
            return s.name;
        }
    }
    return "UnknownLinkMode";
}

string measurementTypeName(MeasurementType mt)
{
    switch (mt) {
    case MeasurementType::Any: return "any";
    case MeasurementType::Instantaneous: return "instantaneous";
    case MeasurementType::Maximum: return "maximum";
    case MeasurementType::Minimum: return "minimum";
    case MeasurementType::AtError: return "aterror";
    case MeasurementType::Unknown: return "unknown";
    }
    assert(0);
}

BusDevice::~BusDevice() {
}

bool Telegram::findFormatBytesFromKnownMeterSignatures(vector<uchar> *format_bytes)
{
    bool ok = true;
    if (format_signature == 0xa8ed)
    {
        hex2bin("02FF2004134413615B6167", format_bytes);
        debug("(wmbus) using hard coded format for hash a8ed\n");
    }
    else if (format_signature == 0xc412)
    {
        hex2bin("02FF20041392013BA1015B8101E7FF0F", format_bytes);
        debug("(wmbus) using hard coded format for hash c412\n");
    }
    else if (format_signature == 0x61eb)
    {
        hex2bin("02FF2004134413A1015B8101E7FF0F", format_bytes);
        debug("(wmbus) using hard coded format for hash 61eb\n");
    }
    else if (format_signature == 0xd2f7)
    {
        hex2bin("02FF2004134413615B5167", format_bytes);
        debug("(wmbus) using hard coded format for hash d2f7\n");
    }
    else if (format_signature == 0xdd34)
    {
        hex2bin("02FF2004134413", format_bytes);
        debug("(wmbus) using hard coded format for hash dd34\n");
    }
    else if (format_signature == 0x7c0e)
    {
        hex2bin("02FF200413523B", format_bytes);
        debug("(wmbus) using hard coded format for hash 7c0e\n");
    }
    else if (format_signature == 0x0905)
    {
        hex2bin("04FF234413523B06FF1B426C61675167023B04138101E7FF0F", format_bytes);
        debug("(wmbus) using hard coded format for hash 0905\n");
    }
    else
    {
        ok = false;
    }
    return ok;
}

BusDeviceCommonImplementation::~BusDeviceCommonImplementation()
{
    manager_->listenTo(this->serial(), NULL);
    manager_->onDisappear(this->serial(), NULL);
    debug("(wmbus) deleted %s\n", toString(type()));
}

BusDeviceCommonImplementation::BusDeviceCommonImplementation(string bus_alias,
                                                     BusDeviceType t,
                                                     shared_ptr<SerialCommunicationManager> manager,
                                                     shared_ptr<SerialDevice> serial,
                                                     bool is_serial)
    : manager_(manager),
      bus_alias_(bus_alias),
      is_serial_(is_serial),
      is_working_(true),
      type_(t),
      serial_(serial),
      cached_device_id_(""),
      cached_device_unique_id_(""),
      command_mutex_("wmbus_command_mutex"),
      waiting_for_response_sem_("waiting_for_response_sem"),
      receiving_buffer_mutex_("receiving_buffer_mutex")
{
    // Initialize timeout from now.
    last_received_ = time(NULL);
    last_reset_ = time(NULL);
    manager_->listenTo(this->serial(),call(this,processSerialData));
    manager_->onDisappear(this->serial(),call(this,disconnectedFromDevice));
}

string BusDeviceCommonImplementation::hr()
{
    if (cached_hr_ == "")
    {
        cached_hr_ = device()+":"+toString(type())+"["+getDeviceId()+"]";
    }

    return cached_hr_;
}

bool BusDeviceCommonImplementation::isSerial()
{
    return is_serial_;
}

void BusDeviceCommonImplementation::markAsNoLongerSerial()
{
    // When you override the serial device with a file for an im871a, then
    // it is no longer a serial device.
    is_serial_ = false;
}

BusDeviceType BusDeviceCommonImplementation::type()
{
    return type_;
}

string BusDeviceCommonImplementation::busAlias()
{
    return bus_alias_;
}

void BusDeviceCommonImplementation::onTelegram(function<bool(AboutTelegram&,vector<uchar>)> cb)
{
    telegram_listeners_.push_back(cb);
}

bool BusDeviceCommonImplementation::sendTelegram(LinkMode link_mode, TelegramFormat format, vector<uchar> &content)
{
    warning("(bus) Trying to send telegram to bus that has not implemented sending!\n");
    return false;
}

static bool ignore_duplicate_telegrams_ = false;

void setIgnoreDuplicateTelegrams(bool idt)
{
    ignore_duplicate_telegrams_ = idt;
}

static bool detailed_first_ = false;

void setDetailedFirst(bool df)
{
    detailed_first_ = df;
}

bool getDetailedFirst()
{
    return detailed_first_;
}

bool BusDeviceCommonImplementation::handleTelegram(AboutTelegram &about, vector<uchar> frame)
{
    bool handled = false;
    last_received_ = time(NULL);

    assert(frame.size() > 0);

    if (about.type == FrameType::MBUS && frame.size() == 1)
    {
        if (frame[0] == 0xe5)
        {
            // Ack from meter, currently ignored.
            return true;
        }
        // Something else that we currently do not understand.
        return false;
    }

    if (ignore_duplicate_telegrams_ && about.type == FrameType::WMBUS && seen_this_telegram_before(frame))
    {
        verbose("(wmbus) skipping already handled telegram leng=%zu.\n", frame.size());
        return true;
    }

    if (about.type == FrameType::WMBUS)
    {
        size_t expected_len = frame[0]+1;
        if (frame.size() > 0 && expected_len != frame.size())
        {
            warning("(wmbus) telegram length byte (the first) 0x%02x (%d) is probably wrong. Expected 0x%02x (%zu) based on the length of the telegram.\n",
                    frame[0], frame[0], frame.size()-1, frame.size()-1);
        }
    }

    for (auto f : telegram_listeners_)
    {
        if (f)
        {
            bool h = f(about, frame);
            if (h) handled = true;
        }
    }

    return handled;
}

void BusDeviceCommonImplementation::protocolErrorDetected()
{
    protocol_error_count_++;
}

void BusDeviceCommonImplementation::resetProtocolErrorCount()
{
    protocol_error_count_ = 0;
}

void BusDeviceCommonImplementation::setLinkModes(LinkModeSet lms)
{
    link_modes_ = lms;
    retrySetLinkModes(lms);
    link_modes_configured_ = true;
}

void BusDeviceCommonImplementation::retrySetLinkModes(LinkModeSet lms)
{
    int tries = 0;
    for (;;)
    {
        bool ok = deviceSetLinkModes(lms);
        if (ok) break;
        if (!manager_->isRunning()) break;
        tries++;
        if (tries > 3)
        {
            string msg = tostrprintf("failed to reset wmbus device %s exiting wmbusmeters", hr().c_str());
            warning("(wmbus) Permanent error, %s\n", msg.c_str());
            logAlarm(Alarm::DeviceFailure, msg);
            manager_->stop();
        }
    }
}

bool BusDeviceCommonImplementation::areLinkModesConfigured()
{
    return link_modes_configured_;
}

void BusDeviceCommonImplementation::setDeviceMode(DeviceMode mode)
{
    device_mode_ = mode;
    deviceSetDeviceMode(mode);
}

void BusDeviceCommonImplementation::deviceSetDeviceMode(DeviceMode mode)
{
}

DeviceMode BusDeviceCommonImplementation::deviceMode()
{
    return device_mode_;
}

LinkModeSet BusDeviceCommonImplementation::protectedGetLinkModes()
{
    return link_modes_;
}

void BusDeviceCommonImplementation::deviceClose()
{
}

void BusDeviceCommonImplementation::close()
{
    debug("(wmbus) closing....\n");
    if (serial())
    {
        if (serial()->opened() && serial()->working())
        {
            debug("(wmbus) yes closing....\n");
            serial()->close();
            manager_->removeNonWorking(serial()->device());
            serial_ = NULL;
        }
    }

    // Invoke any other device specific close for this device.
    deviceClose();
}

bool BusDeviceCommonImplementation::reset()
{
    last_reset_ = time(NULL);
    bool resetting = false;
    if (serial())
    {
        if (serial()->opened() && serial()->working())
        {
            // This is a reset, not an init. Close the serial device.
            resetting = true;
            serial()->resetInitiated();
            serial()->close();
            notice_timestamp("(wmbus) resetting %s\n", hr().c_str());

            // Give the device 3 seconds to shut down properly.
            usleep(3000*1000);
        }

        bool ok = serial()->open(false);

        if (!ok)
        {
            // Ouch....
            return false;
        }
    }

    // Invoke any other device specific resets for this device.
    deviceReset();

    if (resetting)
    {
        serial()->resetCompleted();
    }

    // If init, then no link modes are configured.
    // If reset, re-initialize the link modes.
    if (areLinkModesConfigured())
    {
        retrySetLinkModes(protectedGetLinkModes());
    }

    if (resetting)
    {
        notice_timestamp("(wmbus) reset completed %s\n", hr().c_str());
    }
    return true;
}

void BusDeviceCommonImplementation::disconnectedFromDevice()
{
    if (is_working_)
    {
        debug("(wmbus) disconnected %s %s\n", device().c_str(), toString(type()));
        is_working_ = false;
    }
}

bool BusDeviceCommonImplementation::isWorking()
{
    return is_working_;
}

void BusDeviceCommonImplementation::checkStatus()
{
    trace("[ALARM] check status\n");

    time_t since_last_reset = time(NULL) - last_reset_;
    if (reset_timeout_ > 1 &&
        since_last_reset > reset_timeout_ &&
        !serial()->checkIfDataIsPending() &&
        !serial()->readonly())
    {
        verbose("(wmbus) regular reset of %s %s\n", device().c_str(), toString(type()));
        bool ok = reset();
        if (ok) return;
        string msg;
        strprintf(&msg, "failed regular reset of %s %s", device().c_str(), toString(type()));
        logAlarm(Alarm::RegularResetFailure, msg);
        return;
    }

    if (protocol_error_count_ >= 20)
    {
        string msg;
        strprintf(&msg, "too many protocol errors(%d) resetting %s %s", protocol_error_count_, device().c_str(), toString(type()));
        logAlarm(Alarm::DeviceFailure, msg);
        bool ok = reset();
        if (ok)
        {
            warning("(wmbus) successfully reset wmbus device\n");
            resetProtocolErrorCount();
            return;
        }

        strprintf(&msg, "failed to reset wmbus device %s %s exiting wmbusmeters", device().c_str(), toString(type()));
        logAlarm(Alarm::DeviceFailure, msg);
        manager_->stop();
        return;
    }

    time_t now = time(NULL);
    time_t then = now - timeout_;
    time_t since = now-last_received_;

    // If no timeout set, just return.
    if (timeout_ == 0) return;

    if (since < timeout_)
    {
        trace("[WMBUS] No timeout since=%d timeout=%d. All ok.\n", since, timeout_);
        return;
    }

    last_received_ = time(NULL);
    debug("(wmbus) updated_last received for %s (%s)\n", toString(type()), device().c_str());

    // The timeout has expired! But is the timeout expected because there should be no activity now?
    // Also, do not sound the alarm unless we actually have a possible timeout within the expected activity,
    // otherwise we will always get an alarm when we enter the expected activity period.
    if (!(isInsideTimePeriod(now, expected_activity_) &&
          isInsideTimePeriod(then, expected_activity_)))
    {
        trace("[WMBUS] hit timeout(%d s) but this is ok, since there is no expected activity.\n", timeout_);
        return;
    }

    // Ok, timeout has triggered for real! Deal with it!
    struct tm nowtm;
    localtime_r(&now, &nowtm);

    string nowtxt = strdatetime(&nowtm);

    string msg;
    strprintf(&msg, "%d seconds of inactivity resetting %s %s "
              "(timeout %ds expected %s now %s)",
              since, device().c_str(), toString(type()),
              timeout_, expected_activity_.c_str(), nowtxt.c_str());

    logAlarm(Alarm::DeviceInactivity, msg);

    bool ok = reset();
    if (ok)
    {
        warning("(wmbus) successfully reset wmbus device\n");
    }
    else
    {
        strprintf(&msg, "failed to reset wmbus device %s %s exiting wmbusmeters", device().c_str(), toString(type()));
        logAlarm(Alarm::DeviceFailure, msg);
        manager_->stop();
    }
}

void BusDeviceCommonImplementation::setResetInterval(int seconds)
{
    reset_timeout_ = seconds;
}

void BusDeviceCommonImplementation::setTimeout(int seconds, string expected_activity)
{
    assert(seconds >= 0);

    timeout_ = seconds;
    if (expected_activity == "")
    {
        expected_activity = "mon-sun(00-23)";
    }
    expected_activity_ = expected_activity;
    if (seconds > 0)
    {
        debug("(wmbus) set timeout %s to \"%d\" with expected activity \"%s\"\n", toString(type_), timeout_, expected_activity_.c_str());
    }
    else
    {
        debug("(wmbus) no alarm (expected activity) for %s\n", toString(type_));
    }
}

bool BusDeviceCommonImplementation::waitForResponse(int id)
{
    assert(id != 0);

    if (waiting_for_response_id_ != 0)
    {
        error("(wmbus) bad internal state tried waitForResponse(%d) but already waiting for %d! Exiting!\n", id, waiting_for_response_id_);
    }

    waiting_for_response_id_ = id;

    bool ok = waiting_for_response_sem_.wait();
    if (ok) return true;

    // Ouch, we had a timeout. Reset the waiting for value here.
    warning("(wmbus device) timeout request id %d\n", waiting_for_response_id_);
    waiting_for_response_id_ = 0;

    return false;
}

bool BusDeviceCommonImplementation::notifyResponseIsHere(int id)
{
    if (id != waiting_for_response_id_) return false;

    waiting_for_response_id_ = 0;
    waiting_for_response_sem_.notify();

    return true;
}

int toInt(TPLSecurityMode tsm)
{
    switch (tsm) {

#define X(name,nr) case TPLSecurityMode::name : return nr;
LIST_OF_TPL_SECURITY_MODES
#undef X
    }

    return 16;
}

const char *toString(TPLSecurityMode tsm)
{
    switch (tsm) {

#define X(name,nr) case TPLSecurityMode::name : return #name;
LIST_OF_TPL_SECURITY_MODES
#undef X
    }

    return "Reserved";
}

TPLSecurityMode fromIntToTPLSecurityMode(int i)
{
    switch (i) {

#define X(name,nr) case nr: return TPLSecurityMode::name;
LIST_OF_TPL_SECURITY_MODES
#undef X
    }

    return TPLSecurityMode::SPECIFIC_16_31;
}

int toInt(ELLSecurityMode esm)
{
    switch (esm) {

#define X(name,nr) case ELLSecurityMode::name : return nr;
LIST_OF_ELL_SECURITY_MODES
#undef X
    }

    return 2;
}

const char *toString(ELLSecurityMode esm)
{
    switch (esm) {

#define X(name,nr) case ELLSecurityMode::name : return #name;
LIST_OF_ELL_SECURITY_MODES
#undef X
    }

    return "?";
}

ELLSecurityMode fromIntToELLSecurityMode(int i)
{
    switch (i) {

#define X(name,nr) case nr: return ELLSecurityMode::name;
LIST_OF_ELL_SECURITY_MODES
#undef X
    }

    return ELLSecurityMode::RESERVED;
}

void Telegram::extractMfctData(vector<uchar> *pl)
{
    pl->clear();
    if (mfct_0f_index == -1) return;

    vector<uchar>::iterator from = frame.begin()+header_size+mfct_0f_index;
    vector<uchar>::iterator to = frame.end()-suffix_size;
    pl->insert(pl->end(), from, to);
}

void Telegram::extractPayload(vector<uchar> *pl)
{
    pl->clear();
    vector<uchar>::iterator from = frame.begin()+header_size;
    vector<uchar>::iterator to = frame.end()-suffix_size;
    pl->insert(pl->end(), from, to);
}

void Telegram::extractFrame(vector<uchar> *fr)
{
    *fr = frame;
}

int toInt(AFLAuthenticationType aat)
{
    switch (aat) {

#define X(name,nr,len) case AFLAuthenticationType::name : return nr;
LIST_OF_AFL_AUTH_TYPES
#undef X
    }

    return 16;
}

int toLen(AFLAuthenticationType aat)
{
    switch (aat) {

#define X(name,nr,len) case AFLAuthenticationType::name : return len;
LIST_OF_AFL_AUTH_TYPES
#undef X
    }

    return 0;
}

const char *toString(AFLAuthenticationType tsm)
{
    switch (tsm) {

#define X(name,nr,len) case AFLAuthenticationType::name : return #name;
LIST_OF_AFL_AUTH_TYPES
#undef X
    }

    return "Reserved";
}

AFLAuthenticationType fromIntToAFLAuthenticationType(int i)
{
    switch (i) {
#define X(name,nr,len) case nr: return AFLAuthenticationType::name;
LIST_OF_AFL_AUTH_TYPES
#undef X
    }

    return AFLAuthenticationType::Reserved1;
}

bool trimCRCsFrameFormatAInternal(std::vector<uchar> &payload, bool fail_is_ok)
{
    if (payload.size() < 12) {
        if (!fail_is_ok)
        {
            debug("(wmbus) not enough bytes! expected at least 12 but got (%zu)!\n", payload.size());
        }
        return false;
    }
    size_t len = payload.size();
    if (!fail_is_ok)
    {
        debugPayload("(wmbus) trimming frame A", payload);
    }

    vector<uchar> out;

    uint16_t calc_crc = crc16_EN13757(safeButUnsafeVectorPtr(payload), 10);
    uint16_t check_crc = payload[10] << 8 | payload[11];

    if (calc_crc != check_crc && !FUZZING)
    {
        if (!fail_is_ok)
        {
            debug("(wmbus) ff a dll crc first (calculated %04x) did not match (expected %04x) for bytes 0-%zu!\n", calc_crc, check_crc, 10);
        }
        return false;
    }
    out.insert(out.end(), payload.begin(), payload.begin()+10);
    if (!fail_is_ok)
    {
        debug("(wmbus) ff a dll crc 0-%zu %04x ok\n", 10-1, calc_crc);
    }

    size_t pos = 12;
    for (pos = 12; pos+18 <= len; pos += 18)
    {
        size_t to = pos+16;
        calc_crc = crc16_EN13757(&payload[pos], 16);
        check_crc = payload[to] << 8 | payload[to+1];
        if (calc_crc != check_crc && !FUZZING)
        {
            if (!fail_is_ok)
            {
                debug("(wmbus) ff a dll crc mid (calculated %04x) did not match (expected %04x) for bytes %zu-%zu!\n",
                      calc_crc, check_crc, pos, to-1);
            }
            return false;
        }
        out.insert(out.end(), payload.begin()+pos, payload.begin()+pos+16);
        if (!fail_is_ok)
        {
            debug("(wmbus) ff a dll crc mid %zu-%zu %04x ok\n", pos, to-1, calc_crc);
        }
    }

    if (pos < len-2)
    {
        size_t tto = len-2;
        size_t blen = (tto-pos);
        calc_crc = crc16_EN13757(&payload[pos], blen);
        check_crc = payload[tto] << 8 | payload[tto+1];
        if (calc_crc != check_crc && !FUZZING)
        {
            if (!fail_is_ok)
            {
                debug("(wmbus) ff a dll crc final (calculated %04x) did not match (expected %04x) for bytes %zu-%zu!\n",
                      calc_crc, check_crc, pos, tto-1);
            }
            return false;
        }
        out.insert(out.end(), payload.begin()+pos, payload.begin()+tto);
        if (!fail_is_ok)
        {
            debug("(wmbus) ff a dll crc final %zu-%zu %04x ok\n", pos, tto-1, calc_crc);
        }
    }

    debugPayload("(wmbus) trimming frame A", payload);

    out[0] = out.size()-1;
    size_t new_len = out[0]+1;
    size_t old_size = payload.size();
    payload = out;
    size_t new_size = payload.size();

    debug("(wmbus) trimmed %zu dll crc bytes from frame a and ignored %zu suffix bytes.\n", (len-new_len), (old_size-new_size)-(len-new_len));
    debugPayload("(wmbus) trimmed frame A", payload);

    return true;
}

bool trimCRCsFrameFormatBInternal(std::vector<uchar> &payload, bool fail_is_ok)
{
    if (payload.size() < 12) {
        if (!fail_is_ok)
        {
            debug("(wmbus) not enough bytes! expected at least 12 but got (%zu)!\n", payload.size());
        }
        return false;
    }
    size_t len = payload.size();
    if (!fail_is_ok)
    {
        debugPayload("(wmbus) trimming frame B", payload);
    }

    vector<uchar> out;
    size_t crc1_pos, crc2_pos;
    if (len <= 128)
    {
        crc1_pos = len-2;
        crc2_pos = 0;
    }
    else
    {
        crc1_pos = 126;
        crc2_pos = len-2;
    }

    uchar *from1 = &payload[0];
    size_t len1 = crc1_pos;
    uint16_t calc_crc = crc16_EN13757(from1, len1);
    uint16_t check_crc = payload[crc1_pos] << 8 | payload[crc1_pos+1];

    if (calc_crc != check_crc && !FUZZING)
    {
        if (!fail_is_ok)
        {
            debug("(wmbus) ff b dll crc (calculated %04x) did not match (expected %04x) for bytes 0-%zu!\n", calc_crc, check_crc, crc1_pos);
        }
        return false;
    }

    out.insert(out.end(), payload.begin(), payload.begin()+crc1_pos);
    if (!fail_is_ok)
    {
        debug("(wmbus) ff b dll crc first 0-%zu %04x ok\n", crc1_pos, calc_crc);
    }

    if (crc2_pos > 0)
    {
        uchar *from2 = &payload[crc1_pos+2];
        size_t len2 = crc2_pos-crc1_pos-2;
        calc_crc = crc16_EN13757(from2, len2);
        check_crc = payload[crc2_pos] << 8 | payload[crc2_pos+1];

        if (calc_crc != check_crc && !FUZZING)
        {
            if (!fail_is_ok)
            {
                debug("(wmbus) ff b dll crc (calculated %04x) did not match (expected %04x) for bytes %zu-%zu!\n",
                      calc_crc, check_crc, crc1_pos+2, crc2_pos);
            }
            return false;
        }

        out.insert(out.end(), payload.begin()+crc1_pos+2, payload.begin()+crc2_pos);
        if (!fail_is_ok)
        {
            debug("(wmbus) ff b dll crc final %zu-%zu %04x ok\n", crc1_pos+2, crc2_pos, calc_crc);
        }
    }

    debugPayload("(wmbus) trimming frame B", payload);

    out[0] = out.size()-1;
    size_t new_len = out[0]+1;
    size_t old_size = payload.size();
    payload = out;
    size_t new_size = payload.size();

    debug("(wmbus) trimmed %zu dll crc bytes from frame b and ignored %zu suffix bytes.\n", (len-new_len), (old_size-new_size)-(len-new_len));
    debugPayload("(wmbus) trimmed frame B", payload);

    return true;
}

void removeAnyDLLCRCs(std::vector<uchar> &payload)
{
    bool trimmed = trimCRCsFrameFormatAInternal(payload, true);
    if (!trimmed) trimCRCsFrameFormatBInternal(payload, true);
}

bool trimCRCsFrameFormatA(std::vector<uchar> &payload)
{
    return trimCRCsFrameFormatAInternal(payload, false);
}

bool trimCRCsFrameFormatB(std::vector<uchar> &payload)
{
    return trimCRCsFrameFormatBInternal(payload, false);
}

FrameStatus checkWMBusFrame(vector<uchar> &data,
                            size_t *frame_length,
                            int *payload_len_out,
                            int *payload_offset,
                            bool only_test)
{
    // Nice clean: 2A442D2C998734761B168D2021D0871921|58387802FF2071000413F81800004413F8180000615B
    // Ugly: 00615B2A442D2C998734761B168D2021D0871921|58387802FF2071000413F81800004413F8180000615B
    // Here the frame is prefixed with some random data.

    debugPayload("(wmbus) checkWMBUSFrame", data);

    if (data.size() < 11)
    {
        debug("(wmbus) less than 11 bytes, partial frame\n");
        return PartialFrame;
    }
    int payload_len = data[0];
    int type = data[1];
    int offset = 1;

    if (data[0] == 0x68 && data[3] == 0x68 && data[1] == data[2])
    {
        // Ooups this is not a wmbus frame. Its clearly an mbus frame.
        return PartialFrame;
    }
    if (!isValidWMBusCField(type))
    {
        // Ouch, we are out of sync with the wmbus frames that we expect!
        // Since we currently do not handle any other type of frame, we can
        // look for a valid c field (ie 0x44 0x46 etc) in the buffer.
        // If we find such a byte and the length byte before maps to the end
        // of the buffer, then we have found a valid telegram.
        bool found = false;
        for (size_t i = 0; i < data.size()-2; ++i)
        {
            if (isValidWMBusCField(data[i+1]))
            {
                payload_len = data[i];
                size_t remaining = data.size()-i;
                if (data[i]+1 == (uchar)remaining && data[i+1] == 0x44)
                {
                    found = true;
                    offset = i+1;
                    verbose("(wmbus) out of sync, skipping %d bytes.\n", (int)i);
                    break;
                }
            }
        }
        if (!found)
        {
            // No sensible telegram in the buffer. Flush it!
            if (!only_test)
            {
                verbose("(wmbus) no sensible telegram found, clearing buffer.\n");
                data.clear();
            }
            else
            {
                debug("(wmbus) not a proper wmbus frame.\n");
            }
            return ErrorInFrame;
        }
    }
    *payload_len_out = payload_len;
    *payload_offset = offset;
    *frame_length = payload_len+offset;
    if (data.size() < *frame_length)
    {
        // Not enough bytes for this payload_len....
        if (only_test)
        {
            // This is used from simulate files and hex in command line and analyze.
            // Lets be lax and just adjust the length to what is available.
            payload_len = data.size() - offset;
            *payload_len_out = payload_len;
            *frame_length = payload_len+offset;
            warning("(wmbus) not enough bytes, frame length byte changed from %d(%02x) to %d(%02x)!\n",
                    data[offset-1], data[offset-1],
                    payload_len, payload_len);
            data[offset-1] = payload_len;

            return FullFrame;
        }
        debug("(wmbus) not enough bytes, partial frame %d %d\n", data.size(), *frame_length);
        return PartialFrame;
    }

    if (!only_test)
    {
        debug("(wmbus) received full frame.\n");
    }
    return FullFrame;
}

FrameStatus checkMBusFrame(vector<uchar> &data,
                           size_t *frame_length,
                           int *payload_len_out,
                           int *payload_offset,
                           bool only_test)
{
    // Example:
    // E5
    // 68383868 start length 0x38 = 56
    // 56 bytes of payload data:
    // 080072840200102941011B010000000265AE084265C208B20165000002FB1A450142FB1A4C01B201FB1A00000C788402001002FD0F21000F
    // 5E checksum
    // 16 stop

    debugPayload("(mbus) checkMBUSFrame", data);

    if (data.size() > 0 && data[0] == 0xe5)
    {
        // Single character confirmation frame.
        if (only_test)
        {
            // For testing purposes we require the frame to be a single char as well.
            // This happens when we pass a frame on the command line or in a simulation file.
            // Otherwise a normal wmbus telegram with length e5 will triggere this mbus command.
            // (When reading from a serial line, we might have data coming after the e5,
            // but then we know that we are talking mbus.)
            if (data.size() != 1)
            {
                return ErrorInFrame;
            }
        }
        *payload_len_out = 1;
        *payload_offset = 0;
        *frame_length = 1;
        if (!only_test)
        {
            debug("(mbus) received E5 single byte frame.\n");
        }
        return FullFrame;
    }
    if (data.size() < 6)
    {
        // 4 byte start, 1 checksum, 1 stop
        if (!only_test)
        {
            debug("(mbus) less than 6 bytes, partial frame\n");
        }
        return PartialFrame;
    }
    if (data[0] != 0x68 && data[3] != 0x68)
    {
        if (!only_test)
        {
            verbose("(mbus) no 0x68 byte found, clearing buffer.\n");
            data.clear();
        }
        return ErrorInFrame;
    }

    if (data[1] != data[2])
    {
        if (!only_test)
        {
            verbose("(mbus) lengths not matching, clearing buffer.\n");
            data.clear();
        }
        return ErrorInFrame;
    }
    int payload_len = data[1];
    *frame_length = payload_len+4+1+1; // start(4)+cs(1)+stop(1)
    if (data.size() < *frame_length)
    {
        if (!only_test)
        {
            debug("(mbus) not enough bytes, partial frame %d %d\n", data.size(), *frame_length);
        }
        return PartialFrame;
    }
    uchar stop = data[*frame_length-1];
    if (stop != 0x16)
    {
        warning("(mbus) stop byte (0x%02x) at pos %d is not 0x16, clearing buffer.\n", stop, *frame_length-1);
        data.clear();
        return ErrorInFrame;
    }
    uchar csc = 0;
    for (size_t i=4; i<(*frame_length-2); ++i) csc += data[i];
    uchar cs = data[*frame_length-2];
    if (cs != csc)
    {
        warning("(mbus) expected checksum 0x%02x but got 0x%02x, clearing buffer.\n", csc, cs);
        data.clear();
        return ErrorInFrame;
    }

    *payload_len_out = *frame_length;
    *payload_offset = 0;
    if (!only_test)
    {
        debug("(mbus) received full frame.\n");
    }
    return FullFrame;
}

string decodeTPLStatusByteOnlyStandardBits(uchar sts)
{
    // Bits 0-4 are standard defined. Bits 5-7 are mfct specific.
    string s;

    if (sts == 0) return "OK";
    if ((sts & 0x03) == 0x01) s += "BUSY ";  // Meter busy, cannot respond.
    if ((sts & 0x03) == 0x02) s += "ERROR "; // E.g. meter failed to understand a message sent to it.
                                             // More information about the error can be sent using error reporting, EN13757-3:2018 §10
    if ((sts & 0x03) == 0x03) s += "ALARM "; // E.g. an abnormal condition like water is continuously running.

    if ((sts & 0x04) == 0x04) s += "POWER_LOW "; // E.g. battery end of life or external power supply failure
    if ((sts & 0x08) == 0x08) s += "PERMANENT_ERROR "; // E.g. meter needs service to work again.
    if ((sts & 0x10) == 0x10) s += "TEMPORARY_ERROR ";

    while (s.size() > 0 && s.back() == ' ') s.pop_back();
    return s;
}

string decodeTPLStatusByteNoMfct(uchar sts)
{
    string t = "OK";

    if ((sts & 0xe0) != 0)
    {
        t = tostrprintf("UNKNOWN_%02X", sts & 0xe0);
    }

    return t;
}

string decodeTPLStatusByteWithMfct(uchar sts, Translate::Lookup &lookup)
{
    string s = decodeTPLStatusByteOnlyStandardBits(sts);
    string t = "OK";

    if ((sts & 0xe0) != 0)
    {
        // Vendor specific bits are set, lets translate them.
        if (lookup.hasLookups())
        {
            t = lookup.translate(sts & 0xe0);
        }
        else
        {
            t = decodeTPLStatusByteNoMfct(sts & 0xe0);
        }
    }

    if (t == "OK" || t == "") return s;
    if (s == "OK" || s == "") return t;

    return s+" "+t;
}

const char *toString(BusDeviceType t)
{
    switch (t)
    {
#define X(name,text,tty,rtlsdr,detector) case DEVICE_ ## name: return #text;
LIST_OF_MBUS_DEVICES
#undef X

    }
    return "?";
}

const char *toLowerCaseString(BusDeviceType t)
{
    switch (t)
    {
#define X(name,text,tty,rtlsdr,detector) case DEVICE_ ## name: return #text;
LIST_OF_MBUS_DEVICES
#undef X

    }
    return "?";
}

BusDeviceType toBusDeviceType(string &t)
{
#define X(name,text,tty,rtlsdr,detector) if (t == #text) return DEVICE_ ## name;
LIST_OF_MBUS_DEVICES
#undef X
    return DEVICE_UNKNOWN;
}

bool is_command(string b, string *cmd)
{
    // Check if CMD(.)
    if (b.length() < 6) return false;
    if (b.rfind("CMD(", 0) != 0) return false;
    if (b.back() != ')') return false;
    *cmd = b.substr(4, b.length()-5);
    return true;
}

bool check_file(string f, bool *is_tty, bool *is_stdin, bool *is_file, bool *is_simulation, bool *is_hex_simulation)
{
    *is_tty = *is_stdin = *is_file = *is_simulation = *is_hex_simulation = false;
    if (f == "stdin")
    {
        *is_stdin = true;
        return true;
    }
    if (f == "rtlwmbus" || f == "rlt433")
    {
        // Prevent wmbusmeters from finding a file named rtlwmbus or rtl433 since this is probably a usage error.
        // Most likely the user accidentally created such a file. Without this test the existence of such
        // a file will confuse the user to no end....
        return false;
    }
    // A hex string becomes a simulation file with a single line containing a telegram defined by the hex string.
    bool invalid_hex = false;
    if (isHexStringFlex(f.c_str(), &invalid_hex))
    {
        *is_simulation = true;
        *is_hex_simulation = true;
        assert(!invalid_hex);
        return true;
    }
    // A command line arguments simulation_xxyyzz.txt is detected as a simulation file.
    if (checkIfSimulationFile(f.c_str()))
    {
        *is_simulation = true;
        return true;
    }
    if (checkCharacterDeviceExists(f.c_str(), false))
    {
        *is_tty = true;
        return true;
    }
    if (checkFileExists(f.c_str()))
    {
        *is_file = true;
        return true;
    }
    if (f.find("/dev") != string::npos)
    {
        // Meter names are forbidden to have slashes in their names.
        // This is probably a path to /dev/ttyUSB0 that does not exist right now.
        *is_tty = true;
        return true;
    }
    return false;
}

bool is_type_id_extras(string t, BusDeviceType *out_type, string *out_id, string *out_extras)
{
    // im871a im871a[12345678] im871a(foo=123)
    // auto
    // rtlwmbus rtlwmbus[plast123] rtlwmbus(device extras) rtlwmbus[hej](extras)
    // hex
    if (t == "auto")
    {
        *out_type = BusDeviceType::DEVICE_AUTO;
        *out_id = "";
        return true;
    }

    if (t == "hex")
    {
        *out_type = BusDeviceType::DEVICE_HEXTTY;
        *out_id = "";
        return true;
    }

    if (t == "xmql")
    {
        *out_type = BusDeviceType::DEVICE_XMQTTY;
        *out_id = "";
        return true;
    }

    if (t == "jsonl")
    {
        *out_type = BusDeviceType::DEVICE_XMQTTY;
        *out_id = "";
        return true;
    }

    if (t == "xmll")
    {
        *out_type = BusDeviceType::DEVICE_XMQTTY;
        *out_id = "";
        return true;
    }

    size_t bs = t.find('[');
    size_t be = t.find(']');

    size_t ps = t.find('(');
    size_t pe = t.find(')');

    size_t te = 0; // Position after type end.

    bool found_brackets = (bs != string::npos && be != string::npos);
    bool found_parentheses = (ps != string::npos && pe != string::npos);

    if (!found_brackets && !found_parentheses)
    {
        // No brackets nor parentheses found, is t a known wmbus device? like im871a amb8465 etc....
        BusDeviceType tt = toBusDeviceType(t);
        if (tt == DEVICE_UNKNOWN) return false;
        *out_type = toBusDeviceType(t);
        *out_id = "";
        return true;
    }

    if (found_brackets && !found_parentheses)
    {
        // Bracketed id must be last.
        if (! (bs > 0 && bs < be && be == t.length()-1)) return false;
        te = bs;
    }

    if (found_brackets && found_parentheses)
    {
        // Bracketed id must be second last. Parentheses immediately after bracket.
        if (! (bs > 0 && bs < be &&
               ps == be+1 && ps < pe &&
               pe == t.length()-1)) return false;
        te = bs;
    }

    if (!found_brackets && found_parentheses)
    {
        // Parentheses must be last.
        if (! (ps > 0 && ps < pe && pe == t.length()-1)) return false;
        te = ps;
    }

    string type = t.substr(0, te);
    BusDeviceType tt = toBusDeviceType(type);
    if (tt == DEVICE_UNKNOWN) return false;
    *out_type = toBusDeviceType(type);

    if (found_brackets)
    {
        string id = t.substr(bs+1, be-bs-1);
        *out_id = id;
    }

    if (found_parentheses)
    {
        string extras = t.substr(ps+1, pe-ps-1);
        *out_extras = extras;
    }

    return true;
}

void SpecifiedDevice::clear()
{
    file = "";
    type = BusDeviceType::DEVICE_UNKNOWN;
    id = "";
    extras = "";
    fq = "";
    linkmodes.clear();
}

string SpecifiedDevice::str()
{
    string r;
    if (bus_alias != "")
    {
        r += bus_alias+"=";
    }
    if (file != "") r += file+":";
    if (hex != "") r += "<"+hex+">:";
    if (type != BusDeviceType::DEVICE_UNKNOWN)
    {
        r += toString(type);
        if (id != "")
        {
            r += "["+id+"]";
        }
        if (extras != "")
        {
            r += "("+extras+")";
        }
        r += ":";
    }
    if (bps != "") r += bps+":";
    if (fq != "") r += fq+":";
    if (!linkmodes.empty()) r += linkmodes.hr()+":";
    if (command != "") r += "CMD("+command+"):";

    if (r.size() > 0) r.pop_back();

    if (r == "") return "auto";

    return r;
}

bool SpecifiedDevice::isLikelyDevice(string &arg)
{
    // Check that it is not a send bus content command.
    if (SendBusContent::isLikely(arg)) return false;
    // Only devices are allowed to contain colons.
    // Devices usually contain a colon!
    if (arg.find(":") != string::npos) return true;
    return false;
}

bool SpecifiedDevice::parse(string &arg)
{
    clear();

    size_t ep = arg.find("=");
    if (ep != string::npos)
    {
        // Is there an bus alias first?
        // BUS1=/dev/ttyUSB0
        bus_alias = arg.substr(0, ep);
        if (isValidAlias(bus_alias))
        {
            arg = arg.substr(ep+1);
        }
        else
        {
            bus_alias = "";
        }
    }

    bool file_checked = false;
    bool typeidextras_checked = false;
    bool bps_checked = false;
    bool fq_checked = false;
    bool linkmodes_checked = false;
    bool command_checked = false;

    // The : colon is forbidden inside the parts, except inside CMD(..:..)
    vector<string> parts = splitDeviceString(arg);

    // Most maxed out device spec, though not valid, since file+cmd is not allowed.
    // Example /dev/ttyUSB0:im871a[12345678](device=extras):9600:868.95M:c1,t1:CMD(rtl_433 -F csv -f 123M)

    //         file         type   id        bps  fq     linkmodes command
    for (auto& p : parts)
    {
        if (file_checked && typeidextras_checked && file == "" && type == BusDeviceType::DEVICE_UNKNOWN && id == "")
        {
            // There must be either a file and/or type(id). If none are found,
            // then the specified device string is faulty.
            return false;
        }
        if (!file_checked && check_file(p, &is_tty, &is_stdin, &is_file, &is_simulation, &is_hex_simulation))
        {
            file_checked = true;
            if (!is_hex_simulation)
            {
                file = p;
            }
            else
            {
                hex = p;
            }
        }
        else if (!typeidextras_checked && is_type_id_extras(p, &type, &id, &extras))
        {
            file_checked = true;
            typeidextras_checked = true;
        }
        else if (!bps_checked && isValidBps(p))
        {
            file_checked = true;
            typeidextras_checked = true;
            bps_checked = true;
            bps = p;
        }
        else if (!fq_checked && isFrequency(p))
        {
            file_checked = true;
            typeidextras_checked = true;
            bps_checked = true;
            fq_checked = true;
            fq = p;
        }
        else if (!linkmodes_checked && isValidLinkModes(p))
        {
            file_checked = true;
            typeidextras_checked = true;
            bps_checked = true;
            fq_checked = true;
            linkmodes_checked = true;
            linkmodes = parseLinkModes(p);
        }
        else if (!command_checked && is_command(p, &command))
        {
            file_checked = true;
            typeidextras_checked = true;
            bps_checked = true;
            fq_checked = true;
            linkmodes_checked = true;
            command_checked = true;
        }
        else
        {
            // Unknown part....
            return false;
        }
    }

    // Auto is only allowed to be combined with linkmodes and/or frequencies!
    if (type == BusDeviceType::DEVICE_AUTO && (file != "" || bps != "")) return false;
    // You cannot combine a file with a command.
    if (file != "" && command != "") return false;
    return true;
}

const char *toString(TelegramFormat format)
{
    if (format == TelegramFormat::WMBUS_C_FIELD) return "wmbus_c_field";
    if (format == TelegramFormat::WMBUS_CI_FIELD) return "wmbus_ci_field";
    if (format == TelegramFormat::MBUS_SHORT_FRAME) return "mbus_short_frame";
    if (format == TelegramFormat::MBUS_LONG_FRAME) return "mbus_long_frame";

    return "unknown";
}

TelegramFormat toTelegramFormat(const char *s)
{
    if (!strcmp(s, "wmbus_c_field")) return TelegramFormat::WMBUS_C_FIELD;
    if (!strcmp(s, "wmbus_ci_field")) return TelegramFormat::WMBUS_CI_FIELD;
    if (!strcmp(s, "mbus_short_frame")) return TelegramFormat::MBUS_SHORT_FRAME;
    if (!strcmp(s, "mbus_long_frame")) return TelegramFormat::MBUS_LONG_FRAME;

    return TelegramFormat::UNKNOWN;
}

const char *toString(DeviceMode mode)
{
    if (mode == DeviceMode::METER) return "meter";
    if (mode == DeviceMode::OTHER) return "other";
    return "unknown";
}

DeviceMode toDeviceMode(const char *s)
{
    if (!strcmp(s, "meter")) return DeviceMode::METER;
    if (!strcmp(s, "other")) return DeviceMode::OTHER;

    return DeviceMode::UNKNOWN;
}

bool SendBusContent::isLikely(const string &s)
{
    return s.rfind("send:", 0) == 0;
}

bool SendBusContent::parse(const string &s)
{
    bus = "";
    content = "";
    // Examples:
    //
    // send:LINK_MODE:FORMAT:BUS:DATA
    //
    // send:t2:c_field:OUTBUS:01020304050607
    // send:t1:ci_field:OUTBUS:01020304050607
    // send:mbus:short:OUTMBUS:001122
    // send:mbus:long:OUTMBUS:001122
    //     c1   c2   c3      c4

    size_t c1 = s.find(":");
    if (c1 == string::npos) return false;
    size_t c2 = s.find(":", c1+1);
    if (c2 == string::npos) return false;
    size_t c3 = s.find(":", c2+1);
    if (c3 == string::npos) return false;
    size_t c4 = s.find(":", c3+1);
    if (c4 == string::npos) return false;

    string cmd = s.substr(0, c1);
    if (cmd != "send") return false;

    link_mode = toLinkMode(s.substr(c1+1, c2-c1-1).c_str());
    if (link_mode == LinkMode::UNKNOWN) return false;

    format = toTelegramFormat(s.substr(c2+1, c3-c2-1).c_str());
    if (format == TelegramFormat::UNKNOWN) return false;

    bus = s.substr(c3+1, c4-c3-1);
    if (bus.size() == 0) return false;

    content = s.substr(c4+1);
    if (content.size() == 0) return false;
    if (content.size() % 2 == 1) return false;

    return true;
}

Detected detectBusDeviceOnTTY(string tty,
                              set<BusDeviceType> probe_for,
                              LinkModeSet desired_linkmodes,
                              shared_ptr<SerialCommunicationManager> handler,
                              string bps)
{
    Detected detected;
    // Fake a specified device.
    detected.found_file = tty;
    detected.specified_device.is_tty = true;
    detected.specified_device.linkmodes = desired_linkmodes;
    detected.specified_device.bps = std::move(bps);

    bool has_auto = probe_for.count(BusDeviceType::DEVICE_AUTO);

    AccessCheck ac = handler->checkAccess(tty, handler);
    if (ac != AccessCheck::AccessOK)
    {
        // Oups, some low level problem (permissions/groups etc) that means that we will not
        // be able to talk to the device. Lets stop here.
        return detected;
    }

    // If im87a is tested first, a delay of 1s must be inserted
    // before amb8465/3665 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/3665 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 (has_auto || probe_for.count(BusDeviceType::DEVICE_AMB8465) || probe_for.count(BusDeviceType::DEVICE_AMB3665))
    {
        if (detectAMB8465AMB3665(&detected, handler) == AccessCheck::AccessOK)
        {
            return detected;
        }
    }

    // Talk im871a with it...
    // assumes this device is configured for 57600 bps, which seems to be the default.
    if (has_auto || probe_for.count(BusDeviceType::DEVICE_IM871A) || probe_for.count(BusDeviceType::DEVICE_IM170A))
    {
        if (detectIM871AIM170A(&detected, handler) == AccessCheck::AccessOK)
        {
            return detected;
        }
    }

    // Talk RC1180 with it...
    // assumes this device is configured for 19200 bps, which seems to be the default.
    if (has_auto || probe_for.count(BusDeviceType::DEVICE_RC1180))
    {
        if (detectRC1180(&detected, handler) == AccessCheck::AccessOK)
        {
            return detected;
        }
    }

    // Talk CUL with it...
    // assumes this device is configured for 38400 bps, which seems to be the default.
    if (has_auto || probe_for.count(BusDeviceType::DEVICE_CUL))
    {
        if (detectCUL(&detected, handler) == AccessCheck::AccessOK)
        {
            return detected;
        }
    }

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

    // We could not auto-detect either. default is DEVICE_UNKNOWN.
    return detected;
}

Detected detectBusDeviceWithFileOrHex(SpecifiedDevice &specified_device,
                                      LinkModeSet default_linkmodes,
                                      shared_ptr<SerialCommunicationManager> handler)
{
    assert(specified_device.file != "" || specified_device.hex != "");
    assert(specified_device.command == "");
    debug("(lookup) with file/hex \"%s%s\" %s\n", specified_device.file.c_str(), specified_device.hex.c_str(),
          toString(specified_device.type));

    Detected detected;
    detected.found_file = specified_device.file;
    detected.found_hex = specified_device.hex;
    detected.setSpecifiedDevice(specified_device);
    // If <device>:c1 is missing :c1 then use --c1.
    LinkModeSet lms = specified_device.linkmodes;
    if (lms.empty())
    {
        lms = default_linkmodes;
    }
    if (specified_device.is_simulation)
    {
        debug("(lookup) driver: simulation %s\n", specified_device.hex != "" ? "hex" : "file");
        // A simulation file/hex has a lms of all by default, eg no simulation_foo.txt:t1 nor --t1
        if (specified_device.linkmodes.empty()) lms.setAll();
        detected.setAsFound("", DEVICE_SIMULATION, 0 , false, lms);
        return detected;
    }

    // Special case to cater for /dev/ttyUSB0:9600, ie the rawtty is implicit.
    if (specified_device.type == BusDeviceType::DEVICE_UNKNOWN && specified_device.bps != "" && specified_device.is_tty)
    {
        debug("(lookup) driver: rawtty\n");
        // A rawtty has a lms of all by default, eg no simulation_foo.txt:t1 nor --t1
        if (specified_device.linkmodes.empty()) lms.setAll();
        detected.setAsFound("", DEVICE_RAWTTY, atoi(specified_device.bps.c_str()), false, lms);
        return detected;
    }

    // Special case to cater for /dev/ttyUSB0:mbus:2400, ie an mbus master device.
    if (specified_device.type == BusDeviceType::DEVICE_MBUS)
    {
        debug("(lookup) driver: mbus\n");
        int bps = atoi(specified_device.bps.c_str());
        if (bps < 300)
        {
            // Default to 2400. This will be adjusted every time the meters are probed.
            bps = 2400;
        }
        detected.setAsFound("", DEVICE_MBUS, bps, false, lms);
        return detected;
    }

    // Special case to cater for raw_data.bin, ie the rawtty is implicit.
    if (specified_device.type == BusDeviceType::DEVICE_UNKNOWN && !specified_device.is_tty)
    {
        debug("(lookup) driver: raw file\n");
        // A rawtty has a lms of all by default, eg no simulation_foo.txt:t1 nor --t1
        if (specified_device.linkmodes.empty()) lms.setAll();
        detected.setAsFound("", DEVICE_RAWTTY, 0, true, lms);
        return detected;
    }

    // Now handle all files (ie not ttys) with specified type.
    if (specified_device.type != BusDeviceType::DEVICE_UNKNOWN &&
        specified_device.type != BusDeviceType::DEVICE_AUTO &&
        !specified_device.is_tty)
    {
        debug("(lookup) driver: %s\n", toString(specified_device.type));
        assert(!lms.empty());
        detected.setAsFound("", specified_device.type, 0, specified_device.is_file || specified_device.is_stdin, lms);
        return detected;
    }
    // Ok, we are left with a single /dev/ttyUSB0 lets talk to it
    // to figure out what is connected to it.
    LinkModeSet desired_linkmodes = lms;
    if (specified_device.type == BusDeviceType::DEVICE_UNKNOWN)
    {
        error("You have to specify the expected device type for the tty %s\n",
              specified_device.file.c_str());
    }

    set<BusDeviceType> probe_for = { specified_device.type };

    Detected d = detectBusDeviceOnTTY(specified_device.file, probe_for, desired_linkmodes, handler, specified_device.bps);
    if (specified_device.type != d.found_type &&
        specified_device.type != DEVICE_UNKNOWN)
    {
        warning("Expected %s on %s but did not find it! Ignoring tty!\n",
                toLowerCaseString(specified_device.type),
                specified_device.file.c_str());

        d.found_file = specified_device.file;
        d.found_type = BusDeviceType::DEVICE_UNKNOWN;
    }
    else
    {
        d.specified_device = specified_device;
    }
    return d;
}

Detected detectBusDeviceWithCommand(SpecifiedDevice &specified_device,
                                    LinkModeSet default_linkmodes,
                                    shared_ptr<SerialCommunicationManager> handler)
{
    assert(specified_device.file == "");
    assert(specified_device.command != "");
    debug("(lookup) with cmd \"%s\"\n", specified_device.str().c_str());

    Detected detected;
    detected.setSpecifiedDevice(specified_device);
    LinkModeSet lms = specified_device.linkmodes;
    // If the specified device did not set any linkmodes fall back on the default linkmodes.
    if (lms.empty()) lms = default_linkmodes;
    detected.setAsFound("", specified_device.type, 0, false, lms);

    return detected;
}

AccessCheck detectUNKNOWN(Detected *detected, shared_ptr<SerialCommunicationManager> handler)
{
    return AccessCheck::NoSuchDevice;
}

AccessCheck detectSKIP(Detected *detected, shared_ptr<SerialCommunicationManager> handler)
{
    return AccessCheck::NoSuchDevice;
}

AccessCheck detectSIMULATION(Detected *detected, shared_ptr<SerialCommunicationManager> handler)
{
    return AccessCheck::NoSuchDevice;
}

AccessCheck detectAUTO(Detected *detected, shared_ptr<SerialCommunicationManager> handler)
{
    // Detection of auto is currently not implemented here, but elsewhere.
    return AccessCheck::NoSuchDevice;
}

AccessCheck reDetectDevice(Detected *detected, shared_ptr<SerialCommunicationManager> handler)
{
    BusDeviceType type = detected->specified_device.type;

#define X(name,text,tty,rtlsdr,detector) if (type == BusDeviceType::DEVICE_ ## name) return detector(detected,handler);
LIST_OF_MBUS_DEVICES
#undef X

    assert(0);
    return AccessCheck::NoSuchDevice;
}

bool usesRTLSDR(BusDeviceType t)
{
#define X(name,text,tty,rtlsdr,detector) if (t == BusDeviceType::DEVICE_ ## name) return rtlsdr;
LIST_OF_MBUS_DEVICES
#undef X

    assert(0);
    return false;
}

bool usesTTY(BusDeviceType t)
{
#define X(name,text,tty,rtlsdr,detector) if (t == BusDeviceType::DEVICE_ ## name) return tty;
LIST_OF_MBUS_DEVICES
#undef X

    assert(0);
    return false;
}

const char *toString(FrameType ft)
{
    switch (ft) {
    case FrameType::WMBUS: return "wmbus";
    case FrameType::MBUS: return "mbus";
    case FrameType::HAN: return "han";
    }
    return "?";
}

const char *toString(FrameStatus fs)
{
    switch (fs) {
    case FrameStatus::PartialFrame: return "PartialFrame";
    case FrameStatus::FullFrame: return "FullFrame";
    case FrameStatus::ErrorInFrame: return "ErrorInFrame";
    case FrameStatus::TextAndNotFrame: return "TextAndNotFrame";
    }
    return "?";
}

int genericifyMedia(int media)
{
    if (media == 0x06 || // Warm Water (30°C-90°C) meter
        media == 0x07 || // Water meter
        media == 0x15 || // Hot water (>=90°C) meter
        media == 0x16 || // Cold water meter
        media == 0x28)   // Waste water
    {
        return 0x07; // Return plain water
    }
    return media;
}

bool isCloseEnough(int media1, int media2)
{
    return genericifyMedia(media1) == genericifyMedia(media2);
}
