Files
ebusd/src/lib/knx/knxnet.h
T

655 lines
19 KiB
C++

/*
* ebusd - daemon for communication with eBUS heating systems.
* Copyright (C) 2022-2024 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 <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>
#ifdef __FreeBSD__
#include <machine/endian.h>
#else
#include <endian.h>
#endif
#include <string>
#include <cstdio>
#include <ctime>
#include "lib/knx/knx.h"
#include "lib/utils/tcpsocket.h"
namespace ebusd {
/** @file lib/knx/knxnet.h
* KNXnet/IP implementation of the @a KnxConnection interface based on UDP multicast.
*/
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;
// optional immediately following additional bytes, usually =0. fixed to 0 in cEMI management messages
uint8_t additionalInfoLength;
} 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_STR "224.0.23.12"
#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
// 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");
}
#else
#define PRINTF(...)
#define logTelegram(...)
#endif
/**
* 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.
*/
explicit 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 fd = socketConnect(m_url && m_url[0] ? m_url : SYSTEM_MULTICAST_IP_STR,
SYSTEM_MULTICAST_PORT, IPPROTO_UDP, nullptr, 0x02);
if (fd < 0) {
return "create socket";
}
// set non-blocking
if (fcntl(fd, F_SETFL, O_NONBLOCK) != 0) {
::close(fd);
return "non-blocking";
}
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 slen = recv(m_sock, buf, sizeof(buf), 0);
if (slen < 0 || static_cast<unsigned>(slen) < sizeof(knxnet_header_t)) {
PRINTF("#skip recv short hdr len=%d\n", len);
return KNX_TRANSFER_NONE;
}
size_t len = static_cast<unsigned>(slen);
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 = ::send(m_sock, buf, totalLen, MSG_NOSIGNAL);
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() const 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() const override {
return m_programmingMode;
}
// @copydoc
void setProgrammingMode(bool on) override {
m_programmingMode = on;
}
private:
/** the URL to connect to. */
const char* m_url;
/** 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_