#include "ets_device_runtime.h" #include "gateway_knx_internal.h" #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" #include #include #include #include #include #include namespace gateway::openknx { namespace { thread_local EtsDeviceRuntime* active_function_property_runtime = nullptr; EtsDeviceRuntime* active_group_object_runtime = nullptr; class ActiveFunctionPropertyRuntimeScope { public: explicit ActiveFunctionPropertyRuntimeScope(EtsDeviceRuntime* runtime) : previous_(active_function_property_runtime) { active_function_property_runtime = runtime; } ~ActiveFunctionPropertyRuntimeScope() { active_function_property_runtime = previous_; } private: EtsDeviceRuntime* previous_; }; constexpr uint16_t kInvalidIndividualAddress = 0xffff; constexpr uint16_t kKnxUnconfiguredBroadcastAddress = 0xffff; // KNX broadcast IA for unconfigured devices bool IsUsableIndividualAddress(uint16_t address) { return address != 0 && address != kInvalidIndividualAddress; } std::string HexBytesString(const uint8_t* data, size_t length) { if (data == nullptr || length == 0) { return {}; } std::string out; out.reserve(length * 3); char buffer[4] = {0}; for (size_t index = 0; index < length; ++index) { std::snprintf(buffer, sizeof(buffer), "%02X", data[index]); out += buffer; if (index + 1 < length) { out.push_back(' '); } } return out; } std::string PrintableOrderNumber(const uint8_t* data, size_t length) { if (data == nullptr || length == 0) { return {}; } std::string out; out.reserve(length); for (size_t index = 0; index < length; ++index) { if (data[index] == 0) { break; } out.push_back(static_cast(data[index])); } return out; } void LogReg1DaliIdentity(const std::string& nvs_namespace, Bau07B0& device) { uint8_t program_version[5] = {0}; if (auto* property = device.parameters().property(PID_PROG_VERSION); property != nullptr) { property->read(program_version); } const std::string hardware_type = HexBytesString(device.deviceObject().hardwareType(), LEN_HARDWARE_TYPE); const std::string program_version_hex = HexBytesString(program_version, sizeof(program_version)); const std::string order_number = PrintableOrderNumber(device.deviceObject().orderNumber(), sizeof(knx_internal::kReg1DaliOrderNumber)); ESP_LOGI("gateway_knx", "OpenKNX identity namespace=%s manufacturer=0x%04x mask=0x%04x deviceVersion=0x%04x hardwareType=%s progVersion=%s order=%s", nvs_namespace.c_str(), device.deviceObject().manufacturerId(), device.deviceObject().maskVersion(), device.deviceObject().version(), hardware_type.c_str(), program_version_hex.c_str(), order_number.c_str()); } void ApplyReg1DaliIdentity(Bau07B0& device, EspIdfPlatform& platform) { device.deviceObject().manufacturerId(knx_internal::kReg1DaliManufacturerId); device.deviceObject().bauNumber(platform.uniqueSerialNumber()); device.deviceObject().hardwareType(knx_internal::kReg1DaliHardwareType); device.deviceObject().orderNumber(knx_internal::kReg1DaliOrderNumber); device.parameters().property(PID_PROG_VERSION)->write( knx_internal::kReg1DaliProgramVersion); } } // namespace EtsDeviceRuntime::EtsDeviceRuntime(std::string nvs_namespace, uint16_t fallback_individual_address, uint16_t tunnel_client_address, std::unique_ptr tp_uart_interface) : nvs_namespace_(std::move(nvs_namespace)), tp_uart_interface_(std::move(tp_uart_interface)), platform_(tp_uart_interface_.get(), nvs_namespace_.c_str()), device_(platform_) { platform_.outboundCemiFrameCallback(&EtsDeviceRuntime::HandleOutboundCemiFrame, this); ApplyReg1DaliIdentity(device_, platform_); if (IsUsableIndividualAddress(fallback_individual_address)) { device_.deviceObject().individualAddress(fallback_individual_address); } 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); } LogReg1DaliIdentity(nvs_namespace_, device_); if (auto* server = device_.getCemiServer()) { server->clientAddress(IsUsableIndividualAddress(tunnel_client_address) ? tunnel_client_address : DefaultTunnelClientAddress( device_.deviceObject().individualAddress())); server->deviceAddressPropertiesTargetClient(false); server->tunnelFrameCallback(&EtsDeviceRuntime::EmitTunnelFrame, this); } device_.functionPropertyCallback(&EtsDeviceRuntime::HandleFunctionPropertyCommand); device_.functionPropertyStateCallback(&EtsDeviceRuntime::HandleFunctionPropertyState); #ifdef USE_DATASECURE device_.secureGroupWriteCallback(&EtsDeviceRuntime::HandleSecureGroupWrite, this); #endif } EtsDeviceRuntime::~EtsDeviceRuntime() { if (tp_uart_interface_ != nullptr) { device_.enabled(false); } platform_.outboundCemiFrameCallback(nullptr, nullptr); #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()) { server->tunnelFrameCallback(nullptr, nullptr); } } uint16_t EtsDeviceRuntime::individualAddress() const { return const_cast(device_).deviceObject().individualAddress(); } uint16_t EtsDeviceRuntime::tunnelClientAddress() const { if (auto* server = const_cast(device_).getCemiServer()) { return server->clientAddress(); } return DefaultTunnelClientAddress(individualAddress()); } bool EtsDeviceRuntime::configured() const { return const_cast(device_).configured(); } bool EtsDeviceRuntime::programmingMode() const { return const_cast(device_).deviceObject().progMode(); } void EtsDeviceRuntime::setProgrammingMode(bool enabled) { device_.deviceObject().progMode(enabled); } void EtsDeviceRuntime::toggleProgrammingMode() { setProgrammingMode(!programmingMode()); } DeviceObject& EtsDeviceRuntime::deviceObject() { return device_.deviceObject(); } Platform& EtsDeviceRuntime::platform() { return platform_; } EtsMemorySnapshot EtsDeviceRuntime::snapshot() const { EtsMemorySnapshot out; auto& device = const_cast(device_); out.configured = device.configured(); out.individual_address = device.deviceObject().individualAddress(); device.forEachEtsAssociation( [](uint16_t group_address, uint16_t group_object_number, void* context) { auto* associations = static_cast*>(context); if (associations != nullptr) { associations->push_back(EtsAssociation{group_address, group_object_number}); } }, &out.associations); std::sort(out.associations.begin(), out.associations.end(), [](const EtsAssociation& lhs, const EtsAssociation& rhs) { if (lhs.group_address != rhs.group_address) { return lhs.group_address < rhs.group_address; } return lhs.group_object_number < rhs.group_object_number; }); out.associations.erase( std::unique(out.associations.begin(), out.associations.end(), [](const EtsAssociation& lhs, const EtsAssociation& rhs) { return lhs.group_address == rhs.group_address && lhs.group_object_number == rhs.group_object_number; }), out.associations.end()); return out; } void EtsDeviceRuntime::setFunctionPropertyHandlers(FunctionPropertyHandler command_handler, FunctionPropertyHandler state_handler) { command_handler_ = std::move(command_handler); state_handler_ = std::move(state_handler); } 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); } void EtsDeviceRuntime::setNetworkInterface(esp_netif_t* netif) { platform_.networkInterface(netif); } bool EtsDeviceRuntime::hasTpUart() const { return tp_uart_interface_ != nullptr; } bool EtsDeviceRuntime::enableTpUart(bool enabled) { if (tp_uart_interface_ == nullptr) { return false; } device_.enabled(enabled); loop(); return !enabled || device_.enabled(); } bool EtsDeviceRuntime::tpUartOnline() const { return tp_uart_interface_ != nullptr && const_cast(device_).enabled(); } bool EtsDeviceRuntime::transmitTpFrame(const uint8_t* data, size_t len) { auto* data_link_layer = device_.getDataLinkLayer(); if (tp_uart_interface_ == nullptr || data_link_layer == nullptr || data == nullptr || len < 2 || !data_link_layer->enabled()) { return false; } std::vector frame_data(data, data + len); CemiFrame frame(frame_data.data(), static_cast(frame_data.size())); if (!frame.valid()) { return false; } return data_link_layer->transmitFrame(frame); } bool EtsDeviceRuntime::handleTunnelFrame(const uint8_t* data, size_t len, CemiFrameSender sender) { auto* server = device_.getCemiServer(); if (server == nullptr || data == nullptr || len < 2) { return false; } std::vector frame_data(data, data + len); CemiFrame frame(frame_data.data(), static_cast(frame_data.size())); const bool consumed = shouldConsumeTunnelFrame(frame); 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; } bool EtsDeviceRuntime::handleBusFrame(const uint8_t* data, size_t len) { auto* data_link_layer = device_.getDataLinkLayer(); if (data_link_layer == nullptr || data == nullptr || len < 2) { return false; } std::vector frame_data(data, data + len); CemiFrame frame(frame_data.data(), static_cast(frame_data.size())); const bool consumed = shouldConsumeBusFrame(frame); if (!consumed) { return false; } data_link_layer->externalFrameReceived(frame); loop(); installGroupObjectCallbacks(); return consumed; } bool EtsDeviceRuntime::emitGroupValue(uint16_t group_object_number, const uint8_t* data, size_t len, CemiFrameSender sender) { if (group_object_number == 0 || data == nullptr || !sender || !device_.configured()) { return false; } auto& table = device_.groupObjectTable(); if (group_object_number > table.entryCount()) { return false; } auto& group_object = table.get(group_object_number); if (len != group_object.valueSize() || group_object.valueRef() == nullptr) { return false; } if (group_object.sizeInTelegram() == 0) { group_object.valueRef()[0] = data[0] & 0x01; } 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; } void EtsDeviceRuntime::loop() { device_.loop(); } bool EtsDeviceRuntime::HandleOutboundCemiFrame(CemiFrame& frame, void* context) { auto* self = static_cast(context); if (self == nullptr || !self->sender_) { return false; } self->sender_(frame.data(), frame.dataLength()); return true; } void EtsDeviceRuntime::EmitTunnelFrame(CemiFrame& frame, void* context) { auto* self = static_cast(context); if (self == nullptr) { return; } if (self->sender_) { self->sender_(frame.data(), frame.dataLength()); return; } if (self->bus_frame_sender_) { self->bus_frame_sender_(frame.data(), frame.dataLength()); } } void EtsDeviceRuntime::HandleSecureGroupWrite(uint16_t group_address, const uint8_t* data, uint8_t data_length, void* context) { auto* self = static_cast(context); if (self == nullptr) { return; } 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(ko.asap()), static_cast(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(ko.asap()), static_cast(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, uint8_t length, uint8_t* data, uint8_t* result_data, uint8_t& result_length) { if (active_function_property_runtime == nullptr) { return false; } return DispatchFunctionProperty(&active_function_property_runtime->command_handler_, object_index, property_id, length, data, result_data, result_length); } bool EtsDeviceRuntime::HandleFunctionPropertyState(uint8_t object_index, 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 DispatchFunctionProperty(&active_function_property_runtime->state_handler_, object_index, 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, uint8_t* result_data, uint8_t& result_length) { if (handler == nullptr || !*handler || result_data == nullptr) { return false; } std::vector response; if (!(*handler)(object_index, 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(); 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(count)); group_object_callback_count_ = count; } } uint16_t EtsDeviceRuntime::DefaultTunnelClientAddress(uint16_t individual_address) { if (!IsUsableIndividualAddress(individual_address)) { return 0x1101; } const uint16_t line_base = individual_address & 0xff00; uint16_t device = static_cast((individual_address & 0x00ff) + 1); if (device == 0 || device > 0xff) { device = 1; } return static_cast(line_base | device); } bool EtsDeviceRuntime::shouldConsumeTunnelFrame(CemiFrame& frame) const { switch (frame.messageCode()) { case M_PropRead_req: case M_PropWrite_req: case M_Reset_req: case M_FuncPropCommand_req: case M_FuncPropStateRead_req: return true; case L_data_req: { if (tpUartOnline()) { return true; } // In commissioning / programming mode ETS may address the device via its // individual address, the cEMI-client tunnel address (device+1), or the // unconfigured broadcast address 0xFFFF. Consume only those; let all // other individual-addressed frames (bus-scan, DeviceDescriptorRead, …) // pass through to the physical TP-UART so real KNX devices on the bus // can reply. const uint16_t dest = frame.destinationAddress(); const uint16_t own_address = individualAddress(); const uint16_t client_address = tunnelClientAddress(); const bool commissioning = !const_cast(device_).configured() || programmingMode(); if (frame.addressType() == IndividualAddress) { if (dest == own_address || dest == client_address || (commissioning && dest == kKnxUnconfiguredBroadcastAddress)) { return true; } } return false; } default: return false; } } bool EtsDeviceRuntime::shouldConsumeBusFrame(CemiFrame& frame) const { #ifdef USE_DATASECURE return frame.messageCode() == L_data_ind && frame.addressType() == GroupAddress && frame.apdu().type() == SecureService; #else return false; #endif } } // namespace gateway::openknx