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:
@@ -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();
|
||||
|
||||
@@ -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_);
|
||||
|
||||
Reference in New Issue
Block a user