feat(gateway): add extended function property handlers and diagnostics support for OpenKNX

Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
Tony
2026-05-19 10:02:57 +08:00
parent e091b4301e
commit a3f03719f9
5 changed files with 254 additions and 2 deletions
@@ -29,6 +29,11 @@ class EtsDeviceRuntime {
using FunctionPropertyHandler = std::function<bool(uint8_t object_index, uint8_t property_id,
const uint8_t* data, size_t len,
std::vector<uint8_t>* response)>;
using FunctionPropertyExtHandler = std::function<bool(uint16_t object_type,
uint8_t object_instance,
uint8_t property_id,
const uint8_t* data, size_t len,
std::vector<uint8_t>* 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};
};
@@ -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<uint8_t>* response);
bool handleFunctionPropertyExtState(uint16_t object_type,
uint8_t object_instance,
uint8_t property_id,
const uint8_t* data, size_t len,
std::vector<uint8_t>* response);
static constexpr size_t kMaxTunnelClients = 16;
static constexpr size_t kMaxTcpClients = 4;
@@ -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<EtsDeviceRuntime*>(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<uint8_t> response;
if (!(*handler)(object_type, object_instance, property_id, data, length, &response)) {
return false;
}
result_length = static_cast<uint8_t>(std::min<size_t>(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();
+150
View File
@@ -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<uint8_t> 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<DecodedGroupWrite> DecodeOpenKnxGroupWrite(const uint8_t* data, si
return out;
}
std::optional<DecodedGoDiagnosticsGroupWrite> 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<int> 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<uint8_t>* 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<uint8_t>* 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<uint8_t>* 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<unsigned>(object_type), static_cast<unsigned>(object_instance),
static_cast<unsigned>(property_id), static_cast<unsigned>(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<unsigned>(decoded->group_address), group_address_text.c_str(),
static_cast<unsigned>(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<unsigned>(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<unsigned>(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<unsigned>(decoded->group_address), group_address_text.c_str(),
static_cast<unsigned>(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<uint8_t>* 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<unsigned>(object_type), static_cast<unsigned>(object_instance),
static_cast<unsigned>(property_id), static_cast<unsigned>(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<unsigned>(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_);