feat(gateway): add DALI bus ID configuration and enhance group object write handling
Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "knx/cemi_server.h"
|
||||
#include "knx/group_object.h"
|
||||
#include "knx/secure_application_layer.h"
|
||||
#include "knx/property.h"
|
||||
#include "tpuart_uart_interface.h"
|
||||
@@ -19,6 +20,7 @@ namespace gateway::openknx {
|
||||
namespace {
|
||||
|
||||
thread_local EtsDeviceRuntime* active_function_property_runtime = nullptr;
|
||||
EtsDeviceRuntime* active_group_object_runtime = nullptr;
|
||||
|
||||
class ActiveFunctionPropertyRuntimeScope {
|
||||
public:
|
||||
@@ -121,6 +123,7 @@ EtsDeviceRuntime::EtsDeviceRuntime(std::string nvs_namespace,
|
||||
}
|
||||
ESP_LOGI("gateway_knx", "OpenKNX loading memory namespace=%s", nvs_namespace_.c_str());
|
||||
device_.readMemory();
|
||||
installGroupObjectCallbacks();
|
||||
if (!IsUsableIndividualAddress(device_.deviceObject().individualAddress()) &&
|
||||
IsUsableIndividualAddress(fallback_individual_address)) {
|
||||
device_.deviceObject().individualAddress(fallback_individual_address);
|
||||
@@ -149,6 +152,19 @@ EtsDeviceRuntime::~EtsDeviceRuntime() {
|
||||
#ifdef USE_DATASECURE
|
||||
device_.secureGroupWriteCallback(nullptr, nullptr);
|
||||
#endif
|
||||
#ifdef SMALL_GROUPOBJECT
|
||||
if (active_group_object_runtime == this) {
|
||||
GroupObject::classCallback(GroupObjectUpdatedHandler{});
|
||||
}
|
||||
#else
|
||||
auto& table = device_.groupObjectTable();
|
||||
for (uint16_t asap = 1; asap <= table.entryCount(); ++asap) {
|
||||
table.get(asap).callback(GroupObjectUpdatedHandler{});
|
||||
}
|
||||
#endif
|
||||
if (active_group_object_runtime == this) {
|
||||
active_group_object_runtime = nullptr;
|
||||
}
|
||||
device_.functionPropertyCallback(nullptr);
|
||||
device_.functionPropertyStateCallback(nullptr);
|
||||
if (auto* server = device_.getCemiServer()) {
|
||||
@@ -223,6 +239,11 @@ void EtsDeviceRuntime::setGroupWriteHandler(GroupWriteHandler handler) {
|
||||
group_write_handler_ = std::move(handler);
|
||||
}
|
||||
|
||||
void EtsDeviceRuntime::setGroupObjectWriteHandler(GroupObjectWriteHandler handler) {
|
||||
group_object_write_handler_ = std::move(handler);
|
||||
installGroupObjectCallbacks();
|
||||
}
|
||||
|
||||
void EtsDeviceRuntime::setBusFrameSender(CemiFrameSender sender) {
|
||||
bus_frame_sender_ = std::move(sender);
|
||||
}
|
||||
@@ -272,11 +293,20 @@ bool EtsDeviceRuntime::handleTunnelFrame(const uint8_t* data, size_t len,
|
||||
if (!consumed) {
|
||||
return false;
|
||||
}
|
||||
const bool suppress_group_object_route =
|
||||
frame.messageCode() == L_data_req && frame.addressType() == GroupAddress &&
|
||||
frame.apdu().type() == GroupValueWrite;
|
||||
const bool previous_suppression = suppress_group_object_write_callback_;
|
||||
if (suppress_group_object_route) {
|
||||
suppress_group_object_write_callback_ = true;
|
||||
}
|
||||
sender_ = std::move(sender);
|
||||
ActiveFunctionPropertyRuntimeScope callback_scope(this);
|
||||
server->frameReceived(frame);
|
||||
loop();
|
||||
sender_ = nullptr;
|
||||
suppress_group_object_write_callback_ = previous_suppression;
|
||||
installGroupObjectCallbacks();
|
||||
return consumed;
|
||||
}
|
||||
|
||||
@@ -293,6 +323,7 @@ bool EtsDeviceRuntime::handleBusFrame(const uint8_t* data, size_t len) {
|
||||
}
|
||||
data_link_layer->externalFrameReceived(frame);
|
||||
loop();
|
||||
installGroupObjectCallbacks();
|
||||
return consumed;
|
||||
}
|
||||
|
||||
@@ -314,10 +345,13 @@ bool EtsDeviceRuntime::emitGroupValue(uint16_t group_object_number, const uint8_
|
||||
} else {
|
||||
std::copy_n(data, len, group_object.valueRef());
|
||||
}
|
||||
const bool previous_suppression = suppress_group_object_write_callback_;
|
||||
suppress_group_object_write_callback_ = true;
|
||||
sender_ = std::move(sender);
|
||||
group_object.objectWritten();
|
||||
loop();
|
||||
sender_ = nullptr;
|
||||
suppress_group_object_write_callback_ = previous_suppression;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -349,10 +383,36 @@ void EtsDeviceRuntime::EmitTunnelFrame(CemiFrame& frame, void* context) {
|
||||
void EtsDeviceRuntime::HandleSecureGroupWrite(uint16_t group_address, const uint8_t* data,
|
||||
uint8_t data_length, void* context) {
|
||||
auto* self = static_cast<EtsDeviceRuntime*>(context);
|
||||
if (self == nullptr || !self->group_write_handler_) {
|
||||
if (self == nullptr) {
|
||||
return;
|
||||
}
|
||||
self->group_write_handler_(group_address, data, data_length);
|
||||
if (self->group_object_write_handler_) {
|
||||
return;
|
||||
}
|
||||
if (self->group_write_handler_) {
|
||||
self->group_write_handler_(group_address, data, data_length);
|
||||
}
|
||||
}
|
||||
|
||||
void EtsDeviceRuntime::HandleGroupObjectWrite(GroupObject& ko) {
|
||||
auto* self = active_group_object_runtime;
|
||||
if (self == nullptr || self->suppress_group_object_write_callback_ ||
|
||||
!self->group_object_write_handler_) {
|
||||
return;
|
||||
}
|
||||
const size_t value_size = ko.valueSize();
|
||||
const uint8_t* value = ko.valueRef();
|
||||
if (value == nullptr || value_size == 0) {
|
||||
ESP_LOGW("gateway_knx", "OpenKNX group-object callback ignored namespace=%s ko=%u len=%u",
|
||||
self->nvs_namespace_.c_str(), static_cast<unsigned>(ko.asap()),
|
||||
static_cast<unsigned>(value_size));
|
||||
return;
|
||||
}
|
||||
const std::string value_hex = HexBytesString(value, value_size);
|
||||
ESP_LOGI("gateway_knx", "OpenKNX group-object callback namespace=%s ko=%u len=%u value=%s",
|
||||
self->nvs_namespace_.c_str(), static_cast<unsigned>(ko.asap()),
|
||||
static_cast<unsigned>(value_size), value_hex.c_str());
|
||||
self->group_object_write_handler_(ko.asap(), value, value_size);
|
||||
}
|
||||
|
||||
bool EtsDeviceRuntime::HandleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id,
|
||||
@@ -395,6 +455,24 @@ bool EtsDeviceRuntime::DispatchFunctionProperty(FunctionPropertyHandler* handler
|
||||
return true;
|
||||
}
|
||||
|
||||
void EtsDeviceRuntime::installGroupObjectCallbacks() {
|
||||
active_group_object_runtime = this;
|
||||
auto& table = device_.groupObjectTable();
|
||||
const uint16_t count = table.entryCount();
|
||||
#ifdef SMALL_GROUPOBJECT
|
||||
GroupObject::classCallback(&EtsDeviceRuntime::HandleGroupObjectWrite);
|
||||
#else
|
||||
for (uint16_t asap = 1; asap <= count; ++asap) {
|
||||
table.get(asap).callback(&EtsDeviceRuntime::HandleGroupObjectWrite);
|
||||
}
|
||||
#endif
|
||||
if (count != group_object_callback_count_) {
|
||||
ESP_LOGI("gateway_knx", "OpenKNX group-object callbacks namespace=%s count=%u",
|
||||
nvs_namespace_.c_str(), static_cast<unsigned>(count));
|
||||
group_object_callback_count_ = count;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t EtsDeviceRuntime::DefaultTunnelClientAddress(uint16_t individual_address) {
|
||||
if (!IsUsableIndividualAddress(individual_address)) {
|
||||
return 0x1101;
|
||||
|
||||
@@ -610,6 +610,23 @@ std::optional<int> MetadataInt(const DaliBridgeResult& result, const std::string
|
||||
return getObjectInt(result.metadata, key);
|
||||
}
|
||||
|
||||
std::string HexBytes(const uint8_t* data, size_t len) {
|
||||
if (data == nullptr || len == 0) {
|
||||
return {};
|
||||
}
|
||||
std::string out;
|
||||
out.reserve(len * 3);
|
||||
char buffer[4] = {0};
|
||||
for (size_t index = 0; index < len; ++index) {
|
||||
std::snprintf(buffer, sizeof(buffer), "%02X", data[index]);
|
||||
out += buffer;
|
||||
if (index + 1 < len) {
|
||||
out.push_back(' ');
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
DaliBridgeRequest RequestForTarget(uint16_t group_address,
|
||||
const GatewayKnxDaliTarget& target,
|
||||
BridgeOperation operation) {
|
||||
@@ -642,6 +659,17 @@ DaliBridgeResult ErrorResult(uint16_t group_address, const char* message) {
|
||||
return result;
|
||||
}
|
||||
|
||||
DaliBridgeResult IgnoredResult(uint16_t group_address, uint16_t group_object_number,
|
||||
const char* reason) {
|
||||
DaliBridgeResult result;
|
||||
result.sequence = "knx-" + GatewayKnxGroupAddressString(group_address);
|
||||
result.ok = true;
|
||||
result.metadata["ignored"] = true;
|
||||
result.metadata["groupObjectNumber"] = static_cast<int>(group_object_number);
|
||||
result.metadata["reason"] = reason == nullptr ? "ignored" : reason;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool SendAll(int sock, const uint8_t* data, size_t len, const sockaddr_in& remote) {
|
||||
return sendto(sock, data, len, 0, reinterpret_cast<const sockaddr*>(&remote),
|
||||
sizeof(remote)) == static_cast<int>(len);
|
||||
@@ -716,6 +744,11 @@ std::optional<GatewayKnxConfig> GatewayKnxConfigFromValue(const DaliValue* value
|
||||
config.main_group = static_cast<uint8_t>(
|
||||
std::clamp(ObjectIntAny(object, {"mainGroup", "main_group"}).value_or(config.main_group),
|
||||
0, 31));
|
||||
config.dali_bus_id = static_cast<uint8_t>(std::clamp(
|
||||
ObjectIntAny(object, {"daliBusId", "dali_bus_id", "targetDaliBusId",
|
||||
"target_dali_bus_id"})
|
||||
.value_or(config.dali_bus_id),
|
||||
0, 15));
|
||||
config.udp_port = static_cast<uint16_t>(std::clamp(
|
||||
ObjectIntAny(object, {"udpPort", "port", "udp_port"}).value_or(config.udp_port), 1,
|
||||
65535));
|
||||
@@ -790,6 +823,7 @@ DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config) {
|
||||
out["etsDatabaseEnabled"] = config.ets_database_enabled;
|
||||
out["mappingMode"] = GatewayKnxMappingModeToString(config.mapping_mode);
|
||||
out["mainGroup"] = static_cast<int>(config.main_group);
|
||||
out["daliBusId"] = static_cast<int>(config.dali_bus_id);
|
||||
out["udpPort"] = static_cast<int>(config.udp_port);
|
||||
out["multicastAddress"] = config.multicast_address;
|
||||
out["ipInterfaceIndividualAddress"] =
|
||||
@@ -1186,6 +1220,42 @@ DaliBridgeResult GatewayKnxBridge::handleGroupWrite(uint16_t group_address, cons
|
||||
return executeForDecodedWrite(group_address, data_type.value(), target.value(), data, len);
|
||||
}
|
||||
|
||||
DaliBridgeResult GatewayKnxBridge::handleGroupObjectWrite(uint16_t group_object_number,
|
||||
const uint8_t* data, size_t len) {
|
||||
const uint16_t group_address = GwReg1GroupAddressForObject(config_.main_group,
|
||||
group_object_number);
|
||||
const std::string payload = HexBytes(data, len);
|
||||
ESP_LOGI(kTag, "OpenKNX KO write ko=%u derivedGa=%s len=%u payload=%s",
|
||||
static_cast<unsigned>(group_object_number),
|
||||
GatewayKnxGroupAddressString(group_address).c_str(), static_cast<unsigned>(len),
|
||||
payload.c_str());
|
||||
if (!config_.dali_router_enabled) {
|
||||
return ErrorResult(group_address, "KNX to DALI router disabled");
|
||||
}
|
||||
const auto binding = GwReg1BindingForObject(config_.main_group, group_object_number);
|
||||
if (!binding.has_value()) {
|
||||
ESP_LOGW(kTag, "OpenKNX KO write ignored ko=%u: unsupported GW-REG1 object",
|
||||
static_cast<unsigned>(group_object_number));
|
||||
return IgnoredResult(group_address, group_object_number,
|
||||
"unsupported GW-REG1 group object");
|
||||
}
|
||||
DaliBridgeResult result = executeForDecodedWrite(binding->group_address, binding->data_type,
|
||||
binding->target, data, len);
|
||||
result.metadata["source"] = "openknx_group_object";
|
||||
result.metadata["groupObjectNumber"] = static_cast<int>(group_object_number);
|
||||
result.metadata["objectRole"] = binding->object_role;
|
||||
if (result.ok) {
|
||||
ESP_LOGI(kTag, "OpenKNX KO write routed ko=%u role=%s target=%s",
|
||||
static_cast<unsigned>(group_object_number), binding->object_role.c_str(),
|
||||
TargetName(binding->target).c_str());
|
||||
} else {
|
||||
ESP_LOGW(kTag, "OpenKNX KO write failed ko=%u role=%s error=%s",
|
||||
static_cast<unsigned>(group_object_number), binding->object_role.c_str(),
|
||||
result.error.c_str());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool GatewayKnxBridge::handleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id,
|
||||
const uint8_t* data, size_t len,
|
||||
std::vector<uint8_t>* response) {
|
||||
@@ -1719,6 +1789,10 @@ void GatewayKnxTpIpRouter::setGroupWriteHandler(GroupWriteHandler handler) {
|
||||
group_write_handler_ = std::move(handler);
|
||||
}
|
||||
|
||||
void GatewayKnxTpIpRouter::setGroupObjectWriteHandler(GroupObjectWriteHandler handler) {
|
||||
group_object_write_handler_ = std::move(handler);
|
||||
}
|
||||
|
||||
const GatewayKnxConfig& GatewayKnxTpIpRouter::config() const { return config_; }
|
||||
|
||||
bool GatewayKnxTpIpRouter::tpUartOnline() const { return tp_uart_online_; }
|
||||
@@ -1908,8 +1982,24 @@ esp_err_t GatewayKnxTpIpRouter::initializeRuntime() {
|
||||
ESP_LOGD(kTag, "secure KNX group write not routed to DALI: %s", result.error.c_str());
|
||||
}
|
||||
});
|
||||
ets_device_->setGroupObjectWriteHandler(
|
||||
[this](uint16_t group_object_number, const uint8_t* data, size_t len) {
|
||||
const DaliBridgeResult result = group_object_write_handler_
|
||||
? group_object_write_handler_(group_object_number,
|
||||
data, len)
|
||||
: bridge_.handleGroupObjectWrite(group_object_number,
|
||||
data, len);
|
||||
const bool ignored = getObjectBool(result.metadata, "ignored").value_or(false);
|
||||
if (ignored) {
|
||||
const auto reason = getObjectString(result.metadata, "reason").value_or("ignored");
|
||||
ESP_LOGW(kTag, "OpenKNX group object %u accepted by ETS but ignored: %s",
|
||||
static_cast<unsigned>(group_object_number), reason.c_str());
|
||||
} else if (!result.ok && !result.error.empty()) {
|
||||
ESP_LOGW(kTag, "OpenKNX group object %u not routed to DALI: %s",
|
||||
static_cast<unsigned>(group_object_number), result.error.c_str());
|
||||
}
|
||||
});
|
||||
ets_device_->setBusFrameSender([this](const uint8_t* data, size_t len) {
|
||||
routeOpenKnxGroupWrite(data, len, "KNX TP frame");
|
||||
sendTunnelIndication(data, len);
|
||||
sendRoutingIndication(data, len);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user