f39ae6f0c6
Signed-off-by: Tony <tonylu@tony-cloud.com>
513 lines
16 KiB
C++
513 lines
16 KiB
C++
#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 <algorithm>
|
|
#include <array>
|
|
#include <cstddef>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <utility>
|
|
|
|
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<uint32_t>(mac[2]) << 24) |
|
|
(static_cast<uint32_t>(mac[3]) << 16) |
|
|
(static_cast<uint32_t>(mac[4]) << 8) |
|
|
static_cast<uint32_t>(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<uint8_t, 16> 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<Bau091A&>(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<Bau091A&>(device_).getCemiServer()) {
|
|
return server->clientAddress();
|
|
}
|
|
#endif
|
|
return DefaultTunnelClientAddress(individualAddress());
|
|
}
|
|
|
|
bool OamRouterRuntime::configured() const {
|
|
#if defined(ENABLE_BAU091A_PERSONA)
|
|
return const_cast<Bau091A&>(device_).configured();
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
bool OamRouterRuntime::programmingMode() const {
|
|
#if defined(ENABLE_BAU091A_PERSONA)
|
|
return const_cast<Bau091A&>(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<Bau091A&>(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<Bau091A&>(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<uint8_t> frame_data(data, data + len);
|
|
CemiFrame frame(frame_data.data(), static_cast<uint16_t>(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<uint8_t> frame_data(data, data + len);
|
|
CemiFrame frame(frame_data.data(), static_cast<uint16_t>(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<uint8_t> frame_data(data, data + len);
|
|
CemiFrame frame(frame_data.data(), static_cast<uint16_t>(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<OamRouterRuntime*>(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<OamRouterRuntime*>(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<uint16_t>((individual_address & 0x00ff) + 1);
|
|
if (device == 0 || device > 0xff) {
|
|
device = 2;
|
|
}
|
|
return static_cast<uint16_t>(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
|