990 lines
34 KiB
C++
990 lines
34 KiB
C++
/*
|
|
* 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/>.
|
|
*/
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
# include <config.h>
|
|
#endif
|
|
|
|
#include "ebusd/knxhandler.h"
|
|
#ifdef HAVE_PPOLL
|
|
# include <poll.h>
|
|
#endif
|
|
#include <cmath>
|
|
#include <csignal>
|
|
#include <deque>
|
|
#include "lib/utils/log.h"
|
|
#include "lib/ebus/symbol.h"
|
|
|
|
#ifndef POLLRDHUP
|
|
#define POLLRDHUP 0
|
|
#endif
|
|
|
|
|
|
namespace ebusd {
|
|
|
|
using std::dec;
|
|
|
|
// version is coded as:
|
|
// 5 bits magic (to be incremented with incompatible changes, not shown)
|
|
// 5 bits major, using major directly
|
|
// 6 bits minor, using minor multiplied by 10 to have space for micro versioning in future
|
|
#define VERSION_INT ((PACKAGE_VERSION_MAJOR << 6) |(PACKAGE_VERSION_MINOR*10))
|
|
|
|
#define O_URL -2
|
|
#define O_AGR (O_URL-1)
|
|
#define O_AGW (O_AGR-1)
|
|
#define O_INT (O_AGW-1)
|
|
#define O_VAR (O_INT-1)
|
|
|
|
/** the definition of the KNX arguments. */
|
|
static const argDef g_knx_argDefs[] = {
|
|
{nullptr, 0, nullptr, 0, "KNX options:"},
|
|
{"knxurl", O_URL, "URL", af_optional, "URL to open (i.e. \"[multicast][@interface]\" for KNXnet/IP"
|
|
#ifdef HAVE_KNXD
|
|
" or \"ip:host[:port]\" / \"local:/socketpath\" for knxd"
|
|
#endif
|
|
") (no default)"},
|
|
{"knxrage", O_AGR, "SEC", 0, "Maximum age in seconds for using the last value of read messages (0=disable)"
|
|
" [5]"},
|
|
{"knxwage", O_AGW, "SEC", 0, "Maximum age in seconds for using the last value for reads on write messages"
|
|
" (0=disable), [99999999]"},
|
|
{"knxint", O_INT, "FILE", 0, "Read KNX integration settings from FILE [/etc/ebusd/knx.cfg]"},
|
|
{"knxvar", O_VAR, "NAME=VALUE[,...]", 0, "Add variable(s) to the read KNX integration settings"},
|
|
|
|
{nullptr, 0, nullptr, 0, nullptr},
|
|
};
|
|
|
|
static const char* g_url = nullptr; //!< URL of KNX daemon
|
|
const char* emptyUrl = "";
|
|
static unsigned int g_maxReadAge = 5; //!< max age in seconds for using the last value of read messages
|
|
// max age in seconds for using the last value for reads on write messages
|
|
static unsigned int g_maxWriteAge = 99999999;
|
|
static const char* g_integrationFile = nullptr; //!< the integration settings file
|
|
static vector<string>* g_integrationVars = nullptr; //!< the integration settings variables
|
|
|
|
/**
|
|
* The KNX argument parsing function.
|
|
* @param key the key from @a g_knx_arg_options.
|
|
* @param arg the option argument, or nullptr.
|
|
* @param state the parsing state.
|
|
*/
|
|
static int knx_parse_opt(int key, char *arg, const argParseOpt *parseOpt) {
|
|
result_t result;
|
|
unsigned int value;
|
|
switch (key) {
|
|
case O_URL: // --knxurl[=[multicast][@interface]]
|
|
// empty is allowed
|
|
g_url = arg ? arg : emptyUrl;
|
|
break;
|
|
|
|
case O_AGR: // --knxrage=5
|
|
if (arg == nullptr || arg[0] == 0) {
|
|
argParseError(parseOpt, "invalid knxrage value");
|
|
return EINVAL;
|
|
}
|
|
value = parseInt(arg, 10, 0, 99999999, &result);
|
|
if (result != RESULT_OK) {
|
|
argParseError(parseOpt, "invalid knxrage");
|
|
return EINVAL;
|
|
}
|
|
g_maxReadAge = value;
|
|
break;
|
|
|
|
case O_AGW: // --knxwage=5
|
|
if (arg == nullptr || arg[0] == 0) {
|
|
argParseError(parseOpt, "invalid knxwage value");
|
|
return EINVAL;
|
|
}
|
|
value = parseInt(arg, 10, 0, 99999999, &result);
|
|
if (result != RESULT_OK) {
|
|
argParseError(parseOpt, "invalid knxwage");
|
|
return EINVAL;
|
|
}
|
|
g_maxWriteAge = value;
|
|
break;
|
|
|
|
case O_INT: // --knxint=/etc/ebusd/knx.cfg
|
|
if (arg == nullptr || arg[0] == 0 || strcmp("/", arg) == 0) {
|
|
argParseError(parseOpt, "invalid knxint file");
|
|
return EINVAL;
|
|
}
|
|
g_integrationFile = arg;
|
|
break;
|
|
|
|
case O_VAR: // --knxvar=NAME=VALUE[,NAME=VALUE]*
|
|
{
|
|
if (arg == nullptr || arg[0] == 0 || !strchr(arg, '=')) {
|
|
argParseError(parseOpt, "invalid knxvar");
|
|
return EINVAL;
|
|
}
|
|
if (!g_integrationVars) {
|
|
g_integrationVars = new vector<string>();
|
|
}
|
|
std::istringstream istr;
|
|
istr.str(arg);
|
|
unsigned int lineNo = 0;
|
|
FileReader::splitFields(&istr, g_integrationVars, &lineNo, nullptr, nullptr, false);
|
|
break;
|
|
}
|
|
|
|
default:
|
|
return ESRCH;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static const argParseChildOpt g_knx_arg_child = { g_knx_argDefs, knx_parse_opt };
|
|
|
|
const argParseChildOpt* knxhandler_getargs() {
|
|
return &g_knx_arg_child;
|
|
}
|
|
|
|
bool knxhandler_register(UserInfo* userInfo, BusHandler* busHandler, MessageMap* messages,
|
|
list<DataHandler*>* handlers) {
|
|
if (g_url) {
|
|
handlers->push_back(new KnxHandler(userInfo, busHandler, messages));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
KnxHandler::KnxHandler(UserInfo* userInfo, BusHandler* busHandler, MessageMap* messages)
|
|
: DataSink(userInfo, "knx"), DataSource(busHandler), WaitThread(), m_messages(messages),
|
|
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);
|
|
}
|
|
}
|
|
if (g_integrationVars) {
|
|
for (auto& str : *g_integrationVars) {
|
|
m_replacers.parseLine(str);
|
|
}
|
|
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;
|
|
for (auto& key : keys) {
|
|
auto pos = key.find('/');
|
|
if (pos == string::npos) {
|
|
continue;
|
|
}
|
|
string val = m_replacers.get(key, false);
|
|
pos = val.find('/');
|
|
if (pos == string::npos) {
|
|
continue;
|
|
}
|
|
auto dest = parseAddress(val);
|
|
if (!dest) {
|
|
logOtherError("knx", "invalid assignment %s to %s", key.c_str(), val.c_str());
|
|
continue;
|
|
}
|
|
if (key.substr(0, 7) != "global/") {
|
|
messageCnt++;
|
|
m_messageFieldGroupAddress[key] = dest;
|
|
continue;
|
|
}
|
|
key = key.substr(7);
|
|
global_t index;
|
|
dtlf_t lengthFlag = DTLF_1BIT; // default for <=6 bits
|
|
if (key == "version") {
|
|
index = GLOBAL_VERSION;
|
|
lengthFlag.length = 2;
|
|
} else if (key == "running") {
|
|
index = GLOBAL_RUNNING;
|
|
} else if (key == "uptime") {
|
|
index = GLOBAL_UPTIME;
|
|
lengthFlag.length = 4;
|
|
} else if (key == "signal") {
|
|
index = GLOBAL_SIGNAL;
|
|
} else if (key == "scan") {
|
|
index = GLOBAL_SCAN;
|
|
} else if (key == "updatecheck") {
|
|
index = GLOBAL_UPDATECHECK;
|
|
} else {
|
|
logOtherError("knx", "invalid assignment global/%s to %s", key.c_str(), val.c_str());
|
|
continue;
|
|
}
|
|
m_subscribedGlobals[index] = dest|FLAG_READ;
|
|
groupInfo_t grpInfo;
|
|
grpInfo.messageKey = 0;
|
|
grpInfo.globalIndex = index;
|
|
grpInfo.lengthFlag = lengthFlag;
|
|
m_subscribedGroups[dest|FLAG_READ] = grpInfo;
|
|
globalCnt++;
|
|
}
|
|
logOtherInfo("knx", "parsed %d global and %d message assignments", globalCnt, messageCnt);
|
|
}
|
|
|
|
KnxHandler::~KnxHandler() {
|
|
join();
|
|
if (m_con) {
|
|
delete m_con;
|
|
m_con = nullptr;
|
|
}
|
|
}
|
|
|
|
void KnxHandler::startHandler() {
|
|
WaitThread::start("KNX");
|
|
}
|
|
|
|
void KnxHandler::notifyUpdateCheckResult(const string& checkResult) {
|
|
if (checkResult != m_lastUpdateCheckResult) {
|
|
m_lastUpdateCheckResult = checkResult;
|
|
sendGlobalValue(GLOBAL_UPDATECHECK, checkResult.empty() || checkResult == "OK" ? 0 : 1);
|
|
}
|
|
}
|
|
|
|
void KnxHandler::notifyScanStatus(scanStatus_t scanStatus) {
|
|
if (scanStatus == SCAN_STATUS_FINISHED) {
|
|
m_scanFinishReceived = true;
|
|
}
|
|
if (scanStatus != m_lastScanStatus) {
|
|
m_lastScanStatus = scanStatus;
|
|
sendGlobalValue(GLOBAL_SCAN, m_lastScanStatus == SCAN_STATUS_RUNNING ? 1 : 0);
|
|
}
|
|
}
|
|
|
|
result_t getFieldLength(const SingleDataField *field, dtlf_t *length) {
|
|
const auto dt = field->getDataType();
|
|
if (field->isIgnored() || !dt->isNumeric() || dt->isAdjustableLength()) {
|
|
return RESULT_ERR_INVALID_NUM;
|
|
}
|
|
size_t bitCnt = dt->getBitCount();
|
|
if (bitCnt == 1) {
|
|
*length = DTLF_1BIT;
|
|
return RESULT_OK;
|
|
}
|
|
if (bitCnt < 8) {
|
|
*length = DTLF_8BIT;
|
|
return RESULT_OK;
|
|
}
|
|
const auto nt = dynamic_cast<const NumberDataType*>(dt);
|
|
if (nt->getDivisor() != 1) {
|
|
// adjust bit count to 2 octet or 4 octet float DPT
|
|
if (bitCnt >= 24 && bitCnt < 31) {
|
|
bitCnt = 32;
|
|
} else if (bitCnt < 16) {
|
|
bitCnt = 16;
|
|
}
|
|
// TODO uncommon divisor (e.g. >100) may not fit into KNX 2-octet float or truncates precision
|
|
} else if (bitCnt >= 24 && bitCnt < 31) {
|
|
// adjust bit count for non-existent 24 bit KNX type
|
|
bitCnt = 32;
|
|
}
|
|
*length = {
|
|
.hasDivisor = nt->getDivisor() != 1,
|
|
.isFloat = dt->hasFlag(EXP),
|
|
.isSigned = dt->hasFlag(SIG),
|
|
.lastValueSent = false,
|
|
.length = static_cast<uint8_t>(bitCnt/8),
|
|
.lastValue = 0,
|
|
};
|
|
return RESULT_OK;
|
|
}
|
|
|
|
uint32_t floatToInt16(float val) {
|
|
// (0.01*m)(2^e) format with sign, 12 bits mantissa (incl. sign), 4 bits exponent
|
|
if (val == 0) {
|
|
return 0;
|
|
}
|
|
bool negative = val < 0;
|
|
if (negative) {
|
|
val = -val;
|
|
}
|
|
val *= 100;
|
|
int exp = ilogb(val)-10;
|
|
if (exp < -10 || exp > 15) {
|
|
return 0x7fff; // invalid value DPT 9
|
|
}
|
|
auto shift = exp > 0 ? exp : 0;
|
|
auto sig = static_cast<uint32_t>(val * exp2(-shift));
|
|
uint32_t value = static_cast<uint32_t>(shift << 11) | sig;
|
|
if (negative) {
|
|
return value | 0x8000;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
float int16ToFloat(uint16_t val) {
|
|
if (val == 0) {
|
|
return 0;
|
|
}
|
|
if (val == 0x7fff) {
|
|
return static_cast<float>(0xffffffff); // NaN
|
|
}
|
|
bool negative = val&0x8000;
|
|
int exp = (val>>11)&0xf;
|
|
int sig = val&0x7ff;
|
|
return static_cast<float>(sig * exp2(exp) * (negative ? -0.01 : 0.01));
|
|
}
|
|
|
|
result_t KnxHandler::sendGroupValue(knx_addr_t dest, apci_t apci, dtlf_t& lengthFlag, unsigned int value,
|
|
const SingleDataField *field) const {
|
|
if (!m_con || !m_con->isConnected() || !m_con->getAddress()) {
|
|
return RESULT_EMPTY;
|
|
}
|
|
uint8_t data[] = {0, 0, 0, 0, 0, 0};
|
|
data[0] = static_cast<uint8_t>(apci>>8);
|
|
data[1] = static_cast<uint8_t>(apci&0xff);
|
|
int len = 2;
|
|
// convert value to dpt
|
|
if (lengthFlag.isFloat || lengthFlag.hasDivisor) {
|
|
if (!field) {
|
|
return RESULT_ERR_INVALID_NUM;
|
|
}
|
|
auto nt = dynamic_cast<const NumberDataType*>(field->getDataType());
|
|
float fval;
|
|
result_t ret = nt->getFloatFromRawValue(value, &fval);
|
|
if (ret == RESULT_EMPTY) {
|
|
// replacement value:
|
|
if (lengthFlag.length == 2) {
|
|
// shall have 0x7fff for DPT 9
|
|
value = 0x7fff;
|
|
} else {
|
|
return RESULT_ERR_INVALID_NUM; // not encodable
|
|
}
|
|
} else if (ret != RESULT_OK) {
|
|
return ret;
|
|
} else if (lengthFlag.length == 2) {
|
|
// convert to (0.01*m)(2^e) format with sign, 12 bits mantissa (incl. sign), 4 bits exponent
|
|
value = floatToInt16(fval);
|
|
} else if (lengthFlag.length == 4) {
|
|
// convert to IEEE 754
|
|
value = floatToUint(fval);
|
|
} else {
|
|
return RESULT_ERR_INVALID_NUM; // not encodable
|
|
}
|
|
}
|
|
// else signed values: fine as long as length is identical
|
|
if (apci == APCI_GROUPVALUE_WRITE && lengthFlag.lastValueSent && lengthFlag.lastValue == value) {
|
|
return RESULT_EMPTY; // no need to send the same group value again
|
|
}
|
|
lengthFlag.lastValue = value;
|
|
lengthFlag.lastValueSent = true;
|
|
switch (lengthFlag.length) {
|
|
case 0: // short value <= 6 bit
|
|
data[1] |= static_cast<uint8_t>(value&0x3f);
|
|
break;
|
|
case 1: // 1 octet
|
|
data[2] = static_cast<uint8_t>(value&0xff);
|
|
break;
|
|
case 2: // 2 octets
|
|
data[2] = static_cast<uint8_t>(value>>8);
|
|
data[3] = static_cast<uint8_t>(value&0xff);
|
|
break;
|
|
case 4: // 4 octets
|
|
data[2] = static_cast<uint8_t>(value>>24);
|
|
data[3] = static_cast<uint8_t>(value>>16);
|
|
data[4] = static_cast<uint8_t>(value>>8);
|
|
data[5] = static_cast<uint8_t>(value&0xff);
|
|
break;
|
|
default:
|
|
return RESULT_ERR_INVALID_NUM;
|
|
}
|
|
len += lengthFlag.length;
|
|
const char* err = m_con->sendGroup(dest, len, data);
|
|
if (err) {
|
|
logOtherError("knx", "unable to send %s, dest %4.4x, len %d",
|
|
apci == APCI_GROUPVALUE_WRITE ? "write" : apci == APCI_GROUPVALUE_READ ? "read" : "response",
|
|
dest, len);
|
|
return RESULT_ERR_SEND;
|
|
}
|
|
logOtherDebug("knx", "sent %s, dest %4.4x, len %d",
|
|
apci == APCI_GROUPVALUE_WRITE ? "write" : apci == APCI_GROUPVALUE_READ ? "read" : "response",
|
|
dest, len);
|
|
return RESULT_OK;
|
|
}
|
|
|
|
void KnxHandler::sendGlobalValue(global_t index, unsigned int value, bool response) {
|
|
if (!m_con->isConnected() || !m_con->getAddress()) {
|
|
return;
|
|
}
|
|
const auto vit = m_subscribedGlobals.find(index);
|
|
if (vit == m_subscribedGlobals.cend()) {
|
|
return;
|
|
}
|
|
auto git = m_subscribedGroups.find(vit->second);
|
|
if (git == m_subscribedGroups.end()) {
|
|
return;
|
|
}
|
|
sendGroupValue(static_cast<knx_addr_t>(vit->second&0xffff),
|
|
response ? APCI_GROUPVALUE_RESPONSE : APCI_GROUPVALUE_WRITE,
|
|
git->second.lengthFlag, value);
|
|
}
|
|
|
|
result_t KnxHandler::receiveTelegram(int maxlen, knx_transfer_t* typ, uint8_t *buf, int *recvlen,
|
|
knx_addr_t *src, knx_addr_t *dest, bool wait) {
|
|
struct timespec tdiff = {
|
|
.tv_sec = wait ? 2 : 0, // 2 seconds when waiting
|
|
.tv_nsec = wait ? 0 : 1000, // 1 milliseond when not waiting
|
|
};
|
|
if (!m_con->isConnected()) {
|
|
return RESULT_ERR_GENERIC_IO;
|
|
}
|
|
int fd = m_con->getPollFd();
|
|
#ifdef HAVE_PPOLL
|
|
nfds_t nfds = 1;
|
|
struct pollfd fds[nfds];
|
|
memset(fds, 0, sizeof(fds));
|
|
fds[0].fd = fd;
|
|
fds[0].events = POLLIN | POLLERR | POLLHUP | POLLRDHUP;
|
|
#else
|
|
#ifdef HAVE_PSELECT
|
|
fd_set checkfds, exceptfds;
|
|
FD_ZERO(&checkfds);
|
|
FD_SET(fd, &checkfds);
|
|
FD_ZERO(&exceptfds);
|
|
FD_SET(fd, &exceptfds);
|
|
#endif
|
|
#endif
|
|
int ret;
|
|
#ifdef HAVE_PPOLL
|
|
ret = ppoll(fds, nfds, &tdiff, nullptr);
|
|
#else
|
|
#ifdef HAVE_PSELECT
|
|
fd_set readfds = checkfds;
|
|
ret = pselect(fd + 1, &readfds, nullptr, &exceptfds, &tdiff, nullptr);
|
|
#endif
|
|
#endif
|
|
bool newData;
|
|
#ifdef HAVE_PPOLL
|
|
if (ret < 0 || (ret > 0 && (fds[0].revents & (POLLERR | POLLHUP | POLLRDHUP)))) {
|
|
return RESULT_ERR_GENERIC_IO;
|
|
}
|
|
newData = fds[0].revents & POLLIN;
|
|
#else
|
|
#ifdef HAVE_PSELECT
|
|
if (ret < 0 || FD_ISSET(fd, &exceptfds)) {
|
|
return RESULT_ERR_GENERIC_IO;
|
|
}
|
|
newData = FD_ISSET(fd, &readfds);
|
|
#endif
|
|
#endif
|
|
if (!newData) {
|
|
// timeout
|
|
return RESULT_ERR_TIMEOUT;
|
|
}
|
|
*typ = m_con->getPollData(maxlen, buf, recvlen, src, dest);
|
|
return *typ == KNX_TRANSFER_NONE ? RESULT_EMPTY : RESULT_OK;
|
|
}
|
|
|
|
/*
|
|
void printResponse(knx_addr_t src, knx_addr_t dest, int len, const uint8_t *data) {
|
|
int tctrl = data[0]>>2;
|
|
int apci = ((data[0]&0x03)<<8) | data[1];
|
|
if ((apci & APCI_GROUPVALUE_READ_MASK) == 0) {
|
|
apci &= ~APCI_GROUPVALUE_READ_MASK;
|
|
}
|
|
int value = len == 2 ? data[1]&0x3f : data[2]; // 6 bits or full octet
|
|
if (len>3) {
|
|
value = (value<<8) | data[3]; // up to 16 bits
|
|
}
|
|
if (len>4) {
|
|
value = (value<<8) | data[4]; // up to 24 bits
|
|
}
|
|
if (len>5) {
|
|
value = (value<<8) | data[5]; // up to 32 bits
|
|
}
|
|
logOtherDebug("knx", "recv from %4.4x to %4.4x, %s (0x%3.3x, tctrl 0x%2.2x), len %d", src, dest,
|
|
apci == APCI_GROUPVALUE_WRITE ? "write" : apci == APCI_GROUPVALUE_READ ? "read"
|
|
: apci == APCI_GROUPVALUE_RESPONSE ? "response" : "other",
|
|
apci, tctrl, len);
|
|
}
|
|
*/
|
|
|
|
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) {
|
|
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) {
|
|
if (typ == KNX_TRANSFER_NONE) {
|
|
return;
|
|
}
|
|
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) {
|
|
apci = groupReadWriteApci;
|
|
}
|
|
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(static_cast<knx_addr_t>((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));
|
|
auto sit = m_subscribedGroups.find(subKey);
|
|
if (needsLog(lf_other, ll_debug)) {
|
|
logOtherDebug("knx", "received %ssubscribed %s from %4.4x to %4.4x, len %d",
|
|
sit == m_subscribedGroups.end() ? "un" : "",
|
|
apci == APCI_GROUPVALUE_WRITE ? "write" : apci == APCI_GROUPVALUE_READ ? "read" : "response",
|
|
src, dest, len);
|
|
}
|
|
if (sit == m_subscribedGroups.end()) {
|
|
return; // address+direction not subscribed
|
|
}
|
|
if (sit->second.messageKey == 0) {
|
|
// global values, only readable
|
|
switch (sit->second.globalIndex) {
|
|
case GLOBAL_VERSION:
|
|
sendGlobalValue(GLOBAL_VERSION, VERSION_INT, true);
|
|
break;
|
|
case GLOBAL_RUNNING:
|
|
sendGlobalValue(GLOBAL_RUNNING, 1, true);
|
|
break;
|
|
case GLOBAL_UPTIME:
|
|
sendGlobalValue(GLOBAL_UPTIME, static_cast<unsigned>(time(nullptr) - m_start), true);
|
|
break;
|
|
case GLOBAL_SIGNAL:
|
|
sendGlobalValue(GLOBAL_SIGNAL, m_busHandler->hasSignal() ? 1 : 0, true);
|
|
break;
|
|
case GLOBAL_SCAN:
|
|
sendGlobalValue(GLOBAL_SCAN, m_lastScanStatus == SCAN_STATUS_RUNNING ? 1 : 0, true);
|
|
break;
|
|
case GLOBAL_UPDATECHECK:
|
|
sendGlobalValue(GLOBAL_UPDATECHECK, m_lastUpdateCheckResult.empty() || m_lastUpdateCheckResult == "OK"
|
|
|| m_lastUpdateCheckResult == "." ? 0 : 1, true);
|
|
break;
|
|
default:
|
|
return; // ignore
|
|
}
|
|
return;
|
|
}
|
|
const vector<Message*>* messages = m_messages->getByKey(sit->second.messageKey);
|
|
if (!messages) {
|
|
return;
|
|
}
|
|
Message *msg = nullptr;
|
|
ssize_t fieldIndex = sit->second.fieldIndex;
|
|
const SingleDataField* field = nullptr;
|
|
for (const auto& message : *messages) {
|
|
if (!message->isAvailable() || message->getDstAddress() == SYN) {
|
|
continue;
|
|
}
|
|
if ((message->isWrite() && !message->isPassive()) != isWrite) {
|
|
if (isWrite || message->getLastUpdateTime() <= 0) {
|
|
continue;
|
|
} // else: allow potential "write-read" association to read the last written value
|
|
}
|
|
field = message->getField(fieldIndex);
|
|
if (!field) {
|
|
continue;
|
|
}
|
|
if (isWrite) {
|
|
msg = message;
|
|
break; // best candidate
|
|
}
|
|
if (!msg) {
|
|
msg = message;
|
|
} else if (message->getLastUpdateTime() > 0
|
|
&& message->getLastUpdateTime() > msg->getLastUpdateTime()) {
|
|
// prefer newer updated, even if it is passive
|
|
msg = message;
|
|
} else if (!message->isPassive()) {
|
|
// prefer active read before passive
|
|
msg = message;
|
|
}
|
|
}
|
|
if (!msg) {
|
|
logOtherInfo("knx", "unable to answer %s request to %4.4x", isWrite ? "write" : "read", dest);
|
|
return;
|
|
}
|
|
result_t res;
|
|
const string circuit = msg->getCircuit(), name = msg->getName(), fieldName = msg->getFieldName(fieldIndex);
|
|
if (isWrite) {
|
|
unsigned int value = len == 2 ? data[1]&0x3f : data[2]; // <=6 bits or full octet
|
|
if (len > 3) {
|
|
value = (value << 8) | data[3]; // up to 16 bits
|
|
}
|
|
if (len > 4) {
|
|
value = (value << 8) | data[4]; // up to 24 bits
|
|
}
|
|
if (len > 5) {
|
|
value = (value << 8) | data[5]; // up to 32 bits
|
|
}
|
|
// note: a write from KNX updates the message and thus re-sends the write later on again during update check
|
|
logOtherNotice("knx", "received write request from %4.4x to %4.4x for %s/%s/%s, value %d",
|
|
src, dest, circuit.c_str(), name.c_str(), fieldName.c_str(), value);
|
|
// write new field value to bus if possible
|
|
// ugly but least intrusive: format single num field value to string to have it parsed back later on
|
|
ostringstream str;
|
|
// convert value to dpt
|
|
auto lengthFlag = sit->second.lengthFlag;
|
|
if (lengthFlag.isFloat || lengthFlag.hasDivisor) {
|
|
float fval;
|
|
if (lengthFlag.length == 2) {
|
|
// convert from (0.01*m)(2^e) format with sign, 12 bits mantissa (incl. sign), 4 bits exponent
|
|
fval = int16ToFloat(static_cast<uint16_t>(value));
|
|
} else if (lengthFlag.length == 4) {
|
|
// convert from IEEE 754
|
|
bool negative = (value & (1u << 31)) != 0;
|
|
fval = uintToFloat(value, negative);
|
|
} else {
|
|
logOtherNotice("knx", "unable to decode write request from %4.4x to %4.4x for %s/%s/%s, value %d",
|
|
src, dest, circuit.c_str(), name.c_str(), fieldName.c_str(), value);
|
|
return; // not decodable
|
|
}
|
|
str << static_cast<float>(fval);
|
|
} else {
|
|
if (lengthFlag.isSigned) {
|
|
// signed values: determine sign
|
|
uint32_t bit = 1 << (lengthFlag.length*8-1);
|
|
if (value & bit) {
|
|
value = -(value&~bit);
|
|
}
|
|
str << static_cast<int>(value);
|
|
} else {
|
|
str << static_cast<uint32_t>(value);
|
|
}
|
|
}
|
|
res = m_busHandler->readFromBus(msg, str.str());
|
|
if (res == RESULT_OK) {
|
|
logOtherDebug("knx", "wrote %s %s", circuit.c_str(), name.c_str());
|
|
} else {
|
|
logOtherError("knx", "write %s %s: %s", circuit.c_str(), name.c_str(), getResultCode(res));
|
|
}
|
|
return;
|
|
}
|
|
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());
|
|
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);
|
|
return; // impossible to answer
|
|
}
|
|
} else if (!msg->isPassive() && now >= msg->getLastUpdateTime() + g_maxReadAge) {
|
|
res = m_busHandler->readFromBus(msg, "");
|
|
if (res != RESULT_OK) {
|
|
return;
|
|
}
|
|
}
|
|
unsigned int value = 0;
|
|
res = msg->decodeLastDataNumField(nullptr, fieldIndex, &value);
|
|
if (res == RESULT_OK) {
|
|
logOtherDebug("knx", "read %s %s", circuit.c_str(), name.c_str());
|
|
res = sendGroupValue(dest, APCI_GROUPVALUE_RESPONSE, sit->second.lengthFlag, value, field);
|
|
} else {
|
|
logOtherError("knx", "read %s %s: %s", circuit.c_str(), name.c_str(), getResultCode(res));
|
|
}
|
|
}
|
|
|
|
// interval in seconds for sending the uptime value
|
|
#define UPTIME_INTERVAL 3600
|
|
|
|
void KnxHandler::run() {
|
|
time_t lastTaskRun, now, lastSignal = 0, lastUptime = 0, lastUpdates = 0;
|
|
bool signal = false;
|
|
result_t result = RESULT_OK;
|
|
time(&now);
|
|
m_start = lastTaskRun = now;
|
|
uint8_t data[256];
|
|
int len = 0;
|
|
time_t definitionsSince = 0;
|
|
while (isRunning()) {
|
|
bool wasConnected = m_con->isConnected();
|
|
bool needsWait = true;
|
|
if (!wasConnected) {
|
|
const char* err = m_con->open();
|
|
if (!err) {
|
|
m_lastErrorLogTime = 0;
|
|
logOtherNotice("knx", "connected to %s", m_con->getInfo());
|
|
sendGlobalValue(GLOBAL_VERSION, VERSION_INT);
|
|
sendGlobalValue(GLOBAL_RUNNING, 1);
|
|
}
|
|
if (err) {
|
|
m_con->close();
|
|
time(&now);
|
|
if (now > m_lastErrorLogTime + 10) { // log at most every 10 seconds
|
|
m_lastErrorLogTime = now;
|
|
logOtherError("knx", err);
|
|
}
|
|
}
|
|
}
|
|
bool reconnected = !wasConnected && m_con->isConnected();
|
|
time(&now);
|
|
bool sendSignal = reconnected;
|
|
if (now < m_start) {
|
|
// clock skew
|
|
if (now < lastSignal) {
|
|
lastSignal -= lastTaskRun-now;
|
|
}
|
|
lastTaskRun = now;
|
|
} else if (now > lastTaskRun+(m_scanFinishReceived ? 1 : 15)) {
|
|
m_scanFinishReceived = false;
|
|
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->isConnected() && definitionsSince == 0) {
|
|
definitionsSince = 1;
|
|
}
|
|
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;
|
|
for (const auto& message : messages) {
|
|
const auto mit = m_subscribedMessages.find(message->getKey());
|
|
if (mit != m_subscribedMessages.cend()) {
|
|
continue; // already subscribed
|
|
}
|
|
if (message->getDstAddress() == SYN) {
|
|
continue; // not usable in absence of destination address
|
|
}
|
|
bool isWrite = message->isWrite() && !message->isPassive(); // from KNX perspective
|
|
if (message->getCreateTime() <= definitionsSince) { // only newer defined
|
|
continue;
|
|
}
|
|
ssize_t fieldCount = static_cast<signed>(message->getFieldCount());
|
|
if (isWrite && fieldCount > 1) {
|
|
// impossible with more than one field
|
|
continue;
|
|
}
|
|
bool added = false;
|
|
for (ssize_t index = 0; index < fieldCount; index++) {
|
|
const SingleDataField* field = message->getField(index);
|
|
if (!field || field->isIgnored()) {
|
|
continue;
|
|
}
|
|
string fieldName = message->getFieldName(index);
|
|
if (fieldName.empty() && fieldCount == 1) {
|
|
fieldName = "0"; // might occur for unnamed single field sets
|
|
}
|
|
string key = message->getCircuit()+"/"+message->getName()+"/"+fieldName;
|
|
const auto git = m_messageFieldGroupAddress.find(key);
|
|
if (git == m_messageFieldGroupAddress.cend()) {
|
|
continue;
|
|
}
|
|
// determine field length in telegram
|
|
dtlf_t lengthFlag = {};
|
|
result = getFieldLength(field, &lengthFlag);
|
|
if (result != RESULT_OK) {
|
|
continue;
|
|
}
|
|
// store association
|
|
knx_addr_t dest = git->second;
|
|
auto subKey = static_cast<uint32_t>(dest | (isWrite ? FLAG_WRITE : FLAG_READ));
|
|
auto sit = m_subscribedGroups.find(subKey);
|
|
if (sit != m_subscribedGroups.cend()) {
|
|
if (isWrite) {
|
|
logOtherDebug("knx", "ignored already subscribed %s", key.c_str());
|
|
continue;
|
|
}
|
|
if (sit->second.messageKey == message->getKey()) {
|
|
continue;
|
|
} // else: overwrite "write-read" with readable message
|
|
logOtherDebug("knx", "replacing write-read association %s to %4.4x", key.c_str(), dest);
|
|
}
|
|
groupInfo_t grpInfo;
|
|
grpInfo.messageKey = message->getKey();
|
|
grpInfo.globalIndex = static_cast<global_t>(index);
|
|
grpInfo.lengthFlag = lengthFlag;
|
|
m_subscribedGroups[subKey] = grpInfo;
|
|
m_subscribedMessages[message->getKey()].push_back(subKey);
|
|
logOtherDebug("knx", "added %s association %s to %4.4x", isWrite ? "write" : "read", key.c_str(), dest);
|
|
if (isWrite) {
|
|
// add "write-read" association to allow reading the last written value of a writable message
|
|
// when there is no readable message set directly yet
|
|
subKey = static_cast<uint32_t>(dest | FLAG_READ);
|
|
sit = m_subscribedGroups.find(subKey);
|
|
if (sit == m_subscribedGroups.cend()) {
|
|
m_subscribedGroups[subKey] = grpInfo;
|
|
logOtherDebug("knx", "added write-read association %s to %4.4x", key.c_str(), dest);
|
|
}
|
|
}
|
|
added = true;
|
|
addCnt++;
|
|
}
|
|
if (!added) {
|
|
continue;
|
|
}
|
|
if (message->getLastUpdateTime() > message->getCreateTime()) {
|
|
// ensure data is published as well
|
|
m_updatedMessages[message->getKey()]++;
|
|
} else if (message->isWrite()) {
|
|
// publish data for read pendant of write message
|
|
Message* read = m_messages->find(message->getCircuit(), message->getName(), "", false);
|
|
if (read && read->getLastUpdateTime() > 0) {
|
|
m_updatedMessages[read->getKey()]++;
|
|
}
|
|
}
|
|
}
|
|
if (addCnt > 0) {
|
|
logOtherInfo("knx", "added %d associations, %d active now", addCnt, m_subscribedGroups.size());
|
|
}
|
|
definitionsSince = now;
|
|
needsWait = true;
|
|
}
|
|
time(&lastTaskRun);
|
|
}
|
|
if (sendSignal) {
|
|
if (m_busHandler->hasSignal()) {
|
|
lastSignal = now;
|
|
if (!signal || reconnected) {
|
|
signal = true;
|
|
sendGlobalValue(GLOBAL_SIGNAL, 1);
|
|
}
|
|
} else {
|
|
if (signal || reconnected) {
|
|
signal = false;
|
|
sendGlobalValue(GLOBAL_SIGNAL, 0);
|
|
}
|
|
}
|
|
}
|
|
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
|
|
// limit number of read telegrams in order to give back control to outer loop for checking updates etc
|
|
for (int count = 0; count < 10; count++) {
|
|
// wait for telegram on first iteration only
|
|
result_t res = receiveTelegram(sizeof(data), &typ, data, &len, &src, &dest, count == 0);
|
|
if (res != RESULT_OK) {
|
|
if (res == RESULT_ERR_GENERIC_IO) {
|
|
m_con->close();
|
|
}
|
|
break;
|
|
}
|
|
needsWait = false;
|
|
handleReceivedTelegram(typ, src, dest, len, data);
|
|
}
|
|
}
|
|
if (!m_updatedMessages.empty()) {
|
|
m_messages->lock();
|
|
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) {
|
|
it = m_updatedMessages.erase(it);
|
|
continue;
|
|
}
|
|
for (const auto& message : *messages) {
|
|
if (message->getLastChangeTime() <= 0) {
|
|
continue;
|
|
}
|
|
const auto mit = m_subscribedMessages.find(message->getKey());
|
|
if (mit == m_subscribedMessages.cend()) {
|
|
continue;
|
|
}
|
|
if (!(message->getDataHandlerState()&2)) {
|
|
message->setDataHandlerState(2, true); // first update still needed
|
|
} else if (message->getLastChangeTime() <= lastUpdates) {
|
|
continue;
|
|
}
|
|
for (auto destFlags : mit->second) {
|
|
auto sit = m_subscribedGroups.find(destFlags);
|
|
if (sit == m_subscribedGroups.end()) {
|
|
continue;
|
|
}
|
|
ssize_t index = sit->second.fieldIndex;
|
|
const SingleDataField *field = message->getField(index);
|
|
if (!field || field->isIgnored()) {
|
|
continue;
|
|
}
|
|
knx_addr_t dest = destFlags&0xffff;
|
|
unsigned int value = 0;
|
|
result = message->decodeLastDataNumField(nullptr, index, &value);
|
|
sendGroupValue(dest, APCI_GROUPVALUE_WRITE, sit->second.lengthFlag, value, field);
|
|
}
|
|
}
|
|
it = m_updatedMessages.erase(it);
|
|
}
|
|
time(&lastUpdates);
|
|
} else {
|
|
m_updatedMessages.clear();
|
|
}
|
|
m_messages->unlock();
|
|
}
|
|
if ((!m_con->isConnected() && !Wait(5)) || (needsWait && !Wait(0, 100))
|
|
) {
|
|
break;
|
|
}
|
|
}
|
|
sendGlobalValue(GLOBAL_RUNNING, 0);
|
|
sendGlobalValue(GLOBAL_SIGNAL, 0);
|
|
sendGlobalValue(GLOBAL_SCAN, 0);
|
|
}
|
|
|
|
} // namespace ebusd
|