diff --git a/src/ebusd/knxhandler.cpp b/src/ebusd/knxhandler.cpp index 2db5e7e6..29a6e781 100644 --- a/src/ebusd/knxhandler.cpp +++ b/src/ebusd/knxhandler.cpp @@ -30,6 +30,11 @@ #include "lib/utils/log.h" #include "lib/ebus/symbol.h" +#ifndef POLLRDHUP +#define POLLRDHUP 0 +#endif + + namespace ebusd { using std::dec; @@ -49,7 +54,11 @@ using std::dec; /** the definition of the KNX arguments. */ static const struct argp_option g_knx_argp_options[] = { {nullptr, 0, nullptr, 0, "KNX options:", 1 }, - {"knxurl", O_URL, "URL", 0, "Connect to KNX daemon on URL (i.e. \"ip:host:[port]\" or \"local:/socketpath\") []", 0 }, + {"knxurl", O_URL, "URL", 0, "Connect to KNX daemon on URL (i.e. \"[multicast][@interface]\" for KNXnet/IP" +#ifdef HAVE_KNXD + " or \"ip:host[:port]\" / \"local:/socketpath\" for knxd" +#endif + ") []", 0 }, {"knxrage", O_AGR, "SEC", 0, "Maximum age in seconds for using the last value of read messages (0=disable) [5]", 0 }, {"knxwage", O_AGW, "SEC", 0, "Maximum age in seconds for using the last value for reads on write messages (0=disable), [99999999]", 0 }, {"knxint", O_INT, "FILE", 0, "Read KNX integration settings from FILE [/etc/ebusd/knx.cfg]", 0 }, @@ -149,8 +158,9 @@ bool knxhandler_register(UserInfo* userInfo, BusHandler* busHandler, MessageMap* KnxHandler::KnxHandler(UserInfo* userInfo, BusHandler* busHandler, MessageMap* messages) : DataSink(userInfo, "knx"), DataSource(busHandler), WaitThread(), m_messages(messages), - m_start(0), m_con(nullptr), m_lastUpdateCheckResult("."), + m_start(0), m_lastUpdateCheckResult("."), m_lastScanStatus(SCAN_STATUS_NONE), m_scanFinishReceived(false), m_lastErrorLogTime(0) { + m_con = KnxConnection::create(g_url); if (g_integrationFile != nullptr) { if (!m_replacers.parseFile(g_integrationFile)) { logOtherError("knx", "unable to open integration file %s", g_integrationFile); @@ -163,6 +173,22 @@ KnxHandler::KnxHandler(UserInfo* userInfo, BusHandler* busHandler, MessageMap* m delete g_integrationVars; g_integrationVars = nullptr; } + if (m_con->isProgrammable()) { + string addrStr = m_replacers.get("address", false); + knx_addr_t address = 0; + if (!addrStr.empty()) { + address = parseAddress(addrStr, false); + if (!address) { + logOtherError("knx", "invalid address: %s", addrStr.c_str()); + } + } + if (address) { + m_con->setAddress(address); + } else { + logOtherNotice("knx", "address not assigned yet, entering programming mode"); + m_con->setProgrammingMode(true); + } + } // parse all group to message field assignments vector keys = m_replacers.keys(); int messageCnt = 0, globalCnt = 0; @@ -176,38 +202,11 @@ KnxHandler::KnxHandler(UserInfo* userInfo, BusHandler* busHandler, MessageMap* m if (pos == string::npos) { continue; } - auto pos2 = val.find('/', pos+1); - result_t res = RESULT_OK; - unsigned int v; - v = parseInt(val.substr(0, pos).c_str(), 10, 0, 0x1f, &res); - if (res != RESULT_OK) { + auto dest = parseAddress(val); + if (!dest) { logOtherError("knx", "invalid assignment %s to %s", key.c_str(), val.c_str()); continue; } - auto dest = static_cast(v << 11); - if (pos2 == string::npos) { - // 2 level - v = parseInt(val.substr(pos+1).c_str(), 10, 0, 0x7ff, &res); - if (res != RESULT_OK) { - logOtherError("knx", "invalid 2-level assignment %s to %s", key.c_str(), val.c_str()); - continue; - } - dest |= static_cast(v); - } else { - // 3 level - v = parseInt(val.substr(pos+1, pos2).c_str(), 10, 0, 0x07, &res); - if (res != RESULT_OK) { - logOtherError("knx", "invalid 3-level assignment %s to %s", key.c_str(), val.c_str()); - continue; - } - dest |= static_cast(v << 8); - v = parseInt(val.substr(pos+1, pos2).c_str(), 10, 0, 0xff, &res); - if (res != RESULT_OK) { - logOtherError("knx", "invalid 3-level assignment %s to %s", key.c_str(), val.c_str()); - continue; - } - dest |= static_cast(v); - } if (key.substr(0, 7) != "global/") { messageCnt++; m_messageFieldGroupAddress[key] = dest; @@ -425,7 +424,7 @@ result_t KnxHandler::sendGroupValue(knx_addr_t dest, apci_t apci, dtlf_t& length } void KnxHandler::sendGlobalValue(global_t index, unsigned int value, bool response) { - if (!m_con || !m_con->isConnected() || !m_con->getAddress()) { + if (!m_con->isConnected() || !m_con->getAddress()) { return; } const auto vit = m_subscribedGlobals.find(index); @@ -447,6 +446,9 @@ result_t KnxHandler::receiveTelegram(int maxlen, knx_transfer_t* typ, uint8_t *b .tv_sec = 2, .tv_nsec = 0, }; + if (!m_con->isConnected()) { + return RESULT_ERR_GENERIC_IO; + } int fd = m_con->getPollFd(); #ifdef HAVE_PPOLL nfds_t nfds = 1; @@ -519,10 +521,34 @@ void printResponse(knx_addr_t src, knx_addr_t dest, int len, const uint8_t *data */ void KnxHandler::handleReceivedTelegram(knx_transfer_t typ, knx_addr_t src, knx_addr_t dest, int len, const uint8_t *data) { - if (typ != KNX_TRANSFER_GROUP) { - logOtherNotice("knx", "skipping non-group PDU %3.3x", typ); + if (typ == KNX_TRANSFER_GROUP) { + handleGroupTelegram(src, dest, len, data); return; } + if (m_con->isProgrammable() && src && m_con->getAddress()) { + handleNonGroupTelegram(typ, src, dest, len, data); + } +} + +void KnxHandler::sendNonGroupDisconnect(knx_addr_t dest) { + uint8_t buf[] = {0x00}; + if (m_con->sendTyp(KNX_TRANSFER_DISCONNECT, dest, 1, buf)) { + logOtherDebug("knx", "cannot send"); + } + m_lastConnectTime = 0; // state=closed + m_waitForAck = false; +} + +// the connection timeout in millis (6 seconds) +#define CONNECTION_TIMEOUT 6000 + +void KnxHandler::handleNonGroupTelegram(knx_transfer_t typ, knx_addr_t src, knx_addr_t dest, int len, const uint8_t *data) { + logOtherNotice("knx", "skipping non-group PDU %3.3x", typ); +} + +void KnxHandler::handleGroupTelegram(knx_addr_t src, knx_addr_t dest, int len, const uint8_t *data) { + time_t now; + time(&now); int apci = ((data[0]&0x03)<<8) | data[1]; int groupReadWriteApci = apci & APCI_GROUPVALUE_READ_WRITE_MASK; if (groupReadWriteApci == APCI_GROUPVALUE_WRITE || groupReadWriteApci == APCI_GROUPVALUE_READ) { @@ -530,6 +556,21 @@ void KnxHandler::handleReceivedTelegram(knx_transfer_t typ, knx_addr_t src, knx_ } bool isWrite = apci==APCI_GROUPVALUE_WRITE; if (apci!=APCI_GROUPVALUE_READ && !isWrite) { + if (m_con->isProgrammingMode()) { + if (apci == APCI_INDIVIDUALADDRESS_READ && m_lastIndividualAddressResponseTime>8, APCI_INDIVIDUALADDRESS_RESPONSE&0xff}; + logOtherNotice("knx", "answering to A_IndividualAddress_Read"); + if (m_con->sendGroup(0, 2, buf)) { + logOtherDebug("knx", "cannot send"); + } else { + m_lastIndividualAddressResponseTime = now; + } + } else if (apci==APCI_INDIVIDUALADDRESS_WRITE && len==4 && !m_con->getAddress() && (data[2]|data[3])) { + m_con->setAddress((data[2]<<8)|data[3]); + m_lastIndividualAddressResponseTime = 0; + logOtherNotice("knx", "received new address %x", m_con->getAddress()); + } + } return; // neither A_GroupValue_Read nor A_GroupValue_Write (A_GroupValue_Response not used at all) } const auto subKey = static_cast(dest | (isWrite ? FLAG_WRITE : FLAG_READ)); @@ -666,8 +707,6 @@ void KnxHandler::handleReceivedTelegram(knx_transfer_t typ, knx_addr_t src, knx_ } logOtherNotice("knx", "received read request from %4.4x to %4.4x for %s/%s/%s", src, dest, circuit.c_str(), name.c_str(), fieldName.c_str()); - time_t now; - time(&now); if (msg->isWrite() && !msg->isPassive()) { // reading last value of a write message if (now >= msg->getLastUpdateTime() + g_maxWriteAge) { logOtherInfo("knx", "unable to answer read request to %4.4x on write message", dest); @@ -698,26 +737,22 @@ void KnxHandler::run() { result_t result = RESULT_OK; time(&now); m_start = lastTaskRun = now; - uint8_t data[] = {0, 0, 0, 0, 0, 0, 0, 0}; + uint8_t data[256]; int len = 0; time_t definitionsSince = 0; while (isRunning()) { - bool wasConnected = m_con != nullptr && m_con->isConnected(); + bool wasConnected = m_con->isConnected(); bool needsWait = true; - if (!m_con) { - m_con = KnxConnection::create(); - const char* err = m_con->open(g_url); + if (!wasConnected) { + const char* err = m_con->open(); if (!err) { m_lastErrorLogTime = 0; - logOtherNotice("knx", "connected"); + logOtherNotice("knx", "connected to %s", m_con->getInfo()); sendGlobalValue(GLOBAL_VERSION, VERSION_INT); sendGlobalValue(GLOBAL_RUNNING, 1); } if (err) { - if (m_con) { - delete m_con; - m_con = nullptr; - } + m_con->close(); time(&now); if (now > m_lastErrorLogTime + 10) { // log at most every 10 seconds m_lastErrorLogTime = now; @@ -725,7 +760,7 @@ void KnxHandler::run() { } } } - bool reconnected = !wasConnected && m_con != nullptr; + bool reconnected = !wasConnected && m_con->isConnected(); time(&now); bool sendSignal = reconnected; if (now < m_start) { @@ -736,17 +771,17 @@ void KnxHandler::run() { lastTaskRun = now; } else if (now > lastTaskRun+(m_scanFinishReceived ? 1 : 15)) { m_scanFinishReceived = false; - if (m_con) { + if (m_con->isConnected()) { sendSignal = true; if (now > lastUptime + UPTIME_INTERVAL) { lastUptime = now; sendGlobalValue(GLOBAL_UPTIME, static_cast(now - m_start)); } } - if (m_con && definitionsSince == 0) { + if (m_con->isConnected() && definitionsSince == 0) { definitionsSince = 1; } - if (m_con) { + if (m_con->isConnected()) { deque messages; m_messages->findAll("", "", m_levels, false, true, true, true, true, true, 0, 0, true, &messages); int addCnt = 0; @@ -862,7 +897,13 @@ void KnxHandler::run() { } } } - if (m_con) { + if (m_con->isConnected()) { + if (reconnected) { + // reset the state machine + m_lastConnectTime = 0; + m_waitForAck = false; + } + handleReceivedTelegram(KNX_TRANSFER_NONE, 1, 0, 0, data); // check timeout knx_addr_t src, dest; knx_transfer_t typ; // APDU data starting with octet 6 according to spec, contains 2 bits of application layer @@ -871,8 +912,7 @@ void KnxHandler::run() { res = receiveTelegram(sizeof(data), &typ, data, &len, &src, &dest); if (res != RESULT_OK) { if (res == RESULT_ERR_GENERIC_IO) { - delete m_con; - m_con = nullptr; + m_con->close(); } } else { needsWait = false; @@ -882,7 +922,7 @@ void KnxHandler::run() { } if (!m_updatedMessages.empty()) { m_messages->lock(); - if (m_con) { + if (m_con->isConnected()) { for (auto it = m_updatedMessages.begin(); it != m_updatedMessages.end(); ) { const vector* messages = m_messages->getByKey(it->first); if (!messages) { @@ -925,7 +965,8 @@ void KnxHandler::run() { } m_messages->unlock(); } - if ((!m_con && !Wait(5)) || (needsWait && !Wait(1))) { + if ((!m_con->isConnected() && !Wait(5)) || (needsWait && !Wait(0, 100)) + ) { break; } } diff --git a/src/ebusd/knxhandler.h b/src/ebusd/knxhandler.h index 95932323..375d2799 100644 --- a/src/ebusd/knxhandler.h +++ b/src/ebusd/knxhandler.h @@ -60,9 +60,20 @@ bool knxhandler_register(UserInfo* userInfo, BusHandler* busHandler, MessageMap* /** type for KNX APCI values (application control field). */ enum apci_t { + // within KNX_TRANSFER_GROUP: APCI_GROUPVALUE_READ = 0x000, //!< A_GroupValue_Read-PDU - APCI_GROUPVALUE_RESPONSE = 0x040, //!< A_GroupValue_Response-PDU - APCI_GROUPVALUE_WRITE = 0x080, //!< A_GroupValue_Write-PDU + APCI_GROUPVALUE_RESPONSE = 0x040, //!< A_GroupValue_Response-PDU (mask APCI_GROUPVALUE_READ_WRITE_MASK) + APCI_GROUPVALUE_WRITE = 0x080, //!< A_GroupValue_Write-PDU (mask APCI_GROUPVALUE_READ_WRITE_MASK) + APCI_INDIVIDUALADDRESS_READ = 0x100, //!< A_IndividualAddress_Read-PDU + APCI_INDIVIDUALADDRESS_RESPONSE = 0x140, //!< A_IndividualAddress_Response-PDU + APCI_INDIVIDUALADDRESS_WRITE = 0x0c0, //!< A_IndividualAddress_Write-PDU + // within KNX_TRANSFER_CONNECTED: + APCI_DEVICEDESCRIPTOR_READ = 0x300, //!< A_DeviceDescriptor_Read-PDU + APCI_DEVICEDESCRIPTOR_RESPONSE = 0x340, //!< A_DeviceDescriptor_Read-PDU (mask should be 0x3c0) + APCI_PROPERTYVALUE_READ = 0x3d5, //!< A_PropertyValue_Read-PDU + APCI_PROPERTYVALUE_RESPONSE = 0x3d6, //!< A_PropertyValue_Response-PDU + APCI_PROPERTYVALUE_WRITE = 0x3d7, //!< A_PropertyValue_Write-PDU + APCI_RESTART = 0x380, //!< A_Restart-PDU }; #define APCI_GROUPVALUE_READ_WRITE_MASK 0x3c0 @@ -168,11 +179,11 @@ class KnxHandler : public DataSink, public DataSource, public WaitThread { /** * Handle a received KNX telegram. - * @param typ the poll data type. + * @param typ the transfer data type. * @param src the source address. * @param dest the destination group address. - * @param len the telegram length (starting with ovctet 6). - * @param data the telegram data buffer. + * @param len the data length (including the TPCI/APCI octet 6, i.e. transport control field). + * @param data the data buffer (starting with the TPCI/APCI octet 6, i.e. transport control field). */ void handleReceivedTelegram(knx_transfer_t typ, knx_addr_t src, knx_addr_t dest, int len, const uint8_t *data); @@ -180,6 +191,30 @@ class KnxHandler : public DataSink, public DataSource, public WaitThread { // @copydoc void run() override; + /** + * Handle a received non-group telegram when the device has an individual address and is programmable. + * @param typ the transfer data type. + * @param src the source address (ensured to be non-zero). + * @param dest the destination address (group or individual according to address type encoded in the transfer data type). + * @param len the data length (including the TPCI/APCI octet 6, i.e. transport control field). + * @param data the data buffer (starting with the TPCI/APCI octet 6, i.e. transport control field). + */ + void handleNonGroupTelegram(knx_transfer_t typ, knx_addr_t src, knx_addr_t dest, int len, const uint8_t *data); + + /** + * Send a DISCONNECT to the destination and reset the connected state. + * @param dest the destination individual address to send to. + */ + void sendNonGroupDisconnect(knx_addr_t dest); + + /** + * Handle a received group telegram. + * @param src the source address. + * @param dest the destination group address. + * @param len the data length (including the TPCI/APCI octet 6, i.e. transport control field). + * @param data the data buffer (starting with the TPCI/APCI octet 6, i.e. transport control field). + */ + void handleGroupTelegram(knx_addr_t src, knx_addr_t dest, int len, const uint8_t *data); private: /** the @a MessageMap instance. */ @@ -210,9 +245,27 @@ class KnxHandler : public DataSink, public DataSource, public WaitThread { /** the time the run thread was entered. */ time_t m_start; - /** the knx connection if initialized, or nullptr. */ + /** the knx connection as long as initialized, or nullptr. */ KnxConnection* m_con; + /** the time of the last sent individual address response, or 0. */ + time_t m_lastIndividualAddressResponseTime = 0; + + /** the time of the last connection, or 0 if not connected. */ + long long m_lastConnectTime = 0; + + /** the source address of the last connection, or 0. */ + knx_addr_t m_lastConnectSource = 0; + + /** the SeqNo for reception of the last connection. */ + uint8_t m_lastConnectRecvSeq = 0; + + /** the SeqNo for sending of the last connection. */ + uint8_t m_lastConnectSendSeq = 0; + + /** true when last connection is in state OPEN_WAIT. */ + bool m_waitForAck = false; + /** the last update check result. */ string m_lastUpdateCheckResult; @@ -223,7 +276,7 @@ class KnxHandler : public DataSink, public DataSource, public WaitThread { bool m_scanFinishReceived; /** the last system time when a communication error was logged. */ - time_t m_lastErrorLogTime; + long long m_lastErrorLogTime; }; } // namespace ebusd diff --git a/src/lib/knx/knx.cpp b/src/lib/knx/knx.cpp index 72615759..8988caed 100644 --- a/src/lib/knx/knx.cpp +++ b/src/lib/knx/knx.cpp @@ -24,14 +24,78 @@ #ifdef HAVE_KNXD #include "lib/knx/knxd.h" -#else +#endif #include "lib/knx/knxnet.h" -#endif -KnxConnection* KnxConnection::create() { -#ifdef HAVE_KNXD - return new KnxdConnection(); -#else - return new KnxNetConnection(); -#endif +#include + +namespace ebusd { + +unsigned int parseInt(const char* str, int base, unsigned int minValue, unsigned int maxValue, + bool* error) { + char* strEnd = nullptr; + + unsigned long ret = strtoul(str, &strEnd, base); + + if (strEnd == nullptr || strEnd == str || *strEnd != 0) { + *error = true; // invalid value + return 0; + } + + if (minValue > ret || ret > maxValue) { + *error = true; // invalid value + return 0; + } + return (unsigned int)ret; } + +knx_addr_t parseAddress(const string &str, bool isGroup, bool* error) { + auto sep = isGroup ? '/' : '.'; + auto pos = str.find(sep); + if (pos != string::npos) { + auto pos2 = str.find(sep, pos+1); + bool err = false; + unsigned int v = 0; + v = parseInt(str.substr(0, pos).c_str(), 10, 0, isGroup ? 0x1f : 0x0f, &err); + if (!err) { + auto dest = static_cast(v << (isGroup ? 11 : 12)); + if (pos2 == string::npos) { + // 2 level + if (isGroup) { + v = parseInt(str.substr(pos+1).c_str(), 10, 0, 0x7ff, &err); + if (!err) { + dest |= static_cast(v); + return dest; + } + } + } else { + // 3 level + v = parseInt(str.substr(pos+1, pos2-pos-1).c_str(), 10, 0, isGroup ? 0x07 : 0x0f, &err); + if (!err) { + dest |= static_cast(v << 8); + v = parseInt(str.substr(pos2+1).c_str(), 10, 0, 0xff, &err); + if (!err) { + dest |= static_cast(v); + return dest; + } + } + } + } + } + if (error) { + *error = true; + } + return 0; +} + +// copydoc +KnxConnection *KnxConnection::create(const char *url) { +#ifdef HAVE_KNXD + if (strchr(url, ':')) { + return new KnxdConnection(url); + } +#endif + return new KnxNetConnection(url); +} + +} // namespace ebusd diff --git a/src/lib/knx/knx.h b/src/lib/knx/knx.h index cded4758..8b44a48b 100644 --- a/src/lib/knx/knx.h +++ b/src/lib/knx/knx.h @@ -19,25 +19,55 @@ #ifndef LIB_KNX_KNX_H_ #define LIB_KNX_KNX_H_ +#include #include -// base KNX address type (group or individual) +namespace ebusd { + +/** @file lib/knx/knx.h + * Classes, functions, and constants related to KNX. + */ + +using std::string; + + +/** base KNX address type (group or individual). */ typedef uint16_t knx_addr_t; -// the transfer types (lower 8 bits of transport control field with sequence=0, plus bit 8 with address type) +/** special default address value. */ +#define DEFAULT_ADDRESS 0xffff + +/** the transfer types (lower 8 bits of transport control field with sequence=0, plus bit 8 with address type). */ enum knx_transfer_t { - KNX_TRANSFER_NONE = -1, // no transfer available - KNX_TRANSFER_GROUP = 0x100, // data group or broadcast PDU - KNX_TRANSFER_TAG_GROUP = 0x104, // data tag group PDU - KNX_TRANSFER_INDIVIDUAL = 0x000, // data individual PDU - KNX_TRANSFER_CONNECTED = 0x040, // data connected PDU - KNX_TRANSFER_CONNECT = 0x080, // connect PDU - KNX_TRANSFER_DISCONNECT = 0x081, // disconnect PDU - KNX_TRANSFER_ACK = 0x0c2, // ACK PDU - KNX_TRANSFER_NAK = 0x0c3, // NAK PDU + // no transfer available + KNX_TRANSFER_NONE = -1, + // data group or broadcast PDU + KNX_TRANSFER_GROUP = 0x100, + // data tag group PDU + KNX_TRANSFER_TAG_GROUP = 0x104, + // data individual PDU + KNX_TRANSFER_INDIVIDUAL = 0x000, + // data connected PDU + KNX_TRANSFER_CONNECTED = 0x040, + // connect PDU + KNX_TRANSFER_CONNECT = 0x080, + // disconnect PDU + KNX_TRANSFER_DISCONNECT = 0x081, + // ACK PDU + KNX_TRANSFER_ACK = 0x0c2, + // NAK PDU + KNX_TRANSFER_NAK = 0x0c3, }; +/** + * Parse a group address in the form "A/B/C" or "A/B" or an individual address in the form "A.B.C". + * @param str the group address string to parse. + * @param error optional variable to set to true in case of an invalid address string. + * @return the parsed address, or 0 on error. + */ +knx_addr_t parseAddress(const string &str, bool isGroup = true, bool* error = nullptr); + /** * An abstract KNX connection. */ @@ -55,17 +85,22 @@ class KnxConnection { /** * Create a new KnxConnection. - * @param url the URL to connect to. + * @param url the URL to connect to in the form "[multicast][@interface]" (for KNXnet/IP) or "ip:host[:port]" / + * "local:/socketpath" for knxd (if compiled in). * @return the new KnxConnection, or @a nullptr on error. */ - static KnxConnection* create(); + static KnxConnection* create(const char* url); + + /** + * @return additional infos about this connection for logging. + */ + virtual const char* getInfo() const = 0; /** * Open a connection to the specified URL. - * @param url the URL to connect to. * @return nullptr on success, or an error message. */ - virtual const char* open(const char* url) = 0; + virtual const char* open() = 0; /** * @return true if connected, false otherwise. @@ -89,7 +124,7 @@ class KnxConnection { * @param len pointer to store the actual data length to. * @param src optional pointer to store the source address (if any, depending on poll data type). * @param dst optional pointer to store the destination address (if any, depending on poll data type). - * @return the polled transfer data type. + * @return the polled transfer type. */ virtual knx_transfer_t getPollData(int size, uint8_t* data, int* len, knx_addr_t* src, knx_addr_t* dst) = 0; @@ -102,15 +137,25 @@ class KnxConnection { */ virtual const char* sendGroup(knx_addr_t dst, int len, const uint8_t* data) = 0; + /** + * Send a non-group APDU. + * @param typ the transfer type to send. + * @param dst the destination address. + * @param len the APDU length. + * @param data the APDU data buffer. + * @return nullptr on success, or an error message. + */ + virtual const char* sendTyp(knx_transfer_t typ, knx_addr_t dst, int len, const uint8_t* data) = 0; + /** * @return true if connection allows programming via ETS. */ - virtual bool isProgrammable() { return false; }; + virtual bool isProgrammable() const { return false; }; /** * @return the individual address, or 0 if not programmed yet, or any non-zero value if not programmable. */ - virtual knx_addr_t getAddress() { return 0xffff; }; + virtual knx_addr_t getAddress() { return DEFAULT_ADDRESS; }; /** * @param address the individual address to set. @@ -118,6 +163,24 @@ class KnxConnection { virtual void setAddress(knx_addr_t address) { // default implementation does nothing } + + /** + * Get the programming mode. + * @return true when in programming mode, false if not. + */ + virtual bool isProgrammingMode() { + return false; + } + + /** + * Set the programming mode. + * @param on true to start programming mode, false to stop it. + */ + virtual void setProgrammingMode(bool on) { + // default implementation does nothing + } }; +} // namespace ebusd + #endif // LIB_KNX_KNX_H_ \ No newline at end of file diff --git a/src/lib/knx/knxd.h b/src/lib/knx/knxd.h index 729ddc02..547d3212 100644 --- a/src/lib/knx/knxd.h +++ b/src/lib/knx/knxd.h @@ -22,13 +22,20 @@ #include #include "lib/knx/knx.h" +namespace ebusd { + +/** + * A KnxConnection based on libeibclient using the group communication interface of the connected KNXd. + * Unfortunately, this does not allow acting as a KNX device, i.e. enter programming mode and make individual address + * and group association table writable from ETS. As such, an KNXnet/IP implementation is available as well. + */ class KnxdConnection : public KnxConnection { public: /** * Construct a new instance. */ - KnxdConnection() - : KnxConnection(), m_con(nullptr) {} + KnxdConnection(const char *url) + : KnxConnection(), m_url(url), m_con(nullptr) {} /** * Destructor. @@ -38,9 +45,14 @@ class KnxdConnection : public KnxConnection { } // @copydoc - const char* open(const char* url) override { + const char* getInfo() const override { + return "KNXd"; + } + + // @copydoc + const char* open() override { close(); - m_con = EIBSocketURL(url); + m_con = EIBSocketURL(m_url); if (!m_con) { return "open error"; } @@ -94,9 +106,19 @@ class KnxdConnection : public KnxConnection { return nullptr; } + // @copydoc + const char* sendTyp(knx_transfer_t typ, knx_addr_t dst, int len, const uint8_t* data) override { + return "not available"; + } + private: + /** the URL to connect to. */ + const char* m_url; + /** the knx structure if connected, or nullptr. */ EIBConnection* m_con; }; +} // namespace ebusd + #endif // LIB_KNX_KNXD_H_ \ No newline at end of file diff --git a/src/lib/knx/knxnet.h b/src/lib/knx/knxnet.h new file mode 100644 index 00000000..4c27edfd --- /dev/null +++ b/src/lib/knx/knxnet.h @@ -0,0 +1,728 @@ +/* + * ebusd - daemon for communication with eBUS heating systems. + * Copyright (C) 2022 John Baier + * + * 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 . + */ + +#ifndef LIB_KNX_KNXNET_H_ +#define LIB_KNX_KNXNET_H_ + +#include +#include +#include +#include +#include +#ifndef __CYGWIN__ +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include "lib/knx/knx.h" + +namespace ebusd { + +using std::string; + +// 16 bit unsigned big endian +typedef union __attribute__ ((packed)) { + uint16_t raw; + struct { + uint8_t high; + uint8_t low; + }; +} uint16be_t; + +// 32 bit unsigned big endian +typedef union __attribute__ ((packed)) { + uint32_t raw; + struct { + uint8_t msb1; + uint8_t msb2; + uint8_t msb3; + uint8_t lsb; + }; +} uint32be_t; + +// KNXnet/IP header +typedef struct __attribute__ ((packed)) { + uint8_t headerLength; // =6 + uint8_t protocolVersion; // =0x10 + uint16be_t serviceTypeIdentifier; + uint16be_t totalLength; // complete length including header +} knxnet_header_t; + +/** service types. */ +typedef enum { + SERVICE_TYPE_SEARCH_REQ = 0x0201, + SERVICE_TYPE_SEARCH_RES = 0x0202, + SERVICE_TYPE_DESC_REQ = 0x0203, + SERVICE_TYPE_DESC_RES = 0x0204, +// SERVICE_TYPE_CONN_REQ = 0x0205, +// SERVICE_TYPE_CONN_RES = 0x0206, +// SERVICE_TYPE_CONNSTATE_REQ = 0x0207, +// SERVICE_TYPE_CONNSTATE_RES = 0x0208, +// SERVICE_TYPE_DISCONN_REQ = 0x0209, +// SERVICE_TYPE_DISCONN_RES = 0x020A, +// SERVICE_TYPE_DEVICE_CFG_REQ = 0x0310, +// SERVICE_TYPE_DEVICE_CFG_ACK = 0x0311, +// SERVICE_TYPE_TUNNEL_REQ = 0x0420, +// SERVICE_TYPE_TUNNEL_ACK = 0x0421, + SERVICE_TYPE_ROUTE_IND = 0x0530, + SERVICE_TYPE_ROUTE_LOST = 0x0531, + SERVICE_TYPE_ROUTE_BUSY = 0x0532, +} knxnet_service_type_t; + +// cEMI frame header (external message interface) +typedef struct __attribute__ ((packed)) { + uint8_t messageCode; + uint8_t additionalInfoLength; // optional immediately following additional bytes, usually =0. fixed to 0 in cEMI management messages +} knxnet_cemi_header_t; + +/* cEMI message codes. */ +typedef enum { + // MESSAGE_CODE_BUSMON_IND = 0x2B, + MESSAGE_CODE_DATA_REQ = 0x11, + MESSAGE_CODE_DATA_CON = 0x2E, + MESSAGE_CODE_DATA_IND = 0x29, +// MESSAGE_CODE_RAW_REQ = 0x10, +// MESSAGE_CODE_RAW_CON = 0x2D, +// MESSAGE_CODE_RAW_IND = 0x2F, + // MESSAGE_CODE_POLLDATA_REQ = 0x13, + // MESSAGE_CODE_POLLDATA_CON = 0x25, + // MESSAGE_CODE_DATACONN_REQ = 0x41, + // MESSAGE_CODE_DATACONN_IND = 0x89, + // MESSAGE_CODE_DATAIND_REQ = 0x4A, + // MESSAGE_CODE_DATAIND_IND = 0x94, + // MESSAGE_CODE_PROPREAD_REQ = 0xFC, + // MESSAGE_CODE_PROPREAD_CON = 0xFB, + // MESSAGE_CODE_PROPWRITE_REQ = 0xF6, + // MESSAGE_CODE_PROPWRITE_CON = 0xF5, + // MESSAGE_CODE_PROPINFO_IND = 0xF7, + // MESSAGE_CODE_FUNCPROPCMD_REQ = 0xF8, + // MESSAGE_CODE_FUNCPROPSTATEREAD_REQ = 0xF9, + // MESSAGE_CODE_FUNCPROP_CON = 0xFA, +// MESSAGE_CODE_RESET_IND = 0xF0, +// MESSAGE_CODE_RESET_REQ = 0xF1, +} knxnet_message_code_t; + +// L_Data services header +typedef struct __attribute__ ((packed)) { + union { + uint8_t raw; + struct { +#if __BYTE_ORDER == __BIG_ENDIAN + bool frameType: 1; // 0=extended, 1=standard + bool reserved: 1; + bool repeat: 1; // 0=repeat, 1=do not repeat + bool systemBroadcast: 1; // 0=system broadcast, 1=broadcast + uint8_t priority: 2; // 0=system, 1=normal, 2=urgent, 3=low + bool acknowledgeRequest: 1; // 1=ack requested + bool confirm: 1; // 0=no error, 1=error +#else + bool confirm: 1; // 0=no error, 1=error + bool acknowledgeRequest: 1; // 1=ack requested + uint8_t priority: 2; // 0=system, 1=normal, 2=urgent, 3=low + bool systemBroadcast: 1; // 0=system broadcast, 1=broadcast + bool repeat: 1; // 0=repeat, 1=do not repeat + bool reserved: 1; + bool frameType: 1; // 0=extended, 1=standard +#endif + }; + } controlField1; + union { + uint8_t raw; + struct { +#if __BYTE_ORDER == __BIG_ENDIAN + bool addressType: 1; // 0=individual, 1=group + uint8_t hopCount: 3; + uint8_t extendedFrameFormat: 4; // 0=standard frame, 0xf=escape +#else + uint8_t extendedFrameFormat: 4; // 0=standard frame, 0xf=escape + uint8_t hopCount: 3; + bool addressType: 1; // 0=individual, 1=group +#endif + }; + } controlField2; + uint16be_t sourceAddress; + uint16be_t destinationAddress; + uint8_t informationLength; // number of NPDU octets (not including the TPCI/APCI octet) +} knxnet_l_data_header_t; + + +typedef union __attribute__ ((packed)) { + uint8_t raw; + struct { +#if __BYTE_ORDER == __BIG_ENDIAN + bool controlFlag: 1; // 0=data, 1=control + bool numbered: 1; // 1=has sequence, 0=no sequence + uint8_t sequence: 4; // optional sequence number + uint8_t apci: 2; // highest 2 bits of APCI +#else + uint8_t apci: 2; // highest 2 bits of APCI + uint8_t sequence: 4; // optional sequence number + bool numbered: 1; // 1=has sequence, 0=no sequence + bool controlFlag: 1; // 0=data, 1=control +#endif + }; +} knxnet_tpci_apci_t; + +typedef struct __attribute__ ((packed)) { + uint8_t length; + uint8_t protocolCode; // 0x01=UDP over IPv4 + uint32be_t ipAddressV4; + uint16be_t port; +} knxnet_hpai_t; + +#define PROTOCOL_CODE_IPV4_UDP 0x01 + +typedef struct __attribute__ ((packed)) { + uint8_t length; + uint8_t descriptionCode; // 0x01=device info + uint8_t medium; // 0x20=IP + uint8_t status; // bit 0=programming mode + uint16be_t individualAddress; + uint16be_t projInstId; + uint8_t serial[6]; + in_addr_t multicastAddress; + uint8_t macAddress[6]; + unsigned char name[30]; +} knxnet_dib_devinfo_t; + +typedef struct __attribute__ ((packed)) { + uint8_t length; + uint8_t descriptionCode; // 0x02=services + struct { + uint8_t familyId; + uint8_t familyVersion; + }; // just one for now +} knxnet_dib_services_t; + +// the default system port +#define SYSTEM_MULTICAST_PORT 3671 + +// the default system multicast address 224.0.23.12 +#define SYSTEM_MULTICAST_IP 0xe000170c + +#define LAST_FRAME_TIMEOUT 2 + +class LastFrame { + friend class LastFrames; + + public: + void set(uint8_t* data, size_t len, size_t lOffset, time_t now) { + if (len>=sizeof(m_data)) { + return; + } + memcpy(m_data, data, len); + m_len = len; + m_lOffset = lOffset; + m_time = now; + } + + bool isValid(time_t now) { + return m_len && m_time>=now-LAST_FRAME_TIMEOUT; + } + + bool isSameAs(uint8_t* data, size_t len, size_t lOffset, time_t now, bool isSend = false) { + if (!m_len || len != m_len || lOffset != m_lOffset) { + return false; + } + if (memcmp(data, m_data, len) == 0) { + m_time = now; + return true; + } + int oldHopCount = (m_data[lOffset+1]&0x70)>>4; + int newHopCount = (data[lOffset+1]&0x70)>>4; + if (newHopCount < 6 // top hop count is always tolerated TODO bad idea? + && memcmp(data, m_data, lOffset+1) == 0 // including first byte of l_data header + && (data[lOffset+1]&~0x70)==(m_data[lOffset+1]&~0x70) // ignore hop count + && (isSend ? newHopCount<=oldHopCount : newHopCountcontrolField2.addressType; + PRINTF("%s msgcode=%2.2x, %d.%d.%d > %d%c%d%c%d, repeat=%s, ack=%s, hopcnt=%d, prio=%s, frame=%s, %sbroad, confirm=%s, tpci/apci=%2.2x", + sent ? "send" : "recv", + c->messageCode, + l->sourceAddress.high>>4, + l->sourceAddress.high&0xf, + l->sourceAddress.low, + isGrp ? l->destinationAddress.high>>3 : l->destinationAddress.high>>4, + isGrp ? '/' : '.', + isGrp ? l->destinationAddress.high&0x1f : l->destinationAddress.high&0xf, + isGrp ? '/' : '.', + l->destinationAddress.low, + l->controlField1.repeat ? "yes" : "no", + l->controlField1.acknowledgeRequest ? "yes" : "no", + l->controlField2.hopCount, + l->controlField1.priority==1 ? "normal" : l->controlField1.priority==2 ? "urgent" : l->controlField1.priority==3 ? "low" : "system", + l->controlField1.frameType ? "std" : "ext", + l->controlField1.systemBroadcast ? "" : "sys ", + l->controlField1.confirm ? "error" : "no err", + d[0] + ); + if (d) { + PRINTF(", data="); + for (int i=0; iinformationLength; i++) { + PRINTF("%2.2x ", d[1+i]); + } + } + PRINTF("\n"); +} + + +/** + * A KnxConnection based on IP multicast as alternative to using libeibclient. + * This is still an incomplete KNXnet/IP implementation. + */ +class KnxNetConnection : public KnxConnection { + public: + /** + * Construct a new instance. + */ + KnxNetConnection(const char* url) + : KnxConnection(), m_url(url), m_sock(0), m_programmingMode(false), m_addr(0) {} + + /** + * Destructor. + */ + virtual ~KnxNetConnection() { + close(); + } + + // @copydoc + const char* getInfo() const override { + return "KNXnet/IP multicast"; + } + + // @copydoc + const char* open() override { + close(); + int ret; + struct in_addr mcast = {}; + mcast.s_addr = htonl(SYSTEM_MULTICAST_IP); + m_interface.s_addr = INADDR_ANY; + m_port = SYSTEM_MULTICAST_PORT; + if (m_url && m_url[0]) { // non-empty + string urlStr = m_url; // "[mcast][@intf]" for non-default 224.0.23.12:3671) + if (!urlStr.empty()) { + auto pos = urlStr.find('@'); + if (pos != string::npos) { + string intfStr = urlStr.substr(pos+1); + const char* intfCstr = intfStr.c_str(); + ret = inet_aton(intfCstr, &m_interface); + if (ret == 0) { + return "intf addr"; + } + urlStr = urlStr.substr(0, pos); + } + } + if (!urlStr.empty()) { + const char *mcastStr = urlStr.c_str(); + ret = inet_aton(mcastStr, &mcast); + if (ret == 0) { + return "multicast addr"; + } + } + } + + sockaddr_in address = {}; + address.sin_family = AF_INET; + address.sin_port = htons(m_port); + address.sin_addr.s_addr = INADDR_ANY; + + int fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (fd < 0) { + return "create socket"; + } + + // set non-blocking + ret = fcntl(fd, F_SETFL, O_NONBLOCK); + if (ret != 0) { + ::close(fd); + return "non-blocking"; + } + + // set reuse address option + int optint = 1; + ret = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &optint, sizeof(optint)); + if (ret != 0) { + ::close(fd); + return "reuse"; + } + + // allow multiple processes using the same port for multicast on the same host + unsigned char optchar = 1; + ret = setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, &optchar, sizeof(optchar)); + if (ret != 0) { + ::close(fd); + return "mcast loop"; + } + + if (m_interface.s_addr != INADDR_ANY) { + // set outgoing interface to other than default (determined by routing table) + ret = setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, &m_interface, sizeof(m_interface)); + if (ret != 0) { + ::close(fd); + return "mcast intf"; + } + } + + // bind for incoming multicast + ret = bind(fd, (struct sockaddr*) &address, sizeof(address)); + if (ret != 0) { + ::close(fd); + return "bind socket"; + } + + // set the target address for later use by sendto() + m_multicast = address; + m_multicast.sin_addr = mcast; + + // join the multicast inbound + ip_mreq req = {}; + req.imr_multiaddr = mcast; + req.imr_interface = m_interface; + if (setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &req, sizeof(req)) < 0) { + ::close(fd); + return "join multicast"; + } + + m_sock = fd; + return nullptr; + } + + // @copydoc + bool isConnected() const override { + return m_sock != 0; + } + + // @copydoc + void close() override { + if (m_sock) { + ::close(m_sock); + m_sock = 0; + } + } + + // @copydoc + int getPollFd() const override { + return m_sock; + } + + // @copydoc + knx_transfer_t getPollData(int size, uint8_t* data, int* recvlen, knx_addr_t* src, knx_addr_t* dst) override { + uint8_t buf[128]; + ssize_t len = recv(m_sock, buf, sizeof(buf), 0); + if (len < sizeof(knxnet_header_t)) { + PRINTF("#skip recv short hdr len=%d\n", len); + return KNX_TRANSFER_NONE; + } + auto h = (knxnet_header_t*)buf; + if (h->headerLength != sizeof(knxnet_header_t) || h->protocolVersion != 0x10) { + PRINTF("#skip recv short/proto len=%d\n", len); + return KNX_TRANSFER_NONE; + } + switch (htons(h->serviceTypeIdentifier.raw)) { + case SERVICE_TYPE_ROUTE_IND: + // expected value + break; +// case SERVICE_TYPE_SEARCH_REQ: +// return KNX_TRANSFER_NONE; +// case SERVICE_TYPE_DESC_REQ: +// return KNX_TRANSFER_NONE; + default: + PRINTF("#skip recv service=%4.4x\n", htons(h->serviceTypeIdentifier.raw)); + return KNX_TRANSFER_NONE; + } + // routing indication + size_t totalLen = htons(h->totalLength.raw); + if (len < totalLen || len < sizeof(knxnet_header_t)+sizeof(knxnet_cemi_header_t)) { + PRINTF("#skip recv short cemi len=%d\n", len); + return KNX_TRANSFER_NONE; + } + auto c = (knxnet_cemi_header_t*)(((uint8_t*)h)+sizeof(knxnet_header_t)); + if (c->messageCode != MESSAGE_CODE_DATA_IND) { + PRINTF("#skip recv msgcode=%2.2x\n", c->messageCode); + return KNX_TRANSFER_NONE; + } + auto lOffset = sizeof(knxnet_header_t)+sizeof(knxnet_cemi_header_t)+c->additionalInfoLength; + ssize_t dataLen = totalLen - (lOffset+sizeof(knxnet_l_data_header_t)); + if (dataLen < 0) { + PRINTF("#skip recv short data len=%d\n", len); + return KNX_TRANSFER_NONE; + } + auto l = (knxnet_l_data_header_t*)(((uint8_t*)h)+lOffset); + auto d = ((uint8_t*)l)+sizeof(knxnet_l_data_header_t); + if (!l->controlField1.frameType || !l->controlField1.systemBroadcast) { + // not a regular standard frame broadcast + PRINTF("#skip recv irregular frame len=%d\n", len); + return KNX_TRANSFER_NONE; + } + if (m_addr && (!l->controlField2.addressType && htons(l->destinationAddress.raw) != m_addr)) { + // ignore packets with individual addr destination other than our own + PRINTF("#skip recv not-own dest len=%d\n", len); + return KNX_TRANSFER_NONE; + } + if (m_addr && !l->controlField2.addressType && htons(l->sourceAddress.raw) == m_addr) { + // ignore own source packets + PRINTF("#skip recv own src len=%d\n", len); + return KNX_TRANSFER_NONE; + } + if (dataLen < 0 || dataLen < l->informationLength) { + PRINTF("#skip recv short payload len=%d\n", len); + return KNX_TRANSFER_NONE; + } + // check repeated frames + time_t now; + time(&now); + // PRINTF("getPoll len=%d, last sent len=%d\n", len, m_lastSentLen); + if (m_lastRecvFrames.isRepetition(buf, totalLen, lOffset, now)) { + // last recv packet repeated + PRINTF("#skip recv last recv len=%d\n", totalLen); + return KNX_TRANSFER_NONE; + } + if (m_lastSentFrames.isRepetition(buf, totalLen, lOffset, now, true)) { + // last sent packet re-received + PRINTF("#skip recv last sent len=%d\n", totalLen); + return KNX_TRANSFER_NONE; + } + logTelegram(false, c, l, d); + m_lastRecvFrames.add(buf, totalLen, lOffset, now); + // all fine + int ret = d[0]; + if (l->controlField2.addressType) { + ret |= 0x100; // address type group + } + if (!(ret&0x80)) { + ret &= ~0x03; // remove two apci bits + } + if (ret&0x40) { + ret &= ~0x3c; // remove sequence number + } + *recvlen = size > dataLen ? dataLen : size; + memcpy(data, d, *recvlen); // including the TPCI/APCI octet 6 + if (src) { + *src = htons(l->sourceAddress.raw); + } + if (dst) { + *dst = htons(l->destinationAddress.raw); + } + return (knx_transfer_t)ret; + } + + // @copydoc + const char* sendGroup(knx_addr_t dst, int len, const uint8_t* data) override { + return send(KNX_TRANSFER_GROUP, dst, len, data); + } + + // @copydoc + const char* sendTyp(knx_transfer_t typ, knx_addr_t dst, int len, const uint8_t* data) override { + return send(typ, dst, len, data); + } + + // @copydoc + bool isProgrammable() const override { return true; }; + + private: + /** + * Send a message. + * @param typ the transfer type to send. + * @param dst the destination address. + * @param len the APDU length. + * @param data the APDU data buffer. + * @return nullptr on success, or an error message. + */ + const char* send(knx_transfer_t typ, knx_addr_t dst, int len, const uint8_t* data) { + uint8_t buf[128]; + auto h = (knxnet_header_t*)buf; + h->headerLength = sizeof(knxnet_header_t); + h->protocolVersion = 0x10; + h->serviceTypeIdentifier.raw = htons(SERVICE_TYPE_ROUTE_IND); + // first byte of data is expected to hold the APCI upper byte: + size_t totalLen = sizeof(knxnet_header_t)+sizeof(knxnet_cemi_header_t)+sizeof(knxnet_l_data_header_t)+len; + h->totalLength.raw = htons(totalLen); + auto c = (knxnet_cemi_header_t*)(buf+sizeof(knxnet_header_t)); + c->messageCode = MESSAGE_CODE_DATA_IND; + c->additionalInfoLength = 0; + auto lOffset = sizeof(knxnet_header_t)+sizeof(knxnet_cemi_header_t)+c->additionalInfoLength; + auto l = (knxnet_l_data_header_t*)(((uint8_t*)h)+lOffset); + l->controlField1.raw = 0xbc; // standard frame, no repeat, broadcast, low prio, no ack, no err + l->controlField2.raw = 0xe0; // group address, hop count 6, standard frame + l->controlField2.addressType = (typ&0x100)!=0; + l->sourceAddress.raw = htons(m_addr); + l->destinationAddress.raw = htons(dst); + if (typ&0x100) { + // ensure at least default individual address + if (!m_addr) { + l->sourceAddress.raw = 0xffff; // for "unregistered device" in S-Mode + } + } + l->informationLength = len-1; // subtracting the TPCI/APCI + uint8_t* d = buf+sizeof(knxnet_header_t)+sizeof(knxnet_cemi_header_t)+sizeof(knxnet_l_data_header_t); + // first byte of data is expected to hold the APCI upper byte, copy remainder: + memcpy(d, data, len); + int tpci = typ&0xff; // TPCI/APCI + if ((typ&0x080)==0) { + tpci |= (d[0]&0x03); // highest 2 bits of APCI + } + if (typ&0x040) { + tpci |= d[0]&((0x0f)<<2); // SeqNo + } + d[0] = tpci; + logTelegram(true, c, l, d); + ssize_t sent = sendto(m_sock, buf, totalLen, MSG_NOSIGNAL, (sockaddr*)&m_multicast, sizeof(m_multicast)); + if (sent < 0) { + return "send error"; + } + time_t now; + time(&now); + m_lastSentFrames.add(buf, totalLen, lOffset, now); + return nullptr; + } + + // copydoc + knx_addr_t getAddress() override { + return m_addr; + } + + // copydoc + void setAddress(knx_addr_t address) override { + m_addr = address; + // flush duplication check buffers + m_lastRecvFrames.reset(); + m_lastSentFrames.reset(); + } + + // copydoc + bool isProgrammingMode() override { + return m_programmingMode; + } + + // copydoc + void setProgrammingMode(bool on) override { + m_programmingMode = on; + } + + private: + /** the URL to connect to. */ + const char* m_url; + + /** the multicast address to join. */ + struct sockaddr_in m_multicast; + + /** the port to listen to. */ + in_port_t m_port; + + /** the optional interface address to bind to. */ + struct in_addr m_interface; + + /** the socket if connected, or 0. */ + int m_sock; + + /** true while in programming mode. */ + bool m_programmingMode; + + /** the own address, or 0 if not yet set. */ + knx_addr_t m_addr; + + /** the last received frames. */ + LastFrames m_lastRecvFrames; + + /** the last sent frames. */ + LastFrames m_lastSentFrames; +}; + +} // namespace ebusd + +#endif // LIB_KNX_KNXNET_H_