Add EtsDeviceRuntime class for handling KNX device runtime operations

- Introduced EtsDeviceRuntime class to manage device runtime functionalities including handling tunnel frames and function property commands.
- Added support for individual address management and memory snapshot retrieval.
- Updated EtsMemorySnapshot structure to include individual address.
- Implemented identity application for DALI devices in the memory loader.
- Enhanced CMakeLists.txt to include new source files and compile definitions.
- Updated header files to include new dependencies and declarations.
- Refactored existing memory loading logic to accommodate new device runtime features.

Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
Tony
2026-05-12 05:19:14 +08:00
parent d231460612
commit 36d10702da
14 changed files with 1034 additions and 41 deletions
@@ -0,0 +1,227 @@
#include "openknx_idf/ets_device_runtime.h"
#include "knx/cemi_server.h"
#include "knx/property.h"
#include <algorithm>
#include <cstdint>
#include <utility>
#include <vector>
namespace gateway::openknx {
namespace {
thread_local EtsDeviceRuntime* active_function_property_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 kReg1DaliManufacturerId = 0x00a4;
constexpr uint8_t kReg1DaliApplicationNumber = 0x01;
constexpr uint8_t kReg1DaliApplicationVersion = 0x05;
constexpr uint8_t kReg1DaliOrderNumber[10] = {'R', 'E', 'G', '1', '-', 'D', 'a', 'l', 'i', 0};
bool IsUsableIndividualAddress(uint16_t address) {
return address != 0 && address != kInvalidIndividualAddress;
}
void ApplyReg1DaliIdentity(Bau07B0& device, EspIdfPlatform& platform) {
device.deviceObject().manufacturerId(kReg1DaliManufacturerId);
device.deviceObject().bauNumber(platform.uniqueSerialNumber());
device.deviceObject().orderNumber(kReg1DaliOrderNumber);
const uint8_t program_version[5] = {0x00, 0xa4, 0x00, kReg1DaliApplicationNumber,
kReg1DaliApplicationVersion};
device.parameters().property(PID_PROG_VERSION)->write(program_version);
}
} // namespace
EtsDeviceRuntime::EtsDeviceRuntime(std::string nvs_namespace,
uint16_t fallback_individual_address)
: nvs_namespace_(std::move(nvs_namespace)),
platform_(nullptr, nvs_namespace_.c_str()),
device_(platform_) {
ApplyReg1DaliIdentity(device_, platform_);
if (IsUsableIndividualAddress(fallback_individual_address)) {
device_.deviceObject().individualAddress(fallback_individual_address);
}
device_.readMemory();
if (!IsUsableIndividualAddress(device_.deviceObject().individualAddress()) &&
IsUsableIndividualAddress(fallback_individual_address)) {
device_.deviceObject().individualAddress(fallback_individual_address);
}
if (auto* server = device_.getCemiServer()) {
server->clientAddress(DefaultTunnelClientAddress(device_.deviceObject().individualAddress()));
server->tunnelFrameCallback(&EtsDeviceRuntime::EmitTunnelFrame, this);
}
device_.functionPropertyCallback(&EtsDeviceRuntime::HandleFunctionPropertyCommand);
device_.functionPropertyStateCallback(&EtsDeviceRuntime::HandleFunctionPropertyState);
}
EtsDeviceRuntime::~EtsDeviceRuntime() {
device_.functionPropertyCallback(nullptr);
device_.functionPropertyStateCallback(nullptr);
if (auto* server = device_.getCemiServer()) {
server->tunnelFrameCallback(nullptr, nullptr);
}
}
uint16_t EtsDeviceRuntime::individualAddress() const {
return const_cast<Bau07B0&>(device_).deviceObject().individualAddress();
}
uint16_t EtsDeviceRuntime::tunnelClientAddress() const {
if (auto* server = const_cast<Bau07B0&>(device_).getCemiServer()) {
return server->clientAddress();
}
return DefaultTunnelClientAddress(individualAddress());
}
bool EtsDeviceRuntime::configured() const { return const_cast<Bau07B0&>(device_).configured(); }
EtsMemorySnapshot EtsDeviceRuntime::snapshot() const {
EtsMemorySnapshot out;
auto& device = const_cast<Bau07B0&>(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<std::vector<EtsAssociation>*>(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);
}
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<uint8_t> frame_data(data, data + len);
CemiFrame frame(frame_data.data(), static_cast<uint16_t>(frame_data.size()));
const bool consumed = shouldConsumeTunnelFrame(frame);
if (!consumed) {
return false;
}
sender_ = std::move(sender);
ActiveFunctionPropertyRuntimeScope callback_scope(this);
server->frameReceived(frame);
loop();
sender_ = nullptr;
return consumed;
}
void EtsDeviceRuntime::loop() { device_.loop(); }
void EtsDeviceRuntime::EmitTunnelFrame(CemiFrame& frame, void* context) {
auto* self = static_cast<EtsDeviceRuntime*>(context);
if (self == nullptr || !self->sender_) {
return;
}
self->sender_(frame.data(), frame.dataLength());
}
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<uint8_t> response;
if (!(*handler)(object_index, 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;
}
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<uint16_t>((individual_address & 0x00ff) + 1);
if (device == 0 || device > 0xff) {
device = 1;
}
return static_cast<uint16_t>(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:
return frame.addressType() == IndividualAddress &&
frame.destinationAddress() == individualAddress();
default:
return false;
}
}
} // namespace gateway::openknx
@@ -3,8 +3,11 @@
#include "openknx_idf/esp_idf_platform.h"
#include "knx/bau07B0.h"
#include "knx/property.h"
#include <algorithm>
#include <cstdint>
#include <memory>
namespace gateway::openknx {
namespace {
@@ -18,18 +21,45 @@ void CollectAssociation(uint16_t group_address, uint16_t group_object_number,
associations->push_back(EtsAssociation{group_address, group_object_number});
}
bool IsErasedMemory(const uint8_t* data, size_t size) {
if (data == nullptr) {
return false;
}
return std::all_of(data, data + size, [](uint8_t value) { return value == 0xff; });
}
constexpr uint16_t kReg1DaliManufacturerId = 0x00a4;
constexpr uint8_t kReg1DaliApplicationNumber = 0x01;
constexpr uint8_t kReg1DaliApplicationVersion = 0x05;
constexpr uint8_t kReg1DaliOrderNumber[10] = {'R', 'E', 'G', '1', '-', 'D', 'a', 'l', 'i', 0};
void ApplyReg1DaliIdentity(Bau07B0& device, EspIdfPlatform& platform) {
device.deviceObject().manufacturerId(kReg1DaliManufacturerId);
device.deviceObject().bauNumber(platform.uniqueSerialNumber());
device.deviceObject().orderNumber(kReg1DaliOrderNumber);
const uint8_t program_version[5] = {0x00, 0xa4, 0x00, kReg1DaliApplicationNumber,
kReg1DaliApplicationVersion};
device.parameters().property(PID_PROG_VERSION)->write(program_version);
}
} // namespace
EtsMemorySnapshot LoadEtsMemorySnapshot(const std::string& nvs_namespace) {
EspIdfPlatform platform(nullptr, nvs_namespace.c_str());
Bau07B0 device(platform);
device.deviceObject().manufacturerId(0xfa);
device.deviceObject().bauNumber(platform.uniqueSerialNumber());
device.readMemory();
EtsMemorySnapshot snapshot;
snapshot.configured = device.configured();
device.forEachEtsAssociation(CollectAssociation, &snapshot.associations);
const uint8_t* memory = platform.getNonVolatileMemoryStart();
const size_t memory_size = platform.getNonVolatileMemorySize();
if (memory == nullptr || memory_size == 0 || IsErasedMemory(memory, memory_size)) {
return snapshot;
}
auto device = std::make_unique<Bau07B0>(platform);
ApplyReg1DaliIdentity(*device, platform);
device->readMemory();
snapshot.configured = device->configured();
snapshot.individual_address = device->deviceObject().individualAddress();
device->forEachEtsAssociation(CollectAssociation, &snapshot.associations);
std::sort(snapshot.associations.begin(), snapshot.associations.end(),
[](const EtsAssociation& lhs, const EtsAssociation& rhs) {
if (lhs.group_address != rhs.group_address) {