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
+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_);