more abstraction, add knxnet/ip implementation and allow knxd in same binary as alternative to knxnet, start programming mode, docs

This commit is contained in:
John
2022-09-10 14:18:16 +02:00
parent cf6418c0ba
commit 2b07439794
6 changed files with 1063 additions and 92 deletions
+96 -55
View File
@@ -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<string> 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<knx_addr_t>(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<knx_addr_t>(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<knx_addr_t>(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<knx_addr_t>(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<now-3) { // timeout 3 seconds
uint8_t buf[] = {APCI_INDIVIDUALADDRESS_RESPONSE>>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<uint32_t>(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<unsigned int>(now - m_start));
}
}
if (m_con && definitionsSince == 0) {
if (m_con->isConnected() && definitionsSince == 0) {
definitionsSince = 1;
}
if (m_con) {
if (m_con->isConnected()) {
deque<Message*> 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<Message*>* 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;
}
}
+60 -7
View File
@@ -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
+72 -8
View File
@@ -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 <string.h>
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<knx_addr_t>(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<knx_addr_t>(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<knx_addr_t>(v << 8);
v = parseInt(str.substr(pos2+1).c_str(), 10, 0, 0xff, &err);
if (!err) {
dest |= static_cast<knx_addr_t>(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
+81 -18
View File
@@ -19,25 +19,55 @@
#ifndef LIB_KNX_KNX_H_
#define LIB_KNX_KNX_H_
#include <string>
#include <cstdint>
// 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_
+26 -4
View File
@@ -22,13 +22,20 @@
#include <eibclient.h>
#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_
+728
View File
@@ -0,0 +1,728 @@
/*
* ebusd - daemon for communication with eBUS heating systems.
* Copyright (C) 2022 John Baier <ebusd@ebusd.eu>
*
* 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/>.
*/
#ifndef LIB_KNX_KNXNET_H_
#define LIB_KNX_KNXNET_H_
#include <string>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/ioctl.h>
#include <net/if.h>
#ifndef __CYGWIN__
#include <net/if_arp.h>
#endif
#include <netdb.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <endian.h>
#include <cstdio>
#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 : newHopCount<oldHopCount) // decremented hop count?
&& memcmp(data+lOffset+2, m_data+lOffset+2, len-(lOffset+2)) == 0
) {
m_time = now;
return true;
}
return false;
}
void reset() {
m_time = 0;
}
private:
/** the last data. */
uint8_t m_data[256];
/** the length of the last data, or 0 for none. */
size_t m_len;
/** the offset to the L_Data. */
size_t m_lOffset;
/** the time of the last data, or 0 for none. */
time_t m_time;
};
#define CHECK_REPETITION_COUNT 4
class LastFrames {
public:
bool isRepetition(uint8_t* data, size_t len, size_t lOffset, time_t now, bool isSend = false) {
for (int i=0; i<CHECK_REPETITION_COUNT; i++) {
if (m_lastFrames[i].isValid(now)
&& m_lastFrames[i].isSameAs(data, len, lOffset, now, isSend)) {
return true;
}
}
return false;
}
void add(uint8_t* data, size_t len, size_t lOffset, time_t now) {
int oldestPos = -1;
time_t oldestAge = 0;
for (int i=0; i<CHECK_REPETITION_COUNT; i++) {
if (!m_lastFrames[i].isValid(now)) {
m_lastFrames[i].set(data, len, lOffset, now);
return;
}
if (oldestPos<0 || m_lastFrames[i].m_time < oldestAge) {
oldestPos = i;
oldestAge = m_lastFrames[i].m_time;
}
}
m_lastFrames[oldestPos].set(data, len, lOffset, now);
}
void reset() {
for (int i=0; i<CHECK_REPETITION_COUNT; i++) {
m_lastFrames[i].reset();
}
}
private:
/** the list of the last telegrams. */
LastFrame m_lastFrames[CHECK_REPETITION_COUNT];
};
#ifdef DEBUG
#define PRINTF printf
#else
#define PRINTF(...)
#endif
// helper method to log received/sent telegrams
void logTelegram(bool sent, knxnet_cemi_header_t* c, knxnet_l_data_header_t* l, uint8_t* d) {
bool isGrp = l->controlField2.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; i<l->informationLength; 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_