Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 36d10702da | |||
| d231460612 |
@@ -1,3 +1,4 @@
|
||||
**/build/
|
||||
build/
|
||||
**/managed_components/
|
||||
.DS_Store
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
[submodule "knx_dali_gw"]
|
||||
path = knx_dali_gw
|
||||
url = https://git.tonycloud.org/knx/GW-REG1-Dali.git
|
||||
branch = v1
|
||||
branch = tonycloud-dev
|
||||
[submodule "tpuart"]
|
||||
path = tpuart
|
||||
url = https://git.tonycloud.org/knx/tpuart.git
|
||||
|
||||
@@ -12,7 +12,7 @@ This folder hosts the native ESP-IDF C++ rewrite of the Lua DALI gateway.
|
||||
- `dali_domain/`: native DALI domain facade over `dali_cpp` and raw frame sinks.
|
||||
- `gateway_cache/`: DALI scene/group/settings/runtime cache used by controller reconciliation and protocol bridges.
|
||||
- `gateway_bridge/`: per-channel bridge provisioning, command execution, protocol startup, and HTTP bridge actions.
|
||||
- `openknx_idf/`: ESP-IDF port layer for the OpenKNX `gateway/knx` and `gateway/tpuart` submodules, including NVS-backed OpenKNX memory, UDP multicast/unicast plumbing, and a native TP-UART interface without the Arduino framework.
|
||||
- `openknx_idf/`: ESP-IDF port layer for the OpenKNX `gateway/knx` and `gateway/tpuart` submodules, including NVS-backed OpenKNX memory, ETS cEMI programming support, UDP multicast/unicast plumbing, and a native TP-UART interface without the Arduino framework.
|
||||
- `gateway_modbus/`: gateway-owned Modbus TCP/RTU/ASCII config, generated DALI point tables, and provisioned Modbus model override dispatch.
|
||||
- `gateway_bacnet/`: BACnet/IP server adapter backed by bacnet-stack, including the gateway-owned BACnet bridge model adapter.
|
||||
- `gateway_ble/`: NimBLE GATT bridge for BLE transport parity on `FFF1`/`FFF2`/`FFF3`, including raw DALI notifications.
|
||||
|
||||
+15
-11
@@ -666,7 +666,20 @@ CONFIG_GATEWAY_MODBUS_TCP_PORT=1502
|
||||
CONFIG_GATEWAY_MODBUS_UNIT_ID=1
|
||||
CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED=y
|
||||
# CONFIG_GATEWAY_START_BACNET_BRIDGE_ENABLED is not set
|
||||
# CONFIG_GATEWAY_KNX_BRIDGE_SUPPORTED is not set
|
||||
CONFIG_GATEWAY_KNX_BRIDGE_SUPPORTED=y
|
||||
CONFIG_GATEWAY_START_KNX_BRIDGE_ENABLED=y
|
||||
CONFIG_GATEWAY_KNX_MAIN_GROUP=0
|
||||
CONFIG_GATEWAY_KNX_TUNNEL_ENABLED=y
|
||||
CONFIG_GATEWAY_KNX_MULTICAST_ENABLED=y
|
||||
CONFIG_GATEWAY_KNX_UDP_PORT=3671
|
||||
CONFIG_GATEWAY_KNX_MULTICAST_ADDRESS="224.0.23.12"
|
||||
CONFIG_GATEWAY_KNX_INDIVIDUAL_ADDRESS=4353
|
||||
CONFIG_GATEWAY_KNX_TP_UART_PORT=0
|
||||
CONFIG_GATEWAY_KNX_TP_TX_PIN=-1
|
||||
CONFIG_GATEWAY_KNX_TP_RX_PIN=-1
|
||||
CONFIG_GATEWAY_KNX_TP_BAUDRATE=19200
|
||||
CONFIG_GATEWAY_BRIDGE_KNX_TASK_STACK_SIZE=8192
|
||||
CONFIG_GATEWAY_BRIDGE_KNX_TASK_PRIORITY=5
|
||||
CONFIG_GATEWAY_CLOUD_BRIDGE_SUPPORTED=y
|
||||
# CONFIG_GATEWAY_START_CLOUD_BRIDGE_ENABLED is not set
|
||||
CONFIG_GATEWAY_BRIDGE_MODBUS_TASK_STACK_SIZE=6144
|
||||
@@ -675,16 +688,7 @@ CONFIG_GATEWAY_BRIDGE_BACNET_TASK_STACK_SIZE=8192
|
||||
CONFIG_GATEWAY_BRIDGE_BACNET_TASK_PRIORITY=5
|
||||
CONFIG_GATEWAY_USB_STARTUP_DEBUG_JTAG=y
|
||||
# CONFIG_GATEWAY_USB_STARTUP_SETUP_SERIAL is not set
|
||||
CONFIG_GATEWAY_485_CONTROL_ENABLED=y
|
||||
CONFIG_GATEWAY_485_CONTROL_BAUDRATE=9600
|
||||
CONFIG_GATEWAY_485_CONTROL_TX_PIN=-1
|
||||
CONFIG_GATEWAY_485_CONTROL_RX_PIN=-1
|
||||
CONFIG_GATEWAY_485_CONTROL_RX_BUFFER=256
|
||||
CONFIG_GATEWAY_485_CONTROL_TX_BUFFER=256
|
||||
CONFIG_GATEWAY_485_CONTROL_READ_TIMEOUT_MS=20
|
||||
CONFIG_GATEWAY_485_CONTROL_WRITE_TIMEOUT_MS=20
|
||||
CONFIG_GATEWAY_485_CONTROL_TASK_STACK_SIZE=4096
|
||||
CONFIG_GATEWAY_485_CONTROL_TASK_PRIORITY=4
|
||||
# CONFIG_GATEWAY_485_CONTROL_ENABLED is not set
|
||||
# end of Gateway Startup Services
|
||||
|
||||
#
|
||||
|
||||
@@ -666,6 +666,7 @@ CONFIG_GATEWAY_MODBUS_TCP_PORT=1502
|
||||
CONFIG_GATEWAY_MODBUS_UNIT_ID=1
|
||||
CONFIG_GATEWAY_BACNET_BRIDGE_SUPPORTED=y
|
||||
# CONFIG_GATEWAY_START_BACNET_BRIDGE_ENABLED is not set
|
||||
# CONFIG_GATEWAY_KNX_BRIDGE_SUPPORTED is not set
|
||||
CONFIG_GATEWAY_CLOUD_BRIDGE_SUPPORTED=y
|
||||
# CONFIG_GATEWAY_START_CLOUD_BRIDGE_ENABLED is not set
|
||||
CONFIG_GATEWAY_BRIDGE_MODBUS_TASK_STACK_SIZE=6144
|
||||
@@ -744,6 +745,7 @@ CONFIG_COMPILER_ORPHAN_SECTIONS_WARNING=y
|
||||
#
|
||||
# CONFIG_APPTRACE_DEST_JTAG is not set
|
||||
CONFIG_APPTRACE_DEST_NONE=y
|
||||
# CONFIG_APPTRACE_DEST_UART0 is not set
|
||||
# CONFIG_APPTRACE_DEST_UART1 is not set
|
||||
# CONFIG_APPTRACE_DEST_UART2 is not set
|
||||
# CONFIG_APPTRACE_DEST_USB_CDC is not set
|
||||
@@ -1787,18 +1789,15 @@ CONFIG_ESP_MAIN_TASK_AFFINITY_CPU0=y
|
||||
# CONFIG_ESP_MAIN_TASK_AFFINITY_NO_AFFINITY is not set
|
||||
CONFIG_ESP_MAIN_TASK_AFFINITY=0x0
|
||||
CONFIG_ESP_MINIMAL_SHARED_STACK_SIZE=2048
|
||||
CONFIG_ESP_CONSOLE_UART_DEFAULT=y
|
||||
# CONFIG_ESP_CONSOLE_UART_DEFAULT is not set
|
||||
# CONFIG_ESP_CONSOLE_USB_CDC is not set
|
||||
# CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG is not set
|
||||
CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y
|
||||
# CONFIG_ESP_CONSOLE_UART_CUSTOM is not set
|
||||
# CONFIG_ESP_CONSOLE_NONE is not set
|
||||
# CONFIG_ESP_CONSOLE_SECONDARY_NONE is not set
|
||||
CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG=y
|
||||
CONFIG_ESP_CONSOLE_SECONDARY_NONE=y
|
||||
CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG_ENABLED=y
|
||||
CONFIG_ESP_CONSOLE_UART=y
|
||||
CONFIG_ESP_CONSOLE_UART_NUM=0
|
||||
CONFIG_ESP_CONSOLE_ROM_SERIAL_PORT_NUM=0
|
||||
CONFIG_ESP_CONSOLE_UART_BAUDRATE=115200
|
||||
CONFIG_ESP_CONSOLE_UART_NUM=-1
|
||||
CONFIG_ESP_CONSOLE_ROM_SERIAL_PORT_NUM=4
|
||||
CONFIG_ESP_INT_WDT=y
|
||||
CONFIG_ESP_INT_WDT_TIMEOUT_MS=300
|
||||
CONFIG_ESP_INT_WDT_CHECK_CPU1=y
|
||||
|
||||
@@ -1276,7 +1276,8 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
return result;
|
||||
}
|
||||
return knx->handleCemiFrame(data, len);
|
||||
});
|
||||
},
|
||||
openKnxNamespace());
|
||||
if (knx_config.has_value()) {
|
||||
knx->setConfig(knx_config.value());
|
||||
knx_router->setConfig(knx_config.value());
|
||||
@@ -1308,10 +1309,15 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
return;
|
||||
}
|
||||
const auto snapshot = openknx::LoadEtsMemorySnapshot(openKnxNamespace());
|
||||
if (snapshot.associations.empty()) {
|
||||
const bool has_downloaded_address = snapshot.individual_address != 0 &&
|
||||
snapshot.individual_address != 0xffff;
|
||||
if (!snapshot.configured && !has_downloaded_address && snapshot.associations.empty()) {
|
||||
return;
|
||||
}
|
||||
GatewayKnxConfig updated = active_config.value();
|
||||
if (has_downloaded_address) {
|
||||
updated.individual_address = snapshot.individual_address;
|
||||
}
|
||||
updated.ets_associations.clear();
|
||||
updated.ets_associations.reserve(snapshot.associations.size());
|
||||
for (const auto& association : snapshot.associations) {
|
||||
@@ -1319,9 +1325,9 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
association.group_address, association.group_object_number});
|
||||
}
|
||||
knx_config = std::move(updated);
|
||||
ESP_LOGI(kTag, "gateway=%u loaded %u OpenKNX ETS associations from NVS namespace %s",
|
||||
channel.gateway_id, static_cast<unsigned>(snapshot.associations.size()),
|
||||
openKnxNamespace().c_str());
|
||||
ESP_LOGI(kTag, "gateway=%u loaded OpenKNX ETS address=0x%04x associations=%u from NVS namespace %s",
|
||||
channel.gateway_id, snapshot.individual_address,
|
||||
static_cast<unsigned>(snapshot.associations.size()), openKnxNamespace().c_str());
|
||||
}
|
||||
|
||||
std::optional<DaliDomainSnapshot> diagnosticSnapshotLocked(int short_address,
|
||||
|
||||
@@ -13,12 +13,17 @@
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace gateway {
|
||||
|
||||
namespace openknx {
|
||||
class EtsDeviceRuntime;
|
||||
}
|
||||
|
||||
constexpr uint16_t kGatewayKnxDefaultUdpPort = 3671;
|
||||
constexpr const char* kGatewayKnxDefaultMulticastAddress = "224.0.23.12";
|
||||
constexpr uint32_t kGatewayKnxDefaultTpBaudrate = 19200;
|
||||
@@ -96,6 +101,13 @@ struct GatewayKnxDaliBinding {
|
||||
GatewayKnxDaliTarget target;
|
||||
};
|
||||
|
||||
struct GatewayKnxCommissioningBallast {
|
||||
uint8_t high{0};
|
||||
uint8_t middle{0};
|
||||
uint8_t low{0};
|
||||
uint8_t short_address{0xff};
|
||||
};
|
||||
|
||||
std::optional<GatewayKnxConfig> GatewayKnxConfigFromValue(const DaliValue* value);
|
||||
DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config);
|
||||
|
||||
@@ -122,6 +134,12 @@ class GatewayKnxBridge {
|
||||
DaliBridgeResult handleCemiFrame(const uint8_t* data, size_t len);
|
||||
DaliBridgeResult handleGroupWrite(uint16_t group_address, const uint8_t* data,
|
||||
size_t len);
|
||||
bool handleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id,
|
||||
const uint8_t* data, size_t len,
|
||||
std::vector<uint8_t>* response);
|
||||
bool handleFunctionPropertyState(uint8_t object_index, uint8_t property_id,
|
||||
const uint8_t* data, size_t len,
|
||||
std::vector<uint8_t>* response);
|
||||
|
||||
private:
|
||||
DaliBridgeResult executeForDecodedWrite(uint16_t group_address,
|
||||
@@ -133,16 +151,43 @@ class GatewayKnxBridge {
|
||||
const uint8_t* data, size_t len);
|
||||
void rebuildEtsBindings();
|
||||
|
||||
bool handleReg1TypeCommand(const uint8_t* data, size_t len,
|
||||
std::vector<uint8_t>* response);
|
||||
bool handleReg1ScanCommand(const uint8_t* data, size_t len,
|
||||
std::vector<uint8_t>* response);
|
||||
bool handleReg1AssignCommand(const uint8_t* data, size_t len,
|
||||
std::vector<uint8_t>* response);
|
||||
bool handleReg1EvgWriteCommand(const uint8_t* data, size_t len,
|
||||
std::vector<uint8_t>* response);
|
||||
bool handleReg1EvgReadCommand(const uint8_t* data, size_t len,
|
||||
std::vector<uint8_t>* response);
|
||||
bool handleReg1SetSceneCommand(const uint8_t* data, size_t len,
|
||||
std::vector<uint8_t>* response);
|
||||
bool handleReg1GetSceneCommand(const uint8_t* data, size_t len,
|
||||
std::vector<uint8_t>* response);
|
||||
bool handleReg1IdentifyCommand(const uint8_t* data, size_t len,
|
||||
std::vector<uint8_t>* response);
|
||||
bool handleReg1ScanState(const uint8_t* data, size_t len,
|
||||
std::vector<uint8_t>* response);
|
||||
bool handleReg1AssignState(const uint8_t* data, size_t len,
|
||||
std::vector<uint8_t>* response);
|
||||
bool handleReg1FoundEvgsState(const uint8_t* data, size_t len,
|
||||
std::vector<uint8_t>* response);
|
||||
|
||||
DaliBridgeEngine& engine_;
|
||||
GatewayKnxConfig config_;
|
||||
std::map<uint16_t, std::vector<GatewayKnxDaliBinding>> ets_bindings_by_group_address_;
|
||||
bool commissioning_scan_done_{true};
|
||||
bool commissioning_assign_done_{true};
|
||||
std::vector<GatewayKnxCommissioningBallast> commissioning_found_ballasts_;
|
||||
};
|
||||
|
||||
class GatewayKnxTpIpRouter {
|
||||
public:
|
||||
using CemiFrameHandler = std::function<DaliBridgeResult(const uint8_t* data, size_t len)>;
|
||||
|
||||
GatewayKnxTpIpRouter(GatewayKnxBridge& bridge, CemiFrameHandler handler);
|
||||
GatewayKnxTpIpRouter(GatewayKnxBridge& bridge, CemiFrameHandler handler,
|
||||
std::string openknx_namespace = "openknx");
|
||||
~GatewayKnxTpIpRouter();
|
||||
|
||||
void setConfig(const GatewayKnxConfig& config);
|
||||
@@ -179,6 +224,10 @@ class GatewayKnxTpIpRouter {
|
||||
void sendConnectResponse(uint8_t channel_id, uint8_t status,
|
||||
const ::sockaddr_in& remote);
|
||||
void sendRoutingIndication(const uint8_t* data, size_t len);
|
||||
bool handleOpenKnxTunnelFrame(const uint8_t* data, size_t len);
|
||||
void syncOpenKnxConfigFromDevice();
|
||||
uint16_t effectiveIndividualAddress() const;
|
||||
uint16_t effectiveTunnelAddress() const;
|
||||
void pollTpUart();
|
||||
void handleTpUartControlByte(uint8_t byte);
|
||||
void handleTpTelegram(const uint8_t* data, size_t len);
|
||||
@@ -186,7 +235,9 @@ class GatewayKnxTpIpRouter {
|
||||
|
||||
GatewayKnxBridge& bridge_;
|
||||
CemiFrameHandler handler_;
|
||||
std::string openknx_namespace_;
|
||||
GatewayKnxConfig config_;
|
||||
std::unique_ptr<openknx::EtsDeviceRuntime> ets_device_;
|
||||
TaskHandle_t task_handle_{nullptr};
|
||||
std::atomic_bool stop_requested_{false};
|
||||
std::atomic_bool started_{false};
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#include "gateway_knx.hpp"
|
||||
|
||||
#include "dali_define.hpp"
|
||||
#include "driver/uart.h"
|
||||
#include "esp_log.h"
|
||||
#include "lwip/inet.h"
|
||||
#include "lwip/sockets.h"
|
||||
#include "openknx_idf/ets_device_runtime.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
@@ -62,6 +64,20 @@ constexpr uint16_t kGwReg1AppKoBroadcastDimm = 2;
|
||||
constexpr uint8_t kGwReg1KoSwitch = 0;
|
||||
constexpr uint8_t kGwReg1KoDimmAbsolute = 3;
|
||||
constexpr uint8_t kGwReg1KoColor = 6;
|
||||
constexpr uint8_t kReg1DaliFunctionObjectIndex = 160;
|
||||
constexpr uint8_t kReg1DaliFunctionPropertyId = 1;
|
||||
constexpr uint8_t kReg1FunctionType = 2;
|
||||
constexpr uint8_t kReg1FunctionScan = 3;
|
||||
constexpr uint8_t kReg1FunctionAssign = 4;
|
||||
constexpr uint8_t kReg1FunctionEvgWrite = 10;
|
||||
constexpr uint8_t kReg1FunctionEvgRead = 11;
|
||||
constexpr uint8_t kReg1FunctionSetScene = 12;
|
||||
constexpr uint8_t kReg1FunctionGetScene = 13;
|
||||
constexpr uint8_t kReg1FunctionIdentify = 14;
|
||||
constexpr uint8_t kReg1DeviceTypeDt8 = 8;
|
||||
constexpr uint8_t kReg1ColorTypeTw = 1;
|
||||
constexpr uint8_t kDaliDeviceTypeNone = 0xfe;
|
||||
constexpr uint8_t kDaliDeviceTypeMultiple = 0xff;
|
||||
|
||||
struct DecodedGroupWrite {
|
||||
uint16_t group_address{0};
|
||||
@@ -293,6 +309,88 @@ std::optional<DecodedGroupWrite> DecodeCemiGroupWrite(const uint8_t* data, size_
|
||||
return out;
|
||||
}
|
||||
|
||||
uint8_t Reg1PercentToArc(uint8_t value) {
|
||||
if (value == 0 || value == 0xff) {
|
||||
return value;
|
||||
}
|
||||
const double arc = ((253.0 / 3.0) * (std::log10(static_cast<double>(value)) + 1.0)) + 1.0;
|
||||
return static_cast<uint8_t>(std::clamp(static_cast<int>(arc + 0.5), 0, 254));
|
||||
}
|
||||
|
||||
uint8_t Reg1ArcToPercent(uint8_t value) {
|
||||
if (value == 0 || value == 0xff) {
|
||||
return value;
|
||||
}
|
||||
const double percent = std::pow(10.0, ((static_cast<double>(value) - 1.0) / (253.0 / 3.0)) - 1.0);
|
||||
return static_cast<uint8_t>(std::clamp(static_cast<int>(percent + 0.5), 0, 100));
|
||||
}
|
||||
|
||||
GatewayKnxDaliTarget Reg1SceneTarget(uint8_t encoded_target) {
|
||||
if ((encoded_target & 0x80) != 0) {
|
||||
return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kGroup,
|
||||
static_cast<int>(encoded_target & 0x0f)};
|
||||
}
|
||||
return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kShortAddress,
|
||||
static_cast<int>(encoded_target & 0x3f)};
|
||||
}
|
||||
|
||||
DaliBridgeRequest FunctionRequest(const char* sequence, BridgeOperation operation) {
|
||||
DaliBridgeRequest request;
|
||||
request.sequence = sequence == nullptr ? "knx-function-property" : sequence;
|
||||
request.operation = operation;
|
||||
return request;
|
||||
}
|
||||
|
||||
void ApplyTargetToRequest(const GatewayKnxDaliTarget& target, DaliBridgeRequest* request) {
|
||||
if (request == nullptr) {
|
||||
return;
|
||||
}
|
||||
switch (target.kind) {
|
||||
case GatewayKnxDaliTargetKind::kBroadcast:
|
||||
request->metadata["broadcast"] = true;
|
||||
break;
|
||||
case GatewayKnxDaliTargetKind::kShortAddress:
|
||||
request->shortAddress = target.address;
|
||||
break;
|
||||
case GatewayKnxDaliTargetKind::kGroup:
|
||||
request->metadata["group"] = target.address;
|
||||
break;
|
||||
case GatewayKnxDaliTargetKind::kNone:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
DaliBridgeResult ExecuteRaw(DaliBridgeEngine& engine, BridgeOperation operation, uint8_t addr,
|
||||
uint8_t cmd, const char* sequence) {
|
||||
DaliBridgeRequest request = FunctionRequest(sequence, operation);
|
||||
request.rawAddress = addr;
|
||||
request.rawCommand = cmd;
|
||||
return engine.execute(request);
|
||||
}
|
||||
|
||||
std::optional<int> QueryShort(DaliBridgeEngine& engine, uint8_t short_address, uint8_t command,
|
||||
const char* sequence) {
|
||||
const auto result = ExecuteRaw(engine, BridgeOperation::query, DaliComm::toCmdAddr(short_address),
|
||||
command, sequence);
|
||||
if (!result.ok || !result.data.has_value()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return result.data.value();
|
||||
}
|
||||
|
||||
bool SendRaw(DaliBridgeEngine& engine, uint8_t addr, uint8_t cmd, const char* sequence) {
|
||||
return ExecuteRaw(engine, BridgeOperation::send, addr, cmd, sequence).ok;
|
||||
}
|
||||
|
||||
bool SendRawExt(DaliBridgeEngine& engine, uint8_t addr, uint8_t cmd, const char* sequence) {
|
||||
return ExecuteRaw(engine, BridgeOperation::sendExt, addr, cmd, sequence).ok;
|
||||
}
|
||||
|
||||
std::optional<int> MetadataInt(const DaliBridgeResult& result, const std::string& key) {
|
||||
return getObjectInt(result.metadata, key);
|
||||
}
|
||||
|
||||
DaliBridgeRequest RequestForTarget(uint16_t group_address,
|
||||
const GatewayKnxDaliTarget& target,
|
||||
BridgeOperation operation) {
|
||||
@@ -923,6 +1021,421 @@ DaliBridgeResult GatewayKnxBridge::handleGroupWrite(uint16_t group_address, cons
|
||||
return executeForDecodedWrite(group_address, data_type.value(), target.value(), data, len);
|
||||
}
|
||||
|
||||
bool GatewayKnxBridge::handleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id,
|
||||
const uint8_t* data, size_t len,
|
||||
std::vector<uint8_t>* response) {
|
||||
if (object_index != kReg1DaliFunctionObjectIndex || property_id != kReg1DaliFunctionPropertyId ||
|
||||
data == nullptr || len == 0 || response == nullptr) {
|
||||
return false;
|
||||
}
|
||||
switch (data[0]) {
|
||||
case kReg1FunctionType:
|
||||
return handleReg1TypeCommand(data, len, response);
|
||||
case kReg1FunctionScan:
|
||||
return handleReg1ScanCommand(data, len, response);
|
||||
case kReg1FunctionAssign:
|
||||
return handleReg1AssignCommand(data, len, response);
|
||||
case kReg1FunctionEvgWrite:
|
||||
return handleReg1EvgWriteCommand(data, len, response);
|
||||
case kReg1FunctionEvgRead:
|
||||
return handleReg1EvgReadCommand(data, len, response);
|
||||
case kReg1FunctionSetScene:
|
||||
return handleReg1SetSceneCommand(data, len, response);
|
||||
case kReg1FunctionGetScene:
|
||||
return handleReg1GetSceneCommand(data, len, response);
|
||||
case kReg1FunctionIdentify:
|
||||
return handleReg1IdentifyCommand(data, len, response);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool GatewayKnxBridge::handleFunctionPropertyState(uint8_t object_index, uint8_t property_id,
|
||||
const uint8_t* data, size_t len,
|
||||
std::vector<uint8_t>* response) {
|
||||
if (object_index != kReg1DaliFunctionObjectIndex || property_id != kReg1DaliFunctionPropertyId ||
|
||||
data == nullptr || len == 0 || response == nullptr) {
|
||||
return false;
|
||||
}
|
||||
switch (data[0]) {
|
||||
case kReg1FunctionScan:
|
||||
case 5:
|
||||
return handleReg1ScanState(data, len, response);
|
||||
case kReg1FunctionAssign:
|
||||
return handleReg1AssignState(data, len, response);
|
||||
case 7:
|
||||
return handleReg1FoundEvgsState(data, len, response);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool GatewayKnxBridge::handleReg1TypeCommand(const uint8_t* data, size_t len,
|
||||
std::vector<uint8_t>* response) {
|
||||
if (len < 2 || response == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const uint8_t short_address = data[1];
|
||||
const auto type_response = QueryShort(engine_, short_address, DALI_CMD_QUERY_DEVICE_TYPE,
|
||||
"knx-function-type");
|
||||
if (!type_response.has_value()) {
|
||||
*response = {0x01};
|
||||
return true;
|
||||
}
|
||||
uint8_t device_type = static_cast<uint8_t>(type_response.value());
|
||||
if (device_type == kDaliDeviceTypeMultiple) {
|
||||
for (int index = 0; index < 16; ++index) {
|
||||
const auto next_type = QueryShort(engine_, short_address, DALI_CMD_QUERY_NEXT_DEVICE_TYPE,
|
||||
"knx-function-next-device-type");
|
||||
if (!next_type.has_value()) {
|
||||
*response = {0x01};
|
||||
return true;
|
||||
}
|
||||
if (next_type.value() == kDaliDeviceTypeNone) {
|
||||
break;
|
||||
}
|
||||
if (next_type.value() < 20) {
|
||||
device_type = static_cast<uint8_t>(next_type.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
*response = {0x00, device_type};
|
||||
if (device_type == kReg1DeviceTypeDt8) {
|
||||
if (!SendRaw(engine_, DALI_CMD_SPECIAL_DT_SELECT, kReg1DeviceTypeDt8,
|
||||
"knx-function-dt8-select")) {
|
||||
*response = {0x02};
|
||||
return true;
|
||||
}
|
||||
const auto color_features = QueryShort(engine_, short_address, DALI_CMD_QUERY_COLOR_TYPE,
|
||||
"knx-function-color-type");
|
||||
if (!color_features.has_value()) {
|
||||
*response = {0x02};
|
||||
return true;
|
||||
}
|
||||
response->push_back(static_cast<uint8_t>(color_features.value()));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GatewayKnxBridge::handleReg1ScanCommand(const uint8_t* data, size_t len,
|
||||
std::vector<uint8_t>* response) {
|
||||
if (len < 5 || response == nullptr) {
|
||||
return false;
|
||||
}
|
||||
commissioning_scan_done_ = false;
|
||||
commissioning_found_ballasts_.clear();
|
||||
|
||||
const bool delete_all = data[3] == 1;
|
||||
const bool assign = data[4] == 1;
|
||||
if (assign || delete_all) {
|
||||
DaliBridgeRequest allocate = FunctionRequest(
|
||||
"knx-function-scan-allocate",
|
||||
delete_all ? BridgeOperation::resetAndAllocateShortAddresses
|
||||
: BridgeOperation::allocateAllShortAddresses);
|
||||
allocate.value = DaliValue::Object{{"start", 0}, {"removeAddrFirst", delete_all}};
|
||||
engine_.execute(allocate);
|
||||
}
|
||||
|
||||
DaliBridgeRequest search = FunctionRequest("knx-function-scan-search", BridgeOperation::searchAddressRange);
|
||||
search.value = DaliValue::Object{{"start", 0}, {"end", 63}};
|
||||
const auto search_result = engine_.execute(search);
|
||||
if (search_result.ok) {
|
||||
if (const auto* addresses_value = getObjectValue(search_result.metadata, "addresses")) {
|
||||
if (const auto* addresses = addresses_value->asArray()) {
|
||||
for (const auto& address_value : *addresses) {
|
||||
const auto short_address = address_value.asInt();
|
||||
if (!short_address.has_value() || short_address.value() < 0 || short_address.value() > 63) {
|
||||
continue;
|
||||
}
|
||||
GatewayKnxCommissioningBallast ballast;
|
||||
ballast.short_address = static_cast<uint8_t>(short_address.value());
|
||||
ballast.high = static_cast<uint8_t>(
|
||||
QueryShort(engine_, ballast.short_address, DALI_CMD_QUERY_RANDOM_ADDRESS_H,
|
||||
"knx-function-scan-rand-h")
|
||||
.value_or(0));
|
||||
ballast.middle = static_cast<uint8_t>(
|
||||
QueryShort(engine_, ballast.short_address, DALI_CMD_QUERY_RANDOM_ADDRESS_M,
|
||||
"knx-function-scan-rand-m")
|
||||
.value_or(0));
|
||||
ballast.low = static_cast<uint8_t>(
|
||||
QueryShort(engine_, ballast.short_address, DALI_CMD_QUERY_RANDOM_ADDRESS_L,
|
||||
"knx-function-scan-rand-l")
|
||||
.value_or(0));
|
||||
commissioning_found_ballasts_.push_back(ballast);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
commissioning_scan_done_ = true;
|
||||
response->clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GatewayKnxBridge::handleReg1AssignCommand(const uint8_t* data, size_t len,
|
||||
std::vector<uint8_t>* response) {
|
||||
if (len < 5 || response == nullptr) {
|
||||
return false;
|
||||
}
|
||||
commissioning_assign_done_ = false;
|
||||
const uint8_t short_address = data[1] == 99 ? 0xff : data[1];
|
||||
const bool ok = SendRawExt(engine_, DALI_CMD_SPECIAL_INITIALIZE, 0x00,
|
||||
"knx-function-assign-init") &&
|
||||
SendRaw(engine_, DALI_CMD_SPECIAL_SEARCHADDRH, data[2],
|
||||
"knx-function-assign-search-h") &&
|
||||
SendRaw(engine_, DALI_CMD_SPECIAL_SEARCHADDRM, data[3],
|
||||
"knx-function-assign-search-m") &&
|
||||
SendRaw(engine_, DALI_CMD_SPECIAL_SEARCHADDRL, data[4],
|
||||
"knx-function-assign-search-l") &&
|
||||
SendRaw(engine_, DALI_CMD_SPECIAL_PROGRAM_SHORT_ADDRESS,
|
||||
short_address == 0xff ? 0xff : DaliComm::toCmdAddr(short_address),
|
||||
"knx-function-assign-program") &&
|
||||
SendRaw(engine_, DALI_CMD_SPECIAL_TERMINATE, 0x00,
|
||||
"knx-function-assign-terminate");
|
||||
commissioning_assign_done_ = true;
|
||||
if (!ok) {
|
||||
ESP_LOGW(kTag, "REG1-Dali assign command failed while programming short address %u",
|
||||
short_address);
|
||||
}
|
||||
response->clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GatewayKnxBridge::handleReg1EvgWriteCommand(const uint8_t* data, size_t len,
|
||||
std::vector<uint8_t>* response) {
|
||||
if (len < 10 || response == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const uint8_t short_address = data[1];
|
||||
DaliBridgeRequest settings = FunctionRequest("knx-function-evg-write-settings",
|
||||
BridgeOperation::setAddressSettings);
|
||||
settings.shortAddress = short_address;
|
||||
settings.value = DaliValue::Object{
|
||||
{"minLevel", Reg1PercentToArc(data[2])},
|
||||
{"maxLevel", Reg1PercentToArc(data[3])},
|
||||
{"powerOnLevel", Reg1PercentToArc(data[4])},
|
||||
{"systemFailureLevel", Reg1PercentToArc(data[5])},
|
||||
{"fadeTime", static_cast<int>((data[6] >> 4) & 0x0f)},
|
||||
{"fadeRate", static_cast<int>(data[6] & 0x0f)},
|
||||
};
|
||||
const bool settings_ok = engine_.execute(settings).ok;
|
||||
|
||||
DaliBridgeRequest groups = FunctionRequest("knx-function-evg-write-groups",
|
||||
BridgeOperation::setGroupMask);
|
||||
groups.shortAddress = short_address;
|
||||
groups.value = static_cast<int>(static_cast<uint16_t>(data[8]) |
|
||||
(static_cast<uint16_t>(data[9]) << 8));
|
||||
const bool groups_ok = engine_.execute(groups).ok;
|
||||
if (!settings_ok || !groups_ok) {
|
||||
ESP_LOGW(kTag, "REG1-Dali EVG write command failed for short address %u", short_address);
|
||||
}
|
||||
response->clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GatewayKnxBridge::handleReg1EvgReadCommand(const uint8_t* data, size_t len,
|
||||
std::vector<uint8_t>* response) {
|
||||
if (len < 2 || response == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const uint8_t short_address = data[1];
|
||||
response->assign(12, 0x00);
|
||||
(*response)[0] = 0x00;
|
||||
uint8_t error_byte = 0;
|
||||
|
||||
DaliBridgeRequest settings = FunctionRequest("knx-function-evg-read-settings",
|
||||
BridgeOperation::getAddressSettings);
|
||||
settings.shortAddress = short_address;
|
||||
const auto settings_result = engine_.execute(settings);
|
||||
const auto set_level = [&](size_t index, const char* key, uint8_t error_mask) {
|
||||
const auto value = MetadataInt(settings_result, key);
|
||||
if (!settings_result.ok || !value.has_value()) {
|
||||
error_byte |= error_mask;
|
||||
(*response)[index] = 0xff;
|
||||
return;
|
||||
}
|
||||
(*response)[index] = Reg1ArcToPercent(static_cast<uint8_t>(std::clamp(value.value(), 0, 255)));
|
||||
};
|
||||
set_level(1, "minLevel", 0b00000001);
|
||||
set_level(2, "maxLevel", 0b00000010);
|
||||
set_level(3, "powerOnLevel", 0b00000100);
|
||||
set_level(4, "systemFailureLevel", 0b00001000);
|
||||
const auto fade_time = MetadataInt(settings_result, "fadeTime");
|
||||
const auto fade_rate = MetadataInt(settings_result, "fadeRate");
|
||||
if (!settings_result.ok || !fade_time.has_value() || !fade_rate.has_value()) {
|
||||
error_byte |= 0b00010000;
|
||||
(*response)[5] = 0xff;
|
||||
} else {
|
||||
(*response)[5] = static_cast<uint8_t>(((fade_rate.value() & 0x0f) << 4) |
|
||||
(fade_time.value() & 0x0f));
|
||||
}
|
||||
|
||||
DaliBridgeRequest groups = FunctionRequest("knx-function-evg-read-groups", BridgeOperation::getGroupMask);
|
||||
groups.shortAddress = short_address;
|
||||
const auto groups_result = engine_.execute(groups);
|
||||
if (!groups_result.ok || !groups_result.data.has_value()) {
|
||||
error_byte |= 0b11000000;
|
||||
} else {
|
||||
const uint16_t mask = static_cast<uint16_t>(groups_result.data.value());
|
||||
(*response)[7] = static_cast<uint8_t>(mask & 0xff);
|
||||
(*response)[8] = static_cast<uint8_t>((mask >> 8) & 0xff);
|
||||
}
|
||||
(*response)[9] = error_byte;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GatewayKnxBridge::handleReg1SetSceneCommand(const uint8_t* data, size_t len,
|
||||
std::vector<uint8_t>* response) {
|
||||
if (len < 10 || response == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const GatewayKnxDaliTarget target = Reg1SceneTarget(data[1]);
|
||||
const uint8_t scene = data[2] & 0x0f;
|
||||
const bool enabled = data[3] != 0;
|
||||
DaliBridgeRequest request = FunctionRequest(
|
||||
enabled ? "knx-function-set-scene" : "knx-function-remove-scene",
|
||||
enabled ? (data[4] == kReg1DeviceTypeDt8 ? BridgeOperation::storeDt8SceneSnapshot
|
||||
: BridgeOperation::setSceneLevel)
|
||||
: BridgeOperation::removeSceneLevel);
|
||||
ApplyTargetToRequest(target, &request);
|
||||
DaliValue::Object value{{"scene", static_cast<int>(scene)}};
|
||||
if (enabled) {
|
||||
value["brightness"] = static_cast<int>(Reg1PercentToArc(data[6]));
|
||||
if (data[4] == kReg1DeviceTypeDt8) {
|
||||
if (data[5] == kReg1ColorTypeTw) {
|
||||
const uint16_t kelvin = ReadBe16(data + 7);
|
||||
value["colorMode"] = "color_temperature";
|
||||
value["colorTemperature"] = static_cast<int>(kelvin);
|
||||
} else {
|
||||
value["colorMode"] = "rgb";
|
||||
value["r"] = static_cast<int>(data[7]);
|
||||
value["g"] = static_cast<int>(data[8]);
|
||||
value["b"] = static_cast<int>(data[9]);
|
||||
}
|
||||
}
|
||||
}
|
||||
request.value = std::move(value);
|
||||
const auto result = engine_.execute(request);
|
||||
if (!result.ok) {
|
||||
ESP_LOGW(kTag, "REG1-Dali set scene command failed for scene %u", scene);
|
||||
}
|
||||
response->clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GatewayKnxBridge::handleReg1GetSceneCommand(const uint8_t* data, size_t len,
|
||||
std::vector<uint8_t>* response) {
|
||||
if (len < 5 || response == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const uint8_t short_address = data[1];
|
||||
const uint8_t scene = data[2] & 0x0f;
|
||||
DaliBridgeRequest request = FunctionRequest("knx-function-get-scene", BridgeOperation::getSceneLevel);
|
||||
request.shortAddress = short_address;
|
||||
request.value = DaliValue::Object{{"scene", static_cast<int>(scene)}};
|
||||
const auto result = engine_.execute(request);
|
||||
if (!result.ok || !result.data.has_value()) {
|
||||
*response = {0xff};
|
||||
return true;
|
||||
}
|
||||
const uint8_t raw_level = static_cast<uint8_t>(std::clamp(result.data.value(), 0, 255));
|
||||
*response = {static_cast<uint8_t>(raw_level == 0xff ? 0xff : Reg1ArcToPercent(raw_level))};
|
||||
if (raw_level != 0xff && data[3] == kReg1DeviceTypeDt8) {
|
||||
if (data[4] == kReg1ColorTypeTw) {
|
||||
response->resize(3, 0);
|
||||
SendRaw(engine_, DALI_CMD_SPECIAL_SET_DTR0, 0xe2, "knx-function-get-scene-ct-selector");
|
||||
SendRaw(engine_, DALI_CMD_SPECIAL_DT_SELECT, kReg1DeviceTypeDt8,
|
||||
"knx-function-get-scene-ct-dt-select");
|
||||
const uint16_t mirek = static_cast<uint16_t>(
|
||||
(QueryShort(engine_, short_address, DALI_CMD_QUERY_COLOR_VALUE,
|
||||
"knx-function-get-scene-mirek-h")
|
||||
.value_or(0)
|
||||
<< 8) |
|
||||
QueryShort(engine_, short_address, DALI_CMD_QUERY_CONTENT_DTR,
|
||||
"knx-function-get-scene-mirek-l")
|
||||
.value_or(0));
|
||||
const uint16_t kelvin = mirek == 0 ? 0 : static_cast<uint16_t>(1000000U / mirek);
|
||||
(*response)[1] = static_cast<uint8_t>((kelvin >> 8) & 0xff);
|
||||
(*response)[2] = static_cast<uint8_t>(kelvin & 0xff);
|
||||
} else {
|
||||
response->resize(4, 0);
|
||||
const std::array<uint8_t, 3> selectors{0xe9, 0xea, 0xeb};
|
||||
for (size_t index = 0; index < selectors.size(); ++index) {
|
||||
SendRaw(engine_, DALI_CMD_SPECIAL_SET_DTR0, selectors[index],
|
||||
"knx-function-get-scene-rgb-selector");
|
||||
SendRaw(engine_, DALI_CMD_SPECIAL_DT_SELECT, kReg1DeviceTypeDt8,
|
||||
"knx-function-get-scene-rgb-dt-select");
|
||||
(*response)[index + 1] = static_cast<uint8_t>(
|
||||
QueryShort(engine_, short_address, DALI_CMD_QUERY_COLOR_VALUE,
|
||||
"knx-function-get-scene-rgb-value")
|
||||
.value_or(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GatewayKnxBridge::handleReg1IdentifyCommand(const uint8_t* data, size_t len,
|
||||
std::vector<uint8_t>* response) {
|
||||
if (len < 2 || response == nullptr) {
|
||||
return false;
|
||||
}
|
||||
DaliBridgeRequest off = FunctionRequest("knx-function-identify-broadcast-off", BridgeOperation::off);
|
||||
off.metadata["broadcast"] = true;
|
||||
engine_.execute(off);
|
||||
DaliBridgeRequest identify = FunctionRequest("knx-function-identify-recall-max",
|
||||
BridgeOperation::recallMaxLevel);
|
||||
identify.shortAddress = data[1];
|
||||
engine_.execute(identify);
|
||||
response->clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GatewayKnxBridge::handleReg1ScanState(const uint8_t* data, size_t len,
|
||||
std::vector<uint8_t>* response) {
|
||||
if (len < 1 || response == nullptr) {
|
||||
return false;
|
||||
}
|
||||
response->clear();
|
||||
response->push_back(commissioning_scan_done_ ? 1 : 0);
|
||||
if (data[0] == kReg1FunctionScan) {
|
||||
response->push_back(static_cast<uint8_t>(
|
||||
std::min<size_t>(commissioning_found_ballasts_.size(), 0xff)));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GatewayKnxBridge::handleReg1AssignState(const uint8_t* data, size_t len,
|
||||
std::vector<uint8_t>* response) {
|
||||
if (len < 1 || response == nullptr) {
|
||||
return false;
|
||||
}
|
||||
*response = {static_cast<uint8_t>(commissioning_assign_done_ ? 1 : 0)};
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GatewayKnxBridge::handleReg1FoundEvgsState(const uint8_t* data, size_t len,
|
||||
std::vector<uint8_t>* response) {
|
||||
if (len < 2 || response == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (data[1] == 254) {
|
||||
commissioning_found_ballasts_.clear();
|
||||
response->clear();
|
||||
return true;
|
||||
}
|
||||
const size_t index = data[1];
|
||||
response->clear();
|
||||
response->push_back(index < commissioning_found_ballasts_.size() ? 1 : 0);
|
||||
if (index < commissioning_found_ballasts_.size()) {
|
||||
const auto& ballast = commissioning_found_ballasts_[index];
|
||||
response->push_back(ballast.high);
|
||||
response->push_back(ballast.middle);
|
||||
response->push_back(ballast.low);
|
||||
response->push_back(ballast.short_address);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
DaliBridgeResult GatewayKnxBridge::executeEtsBindings(
|
||||
uint16_t group_address, const std::vector<GatewayKnxDaliBinding>& bindings,
|
||||
const uint8_t* data, size_t len) {
|
||||
@@ -1011,8 +1524,11 @@ DaliBridgeResult GatewayKnxBridge::executeForDecodedWrite(uint16_t group_address
|
||||
}
|
||||
}
|
||||
|
||||
GatewayKnxTpIpRouter::GatewayKnxTpIpRouter(GatewayKnxBridge& bridge, CemiFrameHandler handler)
|
||||
: bridge_(bridge), handler_(std::move(handler)) {}
|
||||
GatewayKnxTpIpRouter::GatewayKnxTpIpRouter(GatewayKnxBridge& bridge, CemiFrameHandler handler,
|
||||
std::string openknx_namespace)
|
||||
: bridge_(bridge),
|
||||
handler_(std::move(handler)),
|
||||
openknx_namespace_(std::move(openknx_namespace)) {}
|
||||
|
||||
GatewayKnxTpIpRouter::~GatewayKnxTpIpRouter() { stop(); }
|
||||
|
||||
@@ -1032,7 +1548,19 @@ esp_err_t GatewayKnxTpIpRouter::start(uint32_t task_stack_size, UBaseType_t task
|
||||
if (!configureSocket()) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
ets_device_ = std::make_unique<openknx::EtsDeviceRuntime>(openknx_namespace_,
|
||||
config_.individual_address);
|
||||
ets_device_->setFunctionPropertyHandlers(
|
||||
[this](uint8_t object_index, uint8_t property_id, const uint8_t* data, size_t len,
|
||||
std::vector<uint8_t>* response) {
|
||||
return bridge_.handleFunctionPropertyCommand(object_index, property_id, data, len, response);
|
||||
},
|
||||
[this](uint8_t object_index, uint8_t property_id, const uint8_t* data, size_t len,
|
||||
std::vector<uint8_t>* response) {
|
||||
return bridge_.handleFunctionPropertyState(object_index, property_id, data, len, response);
|
||||
});
|
||||
if (!configureTpUart()) {
|
||||
ets_device_.reset();
|
||||
closeSockets();
|
||||
return ESP_FAIL;
|
||||
}
|
||||
@@ -1075,6 +1603,9 @@ void GatewayKnxTpIpRouter::taskLoop() {
|
||||
reinterpret_cast<sockaddr*>(&remote), &remote_len);
|
||||
if (received <= 0) {
|
||||
pollTpUart();
|
||||
if (ets_device_ != nullptr) {
|
||||
ets_device_->loop();
|
||||
}
|
||||
if (!stop_requested_) {
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
@@ -1082,12 +1613,16 @@ void GatewayKnxTpIpRouter::taskLoop() {
|
||||
}
|
||||
handleUdpDatagram(buffer.data(), static_cast<size_t>(received), remote);
|
||||
pollTpUart();
|
||||
if (ets_device_ != nullptr) {
|
||||
ets_device_->loop();
|
||||
}
|
||||
}
|
||||
finishTask();
|
||||
}
|
||||
|
||||
void GatewayKnxTpIpRouter::finishTask() {
|
||||
closeSockets();
|
||||
ets_device_.reset();
|
||||
started_ = false;
|
||||
task_handle_ = nullptr;
|
||||
vTaskDelete(nullptr);
|
||||
@@ -1209,8 +1744,8 @@ bool GatewayKnxTpIpRouter::initializeTpUart() {
|
||||
saw_reset = true;
|
||||
const std::array<uint8_t, 3> set_address{
|
||||
kTpUartSetAddressRequest,
|
||||
static_cast<uint8_t>((config_.individual_address >> 8) & 0xff),
|
||||
static_cast<uint8_t>(config_.individual_address & 0xff),
|
||||
static_cast<uint8_t>((effectiveIndividualAddress() >> 8) & 0xff),
|
||||
static_cast<uint8_t>(effectiveIndividualAddress() & 0xff),
|
||||
};
|
||||
uart_write_bytes(uart_port, set_address.data(), set_address.size());
|
||||
const uint8_t state_request = kTpUartStateRequest;
|
||||
@@ -1296,6 +1831,10 @@ void GatewayKnxTpIpRouter::handleTunnellingRequest(const uint8_t* body, size_t l
|
||||
sendTunnellingAck(channel_id, sequence, kKnxNoError, remote);
|
||||
const uint8_t* cemi = body + 4;
|
||||
const size_t cemi_len = len - 4;
|
||||
const bool consumed_by_openknx = handleOpenKnxTunnelFrame(cemi, cemi_len);
|
||||
if (consumed_by_openknx) {
|
||||
return;
|
||||
}
|
||||
const DaliBridgeResult result = handler_(cemi, cemi_len);
|
||||
if (!result.ok && !result.error.empty()) {
|
||||
ESP_LOGD(kTag, "KNX tunnel frame not routed to DALI: %s", result.error.c_str());
|
||||
@@ -1400,8 +1939,8 @@ void GatewayKnxTpIpRouter::sendConnectResponse(uint8_t channel_id, uint8_t statu
|
||||
body.insert(body.end(), data_endpoint.begin(), data_endpoint.end());
|
||||
body.push_back(0x04);
|
||||
body.push_back(kKnxConnectionTypeTunnel);
|
||||
body.push_back(static_cast<uint8_t>((config_.individual_address >> 8) & 0xff));
|
||||
body.push_back(static_cast<uint8_t>(config_.individual_address & 0xff));
|
||||
body.push_back(static_cast<uint8_t>((effectiveTunnelAddress() >> 8) & 0xff));
|
||||
body.push_back(static_cast<uint8_t>(effectiveTunnelAddress() & 0xff));
|
||||
const auto packet = KnxNetIpPacket(kServiceConnectResponse, body);
|
||||
SendAll(udp_sock_, packet.data(), packet.size(), remote);
|
||||
}
|
||||
@@ -1419,6 +1958,79 @@ void GatewayKnxTpIpRouter::sendRoutingIndication(const uint8_t* data, size_t len
|
||||
SendAll(udp_sock_, packet.data(), packet.size(), remote);
|
||||
}
|
||||
|
||||
bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t len) {
|
||||
if (ets_device_ == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const bool consumed = ets_device_->handleTunnelFrame(
|
||||
data, len, [this](const uint8_t* response, size_t response_len) {
|
||||
sendTunnelIndication(response, response_len);
|
||||
});
|
||||
syncOpenKnxConfigFromDevice();
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void GatewayKnxTpIpRouter::syncOpenKnxConfigFromDevice() {
|
||||
if (ets_device_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
const auto snapshot = ets_device_->snapshot();
|
||||
bool changed = false;
|
||||
GatewayKnxConfig updated = config_;
|
||||
if (snapshot.individual_address != 0 && snapshot.individual_address != 0xffff &&
|
||||
snapshot.individual_address != updated.individual_address) {
|
||||
updated.individual_address = snapshot.individual_address;
|
||||
changed = true;
|
||||
}
|
||||
if (snapshot.configured || !snapshot.associations.empty()) {
|
||||
std::vector<GatewayKnxEtsAssociation> associations;
|
||||
associations.reserve(snapshot.associations.size());
|
||||
for (const auto& association : snapshot.associations) {
|
||||
associations.push_back(GatewayKnxEtsAssociation{association.group_address,
|
||||
association.group_object_number});
|
||||
}
|
||||
if (associations.size() != updated.ets_associations.size() ||
|
||||
!std::equal(associations.begin(), associations.end(), updated.ets_associations.begin(),
|
||||
[](const GatewayKnxEtsAssociation& lhs,
|
||||
const GatewayKnxEtsAssociation& rhs) {
|
||||
return lhs.group_address == rhs.group_address &&
|
||||
lhs.group_object_number == rhs.group_object_number;
|
||||
})) {
|
||||
updated.ets_associations = std::move(associations);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
config_ = updated;
|
||||
bridge_.setConfig(config_);
|
||||
}
|
||||
|
||||
uint16_t GatewayKnxTpIpRouter::effectiveIndividualAddress() const {
|
||||
if (ets_device_ != nullptr) {
|
||||
const uint16_t address = ets_device_->individualAddress();
|
||||
if (address != 0 && address != 0xffff) {
|
||||
return address;
|
||||
}
|
||||
}
|
||||
return config_.individual_address;
|
||||
}
|
||||
|
||||
uint16_t GatewayKnxTpIpRouter::effectiveTunnelAddress() const {
|
||||
if (ets_device_ != nullptr) {
|
||||
const uint16_t address = ets_device_->tunnelClientAddress();
|
||||
if (address != 0 && address != 0xffff) {
|
||||
return address;
|
||||
}
|
||||
}
|
||||
uint16_t device = static_cast<uint16_t>((config_.individual_address & 0x00ff) + 1);
|
||||
if (device == 0 || device > 0xff) {
|
||||
device = 1;
|
||||
}
|
||||
return static_cast<uint16_t>((config_.individual_address & 0xff00) | device);
|
||||
}
|
||||
|
||||
void GatewayKnxTpIpRouter::pollTpUart() {
|
||||
if (tp_uart_port_ < 0) {
|
||||
return;
|
||||
|
||||
@@ -29,6 +29,7 @@ idf_component_register(
|
||||
SRCS
|
||||
"src/arduino_compat.cpp"
|
||||
"src/esp_idf_platform.cpp"
|
||||
"src/ets_device_runtime.cpp"
|
||||
"src/ets_memory_loader.cpp"
|
||||
"src/tpuart_uart_interface.cpp"
|
||||
${OPENKNX_SRCS}
|
||||
@@ -54,6 +55,7 @@ target_compile_definitions(${COMPONENT_LIB} PUBLIC
|
||||
KNX_FLASH_SIZE=4096
|
||||
KNX_NO_AUTOMATIC_GLOBAL_INSTANCE
|
||||
KNX_NO_SPI
|
||||
USE_CEMI_SERVER
|
||||
)
|
||||
|
||||
target_compile_options(${COMPONENT_LIB} PRIVATE
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include "openknx_idf/esp_idf_platform.h"
|
||||
#include "openknx_idf/ets_memory_loader.h"
|
||||
|
||||
#include "knx/bau07B0.h"
|
||||
#include "knx/cemi_frame.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace gateway::openknx {
|
||||
|
||||
class EtsDeviceRuntime {
|
||||
public:
|
||||
using CemiFrameSender = std::function<void(const uint8_t* data, size_t len)>;
|
||||
using FunctionPropertyHandler = std::function<bool(uint8_t object_index, uint8_t property_id,
|
||||
const uint8_t* data, size_t len,
|
||||
std::vector<uint8_t>* response)>;
|
||||
|
||||
EtsDeviceRuntime(std::string nvs_namespace, uint16_t fallback_individual_address);
|
||||
~EtsDeviceRuntime();
|
||||
|
||||
uint16_t individualAddress() const;
|
||||
uint16_t tunnelClientAddress() const;
|
||||
bool configured() const;
|
||||
EtsMemorySnapshot snapshot() const;
|
||||
|
||||
void setFunctionPropertyHandlers(FunctionPropertyHandler command_handler,
|
||||
FunctionPropertyHandler state_handler);
|
||||
|
||||
bool handleTunnelFrame(const uint8_t* data, size_t len, CemiFrameSender sender);
|
||||
void loop();
|
||||
|
||||
private:
|
||||
static void EmitTunnelFrame(CemiFrame& frame, void* context);
|
||||
static bool HandleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id,
|
||||
uint8_t length, uint8_t* data,
|
||||
uint8_t* result_data, uint8_t& result_length);
|
||||
static bool HandleFunctionPropertyState(uint8_t object_index, uint8_t property_id,
|
||||
uint8_t length, uint8_t* data,
|
||||
uint8_t* result_data, uint8_t& result_length);
|
||||
static uint16_t DefaultTunnelClientAddress(uint16_t individual_address);
|
||||
static bool 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);
|
||||
bool shouldConsumeTunnelFrame(CemiFrame& frame) const;
|
||||
|
||||
std::string nvs_namespace_;
|
||||
EspIdfPlatform platform_;
|
||||
Bau07B0 device_;
|
||||
CemiFrameSender sender_;
|
||||
FunctionPropertyHandler command_handler_;
|
||||
FunctionPropertyHandler state_handler_;
|
||||
};
|
||||
|
||||
} // namespace gateway::openknx
|
||||
@@ -13,6 +13,7 @@ struct EtsAssociation {
|
||||
|
||||
struct EtsMemorySnapshot {
|
||||
bool configured{false};
|
||||
uint16_t individual_address{0};
|
||||
std::vector<EtsAssociation> associations;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "openknx_idf/ets_memory_loader.h"
|
||||
#include "openknx_idf/ets_device_runtime.h"
|
||||
#include "openknx_idf/esp_idf_platform.h"
|
||||
#include "openknx_idf/tpuart_uart_interface.h"
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+1
-1
Submodule knx updated: b747f6284d...339d8472e7
+1
-1
Submodule knx_dali_gw updated: 5cd7e66bf0...6064d84520
Reference in New Issue
Block a user