Implement KNX Gateway functionality with support for DALI integration

- Added gateway_knx.cpp to handle KNX communication and DALI bridge requests.
- Implemented functions for encoding/decoding KNX telegrams and managing group writes.
- Introduced GatewayKnxBridge and GatewayKnxTpIpRouter classes for managing KNX to DALI routing and IP tunneling.
- Added configuration handling for KNX settings, including UART and multicast options.
- Implemented error handling and logging for various KNX operations.

Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
Tony
2026-05-08 18:19:37 +08:00
parent 029785ff1d
commit 1a8ee06ec1
10 changed files with 1589 additions and 5 deletions
+980
View File
@@ -0,0 +1,980 @@
#include "gateway_knx.hpp"
#include "driver/uart.h"
#include "esp_log.h"
#include "lwip/inet.h"
#include "lwip/sockets.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstring>
#include <initializer_list>
#include <utility>
#include <unistd.h>
namespace gateway {
namespace {
constexpr const char* kTag = "gateway_knx";
constexpr uint8_t kCemiLDataReq = 0x11;
constexpr uint8_t kCemiLDataInd = 0x29;
constexpr uint8_t kCemiLDataCon = 0x2e;
constexpr uint16_t kServiceConnectRequest = 0x0205;
constexpr uint16_t kServiceConnectResponse = 0x0206;
constexpr uint16_t kServiceConnectionStateRequest = 0x0207;
constexpr uint16_t kServiceConnectionStateResponse = 0x0208;
constexpr uint16_t kServiceDisconnectRequest = 0x0209;
constexpr uint16_t kServiceDisconnectResponse = 0x020a;
constexpr uint16_t kServiceTunnellingRequest = 0x0420;
constexpr uint16_t kServiceTunnellingAck = 0x0421;
constexpr uint16_t kServiceRoutingIndication = 0x0530;
constexpr uint8_t kKnxNetIpHeaderSize = 0x06;
constexpr uint8_t kKnxNetIpVersion10 = 0x10;
constexpr uint8_t kKnxNoError = 0x00;
constexpr uint8_t kKnxErrorConnectionId = 0x21;
constexpr uint8_t kKnxErrorConnectionType = 0x22;
constexpr uint8_t kKnxErrorNoMoreConnections = 0x24;
constexpr uint8_t kKnxErrorSequenceNumber = 0x04;
constexpr uint8_t kKnxConnectionTypeTunnel = 0x04;
constexpr uint8_t kKnxTunnelLayerLink = 0x02;
struct DecodedGroupWrite {
uint16_t group_address{0};
std::vector<uint8_t> data;
};
uint16_t ReadBe16(const uint8_t* data) {
return static_cast<uint16_t>((static_cast<uint16_t>(data[0]) << 8) | data[1]);
}
void WriteBe16(uint8_t* data, uint16_t value) {
data[0] = static_cast<uint8_t>((value >> 8) & 0xff);
data[1] = static_cast<uint8_t>(value & 0xff);
}
std::optional<int> ObjectIntAny(const DaliValue::Object& object,
std::initializer_list<const char*> keys) {
for (const char* key : keys) {
if (const auto value = getObjectInt(object, key)) {
return value;
}
}
return std::nullopt;
}
std::optional<bool> ObjectBoolAny(const DaliValue::Object& object,
std::initializer_list<const char*> keys) {
for (const char* key : keys) {
if (const auto value = getObjectBool(object, key)) {
return value;
}
}
return std::nullopt;
}
std::optional<std::string> ObjectStringAny(const DaliValue::Object& object,
std::initializer_list<const char*> keys) {
for (const char* key : keys) {
if (const auto value = getObjectString(object, key)) {
return value;
}
}
return std::nullopt;
}
std::string TargetName(const GatewayKnxDaliTarget& target) {
switch (target.kind) {
case GatewayKnxDaliTargetKind::kBroadcast:
return "Broadcast";
case GatewayKnxDaliTargetKind::kShortAddress:
return "A" + std::to_string(target.address);
case GatewayKnxDaliTargetKind::kGroup:
return "Group " + std::to_string(target.address);
case GatewayKnxDaliTargetKind::kNone:
default:
return "Unmapped";
}
}
std::string DataTypeName(GatewayKnxDaliDataType data_type) {
switch (data_type) {
case GatewayKnxDaliDataType::kSwitch:
return "Switch";
case GatewayKnxDaliDataType::kBrightness:
return "Dimmer";
case GatewayKnxDaliDataType::kColorTemperature:
return "Color Temperature";
case GatewayKnxDaliDataType::kRgb:
return "RGB";
case GatewayKnxDaliDataType::kUnknown:
default:
return "Unknown";
}
}
const char* DataTypeDpt(GatewayKnxDaliDataType data_type) {
switch (data_type) {
case GatewayKnxDaliDataType::kSwitch:
return "DPST-1-1";
case GatewayKnxDaliDataType::kBrightness:
return "DPST-5-1";
case GatewayKnxDaliDataType::kColorTemperature:
return "DPST-7-600";
case GatewayKnxDaliDataType::kRgb:
return "DPST-232-600";
case GatewayKnxDaliDataType::kUnknown:
default:
return "";
}
}
std::optional<DecodedGroupWrite> DecodeCemiGroupWrite(const uint8_t* data, size_t len) {
if (data == nullptr || len < 10) {
return std::nullopt;
}
const uint8_t message_code = data[0];
if (message_code != kCemiLDataReq && message_code != kCemiLDataInd &&
message_code != kCemiLDataCon) {
return std::nullopt;
}
const size_t base = 2U + data[1];
if (len < base + 8U) {
return std::nullopt;
}
const uint8_t control2 = data[base + 1];
if ((control2 & 0x80) == 0) {
return std::nullopt;
}
const uint16_t destination = ReadBe16(data + base + 4);
const size_t tpdu_len = static_cast<size_t>(data[base + 6]) + 1U;
if (tpdu_len < 2U || len < base + 7U + tpdu_len) {
return std::nullopt;
}
const uint8_t* tpdu = data + base + 7;
const uint16_t apci = static_cast<uint16_t>(((tpdu[0] & 0x03) << 8) | (tpdu[1] & 0xc0));
if (apci != 0x80) {
return std::nullopt;
}
DecodedGroupWrite out;
out.group_address = destination;
if (tpdu_len == 2U) {
out.data.push_back(tpdu[1] & 0x3f);
} else {
out.data.assign(tpdu + 2, tpdu + tpdu_len);
}
return out;
}
DaliBridgeRequest RequestForTarget(uint16_t group_address,
const GatewayKnxDaliTarget& target,
BridgeOperation operation) {
DaliBridgeRequest request;
request.sequence = "knx-" + GatewayKnxGroupAddressString(group_address);
request.operation = operation;
switch (target.kind) {
case GatewayKnxDaliTargetKind::kBroadcast:
request.metadata["broadcast"] = true;
break;
case GatewayKnxDaliTargetKind::kShortAddress:
request.shortAddress = target.address;
break;
case GatewayKnxDaliTargetKind::kGroup:
request.metadata["group"] = target.address;
break;
case GatewayKnxDaliTargetKind::kNone:
default:
break;
}
request.metadata["sourceProtocol"] = "knx";
request.metadata["knxGroupAddress"] = GatewayKnxGroupAddressString(group_address);
return request;
}
DaliBridgeResult ErrorResult(uint16_t group_address, const char* message) {
DaliBridgeResult result;
result.sequence = "knx-" + GatewayKnxGroupAddressString(group_address);
result.error = message == nullptr ? "KNX error" : message;
return result;
}
bool SendAll(int sock, const uint8_t* data, size_t len, const sockaddr_in& remote) {
return sendto(sock, data, len, 0, reinterpret_cast<const sockaddr*>(&remote),
sizeof(remote)) == static_cast<int>(len);
}
std::vector<uint8_t> KnxNetIpPacket(uint16_t service, const std::vector<uint8_t>& body) {
std::vector<uint8_t> packet(6 + body.size());
packet[0] = kKnxNetIpHeaderSize;
packet[1] = kKnxNetIpVersion10;
WriteBe16(packet.data() + 2, service);
WriteBe16(packet.data() + 4, static_cast<uint16_t>(packet.size()));
if (!body.empty()) {
std::memcpy(packet.data() + 6, body.data(), body.size());
}
return packet;
}
std::array<uint8_t, 8> HpaiForRemote(const sockaddr_in& remote) {
std::array<uint8_t, 8> hpai{};
hpai[0] = 0x08;
hpai[1] = 0x01;
const uint32_t address = ntohl(remote.sin_addr.s_addr);
hpai[2] = static_cast<uint8_t>((address >> 24) & 0xff);
hpai[3] = static_cast<uint8_t>((address >> 16) & 0xff);
hpai[4] = static_cast<uint8_t>((address >> 8) & 0xff);
hpai[5] = static_cast<uint8_t>(address & 0xff);
WriteBe16(hpai.data() + 6, ntohs(remote.sin_port));
return hpai;
}
bool ParseKnxNetIpHeader(const uint8_t* data, size_t len, uint16_t* service,
uint16_t* total_len) {
if (data == nullptr || len < 6 || data[0] != kKnxNetIpHeaderSize ||
data[1] != kKnxNetIpVersion10) {
return false;
}
*service = ReadBe16(data + 2);
*total_len = ReadBe16(data + 4);
return *total_len >= 6 && *total_len <= len;
}
bool IsExtendedTpFrame(const uint8_t* data, size_t len) {
return len > 0 && (data[0] & 0xD3) == 0x10;
}
size_t ExpectedTpFrameSize(const uint8_t* data, size_t len) {
if (data == nullptr || len < 6) {
return 0;
}
if (IsExtendedTpFrame(data, len)) {
return 9U + data[6];
}
return 8U + (data[5] & 0x0F);
}
bool ValidateTpChecksum(const uint8_t* data, size_t len) {
if (data == nullptr || len < 2) {
return false;
}
uint8_t crc = 0xFF;
for (size_t index = 0; index + 1 < len; ++index) {
crc ^= data[index];
}
return data[len - 1] == crc;
}
std::optional<std::vector<uint8_t>> CemiToTpTelegram(const uint8_t* data, size_t len) {
if (data == nullptr || len < 10 || data[1] != 0) {
return std::nullopt;
}
const uint8_t* ctrl = data + 2;
const bool standard = (ctrl[0] & 0x80) != 0;
const size_t tp_len = standard ? len - 2U : len - 1U;
if (tp_len < 8) {
return std::nullopt;
}
std::vector<uint8_t> telegram(tp_len, 0);
if (standard) {
telegram[0] = ctrl[0];
std::memcpy(telegram.data() + 1, ctrl + 2, 4);
telegram[5] = static_cast<uint8_t>((ctrl[1] & 0xF0) | (ctrl[6] & 0x0F));
if (tp_len > 7U) {
std::memcpy(telegram.data() + 6, ctrl + 7, tp_len - 7U);
}
} else {
std::memcpy(telegram.data(), ctrl, tp_len - 1U);
}
uint8_t crc = 0xFF;
for (size_t index = 0; index + 1 < telegram.size(); ++index) {
crc ^= telegram[index];
}
telegram.back() = crc;
return telegram;
}
std::optional<std::vector<uint8_t>> TpTelegramToCemi(const uint8_t* data, size_t len) {
if (data == nullptr || len < 8 || !ValidateTpChecksum(data, len)) {
return std::nullopt;
}
const bool extended = IsExtendedTpFrame(data, len);
const size_t cemi_len = len + (extended ? 2U : 3U) - 1U;
std::vector<uint8_t> cemi(cemi_len, 0);
cemi[0] = kCemiLDataInd;
cemi[1] = 0x00;
cemi[2] = data[0];
if (extended) {
std::memcpy(cemi.data() + 2, data, len - 1U);
} else {
cemi[3] = data[5] & 0xF0;
std::memcpy(cemi.data() + 4, data + 1, 4);
cemi[8] = data[5] & 0x0F;
const size_t copy_len = static_cast<size_t>(cemi[8]) + 1U;
if (9U + copy_len > cemi.size() || 6U + copy_len > len) {
return std::nullopt;
}
std::memcpy(cemi.data() + 9, data + 6, copy_len);
}
return cemi;
}
} // namespace
std::optional<GatewayKnxConfig> GatewayKnxConfigFromValue(const DaliValue* value) {
if (value == nullptr || value->asObject() == nullptr) {
return std::nullopt;
}
const auto& object = *value->asObject();
GatewayKnxConfig config;
config.dali_router_enabled = ObjectBoolAny(object, {"daliRouterEnabled", "dali_router_enabled"})
.value_or(config.dali_router_enabled);
config.ip_router_enabled = ObjectBoolAny(object, {"ipRouterEnabled", "ip_router_enabled"})
.value_or(config.ip_router_enabled);
config.tunnel_enabled = ObjectBoolAny(object, {"tunnelEnabled", "tunnel_enabled"})
.value_or(config.tunnel_enabled);
config.multicast_enabled = ObjectBoolAny(object, {"multicastEnabled", "multicast_enabled"})
.value_or(config.multicast_enabled);
config.main_group = static_cast<uint8_t>(
std::clamp(ObjectIntAny(object, {"mainGroup", "main_group"}).value_or(config.main_group),
0, 31));
config.udp_port = static_cast<uint16_t>(std::clamp(
ObjectIntAny(object, {"udpPort", "port", "udp_port"}).value_or(config.udp_port), 1,
65535));
config.multicast_address = ObjectStringAny(object, {"multicastAddress", "multicast_address"})
.value_or(config.multicast_address);
config.individual_address = static_cast<uint16_t>(std::clamp(
ObjectIntAny(object, {"individualAddress", "individual_address"})
.value_or(config.individual_address),
0, 0xffff));
const auto* tp_uart = getObjectValue(object, "tpUart");
if (tp_uart == nullptr) {
tp_uart = getObjectValue(object, "tp_uart");
}
if (tp_uart != nullptr && tp_uart->asObject() != nullptr) {
const auto& serial = *tp_uart->asObject();
config.tp_uart.uart_port = std::clamp(
ObjectIntAny(serial, {"uartPort", "uart_port"}).value_or(config.tp_uart.uart_port), 0,
2);
config.tp_uart.tx_pin = ObjectIntAny(serial, {"txPin", "tx_pin"}).value_or(config.tp_uart.tx_pin);
config.tp_uart.rx_pin = ObjectIntAny(serial, {"rxPin", "rx_pin"}).value_or(config.tp_uart.rx_pin);
config.tp_uart.baudrate = static_cast<uint32_t>(std::max(
1200, ObjectIntAny(serial, {"baudrate", "baud"}).value_or(config.tp_uart.baudrate)));
config.tp_uart.rx_buffer_size = static_cast<size_t>(std::max(
128, ObjectIntAny(serial, {"rxBufferSize", "rx_buffer_size"})
.value_or(static_cast<int>(config.tp_uart.rx_buffer_size))));
config.tp_uart.tx_buffer_size = static_cast<size_t>(std::max(
128, ObjectIntAny(serial, {"txBufferSize", "tx_buffer_size"})
.value_or(static_cast<int>(config.tp_uart.tx_buffer_size))));
config.tp_uart.read_timeout_ms = static_cast<uint32_t>(std::max(
1, ObjectIntAny(serial, {"readTimeoutMs", "read_timeout_ms"})
.value_or(static_cast<int>(config.tp_uart.read_timeout_ms))));
}
return config;
}
DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config) {
DaliValue::Object out;
out["daliRouterEnabled"] = config.dali_router_enabled;
out["ipRouterEnabled"] = config.ip_router_enabled;
out["tunnelEnabled"] = config.tunnel_enabled;
out["multicastEnabled"] = config.multicast_enabled;
out["mainGroup"] = static_cast<int>(config.main_group);
out["udpPort"] = static_cast<int>(config.udp_port);
out["multicastAddress"] = config.multicast_address;
out["individualAddress"] = static_cast<int>(config.individual_address);
DaliValue::Object serial;
serial["uartPort"] = config.tp_uart.uart_port;
serial["txPin"] = config.tp_uart.tx_pin;
serial["rxPin"] = config.tp_uart.rx_pin;
serial["baudrate"] = static_cast<int>(config.tp_uart.baudrate);
serial["rxBufferSize"] = static_cast<int>(config.tp_uart.rx_buffer_size);
serial["txBufferSize"] = static_cast<int>(config.tp_uart.tx_buffer_size);
serial["readTimeoutMs"] = static_cast<int>(config.tp_uart.read_timeout_ms);
out["tpUart"] = std::move(serial);
return DaliValue(std::move(out));
}
const char* GatewayKnxDataTypeToString(GatewayKnxDaliDataType data_type) {
switch (data_type) {
case GatewayKnxDaliDataType::kSwitch:
return "switch";
case GatewayKnxDaliDataType::kBrightness:
return "brightness";
case GatewayKnxDaliDataType::kColorTemperature:
return "color_temperature";
case GatewayKnxDaliDataType::kRgb:
return "rgb";
case GatewayKnxDaliDataType::kUnknown:
default:
return "unknown";
}
}
const char* GatewayKnxTargetKindToString(GatewayKnxDaliTargetKind kind) {
switch (kind) {
case GatewayKnxDaliTargetKind::kBroadcast:
return "broadcast";
case GatewayKnxDaliTargetKind::kShortAddress:
return "short_address";
case GatewayKnxDaliTargetKind::kGroup:
return "group";
case GatewayKnxDaliTargetKind::kNone:
default:
return "none";
}
}
std::optional<GatewayKnxDaliDataType> GatewayKnxDaliDataTypeForMiddleGroup(
uint8_t middle_group) {
switch (middle_group) {
case 1:
return GatewayKnxDaliDataType::kSwitch;
case 2:
return GatewayKnxDaliDataType::kBrightness;
case 3:
return GatewayKnxDaliDataType::kColorTemperature;
case 4:
return GatewayKnxDaliDataType::kRgb;
default:
return std::nullopt;
}
}
std::optional<GatewayKnxDaliTarget> GatewayKnxDaliTargetForSubgroup(uint8_t sub_group) {
if (sub_group == 0) {
return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kBroadcast, 127};
}
if (sub_group >= 1 && sub_group <= 64) {
return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kShortAddress,
static_cast<int>(sub_group - 1)};
}
if (sub_group >= 65 && sub_group <= 80) {
return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kGroup,
static_cast<int>(sub_group - 65)};
}
return std::nullopt;
}
uint16_t GatewayKnxGroupAddress(uint8_t main_group, uint8_t middle_group,
uint8_t sub_group) {
return static_cast<uint16_t>(((main_group & 0x1f) << 11) |
((middle_group & 0x07) << 8) | sub_group);
}
std::string GatewayKnxGroupAddressString(uint16_t group_address) {
const int main = (group_address >> 11) & 0x1f;
const int middle = (group_address >> 8) & 0x07;
const int sub = group_address & 0xff;
return std::to_string(main) + "/" + std::to_string(middle) + "/" +
std::to_string(sub);
}
GatewayKnxBridge::GatewayKnxBridge(DaliBridgeEngine& engine) : engine_(engine) {}
void GatewayKnxBridge::setConfig(const GatewayKnxConfig& config) { config_ = config; }
const GatewayKnxConfig& GatewayKnxBridge::config() const { return config_; }
std::vector<GatewayKnxDaliBinding> GatewayKnxBridge::describeDaliBindings() const {
std::vector<GatewayKnxDaliBinding> bindings;
bindings.reserve(4 * 81);
for (uint8_t middle = 1; middle <= 4; ++middle) {
const auto data_type = GatewayKnxDaliDataTypeForMiddleGroup(middle);
if (!data_type.has_value()) {
continue;
}
for (uint8_t sub = 0; sub <= 80; ++sub) {
const auto target = GatewayKnxDaliTargetForSubgroup(sub);
if (!target.has_value()) {
continue;
}
GatewayKnxDaliBinding binding;
binding.main_group = config_.main_group;
binding.middle_group = middle;
binding.sub_group = sub;
binding.group_address = GatewayKnxGroupAddress(config_.main_group, middle, sub);
binding.address = GatewayKnxGroupAddressString(binding.group_address);
binding.data_type = data_type.value();
binding.target = target.value();
binding.datapoint_type = DataTypeDpt(data_type.value());
binding.name = TargetName(target.value()) + " - " + DataTypeName(data_type.value());
bindings.push_back(std::move(binding));
}
}
return bindings;
}
DaliBridgeResult GatewayKnxBridge::handleCemiFrame(const uint8_t* data, size_t len) {
const auto decoded = DecodeCemiGroupWrite(data, len);
if (!decoded.has_value()) {
return ErrorResult(0, "unsupported or non group-write cEMI frame");
}
return handleGroupWrite(decoded->group_address, decoded->data.data(), decoded->data.size());
}
DaliBridgeResult GatewayKnxBridge::handleGroupWrite(uint16_t group_address, const uint8_t* data,
size_t len) {
if (!config_.dali_router_enabled) {
return ErrorResult(group_address, "KNX to DALI router disabled");
}
const uint8_t main = static_cast<uint8_t>((group_address >> 11) & 0x1f);
const uint8_t middle = static_cast<uint8_t>((group_address >> 8) & 0x07);
const uint8_t sub = static_cast<uint8_t>(group_address & 0xff);
if (main != config_.main_group) {
return ErrorResult(group_address, "KNX main group does not match gateway config");
}
const auto data_type = GatewayKnxDaliDataTypeForMiddleGroup(middle);
const auto target = GatewayKnxDaliTargetForSubgroup(sub);
if (!data_type.has_value() || !target.has_value()) {
return ErrorResult(group_address, "unmapped KNX group address");
}
return executeForDecodedWrite(group_address, data_type.value(), target.value(), data, len);
}
DaliBridgeResult GatewayKnxBridge::executeForDecodedWrite(uint16_t group_address,
GatewayKnxDaliDataType data_type,
GatewayKnxDaliTarget target,
const uint8_t* data, size_t len) {
if (target.kind == GatewayKnxDaliTargetKind::kNone) {
return ErrorResult(group_address, "missing DALI target");
}
switch (data_type) {
case GatewayKnxDaliDataType::kSwitch: {
if (data == nullptr || len < 1) {
return ErrorResult(group_address, "missing DPT1 switch payload");
}
DaliBridgeRequest request = RequestForTarget(
group_address, target, (data[0] & 0x01) != 0 ? BridgeOperation::on : BridgeOperation::off);
return engine_.execute(request);
}
case GatewayKnxDaliDataType::kBrightness: {
if (data == nullptr || len < 1) {
return ErrorResult(group_address, "missing DPT5 brightness payload");
}
DaliBridgeRequest request = RequestForTarget(group_address, target,
BridgeOperation::setBrightnessPercent);
request.value = (static_cast<double>(data[0]) * 100.0) / 255.0;
return engine_.execute(request);
}
case GatewayKnxDaliDataType::kColorTemperature: {
if (data == nullptr || len < 2) {
return ErrorResult(group_address, "missing DPT7 color temperature payload");
}
DaliBridgeRequest request = RequestForTarget(group_address, target,
BridgeOperation::setColorTemperature);
request.value = static_cast<int>(ReadBe16(data));
return engine_.execute(request);
}
case GatewayKnxDaliDataType::kRgb: {
if (data == nullptr || len < 3) {
return ErrorResult(group_address, "missing DPT232 RGB payload");
}
DaliBridgeRequest request = RequestForTarget(group_address, target,
BridgeOperation::setColourRGB);
DaliValue::Object rgb;
rgb["r"] = static_cast<int>(data[0]);
rgb["g"] = static_cast<int>(data[1]);
rgb["b"] = static_cast<int>(data[2]);
request.value = std::move(rgb);
return engine_.execute(request);
}
case GatewayKnxDaliDataType::kUnknown:
default:
return ErrorResult(group_address, "unsupported KNX data type");
}
}
GatewayKnxTpIpRouter::GatewayKnxTpIpRouter(GatewayKnxBridge& bridge, CemiFrameHandler handler)
: bridge_(bridge), handler_(std::move(handler)) {}
GatewayKnxTpIpRouter::~GatewayKnxTpIpRouter() { stop(); }
void GatewayKnxTpIpRouter::setConfig(const GatewayKnxConfig& config) { config_ = config; }
const GatewayKnxConfig& GatewayKnxTpIpRouter::config() const { return config_; }
esp_err_t GatewayKnxTpIpRouter::start(uint32_t task_stack_size, UBaseType_t task_priority) {
if (started_ || task_handle_ != nullptr) {
return ESP_OK;
}
if (!config_.ip_router_enabled) {
return ESP_ERR_NOT_SUPPORTED;
}
stop_requested_ = false;
last_error_.clear();
const BaseType_t created = xTaskCreate(&GatewayKnxTpIpRouter::TaskEntry, "gw_knx_ip",
task_stack_size, this, task_priority, &task_handle_);
if (created != pdPASS) {
task_handle_ = nullptr;
return ESP_ERR_NO_MEM;
}
started_ = true;
return ESP_OK;
}
esp_err_t GatewayKnxTpIpRouter::stop() {
stop_requested_ = true;
closeSockets();
return ESP_OK;
}
bool GatewayKnxTpIpRouter::started() const { return started_; }
const std::string& GatewayKnxTpIpRouter::lastError() const { return last_error_; }
void GatewayKnxTpIpRouter::TaskEntry(void* arg) {
static_cast<GatewayKnxTpIpRouter*>(arg)->taskLoop();
}
void GatewayKnxTpIpRouter::taskLoop() {
if (!configureSocket()) {
finishTask();
return;
}
configureTpUart();
std::array<uint8_t, 768> buffer{};
while (!stop_requested_) {
sockaddr_in remote{};
socklen_t remote_len = sizeof(remote);
const int received = recvfrom(udp_sock_, buffer.data(), buffer.size(), 0,
reinterpret_cast<sockaddr*>(&remote), &remote_len);
if (received <= 0) {
pollTpUart();
if (!stop_requested_) {
vTaskDelay(pdMS_TO_TICKS(10));
}
continue;
}
handleUdpDatagram(buffer.data(), static_cast<size_t>(received), remote);
pollTpUart();
}
finishTask();
}
void GatewayKnxTpIpRouter::finishTask() {
closeSockets();
started_ = false;
task_handle_ = nullptr;
vTaskDelete(nullptr);
}
void GatewayKnxTpIpRouter::closeSockets() {
if (udp_sock_ >= 0) {
shutdown(udp_sock_, SHUT_RDWR);
close(udp_sock_);
udp_sock_ = -1;
}
if (tp_uart_port_ >= 0) {
uart_driver_delete(static_cast<uart_port_t>(tp_uart_port_));
tp_uart_port_ = -1;
}
}
bool GatewayKnxTpIpRouter::configureSocket() {
udp_sock_ = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
if (udp_sock_ < 0) {
last_error_ = "failed to create KNXnet/IP UDP socket";
return false;
}
int reuse = 1;
setsockopt(udp_sock_, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
sockaddr_in bind_addr{};
bind_addr.sin_family = AF_INET;
bind_addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind_addr.sin_port = htons(config_.udp_port);
if (bind(udp_sock_, reinterpret_cast<sockaddr*>(&bind_addr), sizeof(bind_addr)) < 0) {
last_error_ = "failed to bind KNXnet/IP UDP socket";
closeSockets();
return false;
}
timeval timeout{};
timeout.tv_sec = 0;
timeout.tv_usec = 20000;
setsockopt(udp_sock_, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
if (config_.multicast_enabled) {
uint8_t multicast_loop = 0;
setsockopt(udp_sock_, IPPROTO_IP, IP_MULTICAST_LOOP, &multicast_loop,
sizeof(multicast_loop));
ip_mreq mreq{};
mreq.imr_multiaddr.s_addr = inet_addr(config_.multicast_address.c_str());
mreq.imr_interface.s_addr = htonl(INADDR_ANY);
if (setsockopt(udp_sock_, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) {
ESP_LOGW(kTag, "failed to join KNX multicast group %s", config_.multicast_address.c_str());
}
}
return true;
}
bool GatewayKnxTpIpRouter::configureTpUart() {
const auto& serial = config_.tp_uart;
if (serial.uart_port < 0 || serial.uart_port > 2) {
return false;
}
uart_config_t uart_config{};
uart_config.baud_rate = static_cast<int>(serial.baudrate);
uart_config.data_bits = UART_DATA_8_BITS;
uart_config.parity = UART_PARITY_EVEN;
uart_config.stop_bits = UART_STOP_BITS_1;
uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
uart_config.source_clk = UART_SCLK_DEFAULT;
const uart_port_t uart_port = static_cast<uart_port_t>(serial.uart_port);
if (uart_param_config(uart_port, &uart_config) != ESP_OK) {
return false;
}
if (uart_set_pin(uart_port, serial.tx_pin, serial.rx_pin, UART_PIN_NO_CHANGE,
UART_PIN_NO_CHANGE) != ESP_OK) {
return false;
}
if (uart_driver_install(uart_port, serial.rx_buffer_size, serial.tx_buffer_size, 0, nullptr,
0) != ESP_OK) {
return false;
}
tp_uart_port_ = serial.uart_port;
return true;
}
void GatewayKnxTpIpRouter::handleUdpDatagram(const uint8_t* data, size_t len,
const sockaddr_in& remote) {
uint16_t service = 0;
uint16_t total_len = 0;
if (!ParseKnxNetIpHeader(data, len, &service, &total_len)) {
return;
}
const uint8_t* body = data + 6;
const size_t body_len = total_len - 6;
switch (service) {
case kServiceRoutingIndication:
if (config_.multicast_enabled) {
handleRoutingIndication(body, body_len);
}
break;
case kServiceTunnellingRequest:
if (config_.tunnel_enabled) {
handleTunnellingRequest(body, body_len, remote);
}
break;
case kServiceConnectRequest:
if (config_.tunnel_enabled) {
handleConnectRequest(body, body_len, remote);
}
break;
case kServiceConnectionStateRequest:
handleConnectionStateRequest(body, body_len, remote);
break;
case kServiceDisconnectRequest:
handleDisconnectRequest(body, body_len, remote);
break;
default:
break;
}
}
void GatewayKnxTpIpRouter::handleRoutingIndication(const uint8_t* body, size_t len) {
if (body == nullptr || len == 0) {
return;
}
const DaliBridgeResult result = handler_(body, len);
if (!result.ok && !result.error.empty()) {
ESP_LOGD(kTag, "KNX routing indication ignored: %s", result.error.c_str());
}
forwardCemiToTp(body, len);
}
void GatewayKnxTpIpRouter::handleTunnellingRequest(const uint8_t* body, size_t len,
const sockaddr_in& remote) {
if (body == nullptr || len < 5 || body[0] != 0x04) {
return;
}
const uint8_t channel_id = body[1];
const uint8_t sequence = body[2];
if (!tunnel_connected_ || channel_id != tunnel_channel_id_) {
sendTunnellingAck(channel_id, sequence, kKnxErrorConnectionId, remote);
return;
}
if (sequence != expected_tunnel_sequence_) {
sendTunnellingAck(channel_id, sequence, kKnxErrorSequenceNumber, remote);
return;
}
expected_tunnel_sequence_ = static_cast<uint8_t>((expected_tunnel_sequence_ + 1) & 0xff);
sendTunnellingAck(channel_id, sequence, kKnxNoError, remote);
const uint8_t* cemi = body + 4;
const size_t cemi_len = len - 4;
const DaliBridgeResult result = handler_(cemi, cemi_len);
if (!result.ok && !result.error.empty()) {
ESP_LOGD(kTag, "KNX tunnel frame not routed to DALI: %s", result.error.c_str());
}
forwardCemiToTp(cemi, cemi_len);
}
void GatewayKnxTpIpRouter::handleConnectRequest(const uint8_t* body, size_t len,
const sockaddr_in& remote) {
if (body == nullptr || len < 20) {
return;
}
const size_t cri_offset = 16;
if (body[cri_offset] < 4 || body[cri_offset + 1] != kKnxConnectionTypeTunnel ||
body[cri_offset + 2] != kKnxTunnelLayerLink) {
sendConnectResponse(0, kKnxErrorConnectionType, remote);
return;
}
if (tunnel_connected_) {
sendConnectResponse(0, kKnxErrorNoMoreConnections, remote);
return;
}
tunnel_connected_ = true;
expected_tunnel_sequence_ = 0;
tunnel_send_sequence_ = 0;
tunnel_remote_ = remote;
sendConnectResponse(tunnel_channel_id_, kKnxNoError, remote);
}
void GatewayKnxTpIpRouter::handleConnectionStateRequest(const uint8_t* body, size_t len,
const sockaddr_in& remote) {
if (body == nullptr || len < 2) {
return;
}
const uint8_t channel_id = body[0];
sendConnectionStateResponse(
channel_id, tunnel_connected_ && channel_id == tunnel_channel_id_ ? kKnxNoError
: kKnxErrorConnectionId,
remote);
}
void GatewayKnxTpIpRouter::handleDisconnectRequest(const uint8_t* body, size_t len,
const sockaddr_in& remote) {
if (body == nullptr || len < 2) {
return;
}
const uint8_t channel_id = body[0];
const uint8_t status = tunnel_connected_ && channel_id == tunnel_channel_id_
? kKnxNoError
: kKnxErrorConnectionId;
if (status == kKnxNoError) {
tunnel_connected_ = false;
expected_tunnel_sequence_ = 0;
tunnel_send_sequence_ = 0;
}
sendDisconnectResponse(channel_id, status, remote);
}
void GatewayKnxTpIpRouter::sendTunnellingAck(uint8_t channel_id, uint8_t sequence,
uint8_t status, const sockaddr_in& remote) {
const std::vector<uint8_t> body{0x04, channel_id, sequence, status};
const auto packet = KnxNetIpPacket(kServiceTunnellingAck, body);
SendAll(udp_sock_, packet.data(), packet.size(), remote);
}
void GatewayKnxTpIpRouter::sendTunnelIndication(const uint8_t* data, size_t len) {
if (!tunnel_connected_ || udp_sock_ < 0 || data == nullptr || len == 0) {
return;
}
std::vector<uint8_t> body;
body.reserve(4 + len);
body.push_back(0x04);
body.push_back(tunnel_channel_id_);
body.push_back(tunnel_send_sequence_++);
body.push_back(0x00);
body.insert(body.end(), data, data + len);
const auto packet = KnxNetIpPacket(kServiceTunnellingRequest, body);
SendAll(udp_sock_, packet.data(), packet.size(), tunnel_remote_);
}
void GatewayKnxTpIpRouter::sendConnectionStateResponse(uint8_t channel_id, uint8_t status,
const sockaddr_in& remote) {
const std::vector<uint8_t> body{channel_id, status};
const auto packet = KnxNetIpPacket(kServiceConnectionStateResponse, body);
SendAll(udp_sock_, packet.data(), packet.size(), remote);
}
void GatewayKnxTpIpRouter::sendDisconnectResponse(uint8_t channel_id, uint8_t status,
const sockaddr_in& remote) {
const std::vector<uint8_t> body{channel_id, status};
const auto packet = KnxNetIpPacket(kServiceDisconnectResponse, body);
SendAll(udp_sock_, packet.data(), packet.size(), remote);
}
void GatewayKnxTpIpRouter::sendConnectResponse(uint8_t channel_id, uint8_t status,
const sockaddr_in& remote) {
std::vector<uint8_t> body;
body.reserve(16);
body.push_back(channel_id);
body.push_back(status);
const auto data_endpoint = HpaiForRemote(remote);
body.insert(body.end(), data_endpoint.begin(), data_endpoint.end());
body.push_back(0x04);
body.push_back(kKnxConnectionTypeTunnel);
body.push_back(static_cast<uint8_t>((config_.individual_address >> 8) & 0xff));
body.push_back(static_cast<uint8_t>(config_.individual_address & 0xff));
const auto packet = KnxNetIpPacket(kServiceConnectResponse, body);
SendAll(udp_sock_, packet.data(), packet.size(), remote);
}
void GatewayKnxTpIpRouter::sendRoutingIndication(const uint8_t* data, size_t len) {
if (!config_.multicast_enabled || udp_sock_ < 0 || data == nullptr || len == 0) {
return;
}
sockaddr_in remote{};
remote.sin_family = AF_INET;
remote.sin_port = htons(config_.udp_port);
remote.sin_addr.s_addr = inet_addr(config_.multicast_address.c_str());
const std::vector<uint8_t> body(data, data + len);
const auto packet = KnxNetIpPacket(kServiceRoutingIndication, body);
SendAll(udp_sock_, packet.data(), packet.size(), remote);
}
void GatewayKnxTpIpRouter::pollTpUart() {
if (tp_uart_port_ < 0) {
return;
}
std::array<uint8_t, 128> buffer{};
const int read = uart_read_bytes(static_cast<uart_port_t>(tp_uart_port_), buffer.data(),
buffer.size(), 0);
if (read <= 0) {
return;
}
for (int index = 0; index < read; ++index) {
tp_rx_frame_.push_back(buffer[index]);
const size_t expected = ExpectedTpFrameSize(tp_rx_frame_.data(), tp_rx_frame_.size());
if (expected == 0) {
continue;
}
if (tp_rx_frame_.size() == expected) {
handleTpTelegram(tp_rx_frame_.data(), tp_rx_frame_.size());
tp_rx_frame_.clear();
} else if (tp_rx_frame_.size() > expected || tp_rx_frame_.size() > 263U) {
tp_rx_frame_.clear();
}
}
}
void GatewayKnxTpIpRouter::handleTpTelegram(const uint8_t* data, size_t len) {
const auto cemi = TpTelegramToCemi(data, len);
if (!cemi.has_value()) {
return;
}
const DaliBridgeResult result = handler_(cemi->data(), cemi->size());
if (!result.ok && !result.error.empty()) {
ESP_LOGD(kTag, "KNX TP frame not routed to DALI: %s", result.error.c_str());
}
sendTunnelIndication(cemi->data(), cemi->size());
sendRoutingIndication(cemi->data(), cemi->size());
}
void GatewayKnxTpIpRouter::forwardCemiToTp(const uint8_t* data, size_t len) {
if (tp_uart_port_ < 0 || data == nullptr || len == 0) {
return;
}
const auto telegram = CemiToTpTelegram(data, len);
if (!telegram.has_value()) {
return;
}
uart_write_bytes(static_cast<uart_port_t>(tp_uart_port_), telegram->data(), telegram->size());
}
} // namespace gateway