feat(gateway): enhance KNX support with DALI integration and configuration updates

Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
Tony
2026-05-12 20:34:33 +08:00
parent e58115d303
commit de0edd5ad9
11 changed files with 395 additions and 30 deletions
+151 -13
View File
@@ -64,6 +64,8 @@ constexpr uint16_t kGwReg1AppKoBroadcastDimm = 2;
constexpr uint8_t kGwReg1KoSwitch = 0;
constexpr uint8_t kGwReg1KoDimmAbsolute = 3;
constexpr uint8_t kGwReg1KoColor = 6;
constexpr uint8_t kGwReg1KoSwitchState = 1;
constexpr uint8_t kGwReg1KoDimmState = 4;
constexpr uint8_t kReg1DaliFunctionObjectIndex = 160;
constexpr uint8_t kReg1DaliFunctionPropertyId = 1;
constexpr uint8_t kReg1FunctionType = 2;
@@ -84,6 +86,31 @@ struct DecodedGroupWrite {
std::vector<uint8_t> data;
};
class SemaphoreGuard {
public:
explicit SemaphoreGuard(SemaphoreHandle_t semaphore) : semaphore_(semaphore) {
if (semaphore_ != nullptr) {
xSemaphoreTake(semaphore_, portMAX_DELAY);
locked_ = true;
}
}
~SemaphoreGuard() {
if (locked_) {
xSemaphoreGive(semaphore_);
}
}
private:
SemaphoreHandle_t semaphore_{nullptr};
bool locked_{false};
};
uint8_t DaliArcLevelToDpt5(uint8_t actual_level) {
return static_cast<uint8_t>(
std::clamp<int>(static_cast<int>(std::lround(actual_level * 255.0 / 254.0)), 0, 255));
}
uint16_t ReadBe16(const uint8_t* data) {
return static_cast<uint16_t>((static_cast<uint16_t>(data[0]) << 8) | data[1]);
}
@@ -309,6 +336,22 @@ std::optional<DecodedGroupWrite> DecodeCemiGroupWrite(const uint8_t* data, size_
return out;
}
bool IsCemiGroupFrame(const uint8_t* data, size_t len) {
if (data == nullptr || len < 10) {
return false;
}
const uint8_t message_code = data[0];
if (message_code != kCemiLDataReq && message_code != kCemiLDataInd &&
message_code != kCemiLDataCon) {
return false;
}
const size_t base = 2U + data[1];
if (len < base + 8U) {
return false;
}
return (data[base + 1] & 0x80) != 0;
}
uint8_t Reg1PercentToArc(uint8_t value) {
if (value == 0 || value == 0xff) {
return value;
@@ -1528,9 +1571,17 @@ GatewayKnxTpIpRouter::GatewayKnxTpIpRouter(GatewayKnxBridge& bridge, CemiFrameHa
std::string openknx_namespace)
: bridge_(bridge),
handler_(std::move(handler)),
openknx_namespace_(std::move(openknx_namespace)) {}
openknx_namespace_(std::move(openknx_namespace)) {
openknx_lock_ = xSemaphoreCreateMutex();
}
GatewayKnxTpIpRouter::~GatewayKnxTpIpRouter() { stop(); }
GatewayKnxTpIpRouter::~GatewayKnxTpIpRouter() {
stop();
if (openknx_lock_ != nullptr) {
vSemaphoreDelete(openknx_lock_);
openknx_lock_ = nullptr;
}
}
void GatewayKnxTpIpRouter::setConfig(const GatewayKnxConfig& config) { config_ = config; }
@@ -1559,6 +1610,13 @@ esp_err_t GatewayKnxTpIpRouter::start(uint32_t task_stack_size, UBaseType_t task
std::vector<uint8_t>* response) {
return bridge_.handleFunctionPropertyState(object_index, property_id, data, len, response);
});
ets_device_->setGroupWriteHandler(
[this](uint16_t group_address, const uint8_t* data, size_t len) {
const DaliBridgeResult result = bridge_.handleGroupWrite(group_address, data, len);
if (!result.ok && !result.error.empty()) {
ESP_LOGD(kTag, "secure KNX group write not routed to DALI: %s", result.error.c_str());
}
});
if (!configureTpUart()) {
ets_device_.reset();
closeSockets();
@@ -1590,6 +1648,40 @@ bool GatewayKnxTpIpRouter::started() const { return started_; }
const std::string& GatewayKnxTpIpRouter::lastError() const { return last_error_; }
bool GatewayKnxTpIpRouter::publishDaliStatus(const GatewayKnxDaliTarget& target,
uint8_t actual_level) {
if (!started_ || !config_.ip_router_enabled) {
return false;
}
uint16_t switch_object = 0;
uint16_t dimm_object = 0;
if (target.kind == GatewayKnxDaliTargetKind::kShortAddress) {
if (target.address < 0 || target.address > 63) {
return false;
}
const uint16_t base = kGwReg1AdrKoOffset +
kGwReg1AdrKoBlockSize * static_cast<uint16_t>(target.address);
switch_object = base + kGwReg1KoSwitchState;
dimm_object = base + kGwReg1KoDimmState;
} else if (target.kind == GatewayKnxDaliTargetKind::kGroup) {
if (target.address < 0 || target.address > 15) {
return false;
}
const uint16_t base = kGwReg1GrpKoOffset +
kGwReg1GrpKoBlockSize * static_cast<uint16_t>(target.address);
switch_object = base + kGwReg1KoSwitchState;
dimm_object = base + kGwReg1KoDimmState;
} else {
return false;
}
const uint8_t switch_value = actual_level > 0 ? 1 : 0;
const uint8_t dimm_value = DaliArcLevelToDpt5(actual_level);
bool emitted = emitOpenKnxGroupValue(switch_object, &switch_value, 1);
emitted = emitOpenKnxGroupValue(dimm_object, &dimm_value, 1) || emitted;
return emitted;
}
void GatewayKnxTpIpRouter::TaskEntry(void* arg) {
static_cast<GatewayKnxTpIpRouter*>(arg)->taskLoop();
}
@@ -1603,8 +1695,11 @@ void GatewayKnxTpIpRouter::taskLoop() {
reinterpret_cast<sockaddr*>(&remote), &remote_len);
if (received <= 0) {
pollTpUart();
if (ets_device_ != nullptr) {
ets_device_->loop();
{
SemaphoreGuard guard(openknx_lock_);
if (ets_device_ != nullptr) {
ets_device_->loop();
}
}
if (!stop_requested_) {
vTaskDelay(pdMS_TO_TICKS(10));
@@ -1613,8 +1708,11 @@ void GatewayKnxTpIpRouter::taskLoop() {
}
handleUdpDatagram(buffer.data(), static_cast<size_t>(received), remote);
pollTpUart();
if (ets_device_ != nullptr) {
ets_device_->loop();
{
SemaphoreGuard guard(openknx_lock_);
if (ets_device_ != nullptr) {
ets_device_->loop();
}
}
}
finishTask();
@@ -1622,7 +1720,10 @@ void GatewayKnxTpIpRouter::taskLoop() {
void GatewayKnxTpIpRouter::finishTask() {
closeSockets();
ets_device_.reset();
{
SemaphoreGuard guard(openknx_lock_);
ets_device_.reset();
}
started_ = false;
task_handle_ = nullptr;
vTaskDelete(nullptr);
@@ -1805,9 +1906,12 @@ void GatewayKnxTpIpRouter::handleRoutingIndication(const uint8_t* body, size_t l
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());
const bool consumed_by_openknx = handleOpenKnxBusFrame(body, len);
if (!consumed_by_openknx) {
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);
}
@@ -1831,8 +1935,12 @@ void GatewayKnxTpIpRouter::handleTunnellingRequest(const uint8_t* body, size_t l
sendTunnellingAck(channel_id, sequence, kKnxNoError, remote);
const uint8_t* cemi = body + 4;
const size_t cemi_len = len - 4;
const bool group_frame = IsCemiGroupFrame(cemi, cemi_len);
const bool consumed_by_openknx = handleOpenKnxTunnelFrame(cemi, cemi_len);
if (consumed_by_openknx) {
if (group_frame) {
forwardCemiToTp(cemi, cemi_len);
}
return;
}
const DaliBridgeResult result = handler_(cemi, cemi_len);
@@ -1959,6 +2067,7 @@ void GatewayKnxTpIpRouter::sendRoutingIndication(const uint8_t* data, size_t len
}
bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t len) {
SemaphoreGuard guard(openknx_lock_);
if (ets_device_ == nullptr) {
return false;
}
@@ -1970,6 +2079,32 @@ bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t
return consumed;
}
bool GatewayKnxTpIpRouter::handleOpenKnxBusFrame(const uint8_t* data, size_t len) {
SemaphoreGuard guard(openknx_lock_);
if (ets_device_ == nullptr) {
return false;
}
const bool consumed = ets_device_->handleBusFrame(data, len);
syncOpenKnxConfigFromDevice();
return consumed;
}
bool GatewayKnxTpIpRouter::emitOpenKnxGroupValue(uint16_t group_object_number,
const uint8_t* data, size_t len) {
SemaphoreGuard guard(openknx_lock_);
if (ets_device_ == nullptr) {
return false;
}
const bool emitted = ets_device_->emitGroupValue(
group_object_number, data, len, [this](const uint8_t* frame_data, size_t frame_len) {
sendRoutingIndication(frame_data, frame_len);
sendTunnelIndication(frame_data, frame_len);
forwardCemiToTp(frame_data, frame_len);
});
syncOpenKnxConfigFromDevice();
return emitted;
}
void GatewayKnxTpIpRouter::syncOpenKnxConfigFromDevice() {
if (ets_device_ == nullptr) {
return;
@@ -2122,9 +2257,12 @@ void GatewayKnxTpIpRouter::handleTpTelegram(const uint8_t* data, size_t 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());
const bool consumed_by_openknx = handleOpenKnxBusFrame(cemi->data(), cemi->size());
if (!consumed_by_openknx) {
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());