feat(gateway): enhance DALI and KNX integration with tunnel confirmation handling and improved message routing

Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
Tony
2026-05-16 09:34:14 +08:00
parent 82142dd46c
commit 77fe8c1b02
5 changed files with 246 additions and 47 deletions
+6 -5
View File
@@ -1,6 +1,7 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x6000,
otadata, data, ota, 0xf000, 0x2000,
phy_init, data, phy, 0x11000, 0x1000,
factory, app, factory, 0x20000, 0x280000,
storage, data, spiffs, 0x300000, 0x100000,
nvs, data, nvs, 0x9000, 0x14000,
otadata, data, ota, 0x1D000, 0x2000,
phy_init, data, phy, 0x1F000, 0x1000,
factory, app, factory, 0x20000, 0x300000,
knxprops, data, 0x40, 0x320000, 0x60000,
storage, data, spiffs, 0x380000, 0x80000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x6000 0x14000
3 otadata data ota 0xf000 0x1D000 0x2000
4 phy_init data phy 0x11000 0x1F000 0x1000
5 factory app factory 0x20000 0x280000 0x300000
6 storage knxprops data spiffs 0x40 0x300000 0x320000 0x100000 0x60000
7 storage data spiffs 0x380000 0x80000
@@ -321,6 +321,9 @@ std::optional<GatewayKnxDaliTarget> DecodeKnxDaliTarget(uint8_t raw_addr) {
return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kGroup,
static_cast<int>((raw_addr - kDaliGroupRawMin) >> 1)};
}
if (raw_addr == 0xFE || raw_addr == 0xFF) {
return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kBroadcast, 127};
}
return std::nullopt;
}
@@ -4085,7 +4088,36 @@ void GatewayBridgeService::handleDaliRawFrame(const DaliRawFrame& frame) {
if (!owner->knx_started || owner->knx_router == nullptr) {
return;
}
owner->knx_router->publishDaliStatus(update->target, update->actual_level);
auto publish_target = [&](GatewayKnxDaliTargetKind kind, int address) {
owner->knx_router->publishDaliStatus(GatewayKnxDaliTarget{kind, address},
update->actual_level);
};
publish_target(update->target.kind, update->target.address);
if (update->target.kind == GatewayKnxDaliTargetKind::kGroup &&
update->target.address >= 0 && update->target.address < 16) {
const uint16_t group_bit = static_cast<uint16_t>(1U << update->target.address);
for (int short_address = 0; short_address <= kMaxDaliShortAddress; ++short_address) {
const auto state = owner->cache.daliAddressState(frame.gateway_id,
static_cast<uint8_t>(short_address));
if (!state.group_mask_known || (state.group_mask & group_bit) == 0) {
continue;
}
publish_target(GatewayKnxDaliTargetKind::kShortAddress, short_address);
}
return;
}
if (update->target.kind == GatewayKnxDaliTargetKind::kBroadcast) {
for (int group = 0; group < 16; ++group) {
publish_target(GatewayKnxDaliTargetKind::kGroup, group);
}
for (int short_address = 0; short_address <= kMaxDaliShortAddress; ++short_address) {
publish_target(GatewayKnxDaliTargetKind::kShortAddress, short_address);
}
}
}
void GatewayBridgeService::collectUsedRuntimeResources(
@@ -252,11 +252,14 @@ class GatewayKnxTpIpRouter {
uint8_t connection_type{0};
uint8_t received_sequence{255};
uint8_t send_sequence{0};
uint8_t last_tunnel_confirmation_sequence{0};
uint16_t individual_address{0};
int tcp_sock{-1};
TickType_t last_activity_tick{0};
::sockaddr_in control_remote{};
::sockaddr_in data_remote{};
std::vector<uint8_t> last_received_cemi;
std::vector<uint8_t> last_tunnel_confirmation_packet;
};
static void TaskEntry(void* arg);
@@ -297,7 +300,7 @@ class GatewayKnxTpIpRouter {
void sendSecureSessionStatus(uint8_t status, const ::sockaddr_in& remote);
void sendTunnelIndication(const uint8_t* data, size_t len);
void sendTunnelIndicationToClient(TunnelClient& client, const uint8_t* data, size_t len);
void sendCemiFrameToClient(TunnelClient& client, uint16_t service,
bool sendCemiFrameToClient(TunnelClient& client, uint16_t service,
const uint8_t* data, size_t len);
void sendConnectionStateResponse(uint8_t channel_id, uint8_t status,
const ::sockaddr_in& remote);
+202 -39
View File
@@ -718,6 +718,44 @@ std::string HexBytes(const uint8_t* data, size_t len) {
return out;
}
std::optional<MessageCode> CemiMessageCode(const uint8_t* data, size_t len) {
if (data == nullptr || len == 0) {
return std::nullopt;
}
return static_cast<MessageCode>(data[0]);
}
uint16_t KnxIpServiceForCemi(const uint8_t* data, size_t len, uint16_t fallback_service) {
const auto message_code = CemiMessageCode(data, len);
if (!message_code.has_value()) {
return fallback_service;
}
switch (message_code.value()) {
case L_data_req:
case L_data_con:
case L_data_ind:
return kServiceTunnellingRequest;
default:
return kServiceDeviceConfigurationRequest;
}
}
bool BuildTunnelConfirmationFrame(const uint8_t* data, size_t len,
std::vector<uint8_t>* confirmation) {
if (data == nullptr || confirmation == nullptr || len < 2) {
return false;
}
std::vector<uint8_t> frame_data(data, data + len);
CemiFrame frame(frame_data.data(), static_cast<uint16_t>(frame_data.size()));
if (!frame.valid() || frame.messageCode() != L_data_req) {
return false;
}
frame.messageCode(L_data_con);
frame.confirm(ConfirmNoError);
*confirmation = std::move(frame_data);
return true;
}
DaliBridgeRequest RequestForTarget(uint16_t group_address,
const GatewayKnxDaliTarget& target,
BridgeOperation operation) {
@@ -2327,6 +2365,9 @@ bool GatewayKnxTpIpRouter::publishDaliStatus(const GatewayKnxDaliTarget& target,
kGwReg1GrpKoBlockSize * static_cast<uint16_t>(target.address);
switch_object = base + kGwReg1KoSwitchState;
dimm_object = base + kGwReg1KoDimmState;
} else if (target.kind == GatewayKnxDaliTargetKind::kBroadcast) {
switch_object = kGwReg1AppKoBroadcastSwitch;
dimm_object = kGwReg1AppKoBroadcastDimm;
} else {
return false;
}
@@ -2982,6 +3023,15 @@ void GatewayKnxTpIpRouter::handleUdpDatagram(const uint8_t* data, size_t len,
static_cast<unsigned>(service), static_cast<unsigned>(body[1]),
static_cast<unsigned>(body[2]), static_cast<unsigned>(body[3]),
EndpointString(remote).c_str());
TunnelClient* client = findTunnelClient(body[1]);
if (client != nullptr) {
client->last_activity_tick = xTaskGetTickCount();
if (service == kServiceTunnellingAck && body[3] == kKnxNoError &&
!client->last_tunnel_confirmation_packet.empty() &&
client->last_tunnel_confirmation_sequence == body[2]) {
client->last_tunnel_confirmation_packet.clear();
}
}
}
break;
case kServiceRoutingIndication:
@@ -3123,6 +3173,22 @@ void GatewayKnxTpIpRouter::handleRoutingIndication(const uint8_t* packet_data, s
}
const uint8_t* cemi = frame.data();
const size_t cemi_len = frame.dataLength();
bool consumed_by_local_application = false;
if (frame.messageCode() == L_data_req && frame.addressType() == IndividualAddress &&
ets_device_ != nullptr) {
const uint16_t dest = frame.destinationAddress();
const uint16_t own_address = ets_device_->individualAddress();
const uint16_t client_address = ets_device_->tunnelClientAddress();
const bool commissioning = !ets_device_->configured() || ets_device_->programmingMode();
if (dest == own_address || dest == client_address ||
(commissioning && dest == 0xffff)) {
consumed_by_local_application =
handleOpenKnxTunnelFrame(cemi, cemi_len, nullptr, kServiceRoutingIndication);
}
}
if (consumed_by_local_application) {
return;
}
const bool consumed_by_openknx = handleOpenKnxBusFrame(cemi, cemi_len);
const bool routed_to_dali = routeOpenKnxGroupWrite(cemi, cemi_len, "KNX routing indication");
const bool sent_to_tp = transmitOpenKnxTpFrame(cemi, cemi_len);
@@ -3284,23 +3350,6 @@ void GatewayKnxTpIpRouter::handleTunnellingRequest(const uint8_t* packet_data, s
sendTunnellingAck(channel_id, sequence, kKnxErrorConnectionId, remote);
return;
}
if (sequence == client->received_sequence) {
ESP_LOGD(kTag, "duplicate KNXnet/IP tunnelling request channel=%u seq=%u",
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence));
sendTunnellingAck(channel_id, sequence, kKnxNoError, client->data_remote);
return;
}
if (static_cast<uint8_t>(sequence - 1) != client->received_sequence) {
ESP_LOGW(kTag, "reject KNXnet/IP tunnelling request channel=%u seq=%u expected=%u from %s",
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence),
static_cast<unsigned>(static_cast<uint8_t>(client->received_sequence + 1)),
EndpointString(remote).c_str());
sendTunnellingAck(channel_id, sequence, kKnxErrorSequenceNumber, remote);
return;
}
client->received_sequence = sequence;
client->last_activity_tick = xTaskGetTickCount();
sendTunnellingAck(channel_id, sequence, kKnxNoError, client->data_remote);
CemiFrame& frame = tunneling.frame();
if (!frame.valid()) {
ESP_LOGW(kTag, "invalid OpenKNX tunnel cEMI channel=%u seq=%u from %s",
@@ -3313,6 +3362,46 @@ void GatewayKnxTpIpRouter::handleTunnellingRequest(const uint8_t* packet_data, s
}
const uint8_t* cemi = frame.data();
const size_t cemi_len = frame.dataLength();
const std::vector<uint8_t> current_cemi(cemi, cemi + cemi_len);
const bool duplicate_sequence = sequence == client->received_sequence;
const bool duplicate_payload = duplicate_sequence && client->last_received_cemi == current_cemi;
if (duplicate_payload) {
ESP_LOGD(kTag, "duplicate KNXnet/IP tunnelling request channel=%u seq=%u",
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence));
sendTunnellingAck(channel_id, sequence, kKnxNoError, client->data_remote);
if (!client->last_tunnel_confirmation_packet.empty()) {
if (sendPacketToTunnelClient(*client, client->last_tunnel_confirmation_packet)) {
ESP_LOGI(kTag,
"resent cached KNXnet/IP tunnel confirmation channel=%u confirmSeq=%u after duplicate req seq=%u to %s",
static_cast<unsigned>(channel_id),
static_cast<unsigned>(client->last_tunnel_confirmation_sequence),
static_cast<unsigned>(sequence), EndpointString(client->data_remote).c_str());
} else {
ESP_LOGW(kTag,
"failed to resend cached KNXnet/IP tunnel confirmation channel=%u confirmSeq=%u to %s",
static_cast<unsigned>(channel_id),
static_cast<unsigned>(client->last_tunnel_confirmation_sequence),
EndpointString(client->data_remote).c_str());
}
}
return;
}
if (duplicate_sequence) {
ESP_LOGW(kTag,
"accept KNXnet/IP tunnelling request channel=%u with repeated seq=%u because cEMI payload changed",
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence));
} else if (static_cast<uint8_t>(sequence - 1) != client->received_sequence) {
ESP_LOGW(kTag, "reject KNXnet/IP tunnelling request channel=%u seq=%u expected=%u from %s",
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence),
static_cast<unsigned>(static_cast<uint8_t>(client->received_sequence + 1)),
EndpointString(remote).c_str());
sendTunnellingAck(channel_id, sequence, kKnxErrorSequenceNumber, remote);
return;
}
client->received_sequence = sequence;
client->last_received_cemi = current_cemi;
client->last_activity_tick = xTaskGetTickCount();
sendTunnellingAck(channel_id, sequence, kKnxNoError, client->data_remote);
ESP_LOGI(kTag, "rx KNXnet/IP tunnelling request channel=%u seq=%u cemiLen=%u from %s",
static_cast<unsigned>(channel_id), static_cast<unsigned>(sequence),
static_cast<unsigned>(cemi_len), EndpointString(remote).c_str());
@@ -3320,6 +3409,13 @@ void GatewayKnxTpIpRouter::handleTunnellingRequest(const uint8_t* packet_data, s
const bool consumed_by_openknx = handleOpenKnxTunnelFrame(
cemi, cemi_len, client, kServiceTunnellingRequest);
const bool routed_to_dali = routeOpenKnxGroupWrite(cemi, cemi_len, "KNX tunnel frame");
if (!consumed_by_openknx && routed_to_dali) {
std::vector<uint8_t> tunnel_confirmation;
if (BuildTunnelConfirmationFrame(cemi, cemi_len, &tunnel_confirmation)) {
sendCemiFrameToClient(*client, kServiceTunnellingRequest, tunnel_confirmation.data(),
tunnel_confirmation.size());
}
}
if (consumed_by_openknx || routed_to_dali) {
return;
}
@@ -3604,13 +3700,16 @@ bool GatewayKnxTpIpRouter::currentTransportAllowsTcpHpai() const {
std::optional<std::array<uint8_t, 8>> GatewayKnxTpIpRouter::localHpaiForRemote(
const sockaddr_in& remote, bool tcp) const {
std::array<uint8_t, 8> hpai{};
hpai[0] = 0x08;
hpai[1] = tcp ? kKnxHpaiIpv4Tcp : kKnxHpaiIpv4Udp;
if (tcp) {
return hpai;
}
const auto netif = SelectKnxNetifForRemote(remote);
if (!netif.has_value()) {
return std::nullopt;
}
std::array<uint8_t, 8> hpai{};
hpai[0] = 0x08;
hpai[1] = tcp ? kKnxHpaiIpv4Tcp : kKnxHpaiIpv4Udp;
WriteIp(hpai.data() + 2, netif->address);
WriteBe16(hpai.data() + 6, config_.udp_port);
return hpai;
@@ -3779,10 +3878,10 @@ void GatewayKnxTpIpRouter::sendTunnelIndicationToClient(TunnelClient& client, co
sendCemiFrameToClient(client, kServiceTunnellingRequest, data, len);
}
void GatewayKnxTpIpRouter::sendCemiFrameToClient(TunnelClient& client, uint16_t service,
bool GatewayKnxTpIpRouter::sendCemiFrameToClient(TunnelClient& client, uint16_t service,
const uint8_t* data, size_t len) {
if (!client.connected || data == nullptr || len == 0) {
return;
return false;
}
std::vector<uint8_t> frame_data(data, data + len);
CemiFrame frame(frame_data.data(), static_cast<uint16_t>(frame_data.size()));
@@ -3790,20 +3889,34 @@ void GatewayKnxTpIpRouter::sendCemiFrameToClient(TunnelClient& client, uint16_t
ESP_LOGW(kTag, "not sending invalid OpenKNX cEMI service=0x%04x len=%u to %s",
static_cast<unsigned>(service), static_cast<unsigned>(len),
EndpointString(client.data_remote).c_str());
return;
return false;
}
KnxIpTunnelingRequest request(frame);
request.serviceTypeIdentifier(service);
request.connectionHeader().length(LEN_CH);
request.connectionHeader().channelId(client.channel_id);
request.connectionHeader().sequenceCounter(client.send_sequence++);
const auto message_code = CemiMessageCode(data, len);
const uint8_t send_sequence = client.send_sequence++;
request.connectionHeader().sequenceCounter(send_sequence);
request.connectionHeader().status(kKnxNoError);
const std::vector<uint8_t> packet(request.data(), request.data() + request.totalLength());
sendPacketToTunnelClient(client, packet);
if (!sendPacketToTunnelClient(client, packet)) {
ESP_LOGW(kTag, "failed to send KNXnet/IP cEMI service=0x%04x channel=%u seq=%u to %s",
static_cast<unsigned>(service), static_cast<unsigned>(client.channel_id),
static_cast<unsigned>(request.connectionHeader().sequenceCounter()),
EndpointString(client.data_remote).c_str());
return false;
}
if (service == kServiceTunnellingRequest && message_code.has_value() &&
message_code.value() == L_data_con) {
client.last_tunnel_confirmation_sequence = send_sequence;
client.last_tunnel_confirmation_packet = packet;
}
ESP_LOGI(kTag, "sent KNXnet/IP cEMI service=0x%04x channel=%u seq=%u cemi=0x%02x len=%u to %s",
static_cast<unsigned>(service), static_cast<unsigned>(client.channel_id),
static_cast<unsigned>(request.connectionHeader().sequenceCounter()), static_cast<unsigned>(data[0]),
static_cast<unsigned>(len), EndpointString(client.data_remote).c_str());
return true;
}
void GatewayKnxTpIpRouter::sendConnectionStateResponse(uint8_t channel_id, uint8_t status,
@@ -3844,18 +3957,26 @@ void GatewayKnxTpIpRouter::sendConnectResponse(uint8_t channel_id, uint8_t statu
}
KnxIpConnectResponse response(*knx_ip_parameters_, tunnel_address, config_.udp_port,
channel_id, connection_type);
const bool tcp = currentTransportAllowsTcpHpai();
const uint32_t endpoint_address = ntohl(netif->address);
response.controlEndpoint().code(currentTransportAllowsTcpHpai() ? IPV4_TCP : IPV4_UDP);
response.controlEndpoint().ipAddress(endpoint_address);
response.controlEndpoint().ipPortNumber(config_.udp_port);
response.controlEndpoint().code(tcp ? IPV4_TCP : IPV4_UDP);
response.controlEndpoint().ipAddress(tcp ? 0 : endpoint_address);
response.controlEndpoint().ipPortNumber(tcp ? 0 : config_.udp_port);
const std::vector<uint8_t> packet(response.data(), response.data() + response.totalLength());
sendPacket(packet, remote);
ESP_LOGI(kTag, "sent KNXnet/IP connect response channel=%u type=0x%02x to %s endpoint=%u.%u.%u.%u:%u",
std::string endpoint_string;
if (tcp) {
endpoint_string = "0.0.0.0:0 (TCP HPAI)";
} else {
sockaddr_in local_endpoint{};
local_endpoint.sin_family = AF_INET;
local_endpoint.sin_port = htons(config_.udp_port);
local_endpoint.sin_addr.s_addr = netif->address;
endpoint_string = EndpointString(local_endpoint);
}
ESP_LOGI(kTag, "sent KNXnet/IP connect response channel=%u type=0x%02x to %s endpoint=%s",
static_cast<unsigned>(channel_id), static_cast<unsigned>(connection_type),
EndpointString(remote).c_str(), static_cast<unsigned>((endpoint_address >> 24) & 0xff),
static_cast<unsigned>((endpoint_address >> 16) & 0xff),
static_cast<unsigned>((endpoint_address >> 8) & 0xff),
static_cast<unsigned>(endpoint_address & 0xff), static_cast<unsigned>(config_.udp_port));
EndpointString(remote).c_str(), endpoint_string.c_str());
}
void GatewayKnxTpIpRouter::sendRoutingIndication(const uint8_t* data, size_t len) {
@@ -3908,15 +4029,57 @@ bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t
if (ets_device_ == nullptr) {
return false;
}
std::vector<uint8_t> tunnel_confirmation;
const bool needs_tunnel_confirmation =
response_client != nullptr && response_client->connected &&
response_service == kServiceTunnellingRequest &&
BuildTunnelConfirmationFrame(data, len, &tunnel_confirmation);
bool sent_tunnel_confirmation = false;
const bool consumed = ets_device_->handleTunnelFrame(
data, len, [this, response_client, response_service](const uint8_t* response,
size_t response_len) {
if (response_client != nullptr && response_client->connected) {
sendCemiFrameToClient(*response_client, response_service, response, response_len);
} else {
sendTunnelIndication(response, response_len);
data, len,
[this, response_client, response_service, needs_tunnel_confirmation,
&tunnel_confirmation, &sent_tunnel_confirmation](const uint8_t* response,
size_t response_len) {
if (response == nullptr || response_len == 0) {
return;
}
const bool routing_context =
response_client == nullptr && response_service == kServiceRoutingIndication;
const auto message_code = CemiMessageCode(response, response_len);
if (needs_tunnel_confirmation && !sent_tunnel_confirmation &&
message_code.has_value() && message_code.value() != L_data_con) {
sent_tunnel_confirmation = sendCemiFrameToClient(
*response_client, kServiceTunnellingRequest,
tunnel_confirmation.data(), tunnel_confirmation.size());
}
const uint16_t service = KnxIpServiceForCemi(response, response_len, response_service);
if (service == kServiceDeviceConfigurationRequest) {
if (response_client != nullptr && response_client->connected) {
sendCemiFrameToClient(*response_client, service, response, response_len);
} else if (routing_context) {
sendRoutingIndication(response, response_len);
}
return;
}
if (message_code.has_value() && message_code.value() == L_data_con) {
if (response_client != nullptr && response_client->connected) {
sent_tunnel_confirmation =
sendCemiFrameToClient(*response_client, service, response, response_len) ||
sent_tunnel_confirmation;
}
return;
}
if (routing_context) {
sendRoutingIndication(response, response_len);
return;
}
sendTunnelIndication(response, response_len);
});
if (needs_tunnel_confirmation && consumed && !sent_tunnel_confirmation) {
sendCemiFrameToClient(*response_client, kServiceTunnellingRequest,
tunnel_confirmation.data(), tunnel_confirmation.size());
}
syncOpenKnxConfigFromDevice();
return consumed;
}
+1 -1
Submodule knx updated: 82f22cf571...c39e8cfc55