diff --git a/components/gateway_knx/include/ets_device_runtime.h b/components/gateway_knx/include/ets_device_runtime.h index 8d300ad..85dbb57 100644 --- a/components/gateway_knx/include/ets_device_runtime.h +++ b/components/gateway_knx/include/ets_device_runtime.h @@ -29,6 +29,11 @@ class EtsDeviceRuntime { using FunctionPropertyHandler = std::function* response)>; + using FunctionPropertyExtHandler = std::function* response)>; EtsDeviceRuntime(std::string nvs_namespace, uint16_t fallback_individual_address, @@ -52,6 +57,8 @@ class EtsDeviceRuntime { void setFunctionPropertyHandlers(FunctionPropertyHandler command_handler, FunctionPropertyHandler state_handler); + void setFunctionPropertyExtHandlers(FunctionPropertyExtHandler command_handler, + FunctionPropertyExtHandler state_handler); void setGroupWriteHandler(GroupWriteHandler handler); void setGroupObjectWriteHandler(GroupObjectWriteHandler handler); void setBusFrameSender(CemiFrameSender sender); @@ -79,10 +86,29 @@ class EtsDeviceRuntime { static bool HandleFunctionPropertyState(uint8_t object_index, uint8_t property_id, uint8_t length, uint8_t* data, uint8_t* result_data, uint8_t& result_length); + static bool HandleFunctionPropertyExtCommand(uint16_t object_type, + uint8_t object_instance, + uint8_t property_id, + uint8_t length, uint8_t* data, + uint8_t* result_data, + uint8_t& result_length); + static bool HandleFunctionPropertyExtState(uint16_t object_type, + uint8_t object_instance, + uint8_t property_id, + uint8_t length, uint8_t* data, + uint8_t* result_data, + uint8_t& result_length); static uint16_t DefaultTunnelClientAddress(uint16_t individual_address); static bool DispatchFunctionProperty(FunctionPropertyHandler* handler, uint8_t object_index, uint8_t property_id, uint8_t length, uint8_t* data, uint8_t* result_data, uint8_t& result_length); + static bool DispatchFunctionPropertyExt(FunctionPropertyExtHandler* handler, + uint16_t object_type, + uint8_t object_instance, + uint8_t property_id, + uint8_t length, uint8_t* data, + uint8_t* result_data, + uint8_t& result_length); void installGroupObjectCallbacks(); bool shouldConsumeTunnelFrame(CemiFrame& frame) const; bool shouldConsumeBusFrame(CemiFrame& frame) const; @@ -97,6 +123,8 @@ class EtsDeviceRuntime { GroupObjectWriteHandler group_object_write_handler_; FunctionPropertyHandler command_handler_; FunctionPropertyHandler state_handler_; + FunctionPropertyExtHandler command_ext_handler_; + FunctionPropertyExtHandler state_ext_handler_; bool suppress_group_object_write_callback_{false}; uint16_t group_object_callback_count_{0}; }; diff --git a/components/gateway_knx/include/gateway_knx.hpp b/components/gateway_knx/include/gateway_knx.hpp index ec45f81..36edd6b 100644 --- a/components/gateway_knx/include/gateway_knx.hpp +++ b/components/gateway_knx/include/gateway_knx.hpp @@ -236,6 +236,17 @@ class GatewayKnxTpIpRouter { bool publishDaliStatus(const GatewayKnxDaliTarget& target, uint8_t actual_level); private: + bool handleFunctionPropertyExtCommand(uint16_t object_type, + uint8_t object_instance, + uint8_t property_id, + const uint8_t* data, size_t len, + std::vector* response); + bool handleFunctionPropertyExtState(uint16_t object_type, + uint8_t object_instance, + uint8_t property_id, + const uint8_t* data, size_t len, + std::vector* response); + static constexpr size_t kMaxTunnelClients = 16; static constexpr size_t kMaxTcpClients = 4; diff --git a/components/gateway_knx/src/ets_device_runtime.cpp b/components/gateway_knx/src/ets_device_runtime.cpp index df262c6..d86c16b 100644 --- a/components/gateway_knx/src/ets_device_runtime.cpp +++ b/components/gateway_knx/src/ets_device_runtime.cpp @@ -140,6 +140,8 @@ EtsDeviceRuntime::EtsDeviceRuntime(std::string nvs_namespace, } device_.functionPropertyCallback(&EtsDeviceRuntime::HandleFunctionPropertyCommand); device_.functionPropertyStateCallback(&EtsDeviceRuntime::HandleFunctionPropertyState); + device_.functionPropertyExtCallback(&EtsDeviceRuntime::HandleFunctionPropertyExtCommand); + device_.functionPropertyExtStateCallback(&EtsDeviceRuntime::HandleFunctionPropertyExtState); #ifdef USE_DATASECURE device_.secureGroupWriteCallback(&EtsDeviceRuntime::HandleSecureGroupWrite, this); #endif @@ -168,6 +170,8 @@ EtsDeviceRuntime::~EtsDeviceRuntime() { } device_.functionPropertyCallback(nullptr); device_.functionPropertyStateCallback(nullptr); + device_.functionPropertyExtCallback(nullptr); + device_.functionPropertyExtStateCallback(nullptr); if (auto* server = device_.getCemiServer()) { server->tunnelFrameCallback(nullptr, nullptr); } @@ -251,6 +255,12 @@ void EtsDeviceRuntime::setFunctionPropertyHandlers(FunctionPropertyHandler comma state_handler_ = std::move(state_handler); } +void EtsDeviceRuntime::setFunctionPropertyExtHandlers(FunctionPropertyExtHandler command_handler, + FunctionPropertyExtHandler state_handler) { + command_ext_handler_ = std::move(command_handler); + state_ext_handler_ = std::move(state_handler); +} + void EtsDeviceRuntime::setGroupWriteHandler(GroupWriteHandler handler) { group_write_handler_ = std::move(handler); } @@ -337,6 +347,7 @@ bool EtsDeviceRuntime::handleBusFrame(const uint8_t* data, size_t len) { if (!consumed) { return false; } + ActiveFunctionPropertyRuntimeScope callback_scope(this); data_link_layer->externalFrameReceived(frame); loop(); installGroupObjectCallbacks(); @@ -371,7 +382,10 @@ bool EtsDeviceRuntime::emitGroupValue(uint16_t group_object_number, const uint8_ return true; } -void EtsDeviceRuntime::loop() { device_.loop(); } +void EtsDeviceRuntime::loop() { + ActiveFunctionPropertyRuntimeScope callback_scope(this); + device_.loop(); +} bool EtsDeviceRuntime::HandleOutboundCemiFrame(CemiFrame& frame, void* context) { auto* self = static_cast(context); @@ -453,6 +467,34 @@ bool EtsDeviceRuntime::HandleFunctionPropertyState(uint8_t object_index, uint8_t property_id, length, data, result_data, result_length); } +bool EtsDeviceRuntime::HandleFunctionPropertyExtCommand(uint16_t object_type, + uint8_t object_instance, + uint8_t property_id, + uint8_t length, uint8_t* data, + uint8_t* result_data, + uint8_t& result_length) { + if (active_function_property_runtime == nullptr) { + return false; + } + return DispatchFunctionPropertyExt(&active_function_property_runtime->command_ext_handler_, + object_type, object_instance, property_id, length, data, + result_data, result_length); +} + +bool EtsDeviceRuntime::HandleFunctionPropertyExtState(uint16_t object_type, + uint8_t object_instance, + uint8_t property_id, + uint8_t length, uint8_t* data, + uint8_t* result_data, + uint8_t& result_length) { + if (active_function_property_runtime == nullptr) { + return false; + } + return DispatchFunctionPropertyExt(&active_function_property_runtime->state_ext_handler_, + object_type, object_instance, property_id, length, data, + result_data, result_length); +} + bool EtsDeviceRuntime::DispatchFunctionProperty(FunctionPropertyHandler* handler, uint8_t object_index, uint8_t property_id, uint8_t length, uint8_t* data, @@ -471,6 +513,27 @@ bool EtsDeviceRuntime::DispatchFunctionProperty(FunctionPropertyHandler* handler return true; } +bool EtsDeviceRuntime::DispatchFunctionPropertyExt(FunctionPropertyExtHandler* handler, + uint16_t object_type, + uint8_t object_instance, + uint8_t property_id, + uint8_t length, uint8_t* data, + uint8_t* result_data, + uint8_t& result_length) { + if (handler == nullptr || !*handler || result_data == nullptr) { + return false; + } + std::vector response; + if (!(*handler)(object_type, object_instance, property_id, data, length, &response)) { + return false; + } + result_length = static_cast(std::min(response.size(), result_length)); + if (result_length > 0) { + std::copy_n(response.begin(), result_length, result_data); + } + return true; +} + void EtsDeviceRuntime::installGroupObjectCallbacks() { active_group_object_runtime = this; auto& table = device_.groupObjectTable(); diff --git a/components/gateway_knx/src/gateway_knx.cpp b/components/gateway_knx/src/gateway_knx.cpp index 6771673..6623389 100644 --- a/components/gateway_knx/src/gateway_knx.cpp +++ b/components/gateway_knx/src/gateway_knx.cpp @@ -14,6 +14,7 @@ #include "tpuart_uart_interface.h" #include "knx/cemi_frame.h" +#include "knx/interface_object.h" #include "knx/knx_ip_connect_request.h" #include "knx/knx_ip_connect_response.h" #include "knx/knx_ip_config_request.h" @@ -140,12 +141,22 @@ constexpr uint8_t kReg1DeviceTypeDt8 = 8; constexpr uint8_t kReg1ColorTypeTw = 1; constexpr uint8_t kDaliDeviceTypeNone = 0xfe; constexpr uint8_t kDaliDeviceTypeMultiple = 0xff; +constexpr uint16_t kGroupObjectTableObjectType = OT_GRP_OBJ_TABLE; +constexpr uint8_t kPidGoDiagnostics = 0x42; +constexpr uint8_t kGoDiagnosticsReservedByte = 0x00; +constexpr uint8_t kGoDiagnosticsGroupWriteService = 0x01; struct DecodedGroupWrite { uint16_t group_address{0}; std::vector data; }; +struct DecodedGoDiagnosticsGroupWrite { + uint16_t group_address{0}; + const uint8_t* payload{nullptr}; + size_t payload_len{0}; +}; + struct KnxNetifInfo { const char* key{nullptr}; esp_netif_t* netif{nullptr}; @@ -550,6 +561,26 @@ std::optional DecodeOpenKnxGroupWrite(const uint8_t* data, si return out; } +std::optional DecodeGoDiagnosticsGroupWrite( + const uint8_t* data, size_t len) { + if (data == nullptr || len < 5) { + return std::nullopt; + } + if (data[0] != kGoDiagnosticsReservedByte || data[1] != kGoDiagnosticsGroupWriteService) { + return std::nullopt; + } + const size_t encoded_length = data[2]; + if (encoded_length < 2 || len != encoded_length + 3) { + return std::nullopt; + } + + DecodedGoDiagnosticsGroupWrite out; + out.group_address = ReadBe16(data + 3); + out.payload = data + 5; + out.payload_len = encoded_length - 2; + return out; +} + bool IsOpenKnxGroupValueWrite(const uint8_t* data, size_t len) { return DecodeOpenKnxGroupWrite(data, len).has_value(); } @@ -705,6 +736,27 @@ std::optional MetadataInt(const DaliBridgeResult& result, const std::string return getObjectInt(result.metadata, key); } +uint8_t GoDiagnosticsReturnCode(const DaliBridgeResult& result) { + if (result.ok) { + return ReturnCodes::Success; + } + if (getObjectBool(result.metadata, "ignored").value_or(false)) { + return ReturnCodes::TemporarilyNotAvailable; + } + + const std::string& error = result.error; + if (error.find("unmapped") != std::string::npos || + error.find("does not match gateway config") != std::string::npos) { + return ReturnCodes::AddressVoid; + } + if (error.find("disabled") != std::string::npos || + error.find("commissioning-only") != std::string::npos || + error.find("not configured") != std::string::npos) { + return ReturnCodes::TemporarilyNotAvailable; + } + return ReturnCodes::GenericError; +} + std::string HexBytes(const uint8_t* data, size_t len) { if (data == nullptr || len == 0) { return {}; @@ -2498,6 +2550,17 @@ esp_err_t GatewayKnxTpIpRouter::initializeRuntime() { return bridge_.handleFunctionPropertyState(object_index, property_id, data, len, response); }); + ets_device_->setFunctionPropertyExtHandlers( + [this](uint16_t object_type, uint8_t object_instance, uint8_t property_id, + const uint8_t* data, size_t len, std::vector* response) { + return handleFunctionPropertyExtCommand(object_type, object_instance, property_id, + data, len, response); + }, + [this](uint16_t object_type, uint8_t object_instance, uint8_t property_id, + const uint8_t* data, size_t len, std::vector* response) { + return handleFunctionPropertyExtState(object_type, object_instance, property_id, + data, len, response); + }); ets_device_->setGroupWriteHandler( [this](uint16_t group_address, const uint8_t* data, size_t len) { if (!shouldRouteDaliApplicationFrames()) { @@ -4266,6 +4329,93 @@ bool GatewayKnxTpIpRouter::routeOpenKnxGroupWrite(const uint8_t* data, size_t le return true; } +bool GatewayKnxTpIpRouter::handleFunctionPropertyExtCommand( + uint16_t object_type, uint8_t object_instance, uint8_t property_id, + const uint8_t* data, size_t len, std::vector* response) { + if (response == nullptr || object_type != kGroupObjectTableObjectType || + property_id != kPidGoDiagnostics) { + return false; + } + + const auto decoded = DecodeGoDiagnosticsGroupWrite(data, len); + if (!decoded.has_value()) { + const std::string payload = HexBytes(data, len); + ESP_LOGW(kTag, + "OpenKNX GO diagnostics write malformed objType=0x%04X objInst=%u property=0x%02X len=%u payload=%s", + static_cast(object_type), static_cast(object_instance), + static_cast(property_id), static_cast(len), payload.c_str()); + *response = {ReturnCodes::DataVoid}; + return true; + } + + const std::string group_address_text = + GatewayKnxGroupAddressString(decoded->group_address); + const std::string payload = HexBytes(decoded->payload, decoded->payload_len); + ESP_LOGI(kTag, + "OpenKNX GO diagnostics group write ga=0x%04X (%s) len=%u payload=%s", + static_cast(decoded->group_address), group_address_text.c_str(), + static_cast(decoded->payload_len), payload.c_str()); + + if (!shouldRouteDaliApplicationFrames()) { + ESP_LOGW(kTag, + "OpenKNX GO diagnostics group write ga=0x%04X (%s) blocked by commissioning-only routing state", + static_cast(decoded->group_address), group_address_text.c_str()); + *response = {ReturnCodes::TemporarilyNotAvailable}; + return true; + } + + const DaliBridgeResult result = + group_write_handler_ ? group_write_handler_(decoded->group_address, decoded->payload, + decoded->payload_len) + : bridge_.handleGroupWrite(decoded->group_address, + decoded->payload, + decoded->payload_len); + const uint8_t return_code = GoDiagnosticsReturnCode(result); + if (return_code == ReturnCodes::AddressVoid) { + ESP_LOGW(kTag, + "OpenKNX GO diagnostics group write ga=0x%04X (%s) returning AddressVoid: %s", + static_cast(decoded->group_address), group_address_text.c_str(), + result.error.empty() ? "unmapped KNX group address" + : result.error.c_str()); + } else if (!result.ok) { + ESP_LOGW(kTag, + "OpenKNX GO diagnostics group write ga=0x%04X (%s) failed rc=0x%02X: %s", + static_cast(decoded->group_address), group_address_text.c_str(), + static_cast(return_code), + result.error.empty() ? "command routing failed" : result.error.c_str()); + } + response->assign(1, return_code); + return true; +} + +bool GatewayKnxTpIpRouter::handleFunctionPropertyExtState( + uint16_t object_type, uint8_t object_instance, uint8_t property_id, + const uint8_t* data, size_t len, std::vector* response) { + if (response == nullptr || object_type != kGroupObjectTableObjectType || + property_id != kPidGoDiagnostics) { + return false; + } + + const auto decoded = DecodeGoDiagnosticsGroupWrite(data, len); + if (!decoded.has_value()) { + const std::string payload = HexBytes(data, len); + ESP_LOGW(kTag, + "OpenKNX GO diagnostics state request malformed objType=0x%04X objInst=%u property=0x%02X len=%u payload=%s", + static_cast(object_type), static_cast(object_instance), + static_cast(property_id), static_cast(len), payload.c_str()); + *response = {ReturnCodes::DataVoid}; + return true; + } + + const std::string group_address_text = + GatewayKnxGroupAddressString(decoded->group_address); + ESP_LOGW(kTag, + "OpenKNX GO diagnostics state request unsupported ga=0x%04X (%s)", + static_cast(decoded->group_address), group_address_text.c_str()); + *response = {ReturnCodes::InvalidCommand}; + return true; +} + bool GatewayKnxTpIpRouter::emitOpenKnxGroupValue(uint16_t group_object_number, const uint8_t* data, size_t len) { SemaphoreGuard guard(openknx_lock_); diff --git a/knx b/knx index dac61e2..af9be62 160000 --- a/knx +++ b/knx @@ -1 +1 @@ -Subproject commit dac61e2707fcded1960d7dd416fb17b630e3b844 +Subproject commit af9be625290d145e5d0dac80c3d66eca01a0a637