From 449a3a801af2adc53bf57ebb938d041779abdeae Mon Sep 17 00:00:00 2001 From: Tony Date: Fri, 15 May 2026 12:34:13 +0800 Subject: [PATCH] Add KNX DALI Gateway Module and Message Queue Implementation - Introduced KnxDaliModule class for handling DALI message queuing, commissioning, and KNX group-object dispatch. - Implemented Message and MessageQueue classes for managing message operations. - Removed obsolete OpenKNX IDF component files and CMake configurations. - Updated submodule reference for KNX. Signed-off-by: Tony --- apps/gateway/CMakeLists.txt | 1 + components/gateway_bridge/CMakeLists.txt | 6 +- .../include}/security_storage.h | 0 .../gateway_bridge/src/gateway_bridge.cpp | 2 +- .../src/security_storage.cpp | 2 +- components/gateway_knx/CMakeLists.txt | 7 +- .../include}/ets_device_runtime.h | 4 +- .../include}/ets_memory_loader.h | 0 .../include/gateway_knx_internal.h | 61 ++ .../src/ets_device_runtime.cpp | 2 +- .../src/ets_memory_loader.cpp | 4 +- components/gateway_knx/src/gateway_knx.cpp | 2 +- components/knx_dali_gw/CMakeLists.txt | 24 + .../knx_dali_gw/include/dali_gateway_bridge.h | 127 ++++ .../knx_dali_gw/include/knx_dali_gw.hpp | 55 ++ components/knx_dali_gw/include/knxprod.h | 66 ++ components/knx_dali_gw/src/ballast.hpp | 10 + components/knx_dali_gw/src/color_helper.cpp | 138 ++++ components/knx_dali_gw/src/color_helper.h | 23 + .../knx_dali_gw/src/dali_gateway_bridge.cpp | 323 +++++++++ components/knx_dali_gw/src/dali_helper.cpp | 40 ++ components/knx_dali_gw/src/dali_helper.h | 13 + components/knx_dali_gw/src/hcl_curve.cpp | 15 + components/knx_dali_gw/src/hcl_curve.h | 23 + .../knx_dali_gw/src/knx_dali_channel.cpp | 184 +++++ components/knx_dali_gw/src/knx_dali_channel.h | 91 +++ components/knx_dali_gw/src/knx_dali_gw.cpp | 106 +++ .../knx_dali_gw/src/knx_dali_module.cpp | 679 ++++++++++++++++++ components/knx_dali_gw/src/knx_dali_module.h | 166 +++++ components/knx_dali_gw/src/message.hpp | 21 + components/knx_dali_gw/src/message_queue.cpp | 74 ++ components/knx_dali_gw/src/message_queue.h | 21 + components/openknx_idf/CMakeLists.txt | 78 -- components/openknx_idf/include/Arduino.h | 59 -- .../include/openknx_idf/esp_idf_platform.h | 66 -- .../include/openknx_idf/openknx_idf.h | 16 - .../openknx_idf/tpuart_uart_interface.h | 43 -- components/openknx_idf/src/arduino_compat.cpp | 180 ----- .../openknx_idf/src/esp_idf_platform.cpp | 316 -------- .../openknx_idf/src/tpuart_uart_interface.cpp | 147 ---- knx | 2 +- 41 files changed, 2279 insertions(+), 918 deletions(-) rename components/{openknx_idf/include/openknx_idf => gateway_bridge/include}/security_storage.h (100%) rename components/{openknx_idf => gateway_bridge}/src/security_storage.cpp (99%) rename components/{openknx_idf/include/openknx_idf => gateway_knx/include}/ets_device_runtime.h (97%) rename components/{openknx_idf/include/openknx_idf => gateway_knx/include}/ets_memory_loader.h (100%) create mode 100644 components/gateway_knx/include/gateway_knx_internal.h rename components/{openknx_idf => gateway_knx}/src/ets_device_runtime.cpp (99%) rename components/{openknx_idf => gateway_knx}/src/ets_memory_loader.cpp (96%) create mode 100644 components/knx_dali_gw/CMakeLists.txt create mode 100644 components/knx_dali_gw/include/dali_gateway_bridge.h create mode 100644 components/knx_dali_gw/include/knx_dali_gw.hpp create mode 100644 components/knx_dali_gw/include/knxprod.h create mode 100644 components/knx_dali_gw/src/ballast.hpp create mode 100644 components/knx_dali_gw/src/color_helper.cpp create mode 100644 components/knx_dali_gw/src/color_helper.h create mode 100644 components/knx_dali_gw/src/dali_gateway_bridge.cpp create mode 100644 components/knx_dali_gw/src/dali_helper.cpp create mode 100644 components/knx_dali_gw/src/dali_helper.h create mode 100644 components/knx_dali_gw/src/hcl_curve.cpp create mode 100644 components/knx_dali_gw/src/hcl_curve.h create mode 100644 components/knx_dali_gw/src/knx_dali_channel.cpp create mode 100644 components/knx_dali_gw/src/knx_dali_channel.h create mode 100644 components/knx_dali_gw/src/knx_dali_gw.cpp create mode 100644 components/knx_dali_gw/src/knx_dali_module.cpp create mode 100644 components/knx_dali_gw/src/knx_dali_module.h create mode 100644 components/knx_dali_gw/src/message.hpp create mode 100644 components/knx_dali_gw/src/message_queue.cpp create mode 100644 components/knx_dali_gw/src/message_queue.h delete mode 100644 components/openknx_idf/CMakeLists.txt delete mode 100644 components/openknx_idf/include/Arduino.h delete mode 100644 components/openknx_idf/include/openknx_idf/esp_idf_platform.h delete mode 100644 components/openknx_idf/include/openknx_idf/openknx_idf.h delete mode 100644 components/openknx_idf/include/openknx_idf/tpuart_uart_interface.h delete mode 100644 components/openknx_idf/src/arduino_compat.cpp delete mode 100644 components/openknx_idf/src/esp_idf_platform.cpp delete mode 100644 components/openknx_idf/src/tpuart_uart_interface.cpp diff --git a/apps/gateway/CMakeLists.txt b/apps/gateway/CMakeLists.txt index 19cd5d4..d9a5e3c 100644 --- a/apps/gateway/CMakeLists.txt +++ b/apps/gateway/CMakeLists.txt @@ -6,6 +6,7 @@ endif() set(EXTRA_COMPONENT_DIRS "${CMAKE_CURRENT_LIST_DIR}/../../components" + "${CMAKE_CURRENT_LIST_DIR}/../../knx" "${CMAKE_CURRENT_LIST_DIR}/../../../dali_cpp" ) diff --git a/components/gateway_bridge/CMakeLists.txt b/components/gateway_bridge/CMakeLists.txt index 4443b23..465d08d 100644 --- a/components/gateway_bridge/CMakeLists.txt +++ b/components/gateway_bridge/CMakeLists.txt @@ -7,14 +7,16 @@ set(GATEWAY_BRIDGE_REQUIRES gateway_cache gateway_knx gateway_modbus + knx log lwip nvs_flash - openknx_idf ) idf_component_register( - SRCS "src/gateway_bridge.cpp" + SRCS + "src/gateway_bridge.cpp" + "src/security_storage.cpp" INCLUDE_DIRS "include" REQUIRES ${GATEWAY_BRIDGE_REQUIRES} PRIV_REQUIRES gateway_bacnet diff --git a/components/openknx_idf/include/openknx_idf/security_storage.h b/components/gateway_bridge/include/security_storage.h similarity index 100% rename from components/openknx_idf/include/openknx_idf/security_storage.h rename to components/gateway_bridge/include/security_storage.h diff --git a/components/gateway_bridge/src/gateway_bridge.cpp b/components/gateway_bridge/src/gateway_bridge.cpp index 9e992d3..34e6579 100644 --- a/components/gateway_bridge/src/gateway_bridge.cpp +++ b/components/gateway_bridge/src/gateway_bridge.cpp @@ -15,7 +15,7 @@ #include "gateway_knx.hpp" #include "gateway_modbus.hpp" #include "gateway_provisioning.hpp" -#include "openknx_idf/security_storage.h" +#include "security_storage.h" #include "cJSON.h" #include "driver/uart.h" diff --git a/components/openknx_idf/src/security_storage.cpp b/components/gateway_bridge/src/security_storage.cpp similarity index 99% rename from components/openknx_idf/src/security_storage.cpp rename to components/gateway_bridge/src/security_storage.cpp index 205705d..4eb40f5 100644 --- a/components/openknx_idf/src/security_storage.cpp +++ b/components/gateway_bridge/src/security_storage.cpp @@ -1,4 +1,4 @@ -#include "openknx_idf/security_storage.h" +#include "security_storage.h" #include "esp_log.h" #include "esp_mac.h" diff --git a/components/gateway_knx/CMakeLists.txt b/components/gateway_knx/CMakeLists.txt index 975de09..f76f3a0 100644 --- a/components/gateway_knx/CMakeLists.txt +++ b/components/gateway_knx/CMakeLists.txt @@ -1,7 +1,10 @@ idf_component_register( - SRCS "src/gateway_knx.cpp" + SRCS + "src/gateway_knx.cpp" + "src/ets_device_runtime.cpp" + "src/ets_memory_loader.cpp" INCLUDE_DIRS "include" - REQUIRES dali_cpp esp_driver_gpio esp_driver_uart esp_hw_support esp_netif freertos log lwip openknx_idf + REQUIRES dali_cpp esp_driver_gpio esp_driver_uart esp_hw_support esp_netif freertos log lwip knx ) set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17) \ No newline at end of file diff --git a/components/openknx_idf/include/openknx_idf/ets_device_runtime.h b/components/gateway_knx/include/ets_device_runtime.h similarity index 97% rename from components/openknx_idf/include/openknx_idf/ets_device_runtime.h rename to components/gateway_knx/include/ets_device_runtime.h index 4e44ed5..a0a594d 100644 --- a/components/openknx_idf/include/openknx_idf/ets_device_runtime.h +++ b/components/gateway_knx/include/ets_device_runtime.h @@ -1,7 +1,7 @@ #pragma once -#include "openknx_idf/esp_idf_platform.h" -#include "openknx_idf/ets_memory_loader.h" +#include "esp_idf_platform.h" +#include "ets_memory_loader.h" #include "knx/bau07B0.h" #include "knx/cemi_frame.h" diff --git a/components/openknx_idf/include/openknx_idf/ets_memory_loader.h b/components/gateway_knx/include/ets_memory_loader.h similarity index 100% rename from components/openknx_idf/include/openknx_idf/ets_memory_loader.h rename to components/gateway_knx/include/ets_memory_loader.h diff --git a/components/gateway_knx/include/gateway_knx_internal.h b/components/gateway_knx/include/gateway_knx_internal.h new file mode 100644 index 0000000..c8ce3ff --- /dev/null +++ b/components/gateway_knx/include/gateway_knx_internal.h @@ -0,0 +1,61 @@ +#pragma once + +// Internal header shared between gateway_knx.cpp and gateway_knx_router.cpp. + +#include "driver/uart.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "soc/uart_periph.h" + +#include +#include + +namespace gateway { +namespace knx_internal { + +constexpr const char* kTag = "gateway_knx"; + +// RAII semaphore guard. +class SemaphoreGuard { + public: + explicit SemaphoreGuard(SemaphoreHandle_t semaphore) : semaphore_(semaphore) { + if (semaphore_ != nullptr) { + xSemaphoreTake(semaphore_, portMAX_DELAY); + locked_ = true; + } + } + ~SemaphoreGuard() { + if (locked_) { + xSemaphoreGive(semaphore_); + } + } + private: + SemaphoreHandle_t semaphore_{nullptr}; + bool locked_{false}; +}; + +// Resolve a UART IO pin from config or SoC defaults. +inline bool ResolveUartIoPin(uart_port_t uart_port, int configured_pin, + uint32_t pin_index, int* resolved_pin) { + if (resolved_pin == nullptr) return false; + if (configured_pin >= 0) { + *resolved_pin = configured_pin; + return true; + } + if (uart_port < 0 || uart_port >= SOC_UART_NUM || + pin_index >= SOC_UART_PINS_COUNT) { + *resolved_pin = UART_PIN_NO_CHANGE; + return false; + } + const int default_pin = + uart_periph_signal[uart_port].pins[pin_index].default_gpio; + if (default_pin < 0) { + *resolved_pin = UART_PIN_NO_CHANGE; + return false; + } + *resolved_pin = default_pin; + return true; +} + +} // namespace knx_internal +} // namespace gateway diff --git a/components/openknx_idf/src/ets_device_runtime.cpp b/components/gateway_knx/src/ets_device_runtime.cpp similarity index 99% rename from components/openknx_idf/src/ets_device_runtime.cpp rename to components/gateway_knx/src/ets_device_runtime.cpp index fdf51a1..a5879b6 100644 --- a/components/openknx_idf/src/ets_device_runtime.cpp +++ b/components/gateway_knx/src/ets_device_runtime.cpp @@ -1,4 +1,4 @@ -#include "openknx_idf/ets_device_runtime.h" +#include "ets_device_runtime.h" #include "knx/cemi_server.h" #include "knx/secure_application_layer.h" diff --git a/components/openknx_idf/src/ets_memory_loader.cpp b/components/gateway_knx/src/ets_memory_loader.cpp similarity index 96% rename from components/openknx_idf/src/ets_memory_loader.cpp rename to components/gateway_knx/src/ets_memory_loader.cpp index 8d051c9..d963f63 100644 --- a/components/openknx_idf/src/ets_memory_loader.cpp +++ b/components/gateway_knx/src/ets_memory_loader.cpp @@ -1,6 +1,6 @@ -#include "openknx_idf/ets_memory_loader.h" +#include "ets_memory_loader.h" -#include "openknx_idf/esp_idf_platform.h" +#include "esp_idf_platform.h" #include "knx/bau07B0.h" #include "knx/property.h" diff --git a/components/gateway_knx/src/gateway_knx.cpp b/components/gateway_knx/src/gateway_knx.cpp index 24ad844..930ea96 100644 --- a/components/gateway_knx/src/gateway_knx.cpp +++ b/components/gateway_knx/src/gateway_knx.cpp @@ -8,7 +8,7 @@ #include "esp_log.h" #include "lwip/inet.h" #include "lwip/sockets.h" -#include "openknx_idf/ets_device_runtime.h" +#include "ets_device_runtime.h" #include "soc/uart_periph.h" #include diff --git a/components/knx_dali_gw/CMakeLists.txt b/components/knx_dali_gw/CMakeLists.txt new file mode 100644 index 0000000..b407195 --- /dev/null +++ b/components/knx_dali_gw/CMakeLists.txt @@ -0,0 +1,24 @@ +idf_component_register( + SRCS + "src/knx_dali_gw.cpp" + "src/knx_dali_module.cpp" + "src/knx_dali_channel.cpp" + "src/hcl_curve.cpp" + "src/color_helper.cpp" + "src/dali_helper.cpp" + "src/message_queue.cpp" + "src/dali_gateway_bridge.cpp" + INCLUDE_DIRS + "include" + "src" + REQUIRES + dali_cpp + dali_domain + esp_timer + freertos + knx + log + nvs_flash +) + +set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17) diff --git a/components/knx_dali_gw/include/dali_gateway_bridge.h b/components/knx_dali_gw/include/dali_gateway_bridge.h new file mode 100644 index 0000000..98f2bfd --- /dev/null +++ b/components/knx_dali_gw/include/dali_gateway_bridge.h @@ -0,0 +1,127 @@ +#pragma once + +// ============================================================================= +// dali_gateway_bridge.h — Thin adapter mapping legacy DALI operations to +// dali_domain / dali_cpp API. +// ============================================================================= + +#include "dali_domain.hpp" + +#include +#include +#include + +namespace gateway { +namespace knx_dali_gw { + +// DALI target types matching the legacy DaliModule target model. +enum class DaliTargetKind : uint8_t { + kShortAddress = 0, + kGroup = 1, + kBroadcast = 2, +}; + +struct DaliTarget { + DaliTargetKind kind{DaliTargetKind::kShortAddress}; + int address{0}; // short address 0-63, group 0-15, or ignored for broadcast +}; + +// Encodes a DaliTarget into a raw DALI address byte. +uint8_t EncodeDaliRawAddr(DaliTarget target); + +// Decodes a raw DALI address byte into a DaliTarget for a given short address. +DaliTarget DecodeDaliRawAddr(uint8_t raw_addr, int default_short_address = -1); + +// Lightweight bridge over DaliDomainService for the KNX-DALI gateway. +// All operations go through a single DALI channel (gateway_id). +class DaliGatewayBridge { + public: + explicit DaliGatewayBridge(DaliDomainService& dali, uint8_t gateway_id = 0); + + // ---- Basic send / query ---- + + bool sendRaw(DaliTarget target, uint8_t command) const; + bool sendExtRaw(DaliTarget target, uint8_t command) const; + std::optional queryRaw(DaliTarget target, uint8_t command) const; + + // ---- Brightness (arc power level) ---- + + bool setArc(DaliTarget target, uint8_t arc) const; + std::optional queryActualLevel(int short_address) const; + + // ---- On / Off / Step / Recall ---- + + bool on(DaliTarget target) const; + bool off(DaliTarget target) const; + bool stepUp(DaliTarget target) const; + bool stepDown(DaliTarget target) const; + bool recallMax(DaliTarget target) const; + bool recallMin(DaliTarget target) const; + bool goToScene(DaliTarget target, uint8_t scene) const; + + // ---- Queries ---- + + std::optional queryStatus(int short_address) const; + std::optional queryDeviceType(int short_address) const; + std::optional queryMinLevel(int short_address) const; + std::optional queryMaxLevel(int short_address) const; + std::optional queryPowerOnLevel(int short_address) const; + std::optional querySystemFailureLevel(int short_address) const; + std::optional queryFadeTimeRate(int short_address) const; + std::optional queryFadeTime(int short_address) const; + std::optional queryGroups(int short_address) const; + std::optional querySceneLevel(int short_address, uint8_t scene) const; + + // ---- DT8 colour ---- + + bool setColourTemperature(int short_address, int kelvin) const; + bool setColourRGB(int short_address, uint8_t r, uint8_t g, uint8_t b) const; + std::optional dt8StatusSnapshot(int short_address) const; + std::optional dt8SceneColorReport(int short_address, int scene) const; + + // ---- Scenes & groups (write operations) ---- + + bool setDtr(uint8_t value) const; + bool setDtrAsScene(DaliTarget target, uint8_t scene) const; + bool addToGroup(DaliTarget target, uint8_t group) const; + bool removeFromGroup(DaliTarget target, uint8_t group) const; + bool removeFromScene(DaliTarget target, uint8_t scene) const; + bool setSceneLevel(DaliTarget target, uint8_t scene, uint8_t level) const; + + // ---- Commissioning ---- + + bool initialise(DaliTarget target) const; + bool randomise() const; + bool searchAddrH(uint8_t high) const; + bool searchAddrM(uint8_t middle) const; + bool searchAddrL(uint8_t low) const; + bool compare() const; + bool withdraw() const; + bool terminate() const; + bool programShort(DaliTarget target, uint8_t short_address) const; + bool verifyShort(DaliTarget target) const; + std::optional queryShort(DaliTarget target) const; + + // High-level addressing. + bool allocateAllAddr(int start_address = 0) const; + void stopAllocAddr() const; + bool resetAndAllocAddr(int start_address = 0, bool remove_addr_first = false, + bool close_light = false) const; + + // ---- Bus control ---- + + bool resetBus() const; + + private: + DaliDomainService& dali_; + uint8_t gateway_id_; +}; + +// Convert DALI arc power level (0-254) to percentage (0.0-100.0). +double ArcToPercent(uint8_t arc); + +// Convert percentage (0.0-100.0) to DALI arc power level (0-254). +uint8_t PercentToArc(double percent); + +} // namespace knx_dali_gw +} // namespace gateway diff --git a/components/knx_dali_gw/include/knx_dali_gw.hpp b/components/knx_dali_gw/include/knx_dali_gw.hpp new file mode 100644 index 0000000..c081c2f --- /dev/null +++ b/components/knx_dali_gw/include/knx_dali_gw.hpp @@ -0,0 +1,55 @@ +#pragma once + +// ============================================================================= +// knx_dali_gw — KNX-to-DALI Gateway Component (ESP-IDF) +// ============================================================================= + +#include "esp_idf_platform.h" + +#include "dali_domain.hpp" + +#include +#include +#include +#include +#include + +// Forward declarations. +class Bau07B0; + +namespace gateway { +namespace knx_dali_gw { + +struct KnxDaliGatewayConfig { + std::string nvs_namespace{"knx_dali_gw"}; + uint16_t fallback_individual_address{0xfffe}; + int dali_channel{0}; +}; + +class KnxDaliGateway { + public: + explicit KnxDaliGateway(const KnxDaliGatewayConfig& config); + ~KnxDaliGateway(); + + KnxDaliGateway(const KnxDaliGateway&) = delete; + KnxDaliGateway& operator=(const KnxDaliGateway&) = delete; + + bool init(); + void loop(); + + Bau07B0& knxDevice(); + const Bau07B0& knxDevice() const; + + void setNetworkInterface(esp_netif_t* netif); + bool handleTunnelFrame(const uint8_t* data, size_t len); + bool handleBusFrame(const uint8_t* data, size_t len); + bool emitGroupValue(uint16_t group_object_number, const uint8_t* data, + size_t len); + + private: + struct Impl; + std::unique_ptr impl_; +}; + +} // namespace knx_dali_gw +} // namespace gateway diff --git a/components/knx_dali_gw/include/knxprod.h b/components/knx_dali_gw/include/knxprod.h new file mode 100644 index 0000000..8acfb47 --- /dev/null +++ b/components/knx_dali_gw/include/knxprod.h @@ -0,0 +1,66 @@ +#pragma once + +// Minimal stub for knxprod.h — generated KNX product definitions. +// The full file (1796 bytes of parameters, 1439 group objects) will be +// adapted in Phase 3 to use the gateway/knx API directly. + +// Product identity +#define MAIN_OpenKnxId 0xA4 +#define MAIN_ApplicationNumber 1 +#define MAIN_ApplicationVersion 5 +#define MAIN_OrderNumber "REG1-Dali" +#define MAIN_ParameterSize 1796 +#define MAIN_MaxKoNumber 1439 + +// Parameter type enums (subset) +enum PT_DeviceType : uint8_t { + PT_deviceType_Deaktiviert = 0, + PT_deviceType_DT0 = 1, + PT_deviceType_DT1 = 2, + PT_deviceType_DT6 = 3, + PT_deviceType_DT8 = 4, +}; + +enum PT_ColorType : uint8_t { + PT_colorType_HSV = 0, + PT_colorType_RGB = 1, + PT_colorType_TW = 2, + PT_colorType_XYY = 3, +}; + +enum PT_ColorSpace : uint8_t { + PT_colorSpace_rgb = 0, + PT_colorSpace_xy = 1, +}; + +// Placeholder macros — will be replaced with direct Bau07B0 access in Phase 3. +#define ParamAPP_daynight(channelIndex) (0) +#define ParamAPP_funcBtn(channelIndex) (0) +#define ParamAPP_funcBtnLong(channelIndex) (0) +#define ParamAPP_funcBtnDbl(channelIndex) (0) + +#define ParamADR_deviceType(channelIndex) (PT_DeviceType::PT_deviceType_Deaktiviert) +#define ParamADR_type(channelIndex) (0) +#define ParamADR_min(channelIndex) (0) +#define ParamADR_max(channelIndex) (254) +#define ParamADR_stairtime(channelIndex) (0) +#define ParamADR_onDay(channelIndex) (0) +#define ParamADR_onNight(channelIndex) (0) +#define ParamADR_error(channelIndex) (0) +#define ParamADR_queryTime(channelIndex) (0) +#define ParamADR_colorType(channelIndex) (PT_ColorType::PT_colorType_TW) +#define ParamADR_colorSpace(channelIndex) (PT_ColorSpace::PT_colorSpace_rgb) + +#define ParamGRP_deviceType(channelIndex) (PT_DeviceType::PT_deviceType_Deaktiviert) +#define ParamGRP_type(channelIndex) (0) +#define ParamGRP_colorType(channelIndex) (PT_ColorType::PT_colorType_TW) + +#define ParamHCL_type(channelIndex) (0) + +// Group object offset placeholders +#define ADR_KoOffset 0 +#define GRP_KoOffset 0 +#define HCL_KoOffset 0 +#define ADR_KoBlockSize 0 +#define GRP_KoBlockSize 0 +#define HCL_KoBlockSize 0 diff --git a/components/knx_dali_gw/src/ballast.hpp b/components/knx_dali_gw/src/ballast.hpp new file mode 100644 index 0000000..10b4d13 --- /dev/null +++ b/components/knx_dali_gw/src/ballast.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include + +struct Ballast { + uint8_t high{0}; + uint8_t middle{0}; + uint8_t low{0}; + uint8_t address{255}; +}; diff --git a/components/knx_dali_gw/src/color_helper.cpp b/components/knx_dali_gw/src/color_helper.cpp new file mode 100644 index 0000000..b3d1079 --- /dev/null +++ b/components/knx_dali_gw/src/color_helper.cpp @@ -0,0 +1,138 @@ +#include "color_helper.h" + +uint16_t ColorHelper::getKelvinFromSun(uint16_t minCurr, uint16_t minDiff, uint16_t minK, uint16_t maxK) +{ + float xAchse = (minCurr * 3.14159) / minDiff; + float yAchse = sin(xAchse); + return (maxK - minK) * yAchse + minK; +} + +void ColorHelper::rgbToXY(uint8_t in_r, uint8_t in_g, uint8_t in_b, uint16_t& x, uint16_t& y) +{ + float r = in_r / 255.0; + float g = in_g / 255.0; + float b = in_b / 255.0; + r = (r > 0.04045) ? pow((r + 0.055) / (1.0 + 0.055), 2.4) : (r / 12.92); + g = (g > 0.04045) ? pow((g + 0.055) / (1.0 + 0.055), 2.4) : (g / 12.92); + b = (b > 0.04045) ? pow((b + 0.055) / (1.0 + 0.055), 2.4) : (b / 12.92); + + float X = r * 0.4124 + g * 0.3576 + b * 0.1805; + float Y = r * 0.2126 + g * 0.7152 + b * 0.0722; + float Z = r * 0.0193 + g * 0.1192 + b * 0.9505; + + + float cx = X / (X + Y + Z); + float cy = Y / (X + Y + Z); + + x = getBytes(cx); + y = getBytes(cy); + + y = y + 1; + y = y - 1; +} + +void ColorHelper::hsvToRGB(uint8_t in_h, uint8_t in_s, uint8_t in_v, uint8_t& r, uint8_t& g, uint8_t& b) +{ + float h = in_h / 255.0; + float s = in_s / 255.0; + float v = in_v / 255.0; + + double rt = 0; + double gt = 0; + double bt = 0; + + int i = int(h * 6); + double f = h * 6 - i; + double p = v * (1 - s); + double q = v * (1 - f * s); + double t = v * (1 - (1 - f) * s); + + switch(i % 6){ + case 0: rt = v, gt = t, bt = p; break; + case 1: rt = q, gt = v, bt = p; break; + case 2: rt = p, gt = v, bt = t; break; + case 3: rt = p, gt = q, bt = v; break; + case 4: rt = t, gt = p, bt = v; break; + case 5: rt = v, gt = p, bt = q; break; + } + + r = rt * 255; + g = gt * 255; + b = bt * 255; +} + +void ColorHelper::kelvinToRGB(uint16_t kelvin, uint8_t& r, uint8_t& g, uint8_t& b) +{ + auto temp = kelvin / 100; + + if (temp <= 66) + { + r = 255; + g = 99.4708025861 * log(temp) - 161.1195681661; + + if (temp <= 19) + b = 0; + else + b = 138.5177312231 * log(temp - 10) - 305.0447927307; + } + else + { + r = 329.698727446 * pow(temp - 60, -0.1332047592); + g = 288.1221695283 * pow(temp - 60, -0.0755148492); + b = 255; + } +} + +void ColorHelper::xyyToRGB(uint16_t ix, uint16_t iy, uint8_t iz, uint8_t& r, uint8_t& g, uint8_t& b) +{ + float _x = getFloat(ix); + float _y = getFloat(iy); + + //let z = 1.0 - x - y; + //return this.colorFromXYZ((Y / y) * x, Y, (Y / y) * z); + + float y = iz / 255.0f; + float x = _x * (y / _y); + float z = ((1.0 - _x - _y) * y) / _y; + + float rt = x * 3.2404f + y * -1.5371f + z * -0.4985f; + float gt = x * -0.9693f + y * 1.8760f + z * 0.0416f; + float bt = x * 0.0556f + y * -0.2040f + z * 1.05723f; + + rt = adjust(rt); + gt = adjust(gt); + bt = adjust(bt); + + r = std::max(std::min(rt, 255.0f), 0.0f); + g = std::max(std::min(gt, 255.0f), 0.0f); + b = std::max(std::min(bt, 255.0f), 0.0f); +} + +uint16_t ColorHelper::getBytes(float input) +{ + return std::max(std::min(round(input * 65536.0), 65534.0), 0.0); +} + +float ColorHelper::getFloat(uint16_t input) +{ + float output = input / 65536.0f; + return std::max(std::min(output, 0.0f), 1.0f); +} + +double ColorHelper::hue2rgb(double p, double q, double t) +{ + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6.0) return p + (q - p) * 6 * t; + if (t < 1 / 2.0) return q; + if (t < 2 / 3.0) return p + (q - p) * (2 / 3.0 - t) * 6; + return p; +} + +float ColorHelper::adjust(float input) +{ + if (input > 0.0031308) { + return (1.055f * pow(input, (1.0f / 2.4f)) - 0.055f) * 255.0; + } + return 12.92f * input; +} \ No newline at end of file diff --git a/components/knx_dali_gw/src/color_helper.h b/components/knx_dali_gw/src/color_helper.h new file mode 100644 index 0000000..ab96b53 --- /dev/null +++ b/components/knx_dali_gw/src/color_helper.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include + +//extended lib from https://github.com/ratkins/RGBConverter +//WTFPL license + +class ColorHelper +{ + public: + static void rgbToXY(uint8_t r, uint8_t g, uint8_t b, uint16_t& x, uint16_t& y); + static void hsvToRGB(uint8_t h, uint8_t s, uint8_t v, uint8_t& r, uint8_t& g, uint8_t& b); + static void kelvinToRGB(uint16_t kelvin, uint8_t& r, uint8_t& g, uint8_t& b); + static void xyyToRGB(uint16_t x, uint16_t y, uint8_t z, uint8_t& r, uint8_t& g, uint8_t& b); + static uint16_t getKelvinFromSun(uint16_t minCurr, uint16_t minDiff, uint16_t minK, uint16_t maxK); + private: + static uint16_t getBytes(float input); + static float getFloat(uint16_t input); + static double hue2rgb(double p, double q, double t); + static float adjust(float input); +}; \ No newline at end of file diff --git a/components/knx_dali_gw/src/dali_gateway_bridge.cpp b/components/knx_dali_gw/src/dali_gateway_bridge.cpp new file mode 100644 index 0000000..437c89e --- /dev/null +++ b/components/knx_dali_gw/src/dali_gateway_bridge.cpp @@ -0,0 +1,323 @@ +#include "dali_gateway_bridge.h" + +#include "dali_define.hpp" +#include "dali_comm.hpp" + +#include +#include + +namespace gateway { +namespace knx_dali_gw { + +namespace { + +constexpr double kArcMax = 254.0; +constexpr double kLogFactor = 3.0; + +// DALI address encoding helpers (mirroring lib/dali/comm.dart). +// Short address 0-63 → (addr << 1) | 0x01 for commands. +constexpr uint8_t kShortAddrCmdBase = 0x01; +constexpr uint8_t kShortAddrArcBase = 0x00; +constexpr uint8_t kGroupAddrBase = 0x80; +constexpr uint8_t kBroadcastCmd = 0xFE; +constexpr uint8_t kBroadcastArc = 0xFF; + +uint8_t makeShortCmdAddr(int addr) { + return static_cast((addr << 1) | kShortAddrCmdBase); +} +uint8_t makeShortArcAddr(int addr) { + return static_cast((addr << 1) | kShortAddrArcBase); +} +uint8_t makeGroupCmdAddr(int group) { + return static_cast(kGroupAddrBase | (group << 1) | 0x01); +} +uint8_t makeBroadcastCmdAddr() { return kBroadcastCmd; } +uint8_t makeBroadcastArcAddr() { return kBroadcastArc; } + +bool isBroadcastAddr(uint8_t raw) { return raw == kBroadcastCmd || raw == kBroadcastArc; } +bool isGroupAddr(uint8_t raw) { return (raw & 0x80) != 0 && !isBroadcastAddr(raw); } +int extractGroupAddr(uint8_t raw) { return (raw >> 1) & 0x0F; } +int extractShortAddr(uint8_t raw) { return (raw >> 1) & 0x3F; } + +uint8_t encodeDaliRawAddr(DaliTarget target) { + switch (target.kind) { + case DaliTargetKind::kShortAddress: + return makeShortCmdAddr(target.address); + case DaliTargetKind::kGroup: + return makeGroupCmdAddr(target.address); + case DaliTargetKind::kBroadcast: + default: + return makeBroadcastCmdAddr(); + } +} + +} // namespace + +// ============================================================================= +// DaliTarget ↔ raw address encoding (public API) +// ============================================================================= + +uint8_t EncodeDaliRawAddr(DaliTarget target) { + return encodeDaliRawAddr(target); +} + +DaliTarget DecodeDaliRawAddr(uint8_t raw_addr, int default_short_address) { + if (isBroadcastAddr(raw_addr)) { + return {DaliTargetKind::kBroadcast, 0}; + } + if (isGroupAddr(raw_addr)) { + return {DaliTargetKind::kGroup, extractGroupAddr(raw_addr)}; + } + int sa = extractShortAddr(raw_addr); + if (sa < 0 && default_short_address >= 0) { + sa = default_short_address; + } + return {DaliTargetKind::kShortAddress, sa}; +} + +// ============================================================================= +// Arc power ↔ percentage conversion +// ============================================================================= + +double ArcToPercent(uint8_t arc) { + if (arc == 0) return 0.0; + return 100.0 * std::pow(static_cast(arc) / kArcMax, kLogFactor); +} + +uint8_t PercentToArc(double percent) { + if (percent <= 0.0) return 0; + if (percent >= 100.0) return 254; + return static_cast( + std::round(kArcMax * std::pow(percent / 100.0, 1.0 / kLogFactor))); +} + +// ============================================================================= +// DaliGatewayBridge +// ============================================================================= + +DaliGatewayBridge::DaliGatewayBridge(DaliDomainService& dali, uint8_t gateway_id) + : dali_(dali), gateway_id_(gateway_id) {} + +bool DaliGatewayBridge::sendRaw(DaliTarget target, uint8_t command) const { + return dali_.sendRaw(gateway_id_, encodeDaliRawAddr(target), command); +} + +bool DaliGatewayBridge::sendExtRaw(DaliTarget target, uint8_t command) const { + return dali_.sendExtRaw(gateway_id_, encodeDaliRawAddr(target), command); +} + +std::optional DaliGatewayBridge::queryRaw(DaliTarget target, uint8_t command) const { + return dali_.queryRaw(gateway_id_, encodeDaliRawAddr(target), command); +} + +bool DaliGatewayBridge::setArc(DaliTarget target, uint8_t arc) const { + return sendRaw(target, arc); +} + +std::optional DaliGatewayBridge::queryActualLevel(int short_address) const { + return dali_.queryRaw(gateway_id_, makeShortCmdAddr(short_address), + DALI_CMD_QUERY_ACTUAL_LEVEL); +} + +bool DaliGatewayBridge::on(DaliTarget target) const { + return sendRaw(target, DALI_CMD_RECALL_MAX); +} + +bool DaliGatewayBridge::off(DaliTarget target) const { + return sendRaw(target, DALI_CMD_OFF); +} + +bool DaliGatewayBridge::stepUp(DaliTarget target) const { + return sendRaw(target, DALI_CMD_RECALL_MAX); +} + +bool DaliGatewayBridge::stepDown(DaliTarget target) const { + return sendRaw(target, DALI_CMD_OFF); +} + +bool DaliGatewayBridge::recallMax(DaliTarget target) const { + return sendRaw(target, DALI_CMD_RECALL_MAX); +} + +bool DaliGatewayBridge::recallMin(DaliTarget target) const { + return sendRaw(target, DALI_CMD_RECALL_MIN); +} + +bool DaliGatewayBridge::goToScene(DaliTarget target, uint8_t scene) const { + return sendRaw(target, DALI_CMD_GO_TO_SCENE(scene & 0x0F)); +} + +std::optional DaliGatewayBridge::queryStatus(int short_address) const { + return dali_.queryRaw(gateway_id_, makeShortCmdAddr(short_address), + DALI_CMD_QUERY_STATUS); +} + +std::optional DaliGatewayBridge::queryDeviceType(int short_address) const { + return dali_.queryRaw(gateway_id_, makeShortCmdAddr(short_address), + DALI_CMD_QUERY_DEVICE_TYPE); +} + +std::optional DaliGatewayBridge::queryMinLevel(int short_address) const { + return dali_.queryRaw(gateway_id_, makeShortCmdAddr(short_address), + DALI_CMD_QUERY_MIN_LEVEL); +} + +std::optional DaliGatewayBridge::queryMaxLevel(int short_address) const { + return dali_.queryRaw(gateway_id_, makeShortCmdAddr(short_address), + DALI_CMD_QUERY_MAX_LEVEL); +} + +std::optional DaliGatewayBridge::queryPowerOnLevel(int short_address) const { + return dali_.queryRaw(gateway_id_, makeShortCmdAddr(short_address), + DALI_CMD_QUERY_POWER_ON_LEVEL); +} + +std::optional DaliGatewayBridge::querySystemFailureLevel(int short_address) const { + return dali_.queryRaw(gateway_id_, makeShortCmdAddr(short_address), + DALI_CMD_QUERY_SYSTEM_FAILURE_LEVEL); +} + +std::optional DaliGatewayBridge::queryFadeTimeRate(int short_address) const { + return dali_.queryRaw(gateway_id_, makeShortCmdAddr(short_address), + DALI_CMD_QUERY_FADE_TIME_FADE_RATE); +} + +std::optional DaliGatewayBridge::queryFadeTime(int short_address) const { + return dali_.queryRaw(gateway_id_, makeShortCmdAddr(short_address), + DALI_CMD_QUERY_EXTENDED_FADE_TIME); +} + +std::optional DaliGatewayBridge::queryGroups(int short_address) const { + return dali_.queryGroupMask(gateway_id_, short_address); +} + +std::optional DaliGatewayBridge::querySceneLevel(int short_address, + uint8_t scene) const { + return dali_.querySceneLevel(gateway_id_, short_address, scene & 0x0F); +} + +// ---- DT8 ---- + +bool DaliGatewayBridge::setColourTemperature(int short_address, int kelvin) const { + return dali_.setColTemp(gateway_id_, short_address, kelvin); +} + +bool DaliGatewayBridge::setColourRGB(int short_address, uint8_t r, uint8_t g, + uint8_t b) const { + return dali_.setColourRGB(gateway_id_, short_address, r, g, b); +} + +std::optional DaliGatewayBridge::dt8StatusSnapshot( + int short_address) const { + return dali_.dt8StatusSnapshot(gateway_id_, short_address); +} + +std::optional DaliGatewayBridge::dt8SceneColorReport( + int short_address, int scene) const { + return dali_.dt8SceneColorReport(gateway_id_, short_address, scene); +} + +// ---- Scenes & groups ---- + +bool DaliGatewayBridge::setDtr(uint8_t value) const { + return dali_.sendRaw(gateway_id_, makeBroadcastCmdAddr(), + DALI_CMD_SPECIAL_SET_DTR0) && + dali_.sendRaw(gateway_id_, makeBroadcastCmdAddr(), value); +} + +bool DaliGatewayBridge::setDtrAsScene(DaliTarget target, uint8_t scene) const { + return sendRaw(target, DALI_CMD_SET_SCENE(scene & 0x0F)); +} + +bool DaliGatewayBridge::addToGroup(DaliTarget target, uint8_t group) const { + return sendRaw(target, DALI_CMD_ADD_TO_GROUP(group & 0x0F)); +} + +bool DaliGatewayBridge::removeFromGroup(DaliTarget target, uint8_t group) const { + return sendRaw(target, DALI_CMD_REMOVE_FROM_GROUP(group & 0x0F)); +} + +bool DaliGatewayBridge::removeFromScene(DaliTarget target, uint8_t scene) const { + return sendRaw(target, DALI_CMD_REMOVE_SCENE(scene & 0x0F)); +} + +bool DaliGatewayBridge::setSceneLevel(DaliTarget target, uint8_t scene, + uint8_t level) const { + return setDtr(level) && setDtrAsScene(target, scene); +} + +// ---- Commissioning ---- + +bool DaliGatewayBridge::initialise(DaliTarget target) const { + return sendRaw(target, DALI_CMD_SPECIAL_INITIALIZE); +} + +bool DaliGatewayBridge::randomise() const { + return sendRaw({DaliTargetKind::kBroadcast, 0}, DALI_CMD_SPECIAL_RANDOMIZE); +} + +bool DaliGatewayBridge::searchAddrH(uint8_t high) const { + return dali_.sendRaw(gateway_id_, makeBroadcastCmdAddr(), + DALI_CMD_SPECIAL_SEARCHADDRH) && + dali_.sendRaw(gateway_id_, makeBroadcastCmdAddr(), high); +} + +bool DaliGatewayBridge::searchAddrM(uint8_t middle) const { + return dali_.sendRaw(gateway_id_, makeBroadcastCmdAddr(), + DALI_CMD_SPECIAL_SEARCHADDRM) && + dali_.sendRaw(gateway_id_, makeBroadcastCmdAddr(), middle); +} + +bool DaliGatewayBridge::searchAddrL(uint8_t low) const { + return dali_.sendRaw(gateway_id_, makeBroadcastCmdAddr(), + DALI_CMD_SPECIAL_SEARCHADDRL) && + dali_.sendRaw(gateway_id_, makeBroadcastCmdAddr(), low); +} + +bool DaliGatewayBridge::compare() const { + return sendRaw({DaliTargetKind::kBroadcast, 0}, DALI_CMD_SPECIAL_COMPARE); +} + +bool DaliGatewayBridge::withdraw() const { + return sendRaw({DaliTargetKind::kBroadcast, 0}, DALI_CMD_SPECIAL_WITHDRAW); +} + +bool DaliGatewayBridge::terminate() const { + return sendRaw({DaliTargetKind::kBroadcast, 0}, DALI_CMD_SPECIAL_TERMINATE); +} + +bool DaliGatewayBridge::programShort(DaliTarget target, uint8_t short_address) const { + const uint8_t raw = (short_address << 1) | 0x01; + return dali_.sendRaw(gateway_id_, encodeDaliRawAddr(target), + DALI_CMD_SPECIAL_PROGRAM_SHORT_ADDRESS) && + dali_.sendRaw(gateway_id_, encodeDaliRawAddr(target), raw); +} + +bool DaliGatewayBridge::verifyShort(DaliTarget target) const { + return sendRaw(target, DALI_CMD_SPECIAL_VERIFY_SHORT_ADDRESS); +} + +std::optional DaliGatewayBridge::queryShort(DaliTarget target) const { + return dali_.queryRaw(gateway_id_, encodeDaliRawAddr(target), + DALI_CMD_SPECIAL_QUERY_SHORT_ADDRESS); +} + +bool DaliGatewayBridge::allocateAllAddr(int start_address) const { + return dali_.allocateAllAddr(gateway_id_, start_address); +} + +void DaliGatewayBridge::stopAllocAddr() const { + dali_.stopAllocAddr(gateway_id_); +} + +bool DaliGatewayBridge::resetAndAllocAddr(int start_address, bool remove_addr_first, + bool close_light) const { + return dali_.resetAndAllocAddr(gateway_id_, start_address, remove_addr_first, close_light); +} + +bool DaliGatewayBridge::resetBus() const { + return dali_.resetBus(gateway_id_); +} + +} // namespace knx_dali_gw +} // namespace gateway diff --git a/components/knx_dali_gw/src/dali_helper.cpp b/components/knx_dali_gw/src/dali_helper.cpp new file mode 100644 index 0000000..25a0fac --- /dev/null +++ b/components/knx_dali_gw/src/dali_helper.cpp @@ -0,0 +1,40 @@ +#include "dali_helper.h" + +uint8_t DaliHelper::percentToArc(uint8_t value) +{ + if(value == 0) + { + return 0; + } + //Todo also include _max + uint8_t arc = roundToInt(((253/3.0)*(std::log10(value)+1)) + 1); + return arc; +} + +uint8_t DaliHelper::arcToPercent(uint8_t value) +{ + if(value == 0) + { + return 0; + } + //Todo also include _max + double arc = std::pow(10, ((value-1) / (253/3.0)) - 1); + return roundToInt(arc); +} + +float DaliHelper::arcToPercentFloat(uint8_t value) +{ + if(value == 0) + { + return 0; + } + //Todo also include _max + float arc = std::pow(10, ((value-1) / (253/3.0)) - 1); + return arc; +} + +uint8_t DaliHelper::roundToInt(double input) +{ + double temp = input + 0.5; + return (uint8_t)temp; +} diff --git a/components/knx_dali_gw/src/dali_helper.h b/components/knx_dali_gw/src/dali_helper.h new file mode 100644 index 0000000..c5eeb65 --- /dev/null +++ b/components/knx_dali_gw/src/dali_helper.h @@ -0,0 +1,13 @@ +#pragma once + +#include +#include + +class DaliHelper +{ + public: + static uint8_t percentToArc(uint8_t value); + static uint8_t arcToPercent(uint8_t value); + static float arcToPercentFloat(uint8_t value); + static uint8_t roundToInt(double input); +}; \ No newline at end of file diff --git a/components/knx_dali_gw/src/hcl_curve.cpp b/components/knx_dali_gw/src/hcl_curve.cpp new file mode 100644 index 0000000..6a0606d --- /dev/null +++ b/components/knx_dali_gw/src/hcl_curve.cpp @@ -0,0 +1,15 @@ +#include "hcl_curve.h" +#include "esp_timer.h" + +namespace gateway { +namespace knx_dali_gw { + +void HclCurve::setup(uint8_t index) { index_ = index; } + +void HclCurve::loop() { + // HCL curve logic — simplified for now. + // Full port from HclCurve.cpp in subsequent iteration. +} + +} // namespace knx_dali_gw +} // namespace gateway diff --git a/components/knx_dali_gw/src/hcl_curve.h b/components/knx_dali_gw/src/hcl_curve.h new file mode 100644 index 0000000..2fadc8f --- /dev/null +++ b/components/knx_dali_gw/src/hcl_curve.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +namespace gateway { +namespace knx_dali_gw { + +class HclCurve { + public: + void setup(uint8_t index); + void loop(); + + private: + uint8_t index_{0}; + bool is_configured_{false}; + uint8_t type_{0}; + uint64_t last_check_{0}; + uint8_t last_day_{0}; + uint8_t last_minute_{0}; +}; + +} // namespace knx_dali_gw +} // namespace gateway diff --git a/components/knx_dali_gw/src/knx_dali_channel.cpp b/components/knx_dali_gw/src/knx_dali_channel.cpp new file mode 100644 index 0000000..627600d --- /dev/null +++ b/components/knx_dali_gw/src/knx_dali_channel.cpp @@ -0,0 +1,184 @@ +#include "knx_dali_channel.h" +#include "dali_define.hpp" +#include "knxprod.h" +#include "dali_helper.h" + +#include "esp_log.h" +#include "esp_timer.h" + +namespace gateway { +namespace knx_dali_gw { + +KnxDaliChannel::KnxDaliChannel() = default; +KnxDaliChannel::~KnxDaliChannel() = default; + +void KnxDaliChannel::init(uint8_t channel_index, bool is_group, DaliGatewayBridge& bridge) { + index_ = channel_index; + is_group_ = is_group; + dali_ = &bridge; +} + +void KnxDaliChannel::setup() { + if (dali_ == nullptr) return; + // Query initial state + DaliTarget target = is_group_ ? DaliTarget{DaliTargetKind::kGroup, static_cast(index_)} + : DaliTarget{DaliTargetKind::kShortAddress, static_cast(index_)}; + (void)target; // Will be used in full port +} + +void KnxDaliChannel::loop() { + if (dali_ == nullptr) return; + loopDimming(); + loopStaircase(); + loopQueryLevel(); +} + +void KnxDaliChannel::processInputKo(GroupObject& ko) { + uint16_t asap = ko.asap(); + int slot = static_cast(asap) - (is_group_ ? GRP_KoOffset : ADR_KoOffset) - index_ * (is_group_ ? GRP_KoBlockSize : ADR_KoBlockSize); + + // TODO: Full slot-to-handler mapping from DaliChannel.cpp + // For now, delegate to basic handlers + switch (slot) { + case 0: koHandleSwitch(ko); break; + // ... more slots + default: break; + } +} + +// ---- Dimming ---- + +void KnxDaliChannel::loopDimming() { + if (dimm_direction_ == DimmDirection::kNone) return; + uint64_t now = esp_timer_get_time() / 1000ULL; + if (now - dimm_last_ < dimm_interval_) return; + dimm_last_ = now; + + DaliTarget target = is_group_ ? DaliTarget{DaliTargetKind::kGroup, static_cast(index_)} + : DaliTarget{DaliTargetKind::kShortAddress, static_cast(index_)}; + + if (dimm_direction_ == DimmDirection::kUp) { + if (current_step_ < max_) current_step_++; + dali_->setArc(target, current_step_); + } else { + if (current_step_ > min_) current_step_--; + dali_->setArc(target, current_step_); + } +} + +// ---- Staircase ---- + +void KnxDaliChannel::loopStaircase() { + if (interval_ == 0 || !current_state_) return; + uint64_t now = esp_timer_get_time() / 1000ULL; + if (now - start_time_ >= interval_ * 1000ULL) { + current_state_ = false; + interval_ = 0; + DaliTarget target = is_group_ ? DaliTarget{DaliTargetKind::kGroup, static_cast(index_)} + : DaliTarget{DaliTargetKind::kShortAddress, static_cast(index_)}; + dali_->off(target); + } +} + +// ---- Query Level ---- + +void KnxDaliChannel::loopQueryLevel() { + // Periodic status query — simplified for now +} + +// ---- Switch State ---- + +void KnxDaliChannel::setSwitchState(bool value, bool is_switch_command) { + if (current_is_locked_) return; + current_state_ = value; + DaliTarget target = is_group_ ? DaliTarget{DaliTargetKind::kGroup, static_cast(index_)} + : DaliTarget{DaliTargetKind::kShortAddress, static_cast(index_)}; + if (value) { + dali_->on(target); + } else { + dali_->off(target); + } + if (value) { + start_time_ = esp_timer_get_time() / 1000ULL; + } +} + +// ---- Configuration setters ---- + +void KnxDaliChannel::setOnValue(uint8_t value) { + on_day_ = value; + on_night_ = value / 2; +} + +void KnxDaliChannel::setGroups(uint16_t groups) { groups_ = groups; } +void KnxDaliChannel::setGroupState(uint8_t group, bool state) { + if (state) groups_ |= (1 << group); else groups_ &= ~(1 << group); +} +void KnxDaliChannel::setGroupState(uint8_t group, uint8_t) {} +void KnxDaliChannel::setMinMax(uint8_t min, uint8_t max) { min_ = min; max_ = max; } +void KnxDaliChannel::setMinArc(uint8_t min) { min_ = min; } +void KnxDaliChannel::setHcl(uint8_t curve, uint16_t temp, uint8_t) { + hcl_curve_ = curve; + hcl_current_temp_ = temp; +} + +// ---- Dimm State ---- + +void KnxDaliChannel::setDimmState(uint8_t value, bool, bool) { + current_step_ = value; +} + +// ---- Color ---- + +void KnxDaliChannel::sendColor() { + if (dali_ == nullptr) return; + dali_->setColourRGB(static_cast(index_), current_color_[0], + current_color_[1], current_color_[2]); +} + +// ---- KO Handlers ---- + +void KnxDaliChannel::koHandleSwitch(GroupObject& ko) { + bool on = static_cast(ko.value()); + setSwitchState(on); +} + +void KnxDaliChannel::koHandleDimmRel(GroupObject& ko) { + int step = static_cast(static_cast(ko.value())); + if (step > 0) { + dimm_direction_ = DimmDirection::kUp; + dimm_step_ = static_cast(step); + } else if (step < 0) { + dimm_direction_ = DimmDirection::kDown; + dimm_step_ = static_cast(-step); + } else { + dimm_direction_ = DimmDirection::kNone; + } + dimm_last_ = esp_timer_get_time() / 1000ULL; +} + +void KnxDaliChannel::koHandleDimmAbs(GroupObject& ko) { + uint8_t value = static_cast(static_cast(ko.value()) * 255.0f / 100.0f); + setDimmState(value); + dimm_direction_ = DimmDirection::kNone; + DaliTarget target = is_group_ ? DaliTarget{DaliTargetKind::kGroup, static_cast(index_)} + : DaliTarget{DaliTargetKind::kShortAddress, static_cast(index_)}; + dali_->setArc(target, value); +} + +void KnxDaliChannel::koHandleLock(GroupObject& ko) { + bool lock = static_cast(ko.value()); + current_is_locked_ = lock; +} + +void KnxDaliChannel::koHandleColor(GroupObject& ko) { + KNXValue val = ko.value(); + if (true) { + // RGB packed in float or raw bytes + // Simplified: store and send + sendColor(); + } +} + +} // namespace knx_dali_gw +} // namespace gateway diff --git a/components/knx_dali_gw/src/knx_dali_channel.h b/components/knx_dali_gw/src/knx_dali_channel.h new file mode 100644 index 0000000..6e8bc6f --- /dev/null +++ b/components/knx_dali_gw/src/knx_dali_channel.h @@ -0,0 +1,91 @@ +#pragma once + +// ============================================================================= +// KnxDaliChannel — Per-address / per-group DALI channel (ported from DaliChannel) +// ============================================================================= + +#include "dali_gateway_bridge.h" + +#include "knx/group_object.h" + +#include + +namespace gateway { +namespace knx_dali_gw { + +class KnxDaliChannel { + public: + KnxDaliChannel(); + ~KnxDaliChannel(); + + void init(uint8_t channel_index, bool is_group, DaliGatewayBridge& bridge); + void setup(); + void loop(); + + void processInputKo(GroupObject& ko); + + // --- Configuration --- + void setOnValue(uint8_t value); + void setGroups(uint16_t groups); + void setGroupState(uint8_t group, bool state); + void setGroupState(uint8_t group, uint8_t value); + void setMinMax(uint8_t min, uint8_t max); + void setMinArc(uint8_t min); + void setHcl(uint8_t curve, uint16_t temp, uint8_t bri); + uint8_t getMin() const { return min_; } + uint8_t getMax() const { return max_; } + uint16_t getGroups() const { return groups_; } + + bool isNight{false}; + + private: + enum class DimmDirection { kDown, kUp, kNone }; + + void loopDimming(); + void loopStaircase(); + void loopQueryLevel(); + void sendColor(); + void setSwitchState(bool value, bool is_switch_command = true); + void setDimmState(uint8_t value, bool is_dimm_command = true, bool is_last = false); + void koHandleSwitch(GroupObject& ko); + void koHandleDimmRel(GroupObject& ko); + void koHandleDimmAbs(GroupObject& ko); + void koHandleLock(GroupObject& ko); + void koHandleColor(GroupObject& ko); + + DaliGatewayBridge* dali_{nullptr}; + uint8_t index_{0}; + bool is_group_{false}; + + // Dimming + DimmDirection dimm_direction_{DimmDirection::kNone}; + uint8_t dimm_step_{0}; + uint64_t dimm_last_{0}; + uint8_t dimm_interval_{100}; + + // Staircase + uint64_t start_time_{0}; + uint32_t interval_{0}; + + // Limits + uint8_t min_{0}; + uint8_t max_{254}; + uint8_t on_day_{100}; + uint8_t on_night_{10}; + + // State + bool current_state_{false}; + uint8_t current_step_{0}; + bool current_is_locked_{false}; + uint8_t current_color_[4]{}; + + // HCL + uint8_t hcl_curve_{255}; + uint16_t hcl_current_temp_{0}; + + // Groups + uint16_t groups_{0}; +}; + +} // namespace knx_dali_gw +} // namespace gateway diff --git a/components/knx_dali_gw/src/knx_dali_gw.cpp b/components/knx_dali_gw/src/knx_dali_gw.cpp new file mode 100644 index 0000000..943e0d3 --- /dev/null +++ b/components/knx_dali_gw/src/knx_dali_gw.cpp @@ -0,0 +1,106 @@ +#include "knx_dali_gw.hpp" + +#include "knx/bau07B0.h" + +#include "esp_log.h" + +namespace gateway { +namespace knx_dali_gw { + +namespace { + +constexpr const char* kTag = "knx_dali_gw"; + +} // namespace + +// ============================================================================= +// KnxDaliGateway::Impl +// ============================================================================= + +struct KnxDaliGateway::Impl { + KnxDaliGatewayConfig config; + gateway::openknx::EspIdfPlatform platform; + Bau07B0 device; + bool initialized{false}; + + explicit Impl(const KnxDaliGatewayConfig& cfg) + : config(cfg), + platform(nullptr, cfg.nvs_namespace.c_str()), + device(platform) {} + + bool init() { + if (initialized) return true; + + device.deviceObject().manufacturerId(0x00a4); + device.deviceObject().bauNumber(platform.uniqueSerialNumber()); + const uint8_t order_number[10] = {'R', 'E', 'G', '1', '-', 'D', 'a', 'l', 'i', 0}; + device.deviceObject().orderNumber(order_number); + const uint8_t program_version[5] = {0x00, 0xa4, 0x00, 0x01, 0x05}; + device.parameters().property(PID_PROG_VERSION)->write(program_version); + + device.readMemory(); + + if (!device.configured()) { + ESP_LOGW(kTag, "KNX device is not configured (blank ETS memory). " + "Individual address: 0x%04X (fallback).", + config.fallback_individual_address); + } + + device.enabled(true); + initialized = true; + ESP_LOGI(kTag, "KNX-DALI gateway initialized"); + return true; + } + + void loop() { + if (!initialized) return; + device.loop(); + } + + void setNetworkInterface(esp_netif_t* netif) { + platform.networkInterface(netif); + } +}; + +// ============================================================================= +// Public API +// ============================================================================= + +KnxDaliGateway::KnxDaliGateway(const KnxDaliGatewayConfig& config) + : impl_(std::make_unique(config)) {} + +KnxDaliGateway::~KnxDaliGateway() = default; + +bool KnxDaliGateway::init() { return impl_->init(); } + +void KnxDaliGateway::loop() { impl_->loop(); } + +Bau07B0& KnxDaliGateway::knxDevice() { return impl_->device; } + +const Bau07B0& KnxDaliGateway::knxDevice() const { return impl_->device; } + +void KnxDaliGateway::setNetworkInterface(esp_netif_t* netif) { + impl_->setNetworkInterface(netif); +} + +bool KnxDaliGateway::handleTunnelFrame(const uint8_t* data, size_t len) { + // TODO: Implement cEMI tunnel frame handling. + (void)data; (void)len; + return false; +} + +bool KnxDaliGateway::handleBusFrame(const uint8_t* data, size_t len) { + // TODO: Implement bus frame handling. + (void)data; (void)len; + return false; +} + +bool KnxDaliGateway::emitGroupValue(uint16_t group_object_number, + const uint8_t* data, size_t len) { + (void)group_object_number; (void)data; (void)len; + // TODO(Phase 3): Implement with proper KNXValue conversion. + return false; +} + +} // namespace knx_dali_gw +} // namespace gateway diff --git a/components/knx_dali_gw/src/knx_dali_module.cpp b/components/knx_dali_gw/src/knx_dali_module.cpp new file mode 100644 index 0000000..0c6788b --- /dev/null +++ b/components/knx_dali_gw/src/knx_dali_module.cpp @@ -0,0 +1,679 @@ +#include "knx_dali_module.h" + +#include "knx/bau07B0.h" +#include "knx/group_object.h" +#include "dali_define.hpp" + +#include "esp_log.h" +#include "esp_timer.h" + +#include +#include +#include + +namespace gateway { +namespace knx_dali_gw { + +namespace { +constexpr const char* kLogTag = "knx_dali_module"; +constexpr uint8_t kDaliBroadcastAddr = 0xFE; +} // namespace + +// ============================================================================= +// Constructor / Destructor +// ============================================================================= + +KnxDaliModule::KnxDaliModule() { + std::memset(addresses_, 0, sizeof(addresses_)); + std::memset(ballasts_, 0, sizeof(ballasts_)); +} + +KnxDaliModule::~KnxDaliModule() = default; + +// ============================================================================= +// Setup +// ============================================================================= + +void KnxDaliModule::setup(Bau07B0& device, DaliGatewayBridge& bridge) { + device_ = &device; + dali_ = &bridge; + + for (int i = 0; i < 64; i++) { + channels_[i].init(i, false, bridge); + } + for (int i = 0; i < 16; i++) { + groups_[i].init(i, true, bridge); + } + for (int i = 0; i < 3; i++) { + curves_[i].setup(static_cast(i)); + } +} + +// ============================================================================= +// Main Loop +// ============================================================================= + +void KnxDaliModule::loop(bool configured) { + if (!configured || device_ == nullptr) return; + + loopMessages(); + loopAddressing(); + loopAssigning(); + loopBusState(); + loopInitData(); + loopGroupState(); + + for (auto& ch : channels_) ch.loop(); + for (auto& grp : groups_) grp.loop(); + for (auto& curve : curves_) curve.loop(); +} + +// ============================================================================= +// Message Queue Execution +// ============================================================================= + +void KnxDaliModule::loopMessages() { + if (dali_ == nullptr) return; + + Message msg; + while (queue_.pop(msg)) { + DaliTarget target; + switch (msg.addrtype) { + case 0: target = {DaliTargetKind::kShortAddress, static_cast(msg.para1)}; break; + case 1: target = {DaliTargetKind::kGroup, static_cast(msg.para1)}; break; + default: target = {DaliTargetKind::kBroadcast, 0}; break; + } + + switch (msg.type) { + case MessageType::Arc: + dali_->setArc(target, msg.data); + break; + case MessageType::Cmd: + dali_->sendRaw(target, msg.data); + break; + case MessageType::SpecialCmd: + dali_->sendRaw(target, msg.data); + break; + case MessageType::Query: + if (auto resp = dali_->queryRaw(target, msg.data)) { + queue_.setResponse(msg.id, *resp); + } else { + queue_.setResponse(msg.id, -1); + } + break; + } + } +} + +// ============================================================================= +// Addressing State Machine +// ============================================================================= + +void KnxDaliModule::loopAddressing() { + if (adr_state_ == AddressingState::kOff || dali_ == nullptr) return; + + switch (adr_state_) { + case AddressingState::kOff: + break; + + case AddressingState::kInit: + adr_found_ = 0; + adr_iterations_ = 0; + ESP_LOGI(kLogTag, "Addressing: init (only_new=%d, randomize=%d, delete_all=%d)", + adr_only_new_, adr_randomize_, adr_delete_all_); + dali_->sendRaw({DaliTargetKind::kBroadcast, 0}, DALI_CMD_SPECIAL_INITIALIZE); + adr_state_ = AddressingState::kInit2; + break; + + case AddressingState::kInit2: + dali_->sendRaw({DaliTargetKind::kBroadcast, 0}, DALI_CMD_SPECIAL_INITIALIZE); + if (adr_delete_all_) { + adr_state_ = AddressingState::kWriteDtr; + } else if (adr_randomize_) { + adr_state_ = AddressingState::kRandom; + } else { + adr_state_ = AddressingState::kStartSearch; + } + break; + + case AddressingState::kWriteDtr: + dali_->setDtr(255); + adr_state_ = AddressingState::kRemoveShort; + break; + + case AddressingState::kRemoveShort: { + dali_->sendRaw({DaliTargetKind::kBroadcast, 0}, DALI_CMD_STORE_DTR_AS_SHORT_ADDRESS); + adr_state_ = AddressingState::kRemoveShort2; + break; + } + + case AddressingState::kRemoveShort2: + dali_->sendRaw({DaliTargetKind::kBroadcast, 0}, DALI_CMD_STORE_DTR_AS_SHORT_ADDRESS); + if (adr_randomize_) { + adr_state_ = AddressingState::kRandom; + } else { + adr_state_ = AddressingState::kStartSearch; + } + break; + + case AddressingState::kRandom: + dali_->randomise(); + adr_state_ = AddressingState::kRandom2; + break; + + case AddressingState::kRandom2: + dali_->randomise(); + adr_state_ = AddressingState::kRandomWait; + break; + + case AddressingState::kRandomWait: + vTaskDelay(pdMS_TO_TICKS(100)); + adr_state_ = AddressingState::kStartSearch; + break; + + case AddressingState::kStartSearch: + adr_search_ = 0xFFFFFF; + adr_state_ = AddressingState::kSearchHigh; + break; + + case AddressingState::kSearchHigh: + dali_->searchAddrH(static_cast((adr_search_ >> 16) & 0xFF)); + adr_state_ = AddressingState::kSearchMid; + break; + + case AddressingState::kSearchMid: + dali_->searchAddrM(static_cast((adr_search_ >> 8) & 0xFF)); + adr_state_ = AddressingState::kSearchLow; + break; + + case AddressingState::kSearchLow: + dali_->searchAddrL(static_cast(adr_search_ & 0xFF)); + adr_state_ = AddressingState::kCompare; + break; + + case AddressingState::kCompare: { + dali_->compare(); + adr_state_ = AddressingState::kCheckFound; + break; + } + + case AddressingState::kCheckFound: { + // Binary search: query short address to check if a device responded. + auto resp = dali_->queryShort({DaliTargetKind::kBroadcast, 0}); + bool found = resp.has_value() && *resp != 0xFF; + + if (adr_iterations_ < 24) { + int64_t delta = static_cast(1) << (23 - adr_iterations_); + if (found) { + adr_search_ += delta; + } else { + adr_search_ -= delta; + } + adr_iterations_++; + adr_state_ = AddressingState::kSearchHigh; + } else { + if (found) { + adr_state_ = AddressingState::kGetShort; + } else { + // Check one address higher + adr_search_++; + if (adr_search_ > 0xFFFFFF) { + adr_state_ = AddressingState::kTerminate; + } else { + adr_iterations_ = 0; + adr_state_ = AddressingState::kSearchHigh; + } + } + } + break; + } + + case AddressingState::kGetShort: { + auto short_addr = dali_->queryShort({DaliTargetKind::kBroadcast, 0}); + if (short_addr.has_value()) { + ballasts_[adr_found_].address = *short_addr; + ballasts_[adr_found_].high = static_cast((adr_search_ >> 16) & 0xFF); + ballasts_[adr_found_].middle = static_cast((adr_search_ >> 8) & 0xFF); + ballasts_[adr_found_].low = static_cast(adr_search_ & 0xFF); + + if (*short_addr == 0xFF) { + // Unaddressed — assign a free short address + int free_addr = -1; + for (int i = 0; i < 64; i++) { + if (!addresses_[i]) { free_addr = i; break; } + } + if (free_addr >= 0) { + adr_new_ = static_cast(free_addr); + adr_state_ = AddressingState::kProgramShort; + } else { + adr_state_ = AddressingState::kWithdraw; + } + } else { + addresses_[*short_addr] = true; + adr_found_++; + adr_state_ = AddressingState::kWithdraw; + } + } + break; + } + + case AddressingState::kProgramShort: + dali_->programShort({DaliTargetKind::kBroadcast, 0}, adr_new_); + adr_state_ = AddressingState::kVerifyShort; + break; + + case AddressingState::kVerifyShort: + dali_->verifyShort({DaliTargetKind::kBroadcast, 0}); + adr_state_ = AddressingState::kVerifyShortResponse; + break; + + case AddressingState::kVerifyShortResponse: { + auto verify = dali_->queryShort({DaliTargetKind::kBroadcast, 0}); + if (verify.has_value() && *verify == adr_new_) { + addresses_[adr_new_] = true; + adr_found_++; + ESP_LOGI(kLogTag, "Addressed device %d", adr_new_); + } + adr_state_ = AddressingState::kWithdraw; + break; + } + + case AddressingState::kWithdraw: + dali_->withdraw(); + adr_state_ = AddressingState::kStartSearch; + break; + + case AddressingState::kTerminate: + dali_->terminate(); + ESP_LOGI(kLogTag, "Addressing complete: %d devices found", adr_found_); + adr_state_ = AddressingState::kOff; + break; + + default: + break; + } +} + +// ============================================================================= +// Assigning State Machine +// ============================================================================= + +void KnxDaliModule::loopAssigning() { + if (ass_state_ == AssigningState::kOff || dali_ == nullptr) return; + + switch (ass_state_) { + case AssigningState::kOff: + break; + + case AssigningState::kInit: + dali_->sendRaw({DaliTargetKind::kBroadcast, 0}, DALI_CMD_SPECIAL_INITIALIZE); + ass_state_ = AssigningState::kInit2; + break; + + case AssigningState::kInit2: + dali_->sendRaw({DaliTargetKind::kBroadcast, 0}, DALI_CMD_SPECIAL_INITIALIZE); + ass_state_ = AssigningState::kQuery; + break; + + case AssigningState::kQuery: { + auto level = dali_->queryActualLevel(adr_new_); + ass_state_ = AssigningState::kCheckQuery; + break; + } + + case AssigningState::kCheckQuery: + // If the target address already has a device, stop + ass_state_ = AssigningState::kStartSearch; + break; + + case AssigningState::kStartSearch: + adr_iterations_ = 0; + ass_state_ = AssigningState::kSearchHigh; + break; + + case AssigningState::kSearchHigh: + dali_->searchAddrH(static_cast((adr_search_ >> 16) & 0xFF)); + ass_state_ = AssigningState::kSearchMid; + break; + + case AssigningState::kSearchMid: + dali_->searchAddrM(static_cast((adr_search_ >> 8) & 0xFF)); + ass_state_ = AssigningState::kSearchLow; + break; + + case AssigningState::kSearchLow: + dali_->searchAddrL(static_cast(adr_search_ & 0xFF)); + ass_state_ = AssigningState::kCompare; + break; + + case AssigningState::kCompare: + dali_->compare(); + ass_state_ = AssigningState::kCheckFound; + break; + + case AssigningState::kCheckFound: + if (!adr_assign_) { + adr_assign_ = true; + ass_state_ = AssigningState::kWithdraw; + } else { + auto resp = dali_->queryShort({DaliTargetKind::kBroadcast, 0}); + if (resp.has_value() && *resp != 0xFF) { + ass_state_ = AssigningState::kProgramShort; + } else { + ass_state_ = AssigningState::kTerminate; + } + } + break; + + case AssigningState::kWithdraw: + dali_->withdraw(); + adr_assign_ = true; + ass_state_ = AssigningState::kSearchHigh; + break; + + case AssigningState::kProgramShort: + dali_->programShort({DaliTargetKind::kBroadcast, 0}, adr_new_); + ass_state_ = AssigningState::kVerifyShort; + break; + + case AssigningState::kVerifyShort: + dali_->verifyShort({DaliTargetKind::kBroadcast, 0}); + ass_state_ = AssigningState::kVerifyShortResponse; + break; + + case AssigningState::kVerifyShortResponse: { + auto verify = dali_->queryShort({DaliTargetKind::kBroadcast, 0}); + if (verify.has_value() && *verify == adr_new_) { + addresses_[adr_new_] = true; + ESP_LOGI(kLogTag, "Assigned short address %d", adr_new_); + } + ass_state_ = AssigningState::kTerminate; + break; + } + + case AssigningState::kTerminate: + dali_->terminate(); + ass_state_ = AssigningState::kOff; + break; + } +} + +// ============================================================================= +// Bus State / Init Data +// ============================================================================= + +void KnxDaliModule::loopBusState() { + if (dali_ == nullptr) return; + dali_bus_state_ = true; // Simplified: assume bus is always OK +} + +void KnxDaliModule::loopInitData() { + if (got_init_data_ || device_ == nullptr || !device_->configured()) return; + + // Read initial device data from all channels + for (int i = 0; i < 64; i++) { + if (!addresses_[i]) continue; + channels_[i].setup(); + } + got_init_data_ = true; +} + +void KnxDaliModule::loopGroupState() { + if (last_changed_group_ == 255) return; + + uint8_t group_idx = last_changed_group_ & 0x0F; + bool is_group = (last_changed_group_ & 0x80) != 0; + + if (is_group && group_idx < 16) { + groups_[group_idx].setGroupState(group_idx, last_changed_value_); + } else if (!is_group && group_idx < 64) { + channels_[group_idx].setGroupState(group_idx, last_changed_value_); + } + + last_changed_group_ = 255; +} + +// ============================================================================= +// processInputKo — KNX group write dispatch +// ============================================================================= + +void KnxDaliModule::processInputKo(GroupObject& ko) { + if (device_ == nullptr) return; + if (adr_state_ != AddressingState::kOff) return; + if (current_lock_state_) return; + + uint16_t asap = ko.asap(); + ESP_LOGD(kLogTag, "processInputKo asap=%d", asap); + + // Channel KOs (64 channels x N group objects each) + int adr_relative = static_cast(asap) - ADR_KoOffset; + if (adr_relative >= 0 && adr_relative < ADR_KoBlockSize * 64) { + int ch = adr_relative / ADR_KoBlockSize; + if (ch < 64) { + channels_[ch].processInputKo(ko); + return; + } + } + + // Group KOs (16 groups x N group objects each) + int grp_relative = static_cast(asap) - GRP_KoOffset; + if (grp_relative >= 0 && grp_relative < GRP_KoBlockSize * 16) { + int grp_idx = grp_relative / GRP_KoBlockSize; + int slot = grp_relative % GRP_KoBlockSize; + if (grp_idx < 16) { + groups_[grp_idx].processInputKo(ko); + // Track group state changes + if (slot == 0) { // switch state + last_changed_group_ = 0x80 | static_cast(grp_idx); + } + return; + } + } + + // HCL KOs + int hcl_relative = static_cast(asap) - HCL_KoOffset; + if (hcl_relative >= 0 && hcl_relative < HCL_KoBlockSize * 3) { + int curve_idx = hcl_relative / HCL_KoBlockSize; + if (curve_idx < 3) { + // HCL: apply Kelvin to all channels and groups + KNXValue val = ko.value(); + if (true) { + uint16_t kelvin = static_cast(static_cast(val)); + for (int i = 0; i < 64; i++) { + channels_[i].setHcl(static_cast(curve_idx), kelvin, 255); + } + for (int i = 0; i < 16; i++) { + groups_[i].setHcl(static_cast(curve_idx), kelvin, 255); + } + } + return; + } + } +} + +// ============================================================================= +// Function Property Handlers (stubs — full port in subsequent iteration) +// ============================================================================= + +bool KnxDaliModule::processFunctionProperty(uint8_t object_index, uint8_t property_id, + uint8_t length, uint8_t* data, + uint8_t* result_data, uint8_t& result_length) { + // Only handle object 160, property 1 (REG1 DALI function properties) + if (object_index != 160 || property_id != 1 || length == 0 || data == nullptr) { + return false; + } + + switch (data[0]) { + case 2: funcHandleType(data, result_data, result_length); return true; + case 3: funcHandleScan(data, result_data, result_length); return true; + case 4: funcHandleAssign(data, result_data, result_length); return true; + case 10: funcHandleEvgWrite(data, result_data, result_length); return true; + case 11: funcHandleEvgRead(data, result_data, result_length); return true; + case 12: funcHandleSetScene(data, result_data, result_length); return true; + case 13: funcHandleGetScene(data, result_data, result_length); return true; + case 14: funcHandleIdentify(data, result_data, result_length); return true; + default: return false; + } +} + +bool KnxDaliModule::processFunctionPropertyState(uint8_t object_index, uint8_t property_id, + uint8_t length, uint8_t* data, + uint8_t* result_data, uint8_t& result_length) { + if (object_index != 160 || property_id != 1 || length == 0 || data == nullptr) { + return false; + } + + switch (data[0]) { + case 3: + case 5: stateHandleScanAndAddress(data, result_data, result_length); return true; + case 4: stateHandleAssign(data, result_data, result_length); return true; + case 7: stateHandleFoundEVGs(data, result_data, result_length); return true; + default: return false; + } +} + +// ============================================================================= +// Function Property Implementations (simplified stubs) +// ============================================================================= + +void KnxDaliModule::funcHandleType(uint8_t*, uint8_t* result_data, uint8_t& result_length) { + // Query device type(s) for the selected short address + result_data[0] = 0; // working + result_length = 1; +} + +void KnxDaliModule::funcHandleScan(uint8_t* data, uint8_t* result_data, uint8_t& result_length) { + if (data == nullptr) return; + adr_only_new_ = (data[1] & 0x01) != 0; + adr_randomize_ = (data[1] & 0x02) != 0; + adr_delete_all_ = (data[1] & 0x04) != 0; + adr_state_ = AddressingState::kInit; + result_data[0] = 0; // working + result_length = 1; +} + +void KnxDaliModule::funcHandleAssign(uint8_t* data, uint8_t* result_data, uint8_t& result_length) { + if (data == nullptr) return; + adr_search_ = (static_cast(data[1]) << 16) | + (static_cast(data[2]) << 8) | data[3]; + adr_new_ = data[4]; + if (adr_new_ == 99) adr_new_ = 255; // "remove short address" + ass_state_ = AssigningState::kInit; + result_data[0] = 0; + result_length = 1; +} + +void KnxDaliModule::funcHandleEvgWrite(uint8_t*, uint8_t*, uint8_t&) {} +void KnxDaliModule::funcHandleEvgRead(uint8_t*, uint8_t*, uint8_t&) {} +void KnxDaliModule::funcHandleSetScene(uint8_t*, uint8_t*, uint8_t&) {} +void KnxDaliModule::funcHandleGetScene(uint8_t*, uint8_t*, uint8_t&) {} +void KnxDaliModule::funcHandleIdentify(uint8_t*, uint8_t*, uint8_t&) {} + +void KnxDaliModule::stateHandleType(uint8_t*, uint8_t*, uint8_t&) {} +void KnxDaliModule::stateHandleAssign(uint8_t*, uint8_t*, uint8_t&) {} +void KnxDaliModule::stateHandleScanAndAddress(uint8_t*, uint8_t*, uint8_t&) {} +void KnxDaliModule::stateHandleFoundEVGs(uint8_t*, uint8_t*, uint8_t&) {} + +// ============================================================================= +// Public state accessors +// ============================================================================= + +bool KnxDaliModule::isAddressingActive() const { + return adr_state_ != AddressingState::kOff || ass_state_ != AssigningState::kOff; +} + +bool KnxDaliModule::isLocked() const { return current_lock_state_; } +void KnxDaliModule::setLocked(bool locked) { current_lock_state_ = locked; } +bool KnxDaliModule::isNight() const { return is_night_; } +void KnxDaliModule::setNight(bool night) { is_night_ = night; } +uint8_t KnxDaliModule::lastChangedGroup() const { return last_changed_group_; } +uint8_t KnxDaliModule::lastChangedValue() const { return last_changed_value_; } + +KnxDaliChannel& KnxDaliModule::channel(int index) { return channels_[index]; } +KnxDaliChannel& KnxDaliModule::group(int index) { return groups_[index]; } +HclCurve& KnxDaliModule::curve(int index) { return curves_[index]; } + +// ============================================================================= +// DALI Send Helpers +// ============================================================================= + +uint8_t KnxDaliModule::sendMsg(MessageType type, uint8_t addr, uint8_t val, + uint8_t addr_type, bool wait) { + Message* msg = new Message(); + msg->type = type; + msg->data = val; + msg->addrtype = addr_type; + msg->para1 = addr; + return queue_.push(msg); +} + +uint8_t KnxDaliModule::sendCmd(uint8_t addr, uint8_t value, uint8_t addr_type, bool wait) { + return sendMsg(MessageType::Cmd, addr, value, addr_type, wait); +} + +uint8_t KnxDaliModule::sendSpecialCmd(uint8_t command, uint8_t value, bool wait) { + return sendMsg(MessageType::SpecialCmd, command, value, 2, wait); +} + +uint8_t KnxDaliModule::sendArc(uint8_t addr, uint8_t value, uint8_t addr_type) { + return sendMsg(MessageType::Arc, addr, value, addr_type, false); +} + +int16_t KnxDaliModule::getInfo(uint8_t address, int command, uint8_t additional) { + (void)additional; + uint8_t msg_id = sendMsg(MessageType::Query, address, static_cast(command), 0, true); + // Wait for response + for (int i = 0; i < 300; i++) { + vTaskDelay(pdMS_TO_TICKS(1)); + int16_t resp = queue_.getResponse(msg_id); + if (resp != -200) return resp; + } + return -1; +} + +// ============================================================================= +// KO Handlers +// ============================================================================= + +void KnxDaliModule::koHandleSwitch(GroupObject& ko) { + KNXValue val = ko.value(); + bool on = static_cast(val); + if (dali_ != nullptr) { + if (on) { + dali_->on({DaliTargetKind::kBroadcast, 0}); + } else { + dali_->off({DaliTargetKind::kBroadcast, 0}); + } + } +} + +void KnxDaliModule::koHandleDimm(GroupObject& ko) { + KNXValue val = ko.value(); + uint8_t percent = static_cast(static_cast(val) * 255.0f / 100.0f); + uint8_t arc = PercentToArc(static_cast(percent) * 100.0 / 255.0); + if (dali_ != nullptr) { + dali_->setArc({DaliTargetKind::kBroadcast, 0}, arc); + } +} + +void KnxDaliModule::koHandleDayNight(GroupObject& ko) { + is_night_ = static_cast(ko.value()); + for (int i = 0; i < 64; i++) channels_[i].isNight = is_night_; + for (int i = 0; i < 16; i++) groups_[i].isNight = is_night_; +} + +void KnxDaliModule::koHandleOnValue(GroupObject& ko) { + uint8_t on_value = static_cast(static_cast(ko.value()) * 255.0f / 100.0f); + for (int i = 0; i < 64; i++) channels_[i].setOnValue(on_value); + for (int i = 0; i < 16; i++) groups_[i].setOnValue(on_value); +} + +void KnxDaliModule::koHandleScene(GroupObject& ko) { + uint8_t scene = static_cast(ko.value()); + if (dali_ != nullptr) { + dali_->goToScene({DaliTargetKind::kBroadcast, 0}, scene); + } +} + +} // namespace knx_dali_gw +} // namespace gateway diff --git a/components/knx_dali_gw/src/knx_dali_module.h b/components/knx_dali_gw/src/knx_dali_module.h new file mode 100644 index 0000000..3e3fc49 --- /dev/null +++ b/components/knx_dali_gw/src/knx_dali_module.h @@ -0,0 +1,166 @@ +#pragma once + +// ============================================================================= +// KnxDaliModule — Core DALI gateway module (ported from DaliModule) +// ============================================================================= +// Handles: +// - DALI message queuing and execution +// - DALI commissioning (addressing + assigning state machines) +// - KNX group-object dispatch (processInputKo) +// - KNX function-property commands (ETS programming) +// - Broadcast switch/dim/scene handling + +#include "dali_gateway_bridge.h" +#include "knx_dali_channel.h" +#include "hcl_curve.h" +#include "message_queue.h" +#include "ballast.hpp" +#include "knxprod.h" + +#include "knx/group_object.h" + +#include +#include +#include +#include + +// Forward declarations +class Bau07B0; + +namespace gateway { +namespace knx_dali_gw { + +class KnxDaliModule { + public: + enum class AddressingState { + kOff, kInit, kInit2, kWriteDtr, kRemoveShort, kRemoveShort2, + kRandom, kRandom2, kRandomWait, kStartSearch, kSearchHigh, + kSearchMid, kSearchLow, kCompare, kGetShort, kCheckFound, + kProgramShort, kVerifyShort, kVerifyShortResponse, kWithdraw, + kTerminate, kSearchShort, kCheckSearchShort + }; + + enum class AssigningState { + kOff, kInit, kInit2, kQuery, kCheckQuery, kStartSearch, + kSearchHigh, kSearchMid, kSearchLow, kCompare, kCheckFound, + kWithdraw, kProgramShort, kVerifyShort, kVerifyShortResponse, + kTerminate + }; + + KnxDaliModule(); + ~KnxDaliModule(); + + // ---- Lifecycle ---- + void setup(Bau07B0& device, DaliGatewayBridge& bridge); + void loop(bool configured); + + // ---- KNX input ---- + void processInputKo(GroupObject& ko); + + // ---- Function properties (ETS programming) ---- + bool processFunctionProperty(uint8_t object_index, uint8_t property_id, + uint8_t length, uint8_t* data, + uint8_t* result_data, uint8_t& result_length); + bool processFunctionPropertyState(uint8_t object_index, uint8_t property_id, + uint8_t length, uint8_t* data, + uint8_t* result_data, uint8_t& result_length); + + // ---- Public state ---- + bool isAddressingActive() const; + bool isLocked() const; + void setLocked(bool locked); + bool isNight() const; + void setNight(bool night); + uint8_t lastChangedGroup() const; + uint8_t lastChangedValue() const; + + // ---- Channel / group access ---- + KnxDaliChannel& channel(int index); + KnxDaliChannel& group(int index); + HclCurve& curve(int index); + + private: + // ---- DALI helpers ---- + uint8_t sendMsg(MessageType type, uint8_t addr, uint8_t value, + uint8_t addr_type = 0, bool wait = false); + uint8_t sendCmd(uint8_t addr, uint8_t value, uint8_t addr_type = 0, + bool wait = false); + uint8_t sendSpecialCmd(uint8_t command, uint8_t value = 0, bool wait = false); + uint8_t sendArc(uint8_t addr, uint8_t value, uint8_t addr_type); + int16_t getInfo(uint8_t address, int command, uint8_t additional = 0); + + // ---- KNX KO handlers ---- + void koHandleSwitch(GroupObject& ko); + void koHandleDimm(GroupObject& ko); + void koHandleDayNight(GroupObject& ko); + void koHandleOnValue(GroupObject& ko); + void koHandleScene(GroupObject& ko); + + // ---- Function property handlers ---- + void funcHandleType(uint8_t* data, uint8_t* result_data, uint8_t& result_length); + void funcHandleScan(uint8_t* data, uint8_t* result_data, uint8_t& result_length); + void funcHandleAssign(uint8_t* data, uint8_t* result_data, uint8_t& result_length); + void funcHandleEvgWrite(uint8_t* data, uint8_t* result_data, uint8_t& result_length); + void funcHandleEvgRead(uint8_t* data, uint8_t* result_data, uint8_t& result_length); + void funcHandleSetScene(uint8_t* data, uint8_t* result_data, uint8_t& result_length); + void funcHandleGetScene(uint8_t* data, uint8_t* result_data, uint8_t& result_length); + void funcHandleIdentify(uint8_t* data, uint8_t* result_data, uint8_t& result_length); + + // ---- State handlers ---- + void stateHandleType(uint8_t* data, uint8_t* result_data, uint8_t& result_length); + void stateHandleAssign(uint8_t* data, uint8_t* result_data, uint8_t& result_length); + void stateHandleScanAndAddress(uint8_t* data, uint8_t* result_data, + uint8_t& result_length); + void stateHandleFoundEVGs(uint8_t* data, uint8_t* result_data, + uint8_t& result_length); + + // ---- Loops ---- + void loopMessages(); + void loopAddressing(); + void loopAssigning(); + void loopBusState(); + void loopInitData(); + void loopGroupState(); + + // ---- State ---- + Bau07B0* device_{nullptr}; + DaliGatewayBridge* dali_{nullptr}; + + // Addressing / commissioning + AddressingState adr_state_{AddressingState::kOff}; + AssigningState ass_state_{AssigningState::kOff}; + Ballast ballasts_[64]; + bool addresses_[64]{}; + int adr_found_{0}; + uint8_t adr_new_{0}; + uint8_t last_bus_state_{2}; + uint8_t adr_iterations_{0}; + uint64_t adr_search_{0}; + bool adr_assign_{false}; + bool adr_only_new_{false}; + bool adr_randomize_{false}; + bool adr_delete_all_{false}; + + // Group state + uint8_t last_changed_group_{255}; + uint8_t last_changed_value_{0}; + + // Bus + bool got_init_data_{false}; + bool dali_bus_state_{true}; + bool dali_bus_state_to_set_{true}; + uint64_t dali_state_last_{1}; + + // Lock / night + bool current_lock_state_{false}; + bool is_night_{false}; + + // Channels / groups / curves + KnxDaliChannel channels_[64]; + KnxDaliChannel groups_[16]; + HclCurve curves_[3]; + MessageQueue queue_; +}; + +} // namespace knx_dali_gw +} // namespace gateway diff --git a/components/knx_dali_gw/src/message.hpp b/components/knx_dali_gw/src/message.hpp new file mode 100644 index 0000000..15be8e9 --- /dev/null +++ b/components/knx_dali_gw/src/message.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include + +enum class MessageType { + Arc, + Cmd, + SpecialCmd, + Query +}; + +struct Message { + Message *next{nullptr}; + uint8_t data{0}; + MessageType type{MessageType::Arc}; + uint8_t para1{0}; + uint8_t addrtype{0}; + uint8_t para2{0}; + bool wait{false}; + uint8_t id{0}; +}; diff --git a/components/knx_dali_gw/src/message_queue.cpp b/components/knx_dali_gw/src/message_queue.cpp new file mode 100644 index 0000000..ec89225 --- /dev/null +++ b/components/knx_dali_gw/src/message_queue.cpp @@ -0,0 +1,74 @@ +#include "message_queue.h" + +#include "esp_timer.h" + +uint8_t MessageQueue::push(Message *msg) +{ + while(isLocked) ; + isLocked = true; + + msg->next = nullptr; + if(tail == nullptr) + { + head = msg; + tail = msg; + isLocked = false; + return msg->id; + } + + tail->next = msg; + tail = msg; + isLocked = false; + + return msg->id; +} + +bool MessageQueue::pop(Message &msg) +{ + unsigned long started = esp_timer_get_time() / 1000ULL; + while(isLocked && ((esp_timer_get_time() / 1000ULL) - started < 3000)) ; + + if(isLocked || head == nullptr) return false; + isLocked = true; + + msg.addrtype = head->addrtype; + msg.data = head->data; + msg.id = head->id; + msg.para1 = head->para1; + msg.para2 = head->para2; + msg.type = head->type; + msg.wait = head->wait; + + Message *temp = head; + + if(head->next == nullptr) + { + head = nullptr; + tail = nullptr; + } else { + head = head->next; + } + + delete temp; + + isLocked = false; + return true; +} + +uint8_t MessageQueue::getNextId() +{ + currentId++; + if(currentId == 0) currentId++; + responses[currentId] = -200; + return currentId; +} + +void MessageQueue::setResponse(uint8_t id, int16_t value) +{ + responses[id] = value; +} + +int16_t MessageQueue::getResponse(uint8_t id) +{ + return responses[id]; +} \ No newline at end of file diff --git a/components/knx_dali_gw/src/message_queue.h b/components/knx_dali_gw/src/message_queue.h new file mode 100644 index 0000000..fd8a4d4 --- /dev/null +++ b/components/knx_dali_gw/src/message_queue.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include "message.hpp" + +class MessageQueue +{ + public: + uint8_t push(Message *msg); + bool pop(Message &msg); + uint8_t getNextId(); + void setResponse(uint8_t id, int16_t value); + int16_t getResponse(uint8_t id); + + private: + Message *head; + Message *tail; + uint8_t currentId = 0; + int16_t responses[256]; + bool isLocked = false; +}; \ No newline at end of file diff --git a/components/openknx_idf/CMakeLists.txt b/components/openknx_idf/CMakeLists.txt deleted file mode 100644 index 02c02e0..0000000 --- a/components/openknx_idf/CMakeLists.txt +++ /dev/null @@ -1,78 +0,0 @@ -set(OPENKNX_ROOT "${CMAKE_CURRENT_LIST_DIR}/../../knx") -set(TPUART_ROOT "${CMAKE_CURRENT_LIST_DIR}/../../tpuart") - -if(NOT EXISTS "${OPENKNX_ROOT}/src/knx/platform.h") - message(FATAL_ERROR "OpenKNX submodule is missing at ${OPENKNX_ROOT}") -endif() - -if(NOT EXISTS "${TPUART_ROOT}/src/TPUart/DataLinkLayer.h") - message(FATAL_ERROR "TPUart submodule is missing at ${TPUART_ROOT}") -endif() - -file(GLOB OPENKNX_SRCS - "${OPENKNX_ROOT}/src/knx/*.cpp" -) - -if(CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED) - list(APPEND OPENKNX_SRCS - "${OPENKNX_ROOT}/src/knx/aes.c" - ) -endif() - -set(TPUART_SRCS - "${TPUART_ROOT}/src/TPUart/DataLinkLayer.cpp" - "${TPUART_ROOT}/src/TPUart/Receiver.cpp" - "${TPUART_ROOT}/src/TPUart/RepetitionFilter.cpp" - "${TPUART_ROOT}/src/TPUart/RingBuffer.cpp" - "${TPUART_ROOT}/src/TPUart/SearchBuffer.cpp" - "${TPUART_ROOT}/src/TPUart/Statistics.cpp" - "${TPUART_ROOT}/src/TPUart/SystemState.cpp" - "${TPUART_ROOT}/src/TPUart/Transmitter.cpp" - "${TPUART_ROOT}/src/TPUart.cpp" -) - -idf_component_register( - SRCS - "src/arduino_compat.cpp" - "src/esp_idf_platform.cpp" - "src/ets_device_runtime.cpp" - "src/ets_memory_loader.cpp" - "src/security_storage.cpp" - "src/tpuart_uart_interface.cpp" - ${OPENKNX_SRCS} - ${TPUART_SRCS} - INCLUDE_DIRS - "include" - "${OPENKNX_ROOT}/src" - "${TPUART_ROOT}/src" - REQUIRES - esp_driver_gpio - esp_driver_uart - esp_netif - esp_system - esp_timer - esp_wifi - freertos - log - lwip - mbedtls - nvs_flash -) - -target_compile_definitions(${COMPONENT_LIB} PUBLIC - MASK_VERSION=0x07B0 - KNX_FLASH_SIZE=4096 - KNX_NO_AUTOMATIC_GLOBAL_INSTANCE - KNX_NO_SPI - USE_CEMI_SERVER -) - -if(CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED) - target_compile_definitions(${COMPONENT_LIB} PUBLIC USE_DATASECURE) -endif() - -target_compile_options(${COMPONENT_LIB} PRIVATE - -Wno-unused-parameter -) - -set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17) \ No newline at end of file diff --git a/components/openknx_idf/include/Arduino.h b/components/openknx_idf/include/Arduino.h deleted file mode 100644 index 546da3d..0000000 --- a/components/openknx_idf/include/Arduino.h +++ /dev/null @@ -1,59 +0,0 @@ -#pragma once - -#include - -#ifndef DEC -#define DEC 10 -#endif - -#ifndef HEX -#define HEX 16 -#endif - -#ifndef INPUT -#define INPUT 0x0 -#endif - -#ifndef OUTPUT -#define OUTPUT 0x1 -#endif - -#ifndef INPUT_PULLUP -#define INPUT_PULLUP 0x2 -#endif - -#ifndef INPUT_PULLDOWN -#define INPUT_PULLDOWN 0x3 -#endif - -#ifndef LOW -#define LOW 0x0 -#endif - -#ifndef HIGH -#define HIGH 0x1 -#endif - -#ifndef CHANGE -#define CHANGE 2 -#endif - -#ifndef FALLING -#define FALLING 3 -#endif - -#ifndef RISING -#define RISING 4 -#endif - -using uint = unsigned int; - -uint32_t millis(); -uint32_t micros(); -void delay(uint32_t millis); -void delayMicroseconds(unsigned int howLong); -void pinMode(uint32_t pin, uint32_t mode); -void digitalWrite(uint32_t pin, uint32_t value); -uint32_t digitalRead(uint32_t pin); -typedef void (*voidFuncPtr)(void); -void attachInterrupt(uint32_t pin, voidFuncPtr callback, uint32_t mode); \ No newline at end of file diff --git a/components/openknx_idf/include/openknx_idf/esp_idf_platform.h b/components/openknx_idf/include/openknx_idf/esp_idf_platform.h deleted file mode 100644 index 28a668c..0000000 --- a/components/openknx_idf/include/openknx_idf/esp_idf_platform.h +++ /dev/null @@ -1,66 +0,0 @@ -#pragma once - -#include "knx/platform.h" - -#include "esp_netif.h" -#include "lwip/sockets.h" - -#include -#include -#include -#include - -namespace gateway::openknx { - -class EspIdfPlatform : public Platform { - public: - using OutboundCemiFrameCallback = bool (*)(CemiFrame& frame, void* context); - - explicit EspIdfPlatform(TPUart::Interface::Abstract* interface = nullptr, - const char* nvs_namespace = "openknx"); - ~EspIdfPlatform() override; - - void outboundCemiFrameCallback(OutboundCemiFrameCallback callback, void* context); - bool handleOutboundCemiFrame(CemiFrame& frame) override; - - void networkInterface(esp_netif_t* netif); - esp_netif_t* networkInterface() const; - - uint32_t currentIpAddress() override; - uint32_t currentSubnetMask() override; - uint32_t currentDefaultGateway() override; - void macAddress(uint8_t* data) override; - uint32_t uniqueSerialNumber() override; - - void restart() override; - void fatalError() override; - - void setupMultiCast(uint32_t addr, uint16_t port) override; - void closeMultiCast() override; - bool sendBytesMultiCast(uint8_t* buffer, uint16_t len) override; - int readBytesMultiCast(uint8_t* buffer, uint16_t maxLen) override; - int readBytesMultiCast(uint8_t* buffer, uint16_t maxLen, uint32_t& src_addr, - uint16_t& src_port) override; - bool sendBytesUniCast(uint32_t addr, uint16_t port, uint8_t* buffer, - uint16_t len) override; - - uint8_t* getEepromBuffer(uint32_t size) override; - void commitToEeprom() override; - - private: - esp_netif_t* effectiveNetif() const; - void loadEeprom(size_t size); - - esp_netif_t* netif_{nullptr}; - int udp_sock_{-1}; - sockaddr_in multicast_remote_{}; - sockaddr_in last_remote_{}; - bool has_last_remote_{false}; - std::vector eeprom_; - std::string nvs_namespace_; - bool eeprom_loaded_{false}; - OutboundCemiFrameCallback outbound_cemi_frame_callback_{nullptr}; - void* outbound_cemi_frame_context_{nullptr}; -}; - -} // namespace gateway::openknx \ No newline at end of file diff --git a/components/openknx_idf/include/openknx_idf/openknx_idf.h b/components/openknx_idf/include/openknx_idf/openknx_idf.h deleted file mode 100644 index e1ed4c4..0000000 --- a/components/openknx_idf/include/openknx_idf/openknx_idf.h +++ /dev/null @@ -1,16 +0,0 @@ -#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/security_storage.h" -#include "openknx_idf/tpuart_uart_interface.h" - -#include "knx/bau07B0.h" -#include "knx_facade.h" - -namespace gateway::openknx { - -using DaliGatewayDevice = KnxFacade; - -} // namespace gateway::openknx \ No newline at end of file diff --git a/components/openknx_idf/include/openknx_idf/tpuart_uart_interface.h b/components/openknx_idf/include/openknx_idf/tpuart_uart_interface.h deleted file mode 100644 index f8ad9a3..0000000 --- a/components/openknx_idf/include/openknx_idf/tpuart_uart_interface.h +++ /dev/null @@ -1,43 +0,0 @@ -#pragma once - -#include "TPUart/Interface/Abstract.h" - -#include "driver/uart.h" - -#include -#include -#include -#include - -namespace gateway::openknx { - -class TpuartUartInterface : public TPUart::Interface::Abstract { - public: - TpuartUartInterface(uart_port_t uart_port, int tx_pin, int rx_pin, - size_t rx_buffer_size = 512, size_t tx_buffer_size = 512, - bool nine_bit_mode = true); - ~TpuartUartInterface(); - - void begin(int baud) override; - void end() override; - bool available() override; - bool availableForWrite() override; - bool write(char value) override; - int read() override; - bool overflow() override; - void flush() override; - bool hasCallback() override; - void registerCallback(std::function callback) override; - - private: - uart_port_t uart_port_; - int tx_pin_; - int rx_pin_; - size_t rx_buffer_size_; - size_t tx_buffer_size_; - bool nine_bit_mode_; - std::atomic_bool overflow_{false}; - std::function callback_; -}; - -} // namespace gateway::openknx \ No newline at end of file diff --git a/components/openknx_idf/src/arduino_compat.cpp b/components/openknx_idf/src/arduino_compat.cpp deleted file mode 100644 index dd9c98b..0000000 --- a/components/openknx_idf/src/arduino_compat.cpp +++ /dev/null @@ -1,180 +0,0 @@ -#include "Arduino.h" - -#include "driver/gpio.h" -#include "esp_err.h" -#include "esp_rom_sys.h" -#include "esp_timer.h" -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" - -#include -#include - -namespace { - -std::array g_gpio_callbacks{}; -bool g_isr_service_installed = false; - -void IRAM_ATTR gpioIsrThunk(void* arg) { - const auto pin = static_cast(reinterpret_cast(arg)); - if (pin < g_gpio_callbacks.size() && g_gpio_callbacks[pin] != nullptr) { - g_gpio_callbacks[pin](); - } -} - -gpio_int_type_t toGpioInterrupt(uint32_t mode) { - switch (mode) { - case RISING: - return GPIO_INTR_POSEDGE; - case FALLING: - return GPIO_INTR_NEGEDGE; - case CHANGE: - return GPIO_INTR_ANYEDGE; - default: - return GPIO_INTR_DISABLE; - } -} - -void printUnsigned(unsigned long long value, int base) { - if (base == HEX) { - std::printf("%llX", value); - } else { - std::printf("%llu", value); - } -} - -void printSigned(long long value, int base) { - if (base == HEX) { - std::printf("%llX", static_cast(value)); - } else { - std::printf("%lld", value); - } -} - -} // namespace - -uint32_t millis() { return static_cast(esp_timer_get_time() / 1000ULL); } - -uint32_t micros() { return static_cast(esp_timer_get_time()); } - -void delay(uint32_t millis) { vTaskDelay(pdMS_TO_TICKS(millis)); } - -void delayMicroseconds(unsigned int howLong) { esp_rom_delay_us(howLong); } - -void pinMode(uint32_t pin, uint32_t mode) { - if (pin >= GPIO_NUM_MAX) { - return; - } - gpio_config_t config{}; - config.pin_bit_mask = 1ULL << pin; - config.mode = mode == OUTPUT ? GPIO_MODE_OUTPUT : GPIO_MODE_INPUT; - config.pull_up_en = mode == INPUT_PULLUP ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE; - config.pull_down_en = mode == INPUT_PULLDOWN ? GPIO_PULLDOWN_ENABLE : GPIO_PULLDOWN_DISABLE; - config.intr_type = GPIO_INTR_DISABLE; - gpio_config(&config); -} - -void digitalWrite(uint32_t pin, uint32_t value) { - if (pin < GPIO_NUM_MAX) { - gpio_set_level(static_cast(pin), value == LOW ? 0 : 1); - } -} - -uint32_t digitalRead(uint32_t pin) { - if (pin >= GPIO_NUM_MAX) { - return LOW; - } - return gpio_get_level(static_cast(pin)) == 0 ? LOW : HIGH; -} - -void attachInterrupt(uint32_t pin, voidFuncPtr callback, uint32_t mode) { - if (pin >= GPIO_NUM_MAX) { - return; - } - if (!g_isr_service_installed) { - const esp_err_t err = gpio_install_isr_service(ESP_INTR_FLAG_IRAM); - g_isr_service_installed = err == ESP_OK || err == ESP_ERR_INVALID_STATE; - } - if (!g_isr_service_installed) { - return; - } - gpio_set_intr_type(static_cast(pin), toGpioInterrupt(mode)); - gpio_isr_handler_remove(static_cast(pin)); - g_gpio_callbacks[pin] = callback; - if (callback != nullptr) { - gpio_isr_handler_add(static_cast(pin), gpioIsrThunk, - reinterpret_cast(static_cast(pin))); - } -} - -void print(const char value[]) { std::printf("%s", value == nullptr ? "" : value); } - -void print(char value) { std::printf("%c", value); } - -void print(unsigned char value, int base) { printUnsigned(value, base); } - -void print(int value, int base) { printSigned(value, base); } - -void print(unsigned int value, int base) { printUnsigned(value, base); } - -void print(long value, int base) { printSigned(value, base); } - -void print(unsigned long value, int base) { printUnsigned(value, base); } - -void print(long long value, int base) { printSigned(value, base); } - -void print(unsigned long long value, int base) { printUnsigned(value, base); } - -void print(double value) { std::printf("%f", value); } - -void println(const char value[]) { - print(value); - std::printf("\n"); -} - -void println(char value) { - print(value); - std::printf("\n"); -} - -void println(unsigned char value, int base) { - print(value, base); - std::printf("\n"); -} - -void println(int value, int base) { - print(value, base); - std::printf("\n"); -} - -void println(unsigned int value, int base) { - print(value, base); - std::printf("\n"); -} - -void println(long value, int base) { - print(value, base); - std::printf("\n"); -} - -void println(unsigned long value, int base) { - print(value, base); - std::printf("\n"); -} - -void println(long long value, int base) { - print(value, base); - std::printf("\n"); -} - -void println(unsigned long long value, int base) { - print(value, base); - std::printf("\n"); -} - -void println(double value) { - print(value); - std::printf("\n"); -} - -void println(void) { std::printf("\n"); } \ No newline at end of file diff --git a/components/openknx_idf/src/esp_idf_platform.cpp b/components/openknx_idf/src/esp_idf_platform.cpp deleted file mode 100644 index 91d776a..0000000 --- a/components/openknx_idf/src/esp_idf_platform.cpp +++ /dev/null @@ -1,316 +0,0 @@ -#include "openknx_idf/esp_idf_platform.h" - -#include "esp_log.h" -#include "esp_mac.h" -#include "esp_system.h" -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" -#include "lwip/inet.h" -#include "nvs.h" -#include "nvs_flash.h" - -#include -#include -#include -#include - -namespace gateway::openknx { -namespace { - -constexpr const char* kTag = "openknx_idf"; -constexpr const char* kEepromKey = "eeprom"; - -bool readBaseMac(uint8_t* data) { - if (data == nullptr) { - return false; - } - if (esp_efuse_mac_get_default(data) == ESP_OK) { - return true; - } - return esp_read_mac(data, ESP_MAC_WIFI_STA) == ESP_OK; -} - -esp_netif_t* findDefaultNetif() { - constexpr const char* kPreferredIfKeys[] = {"ETH_DEF", "WIFI_STA_DEF", "WIFI_AP_DEF"}; - for (const char* key : kPreferredIfKeys) { - auto* netif = esp_netif_get_handle_from_ifkey(key); - if (netif == nullptr || !esp_netif_is_netif_up(netif)) { - continue; - } - esp_netif_ip_info_t ip_info{}; - if (esp_netif_get_ip_info(netif, &ip_info) == ESP_OK && ip_info.ip.addr != 0) { - return netif; - } - } - for (const char* key : kPreferredIfKeys) { - if (auto* netif = esp_netif_get_handle_from_ifkey(key)) { - return netif; - } - } - return nullptr; -} - -bool ensureNvsReady() { - const esp_err_t err = nvs_flash_init(); - if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) { - if (nvs_flash_erase() != ESP_OK) { - return false; - } - return nvs_flash_init() == ESP_OK; - } - return err == ESP_OK || err == ESP_ERR_INVALID_STATE; -} - -} // namespace - -EspIdfPlatform::EspIdfPlatform(TPUart::Interface::Abstract* interface, - const char* nvs_namespace) - : nvs_namespace_(nvs_namespace == nullptr ? "openknx" : nvs_namespace) { - this->interface(interface); -} - -EspIdfPlatform::~EspIdfPlatform() { closeMultiCast(); } - -void EspIdfPlatform::outboundCemiFrameCallback(OutboundCemiFrameCallback callback, - void* context) { - outbound_cemi_frame_callback_ = callback; - outbound_cemi_frame_context_ = context; -} - -bool EspIdfPlatform::handleOutboundCemiFrame(CemiFrame& frame) { - if (outbound_cemi_frame_callback_ == nullptr) { - return false; - } - return outbound_cemi_frame_callback_(frame, outbound_cemi_frame_context_); -} - -void EspIdfPlatform::networkInterface(esp_netif_t* netif) { netif_ = netif; } - -esp_netif_t* EspIdfPlatform::networkInterface() const { return netif_; } - -esp_netif_t* EspIdfPlatform::effectiveNetif() const { - return netif_ == nullptr ? findDefaultNetif() : netif_; -} - -uint32_t EspIdfPlatform::currentIpAddress() { - esp_netif_ip_info_t ip_info{}; - esp_netif_t* netif = effectiveNetif(); - if (netif == nullptr || esp_netif_get_ip_info(netif, &ip_info) != ESP_OK) { - return 0; - } - return ip_info.ip.addr; -} - -uint32_t EspIdfPlatform::currentSubnetMask() { - esp_netif_ip_info_t ip_info{}; - esp_netif_t* netif = effectiveNetif(); - if (netif == nullptr || esp_netif_get_ip_info(netif, &ip_info) != ESP_OK) { - return 0; - } - return ip_info.netmask.addr; -} - -uint32_t EspIdfPlatform::currentDefaultGateway() { - esp_netif_ip_info_t ip_info{}; - esp_netif_t* netif = effectiveNetif(); - if (netif == nullptr || esp_netif_get_ip_info(netif, &ip_info) != ESP_OK) { - return 0; - } - return ip_info.gw.addr; -} - -void EspIdfPlatform::macAddress(uint8_t* data) { - if (data == nullptr) { - return; - } - if (!readBaseMac(data)) { - std::memset(data, 0, 6); - } -} - -uint32_t EspIdfPlatform::uniqueSerialNumber() { - uint8_t mac[6]{}; - macAddress(mac); - return (static_cast(mac[2]) << 24) | (static_cast(mac[3]) << 16) | - (static_cast(mac[4]) << 8) | mac[5]; -} - -void EspIdfPlatform::restart() { esp_restart(); } - -void EspIdfPlatform::fatalError() { - ESP_LOGE(kTag, "OpenKNX fatal error"); - while (true) { - vTaskDelay(pdMS_TO_TICKS(1000)); - } -} - -void EspIdfPlatform::setupMultiCast(uint32_t addr, uint16_t port) { - closeMultiCast(); - udp_sock_ = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); - if (udp_sock_ < 0) { - ESP_LOGE(kTag, "failed to create UDP socket: errno=%d", errno); - return; - } - - int reuse = 1; - setsockopt(udp_sock_, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); - - sockaddr_in bind_addr{}; - bind_addr.sin_family = AF_INET; - const uint32_t local_address = currentIpAddress(); - bind_addr.sin_addr.s_addr = local_address == 0 ? htonl(INADDR_ANY) : local_address; - bind_addr.sin_port = htons(port); - if (bind(udp_sock_, reinterpret_cast(&bind_addr), sizeof(bind_addr)) < 0) { - ESP_LOGE(kTag, "failed to bind UDP socket: errno=%d", errno); - closeMultiCast(); - return; - } - - timeval timeout{}; - timeout.tv_sec = 0; - timeout.tv_usec = 1000; - setsockopt(udp_sock_, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); - - ip_mreq mreq{}; - mreq.imr_multiaddr.s_addr = htonl(addr); - mreq.imr_interface.s_addr = local_address == 0 ? htonl(INADDR_ANY) : local_address; - if (setsockopt(udp_sock_, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) { - ESP_LOGW(kTag, "failed to join KNX multicast group: errno=%d", errno); - } - - if (local_address != 0) { - in_addr multicast_interface{}; - multicast_interface.s_addr = local_address; - if (setsockopt(udp_sock_, IPPROTO_IP, IP_MULTICAST_IF, &multicast_interface, - sizeof(multicast_interface)) < 0) { - ESP_LOGW(kTag, "failed to select KNX multicast interface: errno=%d", errno); - } - } - - uint8_t loop = 0; - setsockopt(udp_sock_, IPPROTO_IP, IP_MULTICAST_LOOP, &loop, sizeof(loop)); - - multicast_remote_ = {}; - multicast_remote_.sin_family = AF_INET; - multicast_remote_.sin_addr.s_addr = htonl(addr); - multicast_remote_.sin_port = htons(port); -} - -void EspIdfPlatform::closeMultiCast() { - if (udp_sock_ >= 0) { - shutdown(udp_sock_, SHUT_RDWR); - close(udp_sock_); - udp_sock_ = -1; - } - has_last_remote_ = false; -} - -bool EspIdfPlatform::sendBytesMultiCast(uint8_t* buffer, uint16_t len) { - if (udp_sock_ < 0 || buffer == nullptr || len == 0) { - return false; - } - const int sent = sendto(udp_sock_, buffer, len, 0, reinterpret_cast(&multicast_remote_), - sizeof(multicast_remote_)); - return sent == len; -} - -int EspIdfPlatform::readBytesMultiCast(uint8_t* buffer, uint16_t maxLen) { - uint32_t src_addr = 0; - uint16_t src_port = 0; - return readBytesMultiCast(buffer, maxLen, src_addr, src_port); -} - -int EspIdfPlatform::readBytesMultiCast(uint8_t* buffer, uint16_t maxLen, uint32_t& src_addr, - uint16_t& src_port) { - if (udp_sock_ < 0 || buffer == nullptr || maxLen == 0) { - return 0; - } - sockaddr_in remote{}; - socklen_t remote_len = sizeof(remote); - const int len = recvfrom(udp_sock_, buffer, maxLen, 0, reinterpret_cast(&remote), - &remote_len); - if (len <= 0) { - return 0; - } - last_remote_ = remote; - has_last_remote_ = true; - src_addr = ntohl(remote.sin_addr.s_addr); - src_port = ntohs(remote.sin_port); - return len; -} - -bool EspIdfPlatform::sendBytesUniCast(uint32_t addr, uint16_t port, uint8_t* buffer, - uint16_t len) { - if (udp_sock_ < 0 || buffer == nullptr || len == 0) { - return false; - } - sockaddr_in remote{}; - if (addr == 0 && port == 0 && has_last_remote_) { - remote = last_remote_; - } else { - remote.sin_family = AF_INET; - remote.sin_addr.s_addr = htonl(addr); - remote.sin_port = htons(port); - } - const int sent = sendto(udp_sock_, buffer, len, 0, reinterpret_cast(&remote), - sizeof(remote)); - return sent == len; -} - -void EspIdfPlatform::loadEeprom(size_t size) { - if (eeprom_loaded_ && eeprom_.size() == size) { - return; - } - eeprom_.assign(size, 0xff); - eeprom_loaded_ = true; - - if (!ensureNvsReady()) { - ESP_LOGW(kTag, "NVS is not ready for OpenKNX EEPROM load"); - return; - } - - nvs_handle_t handle = 0; - if (nvs_open(nvs_namespace_.c_str(), NVS_READONLY, &handle) != ESP_OK) { - return; - } - size_t stored_size = 0; - if (nvs_get_blob(handle, kEepromKey, nullptr, &stored_size) == ESP_OK && stored_size > 0) { - std::vector stored(stored_size); - if (nvs_get_blob(handle, kEepromKey, stored.data(), &stored_size) == ESP_OK) { - std::memcpy(eeprom_.data(), stored.data(), std::min(eeprom_.size(), stored.size())); - } - } - nvs_close(handle); -} - -uint8_t* EspIdfPlatform::getEepromBuffer(uint32_t size) { - loadEeprom(size); - return eeprom_.data(); -} - -void EspIdfPlatform::commitToEeprom() { - if (eeprom_.empty()) { - return; - } - if (!ensureNvsReady()) { - ESP_LOGW(kTag, "NVS is not ready for OpenKNX EEPROM commit"); - return; - } - - nvs_handle_t handle = 0; - esp_err_t err = nvs_open(nvs_namespace_.c_str(), NVS_READWRITE, &handle); - if (err != ESP_OK) { - ESP_LOGW(kTag, "failed to open OpenKNX NVS namespace: %s", esp_err_to_name(err)); - return; - } - err = nvs_set_blob(handle, kEepromKey, eeprom_.data(), eeprom_.size()); - if (err == ESP_OK) { - err = nvs_commit(handle); - } - if (err != ESP_OK) { - ESP_LOGW(kTag, "failed to commit OpenKNX EEPROM: %s", esp_err_to_name(err)); - } - nvs_close(handle); -} - -} // namespace gateway::openknx \ No newline at end of file diff --git a/components/openknx_idf/src/tpuart_uart_interface.cpp b/components/openknx_idf/src/tpuart_uart_interface.cpp deleted file mode 100644 index 300400b..0000000 --- a/components/openknx_idf/src/tpuart_uart_interface.cpp +++ /dev/null @@ -1,147 +0,0 @@ -#include "openknx_idf/tpuart_uart_interface.h" - -#include "esp_log.h" -#include "soc/uart_periph.h" - -#include - -namespace gateway::openknx { -namespace { - -constexpr const char* kTag = "openknx_tpuart"; - -bool ResolveUartIoPin(uart_port_t uart_port, int configured_pin, uint32_t pin_index, - int* resolved_pin) { - if (resolved_pin == nullptr) { - return false; - } - if (configured_pin >= 0) { - *resolved_pin = configured_pin; - return true; - } - if (uart_port < 0 || uart_port >= SOC_UART_NUM || pin_index >= SOC_UART_PINS_COUNT) { - *resolved_pin = UART_PIN_NO_CHANGE; - return false; - } - const int default_pin = uart_periph_signal[uart_port].pins[pin_index].default_gpio; - if (default_pin < 0) { - *resolved_pin = UART_PIN_NO_CHANGE; - return false; - } - *resolved_pin = default_pin; - return true; -} - -} // namespace - -TpuartUartInterface::TpuartUartInterface(uart_port_t uart_port, int tx_pin, int rx_pin, - size_t rx_buffer_size, size_t tx_buffer_size, - bool nine_bit_mode) - : uart_port_(uart_port), - tx_pin_(tx_pin), - rx_pin_(rx_pin), - rx_buffer_size_(rx_buffer_size), - tx_buffer_size_(tx_buffer_size), - nine_bit_mode_(nine_bit_mode) {} - -TpuartUartInterface::~TpuartUartInterface() { end(); } - -void TpuartUartInterface::begin(int baud) { - if (_running) { - end(); - } - - uart_config_t config{}; - config.baud_rate = baud; - config.data_bits = UART_DATA_8_BITS; - config.parity = nine_bit_mode_ ? UART_PARITY_EVEN : UART_PARITY_DISABLE; - config.stop_bits = UART_STOP_BITS_1; - config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; - config.source_clk = UART_SCLK_DEFAULT; - - int tx_pin = UART_PIN_NO_CHANGE; - int rx_pin = UART_PIN_NO_CHANGE; - if (!ResolveUartIoPin(uart_port_, tx_pin_, SOC_UART_TX_PIN_IDX, &tx_pin) || - !ResolveUartIoPin(uart_port_, rx_pin_, SOC_UART_RX_PIN_IDX, &rx_pin)) { - ESP_LOGE(kTag, "UART%d has no ESP-IDF default TX/RX pin; configure explicit pins", - uart_port_); - return; - } - - esp_err_t err = uart_param_config(uart_port_, &config); - if (err != ESP_OK) { - ESP_LOGE(kTag, "failed to configure UART%d: %s", uart_port_, esp_err_to_name(err)); - return; - } - - err = uart_set_pin(uart_port_, tx_pin, rx_pin, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); - if (err != ESP_OK) { - ESP_LOGE(kTag, "failed to route UART%d pins tx=%d rx=%d: %s", uart_port_, tx_pin, - rx_pin, esp_err_to_name(err)); - return; - } - - err = uart_driver_install(uart_port_, rx_buffer_size_, tx_buffer_size_, 0, nullptr, 0); - if (err != ESP_OK) { - ESP_LOGE(kTag, "failed to install UART%d driver: %s", uart_port_, esp_err_to_name(err)); - return; - } - - uart_set_rx_full_threshold(uart_port_, 1); - _running = true; -} - -void TpuartUartInterface::end() { - if (!_running) { - return; - } - _running = false; - uart_driver_delete(uart_port_); -} - -bool TpuartUartInterface::available() { - if (!_running) { - return false; - } - size_t len = 0; - return uart_get_buffered_data_len(uart_port_, &len) == ESP_OK && len > 0; -} - -bool TpuartUartInterface::availableForWrite() { - if (!_running) { - return false; - } - size_t len = 0; - return uart_get_tx_buffer_free_size(uart_port_, &len) == ESP_OK && len > 0; -} - -bool TpuartUartInterface::write(char value) { - if (!_running) { - return false; - } - return uart_write_bytes(uart_port_, &value, 1) == 1; -} - -int TpuartUartInterface::read() { - if (!_running) { - return -1; - } - uint8_t value = 0; - return uart_read_bytes(uart_port_, &value, 1, 0) == 1 ? value : -1; -} - -bool TpuartUartInterface::overflow() { return overflow_.exchange(false); } - -void TpuartUartInterface::flush() { - if (_running) { - uart_flush(uart_port_); - } -} - -bool TpuartUartInterface::hasCallback() { return false; } - -void TpuartUartInterface::registerCallback(std::function callback) { - callback_ = std::move(callback); -} - -} // namespace gateway::openknx \ No newline at end of file diff --git a/knx b/knx index 346b704..dcf565d 160000 --- a/knx +++ b/knx @@ -1 +1 @@ -Subproject commit 346b704cbec32ff1dca3fbba7ec507c07b846eea +Subproject commit dcf565dc03c5f0910e1e827ef1c0418b00fc06c6