#include "oam_router_runtime.h" #include "gateway_knx_internal.h" #include "esp_log.h" #include "esp_mac.h" #include "esp_timer.h" #include "knx/cemi_server.h" #include "knx/property.h" #include #include #include #include #include #include extern "C" bool knx_platform_get_fdsk_for_namespace(const char* nvs_namespace, uint8_t* data, size_t len) __attribute__((weak)); namespace gateway::openknx { namespace { constexpr uint16_t kInvalidIndividualAddress = 0xffff; constexpr uint16_t kKnxUnconfiguredBroadcastAddress = 0xffff; constexpr uint8_t kSecureDataPdu = 0; constexpr uint8_t kSecureSyncRequest = 2; constexpr uint8_t kSecureToolAccessFlag = 0x80; constexpr size_t kKnxSerialLength = 6; constexpr size_t kSecureApduScfOffset = 1; constexpr size_t kSecureApduSerialOffset = 8; constexpr size_t kSecureApduMinimumSyncRequestLength = kSecureApduSerialOffset + kKnxSerialLength; constexpr int64_t kSecureToolAccessRouteWindowUs = 120LL * 1000LL * 1000LL; constexpr uint16_t kOamDeviceObjectVersion = 3; bool HardwareTypeEquals(const uint8_t* actual, const uint8_t* expected) { return actual != nullptr && expected != nullptr && std::memcmp(actual, expected, sizeof(knx_internal::kOamRouterHardwareType)) == 0; } VersionCheckResult OamRouterVersionCheck(uint16_t manufacturer_id, uint8_t* hardware_type, uint16_t version) { if (manufacturer_id != knx_internal::kOamRouterManufacturerId || (!HardwareTypeEquals(hardware_type, knx_internal::kOamRouterHardwareType) && !HardwareTypeEquals(hardware_type, knx_internal::kOamRouterLegacyHardwareType))) { return FlashAllInvalid; } return version == kOamDeviceObjectVersion ? FlashValid : FlashTablesInvalid; } bool IsUsableIndividualAddress(uint16_t address) { return address != 0 && address != kInvalidIndividualAddress; } bool IsBroadcastManagementRequest(CemiFrame& frame) { if (frame.addressType() != GroupAddress || frame.destinationAddress() != 0x0000) { return false; } switch (frame.apdu().type()) { case IndividualAddressWrite: case IndividualAddressRead: case IndividualAddressSerialNumberRead: case IndividualAddressSerialNumberWrite: return true; default: return false; } } bool IsGroupBroadcastSecureToolAccess(CemiFrame& frame, uint8_t* service) { if (frame.addressType() != GroupAddress || frame.destinationAddress() != 0x0000 || frame.apdu().type() != SecureService || frame.apdu().length() <= kSecureApduScfOffset) { return false; } const uint8_t scf = frame.apdu().data()[kSecureApduScfOffset]; if ((scf & kSecureToolAccessFlag) == 0) { return false; } if (service != nullptr) { *service = scf & 0x07; } return true; } uint32_t OamBauNumberFromBaseMac() { uint8_t mac[6]{}; if (esp_efuse_mac_get_default(mac) != ESP_OK && esp_read_mac(mac, ESP_MAC_WIFI_STA) != ESP_OK) { return 0; } uint32_t suffix = (static_cast(mac[2]) << 24) | (static_cast(mac[3]) << 16) | (static_cast(mac[4]) << 8) | static_cast(mac[5]); return suffix + knx_internal::kOamRouterSerialMacIncrement; } #if defined(ENABLE_BAU091A_PERSONA) void ApplyOamRouterIdentity(Bau091A& device) { device.deviceObject().manufacturerId(knx_internal::kOamRouterManufacturerId); device.deviceObject().bauNumber(OamBauNumberFromBaseMac()); device.deviceObject().hardwareType(knx_internal::kOamRouterHardwareType); device.deviceObject().orderNumber(knx_internal::kOamRouterOrderNumber); if (auto* property = device.parameters().property(PID_PROG_VERSION); property != nullptr) { property->write(knx_internal::kOamRouterProgramVersion); } } void ApplyOamFactoryFdsk(Bau091A& device, const std::string& nvs_namespace) { #if defined(USE_DATASECURE) if (knx_platform_get_fdsk_for_namespace == nullptr) { return; } std::array fdsk{}; if (knx_platform_get_fdsk_for_namespace(nvs_namespace.c_str(), fdsk.data(), fdsk.size())) { device.factoryFdsk(fdsk.data(), fdsk.size()); } std::fill(fdsk.begin(), fdsk.end(), 0); #else (void)device; (void)nvs_namespace; #endif } #endif } // namespace OamRouterRuntime::OamRouterRuntime(std::string nvs_namespace, uint16_t fallback_individual_address, uint16_t tunnel_client_address) : nvs_namespace_(std::move(nvs_namespace)) #if defined(ENABLE_BAU091A_PERSONA) , platform_(nullptr, nvs_namespace_.c_str()), device_(platform_) #endif { #if defined(ENABLE_BAU091A_PERSONA) platform_.outboundCemiFrameCallback(&OamRouterRuntime::HandleOutboundCemiFrame, this); ApplyOamFactoryFdsk(device_, nvs_namespace_); ApplyOamRouterIdentity(device_); device_.versionCheckCallback(&OamRouterVersionCheck); if (IsUsableIndividualAddress(fallback_individual_address)) { device_.deviceObject().individualAddress(fallback_individual_address); } ESP_LOGI("gateway_knx", "OAM OpenKNX loading memory namespace=%s", nvs_namespace_.c_str()); device_.readMemory(); ApplyOamFactoryFdsk(device_, nvs_namespace_); ApplyOamRouterIdentity(device_); if (!IsUsableIndividualAddress(device_.deviceObject().individualAddress()) && IsUsableIndividualAddress(fallback_individual_address)) { device_.deviceObject().individualAddress(fallback_individual_address); } #ifdef USE_CEMI_SERVER if (auto* server = device_.getCemiServer()) { server->clientAddress(IsUsableIndividualAddress(tunnel_client_address) ? tunnel_client_address : DefaultTunnelClientAddress( device_.deviceObject().individualAddress())); server->deviceAddressPropertiesTargetClient(false); server->tunnelFrameCallback(&OamRouterRuntime::EmitTunnelFrame, this); } #endif uint8_t program_version[5]{}; if (auto* property = device_.parameters().property(PID_PROG_VERSION); property != nullptr) { property->read(program_version); } ESP_LOGI("gateway_knx", "OAM router runtime namespace=%s configured=%d manufacturer=0x%04x mask=0x%04x device=0x%04x tunnelClient=0x%04x progVersion=%02X %02X %02X %02X %02X", nvs_namespace_.c_str(), device_.configured(), device_.deviceObject().manufacturerId(), device_.deviceObject().maskVersion(), device_.deviceObject().individualAddress(), tunnelClientAddress(), program_version[0], program_version[1], program_version[2], program_version[3], program_version[4]); #else (void)fallback_individual_address; (void)tunnel_client_address; #endif } OamRouterRuntime::~OamRouterRuntime() { #if defined(ENABLE_BAU091A_PERSONA) platform_.outboundCemiFrameCallback(nullptr, nullptr); #ifdef USE_CEMI_SERVER if (auto* server = device_.getCemiServer()) { server->tunnelFrameCallback(nullptr, nullptr); } #endif #endif } bool OamRouterRuntime::available() const { #if defined(ENABLE_BAU091A_PERSONA) return true; #else return false; #endif } uint16_t OamRouterRuntime::individualAddress() const { #if defined(ENABLE_BAU091A_PERSONA) return const_cast(device_).deviceObject().individualAddress(); #else return 0xffff; #endif } uint16_t OamRouterRuntime::tunnelClientAddress() const { #if defined(ENABLE_BAU091A_PERSONA) && defined(USE_CEMI_SERVER) if (auto* server = const_cast(device_).getCemiServer()) { return server->clientAddress(); } #endif return DefaultTunnelClientAddress(individualAddress()); } bool OamRouterRuntime::configured() const { #if defined(ENABLE_BAU091A_PERSONA) return const_cast(device_).configured(); #else return false; #endif } bool OamRouterRuntime::programmingMode() const { #if defined(ENABLE_BAU091A_PERSONA) return const_cast(device_).deviceObject().progMode(); #else return false; #endif } void OamRouterRuntime::setProgrammingMode(bool enabled) { #if defined(ENABLE_BAU091A_PERSONA) device_.deviceObject().progMode(enabled); #else (void)enabled; #endif } void OamRouterRuntime::toggleProgrammingMode() { setProgrammingMode(!programmingMode()); } bool OamRouterRuntime::matchesSecureSyncSerial(CemiFrame& frame) const { #if defined(ENABLE_BAU091A_PERSONA) && defined(USE_DATASECURE) uint8_t service = 0; if (!IsGroupBroadcastSecureToolAccess(frame, &service) || frame.apdu().length() < kSecureApduMinimumSyncRequestLength) { return false; } if (service != kSecureSyncRequest) { return false; } const uint8_t* apdu_data = frame.apdu().data(); const uint8_t* serial = apdu_data + kSecureApduSerialOffset; const uint8_t* local_serial = const_cast(device_).deviceObject().propertyData(PID_SERIAL_NUMBER); const bool matches = local_serial != nullptr && std::memcmp(serial, local_serial, kKnxSerialLength) == 0; if (matches) { recent_secure_tool_source_ = frame.sourceAddress(); recent_secure_tool_sync_us_ = esp_timer_get_time(); } return matches; #else (void)frame; return false; #endif } bool OamRouterRuntime::matchesRecentSecureToolAccess(CemiFrame& frame) const { #if defined(ENABLE_BAU091A_PERSONA) && defined(USE_DATASECURE) uint8_t service = 0; if (!IsGroupBroadcastSecureToolAccess(frame, &service) || service != kSecureDataPdu || recent_secure_tool_sync_us_ <= 0 || frame.sourceAddress() != recent_secure_tool_source_) { return false; } const int64_t now = esp_timer_get_time(); if (now < recent_secure_tool_sync_us_ || now - recent_secure_tool_sync_us_ > kSecureToolAccessRouteWindowUs) { return false; } recent_secure_tool_sync_us_ = now; return true; #else (void)frame; return false; #endif } EtsMemorySnapshot OamRouterRuntime::snapshot() const { EtsMemorySnapshot out; #if defined(ENABLE_BAU091A_PERSONA) auto& device = const_cast(device_); out.configured = device.configured(); out.individual_address = device.deviceObject().individualAddress(); #endif return out; } DeviceObject* OamRouterRuntime::deviceObject() { #if defined(ENABLE_BAU091A_PERSONA) return &device_.deviceObject(); #else return nullptr; #endif } Platform* OamRouterRuntime::platform() { #if defined(ENABLE_BAU091A_PERSONA) return &platform_; #else return nullptr; #endif } void OamRouterRuntime::setNetworkInterface(esp_netif_t* netif) { #if defined(ENABLE_BAU091A_PERSONA) platform_.networkInterface(netif); #else (void)netif; #endif } void OamRouterRuntime::setBusFrameSender(CemiFrameSender sender) { bus_frame_sender_ = std::move(sender); } bool OamRouterRuntime::handleTunnelFrame(const uint8_t* data, size_t len, CemiFrameSender sender) { #if defined(ENABLE_BAU091A_PERSONA) && defined(USE_CEMI_SERVER) 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())); if (!shouldConsumeTunnelFrame(frame)) { return false; } sender_ = std::move(sender); server->frameReceived(frame); loop(); sender_ = nullptr; return true; #else (void)data; (void)len; (void)sender; return false; #endif } bool OamRouterRuntime::handleLocalBroadcastManagementFrame(const uint8_t* data, size_t len, CemiFrameSender sender) { #if defined(ENABLE_BAU091A_PERSONA) if (data == nullptr || len < 2) { return false; } std::vector frame_data(data, data + len); CemiFrame frame(frame_data.data(), static_cast(frame_data.size())); if (!frame.valid() || !IsBroadcastManagementRequest(frame)) { return false; } const HopCountType hop_type = frame.npdu().hopCount() == 7 ? UnlimitedRouting : NetworkLayerParameter; sender_ = std::move(sender); device_.injectDataBroadcastIndication(hop_type, frame.priority(), frame.sourceAddress(), frame.apdu()); loop(); sender_ = nullptr; return true; #else (void)data; (void)len; (void)sender; return false; #endif } bool OamRouterRuntime::handleBusFrame(const uint8_t* data, size_t len, CemiFrameSender sender) { #if defined(ENABLE_BAU091A_PERSONA) auto* data_link_layer = device_.getSecondaryDataLinkLayer(); 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())); if (!shouldConsumeBusFrame(frame)) { return false; } if (frame.messageCode() == L_data_req) { frame.messageCode(L_data_ind); } sender_ = std::move(sender); data_link_layer->externalFrameReceived(frame); loop(); sender_ = nullptr; return true; #else (void)data; (void)len; (void)sender; return false; #endif } void OamRouterRuntime::loop() { #if defined(ENABLE_BAU091A_PERSONA) device_.loop(); #endif } bool OamRouterRuntime::HandleOutboundCemiFrame(CemiFrame& frame, void* context) { auto* self = static_cast(context); if (self == nullptr) { return false; } if (self->sender_) { self->sender_(frame.data(), frame.dataLength()); return true; } if (self->bus_frame_sender_) { self->bus_frame_sender_(frame.data(), frame.dataLength()); return true; } return false; } void OamRouterRuntime::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()); } } uint16_t OamRouterRuntime::DefaultTunnelClientAddress(uint16_t individual_address) { if (!IsUsableIndividualAddress(individual_address)) { return 0x1102; } const uint16_t line_base = individual_address & 0xff00; uint16_t device = static_cast((individual_address & 0x00ff) + 1); if (device == 0 || device > 0xff) { device = 2; } return static_cast(line_base | device); } bool OamRouterRuntime::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: { const uint16_t dest = frame.destinationAddress(); const bool commissioning = !configured() || programmingMode(); if (frame.addressType() == IndividualAddress) { return dest == individualAddress() || dest == tunnelClientAddress() || (commissioning && dest == kKnxUnconfiguredBroadcastAddress); } if (matchesSecureSyncSerial(frame)) { return true; } if (matchesRecentSecureToolAccess(frame)) { return true; } if (IsBroadcastManagementRequest(frame)) { return true; } return false; } default: return false; } } bool OamRouterRuntime::shouldConsumeBusFrame(CemiFrame& frame) const { if (frame.messageCode() != L_data_ind && frame.messageCode() != L_data_req) { return false; } if (IsBroadcastManagementRequest(frame)) { return true; } if (matchesSecureSyncSerial(frame)) { return true; } if (matchesRecentSecureToolAccess(frame)) { return true; } if (frame.addressType() != IndividualAddress) { return false; } const uint16_t dest = frame.destinationAddress(); const bool commissioning = !configured() || programmingMode(); return dest == individualAddress() || dest == tunnelClientAddress() || (commissioning && dest == kKnxUnconfiguredBroadcastAddress); } } // namespace gateway::openknx